diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..4bd29f751f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea/ +.DS_Store +.vscode +.temp +.cache +*.iml +__pycache__ diff --git a/README.md b/README.md index 48ac73378a..993d7c6df8 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,23 @@ -

- -

-

- - - - - - -

- - -目录: -================= - -* [算法面试思维导图](#算法面试思维导图) -* [B站算法视频讲解](#B站算法视频讲解) -* [LeetCode 刷题攻略](#LeetCode-刷题攻略) -* [算法模板](#算法模板) -* [关于作者](#关于作者) - -# 算法面试思维导图 -![算法面试知识大纲](./pics/算法大纲.png) +👉 推荐 [在线阅读](http://programmercarl.com/) (Github在国内访问经常不稳定) +👉 推荐 [Gitee同步](https://gitee.com/programmercarl/leetcode-master) -# B站算法视频讲解 +> 1. **介绍** :本项目是一套完整的刷题计划,旨在帮助大家少走弯路,循序渐进学算法,[关注作者](#关于作者) +> 2. **正式出版** :[《代码随想录》](https://programmercarl.com/qita/publish.html) 。 +> 3. **PDF版本** :[「代码随想录」算法精讲 PDF 版本](https://programmercarl.com/qita/algo_pdf.html) 。 +> 4. **算法公开课** :[《代码随想录》算法视频公开课](https://www.bilibili.com/video/BV1fA4y1o715) 。 +> 5. **最强八股文** :[代码随想录知识星球精华PDF](https://www.programmercarl.com/other/kstar_baguwen.html) 。 +> 6. **刷题顺序** :README已经将刷题顺序排好了,按照顺序一道一道刷就可以。 +> 7. **学习社区** :一起学习打卡/面试技巧/如何选择offer/大厂内推/职场规则/简历修改/技术分享/程序人生。欢迎加入[「代码随想录」知识星球](https://programmercarl.com/other/kstar.html) 。 +> 8. **提交代码** :本项目统一使用C++语言进行讲解,但已经有Java、Python、Go、JavaScript等等多语言版本,感谢[这里的每一位贡献者](https://github.com/youngyangyang04/leetcode-master/graphs/contributors),如果你也想贡献代码点亮你的头像,[点击这里](https://www.programmercarl.com/qita/join.html)了解提交代码的方式。 +> 9. **转载须知** :以下所有文章皆为我([程序员Carl](https://github.com/youngyangyang04))的原创。引用本项目文章请注明出处,发现恶意抄袭或搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! -以下为[B站「代码随想录」](https://space.bilibili.com/525438321)算法讲解视频: -* [帮你把KMP算法学个通透!(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd) -* [帮你把KMP算法学个通透!(代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) -* [带你学透回溯算法(理论篇)](https://www.bilibili.com/video/BV1cy4y167mM) -* [回溯算法之组合问题(力扣题目:77.组合)](https://www.bilibili.com/video/BV1ti4y1L7cv) -* [组合问题的剪枝操作(对应力扣题目:77.组合)](https://www.bilibili.com/video/BV1wi4y157er) -* [组合总和(对应力扣题目:39.组合总和)](https://www.bilibili.com/video/BV1KT4y1M7HJ/) - -(持续更新中....) +

+ + + +

# LeetCode 刷题攻略 @@ -43,366 +25,483 @@ 很多刚开始刷题的同学都有一个困惑:面对leetcode上近两千道题目,从何刷起。 +大家平时刷题感觉效率低,浪费的时间主要在三点: + +* 找题 +* 找到了不应该现阶段做的题 +* 没有全套的优质题解可以参考 + 其实我之前在知乎上回答过这个问题,回答内容大概是按照如下类型来刷数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论->高级数据结构,再从简单刷起,做了几个类型题目之后,再慢慢做中等题目、困难题目。 但我能设身处地的感受到:即使有这样一个整体规划,对于一位初学者甚至算法老手寻找合适自己的题目也是很困难,时间成本很高,而且题目还不一定就是经典题目。 -对于刷题,我们都是想用最短的时间把经典题目都做一篇,这样效率才是最高的! +对于刷题,我们都是想用最短的时间**按照循序渐进的难度顺序把经典题目都做一遍**,这样效率才是最高的! -所以我整理了leetcode刷题攻略:一个超级详细的刷题顺序,**每道题目都是我精心筛选,都是经典题目高频面试题**,大家只要按照这个顺序刷就可以了,**你没看错,就是题目顺序都排好了,文章顺序就是刷题顺序!挨个刷就可以,不用自己再去题海里选题了!** +所以我整理了leetcode刷题攻略:一个超级详细的刷题顺序,**每道题目都是我精心筛选,都是经典题目高频面试题**,大家只要按照这个顺序刷就可以了,**你没看错,README已经把题目顺序都排好了,文章顺序就是刷题顺序!挨个刷就可以,不用自己再去题海里选题了!** 而且每道题目我都写了的详细题解(图文并茂,难点配有视频),力扣上我的题解都是排在对应题目的首页,质量是有目共睹的。 -**那么今天我把这个刷题顺序整理出来,是为了帮助更多的学习算法的同学少走弯路!** +**那么现在我把刷题顺序都整理出来,是为了帮助更多的学习算法的同学少走弯路!** 如果你在刷leetcode,强烈建议先按照本攻略刷题顺序来刷,刷完了你会发现对整个知识体系有一个质的飞跃,不用在题海茫然的寻找方向。 -**文章会首发在公众号[「代码随想录」](https://img-blog.csdnimg.cn/20201124161234338.png),赶紧去看看吧,你一定会发现相见恨晚!** +
最新文章会首发在公众号「代码随想录」,扫码看看吧,你会发现相见恨晚!
-## 如何使用该刷题攻略 - -电脑端还看不到留言,大家可以在公众号[「代码随想录」](https://img-blog.csdnimg.cn/20201124161234338.png),后台回复:力扣刷题指南,便可以获得手机版刷题指南,看完就会发现有很多录友(代码随想录的朋友们)在文章下留言打卡,这份刷题顺序和题解已经陪伴了上万录友了,同时也说明文章的质量是经过上万人的考验! +
-欢迎每一位学习算法的小伙伴加入到这个学习阵营来! +## 如何使用该刷题攻略 -**目前已经更新了,数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心,八个专题了,即将开始动态规划!** +按照先面的排列顺序,从数组开始刷起就可以了,顺序都安排好了,按顺序刷就好。 -在刷题指南中,每个专题开始都有理论基础篇,并不像是教科书般的理论介绍,而是从实战中归纳需要的基础知识。每个专题结束都有总结篇,最这个专题的归纳总结。 +在刷题攻略中,每个专题开始都有理论基础篇,并不像是教科书般的理论介绍,而是从实战中归纳需要的基础知识。每个专题结束都有总结篇,最这个专题的归纳总结。 如果你是算法老手,这篇攻略也是复习的最佳资料,如果把每个系列对应的总结篇,快速过一遍,整个算法知识体系以及各种解法就重现脑海了。 -在按照如下顺序刷题的过程中,每一道题解一定要看对应文章下面的留言(留言目前只能在手机端查看)。 +**这里每一篇题解,都是精品,值得仔细琢磨**。 -如果你有疑问或者发现文章哪里有不对的地方,都可以在留言区都能找到答案,还有很多录友的总结非常赞,看完之后也很有收获。 +我在题目讲解中统一使用C++,但你会发现下面几乎每篇题解都配有其他语言版本,Java、Python、Go、JavaScript等等,正是这些[热心小伙们](https://github.com/youngyangyang04/leetcode-master/graphs/contributors)贡献的代码,当然我也会严格把控代码质量。 -目前「代码随想录」刷题指南更新了:**160篇文章,精讲了100+道经典算法题目,共50w字的详细图解,部分难点题目还搭配了20分钟左右的视频讲解**。 +**所以也欢迎大家参与进来,完善题解的各个语言版本,拥抱开源,让更多小伙伴们受益**。 准备好了么,刷题攻略开始咯,go go go! -## 资源下载 - -**本资源由代码随想录原创出品:** +--------------------------------------------- -* [二叉树学习手册PDF开放下载!!](https://mp.weixin.qq.com/s/uSyeCq0UF6kwRKcWkLnXuw) -* [回溯算法学习手册PDF开放下载!!](https://mp.weixin.qq.com/s/aSToAWOPnPV4GiDf9n0p-w) -* [贪心算法学习手册PDF开放下载!!](https://mp.weixin.qq.com/s/dUUWPVB7aVMpStPOG8cKnQ) -* [背包问题学习手册PDF开放下载!](https://mp.weixin.qq.com/s/X220c9ouxSW-gBrKC8ZAEw) +## 前序 -(将陆续整理各个专题的PDF下载版本) +* [做项目(多个C++、Java、Go、前端、测开项目)](https://programmercarl.com/other/kstar.html) -## 前序 * 编程语言 * [C++面试&C++学习指南知识点整理](https://github.com/youngyangyang04/TechCPP) + * [编程语言基础课](https://kamacoder.com/courseshop.php) + * [23种设计模式](https://github.com/youngyangyang04/kama-DesignPattern) + * [大厂算法笔试题](https://kamacoder.com/company.php) -* 编程素养 - * [看了这么多代码,谈一谈代码风格!](https://mp.weixin.qq.com/s/UR9ztxz3AyL3qdHn_zMbqw) - * [力扣上的代码想在本地编译运行?](https://mp.weixin.qq.com/s/r1696t8lvcw7Rz4gb_jacw) * 工具 * [一站式vim配置](https://github.com/youngyangyang04/PowerVim) * [保姆级Git入门教程,万字详解](https://mp.weixin.qq.com/s/Q_O0ey4C9tryPZaZeJocbA) - * [程序员应该用什么用具来写文档?](https://mp.weixin.qq.com/s/s_hig9nioq8nT-2F7AL0SQ) + * [程序员应该用什么用具来写文档?](./problems/前序/程序员写文档工具.md) * 求职 - * [程序员的简历应该这么写!!(附简历模板)](https://mp.weixin.qq.com/s/nCTUzuRTBo1_R_xagVszsA) - * [BAT级别技术面试流程和注意事项都在这里了](https://mp.weixin.qq.com/s/815qCyFGVIxwut9I_7PNFw) - * [北京有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/BKrjK4myNB-FYbMqW9f3yw) - * [上海有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/iW4_rXQzc0fJDuSmPTUVdQ) - * [深圳有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q) - * [广州有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Ir_hQP0clbnvHrWzDL-qXg) - * [成都有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Y9Qg22WEsBngs8B-K8acqQ) - * [杭州有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/33FmPJYrOU-ygovoxIaEUw) + * [ACM模式练习网站,卡码网](https://kamacoder.com/) + * [程序员的简历应该这么写!!(附简历模板)](./problems/前序/程序员简历.md) + * [【专业技能】应该这样写!](https://programmercarl.com/other/jianlizhuanye.html) + * [【项目经历】应该这样写!](https://programmercarl.com/other/jianlixiangmu.html) + * [BAT级别技术面试流程和注意事项都在这里了](./problems/前序/BAT级别技术面试流程和注意事项都在这里了.md) * 算法性能分析 - * [关于时间复杂度,你不知道的都在这里!](https://mp.weixin.qq.com/s/LWBfehW1gMuEnXtQjJo-sw) - * [O(n)的算法居然超时了,此时的n究竟是多大?](https://mp.weixin.qq.com/s/73ryNsuPFvBQkt6BbhNzLA) - * [通过一道面试题目,讲一讲递归算法的时间复杂度!](https://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ) - * [本周小结!(算法性能分析系列一)](https://mp.weixin.qq.com/s/5m8xDbGUeGgYJsESeg5ITQ) - * [关于空间复杂度,可能有几个疑问?](https://mp.weixin.qq.com/s/IPv4pTD6UxKkFBkgeDCZyg) + * [关于时间复杂度,你不知道的都在这里!](./problems/前序/时间复杂度.md) + * [O(n)的算法居然超时了,此时的n究竟是多大?](./problems/前序/算法超时.md) + * [通过一道面试题目,讲一讲递归算法的时间复杂度!](./problems/前序/递归算法的时间复杂度.md) + * [关于空间复杂度,可能有几个疑问?](./problems/前序/空间复杂度.md) + * [递归算法的时间与空间复杂度分析!](./problems/前序/递归算法的时间与空间复杂度分析.md) + * [刷了这么多题,你了解自己代码的内存消耗么?](./problems/前序/内存消耗.md) -(持续更新中.....) ## 数组 -1. [数组过于简单,但你该了解这些!](https://mp.weixin.qq.com/s/c2KABb-Qgg66HrGf8z-8Og) -2. [数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) -3. [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) -4. [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) -5. [数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg) -6. [数组:总结篇](https://mp.weixin.qq.com/s/LIfQFRJBH5ENTZpvixHEmg) +1. [数组过于简单,但你该了解这些!](./problems/数组理论基础.md) +2. [数组:704.二分查找](./problems/0704.二分查找.md) +3. [数组:27.移除元素](./problems/0027.移除元素.md) +4. [数组:977.有序数组的平方](./problems/0977.有序数组的平方.md) +5. [数组:209.长度最小的子数组](./problems/0209.长度最小的子数组.md) +6. [数组:区间和](./problems/kamacoder/0058.区间和.md) +7. [数组:开发商购买土地](./problems/kamacoder/0044.开发商购买土地.md) +8. [数组:59.螺旋矩阵II](./problems/0059.螺旋矩阵II.md) +9. [数组:总结篇](./problems/数组总结篇.md) ## 链表 -1. [关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ) -2. [链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) -3. [链表:一道题目考察了常见的五个操作!](https://mp.weixin.qq.com/s/Cf95Lc6brKL4g2j8YyF3Mg) -4. [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) -5. [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) -6. [链表:总结篇!](https://mp.weixin.qq.com/s/vK0JjSTHfpAbs8evz5hH8A) +1. [关于链表,你该了解这些!](./problems/链表理论基础.md) +2. [链表:203.移除链表元素](./problems/0203.移除链表元素.md) +3. [链表:707.设计链表](./problems/0707.设计链表.md) +4. [链表:206.翻转链表](./problems/0206.翻转链表.md) +5. [链表:24.两两交换链表中的节点](./problems/0024.两两交换链表中的节点.md) +6. [链表:19.删除链表的倒数第 N 个结点](./problems/0019.删除链表的倒数第N个节点.md) +7. [链表:链表相交](./problems/面试题02.07.链表相交.md) +8. [链表:142.环形链表](./problems/0142.环形链表II.md) +9. [链表:总结篇!](./problems/链表总结篇.md) ## 哈希表 -1. [关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA) -2. [哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig) -3. [哈希表:哈希值太大了,还是得用set](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA) -4. [哈希表:用set来判断快乐数](https://mp.weixin.qq.com/s/G4Q2Zfpfe706gLK7HpZHpA) -5. [哈希表:map等候多时了](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ) -6. [哈希表:其实需要哈希的地方都能找到map的身影](https://mp.weixin.qq.com/s/Ue8pKKU5hw_m-jPgwlHcbA) -7. [哈希表:这道题目我做过?](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) -8. [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) -9. [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) -10. [哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg) +1. [关于哈希表,你该了解这些!](./problems/哈希表理论基础.md) +2. [哈希表:242.有效的字母异位词](./problems/0242.有效的字母异位词.md) +3. [哈希表:1002.查找常用字符](./problems/1002.查找常用字符.md) +4. [哈希表:349.两个数组的交集](./problems/0349.两个数组的交集.md) +5. [哈希表:202.快乐数](./problems/0202.快乐数.md) +6. [哈希表:1.两数之和](./problems/0001.两数之和.md) +7. [哈希表:454.四数相加II](./problems/0454.四数相加II.md) +8. [哈希表:383.赎金信](./problems/0383.赎金信.md) +9. [哈希表:15.三数之和](./problems/0015.三数之和.md) +10. [双指针法:18.四数之和](./problems/0018.四数之和.md) +11. [哈希表:总结篇!](./problems/哈希表总结.md) ## 字符串 -1. [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) -2. [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw) -3. [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) -4. [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) -5. [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ) -6. [帮你把KMP算法学个通透!(理论篇)B站视频](https://www.bilibili.com/video/BV1PD4y1o7nd) -7. [帮你把KMP算法学个通透!(代码篇)B站视频](https://www.bilibili.com/video/BV1M5411j7Xx) -8. [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) -9. [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ) -10. [字符串:前缀表不右移,难道就写不出KMP了?](https://mp.weixin.qq.com/s/p3hXynQM2RRROK5c6X7xfw) -11. [字符串:总结篇!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg) +1. [字符串:344.反转字符串](./problems/0344.反转字符串.md) +2. [字符串:541.反转字符串II](./problems/0541.反转字符串II.md) +3. [字符串:替换数字](./problems/kamacoder/0054.替换数字.md) +4. [字符串:151.翻转字符串里的单词](./problems/0151.翻转字符串里的单词.md) +5. [字符串:右旋字符串](./problems/kamacoder/0055.右旋字符串.md) +6. [帮你把KMP算法学个通透](./problems/0028.实现strStr.md) +8. [字符串:459.重复的子字符串](./problems/0459.重复的子字符串.md) +9. [字符串:总结篇!](./problems/字符串总结.md) ## 双指针法 双指针法基本都是应用在数组,字符串与链表的题目上 -1. [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) -2. [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) -3. [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) -4. [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) -5. [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) -6. [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) -7. [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) -8. [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) -9. [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) +1. [数组:27.移除元素](./problems/0027.移除元素.md) +2. [字符串:344.反转字符串](./problems/0344.反转字符串.md) +3. [字符串:替换数字](./problems/kamacoder/0054.替换数字.md) +4. [字符串:151.翻转字符串里的单词](./problems/0151.翻转字符串里的单词.md) +5. [链表:206.翻转链表](./problems/0206.翻转链表.md) +6. [链表:19.删除链表的倒数第 N 个结点](./problems/0019.删除链表的倒数第N个节点.md) +7. [链表:链表相交](./problems/面试题02.07.链表相交.md) +8. [链表:142.环形链表](./problems/0142.环形链表II.md) +9. [双指针:15.三数之和](./problems/0015.三数之和.md) +10. [双指针:18.四数之和](./problems/0018.四数之和.md) +11. [双指针:总结篇!](./problems/双指针总结.md) ## 栈与队列 -1. [栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ) -2. [栈与队列:我用栈来实现队列怎么样?](https://mp.weixin.qq.com/s/P6tupDwRFi6Ay-L7DT4NVg) -3. [栈与队列:用队列实现栈还有点别扭](https://mp.weixin.qq.com/s/yzn6ktUlL-vRG3-m5a8_Yw) -4. [栈与队列:系统中处处都是栈的应用](https://mp.weixin.qq.com/s/nLlmPMsDCIWSqAtr0jbrpQ) -5. [栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg) -6. [栈与队列:有没有想过计算机是如何处理表达式的?](https://mp.weixin.qq.com/s/hneh2nnLT91rR8ms2fm_kw) -7. [栈与队列:滑动窗口里求最大值引出一个重要数据结构](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA) -8. [栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng) -9. [栈与队列:总结篇!](https://mp.weixin.qq.com/s/xBcHyvHlWq4P13fzxEtkPg) +1. [栈与队列:理论基础](./problems/栈与队列理论基础.md) +2. [栈与队列:232.用栈实现队列](./problems/0232.用栈实现队列.md) +3. [栈与队列:225.用队列实现栈](./problems/0225.用队列实现栈.md) +4. [栈与队列:20.有效的括号](./problems/0020.有效的括号.md) +5. [栈与队列:1047.删除字符串中的所有相邻重复项](./problems/1047.删除字符串中的所有相邻重复项.md) +6. [栈与队列:150.逆波兰表达式求值](./problems/0150.逆波兰表达式求值.md) +7. [栈与队列:239.滑动窗口最大值](./problems/0239.滑动窗口最大值.md) +8. [栈与队列:347.前K个高频元素](./problems/0347.前K个高频元素.md) +9. [栈与队列:总结篇!](./problems/栈与队列总结.md) ## 二叉树 + 题目分类大纲如下: -二叉树大纲 - -1. [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A) -2. [二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) -3. [二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) -4. [二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) -5. [二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog) -6. [二叉树:你真的会翻转二叉树么?](https://mp.weixin.qq.com/s/6gY1MiXrnm-khAAJiIb5Bg) -7. [本周小结!(二叉树)](https://mp.weixin.qq.com/s/JWmTeC7aKbBfGx4TY6uwuQ) -8. [二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg) -9. [二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg) -10. [二叉树:看看这些树的最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA) -11. [二叉树:我有多少个节点?](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw) -12. [二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww) -13. [二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA) -14. [还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg) -15. [二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) -16. [二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) -17. [二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw) -18. [二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg) -19. [二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) -20. [二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w) -21. [本周小结!(二叉树系列三)](https://mp.weixin.qq.com/s/JLLpx3a_8jurXcz6ovgxtg) -22. [二叉树:合并两个二叉树](https://mp.weixin.qq.com/s/3f5fbjOFaOX_4MXzZ97LsQ) -23. [二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg) -24. [二叉树:我是不是一棵二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q) -25. [二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ) -26. [二叉树:我的众数是多少?](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg) -27. [二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ) -28. [本周小结!(二叉树系列四)](https://mp.weixin.qq.com/s/CbdtOTP0N-HIP7DR203tSg) -29. [二叉树:搜索树的公共祖先问题](https://mp.weixin.qq.com/s/Ja9dVw2QhBcg_vV-1fkiCg) -30. [二叉树:搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA) -31. [二叉树:搜索树中的删除操作](https://mp.weixin.qq.com/s/-p-Txvch1FFk3ygKLjPAKw) -32. [二叉树:修剪一棵搜索树](https://mp.weixin.qq.com/s/QzmGfYUMUWGkbRj7-ozHoQ) -33. [二叉树:构造一棵搜索树](https://mp.weixin.qq.com/s/sy3ygnouaZVJs8lhFgl9mw) -34. [二叉树:搜索树转成累加树](https://mp.weixin.qq.com/s/hZtJh4T5lIGBarY-lZJf6Q) -35. [二叉树:总结篇!(需要掌握的二叉树技能都在这里了)](https://mp.weixin.qq.com/s/-ZJn3jJVdF683ap90yIj4Q) - +二叉树大纲 + +1. [关于二叉树,你该了解这些!](./problems/二叉树理论基础.md) +2. [二叉树:二叉树的递归遍历](./problems/二叉树的递归遍历.md) +3. [二叉树:二叉树的迭代遍历](./problems/二叉树的迭代遍历.md) +4. [二叉树:二叉树的统一迭代法](./problems/二叉树的统一迭代法.md) +5. [二叉树:二叉树的层序遍历](./problems/0102.二叉树的层序遍历.md) +6. [二叉树:226.翻转二叉树](./problems/0226.翻转二叉树.md) +7. [本周小结!(二叉树)](./problems/周总结/20200927二叉树周末总结.md) +8. [二叉树:101.对称二叉树](./problems/0101.对称二叉树.md) +9. [二叉树:104.二叉树的最大深度](./problems/0104.二叉树的最大深度.md) +10. [二叉树:111.二叉树的最小深度](./problems/0111.二叉树的最小深度.md) +11. [二叉树:222.完全二叉树的节点个数](./problems/0222.完全二叉树的节点个数.md) +12. [二叉树:110.平衡二叉树](./problems/0110.平衡二叉树.md) +13. [二叉树:257.二叉树的所有路径](./problems/0257.二叉树的所有路径.md) +14. [本周总结!(二叉树)](./problems/周总结/20201003二叉树周末总结.md) +16. [二叉树:404.左叶子之和](./problems/0404.左叶子之和.md) +17. [二叉树:513.找树左下角的值](./problems/0513.找树左下角的值.md) +18. [二叉树:112.路径总和](./problems/0112.路径总和.md) +19. [二叉树:106.构造二叉树](./problems/0106.从中序与后序遍历序列构造二叉树.md) +20. [二叉树:654.最大二叉树](./problems/0654.最大二叉树.md) +21. [本周小结!(二叉树)](./problems/周总结/20201010二叉树周末总结.md) +22. [二叉树:617.合并两个二叉树](./problems/0617.合并二叉树.md) +23. [二叉树:700.二叉搜索树登场!](./problems/0700.二叉搜索树中的搜索.md) +24. [二叉树:98.验证二叉搜索树](./problems/0098.验证二叉搜索树.md) +25. [二叉树:530.搜索树的最小绝对差](./problems/0530.二叉搜索树的最小绝对差.md) +26. [二叉树:501.二叉搜索树中的众数](./problems/0501.二叉搜索树中的众数.md) +27. [二叉树:236.公共祖先问题](./problems/0236.二叉树的最近公共祖先.md) +28. [本周小结!(二叉树)](./problems/周总结/20201017二叉树周末总结.md) +29. [二叉树:235.搜索树的最近公共祖先](./problems/0235.二叉搜索树的最近公共祖先.md) +30. [二叉树:701.搜索树中的插入操作](./problems/0701.二叉搜索树中的插入操作.md) +31. [二叉树:450.搜索树中的删除操作](./problems/0450.删除二叉搜索树中的节点.md) +32. [二叉树:669.修剪二叉搜索树](./problems/0669.修剪二叉搜索树.md) +33. [二叉树:108.将有序数组转换为二叉搜索树](./problems/0108.将有序数组转换为二叉搜索树.md) +34. [二叉树:538.把二叉搜索树转换为累加树](./problems/0538.把二叉搜索树转换为累加树.md) +35. [二叉树:总结篇!(需要掌握的二叉树技能都在这里了)](./problems/二叉树总结篇.md) + ## 回溯算法 题目分类大纲如下: -回溯算法大纲 - -1. [关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw) -2. [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ) -3. [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) -4. [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) -5. [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) -6. [本周小结!(回溯算法系列一)](https://mp.weixin.qq.com/s/m2GnTJdkYhAamustbb6lmw) -7. [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) -8. [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) -9. [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) -10. [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) -11. [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA) -12. [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw) -13. [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) -14. [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ) -15. [回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw) -16. [回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA) -17. [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) -18. [本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag) -19. [视频来了!!带你学透回溯算法(理论篇)](https://www.bilibili.com/video/BV1cy4y167mM) -20. [视频来了!!回溯算法(力扣题目:77.组合)](https://www.bilibili.com/video/BV1ti4y1L7cv) -21. [视频来了!!回溯算法剪枝操作(力扣题目:77.组合)](https://www.bilibili.com/video/BV1wi4y157er) -22. [视频来了!!回溯算法(力扣题目:39.组合总和)](https://www.bilibili.com/video/BV1KT4y1M7HJ/) -23. [回溯算法:重新安排行程](https://mp.weixin.qq.com/s/3kmbS4qDsa6bkyxR92XCTA) -24. [回溯算法:N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg) -25. [回溯算法:解数独](https://mp.weixin.qq.com/s/eWE9TapVwm77yW9Q81xSZQ) -26. [一篇总结带你彻底搞透回溯算法!](https://mp.weixin.qq.com/s/r73thpBnK1tXndFDtlsdCQ) +回溯算法大纲 + +1. [关于回溯算法,你该了解这些!](./problems/回溯算法理论基础.md) +2. [回溯算法:77.组合](./problems/0077.组合.md) +3. [回溯算法:77.组合优化](./problems/0077.组合优化.md) +4. [回溯算法:216.组合总和III](./problems/0216.组合总和III.md) +5. [回溯算法:17.电话号码的字母组合](./problems/0017.电话号码的字母组合.md) +6. [本周小结!(回溯算法系列一)](./problems/周总结/20201030回溯周末总结.md) +7. [回溯算法:39.组合总和](./problems/0039.组合总和.md) +8. [回溯算法:40.组合总和II](./problems/0040.组合总和II.md) +9. [回溯算法:131.分割回文串](./problems/0131.分割回文串.md) +10. [回溯算法:93.复原IP地址](./problems/0093.复原IP地址.md) +11. [回溯算法:78.子集](./problems/0078.子集.md) +12. [本周小结!(回溯算法系列二)](./problems/周总结/20201107回溯周末总结.md) +13. [回溯算法:90.子集II](./problems/0090.子集II.md) +14. [回溯算法:491.递增子序列](./problems/0491.递增子序列.md) +15. [回溯算法:46.全排列](./problems/0046.全排列.md) +16. [回溯算法:47.全排列II](./problems/0047.全排列II.md) +17. [本周小结!(回溯算法系列三)](./problems/周总结/20201112回溯周末总结.md) +18. [回溯算法去重问题的另一种写法](./problems/回溯算法去重问题的另一种写法.md) +19. [回溯算法:332.重新安排行程](./problems/0332.重新安排行程.md) +20. [回溯算法:51.N皇后](./problems/0051.N皇后.md) +21. [回溯算法:37.解数独](./problems/0037.解数独.md) +22. [回溯算法总结篇](./problems/回溯总结.md) ## 贪心算法 题目分类大纲如下: -贪心算法大纲 - -1. [关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg) -2. [贪心算法:分发饼干](https://mp.weixin.qq.com/s/YSuLIAYyRGlyxbp9BNC1uw) -3. [贪心算法:摆动序列](https://mp.weixin.qq.com/s/Xytl05kX8LZZ1iWWqjMoHA) -4. [贪心算法:最大子序和](https://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg) -5. [本周小结!(贪心算法系列一)](https://mp.weixin.qq.com/s/KQ2caT9GoVXgB1t2ExPncQ) -6. [贪心算法:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg) -7. [贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA) -8. [贪心算法:跳跃游戏II](https://mp.weixin.qq.com/s/kJBcsJ46DKCSjT19pxrNYg) -9. [贪心算法:K次取反后最大化的数组和](https://mp.weixin.qq.com/s/dMTzBBVllRm_Z0aaWvYazA) -10. [本周小结!(贪心算法系列二)](https://mp.weixin.qq.com/s/RiQri-4rP9abFmq_mlXNiQ) -11. [贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw) -12. [贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ) -13. [贪心算法:柠檬水找零](https://mp.weixin.qq.com/s/0kT4P-hzY7H6Ae0kjQqnZg) -14. [贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw) -15. [本周小结!(贪心算法系列三)](https://mp.weixin.qq.com/s/JfeuK6KgmifscXdpEyIm-g) -16. [贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ) -17. [贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw) -18. [贪心算法:无重叠区间](https://mp.weixin.qq.com/s/oFOEoW-13Bm4mik-aqAOmw) -19. [贪心算法:划分字母区间](https://mp.weixin.qq.com/s/pdX4JwV1AOpc_m90EcO2Hw) -20. [贪心算法:合并区间](https://mp.weixin.qq.com/s/royhzEM5tOkUFwUGrNStpw) -21. [本周小结!(贪心算法系列四)](https://mp.weixin.qq.com/s/zAMHT6JfB19ZSJNP713CAQ) -22. [贪心算法:单调递增的数字](https://mp.weixin.qq.com/s/TAKO9qPYiv6KdMlqNq_ncg) -23. [贪心算法:买卖股票的最佳时机含手续费](https://mp.weixin.qq.com/s/olWrUuDEYw2Jx5rMeG7XAg) -24. [贪心算法:我要监控二叉树!](https://mp.weixin.qq.com/s/kCxlLLjWKaE6nifHC3UL2Q) -25. [贪心算法:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/ItyoYNr0moGEYeRtcjZL3Q) + +贪心算法大纲 + +1. [关于贪心算法,你该了解这些!](./problems/贪心算法理论基础.md) +2. [贪心算法:455.分发饼干](./problems/0455.分发饼干.md) +3. [贪心算法:376.摆动序列](./problems/0376.摆动序列.md) +4. [贪心算法:53.最大子序和](./problems/0053.最大子序和.md) +5. [本周小结!(贪心算法系列一)](./problems/周总结/20201126贪心周末总结.md) +6. [贪心算法:122.买卖股票的最佳时机II](./problems/0122.买卖股票的最佳时机II.md) +7. [贪心算法:55.跳跃游戏](./problems/0055.跳跃游戏.md) +8. [贪心算法:45.跳跃游戏II](./problems/0045.跳跃游戏II.md) +9. [贪心算法:1005.K次取反后最大化的数组和](./problems/1005.K次取反后最大化的数组和.md) +10. [本周小结!(贪心算法系列二)](./problems/周总结/20201203贪心周末总结.md) +11. [贪心算法:134.加油站](./problems/0134.加油站.md) +12. [贪心算法:135.分发糖果](./problems/0135.分发糖果.md) +13. [贪心算法:860.柠檬水找零](./problems/0860.柠檬水找零.md) +14. [贪心算法:406.根据身高重建队列](./problems/0406.根据身高重建队列.md) +15. [本周小结!(贪心算法系列三)](./problems/周总结/20201217贪心周末总结.md) +16. [贪心算法:406.根据身高重建队列(续集)](./problems/根据身高重建队列(vector原理讲解).md) +17. [贪心算法:452.用最少数量的箭引爆气球](./problems/0452.用最少数量的箭引爆气球.md) +18. [贪心算法:435.无重叠区间](./problems/0435.无重叠区间.md) +19. [贪心算法:763.划分字母区间](./problems/0763.划分字母区间.md) +20. [贪心算法:56.合并区间](./problems/0056.合并区间.md) +21. [本周小结!(贪心算法系列四)](./problems/周总结/20201224贪心周末总结.md) +22. [贪心算法:738.单调递增的数字](./problems/0738.单调递增的数字.md) +23. [贪心算法:968.监控二叉树](./problems/0968.监控二叉树.md) +24. [贪心算法:总结篇!(每逢总结必经典)](./problems/贪心算法总结篇.md) ## 动态规划 动态规划专题已经开始啦,来不及解释了,小伙伴们上车别掉队! -1. [关于动态规划,你该了解这些!](https://mp.weixin.qq.com/s/ocZwfPlCWrJtVGACqFNAag) -2. [动态规划:斐波那契数](https://mp.weixin.qq.com/s/ko0zLJplF7n_4TysnPOa_w) -3. [动态规划:爬楼梯](https://mp.weixin.qq.com/s/Ohop0jApSII9xxOMiFhGIw) -4. [动态规划:使用最小花费爬楼梯](https://mp.weixin.qq.com/s/djZB9gkyLFAKcQcSvKDorA) -5. [本周小结!(动态规划系列一)](https://mp.weixin.qq.com/s/95VqGEDhtBBBSb-rM4QSMA) -6. [动态规划:不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A) -7. [动态规划:不同路径还不够,要有障碍!](https://mp.weixin.qq.com/s/lhqF0O4le9-wvalptOVOww) -8. [动态规划:整数拆分,你要怎么拆?](https://mp.weixin.qq.com/s/cVbyHrsWH_Rfzlj-ESr01A) -9. [动态规划:不同的二叉搜索树](https://mp.weixin.qq.com/s/8VE8pDrGxTf8NEVYBDwONw) -10. [本周小结!(动态规划系列二)](https://mp.weixin.qq.com/s/VVsDwTP57g1f9aVsg6wShw) + +1. [关于动态规划,你该了解这些!](./problems/动态规划理论基础.md) +2. [动态规划:509.斐波那契数](./problems/0509.斐波那契数.md) +3. [动态规划:70.爬楼梯](./problems/0070.爬楼梯.md) +4. [动态规划:746.使用最小花费爬楼梯](./problems/0746.使用最小花费爬楼梯.md) +5. [本周小结!(动态规划系列一)](./problems/周总结/20210107动规周末总结.md) +6. [动态规划:62.不同路径](./problems/0062.不同路径.md) +7. [动态规划:63.不同路径II](./problems/0063.不同路径II.md) +8. [动态规划:343.整数拆分](./problems/0343.整数拆分.md) +9. [动态规划:96.不同的二叉搜索树](./problems/0096.不同的二叉搜索树.md) +10. [本周小结!(动态规划系列二)](./problems/周总结/20210114动规周末总结.md) 背包问题系列: -背包问题大纲 - -11. [动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w) -12. [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA) -13. [动态规划:分割等和子集可以用01背包!](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ) -14. [动态规划:最后一块石头的重量 II](https://mp.weixin.qq.com/s/WbwAo3jaUaNJjvhHgq0BGg) -15. [本周小结!(动态规划系列三)](https://mp.weixin.qq.com/s/7emRqR1O3scH63jbaE678A) -16. [动态规划:目标和!](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw) -17. [动态规划:一和零!](https://mp.weixin.qq.com/s/x-u3Dsp76DlYqtCe0xEKJw) -18. [动态规划:关于完全背包,你该了解这些!](https://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw) -19. [动态规划:给你一些零钱,你要怎么凑?](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) -20. [本周小结!(动态规划系列四)](https://mp.weixin.qq.com/s/vfEXwcOlrSBBcv9gg8VDJQ) -21. [动态规划:Carl称它为排列总和!](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA) -22. [动态规划:以前我没得选,现在我选择再爬一次!](https://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA) -23. [动态规划: 给我个机会,我再兑换一次零钱](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ) -24. [动态规划:一样的套路,再求一次完全平方数](https://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ) -25. [本周小结!(动态规划系列五)](https://mp.weixin.qq.com/s/znj-9j8mWymRFaPjJN2Qnw) -26. [动态规划:单词拆分](https://mp.weixin.qq.com/s/3Spx1B6MbIYjS8YkVbByzA) -27. [动态规划:关于多重背包,你该了解这些!](https://mp.weixin.qq.com/s/b-UUUmbvG7URWyCjQkiuuQ) -28. [听说背包问题很难? 这篇总结篇来拯救你了](https://mp.weixin.qq.com/s/ZOehl3U1mDiyOQjFG1wNJA) +背包问题大纲 + + +11. [动态规划:01背包理论基础(二维dp数组)](./problems/背包理论基础01背包-1.md) +12. [动态规划:01背包理论基础(一维dp数组)](./problems/背包理论基础01背包-2.md) +13. [动态规划:416.分割等和子集](./problems/0416.分割等和子集.md) +14. [动态规划:1049.最后一块石头的重量II](./problems/1049.最后一块石头的重量II.md) +15. [本周小结!(动态规划系列三)](./problems/周总结/20210121动规周末总结.md) +16. [动态规划:494.目标和](./problems/0494.目标和.md) +17. [动态规划:474.一和零](./problems/0474.一和零.md) +18. [动态规划:完全背包理论基础(二维dp数组)](./problems/背包问题理论基础完全背包.md) +19. [动态规划:完全背包理论基础(一维dp数组)](./problems/背包问题完全背包一维.md) +20. [动态规划:518.零钱兑换II](./problems/0518.零钱兑换II.md) +21. [本周小结!(动态规划系列四)](./problems/周总结/20210128动规周末总结.md) +22. [动态规划:377.组合总和Ⅳ](./problems/0377.组合总和Ⅳ.md) +23. [动态规划:70.爬楼梯(完全背包版本)](./problems/0070.爬楼梯完全背包版本.md) +24. [动态规划:322.零钱兑换](./problems/0322.零钱兑换.md) +25. [动态规划:279.完全平方数](./problems/0279.完全平方数.md) +26. [本周小结!(动态规划系列五)](./problems/周总结/20210204动规周末总结.md) +27. [动态规划:139.单词拆分](./problems/0139.单词拆分.md) +28. [动态规划:多重背包理论基础](./problems/背包问题理论基础多重背包.md) +29. [背包问题总结篇](./problems/背包总结篇.md) 打家劫舍系列: -29. [动态规划:开始打家劫舍!](https://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw) -30. [动态规划:继续打家劫舍!](https://mp.weixin.qq.com/s/kKPx4HpH3RArbRcxAVHbeQ) -31. [动态规划:还要打家劫舍!](https://mp.weixin.qq.com/s/BOJ1lHsxbQxUZffXlgglEQ) +29. [动态规划:198.打家劫舍](./problems/0198.打家劫舍.md) +30. [动态规划:213.打家劫舍II](./problems/0213.打家劫舍II.md) +31. [动态规划:337.打家劫舍III](./problems/0337.打家劫舍III.md) 股票系列: -32. [动态规划:买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ) -33. [动态规划:本周我们都讲了这些(系列六)](https://mp.weixin.qq.com/s/GVu-6eF0iNkpVDKRXTPOTA) -33. [动态规划:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w) -34. [动态规划:买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg) -35. [动态规划:买卖股票的最佳时机IV](https://mp.weixin.qq.com/s/jtxZJWAo2y5sUsW647Z5cw) -36. [动态规划:最佳买卖股票时机含冷冻期](https://mp.weixin.qq.com/s/IgC0iWWCDpYL9ZbTHGHgfw) -37. [动态规划:本周我们都讲了这些(系列七)](https://mp.weixin.qq.com/s/vdzDlrEvhXWRzblTnOnzKg) -38. [动态规划:买卖股票的最佳时机含手续费](https://mp.weixin.qq.com/s/2Cd_uINjerZ25VHH0K2IBQ) +股票问题总结 + + +32. [动态规划:121.买卖股票的最佳时机](./problems/0121.买卖股票的最佳时机.md) +33. [动态规划:本周小结(系列六)](./problems/周总结/20210225动规周末总结.md) +34. [动态规划:122.买卖股票的最佳时机II](./problems/0122.买卖股票的最佳时机II(动态规划).md) +35. [动态规划:123.买卖股票的最佳时机III](./problems/0123.买卖股票的最佳时机III.md) +36. [动态规划:188.买卖股票的最佳时机IV](./problems/0188.买卖股票的最佳时机IV.md) +37. [动态规划:309.最佳买卖股票时机含冷冻期](./problems/0309.最佳买卖股票时机含冷冻期.md) +38. [动态规划:本周小结(系列七)](./problems/周总结/20210304动规周末总结.md) +39. [动态规划:714.买卖股票的最佳时机含手续费](./problems/0714.买卖股票的最佳时机含手续费(动态规划).md) +40. [动态规划:股票系列总结篇](./problems/动态规划-股票问题总结篇.md) 子序列系列: -39. [动态规划:最长递增子序列](https://mp.weixin.qq.com/s/f8nLO3JGfgriXep_gJQpqQ) -40. [动态规划:最长连续递增序列](https://mp.weixin.qq.com/s/c0Nn0TtjkTISVdqRsyMmyA) + +41. [动态规划:300.最长递增子序列](./problems/0300.最长上升子序列.md) +42. [动态规划:674.最长连续递增序列](./problems/0674.最长连续递增序列.md) +43. [动态规划:718.最长重复子数组](./problems/0718.最长重复子数组.md) +44. [动态规划:1143.最长公共子序列](./problems/1143.最长公共子序列.md) +45. [动态规划:1035.不相交的线](./problems/1035.不相交的线.md) +46. [动态规划:53.最大子序和](./problems/0053.最大子序和(动态规划).md) +47. [动态规划:392.判断子序列](./problems/0392.判断子序列.md) +48. [动态规划:115.不同的子序列](./problems/0115.不同的子序列.md) +49. [动态规划:583.两个字符串的删除操作](./problems/0583.两个字符串的删除操作.md) +50. [动态规划:72.编辑距离](./problems/0072.编辑距离.md) +51. [编辑距离总结篇](./problems/为了绝杀编辑距离,卡尔做了三步铺垫.md) +52. [动态规划:647.回文子串](./problems/0647.回文子串.md) +53. [动态规划:516.最长回文子序列](./problems/0516.最长回文子序列.md) +54. [动态规划总结篇](./problems/动态规划总结篇.md) -(持续更新中....) +## 单调栈 + +1. [单调栈:739.每日温度](./problems/0739.每日温度.md) +2. [单调栈:496.下一个更大元素I](./problems/0496.下一个更大元素I.md) +3. [单调栈:503.下一个更大元素II](./problems/0503.下一个更大元素II.md) +4. [单调栈:42.接雨水](./problems/0042.接雨水.md) +5. [单调栈:84.柱状图中最大的矩形](./problems/0084.柱状图中最大的矩形.md) + ## 图论 -## 十大排序 +**[图论正式发布](./problems/qita/tulunfabu.md)** + +1. [图论:理论基础](./problems/kamacoder/图论理论基础.md) +2. [图论:深度优先搜索理论基础](./problems/kamacoder/图论深搜理论基础.md) +3. [图论:所有可达路径](./problems/kamacoder/0098.所有可达路径.md) +4. [图论:广度优先搜索理论基础](./problems/kamacoder/图论广搜理论基础.md) +5. [图论:岛屿数量.深搜版](./problems/kamacoder/0099.岛屿的数量深搜.md) +6. [图论:岛屿数量.广搜版](./problems/kamacoder/0099.岛屿的数量广搜.md) +7. [图论:岛屿的最大面积](./problems/kamacoder/0100.岛屿的最大面积.md) +8. [图论:孤岛的总面积](./problems/kamacoder/0101.孤岛的总面积.md) +9. [图论:沉没孤岛](./problems/kamacoder/0102.沉没孤岛.md) +10. [图论:水流问题](./problems/kamacoder/0103.水流问题.md) +11. [图论:建造最大岛屿](./problems/kamacoder/0104.建造最大岛屿.md) +12. [图论:岛屿的周长](./problems/kamacoder/0106.岛屿的周长.md) +13. [图论:字符串接龙](./problems/kamacoder/0110.字符串接龙.md) +14. [图论:有向图的完全可达性](./problems/kamacoder/0105.有向图的完全可达性.md) +15. [图论:并查集理论基础](./problems/kamacoder/图论并查集理论基础.md) +16. [图论:寻找存在的路径](./problems/kamacoder/0107.寻找存在的路径.md) +17. [图论:冗余连接](./problems/kamacoder/0108.冗余连接.md) +18. [图论:冗余连接II](./problems/kamacoder/0109.冗余连接II.md) +19. [图论:最小生成树之prim](./problems/kamacoder/0053.寻宝-prim.md) +20. [图论:最小生成树之kruskal](./problems/kamacoder/0053.寻宝-Kruskal.md) +21. [图论:拓扑排序](./problems/kamacoder/0117.软件构建.md) +22. [图论:dijkstra(朴素版)](./problems/kamacoder/0047.参会dijkstra朴素.md) +23. [图论:dijkstra(堆优化版)](./problems/kamacoder/0047.参会dijkstra堆.md) +24. [图论:Bellman_ford 算法](./problems/kamacoder/0094.城市间货物运输I.md) +25. [图论:Bellman_ford 队列优化算法(又名SPFA)](./problems/kamacoder/0094.城市间货物运输I-SPFA.md) +26. [图论:Bellman_ford之判断负权回路](./problems/kamacoder/0095.城市间货物运输II.md) +27. [图论:Bellman_ford之单源有限最短路](./problems/kamacoder/0096.城市间货物运输III.md) +28. [图论:Floyd 算法](./problems/kamacoder/0097.小明逛公园.md) +29. [图论:A * 算法](./problems/kamacoder/0126.骑士的攻击astar.md) +30. [图论:最短路算法总结篇](./problems/kamacoder/最短路问题总结篇.md) +31. [图论:图论总结篇](./problems/kamacoder/图论总结篇.md) + + +(持续更新中....) + +# 补充题目 + +以上题目是重中之重,大家至少要刷两遍以上才能彻底理解,如果熟练以上题目之后还在找其他题目练手,可以再刷以下题目: + +这些题目很不错,但有的题目是和刷题攻略类似的,有的题解后面还会适当补充,所以我还没有将其纳入到刷题攻略。一些题解等日后我完善一下,再纳入到刷题攻略。 + + +## 数组 + +* [1365.有多少小于当前数字的数字](./problems/1365.有多少小于当前数字的数字.md) +* [941.有效的山脉数组](./problems/0941.有效的山脉数组.md) (双指针) +* [1207.独一无二的出现次数](./problems/1207.独一无二的出现次数.md) 数组在哈希法中的经典应用 +* [283.移动零](./problems/0283.移动零.md) 【数组】【双指针】 +* [189.旋转数组](./problems/0189.旋转数组.md) +* [724.寻找数组的中心索引](./problems/0724.寻找数组的中心索引.md) +* [34.在排序数组中查找元素的第一个和最后一个位置](./problems/0034.在排序数组中查找元素的第一个和最后一个位置.md) (二分法) +* [922.按奇偶排序数组II](./problems/0922.按奇偶排序数组II.md) +* [35.搜索插入位置](./problems/0035.搜索插入位置.md) + +## 链表 + +* [24.两两交换链表中的节点](./problems/0024.两两交换链表中的节点.md) +* [234.回文链表](./problems/0234.回文链表.md) +* [143.重排链表](./problems/0143.重排链表.md)【数组】【双向队列】【直接操作链表】 +* [141.环形链表](./problems/0141.环形链表.md) +* [160.相交链表](./problems/面试题02.07.链表相交.md) -## 数论 +## 哈希表 +* [205.同构字符串](./problems/0205.同构字符串.md):【哈希表的应用】 + +## 字符串 +* [925.长按键入](./problems/0925.长按键入.md) 模拟匹配 +* [0844.比较含退格的字符串](./problems/0844.比较含退格的字符串.md)【栈模拟】【空间更优的双指针】 + +## 二叉树 +* [129.求根到叶子节点数字之和](./problems/0129.求根到叶子节点数字之和.md) +* [1382.将二叉搜索树变平衡](./problems/1382.将二叉搜索树变平衡.md) 构造平衡二叉搜索树 +* [100.相同的树](./problems/0100.相同的树.md) 同101.对称二叉树 一个思路 +* [116.填充每个节点的下一个右侧节点指针](./problems/0116.填充每个节点的下一个右侧节点指针.md) + +## 回溯算法 + +* [52.N皇后II](./problems/0052.N皇后II.md) + -## 高级数据结构经典题目 +## 贪心 +* [649.Dota2参议院](./problems/0649.Dota2参议院.md) 有难度 +* [1221.分割平衡字符](./problems/1221.分割平衡字符串.md) 简单贪心 -* 并查集 -* 最小生成树 -* 线段树 -* 树状数组 -* 字典树 +## 动态规划 +* [5.最长回文子串](./problems/0005.最长回文子串.md) 和[647.回文子串](https://mp.weixin.qq.com/s/2WetyP6IYQ6VotegepVpEw) 差不多是一样的 +* [132.分割回文串II](./problems/0132.分割回文串II.md) 与647.回文子串和 5.最长回文子串 很像 +* [673.最长递增子序列的个数](./problems/0673.最长递增子序列的个数.md) + +## 图论 +* [463.岛屿的周长](./problems/0463.岛屿的周长.md) (模拟) +* [841.钥匙和房间](./problems/0841.钥匙和房间.md) 【有向图】dfs,bfs都可以 +* [127.单词接龙](./problems/0127.单词接龙.md) 广搜 + +## 并查集 +* [684.冗余连接](./problems/0684.冗余连接.md) 【并查集基础题目】 +* [685.冗余连接II](./problems/0685.冗余连接II.md)【并查集的应用】 + +## 模拟 +* [657.机器人能否返回原点](./problems/0657.机器人能否返回原点.md) +* [31.下一个排列](./problems/0031.下一个排列.md) + +## 位运算 +* [1356.根据数字二进制下1的数目排序](./problems/1356.根据数字二进制下1的数目排序.md) -## 海量数据处理 # 算法模板 [各类基础算法模板](https://github.com/youngyangyang04/leetcode/blob/master/problems/算法模板.md) +# 贡献者 -# 关于作者 +[点此这里](https://github.com/youngyangyang04/leetcode-master/graphs/contributors)查看LeetCode-Master的所有贡献者。感谢他们补充了LeetCode-Master的其他语言版本,让更多的读者受益于此项目。 -大家好,我是程序员Carl,哈工大师兄,ACM 校赛、黑龙江省赛、东北四省赛金牌、亚洲区域赛铜牌获得者,先后在腾讯和百度从事后端技术研发,CSDN博客专家。对算法和C++后端技术有一定的见解,利用工作之余重新刷leetcode。 +# Star 趋势 -**加我的微信,备注:「个人简单介绍」+「组队刷题」**, 拉你进刷题群,每天一道经典题目分析,而且题目不是孤立的,每一道题目之间都是有关系的,都是由浅入深一脉相承的,所以学习效果最好是每篇连续着看,也许之前你会某些知识点,但是一直没有把知识点串起来,这里每天一篇文章就会帮你把知识点串起来。 +[![Star History Chart](https://api.star-history.com/svg?repos=youngyangyang04/leetcode-master&type=Date)](https://star-history.com/#youngyangyang04/leetcode-master&Date) -也欢迎找我交流,加微信备注:「个人简单介绍」 + 交流 +# 关于作者 - - +大家好,我是程序员Carl,哈工大师兄,《代码随想录》作者,先后在腾讯和百度从事后端技术底层技术研发。 -# 我的公众号 +# PDF下载 -更多精彩文章持续更新,微信搜索:「代码随想录」第一时间围观,关注后回复:「666」可以获得所有算法专题原创PDF。 +添加如下企业微信,会自动发送给大家PDF版本,顺便可以选择是否加入刷题群。 -**每天8:35准时为你推送一篇经典面试题目,帮你梳理算法知识体系,轻松学习算法!**,并且公众号里有大量学习资源,也有我自己的学习心得和方法总结,更有上万录友们在这里打卡学习,**来看看就你知道了,一定会发现相见恨晚!** +添加微信记得备注,如果是已工作,备注:姓名-城市-岗位。如果学生,备注:姓名-学校-年级。**备注没有自我介绍不通过哦** - +
-![](./pics/公众号.png) diff --git "a/pics/\345\205\254\344\274\227\345\217\267.png" "b/pics/\345\205\254\344\274\227\345\217\267.png" index 9873ef4294..eeec00ad37 100644 Binary files "a/pics/\345\205\254\344\274\227\345\217\267.png" and "b/pics/\345\205\254\344\274\227\345\217\267.png" differ diff --git "a/pics/\345\205\254\344\274\227\345\217\267\344\272\214\347\273\264\347\240\201.jpg" "b/pics/\345\205\254\344\274\227\345\217\267\344\272\214\347\273\264\347\240\201.jpg" new file mode 100644 index 0000000000..a91b2494d7 Binary files /dev/null and "b/pics/\345\205\254\344\274\227\345\217\267\344\272\214\347\273\264\347\240\201.jpg" differ diff --git "a/pics/\347\237\245\350\257\206\346\230\237\347\220\203.png" "b/pics/\347\237\245\350\257\206\346\230\237\347\220\203.png" new file mode 100644 index 0000000000..43a5c6b389 Binary files /dev/null and "b/pics/\347\237\245\350\257\206\346\230\237\347\220\203.png" differ diff --git "a/pics/\347\275\221\347\253\231\346\230\237\347\220\203\345\256\243\344\274\240\346\265\267\346\212\245.jpg" "b/pics/\347\275\221\347\253\231\346\230\237\347\220\203\345\256\243\344\274\240\346\265\267\346\212\245.jpg" new file mode 100644 index 0000000000..547d570418 Binary files /dev/null and "b/pics/\347\275\221\347\253\231\346\230\237\347\220\203\345\256\243\344\274\240\346\265\267\346\212\245.jpg" differ diff --git "a/pics/\350\256\255\347\273\203\350\220\245.png" "b/pics/\350\256\255\347\273\203\350\220\245.png" new file mode 100644 index 0000000000..34433d7b37 Binary files /dev/null and "b/pics/\350\256\255\347\273\203\350\220\245.png" differ diff --git "a/problems/0001.\344\270\244\346\225\260\344\271\213\345\222\214.md" "b/problems/0001.\344\270\244\346\225\260\344\271\213\345\222\214.md" new file mode 100755 index 0000000000..a11527961d --- /dev/null +++ "b/problems/0001.\344\270\244\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,557 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 1. 两数之和 + +[力扣题目链接](https://leetcode.cn/problems/two-sum/) + +给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 + +你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。 + +**示例:** + +给定 nums = [2, 7, 11, 15], target = 9 + +因为 nums[0] + nums[1] = 2 + 7 = 9 + +所以返回 [0, 1] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[梦开始的地方,Leetcode:1.两数之和](https://www.bilibili.com/video/BV1aT41177mK),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +很明显暴力的解法是两层for循环查找,时间复杂度是O(n^2)。 + +建议大家做这道题目之前,先做一下这两道 +* [242. 有效的字母异位词](https://www.programmercarl.com/0242.有效的字母异位词.html) +* [349. 两个数组的交集](https://www.programmercarl.com/0349.两个数组的交集.html) + +[242. 有效的字母异位词](https://www.programmercarl.com/0242.有效的字母异位词.html) 这道题目是用数组作为哈希表来解决哈希问题,[349. 两个数组的交集](https://www.programmercarl.com/0349.两个数组的交集.html)这道题目是通过set作为哈希表来解决哈希问题。 + + +首先我再强调一下 **什么时候使用哈希法**,当我们需要查询一个元素是否出现过,或者一个元素是否在集合里的时候,就要第一时间想到哈希法。 + +本题呢,我就需要一个集合来存放我们遍历过的元素,然后在遍历数组的时候去询问这个集合,某元素是否遍历过,也就是 是否出现在这个集合。 + +那么我们就应该想到使用哈希法了。 + +因为本题,我们不仅要知道元素有没有遍历过,还要知道这个元素对应的下标,**需要使用 key value结构来存放,key来存元素,value来存下标,那么使用map正合适**。 + +再来看一下使用数组和set来做哈希法的局限。 + +* 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。 +* set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。 + +此时就要选择另一种数据结构:map ,map是一种key value的存储结构,可以用key保存数值,用value再保存数值所在的下标。 + +C++中map,有三种类型: + +|映射 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率| +|---|---| --- |---| --- | --- | ---| +|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | O(log n)|O(log n) | +|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|O(log n) |O(log n) | +|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |O(1) | O(1)| + +std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。 + +同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。 更多哈希表的理论知识请看[关于哈希表,你该了解这些!](https://www.programmercarl.com/哈希表理论基础.html)。 + +**这道题目中并不需要key有序,选择std::unordered_map 效率更高!** 使用其他语言的录友注意了解一下自己所用语言的数据结构就行。 + +接下来需要明确两点: + +* **map用来做什么** +* **map中key和value分别表示什么** + +map目的用来存放我们访问过的元素,因为遍历数组的时候,需要记录我们之前遍历过哪些元素和对应的下标,这样才能找到与当前元素相匹配的(也就是相加等于target) + +接下来是map中key和value分别表示什么。 + +这道题 我们需要 给出一个元素,判断这个元素是否出现过,如果出现过,返回这个元素的下标。 + +那么判断元素是否出现,这个元素就要作为key,所以数组中的元素作为key,有key对应的就是value,value用来存下标。 + +所以 map中的存储结构为 {key:数据元素,value:数组元素对应的下标}。 + +在遍历数组的时候,只需要向map去查询是否有和目前遍历元素匹配的数值,如果有,就找到的匹配对,如果没有,就把目前遍历的元素放进map中,因为map存放的就是我们访问过的元素。 + +过程如下: + +![过程一](https://file1.kamacoder.com/i/algo/20220711202638.png) + + +![过程二](https://file1.kamacoder.com/i/algo/20230220223536.png) + +C++代码: + +```CPP +class Solution { +public: + vector twoSum(vector& nums, int target) { + std::unordered_map map; + for(int i = 0; i < nums.size(); i++) { + // 遍历当前元素,并在map中寻找是否有匹配的key + auto iter = map.find(target - nums[i]); + if(iter != map.end()) { + return {iter->second, i}; + } + // 如果没找到匹配对,就把访问过的元素和下标加入到map中 + map.insert(pair(nums[i], i)); + } + return {}; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(n) + +## 总结 + +本题其实有四个重点: + +* 为什么会想到用哈希表 +* 哈希表为什么用map +* 本题map是用来存什么的 +* map中的key和value用来存什么的 + +把这四点想清楚了,本题才算是理解透彻了。 + +很多录友把这道题目 通过了,但都没想清楚map是用来做什么的,以至于对代码的理解其实是 一知半解的。 + + +## 其他语言版本 + +### Java: + +```java +//使用哈希表 +public int[] twoSum(int[] nums, int target) { + int[] res = new int[2]; + if(nums == null || nums.length == 0){ + return res; + } + Map map = new HashMap<>(); + for(int i = 0; i < nums.length; i++){ + int temp = target - nums[i]; // 遍历当前元素,并在map中寻找是否有匹配的key + if(map.containsKey(temp)){ + res[1] = i; + res[0] = map.get(temp); + break; + } + map.put(nums[i], i); // 如果没找到匹配对,就把访问过的元素和下标加入到map中 + } + return res; +} +``` + +```java +//使用哈希表方法2 +public int[] twoSum(int[] nums, int target) { + Map indexMap = new HashMap<>(); + + for(int i = 0; i < nums.length; i++){ + int balance = target - nums[i]; // 记录当前的目标值的余数 + if(indexMap.containsKey(balance)){ // 查找当前的map中是否有满足要求的值 + return new int []{i, indexMap.get(balance)}; // 如果有,返回目标值 + } else{ + indexMap.put(nums[i], i); // 如果没有,把访问过的元素和下标加入map中 + } + } + return null; +} +``` + +```java +//使用双指针 +public int[] twoSum(int[] nums, int target) { + int m=0,n=0,k,board=0; + int[] res=new int[2]; + int[] tmp1=new int[nums.length]; + //备份原本下标的nums数组 + System.arraycopy(nums,0,tmp1,0,nums.length); + //将nums排序 + Arrays.sort(nums); + //双指针 + for(int i=0,j=nums.length-1;itarget) + j--; + else if(nums[i]+nums[j]==target){ + m=i; + n=j; + break; + } + } + //找到nums[m]在tmp1数组中的下标 + for(k=0;k List[int]: + records = dict() + + for index, value in enumerate(nums): + if target - value in records: # 遍历当前元素,并在map中寻找是否有匹配的key + return [records[target- value], index] + records[value] = index # 如果没找到匹配对,就把访问过的元素和下标加入到map中 + return [] +``` +(版本二)使用集合 +```python +class Solution: + def twoSum(self, nums: List[int], target: int) -> List[int]: + #创建一个集合来存储我们目前看到的数字 + seen = set() + for i, num in enumerate(nums): + complement = target - num + if complement in seen: + return [nums.index(complement), i] + seen.add(num) +``` +(版本三)使用双指针 +```python +class Solution: + def twoSum(self, nums: List[int], target: int) -> List[int]: + # 对输入列表进行排序 + nums_sorted = sorted(nums) + + # 使用双指针 + left = 0 + right = len(nums_sorted) - 1 + while left < right: + current_sum = nums_sorted[left] + nums_sorted[right] + if current_sum == target: + # 如果和等于目标数,则返回两个数的下标 + left_index = nums.index(nums_sorted[left]) + right_index = nums.index(nums_sorted[right]) + if left_index == right_index: + right_index = nums[left_index+1:].index(nums_sorted[right]) + left_index + 1 + return [left_index, right_index] + elif current_sum < target: + # 如果总和小于目标,则将左侧指针向右移动 + left += 1 + else: + # 如果总和大于目标值,则将右指针向左移动 + right -= 1 +``` +(版本四)暴力法 +```python +class Solution: + def twoSum(self, nums: List[int], target: int) -> List[int]: + for i in range(len(nums)): + for j in range(i+1, len(nums)): + if nums[i] + nums[j] == target: + return [i,j] +``` + +### Go: + +```go +// 暴力解法 +func twoSum(nums []int, target int) []int { + for k1, _ := range nums { + for k2 := k1 + 1; k2 < len(nums); k2++ { + if target == nums[k1] + nums[k2] { + return []int{k1, k2} + } + } + } + return []int{} +} +``` + +```go +// 使用map方式解题,降低时间复杂度 +func twoSum(nums []int, target int) []int { + m := make(map[int]int) + for index, val := range nums { + if preIndex, ok := m[target-val]; ok { + return []int{preIndex, index} + } else { + m[val] = index + } + } + return []int{} +} +``` + +### Rust: + +```rust +use std::collections::HashMap; + +impl Solution { + pub fn two_sum(nums: Vec, target: i32) -> Vec { + let mut map = HashMap::with_capacity(nums.len()); + + for i in 0..nums.len() { + if let Some(k) = map.get(&(target - nums[i])) { + if *k != i { + return vec![*k as i32, i as i32]; + } + } + map.insert(nums[i], i); + } + panic!("not found") + } +} +``` +```rust +use std::collections::HashMap; + +impl Solution { + pub fn two_sum(nums: Vec, target: i32) -> Vec { + let mut hm: HashMap = HashMap::new(); + for i in 0..nums.len() { + let j = target - nums[i]; + if hm.contains_key(&j) { + return vec![*hm.get(&j).unwrap(), i as i32] + } else { + hm.insert(nums[i], i as i32); + } + } + vec![-1, -1] + } +} +``` + +### JavaScript: + +```javascript +var twoSum = function (nums, target) { + let hash = {}; + for (let i = 0; i < nums.length; i++) { // 遍历当前元素,并在map中寻找是否有匹配的key + if (hash[target - nums[i]] !== undefined) { + return [i, hash[target - nums[i]]]; + } + hash[nums[i]] = i; // 如果没找到匹配对,就把访问过的元素和下标加入到map中 + } + return []; +}; +``` + +### TypeScript: + +```typescript +function twoSum(nums: number[], target: number): number[] { + let helperMap: Map = new Map(); + let index: number | undefined; + let resArr: number[] = []; + for (let i = 0, length = nums.length; i < length; i++) { + index = helperMap.get(target - nums[i]); + if (index !== undefined) { + resArr = [i, index]; + break; + } + helperMap.set(nums[i], i); + } + return resArr; +}; +``` + +### PhP: + +```php +function twoSum(array $nums, int $target): array +{ + $map = []; + foreach($nums as $i => $num) { + if (isset($map[$target - $num])) { + return [ + $i, + $map[$target - $num] + ]; + } else { + $map[$num] = $i; + } + } + return []; +} +``` + +### Swift: + +```swift +func twoSum(_ nums: [Int], _ target: Int) -> [Int] { + // 值: 下标 + var map = [Int: Int]() + for (i, e) in nums.enumerated() { + if let v = map[target - e] { + return [v, i] + } else { + map[e] = i + } + } + return [] +} +``` + +### Scala: + +```scala +object Solution { + // 导入包 + import scala.collection.mutable + def twoSum(nums: Array[Int], target: Int): Array[Int] = { + // key存储值,value存储下标 + val map = new mutable.HashMap[Int, Int]() + for (i <- nums.indices) { + val tmp = target - nums(i) // 计算差值 + // 如果这个差值存在于map,则说明找到了结果 + if (map.contains(tmp)) { + return Array(map.get(tmp).get, i) + } + // 如果不包含把当前值与其下标放到map + map.put(nums(i), i) + } + // 如果没有找到直接返回一个空的数组,return关键字可以省略 + new Array[Int](2) + } +} +``` + +### C#: + +```csharp +public class Solution { + public int[] TwoSum(int[] nums, int target) { + Dictionary dic= new Dictionary(); + for(int i=0;i twoSum(List nums, int target) { + HashMap hashMap = HashMap(); + for (int i = 0; i < nums.length; i++) { + int rest = target - nums[i]; + if (hashMap.containsKey(rest)) { + return [hashMap[rest]!, i]; + } + hashMap.addEntries({nums[i]: i}.entries); + } + return []; +} +``` + +### C: + +```c + + +/** + * Note: The returned array must be malloced, assume caller calls free(). + */ + +// leetcode 支持 ut_hash 函式庫 + + typedef struct { + int key; + int value; + UT_hash_handle hh; // make this structure hashable + } map; + +map* hashMap = NULL; + + void hashMapAdd(int key, int value){ + map* s; + // key already in the hash? + HASH_FIND_INT(hashMap, &key, s); + if(s == NULL){ + s = (map*)malloc(sizeof(map)); + s -> key = key; + HASH_ADD_INT(hashMap, key, s); + } + s -> value = value; + } + +map* hashMapFind(int key){ + map* s; + // *s: output pointer + HASH_FIND_INT(hashMap, &key, s); + return s; + } + + void hashMapCleanup(){ + map* cur, *tmp; + HASH_ITER(hh, hashMap, cur, tmp){ + HASH_DEL(hashMap, cur); + free(cur); + } + } + + void hashPrint(){ + map* s; + for(s = hashMap; s != NULL; s=(map*)(s -> hh.next)){ + printf("key %d, value %d\n", s -> key, s -> value); + } + } + + +int* twoSum(int* nums, int numsSize, int target, int* returnSize){ + int i, *ans; + // hash find result + map* hashMapRes; + hashMap = NULL; + ans = malloc(sizeof(int) * 2); + + for(i = 0; i < numsSize; i++){ + // key 代表 nums[i] 的值,value 代表所在 index; + hashMapAdd(nums[i], i); + } + + hashPrint(); + + for(i = 0; i < numsSize; i++){ + hashMapRes = hashMapFind(target - nums[i]); + if(hashMapRes && hashMapRes -> value != i){ + ans[0] = i; + ans[1] = hashMapRes -> value ; + *returnSize = 2; + return ans; + } + } + + hashMapCleanup(); + return NULL; +} +``` + diff --git "a/problems/0005.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/problems/0005.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" new file mode 100755 index 0000000000..05dd610a72 --- /dev/null +++ "b/problems/0005.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\344\270\262.md" @@ -0,0 +1,732 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 5.最长回文子串 + +[力扣题目链接](https://leetcode.cn/problems/longest-palindromic-substring/) + +给你一个字符串 s,找到 s 中最长的回文子串。 + +示例 1: +* 输入:s = "babad" +* 输出:"bab" +* 解释:"aba" 同样是符合题意的答案。 + +示例 2: +* 输入:s = "cbbd" +* 输出:"bb" + +示例 3: +* 输入:s = "a" +* 输出:"a" + +示例 4: +* 输入:s = "ac" +* 输出:"a" + + +## 思路 + +本题和[647.回文子串](https://programmercarl.com/0647.回文子串.html) 差不多是一样的,但647.回文子串更基本一点,建议可以先做647.回文子串 + +### 暴力解法 + +两层for循环,遍历区间起始位置和终止位置,然后判断这个区间是不是回文。 + +时间复杂度:O(n^3) + +### 动态规划 + +动规五部曲: + +1. 确定dp数组(dp table)以及下标的含义 + +布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。 + + +2. 确定递推公式 + +在确定递推公式时,就要分析如下几种情况。 + +整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。 + +当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。 + +当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况 + +* 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串 +* 情况二:下标i 与 j相差为1,例如aa,也是文子串 +* 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。 + +以上三种情况分析完了,那么递归公式如下: + +```CPP +if (s[i] == s[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { // 情况三 + dp[i][j] = true; + } +} +``` + +注意这里我没有列出当s[i]与s[j]不相等的时候,因为在下面dp[i][j]初始化的时候,就初始为false。 + +在得到[i,j]区间是否是回文子串的时候,直接保存最长回文子串的左边界和右边界,代码如下: + +```CPP +if (s[i] == s[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { // 情况三 + dp[i][j] = true; + } +} +if (dp[i][j] && j - i + 1 > maxlenth) { + maxlenth = j - i + 1; + left = i; + right = j; +} +``` + +3. dp数组如何初始化 + +dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹配上了。 + +所以dp[i][j]初始化为false。 + +4. 确定遍历顺序 + +遍历顺序可有有点讲究了。 + +首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。 + +dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图: + +![647.回文子串](https://file1.kamacoder.com/i/algo/20210121171032473.jpg) + +如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。 + +**所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的**。 + +有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。 + +代码如下: + +```CPP +for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序 + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { // 情况三 + dp[i][j] = true; + } + } + if (dp[i][j] && j - i + 1 > maxlenth) { + maxlenth = j - i + 1; + left = i; + right = j; + } + } + +} +``` + +5. 举例推导dp数组 + +举例,输入:"aaa",dp[i][j]状态如下: + +![647.回文子串1](https://file1.kamacoder.com/i/algo/20210121171059951.jpg) + +**注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分**。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + string longestPalindrome(string s) { + vector> dp(s.size(), vector(s.size(), 0)); + int maxlenth = 0; + int left = 0; + int right = 0; + for (int i = s.size() - 1; i >= 0; i--) { + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { // 情况三 + dp[i][j] = true; + } + } + if (dp[i][j] && j - i + 1 > maxlenth) { + maxlenth = j - i + 1; + left = i; + right = j; + } + } + + } + return s.substr(left, right - left + 1); + } +}; +``` +以上代码是为了凸显情况一二三,当然是可以简洁一下的,如下: + +```CPP +class Solution { +public: + string longestPalindrome(string s) { + vector> dp(s.size(), vector(s.size(), 0)); + int maxlenth = 0; + int left = 0; + int right = 0; + for (int i = s.size() - 1; i >= 0; i--) { + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) { + dp[i][j] = true; + } + if (dp[i][j] && j - i + 1 > maxlenth) { + maxlenth = j - i + 1; + left = i; + right = j; + } + } + } + return s.substr(left, maxlenth); + } +}; + +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n^2) + +### 双指针 + +动态规划的空间复杂度是偏高的,我们再看一下双指针法。 + +首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。 + +**在遍历中心点的时候,要注意中心点有两种情况**。 + +一个元素可以作为中心点,两个元素也可以作为中心点。 + +那么有的同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。 + +所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。 + +**这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算**,代码如下: + +```CPP +class Solution { +public: + int left = 0; + int right = 0; + int maxLength = 0; + string longestPalindrome(string s) { + int result = 0; + for (int i = 0; i < s.size(); i++) { + extend(s, i, i, s.size()); // 以i为中心 + extend(s, i, i + 1, s.size()); // 以i和i+1为中心 + } + return s.substr(left, maxLength); + } + void extend(const string& s, int i, int j, int n) { + while (i >= 0 && j < n && s[i] == s[j]) { + if (j - i + 1 > maxLength) { + left = i; + right = j; + maxLength = j - i + 1; + } + i--; + j++; + } + } +}; + +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + +### Manacher 算法 + +Manacher 算法的关键在于高效利用回文的对称性,通过插入分隔符和维护中心、边界等信息,在线性时间内找到最长回文子串。这种方法避免了重复计算,是处理回文问题的最优解。 + +```c++ +//Manacher 算法 +class Solution { +public: + string longestPalindrome(string s) { + // 预处理字符串,在每个字符之间插入 '#' + string t = "#"; + for (char c : s) { + t += c; // 添加字符 + t += '#';// 添加分隔符 + } + int n = t.size();// 新字符串的长度 + vector p(n, 0);// p[i] 表示以 t[i] 为中心的回文半径 + int center = 0, right = 0;// 当前回文的中心和右边界 + + + // 遍历预处理后的字符串 + for (int i = 0; i < n; i++) { + // 如果当前索引在右边界内,利用对称性初始化 p[i] + if (i < right) { + p[i] = min(right - i, p[2 * center - i]); + } + // 尝试扩展回文 + while (i - p[i] - 1 >= 0 && i + p[i] + 1 < n && t[i - p[i] - 1] == t[i + p[i] + 1]) { + p[i]++;// 增加回文半径 + } + // 如果当前回文扩展超出右边界,更新中心和右边界 + if (i + p[i] > right) { + center = i;// 更新中心 + right = i + p[i];// 更新右边界 + } + } + // 找到最大回文半径和对应的中心 + int maxLen = 0, centerIndex = 0; + for (int i = 0; i < n; i++) { + if (p[i] > maxLen) { + maxLen = p[i];// 更新最大回文长度 + centerIndex = i;// 更新中心索引 + } + } + // 计算原字符串中回文子串的起始位置并返回 + return s.substr((centerIndex - maxLen) / 2, maxLen); + } +}; +``` + + + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +## 其他语言版本 + +### Java: + +```java +// 双指针 动态规划 +class Solution { + public String longestPalindrome(String s) { + if (s.length() == 0 || s.length() == 1) return s; + int length = 1; + int index = 0; + boolean[][] palindrome = new boolean[s.length()][s.length()]; + for (int i = 0; i < s.length(); i++) { + palindrome[i][i] = true; + } + + for (int L = 2; L <= s.length(); L++) { + for (int i = 0; i < s.length(); i++) { + int j = i + L - 1; + if (j >= s.length()) break; + if (s.charAt(i) != s.charAt(j)) { + palindrome[i][j] = false; + } else { + if (j - i < 3) { + palindrome[i][j] = true; + } else { + palindrome[i][j] = palindrome[i + 1][j - 1]; + } + } + if (palindrome[i][j] && j - i + 1 > length) { + length = j - i + 1; + index = i; + } + } + } + return s.substring(index, index + length); + } +} +``` + +```java +// 双指针 中心扩散法 +class Solution { + public String longestPalindrome(String s) { + String s1 = ""; + String s2 = ""; + String res = ""; + for (int i = 0; i < s.length(); i++) { + // 分两种情况:即一个元素作为中心点,两个元素作为中心点 + s1 = extend(s, i, i); // 情况1 + res = s1.length() > res.length() ? s1 : res; + s2 = extend(s, i, i + 1); // 情况2 + res = s2.length() > res.length() ? s2 : res; + } + return res; // 返回最长的 + } + public String extend(String s, int start, int end){ + String tmp = ""; + while (start >= 0 && end < s.length() && s.charAt(start) == s.charAt(end)){ + tmp = s.substring(start, end + 1); // Java中substring是左闭右开的,所以要+1 + // 向两边扩散 + start--; + end++; + } + return tmp; + } +} +``` + +### Python: + +```python +class Solution: + def longestPalindrome(self, s: str) -> str: + dp = [[False] * len(s) for _ in range(len(s))] + maxlenth = 0 + left = 0 + right = 0 + for i in range(len(s) - 1, -1, -1): + for j in range(i, len(s)): + if s[j] == s[i]: + if j - i <= 1 or dp[i + 1][j - 1]: + dp[i][j] = True + if dp[i][j] and j - i + 1 > maxlenth: + maxlenth = j - i + 1 + left = i + right = j + return s[left:right + 1] + +``` +双指针: + +```python +class Solution: + def longestPalindrome(self, s: str) -> str: + + def find_point(i, j, s): + while i >= 0 and j < len(s) and s[i] == s[j]: + i -= 1 + j += 1 + return i + 1, j + + def compare(start, end, left, right): + if right - left > end - start: + return left, right + else: + return start, end + + start = 0 + end = 0 + for i in range(len(s)): + left, right = find_point(i, i, s) + start, end = compare(start, end, left, right) + + left, right = find_point(i, i + 1, s) + start, end = compare(start, end, left, right) + return s[start:end] + +``` +### Go: + +```go +func longestPalindrome(s string) string { + maxLen := 0 + left := 0 + length := 0 + dp := make([][]bool, len(s)) + for i := 0; i < len(s); i++ { + dp[i] = make([]bool,len(s)) + } + for i := len(s)-1; i >= 0; i-- { + for j := i; j < len(s); j++ { + if s[i] == s[j]{ + if j-i <= 1{ // 情况一和情况二 + length = j-i + dp[i][j]=true + }else if dp[i+1][j-1]{ // 情况三 + length = j-i + dp[i][j] = true + } + } + } + if length > maxLen { + maxLen = length + left = i + } + } + return s[left: left+maxLen+1] +} + + +``` + +### JavaScript: + +```js +//动态规划解法 +var longestPalindrome = function(s) { + const len = s.length; + // 布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false + let dp = new Array(len).fill(false).map(() => new Array(len).fill(false)); + // left起始位置 maxlenth回文串长度 + let left = 0, maxlenth = 0; + for(let i = len - 1; i >= 0; i--){ + for(let j = i; j < len; j++){ + // 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串 j - i == 0 + // 情况二:下标i 与 j相差为1,例如aa,也是文子串 j - i == 1 + // 情况一和情况二 可以合并为 j - i <= 1 + // 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]===true + if(s[i] === s[j] && (j - i <= 1 || dp[i + 1][j - 1])){ + dp[i][j] = true; + } + // 只要 dp[i][j] == true 成立,就表示子串 s[i..j] 是回文,此时记录回文长度和起始位置 + if(dp[i][j] && j - i + 1 > maxlenth) { + maxlenth = j - i + 1; // 回文串长度 + left = i; // 起始位置 + } + } + } + return s.substr(left, maxlenth); // 找到子串 +}; + +//双指针 +var longestPalindrome = function(s) { + let left = 0, right = 0, maxLength = 0; + const extend = (s, i, j, n) => {// s为字符串 i,j为双指针 n为字符串长度 + while(i >= 0 && j < n && s[i] === s[j]){ + if(j - i + 1 > maxLength){ + left = i; // 更新开始位置 + right = j; // 更新结尾位置 + maxLength = j - i + 1; // 更新子串最大长度 + } + // 指针移动 + i--; + j++; + } + } + for(let i = 0; i < s.length; i++){ + extend(s, i, i, s.length); // 以i为中心 + extend(s, i, i + 1, s.length); // 以i和i+1为中心 + } + return s.substr(left, maxLength); +}; + +//Manacher算法 +var longestPalindrome = function(s) { + const len = s.length; + if(len < 2) return s; + let maxLength = 1, index = 0; + //Manacher算法,利用回文对称的性质,根据i在上一个回文中心的臂长里的位置去判断i的回文性 + //需要知道上一个回文中心,以及其臂长 + let center = 0; + //注意这里使用了maxRight的而不是真实的臂长length,因为之后需要判断i在臂长的什么位置 + //如果这里臂长用了length,之后还要 计算i - center 去和 length比较,太繁琐 + let maxRight = 0; + //考虑到回文串的长度是偶数的情况,所以这里预处理一下字符串,每个字符间插入特殊字符,把可能性都化为奇数 + //这个处理把回文串长度的可能性都化为了奇数 + //#c#b#b#a# + //#c#b#a#b#d# + let ss = ""; + for(let i = 0; i < s.length; i++){ + ss += "#"+s[i]; + } + ss += "#"; + //需要维护一个每个位置臂长的信息数组positionLength + const pl = new Array(ss.length).fill(0); + //这里需要注意参考的是i关于center对称的点i'的回文性 + //i' = 2*center - i; + //所以列下情况: + //1.i>maxRight,找不到i',无法参考,自己算自己的 + //2.i<=maxRight: + //2.1 i= pl[i‘],大多少需要尝试扩散 + //2.3 i>maxRight-pl[i'],pl[i']的臂长超过了center的臂长,根据对称性,i中心扩散到MaxRight处, + // s[2*i-maxRight] !== s[MaxRight]必不相等,所以pl[i] = maxRight-i; + //总结就是pl[i] = Math.min(maxRight-i,pl[i']);提示i= 0 && right maxRight){ + center = i; + maxRight = pl[i] + i; + } + if (pl[i] * 2 + 1 > maxLength){ + maxLength = pl[i]*2+1; + index = i - pl[i]; + } + } + return ss.substr(index, maxLength).replace(/#/g,""); +}; +``` + +### C: + +动态规划: +```c +//初始化dp数组,全部初始为false +bool **initDP(int strLen) { + bool **dp = (bool **)malloc(sizeof(bool *) * strLen); + int i, j; + for(i = 0; i < strLen; ++i) { + dp[i] = (bool *)malloc(sizeof(bool) * strLen); + for(j = 0; j < strLen; ++j) + dp[i][j] = false; + } + return dp; +} + +char * longestPalindrome(char * s){ + //求出字符串长度 + int strLen = strlen(s); + //初始化dp数组,元素初始化为false + bool **dp = initDP(strLen); + int maxLength = 0, left = 0, right = 0; + + //从下到上,从左到右遍历 + int i, j; + for(i = strLen - 1; i >= 0; --i) { + for(j = i; j < strLen; ++j) { + //若当前i与j所指字符一样 + if(s[i] == s[j]) { + //若i、j指向相邻字符或同一字符,则为回文字符串 + if(j - i <= 1) + dp[i][j] = true; + //若i+1与j-1所指字符串为回文字符串,则i、j所指字符串为回文字符串 + else if(dp[i + 1][j - 1]) + dp[i][j] = true; + } + //若新的字符串的长度大于之前的最大长度,进行更新 + if(dp[i][j] && j - i + 1 > maxLength) { + maxLength = j - i + 1; + left = i; + right = j; + } + } + } + //复制回文字符串,并返回 + char *ret = (char*)malloc(sizeof(char) * (maxLength + 1)); + memcpy(ret, s + left, maxLength); + ret[maxLength] = 0; + return ret; +} +``` + +双指针: +```c +int left, maxLength; +void extend(char *str, int i, int j, int size) { + while(i >= 0 && j < size && str[i] == str[j]) { + //若当前子字符串长度大于最长的字符串长度,进行更新 + if(j - i + 1 > maxLength) { + maxLength = j - i + 1; + left = i; + } + //左指针左移,右指针右移。扩大搜索范围 + ++j, --i; + } +} + +char * longestPalindrome(char * s){ + left = right = maxLength = 0; + int size = strlen(s); + + int i; + for(i = 0; i < size; ++i) { + //长度为单数的子字符串 + extend(s, i, i, size); + //长度为双数的子字符串 + extend(s, i, i + 1, size); + } + + //复制子字符串 + char *subStr = (char *)malloc(sizeof(char) * (maxLength + 1)); + memcpy(subStr, s + left, maxLength); + subStr[maxLength] = 0; + + return subStr; +} +``` + +### C#: + +動態規則: +```csharp +public class Solution { + + public string LongestPalindrome(string s) { + bool[,] dp = new bool[s.Length, s.Length]; + int maxlenth = 0; + int left = 0; + int right = 0; + for(int i = s.Length-1 ; i>=0; i--){ + for(int j = i; j maxlenth){ + maxlenth = j-i+1; + left = i; + right = j; + } + } + } + return s.Substring(left, maxlenth); + } +} +``` + +雙指針: +```csharp +public class Solution { + int maxlenth = 0; + int left = 0; + int right = 0; + + public string LongestPalindrome(string s) { + int result = 0; + for (int i = 0; i < s.Length; i++) { + extend(s, i, i, s.Length); // 以i為中心 + extend(s, i, i + 1, s.Length); // 以i和i+1為中心 + } + return s.Substring(left, maxlenth); + } + + private void extend(string s, int i, int j, int n) { + while (i >= 0 && j < n && s[i] == s[j]) { + if (j - i + 1 > maxlenth) { + left = i; + right = j; + maxlenth = j - i + 1; + } + i--; + j++; + } + } +} +``` + + + diff --git "a/problems/0015.\344\270\211\346\225\260\344\271\213\345\222\214.md" "b/problems/0015.\344\270\211\346\225\260\344\271\213\345\222\214.md" new file mode 100755 index 0000000000..e2cb3f4612 --- /dev/null +++ "b/problems/0015.\344\270\211\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,980 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 第15题. 三数之和 + +[力扣题目链接](https://leetcode.cn/problems/3sum/) + +给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。 + +**注意:** 答案中不可以包含重复的三元组。 + +示例: + +给定数组 nums = [-1, 0, 1, 2, -1, -4], + +满足要求的三元组集合为: +[ + [-1, 0, 1], + [-1, -1, 2] +] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[梦破碎的地方!| LeetCode:15.三数之和](https://www.bilibili.com/video/BV1GW4y127qo),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +**注意[0, 0, 0, 0] 这组数据** + +## 思路 + +### 哈希解法 + +两层for循环就可以确定 两个数值,可以使用哈希法来确定 第三个数 0-(a+b) 或者 0 - (a + c) 是否在 数组里出现过,其实这个思路是正确的,但是我们有一个非常棘手的问题,就是题目中说的不可以包含重复的三元组。 + +把符合条件的三元组放进vector中,然后再去重,这样是非常费时的,很容易超时,也是这道题目通过率如此之低的根源所在。 + +去重的过程不好处理,有很多小细节,如果在面试中很难想到位。 + +时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。 + +大家可以尝试使用哈希法写一写,就知道其困难的程度了。 + +哈希法C++代码: +```CPP +class Solution { +public: + // 在一个数组中找到3个数形成的三元组,它们的和为0,不能重复使用(三数下标互不相同),且三元组不能重复。 + // b(存储)== 0-(a+c)(检索) + vector> threeSum(vector& nums) { + vector> result; + sort(nums.begin(), nums.end()); + + for (int i = 0; i < nums.size(); i++) { + // 如果a是正数,a 0) + break; + + // [a, a, ...] 如果本轮a和上轮a相同,那么找到的b,c也是相同的,所以去重a + if (i > 0 && nums[i] == nums[i - 1]) + continue; + + // 这个set的作用是存储b + unordered_set set; + + for (int k = i + 1; k < nums.size(); k++) { + // 去重b=c时的b和c + if (k > i + 2 && nums[k] == nums[k - 1] && nums[k - 1] == nums[k - 2]) + continue; + + // a+b+c=0 <=> b=0-(a+c) + int target = 0 - (nums[i] + nums[k]); + if (set.find(target) != set.end()) { + result.push_back({nums[i], target, nums[k]}); // nums[k]成为c + set.erase(target); + } + else { + set.insert(nums[k]); // nums[k]成为b + } + } + } + + return result; + } +}; +``` + +* 时间复杂度: O(n^2) +* 空间复杂度: O(n),额外的 set 开销 + + +### 双指针 + +**其实这道题目使用哈希法并不十分合适**,因为在去重的操作中有很多细节需要注意,在面试中很难直接写出没有bug的代码。 + +而且使用哈希法 在使用两层for循环的时候,能做的剪枝操作很有限,虽然时间复杂度是O(n^2),也是可以在leetcode上通过,但是程序的执行时间依然比较长 。 + +接下来我来介绍另一个解法:双指针法,**这道题目使用双指针法 要比哈希法高效一些**,那么来讲解一下具体实现的思路。 + +动画效果如下: + +![15.三数之和](https://file1.kamacoder.com/i/algo/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.gif) + +拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下标0的地方开始,同时定一个下标left 定义在i+1的位置上,定义下标right 在数组结尾的位置上。 + +依然还是在数组中找到 abc 使得a + b +c =0,我们这里相当于 a = nums[i],b = nums[left],c = nums[right]。 + +接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下标就应该向左移动,这样才能让三数之和小一些。 + +如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。 + +时间复杂度:O(n^2)。 + +C++代码代码如下: + +```CPP +class Solution { +public: + vector> threeSum(vector& nums) { + vector> result; + sort(nums.begin(), nums.end()); + // 找出a + b + c = 0 + // a = nums[i], b = nums[left], c = nums[right] + for (int i = 0; i < nums.size(); i++) { + // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了 + if (nums[i] > 0) { + return result; + } + // 错误去重a方法,将会漏掉-1,-1,2 这种情况 + /* + if (nums[i] == nums[i + 1]) { + continue; + } + */ + // 正确去重a方法 + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + int left = i + 1; + int right = nums.size() - 1; + while (right > left) { + // 去重复逻辑如果放在这里,0,0,0 的情况,可能直接导致 right<=left 了,从而漏掉了 0,0,0 这种三元组 + /* + while (right > left && nums[right] == nums[right - 1]) right--; + while (right > left && nums[left] == nums[left + 1]) left++; + */ + if (nums[i] + nums[left] + nums[right] > 0) right--; + else if (nums[i] + nums[left] + nums[right] < 0) left++; + else { + result.push_back(vector{nums[i], nums[left], nums[right]}); + // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重 + while (right > left && nums[right] == nums[right - 1]) right--; + while (right > left && nums[left] == nums[left + 1]) left++; + + // 找到答案时,双指针同时收缩 + right--; + left++; + } + } + + } + return result; + } +}; +``` + +* 时间复杂度: O(n^2) +* 空间复杂度: O(1) + + +### 去重逻辑的思考 + +#### a的去重 + +说到去重,其实主要考虑三个数的去重。 a, b ,c, 对应的就是 nums[i],nums[left],nums[right] + +a 如果重复了怎么办,a是nums里遍历的元素,那么应该直接跳过去。 + +但这里有一个问题,是判断 nums[i] 与 nums[i + 1]是否相同,还是判断 nums[i] 与 nums[i-1] 是否相同。 + +有同学可能想,这不都一样吗。 + +其实不一样! + +都是和 nums[i]进行比较,是比较它的前一个,还是比较它的后一个。 + +如果我们的写法是 这样: + +```C++ +if (nums[i] == nums[i + 1]) { // 去重操作 + continue; +} +``` + +那我们就把 三元组中出现重复元素的情况直接pass掉了。 例如{-1, -1 ,2} 这组数据,当遍历到第一个-1 的时候,判断 下一个也是-1,那这组数据就pass了。 + +**我们要做的是 不能有重复的三元组,但三元组内的元素是可以重复的!** + +所以这里是有两个重复的维度。 + +那么应该这么写: + +```C++ +if (i > 0 && nums[i] == nums[i - 1]) { + continue; +} +``` + +这么写就是当前使用 nums[i],我们判断前一位是不是一样的元素,在看 {-1, -1 ,2} 这组数据,当遍历到 第一个 -1 的时候,只要前一位没有-1,那么 {-1, -1 ,2} 这组数据一样可以收录到 结果集里。 + +这是一个非常细节的思考过程。 + +#### b与c的去重 + +很多同学写本题的时候,去重的逻辑多加了 对right 和left 的去重:(代码中注释部分) + +```C++ +while (right > left) { + if (nums[i] + nums[left] + nums[right] > 0) { + right--; + // 去重 right + while (left < right && nums[right] == nums[right + 1]) right--; + } else if (nums[i] + nums[left] + nums[right] < 0) { + left++; + // 去重 left + while (left < right && nums[left] == nums[left - 1]) left++; + } else { + } +} +``` + +但细想一下,这种去重其实对提升程序运行效率是没有帮助的。 + +拿right去重为例,即使不加这个去重逻辑,依然根据 `while (right > left) ` 和 `if (nums[i] + nums[left] + nums[right] > 0)` 去完成right-- 的操作。 + +多加了 ` while (left < right && nums[right] == nums[right + 1]) right--;` 这一行代码,其实就是把 需要执行的逻辑提前执行了,但并没有减少 判断的逻辑。 + +最直白的思考过程,就是right还是一个数一个数的减下去的,所以在哪里减的都是一样的。 + +所以这种去重 是可以不加的。 仅仅是 把去重的逻辑提前了而已。 + + +## 思考题 + + +既然三数之和可以使用双指针法,我们之前讲过的[1.两数之和](https://programmercarl.com/0001.两数之和.html),可不可以使用双指针法呢? + +如果不能,题意如何更改就可以使用双指针法呢? **大家留言说出自己的想法吧!** + +两数之和 就不能使用双指针法,因为[1.两数之和](https://programmercarl.com/0001.两数之和.html)要求返回的是索引下标, 而双指针法一定要排序,一旦排序之后原数组的索引就被改变了。 + +如果[1.两数之和](https://programmercarl.com/0001.两数之和.html)要求返回的是数值的话,就可以使用双指针法了。 + + + + +## 其他语言版本 + +### Java: +(版本一) 双指针 +```Java +class Solution { + public List> threeSum(int[] nums) { + List> result = new ArrayList<>(); + Arrays.sort(nums); + // 找出a + b + c = 0 + // a = nums[i], b = nums[left], c = nums[right] + for (int i = 0; i < nums.length; i++) { + // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了 + if (nums[i] > 0) { + return result; + } + + if (i > 0 && nums[i] == nums[i - 1]) { // 去重a + continue; + } + + int left = i + 1; + int right = nums.length - 1; + while (right > left) { + int sum = nums[i] + nums[left] + nums[right]; + if (sum > 0) { + right--; + } else if (sum < 0) { + left++; + } else { + result.add(Arrays.asList(nums[i], nums[left], nums[right])); + // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重 + while (right > left && nums[right] == nums[right - 1]) right--; + while (right > left && nums[left] == nums[left + 1]) left++; + + right--; + left++; + } + } + } + return result; + } +} +``` +(版本二) 使用哈希集合 +```Java +class Solution { + public List> threeSum(int[] nums) { + List> result = new ArrayList<>(); + Arrays.sort(nums); + + for (int i = 0; i < nums.length; i++) { + // 如果第一个元素大于零,不可能凑成三元组 + if (nums[i] > 0) { + return result; + } + // 三元组元素a去重 + if (i > 0 && nums[i] == nums[i - 1]) { + continue; + } + + HashSet set = new HashSet<>(); + for (int j = i + 1; j < nums.length; j++) { + // 三元组元素b去重 + if (j > i + 2 && nums[j] == nums[j - 1] && nums[j - 1] == nums[j - 2]) { + continue; + } + + int c = -nums[i] - nums[j]; + if (set.contains(c)) { + result.add(Arrays.asList(nums[i], nums[j], c)); + set.remove(c); // 三元组元素c去重 + } else { + set.add(nums[j]); + } + } + } + return result; + } +} +``` +### Python: +(版本一) 双指针 + +```Python +class Solution: + def threeSum(self, nums: List[int]) -> List[List[int]]: + result = [] + nums.sort() + + for i in range(len(nums)): + # 如果第一个元素已经大于0,不需要进一步检查 + if nums[i] > 0: + return result + + # 跳过相同的元素以避免重复 + if i > 0 and nums[i] == nums[i - 1]: + continue + + left = i + 1 + right = len(nums) - 1 + + while right > left: + sum_ = nums[i] + nums[left] + nums[right] + + if sum_ < 0: + left += 1 + elif sum_ > 0: + right -= 1 + else: + result.append([nums[i], nums[left], nums[right]]) + + # 跳过相同的元素以避免重复 + while right > left and nums[right] == nums[right - 1]: + right -= 1 + while right > left and nums[left] == nums[left + 1]: + left += 1 + + right -= 1 + left += 1 + + return result +``` +(版本二) 使用字典 + +```python +class Solution: + def threeSum(self, nums: List[int]) -> List[List[int]]: + result = [] + nums.sort() + # 找出a + b + c = 0 + # a = nums[i], b = nums[j], c = -(a + b) + for i in range(len(nums)): + # 排序之后如果第一个元素已经大于零,那么不可能凑成三元组 + if nums[i] > 0: + break + if i > 0 and nums[i] == nums[i - 1]: #三元组元素a去重 + continue + d = {} + for j in range(i + 1, len(nums)): + if j > i + 2 and nums[j] == nums[j-1] == nums[j-2]: # 三元组元素b去重 + continue + c = 0 - (nums[i] + nums[j]) + if c in d: + result.append([nums[i], nums[j], c]) + d.pop(c) # 三元组元素c去重 + else: + d[nums[j]] = j + return result +``` + +### Go: +(版本一) 双指针 + +```Go +func threeSum(nums []int) [][]int { + sort.Ints(nums) + res := [][]int{} + // 找出a + b + c = 0 + // a = nums[i], b = nums[left], c = nums[right] + for i := 0; i < len(nums)-2; i++ { + // 排序之后如果第一个元素已经大于零,那么无论如何组合都不可能凑成三元组,直接返回结果就可以了 + n1 := nums[i] + if n1 > 0 { + break + } + // 去重a + if i > 0 && n1 == nums[i-1] { + continue + } + l, r := i+1, len(nums)-1 + for l < r { + n2, n3 := nums[l], nums[r] + if n1+n2+n3 == 0 { + res = append(res, []int{n1, n2, n3}) + // 去重逻辑应该放在找到一个三元组之后,对b 和 c去重 + for l < r && nums[l] == n2 { + l++ + } + for l < r && nums[r] == n3 { + r-- + } + } else if n1+n2+n3 < 0 { + l++ + } else { + r-- + } + } + } + return res +} +``` +(版本二) 哈希解法 + +```Go +func threeSum(nums []int) [][]int { + res := make([][]int, 0) + sort.Ints(nums) + // 找出a + b + c = 0 + // a = nums[i], b = nums[j], c = -(a + b) + for i := 0; i < len(nums); i++ { + // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组 + if nums[i] > 0 { + break + } + // 三元组元素a去重 + if i > 0 && nums[i] == nums[i-1] { + continue + } + set := make(map[int]struct{}) + for j := i + 1; j < len(nums); j++ { + // 三元组元素b去重 + if j > i + 2 && nums[j] == nums[j-1] && nums[j-1] == nums[j-2] { + continue + } + c := -nums[i] - nums[j] + if _, ok := set[c]; ok { + res = append(res, []int{nums[i], nums[j], c}) + // 三元组元素c去重 + delete(set, c) + } else { + set[nums[j]] = struct{}{} + } + } + } + return res +} +``` + +### JavaScript: + +```js +var threeSum = function(nums) { + const res = [], len = nums.length + // 将数组排序 + nums.sort((a, b) => a - b) + for (let i = 0; i < len; i++) { + let l = i + 1, r = len - 1, iNum = nums[i] + // 数组排过序,如果第一个数大于0直接返回res + if (iNum > 0) return res + // 去重 + if (iNum == nums[i - 1]) continue + while(l < r) { + let lNum = nums[l], rNum = nums[r], threeSum = iNum + lNum + rNum + // 三数之和小于0,则左指针向右移动 + if (threeSum < 0) l++ + else if (threeSum > 0) r-- + else { + res.push([iNum, lNum, rNum]) + // 去重 + while(l < r && nums[l] == nums[l + 1]){ + l++ + } + while(l < r && nums[r] == nums[r - 1]) { + r-- + } + l++ + r-- + } + } + } + return res +}; +``` + +解法二:nSum通用解法。递归 + +```js +/** + * nsum通用解法,支持2sum,3sum,4sum...等等 + * 时间复杂度分析: + * 1. n = 2时,时间复杂度O(NlogN),排序所消耗的时间。、 + * 2. n > 2时,时间复杂度为O(N^n-1),即N的n-1次方,至少是2次方,此时可省略排序所消耗的时间。举例:3sum为O(n^2),4sum为O(n^3) + * @param {number[]} nums + * @return {number[][]} + */ +var threeSum = function (nums) { + // nsum通用解法核心方法 + function nSumTarget(nums, n, start, target) { + // 前提:nums要先排序好 + let res = []; + if (n === 2) { + res = towSumTarget(nums, start, target); + } else { + for (let i = start; i < nums.length; i++) { + // 递归求(n - 1)sum + let subRes = nSumTarget( + nums, + n - 1, + i + 1, + target - nums[i] + ); + for (let j = 0; j < subRes.length; j++) { + res.push([nums[i], ...subRes[j]]); + } + // 跳过相同元素 + while (nums[i] === nums[i + 1]) i++; + } + } + return res; + } + + function towSumTarget(nums, start, target) { + // 前提:nums要先排序好 + let res = []; + let len = nums.length; + let left = start; + let right = len - 1; + while (left < right) { + let sum = nums[left] + nums[right]; + if (sum < target) { + while (nums[left] === nums[left + 1]) left++; + left++; + } else if (sum > target) { + while (nums[right] === nums[right - 1]) right--; + right--; + } else { + // 相等 + res.push([nums[left], nums[right]]); + // 跳过相同元素 + while (nums[left] === nums[left + 1]) left++; + while (nums[right] === nums[right - 1]) right--; + left++; + right--; + } + } + return res; + } + nums.sort((a, b) => a - b); + // n = 3,此时求3sum之和 + return nSumTarget(nums, 3, 0, 0); +}; +``` + +### TypeScript: + +```typescript +function threeSum(nums: number[]): number[][] { + nums.sort((a, b) => a - b); + let length = nums.length; + let left: number = 0, + right: number = length - 1; + let resArr: number[][] = []; + for (let i = 0; i < length; i++) { + if (nums[i]>0) { + return resArr; //nums经过排序后,只要nums[i]>0, 此后的nums[i] + nums[left] + nums[right]均大于0,可以提前终止循环。 + } + if (i > 0 && nums[i] === nums[i - 1]) { + continue; + } + left = i + 1; + right = length - 1; + while (left < right) { + let total: number = nums[i] + nums[left] + nums[right]; + if (total === 0) { + resArr.push([nums[i], nums[left], nums[right]]); + left++; + right--; + while (nums[right] === nums[right + 1]) { + right--; + } + while (nums[left] === nums[left - 1]) { + left++; + } + } else if (total < 0) { + left++; + } else { + right--; + } + } + } + return resArr; +}; +``` + +### Ruby: + +```ruby +def is_valid(strs) + symbol_map = {')' => '(', '}' => '{', ']' => '['} + stack = [] + strs.size.times {|i| + c = strs[i] + if symbol_map.has_key?(c) + top_e = stack.shift + return false if symbol_map[c] != top_e + else + stack.unshift(c) + end + } + stack.empty? +end +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer[] $nums + * @return Integer[][] + */ + function threeSum($nums) { + $res = []; + sort($nums); + for ($i = 0; $i < count($nums); $i++) { + if ($nums[$i] > 0) { + return $res; + } + if ($i > 0 && $nums[$i] == $nums[$i - 1]) { + continue; + } + $left = $i + 1; + $right = count($nums) - 1; + while ($left < $right) { + $sum = $nums[$i] + $nums[$left] + $nums[$right]; + if ($sum < 0) { + $left++; + } + else if ($sum > 0) { + $right--; + } + else { + $res[] = [$nums[$i], $nums[$left], $nums[$right]]; + while ($left < $right && $nums[$left] == $nums[$left + 1]) $left++; + while ($left < $right && $nums[$right] == $nums[$right - 1]) $right--; + $left++; + $right--; + } + } + } + return $res; + } +} +``` + +### Swift: + +```swift +// 双指针法 +func threeSum(_ nums: [Int]) -> [[Int]] { + var res = [[Int]]() + var sorted = nums + sorted.sort() + for i in 0 ..< sorted.count { + if sorted[i] > 0 { + return res + } + if i > 0 && sorted[i] == sorted[i - 1] { + continue + } + var left = i + 1 + var right = sorted.count - 1 + while left < right { + let sum = sorted[i] + sorted[left] + sorted[right] + if sum < 0 { + left += 1 + } else if sum > 0 { + right -= 1 + } else { + res.append([sorted[i], sorted[left], sorted[right]]) + + while left < right && sorted[left] == sorted[left + 1] { + left += 1 + } + while left < right && sorted[right] == sorted[right - 1] { + right -= 1 + } + + left += 1 + right -= 1 + } + } + } + return res +} +``` + +### Rust: + +```Rust +// 哈希解法 +use std::collections::HashSet; +impl Solution { + pub fn three_sum(nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut nums = nums; + nums.sort(); + let len = nums.len(); + for i in 0..len { + if nums[i] > 0 { break; } + if i > 0 && nums[i] == nums[i - 1] { continue; } + let mut set = HashSet::new(); + for j in (i + 1)..len { + if j > i + 2 && nums[j] == nums[j - 1] && nums[j] == nums[j - 2] { continue; } + let c = 0 - (nums[i] + nums[j]); + if set.contains(&c) { + result.push(vec![nums[i], nums[j], c]); + set.remove(&c); + } else { set.insert(nums[j]); } + } + } + result + } +} +``` + +```Rust +// 双指针法 +use std::cmp::Ordering; +impl Solution { + pub fn three_sum(nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut nums = nums; + nums.sort(); + let len = nums.len(); + for i in 0..len { + if nums[i] > 0 { return result; } + if i > 0 && nums[i] == nums[i - 1] { continue; } + let (mut left, mut right) = (i + 1, len - 1); + while left < right { + match (nums[i] + nums[left] + nums[right]).cmp(&0){ + Ordering::Equal =>{ + result.push(vec![nums[i], nums[left], nums[right]]); + left +=1; + right -=1; + while left < right && nums[left] == nums[left - 1]{ + left += 1; + } + while left < right && nums[right] == nums[right+1]{ + right -= 1; + } + } + Ordering::Greater => right -= 1, + Ordering::Less => left += 1, + } + } + } + result + } +} +``` + +### C: + +```C +//qsort辅助cmp函数 +int cmp(const void* ptr1, const void* ptr2) { + return *((int*)ptr1) > *((int*)ptr2); +} + +int** threeSum(int* nums, int numsSize, int* returnSize, int** returnColumnSizes) { + //开辟ans数组空间 + int **ans = (int**)malloc(sizeof(int*) * 18000); + int ansTop = 0; + //若传入nums数组大小小于3,则需要返回数组大小为0 + if(numsSize < 3) { + *returnSize = 0; + return ans; + } + //对nums数组进行排序 + qsort(nums, numsSize, sizeof(int), cmp); + + + int i; + //用for循环遍历数组,结束条件为i < numsSize - 2(因为要预留左右指针的位置) + for(i = 0; i < numsSize - 2; i++) { + //若当前i指向元素>0,则代表left和right以及i的和大于0。直接break + if(nums[i] > 0) + break; + //去重:i > 0 && nums[i] == nums[i-1] + if(i > 0 && nums[i] == nums[i-1]) + continue; + //定义左指针和右指针 + int left = i + 1; + int right = numsSize - 1; + //当右指针比左指针大时进行循环 + while(right > left) { + //求出三数之和 + int sum = nums[right] + nums[left] + nums[i]; + //若和小于0,则左指针+1(因为左指针右边的数比当前所指元素大) + if(sum < 0) + left++; + //若和大于0,则将右指针-1 + else if(sum > 0) + right--; + //若和等于0 + else { + //开辟一个大小为3的数组空间,存入nums[i], nums[left]和nums[right] + int* arr = (int*)malloc(sizeof(int) * 3); + arr[0] = nums[i]; + arr[1] = nums[left]; + arr[2] = nums[right]; + //将开辟数组存入ans中 + ans[ansTop++] = arr; + //去重 + while(right > left && nums[right] == nums[right - 1]) + right--; + while(left < right && nums[left] == nums[left + 1]) + left++; + //更新左右指针 + left++; + right--; + } + } + } + + //设定返回的数组大小 + *returnSize = ansTop; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int z; + for(z = 0; z < ansTop; z++) { + (*returnColumnSizes)[z] = 3; + } + return ans; +} +``` + +### C#: + +```csharp +public class Solution +{ + public IList> ThreeSum(int[] nums) + { + var result = new List>(); + + Array.Sort(nums); + + for (int i = 0; i < nums.Length - 2; i++) + { + int n1 = nums[i]; + + if (n1 > 0) + break; + + if (i > 0 && n1 == nums[i - 1]) + continue; + + int left = i + 1; + int right = nums.Length - 1; + + while (left < right) + { + int n2 = nums[left]; + int n3 = nums[right]; + int sum = n1 + n2 + n3; + + if (sum > 0) + { + right--; + } + else if (sum < 0) + { + left++; + } + else + { + result.Add(new List { n1, n2, n3 }); + + while (left < right && nums[left] == n2) + { + left++; + } + + while (left < right && nums[right] == n3) + { + right--; + } + } + } + } + + return result; + } +} +``` +### Scala: + +```scala +object Solution { + // 导包 + import scala.collection.mutable.ListBuffer + import scala.util.control.Breaks.{break, breakable} + + def threeSum(nums: Array[Int]): List[List[Int]] = { + // 定义结果集,最后需要转换为List + val res = ListBuffer[List[Int]]() + val nums_tmp = nums.sorted // 对nums进行排序 + for (i <- nums_tmp.indices) { + // 如果要排的第一个数字大于0,直接返回结果 + if (nums_tmp(i) > 0) { + return res.toList + } + // 如果i大于0并且和前一个数字重复,则跳过本次循环,相当于continue + breakable { + if (i > 0 && nums_tmp(i) == nums_tmp(i - 1)) { + break + } else { + var left = i + 1 + var right = nums_tmp.length - 1 + while (left < right) { + var sum = nums_tmp(i) + nums_tmp(left) + nums_tmp(right) // 求三数之和 + if (sum < 0) left += 1 + else if (sum > 0) right -= 1 + else { + res += List(nums_tmp(i), nums_tmp(left), nums_tmp(right)) // 如果等于0 添加进结果集 + // 为了避免重复,对left和right进行移动 + while (left < right && nums_tmp(left) == nums_tmp(left + 1)) left += 1 + while (left < right && nums_tmp(right) == nums_tmp(right - 1)) right -= 1 + left += 1 + right -= 1 + } + } + } + } + } + // 最终返回需要转换为List,return关键字可以省略 + res.toList + } +} +``` + diff --git "a/problems/0017.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" "b/problems/0017.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" new file mode 100755 index 0000000000..6dcf9ee690 --- /dev/null +++ "b/problems/0017.\347\224\265\350\257\235\345\217\267\347\240\201\347\232\204\345\255\227\346\257\215\347\273\204\345\220\210.md" @@ -0,0 +1,766 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 17.电话号码的字母组合 + +[力扣题目链接](https://leetcode.cn/problems/letter-combinations-of-a-phone-number/) + +给定一个仅包含数字 2-9 的字符串,返回所有它能表示的字母组合。 + +给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 + +![17.电话号码的字母组合](https://file1.kamacoder.com/i/algo/2020102916424043.png) + +示例: +* 输入:"23" +* 输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. + +说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)::[还得用回溯算法!| LeetCode:17.电话号码的字母组合](https://www.bilibili.com/video/BV1yV4y1V7Ug),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +从示例上来说,输入"23",最直接的想法就是两层for循环遍历了吧,正好把组合的情况都输出了。 + +如果输入"233"呢,那么就三层for循环,如果"2333"呢,就四层for循环....... + +大家应该感觉出和[77.组合](https://programmercarl.com/0077.组合.html)遇到的一样的问题,就是这for循环的层数如何写出来,此时又是回溯法登场的时候了。 + +理解本题后,要解决如下三个问题: + +1. 数字和字母如何映射 +2. 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来 +3. 输入1 * #按键等等异常情况 + +### 数字和字母如何映射 + +可以使用map或者定义一个二维数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下: + +```cpp +const string letterMap[10] = { + "", // 0 + "", // 1 + "abc", // 2 + "def", // 3 + "ghi", // 4 + "jkl", // 5 + "mno", // 6 + "pqrs", // 7 + "tuv", // 8 + "wxyz", // 9 +}; +``` + +### 回溯法来解决n个for循环的问题 + +对于回溯法还不了解的同学看这篇:[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html) + + +例如:输入:"23",抽象为树形结构,如图所示: + +![17. 电话号码的字母组合](https://file1.kamacoder.com/i/algo/20201123200304469.png) + +图中可以看出遍历的深度,就是输入"23"的长度,而叶子节点就是我们要收集的结果,输出["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]。 + +回溯三部曲: + +* 确定回溯函数参数 + +首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量我依然定义为全局。 + +再来看参数,参数指定是有题目中给的string digits,然后还要有一个参数就是int型的index。 + +注意这个index可不是 [77.组合](https://programmercarl.com/0077.组合.html)和[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)中的startIndex了。 + +这个index是记录遍历第几个数字了,就是用来遍历digits的(题目中给出数字字符串),同时index也表示树的深度。 + +代码如下: + +```cpp +vector result; +string s; +void backtracking(const string& digits, int index) +``` + +* 确定终止条件 + +例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。 + +那么终止条件就是如果index 等于 输入的数字个数(digits.size)了(本来index就是用来遍历digits的)。 + +然后收集结果,结束本层递归。 + +代码如下: + +```cpp +if (index == digits.size()) { + result.push_back(s); + return; +} +``` + +* 确定单层遍历逻辑 + +首先要取index指向的数字,并找到对应的字符集(手机键盘的字符集)。 + +然后for循环来处理这个字符集,代码如下: + +```CPP +int digit = digits[index] - '0'; // 将index指向的数字转为int +string letters = letterMap[digit]; // 取数字对应的字符集 +for (int i = 0; i < letters.size(); i++) { + s.push_back(letters[i]); // 处理 + backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了 + s.pop_back(); // 回溯 +} +``` + +**注意这里for循环,可不像是在[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)和[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中从startIndex开始遍历的**。 + +**因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而[77. 组合](https://programmercarl.com/0077.组合.html)和[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)都是求同一个集合中的组合!** + + +注意:输入1 * #按键等等异常情况 + +代码中最好考虑这些异常情况,但题目的测试数据中应该没有异常情况的数据,所以我就没有加了。 + +**但是要知道会有这些异常,如果是现场面试中,一定要考虑到!** + +关键地方都讲完了,按照[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中的回溯法模板,不难写出如下C++代码: + + +```CPP +// 版本一 +class Solution { +private: + const string letterMap[10] = { + "", // 0 + "", // 1 + "abc", // 2 + "def", // 3 + "ghi", // 4 + "jkl", // 5 + "mno", // 6 + "pqrs", // 7 + "tuv", // 8 + "wxyz", // 9 + }; +public: + vector result; + string s; + void backtracking(const string& digits, int index) { + if (index == digits.size()) { + result.push_back(s); + return; + } + int digit = digits[index] - '0'; // 将index指向的数字转为int + string letters = letterMap[digit]; // 取数字对应的字符集 + for (int i = 0; i < letters.size(); i++) { + s.push_back(letters[i]); // 处理 + backtracking(digits, index + 1); // 递归,注意index+1,一下层要处理下一个数字了 + s.pop_back(); // 回溯 + } + } + vector letterCombinations(string digits) { + s.clear(); + result.clear(); + if (digits.size() == 0) { + return result; + } + backtracking(digits, 0); + return result; + } +}; +``` +* 时间复杂度: O(3^m * 4^n),其中 m 是对应三个字母的数字个数,n 是对应四个字母的数字个数 +* 空间复杂度: O(3^m * 4^n) + +一些写法,是把回溯的过程放在递归函数里了,例如如下代码,我可以写成这样:(注意注释中不一样的地方) + +```CPP +// 版本二 +class Solution { +private: + const string letterMap[10] = { + "", // 0 + "", // 1 + "abc", // 2 + "def", // 3 + "ghi", // 4 + "jkl", // 5 + "mno", // 6 + "pqrs", // 7 + "tuv", // 8 + "wxyz", // 9 + }; +public: + vector result; + void getCombinations(const string& digits, int index, const string& s) { // 注意参数的不同 + if (index == digits.size()) { + result.push_back(s); + return; + } + int digit = digits[index] - '0'; + string letters = letterMap[digit]; + for (int i = 0; i < letters.size(); i++) { + getCombinations(digits, index + 1, s + letters[i]); // 注意这里的不同 + } + } + vector letterCombinations(string digits) { + result.clear(); + if (digits.size() == 0) { + return result; + } + getCombinations(digits, 0, ""); + return result; + + } +}; +``` + +我不建议把回溯藏在递归的参数里这种写法,很不直观,我在[二叉树:以为使用了递归,其实还隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html)这篇文章中也深度分析了,回溯隐藏在了哪里。 + +所以大家可以按照版本一来写就可以了。 + +## 总结 + +本篇将题目的三个要点一一列出,并重点强调了和前面讲解过的[77. 组合](https://programmercarl.com/0077.组合.html)和[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)的区别,本题是多个集合求组合,所以在回溯的搜索过程中,都有一些细节需要注意的。 + +其实本题不算难,但也处处是细节,大家还要自己亲自动手写一写。 + + + +## 其他语言版本 + + +### Java +```Java +class Solution { + + //设置全局列表存储最后的结果 + List list = new ArrayList<>(); + + public List letterCombinations(String digits) { + if (digits == null || digits.length() == 0) { + return list; + } + //初始对应所有的数字,为了直接对应2-9,新增了两个无效的字符串"" + String[] numString = {"", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"}; + //迭代处理 + backTracking(digits, numString, 0); + return list; + + } + + //每次迭代获取一个字符串,所以会涉及大量的字符串拼接,所以这里选择更为高效的 StringBuilder + StringBuilder temp = new StringBuilder(); + + //比如digits如果为"23",num 为0,则str表示2对应的 abc + public void backTracking(String digits, String[] numString, int num) { + //遍历全部一次记录一次得到的字符串 + if (num == digits.length()) { + list.add(temp.toString()); + return; + } + //str 表示当前num对应的字符串 + String str = numString[digits.charAt(num) - '0']; + for (int i = 0; i < str.length(); i++) { + temp.append(str.charAt(i)); + //递归,处理下一层 + backTracking(digits, numString, num + 1); + //剔除末尾的继续尝试 + temp.deleteCharAt(temp.length() - 1); + } + } +} +``` + +### Python +回溯 +```python +class Solution: + def __init__(self): + self.letterMap = [ + "", # 0 + "", # 1 + "abc", # 2 + "def", # 3 + "ghi", # 4 + "jkl", # 5 + "mno", # 6 + "pqrs", # 7 + "tuv", # 8 + "wxyz" # 9 + ] + self.result = [] + self.s = "" + + def backtracking(self, digits, index): + if index == len(digits): + self.result.append(self.s) + return + digit = int(digits[index]) # 将索引处的数字转换为整数 + letters = self.letterMap[digit] # 获取对应的字符集 + for i in range(len(letters)): + self.s += letters[i] # 处理字符 + self.backtracking(digits, index + 1) # 递归调用,注意索引加1,处理下一个数字 + self.s = self.s[:-1] # 回溯,删除最后添加的字符 + + def letterCombinations(self, digits): + if len(digits) == 0: + return self.result + self.backtracking(digits, 0) + return self.result + +``` +回溯精简(版本一) +```python +class Solution: + def __init__(self): + self.letterMap = [ + "", # 0 + "", # 1 + "abc", # 2 + "def", # 3 + "ghi", # 4 + "jkl", # 5 + "mno", # 6 + "pqrs", # 7 + "tuv", # 8 + "wxyz" # 9 + ] + self.result = [] + + def getCombinations(self, digits, index, s): + if index == len(digits): + self.result.append(s) + return + digit = int(digits[index]) + letters = self.letterMap[digit] + for letter in letters: + self.getCombinations(digits, index + 1, s + letter) + + def letterCombinations(self, digits): + if len(digits) == 0: + return self.result + self.getCombinations(digits, 0, "") + return self.result + +``` +回溯精简(版本二) +```python +class Solution: + def __init__(self): + self.letterMap = [ + "", # 0 + "", # 1 + "abc", # 2 + "def", # 3 + "ghi", # 4 + "jkl", # 5 + "mno", # 6 + "pqrs", # 7 + "tuv", # 8 + "wxyz" # 9 + ] + + def getCombinations(self, digits, index, s, result): + if index == len(digits): + result.append(s) + return + digit = int(digits[index]) + letters = self.letterMap[digit] + for letter in letters: + self.getCombinations(digits, index + 1, s + letter, result) + + def letterCombinations(self, digits): + result = [] + if len(digits) == 0: + return result + self.getCombinations(digits, 0, "", result) + return result + + +``` + +回溯优化使用列表 +```python +class Solution: + def __init__(self): + self.letterMap = [ + "", # 0 + "", # 1 + "abc", # 2 + "def", # 3 + "ghi", # 4 + "jkl", # 5 + "mno", # 6 + "pqrs", # 7 + "tuv", # 8 + "wxyz" # 9 + ] + + def getCombinations(self, digits, index, path, result): + if index == len(digits): + result.append(''.join(path)) + return + digit = int(digits[index]) + letters = self.letterMap[digit] + for letter in letters: + path.append(letter) + self.getCombinations(digits, index + 1, path, result) + path.pop() + + def letterCombinations(self, digits): + result = [] + if len(digits) == 0: + return result + self.getCombinations(digits, 0, [], result) + return result + + + +``` + + + +### Go + +主要在于递归中传递下一个数字 + +```go +var ( + m []string + path []byte + res []string +) +func letterCombinations(digits string) []string { + m = []string{"abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz"} + path, res = make([]byte, 0), make([]string, 0) + if digits == "" { + return res + } + dfs(digits, 0) + return res +} +func dfs(digits string, start int) { + if len(path) == len(digits) { //终止条件,字符串长度等于digits的长度 + tmp := string(path) + res = append(res, tmp) + return + } + digit := int(digits[start] - '0') // 将index指向的数字转为int(确定下一个数字) + str := m[digit-2] // 取数字对应的字符集(注意和map中的对应) + for j := 0; j < len(str); j++ { + path = append(path, str[j]) + dfs(digits, start+1) + path = path[:len(path)-1] + } +} +``` + +### JavaScript + +```js +var letterCombinations = function(digits) { + const k = digits.length; + const map = ["","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"]; + if(!k) return []; + if(k === 1) return map[digits].split(""); + + const res = [], path = []; + backtracking(digits, k, 0); + return res; + + function backtracking(n, k, a) { + if(path.length === k) { + res.push(path.join("")); + return; + } + for(const v of map[n[a]]) { + path.push(v); + backtracking(n, k, a + 1); + path.pop(); + } + } +}; +``` + +### TypeScript + +```typescript +function letterCombinations(digits: string): string[] { + if (digits === '') return []; + const strMap: { [index: string]: string[] } = { + 1: [], + 2: ['a', 'b', 'c'], + 3: ['d', 'e', 'f'], + 4: ['g', 'h', 'i'], + 5: ['j', 'k', 'l'], + 6: ['m', 'n', 'o'], + 7: ['p', 'q', 'r', 's'], + 8: ['t', 'u', 'v'], + 9: ['w', 'x', 'y', 'z'], + } + const resArr: string[] = []; + function backTracking(digits: string, curIndex: number, route: string[]): void { + if (curIndex === digits.length) { + resArr.push(route.join('')); + return; + } + let tempArr: string[] = strMap[digits[curIndex]]; + for (let i = 0, length = tempArr.length; i < length; i++) { + route.push(tempArr[i]); + backTracking(digits, curIndex + 1, route); + route.pop(); + } + } + backTracking(digits, 0, []); + return resArr; +}; +``` + +### Rust + +```Rust +const map: [&str; 10] = [ + "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz", +]; +impl Solution { + fn back_trace(result: &mut Vec, s: &mut String, digits: &String, index: usize) { + let len = digits.len(); + if len == index { + result.push(s.to_string()); + return; + } + let digit = (digits.as_bytes()[index] - b'0') as usize; + for i in map[digit].chars() { + s.push(i); + Self::back_trace(result, s, digits, index + 1); + s.pop(); + } + } + pub fn letter_combinations(digits: String) -> Vec { + if digits.is_empty() { + return vec![]; + } + let mut res = vec![]; + let mut s = String::new(); + Self::back_trace(&mut res, &mut s, &digits, 0); + res + } +} +``` + +### C + +```c +char* path; +int pathTop; +char** result; +int resultTop; +char* letterMap[10] = {"", //0 + "", //1 + "abc", //2 + "def", //3 + "ghi", //4 + "jkl", //5 + "mno", //6 + "pqrs", //7 + "tuv", //8 + "wxyz", //9 +}; +void backTracking(char* digits, int index) { + //若当前下标等于digits数组长度 + if(index == strlen(digits)) { + //复制digits数组,因为最后要多存储一个0,所以数组长度要+1 + char* tempString = (char*)malloc(sizeof(char) * strlen(digits) + 1); + int j; + for(j = 0; j < strlen(digits); j++) { + tempString[j] = path[j]; + } + //char数组最后要以0结尾 + tempString[strlen(digits)] = 0; + result[resultTop++] = tempString; + return ; + } + //将字符数字转换为真的数字 + int digit = digits[index] - '0'; + //找到letterMap中对应的字符串 + char* letters = letterMap[digit]; + int i; + for(i = 0; i < strlen(letters); i++) { + path[pathTop++] = letters[i]; + //递归,处理下一层数字 + backTracking(digits, index+1); + pathTop--; + } +} + +char ** letterCombinations(char * digits, int* returnSize){ + //初始化path和result + path = (char*)malloc(sizeof(char) * strlen(digits)); + result = (char**)malloc(sizeof(char*) * 300); + + *returnSize = 0; + //若digits数组中元素个数为0,返回空集 + if(strlen(digits) == 0) + return result; + pathTop = resultTop = 0; + backTracking(digits, 0); + *returnSize = resultTop; + + return result; +} +``` + +### Swift + +```swift +func letterCombinations(_ digits: String) -> [String] { + // 按键与字母串映射 + let letterMap = [ + "", + "", "abc", "def", + "ghi", "jkl", "mno", + "pqrs", "tuv", "wxyz" + ] + // 把输入的按键字符串转成Int数组 + let baseCode = ("0" as Character).asciiValue! + let digits = digits.map { c in + guard let code = c.asciiValue else { return -1 } + return Int(code - baseCode) + }.filter { $0 >= 0 && $0 <= 9 } + guard !digits.isEmpty else { return [] } + + var result = [String]() + var s = "" + func backtracking(index: Int) { + // 结束条件:收集结果 + if index == digits.count { + result.append(s) + return + } + + // 遍历当前按键对应的字母串 + let letters = letterMap[digits[index]] + for letter in letters { + s.append(letter) // 处理 + backtracking(index: index + 1) // 递归,记得+1 + s.removeLast() // 回溯 + } + } + backtracking(index: 0) + return result +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def letterCombinations(digits: String): List[String] = { + var result = mutable.ListBuffer[String]() + if(digits == "") return result.toList // 如果参数为空,返回空结果集的List形式 + var path = mutable.ListBuffer[Char]() + // 数字和字符的映射关系 + val map = Array[String]("", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz") + + def backtracking(index: Int): Unit = { + if (index == digits.size) { + result.append(path.mkString) // mkString语法:将数组类型直接转换为字符串 + return + } + var digit = digits(index) - '0' // 这里使用toInt会报错!必须 -'0' + for (i <- 0 until map(digit).size) { + path.append(map(digit)(i)) + backtracking(index + 1) + path = path.take(path.size - 1) + } + } + + backtracking(0) + result.toList + } +} +``` + +### Ruby +```ruby +def letter_combinations(digits) + letter_map = { + 2 => ['a','b','c'], + 3 => ['d','e','f'], + 4 => ['g','h','i'], + 5 => ['j','k','l'], + 6 => ['m','n','o'], + 7 => ['p','q','r','s'], + 8 => ['t','u','v'], + 9 => ['w','x','y','z'] + } + + result = [] + path = [] + + return result if digits.size == 0 + + backtracking(result, letter_map, digits.split(''), path, 0) + result +end + +def backtracking(result, letter_map, digits, path, index) + if path.size == digits.size + result << path.join('') + return + end + + hash[digits[index].to_i].each do |chr| + path << chr + #index + 1代表处理下一个数字 + backtracking(result, letter_map, digits, path, index + 1) + #回溯,撤销处理过的数字 + path.pop + end +end +``` +### C# +```csharp +public class Solution +{ + public IList res = new List(); + public string s; + public string[] letterMap = new string[10] { "", "", "abc", "def", "ghi", "jkl", "mno", "pqrs", "tuv", "wxyz" }; + public IList LetterCombinations(string digits) + { + if (digits.Length == 0) + return res; + BackTracking(digits, 0); + return res; + } + public void BackTracking(string digits, int index) + { + if (index == digits.Length) + { + res.Add(s); + return; + } + int digit = digits[index] - '0'; + string letters = letterMap[digit]; + for (int i = 0; i < letters.Length; i++) + { + s += letters[i]; + BackTracking(digits, index + 1); + s = s.Substring(0, s.Length - 1); + } + } +} +``` + + diff --git "a/problems/0018.\345\233\233\346\225\260\344\271\213\345\222\214.md" "b/problems/0018.\345\233\233\346\225\260\344\271\213\345\222\214.md" new file mode 100755 index 0000000000..bf7d3bd4ee --- /dev/null +++ "b/problems/0018.\345\233\233\346\225\260\344\271\213\345\222\214.md" @@ -0,0 +1,799 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 一样的道理,能解决四数之和 +> 那么五数之和、六数之和、N数之和呢? + +# 第18题. 四数之和 + +[力扣题目链接](https://leetcode.cn/problems/4sum/) + +题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。 + +**注意:** + +答案中不可以包含重复的四元组。 + +示例: +给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 +满足要求的四元组集合为: +[ + [-1, 0, 0, 1], + [-2, -1, 1, 2], + [-2, 0, 0, 2] +] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[难在去重和剪枝!| LeetCode:18. 四数之和](https://www.bilibili.com/video/BV1DS4y147US),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +四数之和,和[15.三数之和](https://programmercarl.com/0015.三数之和.html)是一个思路,都是使用双指针法, 基本解法就是在[15.三数之和](https://programmercarl.com/0015.三数之和.html) 的基础上再套一层for循环。 + +但是有一些细节需要注意,例如: 不要判断`nums[k] > target` 就返回了,三数之和 可以通过 `nums[i] > 0` 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。比如:数组是`[-4, -3, -2, -1]`,`target`是`-10`,不能因为`-4 > -10`而跳过。但是我们依旧可以去做剪枝,逻辑变成`nums[k] > target && (nums[k] >=0 || target >= 0)`就可以了。 + +[15.三数之和](https://programmercarl.com/0015.三数之和.html)的双指针解法是一层for循环num[i]为确定值,然后循环内有left和right下标作为双指针,找到nums[i] + nums[left] + nums[right] == 0。 + +四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下标作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。 + +那么一样的道理,五数之和、六数之和等等都采用这种解法。 + +对于[15.三数之和](https://programmercarl.com/0015.三数之和.html)双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。 + +之前我们讲过哈希表的经典题目:[454.四数相加II](https://programmercarl.com/0454.四数相加II.html),相对于本题简单很多,因为本题是要求在一个集合中找出四个数相加等于target,同时四元组不能重复。 + +而[454.四数相加II](https://programmercarl.com/0454.四数相加II.html)是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于本题还是简单了不少! + +我们来回顾一下,几道题目使用了双指针法。 + +双指针法将时间复杂度:O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下: + +* [27.移除元素](https://programmercarl.com/0027.移除元素.html) +* [15.三数之和](https://programmercarl.com/0015.三数之和.html) +* [18.四数之和](https://programmercarl.com/0018.四数之和.html) + +链表相关双指针题目: + +* [206.反转链表](https://programmercarl.com/0206.翻转链表.html) +* [19.删除链表的倒数第N个节点](https://programmercarl.com/0019.删除链表的倒数第N个节点.html) +* [面试题 02.07. 链表相交](https://programmercarl.com/面试题02.07.链表相交.html) +* [142题.环形链表II](https://programmercarl.com/0142.环形链表II.html) + +双指针法在字符串题目中还有很多应用,后面还会介绍到。 + +C++代码 + +```CPP +class Solution { +public: + vector> fourSum(vector& nums, int target) { + vector> result; + sort(nums.begin(), nums.end()); + for (int k = 0; k < nums.size(); k++) { + // 剪枝处理 + if (nums[k] > target && nums[k] >= 0) { + break; // 这里使用break,统一通过最后的return返回 + } + // 对nums[k]去重 + if (k > 0 && nums[k] == nums[k - 1]) { + continue; + } + for (int i = k + 1; i < nums.size(); i++) { + // 2级剪枝处理 + if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) { + break; + } + + // 对nums[i]去重 + if (i > k + 1 && nums[i] == nums[i - 1]) { + continue; + } + int left = i + 1; + int right = nums.size() - 1; + while (right > left) { + // nums[k] + nums[i] + nums[left] + nums[right] > target 会溢出 + if ((long) nums[k] + nums[i] + nums[left] + nums[right] > target) { + right--; + // nums[k] + nums[i] + nums[left] + nums[right] < target 会溢出 + } else if ((long) nums[k] + nums[i] + nums[left] + nums[right] < target) { + left++; + } else { + result.push_back(vector{nums[k], nums[i], nums[left], nums[right]}); + // 对nums[left]和nums[right]去重 + while (right > left && nums[right] == nums[right - 1]) right--; + while (right > left && nums[left] == nums[left + 1]) left++; + + // 找到答案时,双指针同时收缩 + right--; + left++; + } + } + + } + } + return result; + } +}; + + +``` + +* 时间复杂度: O(n^3) +* 空间复杂度: O(1) + + +## 补充 + +二级剪枝的部分: + +```C++ +if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) { + break; +} +``` + +可以优化为: + +```C++ +if (nums[k] + nums[i] > target && nums[i] >= 0) { + break; +} +``` + +因为只要 nums[k] + nums[i] > target,那么 nums[i] 后面的数都是正数的话,就一定 不符合条件了。 + +不过这种剪枝 其实有点 小绕,大家能够理解 文章给的完整代码的剪枝 就够了。 + +## 其他语言版本 + +### C: + +```C +/* qsort */ +static int cmp(const void* arg1, const void* arg2) { + int a = *(int *)arg1; + int b = *(int *)arg2; + return (a > b); +} + +int** fourSum(int* nums, int numsSize, int target, int* returnSize, int** returnColumnSizes) { + + /* 对nums数组进行排序 */ + qsort(nums, numsSize, sizeof(int), cmp); + + int **res = (int **)malloc(sizeof(int *) * 40000); + int index = 0; + + /* k */ + for (int k = 0; k < numsSize - 3; k++) { /* 第一级 */ + + /* k剪枝 */ + if ((nums[k] > target) && (nums[k] >= 0)) { + break; + } + /* k去重 */ + if ((k > 0) && (nums[k] == nums[k - 1])) { + continue; + } + + /* i */ + for (int i = k + 1; i < numsSize - 2; i++) { /* 第二级 */ + + /* i剪枝 */ + if ((nums[k] + nums[i] > target) && (nums[i] >= 0)) { + break; + } + /* i去重 */ + if ((i > (k + 1)) && (nums[i] == nums[i - 1])) { + continue; + } + + /* left and right */ + int left = i + 1; + int right = numsSize - 1; + + while (left < right) { + + /* 防止大数溢出 */ + long long val = (long long)nums[k] + nums[i] + nums[left] + nums[right]; + if (val > target) { + right--; + } else if (val < target) { + left++; + } else { + int *res_tmp = (int *)malloc(sizeof(int) * 4); + res_tmp[0] = nums[k]; + res_tmp[1] = nums[i]; + res_tmp[2] = nums[left]; + res_tmp[3] = nums[right]; + res[index++] = res_tmp; + + /* right去重 */ + while ((right > left) && (nums[right] == nums[right - 1])) { + right--; + } + /* left去重 */ + while ((left < right) && (nums[left] == nums[left + 1])) { + left++; + } + + /* 更新right与left */ + left++, right--; + } + } + } + } + + /* 返回值处理 */ + *returnSize = index; + + int *column = (int *)malloc(sizeof(int) * index); + for (int i = 0; i < index; i++) { + column[i] = 4; + } + *returnColumnSizes = column; + return res; +} +``` + +### Java: + +```Java +import java.util.*; + +public class Solution { + public List> fourSum(int[] nums, int target) { + Arrays.sort(nums); // 排序数组 + List> result = new ArrayList<>(); // 结果集 + for (int k = 0; k < nums.length; k++) { + // 剪枝处理 + if (nums[k] > target && nums[k] >= 0) { + break; // 此处的break可以等价于return result; + } + // 对nums[k]去重 + if (k > 0 && nums[k] == nums[k - 1]) { + continue; + } + for (int i = k + 1; i < nums.length; i++) { + // 第二级剪枝 + if (nums[k] + nums[i] > target && nums[k] + nums[i] >= 0) { + break; // 注意是break到上一级for循环,如果直接return result;会有遗漏 + } + // 对nums[i]去重 + if (i > k + 1 && nums[i] == nums[i - 1]) { + continue; + } + int left = i + 1; + int right = nums.length - 1; + while (right > left) { + long sum = (long) nums[k] + nums[i] + nums[left] + nums[right]; + if (sum > target) { + right--; + } else if (sum < target) { + left++; + } else { + result.add(Arrays.asList(nums[k], nums[i], nums[left], nums[right])); + // 对nums[left]和nums[right]去重 + while (right > left && nums[right] == nums[right - 1]) right--; + while (right > left && nums[left] == nums[left + 1]) left++; + right--; + left++; + } + } + } + } + return result; + } + + public static void main(String[] args) { + Solution solution = new Solution(); + int[] nums = {1, 0, -1, 0, -2, 2}; + int target = 0; + List> results = solution.fourSum(nums, target); + for (List result : results) { + System.out.println(result); + } + } +} +``` + +### Python: +(版本一) 双指针 + +```python +class Solution: + def fourSum(self, nums: List[int], target: int) -> List[List[int]]: + nums.sort() + n = len(nums) + result = [] + for i in range(n): + if nums[i] > target and nums[i] > 0 and target > 0:# 剪枝(可省) + break + if i > 0 and nums[i] == nums[i-1]:# 去重 + continue + for j in range(i+1, n): + if nums[i] + nums[j] > target and target > 0: #剪枝(可省) + break + if j > i+1 and nums[j] == nums[j-1]: # 去重 + continue + left, right = j+1, n-1 + while left < right: + s = nums[i] + nums[j] + nums[left] + nums[right] + if s == target: + result.append([nums[i], nums[j], nums[left], nums[right]]) + while left < right and nums[left] == nums[left+1]: + left += 1 + while left < right and nums[right] == nums[right-1]: + right -= 1 + left += 1 + right -= 1 + elif s < target: + left += 1 + else: + right -= 1 + return result + +``` +(版本二) 使用字典 + +```python +class Solution(object): + def fourSum(self, nums, target): + """ + :type nums: List[int] + :type target: int + :rtype: List[List[int]] + """ + # 创建一个字典来存储输入列表中每个数字的频率 + freq = {} + for num in nums: + freq[num] = freq.get(num, 0) + 1 + + # 创建一个集合来存储最终答案,并遍历4个数字的所有唯一组合 + ans = set() + for i in range(len(nums)): + for j in range(i + 1, len(nums)): + for k in range(j + 1, len(nums)): + val = target - (nums[i] + nums[j] + nums[k]) + if val in freq: + # 确保没有重复 + count = (nums[i] == val) + (nums[j] == val) + (nums[k] == val) + if freq[val] > count: + ans.add(tuple(sorted([nums[i], nums[j], nums[k], val]))) + + return [list(x) for x in ans] + +``` + +### Go: + +```go +func fourSum(nums []int, target int) [][]int { + if len(nums) < 4 { + return nil + } + sort.Ints(nums) + var res [][]int + for i := 0; i < len(nums)-3; i++ { + n1 := nums[i] + // if n1 > target { // 不能这样写,因为可能是负数 + // break + // } + if i > 0 && n1 == nums[i-1] { // 对nums[i]去重 + continue + } + for j := i + 1; j < len(nums)-2; j++ { + n2 := nums[j] + if j > i+1 && n2 == nums[j-1] { // 对nums[j]去重 + continue + } + l := j + 1 + r := len(nums) - 1 + for l < r { + n3 := nums[l] + n4 := nums[r] + sum := n1 + n2 + n3 + n4 + if sum < target { + l++ + } else if sum > target { + r-- + } else { + res = append(res, []int{n1, n2, n3, n4}) + for l < r && n3 == nums[l+1] { // 去重 + l++ + } + for l < r && n4 == nums[r-1] { // 去重 + r-- + } + // 找到答案时,双指针同时靠近 + r-- + l++ + } + } + } + } + return res +} +``` + +### JavaScript: + +```js +/** + * @param {number[]} nums + * @param {number} target + * @return {number[][]} + */ +var fourSum = function(nums, target) { + const len = nums.length; + if(len < 4) return []; + nums.sort((a, b) => a - b); + const res = []; + for(let i = 0; i < len - 3; i++) { + // 去重i + if(i > 0 && nums[i] === nums[i - 1]) continue; + for(let j = i + 1; j < len - 2; j++) { + // 去重j + if(j > i + 1 && nums[j] === nums[j - 1]) continue; + let l = j + 1, r = len - 1; + while(l < r) { + const sum = nums[i] + nums[j] + nums[l] + nums[r]; + if(sum < target) { l++; continue} + if(sum > target) { r--; continue} + res.push([nums[i], nums[j], nums[l], nums[r]]); + + // 对nums[left]和nums[right]去重 + while(l < r && nums[l] === nums[++l]); + while(l < r && nums[r] === nums[--r]); + } + } + } + return res; +}; +``` + +### TypeScript: + +```typescript +function fourSum(nums: number[], target: number): number[][] { + nums.sort((a, b) => a - b); + let first: number = 0, + second: number, + third: number, + fourth: number; + let length: number = nums.length; + let resArr: number[][] = []; + for (; first < length; first++) { + if (first > 0 && nums[first] === nums[first - 1]) { + continue; + } + for (second = first + 1; second < length; second++) { + if ((second - first) > 1 && nums[second] === nums[second - 1]) { + continue; + } + third = second + 1; + fourth = length - 1; + while (third < fourth) { + let total: number = nums[first] + nums[second] + nums[third] + nums[fourth]; + if (total === target) { + resArr.push([nums[first], nums[second], nums[third], nums[fourth]]); + third++; + fourth--; + while (nums[third] === nums[third - 1]) third++; + while (nums[fourth] === nums[fourth + 1]) fourth--; + } else if (total < target) { + third++; + } else { + fourth--; + } + } + } + } + return resArr; +}; +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer[] $nums + * @param Integer $target + * @return Integer[][] + */ + function fourSum($nums, $target) { + $res = []; + sort($nums); + for ($i = 0; $i < count($nums); $i++) { + if ($i > 0 && $nums[$i] == $nums[$i - 1]) { + continue; + } + for ($j = $i + 1; $j < count($nums); $j++) { + if ($j > $i + 1 && $nums[$j] == $nums[$j - 1]) { + continue; + } + $left = $j + 1; + $right = count($nums) - 1; + while ($left < $right) { + $sum = $nums[$i] + $nums[$j] + $nums[$left] + $nums[$right]; + if ($sum < $target) { + $left++; + } + else if ($sum > $target) { + $right--; + } + else { + $res[] = [$nums[$i], $nums[$j], $nums[$left], $nums[$right]]; + while ($left < $right && $nums[$left] == $nums[$left+1]) $left++; + while ($left < $right && $nums[$right] == $nums[$right-1]) $right--; + $left++; + $right--; + } + } + } + } + return $res; + } +} +``` + +### Swift: + +```swift +func fourSum(_ nums: [Int], _ target: Int) -> [[Int]] { + var res = [[Int]]() + var sorted = nums + sorted.sort() + for k in 0 ..< sorted.count { + // 这种剪枝不行,target可能是负数 +// if sorted[k] > target { +// return res +// } + // 去重 + if k > 0 && sorted[k] == sorted[k - 1] { + continue + } + + let target2 = target - sorted[k] + for i in (k + 1) ..< sorted.count { + if i > (k + 1) && sorted[i] == sorted[i - 1] { + continue + } + var left = i + 1 + var right = sorted.count - 1 + while left < right { + let sum = sorted[i] + sorted[left] + sorted[right] + if sum < target2 { + left += 1 + } else if sum > target2 { + right -= 1 + } else { + res.append([sorted[k], sorted[i], sorted[left], sorted[right]]) + while left < right && sorted[left] == sorted[left + 1] { + left += 1 + } + while left < right && sorted[right] == sorted[right - 1] { + right -= 1 + } + // 找到答案 双指针同时收缩 + left += 1 + right -= 1 + } + } + } + } + return res +} +``` + +### C#: + +```csharp +public class Solution +{ + public IList> FourSum(int[] nums, int target) + { + var result = new List>(); + + Array.Sort(nums); + + for (int i = 0; i < nums.Length - 3; i++) + { + int n1 = nums[i]; + if (i > 0 && n1 == nums[i - 1]) + continue; + + for (int j = i + 1; j < nums.Length - 2; j++) + { + int n2 = nums[j]; + if (j > i + 1 && n2 == nums[j - 1]) + continue; + + int left = j + 1; + int right = nums.Length - 1; + + while (left < right) + { + int n3 = nums[left]; + int n4 = nums[right]; + int sum = n1 + n2 + n3 + n4; + + if (sum > target) + { + right--; + } + else if (sum < target) + { + left++; + } + else + { + result.Add(new List { n1, n2, n3, n4 }); + + while (left < right && nums[left] == n3) + { + left++; + } + + while (left < right && nums[right] == n4) + { + right--; + } + } + } + } + } + + return result; + } +} +``` + +### Rust: + +```Rust +use std::cmp::Ordering; +impl Solution { + pub fn four_sum(nums: Vec, target: i32) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut nums = nums; + nums.sort(); + let len = nums.len(); + for k in 0..len { + // 剪枝 + if nums[k] > target && (nums[k] > 0 || target > 0) { break; } + // 去重 + if k > 0 && nums[k] == nums[k - 1] { continue; } + for i in (k + 1)..len { + // 剪枝 + if nums[k] + nums[i] > target && (nums[k] + nums[i] >= 0 || target >= 0) { break; } + // 去重 + if i > k + 1 && nums[i] == nums[i - 1] { continue; } + let (mut left, mut right) = (i + 1, len - 1); + while left < right { + match (nums[k] + nums[i] + nums[left] + nums[right]).cmp(&target){ + Ordering::Equal => { + result.push(vec![nums[k], nums[i], nums[left], nums[right]]); + left += 1; + right -= 1; + while left < right && nums[left] == nums[left - 1]{ + left += 1; + } + while left < right && nums[right] == nums[right + 1]{ + right -= 1; + } + } + Ordering::Less => { + left +=1; + }, + Ordering::Greater => { + right -= 1; + } + } + } + } + } + result + } +} +``` + +### Scala: + +```scala +object Solution { + // 导包 + import scala.collection.mutable.ListBuffer + import scala.util.control.Breaks.{break, breakable} + def fourSum(nums: Array[Int], target: Int): List[List[Int]] = { + val res = ListBuffer[List[Int]]() + val nums_tmp = nums.sorted // 先排序 + for (i <- nums_tmp.indices) { + breakable { + if (i > 0 && nums_tmp(i) == nums_tmp(i - 1)) { + break // 如果该值和上次的值相同,跳过本次循环,相当于continue + } else { + for (j <- i + 1 until nums_tmp.length) { + breakable { + if (j > i + 1 && nums_tmp(j) == nums_tmp(j - 1)) { + break // 同上 + } else { + // 双指针 + var (left, right) = (j + 1, nums_tmp.length - 1) + while (left < right) { + var sum = nums_tmp(i) + nums_tmp(j) + nums_tmp(left) + nums_tmp(right) + if (sum == target) { + // 满足要求,直接加入到集合里面去 + res += List(nums_tmp(i), nums_tmp(j), nums_tmp(left), nums_tmp(right)) + while (left < right && nums_tmp(left) == nums_tmp(left + 1)) left += 1 + while (left < right && nums_tmp(right) == nums_tmp(right - 1)) right -= 1 + left += 1 + right -= 1 + } else if (sum < target) left += 1 + else right -= 1 + } + } + } + } + } + } + } + // 最终返回的res要转换为List,return关键字可以省略 + res.toList + } +} +``` +### Ruby: + +```ruby +def four_sum(nums, target) + #结果集 + result = [] + nums = nums.sort! + + for i in 0..nums.size - 1 + return result if i > 0 && nums[i] > target && nums[i] >= 0 + #对a进行去重 + next if i > 0 && nums[i] == nums[i - 1] + + for j in i + 1..nums.size - 1 + break if nums[i] + nums[j] > target && nums[i] + nums[j] >= 0 + #对b进行去重 + next if j > i + 1 && nums[j] == nums[j - 1] + left = j + 1 + right = nums.size - 1 + while left < right + sum = nums[i] + nums[j] + nums[left] + nums[right] + if sum > target + right -= 1 + elsif sum < target + left += 1 + else + result << [nums[i], nums[j], nums[left], nums[right]] + + #对c进行去重 + while left < right && nums[left] == nums[left + 1] + left += 1 + end + + #对d进行去重 + while left < right && nums[right] == nums[right - 1] + right -= 1 + end + + right -= 1 + left += 1 + end + end + end + end + + return result +end +``` + + diff --git "a/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" "b/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" new file mode 100755 index 0000000000..08f602c1c1 --- /dev/null +++ "b/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" @@ -0,0 +1,479 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 19.删除链表的倒数第N个节点 + +[力扣题目链接](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) + +给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 + +进阶:你能尝试使用一趟扫描实现吗? + +示例 1: + + +![19.删除链表的倒数第N个节点](https://file1.kamacoder.com/i/algo/20210510085957392.png) + +输入:head = [1,2,3,4,5], n = 2 +输出:[1,2,3,5] + +示例 2: + +输入:head = [1], n = 1 +输出:[] + +示例 3: + +输入:head = [1,2], n = 1 +输出:[1] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)::[链表遍历学清楚! | LeetCode:19.删除链表倒数第N个节点](https://www.bilibili.com/video/BV1vW4y1U7Gf),相信结合视频再看本篇题解,更有助于大家对链表的理解。** + + +## 思路 + + +双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。 + +思路是这样的,但要注意一些细节。 + +分为如下几步: + +* 首先这里我推荐大家使用虚拟头结点,这样方便处理删除实际头结点的逻辑,如果虚拟头结点不清楚,可以看这篇: [链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html) + +* 定义fast指针和slow指针,初始值为虚拟头结点,如图: + + + +* fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图: + + +* fast和slow同时移动,直到fast指向末尾,如题: + +//图片中有错别词:应该将“只到”改为“直到” +* 删除slow指向的下一个节点,如图: + + +此时不难写出如下C++代码: + +```CPP +class Solution { +public: + ListNode* removeNthFromEnd(ListNode* head, int n) { + ListNode* dummyHead = new ListNode(0); + dummyHead->next = head; + ListNode* slow = dummyHead; + ListNode* fast = dummyHead; + while(n-- && fast != NULL) { + fast = fast->next; + } + fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点 + while (fast != NULL) { + fast = fast->next; + slow = slow->next; + } + slow->next = slow->next->next; + + // ListNode *tmp = slow->next; C++释放内存的逻辑 + // slow->next = tmp->next; + // delete tmp; + + return dummyHead->next; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + +## 其他语言版本 + +### Java: + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + //新建一个虚拟头节点指向head + ListNode dummyNode = new ListNode(0); + dummyNode.next = head; + //快慢指针指向虚拟头节点 + ListNode fastIndex = dummyNode; + ListNode slowIndex = dummyNode; + + // 只要快慢指针相差 n 个结点即可 + for (int i = 0; i <= n; i++) { + fastIndex = fastIndex.next; + } + while (fastIndex != null) { + fastIndex = fastIndex.next; + slowIndex = slowIndex.next; + } + + // 此时 slowIndex 的位置就是待删除元素的前一个位置。 + // 具体情况可自己画一个链表长度为 3 的图来模拟代码来理解 + // 检查 slowIndex.next 是否为 null,以避免空指针异常 + if (slowIndex.next != null) { + slowIndex.next = slowIndex.next.next; + } + return dummyNode.next; + } +} +``` + + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + // 创建一个新的哑节点,指向原链表头 + ListNode s = new ListNode(-1, head); + // 递归调用remove方法,从哑节点开始进行删除操作 + remove(s, n); + // 返回新链表的头(去掉可能的哑节点) + return s.next; + } + + public int remove(ListNode p, int n) { + // 递归结束条件:如果当前节点为空,返回0 + if (p == null) { + return 0; + } + // 递归深入到下一个节点 + int net = remove(p.next, n); + // 如果当前节点是倒数第n个节点,进行删除操作 + if (net == n) { + p.next = p.next.next; + } + // 返回当前节点的总深度 + return net + 1; + } +} +``` + + +### Python: + +```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_head = ListNode(0, head) + + # 创建两个指针,慢指针和快指针,并将它们初始化为虚拟节点 + slow = fast = dummy_head + + # 快指针比慢指针快 n+1 步 + for i in range(n+1): + fast = fast.next + + # 移动两个指针,直到快速指针到达链表的末尾 + while fast: + slow = slow.next + fast = fast.next + + # 通过更新第 (n-1) 个节点的 next 指针删除第 n 个节点 + slow.next = slow.next.next + + return dummy_head.next + +``` +### Go: + +```Go +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func removeNthFromEnd(head *ListNode, n int) *ListNode { + dummyNode := &ListNode{0, head} + fast, slow := dummyNode, dummyNode + for i := 0; i <= n; i++ { // 注意<=,否则快指针为空时,慢指针正好在倒数第n个上面 + fast = fast.Next + } + for fast != nil { + fast = fast.Next + slow = slow.Next + } + slow.Next = slow.Next.Next + return dummyNode.Next +} +``` + +### JavaScript: + +```js +/** + * @param {ListNode} head + * @param {number} n + * @return {ListNode} + */ +var removeNthFromEnd = function (head, n) { + // 创建哨兵节点,简化解题逻辑 + let dummyHead = new ListNode(0, head); + let fast = dummyHead; + let slow = dummyHead; + while (n--) fast = fast.next; + while (fast.next !== null) { + slow = slow.next; + fast = fast.next; + } + slow.next = slow.next.next; + return dummyHead.next; +}; +``` +### TypeScript: + +版本一(快慢指针法): + +```typescript +function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null { + let newHead: ListNode | null = new ListNode(0, head); + //根据leetcode题目的定义可推断这里快慢指针均不需要定义为ListNode | null。 + let slowNode: ListNode = newHead; + let fastNode: ListNode = newHead; + + while(n--) { + fastNode = fastNode.next!; //由虚拟头节点前进n个节点时,fastNode.next可推断不为null。 + } + while(fastNode.next) { //遍历直至fastNode.next = null, 即尾部节点。 此时slowNode指向倒数第n个节点。 + fastNode = fastNode.next; + slowNode = slowNode.next!; + } + slowNode.next = slowNode.next!.next; //倒数第n个节点可推断其next节点不为空。 + return newHead.next; +} +``` + +版本二(计算节点总数法): + +```typescript +function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null { + let curNode: ListNode | null = head; + let listSize: number = 0; + while (curNode) { + curNode = curNode.next; + listSize++; + } + if (listSize === n) { + head = head.next; + } else { + curNode = head; + for (let i = 0; i < listSize - n - 1; i++) { + curNode = curNode.next; + } + curNode.next = curNode.next.next; + } + return head; +}; +``` + +版本三(递归倒退n法): + +```typescript +function removeNthFromEnd(head: ListNode | null, n: number): ListNode | null { + let newHead: ListNode | null = new ListNode(0, head); + let cnt = 0; + function recur(node) { + if (node === null) return; + recur(node.next); + cnt++; + if (cnt === n + 1) { + node.next = node.next.next; + } + } + recur(newHead); + return newHead.next; +}; +``` + +### Kotlin: + +```Kotlin +fun removeNthFromEnd(head: ListNode?, n: Int): ListNode? { + val pre = ListNode(0).apply { + this.next = head + } + var fastNode: ListNode? = pre + var slowNode: ListNode? = pre + for (i in 0..n) { + fastNode = fastNode?.next + } + while (fastNode != null) { + slowNode = slowNode?.next + fastNode = fastNode.next + } + slowNode?.next = slowNode?.next?.next + return pre.next +} +``` + +### Swift: + +```swift +func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? { + if head == nil { + return nil + } + if n == 0 { + return head + } + let dummyHead = ListNode(-1, head) + var fast: ListNode? = dummyHead + var slow: ListNode? = dummyHead + // fast 前移 n + for _ in 0 ..< n { + fast = fast?.next + } + while fast?.next != nil { + fast = fast?.next + slow = slow?.next + } + slow?.next = slow?.next?.next + return dummyHead.next +} +``` + +### PHP: + +```php +function removeNthFromEnd($head, $n) { + // 设置虚拟头节点 + $dummyHead = new ListNode(); + $dummyHead->next = $head; + + $slow = $fast = $dummyHead; + while($n-- && $fast != null){ + $fast = $fast->next; + } + // fast 再走一步,让 slow 指向删除节点的上一个节点 + $fast = $fast->next; + while ($fast != NULL) { + $fast = $fast->next; + $slow = $slow->next; + } + $slow->next = $slow->next->next; + return $dummyHead->next; + } +``` + +### Scala: + +```scala +object Solution { + def removeNthFromEnd(head: ListNode, n: Int): ListNode = { + val dummy = new ListNode(-1, head) // 定义虚拟头节点 + var fast = head // 快指针从头开始走 + var slow = dummy // 慢指针从虚拟头开始头 + // 因为参数 n 是不可变量,所以不能使用 while(n>0){n-=1}的方式 + for (i <- 0 until n) { + fast = fast.next + } + // 快指针和满指针一起走,直到fast走到null + while (fast != null) { + slow = slow.next + fast = fast.next + } + // 删除slow的下一个节点 + slow.next = slow.next.next + // 返回虚拟头节点的下一个 + dummy.next + } +} +``` + +### Rust: + +```rust +impl Solution { + pub fn remove_nth_from_end(head: Option>, mut n: i32) -> Option> { + let mut dummy_head = Box::new(ListNode::new(0)); + dummy_head.next = head; + let mut fast = &dummy_head.clone(); + let mut slow = &mut dummy_head; + while n > 0 { + fast = fast.next.as_ref().unwrap(); + n -= 1; + } + while fast.next.is_some() { + fast = fast.next.as_ref().unwrap(); + slow = slow.next.as_mut().unwrap(); + } + slow.next = slow.next.as_mut().unwrap().next.take(); + dummy_head.next + } +} +``` +### C: + +```c +/**c语言单链表的定义 + * Definition for singly-linked list. + * struct ListNode { + * int val; + * struct ListNode *next; + * }; + */ +struct ListNode* removeNthFromEnd(struct ListNode* head, int n) { + //定义虚拟头节点dummy 并初始化使其指向head + struct ListNode* dummy = malloc(sizeof(struct ListNode)); + dummy->val = 0; + dummy->next = head; + //定义 fast slow 双指针 + struct ListNode* fast = head; + struct ListNode* slow = dummy; + + for (int i = 0; i < n; ++i) { + fast = fast->next; + } + while (fast) { + fast = fast->next; + slow = slow->next; + } + slow->next = slow->next->next;//删除倒数第n个节点 + head = dummy->next; + free(dummy);//删除虚拟节点dummy + return head; +} + + + +``` + +### C#: + +```csharp +public class Solution { + public ListNode RemoveNthFromEnd(ListNode head, int n) { + ListNode dummpHead = new ListNode(0); + dummpHead.next = head; + var fastNode = dummpHead; + var slowNode = dummpHead; + while(n-- != 0 && fastNode != null) + { + fastNode = fastNode.next; + } + while(fastNode.next != null) + { + fastNode = fastNode.next; + slowNode = slowNode.next; + } + slowNode.next = slowNode.next.next; + return dummpHead.next; + } +} +``` diff --git "a/problems/0020.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" "b/problems/0020.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" new file mode 100755 index 0000000000..09cf997839 --- /dev/null +++ "b/problems/0020.\346\234\211\346\225\210\347\232\204\346\213\254\345\217\267.md" @@ -0,0 +1,575 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 数据结构与算法应用往往隐藏在我们看不到的地方 + +# 20. 有效的括号 + +[力扣题目链接](https://leetcode.cn/problems/valid-parentheses/) + +给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 + +有效字符串需满足: +* 左括号必须用相同类型的右括号闭合。 +* 左括号必须以正确的顺序闭合。 +* 注意空字符串可被认为是有效字符串。 + +示例 1: +* 输入: "()" +* 输出: true + +示例 2: +* 输入: "()[]{}" +* 输出: true + +示例 3: +* 输入: "(]" +* 输出: false + +示例 4: +* 输入: "([)]" +* 输出: false + +示例 5: +* 输入: "{[]}" +* 输出: true + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[栈的拿手好戏!| LeetCode:20. 有效的括号](https://www.bilibili.com/video/BV1AF411w78g),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 题外话 + +**括号匹配是使用栈解决的经典问题。** + +题意其实就像我们在写代码的过程中,要求括号的顺序是一样的,有左括号,相应的位置必须要有右括号。 + +如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,也是使用了栈这种数据结构。 + +再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。 + +``` +cd a/b/c/../../ +``` + +这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(其实可以出一道相应的面试题了) + +所以栈在计算机领域中应用是非常广泛的。 + +有的同学经常会想学的这些数据结构有什么用,也开发不了什么软件,大多数同学说的软件应该都是可视化的软件例如APP、网站之类的,那都是非常上层的应用了,底层很多功能的实现都是基础的数据结构和算法。 + +**所以数据结构与算法的应用往往隐藏在我们看不到的地方!** + +这里我就不过多展开了,先来看题。 + +### 进入正题 + +由于栈结构的特殊性,非常适合做对称匹配类的题目。 + +首先要弄清楚,字符串里的括号不匹配有几种情况。 + +**一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越乱。** + +建议在写代码之前要分析好有哪几种不匹配的情况,如果不在动手之前分析好,写出的代码也会有很多问题。 + +先来分析一下 这里有三种不匹配的情况, + + +1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。 +![括号匹配1](https://file1.kamacoder.com/i/algo/2020080915505387.png) + +2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。 +![括号匹配2](https://file1.kamacoder.com/i/algo/20200809155107397.png) + +3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。 +![括号匹配3](https://file1.kamacoder.com/i/algo/20200809155115779.png) + + + +我们的代码只要覆盖了这三种不匹配的情况,就不会出问题,可以看出 动手之前分析好题目的重要性。 + +动画如下: + +![20.有效括号](https://file1.kamacoder.com/i/algo/20.有效括号.gif) + + +第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false + +第二种情况:遍历字符串匹配的过程中,发现栈里没有要匹配的字符。所以return false + +第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号return false + +那么什么时候说明左括号和右括号全都匹配了呢,就是字符串遍历完之后,栈是空的,就说明全都匹配了。 + +分析完之后,代码其实就比较好写了, + +但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了! + +实现C++代码如下: + + +```CPP +class Solution { +public: + bool isValid(string s) { + if (s.size() % 2 != 0) return false; // 如果s的长度为奇数,一定不符合要求 + stack st; + for (int i = 0; i < s.size(); i++) { + if (s[i] == '(') st.push(')'); + else if (s[i] == '{') st.push('}'); + else if (s[i] == '[') st.push(']'); + // 第三种情况:遍历字符串匹配的过程中,栈已经为空了,没有匹配的字符了,说明右括号没有找到对应的左括号 return false + // 第二种情况:遍历字符串匹配的过程中,发现栈里没有我们要匹配的字符。所以return false + else if (st.empty() || st.top() != s[i]) return false; + else st.pop(); // st.top() 与 s[i]相等,栈弹出元素 + } + // 第一种情况:此时我们已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false,否则就return true + return st.empty(); + } +}; + +``` +* 时间复杂度: O(n) +* 空间复杂度: O(n) + +技巧性的东西没有固定的学习方法,还是要多看多练,自己灵活运用了。 + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public boolean isValid(String s) { + Deque deque = new LinkedList<>(); + char ch; + for (int i = 0; i < s.length(); i++) { + ch = s.charAt(i); + //碰到左括号,就把相应的右括号入栈 + if (ch == '(') { + deque.push(')'); + }else if (ch == '{') { + deque.push('}'); + }else if (ch == '[') { + deque.push(']'); + } else if (deque.isEmpty() || deque.peek() != ch) { + return false; + }else {//如果是右括号判断是否和栈顶元素匹配 + deque.pop(); + } + } + //遍历结束,如果栈为空,则括号全部匹配 + return deque.isEmpty(); + } +} +``` + +```java +// 解法二 +// 对应的另一半一定在栈顶 +class Solution { + public boolean isValid(String s) { + Stack stack = new Stack<>(); + for(char c : s.toCharArray()){ + // 有对应的另一半就直接消消乐 + if(c == ')' && !stack.isEmpty() && stack.peek() == '(') + stack.pop(); + else if(c == '}' && !stack.isEmpty() && stack.peek() == '{') + stack.pop(); + else if(c == ']' && !stack.isEmpty() && stack.peek() == '[') + stack.pop(); + else + stack.push(c);// 没有匹配的就放进去 + } + + return stack.isEmpty(); + } +} +``` + +### Python: + +```python +# 方法一,仅使用栈,更省空间 +class Solution: + def isValid(self, s: str) -> bool: + stack = [] + + for item in s: + if item == '(': + stack.append(')') + elif item == '[': + stack.append(']') + elif item == '{': + stack.append('}') + elif not stack or stack[-1] != item: + return False + else: + stack.pop() + + return True if not stack else False +``` + +```python +# 方法二,使用字典 +class Solution: + def isValid(self, s: str) -> bool: + stack = [] + mapping = { + '(': ')', + '[': ']', + '{': '}' + } + for item in s: + if item in mapping.keys(): + stack.append(mapping[item]) + elif not stack or stack[-1] != item: + return False + else: + stack.pop() + return True if not stack else False +``` + +### Go: + +```Go +// 思路: 使用栈来进行括号的匹配 +// 时间复杂度 O(n) +// 空间复杂度 O(n) +func isValid(s string) bool { + // 使用切片模拟栈的行为 + stack := make([]rune, 0) + + // m 用于记录某个右括号对应的左括号 + m := make(map[rune]rune) + m[')'] = '(' + m[']'] = '[' + m['}'] = '{' + + // 遍历字符串中的 rune + for _, c := range s { + // 左括号直接入栈 + if c == '(' || c == '[' || c == '{' { + stack = append(stack, c) + } else { + // 如果是右括号,先判断栈内是否还有元素 + if len(stack) == 0 { + return false + } + // 再判断栈顶元素是否能够匹配 + peek := stack[len(stack)-1] + if peek != m[c] { + return false + } + // 模拟栈顶弹出 + stack = stack[:len(stack)-1] + } + } + + // 若栈中不再包含元素,则能完全匹配 + return len(stack) == 0 +} +``` + +### Ruby: + +```ruby +def is_valid(strs) + symbol_map = {')' => '(', '}' => '{', ']' => '['} + stack = [] + strs.size.times {|i| + c = strs[i] + if symbol_map.has_key?(c) + top_e = stack.shift + return false if symbol_map[c] != top_e + else + stack.unshift(c) + end + } + stack.empty? +end +``` + +### JavaScript: + +```javascript +var isValid = function (s) { + const stack = []; + for (let i = 0; i < s.length; i++) { + let c = s[i]; + switch (c) { + case '(': + stack.push(')'); + break; + case '[': + stack.push(']'); + break; + case '{': + stack.push('}'); + break; + default: + if (c !== stack.pop()) { + return false; + } + } + } + return stack.length === 0; +}; +// 简化版本 +var isValid = function(s) { + const stack = [], + map = { + "(":")", + "{":"}", + "[":"]" + }; + for(const x of s) { + if(x in map) { + stack.push(x); + continue; + }; + if(map[stack.pop()] !== x) return false; + } + return !stack.length; +}; +``` + +### TypeScript: + +版本一:普通版 + +```typescript +function isValid(s: string): boolean { + let helperStack: string[] = []; + for (let i = 0, length = s.length; i < length; i++) { + let x: string = s[i]; + switch (x) { + case '(': + helperStack.push(')'); + break; + case '[': + helperStack.push(']'); + break; + case '{': + helperStack.push('}'); + break; + default: + if (helperStack.pop() !== x) return false; + break; + } + } + return helperStack.length === 0; +}; +``` + +版本二:优化版 + +```typescript +function isValid(s: string): boolean { + type BracketMap = { + [index: string]: string; + } + let helperStack: string[] = []; + let bracketMap: BracketMap = { + '(': ')', + '[': ']', + '{': '}' + } + for (let i of s) { + if (bracketMap.hasOwnProperty(i)) { + helperStack.push(bracketMap[i]); + } else if (i !== helperStack.pop()) { + return false; + } + } + return helperStack.length === 0; +}; +``` + +### Swift: + +```swift +func isValid(_ s: String) -> Bool { + var stack = [String.Element]() + for ch in s { + if ch == "(" { + stack.append(")") + } else if ch == "{" { + stack.append("}") + } else if ch == "[" { + stack.append("]") + } else { + let top = stack.last + if ch == top { + stack.removeLast() + } else { + return false + } + } + } + return stack.isEmpty +} +``` + +### C: + +```C +//辅助函数:判断栈顶元素与输入的括号是否为一对。若不是,则返回False +int notMatch(char par, char* stack, int stackTop) { + switch(par) { + case ']': + return stack[stackTop - 1] != '['; + case ')': + return stack[stackTop - 1] != '('; + case '}': + return stack[stackTop - 1] != '{'; + } + return 0; +} + +bool isValid(char * s){ + int strLen = strlen(s); + //开辟栈空间 + char stack[5000]; + int stackTop = 0; + + //遍历字符串 + int i; + for(i = 0; i < strLen; i++) { + //取出当前下标所对应字符 + char tempChar = s[i]; + //若当前字符为左括号,则入栈 + if(tempChar == '(' || tempChar == '[' || tempChar == '{') + stack[stackTop++] = tempChar; + //若当前字符为右括号,且栈中无元素或右括号与栈顶元素不符,返回False + else if(stackTop == 0 || notMatch(tempChar, stack, stackTop)) + return 0; + //当前字符与栈顶元素为一对括号,将栈顶元素出栈 + else + stackTop--; + } + //若栈中有元素,返回False。若没有元素(stackTop为0),返回True + return !stackTop; +} +``` + +### C#: + +```csharp +public class Solution { + public bool IsValid(string s) { + var len = s.Length; + if(len % 2 == 1) return false; // 字符串长度为单数,直接返回 false + // 初始化栈 + var stack = new Stack(); + // 遍历字符串 + for(int i = 0; i < len; i++){ + // 当字符串为左括号时,进栈对应的右括号 + if(s[i] == '('){ + stack.Push(')'); + }else if(s[i] == '['){ + stack.Push(']'); + }else if(s[i] == '{'){ + stack.Push('}'); + } + // 当字符串为右括号时,当栈为空(无左括号) 或者 出栈字符不是当前的字符 + else if(stack.Count == 0 || stack.Pop() != s[i]) + return false; + } + // 如果栈不为空,例如“((()”,右括号少于左括号,返回false + if (stack.Count > 0) + return false; + // 上面的校验都满足,则返回true + else + return true; + } +} +``` + +### PHP: + +```php +// https://www.php.net/manual/zh/class.splstack.php +class Solution +{ + function isValid($s){ + $stack = new SplStack(); + for ($i = 0; $i < strlen($s); $i++) { + if ($s[$i] == "(") { + $stack->push(')'); + } else if ($s[$i] == "{") { + $stack->push('}'); + } else if ($s[$i] == "[") { + $stack->push(']'); + // 2、遍历匹配过程中,发现栈内没有要匹配的字符 return false + // 3、遍历匹配过程中,栈已为空,没有匹配的字符了,说明右括号没有找到对应的左括号 return false + } else if ($stack->isEmpty() || $stack->top() != $s[$i]) { + return false; + } else {//$stack->top() == $s[$i] + $stack->pop(); + } + } + // 1、遍历完,但是栈不为空,说明有相应的括号没有被匹配,return false + return $stack->isEmpty(); + } +} +``` + +### Scala: + +```scala +object Solution { + import scala.collection.mutable + def isValid(s: String): Boolean = { + if(s.length % 2 != 0) return false // 如果字符串长度是奇数直接返回false + val stack = mutable.Stack[Char]() + // 循环遍历字符串 + for (i <- s.indices) { + val c = s(i) + if (c == '(' || c == '[' || c == '{') stack.push(c) + else if(stack.isEmpty) return false // 如果没有(、[、{则直接返回false + // 以下三种情况,不满足则直接返回false + else if(c==')' && stack.pop() != '(') return false + else if(c==']' && stack.pop() != '[') return false + else if(c=='}' && stack.pop() != '{') return false + } + // 如果为空则正确匹配,否则还有余孽就不匹配 + stack.isEmpty + } +} +``` + +### Rust: + +```rust +impl Solution { + pub fn is_valid(s: String) -> bool { + if s.len() % 2 == 1 { + return false; + } + let mut stack = vec![]; + let mut chars: Vec = s.chars().collect(); + while let Some(s) = chars.pop() { + match s { + ')' => stack.push('('), + ']' => stack.push('['), + '}' => stack.push('{'), + _ => { + if stack.is_empty() || stack.pop().unwrap() != s { + return false; + } + } + } + } + stack.is_empty() + } +} +``` + + diff --git "a/problems/0024.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" "b/problems/0024.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" new file mode 100755 index 0000000000..14d2538f45 --- /dev/null +++ "b/problems/0024.\344\270\244\344\270\244\344\272\244\346\215\242\351\223\276\350\241\250\344\270\255\347\232\204\350\212\202\347\202\271.md" @@ -0,0 +1,527 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 24. 两两交换链表中的节点 + +[力扣题目链接](https://leetcode.cn/problems/swap-nodes-in-pairs/) + +给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。 + +你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。 + + +24.两两交换链表中的节点-题意 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[帮你把链表细节学清楚! | LeetCode:24. 两两交换链表中的节点](https://www.bilibili.com/video/BV1YT411g7br),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +这道题目正常模拟就可以了。 + +建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。 + +对虚拟头结点的操作,还不熟悉的话,可以看这篇[链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html)。 + +接下来就是交换相邻两个元素了,**此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序** + +初始时,cur指向虚拟头结点,然后进行如下三步: + +![24.两两交换链表中的节点1](https://file1.kamacoder.com/i/algo/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B91.png) + +操作之后,链表如下: + +![24.两两交换链表中的节点2](https://file1.kamacoder.com/i/algo/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B92.png) + +看这个可能就更直观一些了: + + +![24.两两交换链表中的节点3](https://file1.kamacoder.com/i/algo/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B93.png) + +对应的C++代码实现如下: (注释中详细和如上图中的三步做对应) + +```CPP +class Solution { +public: + ListNode* swapPairs(ListNode* head) { + ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点 + dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作 + ListNode* cur = dummyHead; + while(cur->next != nullptr && cur->next->next != nullptr) { + ListNode* tmp = cur->next; // 记录临时节点 + ListNode* tmp1 = cur->next->next->next; // 记录临时节点 + + cur->next = cur->next->next; // 步骤一 + cur->next->next = tmp; // 步骤二 + cur->next->next->next = tmp1; // 步骤三 + + cur = cur->next->next; // cur移动两位,准备下一轮交换 + } + ListNode* result = dummyHead->next; + delete dummyHead; + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +## 拓展 + +**这里还是说一下,大家不必太在意力扣上执行用时,打败多少多少用户,这个统计不准确的。** + +做题的时候自己能分析出来时间复杂度就可以了,至于力扣上执行用时,大概看一下就行。 + +上面的代码我第一次提交执行用时8ms,打败6.5%的用户,差点吓到我了。 + +心想应该没有更好的方法了吧,也就 $O(n)$ 的时间复杂度,重复提交几次,这样了: + +![24.两两交换链表中的节点](https://file1.kamacoder.com/i/algo/24.%E4%B8%A4%E4%B8%A4%E4%BA%A4%E6%8D%A2%E9%93%BE%E8%A1%A8%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.png) + +力扣上的统计如果两份代码是 100ms 和 300ms的耗时,其实是需要注意的。 + +如果一个是 4ms 一个是 12ms,看上去好像是一个打败了80%,一个打败了20%,其实是没有差别的。 只不过是力扣上统计的误差而已。 + + +## 其他语言版本 + +### C: + +```c +/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * struct ListNode *next; + * }; + */ +//递归版本 +struct ListNode* swapPairs(struct ListNode* head){ + //递归结束条件:头节点不存在或头节点的下一个节点不存在。此时不需要交换,直接返回head + if(!head || !head->next) + return head; + //创建一个节点指针类型保存头结点下一个节点 + struct ListNode *newHead = head->next; + //更改头结点+2位节点后的值,并将头结点的next指针指向这个更改过的list + head->next = swapPairs(newHead->next); + //将新的头结点的next指针指向老的头节点 + newHead->next = head; + return newHead; +} +``` + +```c +//迭代版本 +struct ListNode* swapPairs(struct ListNode* head){ + //使用双指针避免使用中间变量 + typedef struct ListNode ListNode; + ListNode *fakehead = (ListNode *)malloc(sizeof(ListNode)); + fakehead->next = head; + ListNode* right = fakehead->next; + ListNode* left = fakehead; + while(left && right && right->next ){ + left->next = right->next; + right->next = left->next->next; + left->next->next = right; + left = right; + right = left->next; + } + return fakehead->next; +} +``` + +### Java: + +```Java +// 递归版本 +class Solution { + public ListNode swapPairs(ListNode head) { + // base case 退出提交 + if(head == null || head.next == null) return head; + // 获取当前节点的下一个节点 + ListNode next = head.next; + // 进行递归 + ListNode newNode = swapPairs(next.next); + // 这里进行交换 + next.next = head; + head.next = newNode; + + return next; + } +} +``` + +```java +class Solution { + public ListNode swapPairs(ListNode head) { + ListNode dumyhead = new ListNode(-1); // 设置一个虚拟头结点 + dumyhead.next = head; // 将虚拟头结点指向head,这样方便后面做删除操作 + ListNode cur = dumyhead; + ListNode temp; // 临时节点,保存两个节点后面的节点 + ListNode firstnode; // 临时节点,保存两个节点之中的第一个节点 + ListNode secondnode; // 临时节点,保存两个节点之中的第二个节点 + while (cur.next != null && cur.next.next != null) { + temp = cur.next.next.next; + firstnode = cur.next; + secondnode = cur.next.next; + cur.next = secondnode; // 步骤一 + secondnode.next = firstnode; // 步骤二 + firstnode.next = temp; // 步骤三 + cur = firstnode; // cur移动,准备下一轮交换 + } + return dumyhead.next; + } +} +``` + +```java +// 将步骤 2,3 交换顺序,这样不用定义 temp 节点 +public ListNode swapPairs(ListNode head) { + ListNode dummy = new ListNode(0, head); + ListNode cur = dummy; + while (cur.next != null && cur.next.next != null) { + ListNode node1 = cur.next;// 第 1 个节点 + ListNode node2 = cur.next.next;// 第 2 个节点 + cur.next = node2; // 步骤 1 + node1.next = node2.next;// 步骤 3 + node2.next = node1;// 步骤 2 + cur = cur.next.next; + } + return dummy.next; +} +``` + +### Python: + +```python +# 递归版本 +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next + +class Solution: + def swapPairs(self, head: Optional[ListNode]) -> Optional[ListNode]: + if head is None or head.next is None: + return head + + # 待翻转的两个node分别是pre和cur + pre = head + cur = head.next + next = head.next.next + + cur.next = pre # 交换 + pre.next = self.swapPairs(next) # 将以next为head的后续链表两两交换 + + return cur +``` + +```python +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next + +class Solution: + def swapPairs(self, head: ListNode) -> ListNode: + dummy_head = ListNode(next=head) + current = dummy_head + + # 必须有cur的下一个和下下个才能交换,否则说明已经交换结束了 + while current.next and current.next.next: + temp = current.next # 防止节点修改 + temp1 = current.next.next.next + + current.next = current.next.next + current.next.next = temp + temp.next = temp1 + current = current.next.next + return dummy_head.next + +``` + +### Go: + +```go +func swapPairs(head *ListNode) *ListNode { + dummy := &ListNode{ + Next: head, + } + //head=list[i] + //pre=list[i-1] + pre := dummy + for head != nil && head.Next != nil { + pre.Next = head.Next + next := head.Next.Next + head.Next.Next = head + head.Next = next + //pre=list[(i+2)-1] + pre = head + //head=list[(i+2)] + head = next + } + return dummy.Next +} +``` + +```go +// 递归版本 +func swapPairs(head *ListNode) *ListNode { + if head == nil || head.Next == nil { + return head + } + next := head.Next + head.Next = swapPairs(next.Next) + next.Next = head + return next +} +``` + +### JavaScript: + +```javascript +var swapPairs = function (head) { + let ret = new ListNode(0, head), temp = ret; + while (temp.next && temp.next.next) { + let cur = temp.next.next, pre = temp.next; + pre.next = cur.next; + cur.next = pre; + temp.next = cur; + temp = pre; + } + return ret.next; +}; +``` + +```javascript +// 递归版本 +var swapPairs = function (head) { + if (head == null || head.next == null) { + return head; + } + + let after = head.next; + head.next = swapPairs(after.next); + after.next = head; + + return after; +}; +``` + +### TypeScript: + +```typescript +function swapPairs(head: ListNode | null): ListNode | null { + const dummyNode: ListNode = new ListNode(0, head); + let curNode: ListNode | null = dummyNode; + while (curNode && curNode.next && curNode.next.next) { + let firstNode: ListNode = curNode.next, + secNode: ListNode = curNode.next.next, + thirdNode: ListNode | null = curNode.next.next.next; + curNode.next = secNode; + secNode.next = firstNode; + firstNode.next = thirdNode; + curNode = firstNode; + } + return dummyNode.next; +}; +``` + +### Kotlin: + +```kotlin +fun swapPairs(head: ListNode?): ListNode? { + val dummyNode = ListNode(0).apply { + this.next = head + } + var cur: ListNode? = dummyNode + while (cur?.next != null && cur.next?.next != null) { + val temp = cur.next + val temp2 = cur.next?.next?.next + cur.next = cur.next?.next + cur.next?.next = temp + cur.next?.next?.next = temp2 + cur = cur.next?.next + } + return dummyNode.next +} +``` + +### Swift: + +```swift +func swapPairs(_ head: ListNode?) -> ListNode? { + if head == nil || head?.next == nil { + return head + } + let dummyHead: ListNode = ListNode(-1, head) + var current: ListNode? = dummyHead + while current?.next != nil && current?.next?.next != nil { + let temp1 = current?.next + let temp2 = current?.next?.next?.next + + current?.next = current?.next?.next + current?.next?.next = temp1 + current?.next?.next?.next = temp2 + + current = current?.next?.next + } + return dummyHead.next +} +``` +### Scala: + +```scala +// 虚拟头节点 +object Solution { + def swapPairs(head: ListNode): ListNode = { + var dummy = new ListNode(0, head) // 虚拟头节点 + var pre = dummy + var cur = head + // 当pre的下一个和下下个都不为空,才进行两两转换 + while (pre.next != null && pre.next.next != null) { + var tmp: ListNode = cur.next.next // 缓存下一次要进行转换的第一个节点 + pre.next = cur.next // 步骤一 + cur.next.next = cur // 步骤二 + cur.next = tmp // 步骤三 + // 下面是准备下一轮的交换 + pre = cur + cur = tmp + } + // 最终返回dummy虚拟头节点的下一个,return可以省略 + dummy.next + } +} +``` + +### PHP: + +```php +//虚拟头结点 +function swapPairs($head) { + if ($head == null || $head->next == null) { + return $head; + } + + $dummyNode = new ListNode(0, $head); + $preNode = $dummyNode; //虚拟头结点 + $curNode = $head; + $nextNode = $head->next; + while($curNode && $nextNode) { + $nextNextNode = $nextNode->next; //存下一个节点 + $nextNode->next = $curNode; //交换curHead 和 nextHead + $curNode->next = $nextNextNode; + $preNode->next = $nextNode; //上一个节点的下一个指向指向nextHead + + //更新当前的几个指针 + $preNode = $preNode->next->next; + $curNode = $nextNextNode; + $nextNode = $nextNextNode->next; + } + + return $dummyNode->next; +} + +//递归版本 +function swapPairs($head) +{ + // 终止条件 + if ($head === null || $head->next === null) { + return $head; + } + + //结果要返回的头结点 + $next = $head->next; + $head->next = $this->swapPairs($next->next); //当前头结点->next指向更新 + $next->next = $head; //当前第二个节点的->next指向更新 + return $next; //返回翻转后的头结点 +} +``` + +### Rust: + +```rust +// 虚拟头节点 +impl Solution { + pub fn swap_pairs(head: Option>) -> Option> { + let mut dummy_head = Box::new(ListNode::new(0)); + dummy_head.next = head; + let mut cur = dummy_head.as_mut(); + while let Some(mut node) = cur.next.take() { + if let Some(mut next) = node.next.take() { + node.next = next.next.take(); + next.next = Some(node); + cur.next = Some(next); + cur = cur.next.as_mut().unwrap().next.as_mut().unwrap(); + } else { + cur.next = Some(node); + cur = cur.next.as_mut().unwrap(); + } + } + dummy_head.next + } +} +``` + +```rust +// 递归 +impl Solution { + pub fn swap_pairs(head: Option>) -> Option> { + if head.is_none() || head.as_ref().unwrap().next.is_none() { + return head; + } + + let mut node = head.unwrap(); + + if let Some(mut next) = node.next.take() { + node.next = Solution::swap_pairs(next.next); + next.next = Some(node); + Some(next) + } else { + Some(node) + } + } +} +``` + +### C# +```csharp +// 虚拟头结点 +public ListNode SwapPairs(ListNode head) +{ + var dummyHead = new ListNode(); + dummyHead.next = head; + ListNode cur = dummyHead; + while (cur.next != null && cur.next.next != null) + { + ListNode tmp1 = cur.next; + ListNode tmp2 = cur.next.next.next; + + cur.next = cur.next.next; + cur.next.next = tmp1; + cur.next.next.next = tmp2; + + cur = cur.next.next; + } + return dummyHead.next; +} +``` +``` C# +// 递归 +public ListNode SwapPairs(ListNode head) +{ + if (head == null || head.next == null) return head; + var cur = head.next; + head.next = SwapPairs(head.next.next); + cur.next = head; + return cur; +} +``` + diff --git "a/problems/0027.\347\247\273\351\231\244\345\205\203\347\264\240.md" "b/problems/0027.\347\247\273\351\231\244\345\205\203\347\264\240.md" new file mode 100755 index 0000000000..47e05eec6a --- /dev/null +++ "b/problems/0027.\347\247\273\351\231\244\345\205\203\347\264\240.md" @@ -0,0 +1,519 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 27. 移除元素 + +[力扣题目链接](https://leetcode.cn/problems/remove-element/) + +给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。 + +不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并**原地**修改输入数组。 + +元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 + +示例 1: +给定 nums = [3,2,2,3], val = 3, +函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 +你不需要考虑数组中超出新长度后面的元素。 + +示例 2: +给定 nums = [0,1,2,2,3,0,4,2], val = 2, +函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 + +**你不需要考虑数组中超出新长度后面的元素。** + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[数组中移除元素并不容易!LeetCode:27. 移除元素](https://www.bilibili.com/video/BV12A4y1Z7LP),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +有的同学可能说了,多余的元素,删掉不就得了。 + +**要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。** + +数组的基础知识可以看这里[程序员算法面试中,必须掌握的数组理论知识](https://programmercarl.com/数组理论基础.html)。 + +### 暴力解法 + +这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。 + +删除过程如下: + +![27.移除元素-暴力解法](https://file1.kamacoder.com/i/algo/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E6%9A%B4%E5%8A%9B%E8%A7%A3%E6%B3%95.gif) + +很明显暴力解法的时间复杂度是O(n^2),这道题目暴力解法在leetcode上是可以过的。 + +代码如下: + +```CPP +// 时间复杂度:O(n^2) +// 空间复杂度:O(1) +class Solution { +public: + int removeElement(vector& nums, int val) { + int size = nums.size(); + for (int i = 0; i < size; i++) { + if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位 + for (int j = i + 1; j < size; j++) { + nums[j - 1] = nums[j]; + } + i--; // 因为下标i以后的数值都向前移动了一位,所以i也向前移动一位 + size--; // 此时数组的大小-1 + } + } + return size; + + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + +### 双指针法 + +双指针法(快慢指针法): **通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** + +定义快慢指针 + +* 快指针:寻找新数组的元素 ,新数组就是不含有目标元素的数组 +* 慢指针:指向更新 新数组下标的位置 + +很多同学这道题目做的很懵,就是不理解 快慢指针究竟都是什么含义,所以一定要明确含义,后面的思路就更容易理解了。 + +删除过程如下: + +![27.移除元素-双指针法](https://file1.kamacoder.com/i/algo/27.%E7%A7%BB%E9%99%A4%E5%85%83%E7%B4%A0-%E5%8F%8C%E6%8C%87%E9%92%88%E6%B3%95.gif) + +很多同学不了解 + + +**双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。** + +后续都会一一介绍到,本题代码如下: + +```CPP +// 时间复杂度:O(n) +// 空间复杂度:O(1) +class Solution { +public: + int removeElement(vector& nums, int val) { + int slowIndex = 0; + for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { + if (val != nums[fastIndex]) { + nums[slowIndex++] = nums[fastIndex]; + } + } + return slowIndex; + } +}; +``` +注意这些实现方法并没有改变元素的相对位置! + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + + + +## 相关题目推荐 + +* [26.删除排序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) +* [283.移动零](https://leetcode.cn/problems/move-zeroes/) +* [844.比较含退格的字符串](https://leetcode.cn/problems/backspace-string-compare/) +* [977.有序数组的平方](https://leetcode.cn/problems/squares-of-a-sorted-array/) + +## 其他语言版本 + +### Java: +```java +class Solution { + public int removeElement(int[] nums, int val) { + // 暴力法 + int n = nums.length; + for (int i = 0; i < n; i++) { + if (nums[i] == val) { + for (int j = i + 1; j < n; j++) { + nums[j - 1] = nums[j]; + } + i--; + n--; + } + } + return n; + } +} +``` +```java +class Solution { + public int removeElement(int[] nums, int val) { + // 快慢指针 + int slowIndex = 0; + for (int fastIndex = 0; fastIndex < nums.length; fastIndex++) { + if (nums[fastIndex] != val) { + nums[slowIndex] = nums[fastIndex]; + slowIndex++; + } + } + return slowIndex; + } +} +``` +```java +//相向双指针法 +class Solution { + public int removeElement(int[] nums, int val) { + int left = 0; + int right = nums.length - 1; + while(right >= 0 && nums[right] == val) right--; //将right移到从右数第一个值不为val的位置 + while(left <= right) { + if(nums[left] == val) { //left位置的元素需要移除 + //将right位置的元素移到left(覆盖),right位置移除 + nums[left] = nums[right]; + right--; + } + left++; + while(right >= 0 && nums[right] == val) right--; + } + return left; + } +} +``` + +```java +// 相向双指针法(版本二) +class Solution { + public int removeElement(int[] nums, int val) { + int left = 0; + int right = nums.length - 1; + while(left <= right){ + if(nums[left] == val){ + nums[left] = nums[right]; + right--; + }else { + // 这里兼容了right指针指向的值与val相等的情况 + left++; + } + } + return left; + } +} +``` + +### Python: + + +``` python 3 +(版本一)快慢指针法 +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + # 快慢指针 + fast = 0 # 快指针 + slow = 0 # 慢指针 + size = len(nums) + while fast < size: # 不加等于是因为,a = size 时,nums[a] 会越界 + # slow 用来收集不等于 val 的值,如果 fast 对应值不等于 val,则把它与 slow 替换 + if nums[fast] != val: + nums[slow] = nums[fast] + slow += 1 + fast += 1 + return slow +``` + +``` python 3 +(版本二)暴力法 +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + i, l = 0, len(nums) + while i < l: + if nums[i] == val: # 找到等于目标值的节点 + for j in range(i+1, l): # 移除该元素,并将后面元素向前平移 + nums[j - 1] = nums[j] + l -= 1 + i -= 1 + i += 1 + return l + +``` + +``` python 3 +# 相向双指针法 +# 时间复杂度 O(n) +# 空间复杂度 O(1) +class Solution: + def removeElement(self, nums: List[int], val: int) -> int: + n = len(nums) + left, right = 0, n - 1 + while left <= right: + while left <= right and nums[left] != val: + left += 1 + while left <= right and nums[right] == val: + right -= 1 + if left < right: + nums[left] = nums[right] + left += 1 + right -= 1 + return left + +``` + +### Go: + +```go +// 暴力法 +// 时间复杂度 O(n^2) +// 空间复杂度 O(1) +func removeElement(nums []int, val int) int { + size := len(nums) + for i := 0; i < size; i ++ { + if nums[i] == val { + for j := i + 1; j < size; j ++ { + nums[j - 1] = nums[j] + } + i -- + size -- + } + } + return size +} +``` +```go +// 快慢指针法 +// 时间复杂度 O(n) +// 空间复杂度 O(1) +func removeElement(nums []int, val int) int { + // 初始化慢指针 slow + slow := 0 + // 通过 for 循环移动快指针 fast + // 当 fast 指向的元素等于 val 时,跳过 + // 否则,将该元素写入 slow 指向的位置,并将 slow 后移一位 + for fast := 0; fast < len(nums); fast++ { + if nums[fast] == val { + continue + } + nums[slow] = nums[fast] + slow++ + } + + return slow +} +``` +```go +//相向双指针法 +func removeElement(nums []int, val int) int { + // 有点像二分查找的左闭右闭区间 所以下面是<= + left := 0 + right := len(nums) - 1 + for left <= right { + // 不断寻找左侧的val和右侧的非val 找到时交换位置 目的是将val全覆盖掉 + for left <= right && nums[left] != val { + left++ + } + for left <= right && nums[right] == val { + right-- + } + //各自找到后开始覆盖 覆盖后继续寻找 + if left < right { + nums[left] = nums[right] + left++ + right-- + } + } + return left +} +``` + +### JavaScript: + +```javascript +//时间复杂度:O(n) +//空间复杂度:O(1) +var removeElement = (nums, val) => { + let k = 0; + for(let i = 0;i < nums.length;i++){ + if(nums[i] != val){ + nums[k++] = nums[i] + } + } + return k; +}; +``` + +### TypeScript: + +```typescript +function removeElement(nums: number[], val: number): number { + let slowIndex: number = 0, fastIndex: number = 0; + while (fastIndex < nums.length) { + if (nums[fastIndex] !== val) { + nums[slowIndex++] = nums[fastIndex]; + } + fastIndex++; + } + return slowIndex; +}; +``` + +### Ruby: + +```ruby +def remove_element(nums, val) + i = 0 + nums.each_index do |j| + if nums[j] != val + nums[i] = nums[j] + i+=1 + end + end + i +end +``` +### Rust: + +```rust +impl Solution { + pub fn remove_element(nums: &mut Vec, val: i32) -> i32 { + let mut slowIdx = 0; + for pos in (0..nums.len()) { + if nums[pos]!=val { + nums[slowIdx] = nums[pos]; + slowIdx += 1; + } + } + return (slowIdx) as i32; + } +} +``` + +### Swift: + +```swift +func removeElement(_ nums: inout [Int], _ val: Int) -> Int { + var slowIndex = 0 + + for fastIndex in 0.. nums, int val) { + //相向双指针法 + var left = 0; + var right = nums.length - 1; + while (left <= right) { + //寻找左侧的val,将其被右侧非val覆盖 + if (nums[left] == val) { + while (nums[right] == val&&left<=right) { + right--; + if (right < 0) { + return 0; + } + } + nums[left] = nums[right--]; + } else { + left++; + } + } + //覆盖后可以将0至left部分视为所需部分 + return left; +} + +``` + diff --git "a/problems/0028.\345\256\236\347\216\260strStr.md" "b/problems/0028.\345\256\236\347\216\260strStr.md" new file mode 100755 index 0000000000..ef8a6c58e6 --- /dev/null +++ "b/problems/0028.\345\256\236\347\216\260strStr.md" @@ -0,0 +1,1520 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 在一个串中查找是否出现过另一个串,这是KMP的看家本领。 + +# 28. 实现 strStr() + +[力扣题目链接](https://leetcode.cn/problems/find-the-index-of-the-first-occurrence-in-a-string/) + +实现 strStr() 函数。 + +给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。 + +示例 1: +输入: haystack = "hello", needle = "ll" +输出: 2 + +示例 2: +输入: haystack = "aaaaa", needle = "bba" +输出: -1 + +说明: +当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 +对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。 + +## 算法公开课 + +本题是KMP 经典题目。以下文字如果看不进去,可以看[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html),相信结合视频再看本篇题解,更有助于大家对本题的理解。 + +* [帮你把KMP算法学个通透!B站(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) +* [帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) + + +## 思路 + +KMP的经典思想就是:**当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。** + +本篇将以如下顺序来讲解KMP, + + +* 什么是KMP +* KMP有什么用 +* 什么是前缀表 +* 为什么一定要用前缀表 +* 如何计算前缀表 +* 前缀表与next数组 +* 使用next数组来匹配 +* 时间复杂度分析 +* 构造next数组 +* 使用next数组来做匹配 +* 前缀表统一减一 C++代码实现 +* 前缀表(不减一)C++实现 +* 总结 + + +读完本篇可以顺便把leetcode上28.实现strStr()题目做了。 + + +### 什么是KMP + +说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。 + +因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP + +### KMP有什么用 + +KMP主要应用在字符串匹配上。 + +KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。** + +所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。 + +其实KMP的代码不好理解,一些同学甚至直接把KMP代码的模板背下来。 + +没有彻底搞懂,懵懵懂懂就把代码背下来太容易忘了。 + +不仅面试的时候可能写不出来,如果面试官问:**next数组里的数字表示的是什么,为什么这么表示?** + +估计大多数候选人都是懵逼的。 + +下面Carl就带大家把KMP的精髓,next数组弄清楚。 + +### 什么是前缀表 + +写过KMP的同学,一定都写过next数组,那么这个next数组究竟是个啥呢? + +next数组就是一个前缀表(prefix table)。 + +前缀表有什么作用呢? + +**前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。** + +为了清楚地了解前缀表的来历,我们来举一个例子: + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +请记住文本串和模式串的作用,对于理解下文很重要,要不然容易看懵。所以说三遍: + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +如动画所示: + +![KMP详解1](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B21.gif) + +动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说到。 + +可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,发现不匹配,此时就要从头匹配了。 + +但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。 + +此时就要问了**前缀表是如何记录的呢?** + +首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,再重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。 + +那么什么是前缀表:**记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** + +### 最长公共前后缀 + +文章中字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**。 + +**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。 + +**正确理解什么是前缀什么是后缀很重要**! + +那么网上清一色都说 “kmp 最长公共前后缀” 又是什么回事呢? + +我查了一遍 算法导论 和 算法4里KMP的章节,都没有提到 “最长公共前后缀”这个词,也不知道从哪里来了,我理解是用“最长相等前后缀” 更准确一些。 + +**因为前缀表要求的就是相同前后缀的长度。** + +而最长公共前后缀里面的“公共”,更像是说前缀和后缀公共的长度。这其实并不是前缀表所需要的。 + +所以字符串a的最长相等前后缀为0。 +字符串aa的最长相等前后缀为1。 +字符串aaa的最长相等前后缀为2。 +等等.....。 + + +### 为什么一定要用前缀表 + +这就是前缀表,那为啥就能告诉我们 上次匹配的位置,并跳过去呢? + +回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图: +KMP精讲1 + + +然后就找到了下标2,指向b,继续匹配:如图: +KMP精讲2 + +以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! + +**下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面重新匹配就可以了。** + +所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 + +**很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。** + +### 如何计算前缀表 + +接下来就要说一说怎么计算前缀表。 + +如图: + +KMP精讲5 + +长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。) + +KMP精讲6 + +长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。 + +KMP精讲7 + +长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。 + +以此类推: +长度为前4个字符的子串`aaba`,最长相同前后缀的长度为1。 +长度为前5个字符的子串`aabaa`,最长相同前后缀的长度为2。 +长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。 + +那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: +KMP精讲8 + +可以看出模式串与前缀表对应位置的数字表示的就是:**下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** + +再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示: + +![KMP精讲2](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B22.gif) + +找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。 + +为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。 + +所以要看前一位的 前缀表的数值。 + +前一个字符的前缀表的数值是2, 所以把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。 + +最后就在文本串中找到了和模式串匹配的子串了。 + +### 前缀表与next数组 + +很多KMP算法的实现都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢? + +next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。 + +为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。 + +其实**这并不涉及到KMP的原理,而是具体实现,next数组既可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。** + +后面我会提供两种不同的实现代码,大家就明白了。 + +### 使用next数组来匹配 + +**以下我们以前缀表统一减一之后的next数组来做演示**。 + +有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。 + +注意next数组是新前缀表(旧前缀表统一减一了)。 + +匹配过程动画如下: + +![KMP精讲4](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B24.gif) + +### 时间复杂度分析 + +其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。 + +暴力的解法显而易见是O(n × m),所以**KMP在字符串匹配中极大地提高了搜索的效率。** + +为了和力扣题目28.实现strStr保持一致,方便大家理解,以下文章统称haystack为文本串, needle为模式串。 + +都知道使用KMP算法,一定要构造next数组。 + +### 构造next数组 + +我们定义一个函数getNext来构建next数组,函数参数为指向next数组的指针,和一个字符串。 代码如下: + +``` +void getNext(int* next, const string& s) +``` + +**构造next数组其实就是计算模式串s,前缀表的过程。** 主要有如下三步: + +1. 初始化 +2. 处理前后缀不相同的情况 +3. 处理前后缀相同的情况 + +接下来我们详解一下。 + +1. 初始化: + +定义两个指针i和j,j指向前缀末尾位置,i指向后缀末尾位置。 + +然后还要对next数组进行初始化赋值,如下: + +```cpp +int j = -1; +next[0] = j; +``` + +j 为什么要初始化为 -1呢,因为之前说过 前缀表要统一减一的操作仅仅是其中的一种实现,我们这里选择j初始化为-1,下文我还会给出j不初始化为-1的实现代码。 + +next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j) + +所以初始化next[0] = j 。 + + +2. 处理前后缀不相同的情况 + + +因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。 + +所以遍历模式串s的循环下标i 要从 1开始,代码如下: + +```cpp +for (int i = 1; i < s.size(); i++) { +``` + +如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。 + +怎么回退呢? + +next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。 + +那么 s[i] 与 s[j+1] 不相同,就要找 j+1前一个元素在next数组里的值(就是next[j])。 + +所以,处理前后缀不相同的情况代码如下: + +```cpp +while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 +    j = next[j]; // 向前回退 +} +``` + +3. 处理前后缀相同的情况 + +如果 s[i] 与 s[j + 1] 相同,那么就同时向后移动i 和j 说明找到了相同的前后缀,同时还要将j(前缀的长度)赋给next[i], 因为next[i]要记录相同前后缀的长度。 + +代码如下: + +``` +if (s[i] == s[j + 1]) { // 找到相同的前后缀 +    j++; +} +next[i] = j; +``` + +最后整体构建next数组的函数代码如下: + +```CPP +void getNext(int* next, const string& s){ +    int j = -1; +    next[0] = j; +    for(int i = 1; i < s.size(); i++) { // 注意i从1开始 +        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 +            j = next[j]; // 向前回退 +        } +        if (s[i] == s[j + 1]) { // 找到相同的前后缀 +            j++; +        } +        next[i] = j; // 将j(前缀的长度)赋给next[i] +    } +} +``` + + +代码构造next数组的逻辑流程动画如下: + +![KMP精讲3](https://file1.kamacoder.com/i/algo/KMP%E7%B2%BE%E8%AE%B23.gif) + +得到了next数组之后,就要用这个来做匹配了。 + +### 使用next数组来做匹配 + +在文本串s里 找是否出现过模式串t。 + +定义两个下标j 指向模式串起始位置,i指向文本串起始位置。 + +那么j初始值依然为-1,为什么呢? **依然因为next数组里记录的起始位置为-1。** + +i就从0开始,遍历文本串,代码如下: + +```cpp +for (int i = 0; i < s.size(); i++)  +``` + +接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。 + +如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。 + +代码如下: + +```cpp +while(j >= 0 && s[i] != t[j + 1]) { +    j = next[j]; +} +``` + +如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下: + +```cpp +if (s[i] == t[j + 1]) { +    j++; // i的增加在for循环里 +} +``` + +如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。 + +本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。 + +代码如下: + +```cpp +if (j == (t.size() - 1) ) { +    return (i - t.size() + 1); +} +``` + +那么使用next数组,用模式串匹配文本串的整体代码如下: + +```CPP +int j = -1; // 因为next数组里记录的起始位置为-1 +for (int i = 0; i < s.size(); i++) { // 注意i就从0开始 +    while(j >= 0 && s[i] != t[j + 1]) { // 不匹配 +        j = next[j]; // j 寻找之前匹配的位置 +    } +    if (s[i] == t[j + 1]) { // 匹配,j和i同时向后移动 +        j++; // i的增加在for循环里 +    } +    if (j == (t.size() - 1) ) { // 文本串s里出现了模式串t +        return (i - t.size() + 1); +    } +} +``` + +此时所有逻辑的代码都已经写出来了,力扣 28.实现strStr 题目的整体代码如下: + +### 前缀表统一减一 C++代码实现 + +```CPP +class Solution { +public: +    void getNext(int* next, const string& s) { +        int j = -1; +        next[0] = j; +        for(int i = 1; i < s.size(); i++) { // 注意i从1开始 +            while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 +                j = next[j]; // 向前回退 +            } +            if (s[i] == s[j + 1]) { // 找到相同的前后缀 +                j++; +            } +            next[i] = j; // 将j(前缀的长度)赋给next[i] +        } +    } + int strStr(string haystack, string needle) { + if (needle.size() == 0) { + return 0; + } + vector next(needle.size()); + getNext(&next[0], needle); + int j = -1; // // 因为next数组里记录的起始位置为-1 + for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始 + while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配 + j = next[j]; // j 寻找之前匹配的位置 + } + if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动 + j++; // i的增加在for循环里 + } + if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t + return (i - needle.size() + 1); + } + } + return -1; + } +}; + +``` +* 时间复杂度: O(n + m) +* 空间复杂度: O(m), 只需要保存字符串needle的前缀表 + +### 前缀表(不减一)C++实现 + +那么前缀表就不减一了,也不右移的,到底行不行呢? + +**行!** + +我之前说过,这仅仅是KMP算法实现上的问题,如果就直接使用前缀表可以换一种回退方式,找j=next[j-1] 来进行回退。 + +主要就是j=next[x]这一步最为关键! + +我给出的getNext的实现为:(前缀表统一减一) + +```CPP +void getNext(int* next, const string& s) { +    int j = -1; +    next[0] = j; +    for(int i = 1; i < s.size(); i++) { // 注意i从1开始 +        while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 +            j = next[j]; // 向前回退 +        } +        if (s[i] == s[j + 1]) { // 找到相同的前后缀 +            j++; +        } +        next[i] = j; // 将j(前缀的长度)赋给next[i] +    } +} + +``` +此时如果输入的模式串为aabaaf,对应的next为-1 0 -1 0 1 -1。 + +这里j和next[0]初始化为-1,整个next数组是以 前缀表减一之后的效果来构建的。 + +那么前缀表不减一来构建next数组,代码如下: + +```CPP + void getNext(int* next, const string& s) { + int j = 0; + next[0] = 0; + for(int i = 1; i < s.size(); i++) { + while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作 + j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了 + } + if (s[i] == s[j]) { + j++; + } + next[i] = j; + } + } + +``` + +此时如果输入的模式串为aabaaf,对应的next为 0 1 0 1 2 0,(其实这就是前缀表的数值了)。 + +那么用这样的next数组也可以用来做匹配,代码要有所改动。 + +实现代码如下: + +```CPP +class Solution { +public: + void getNext(int* next, const string& s) { + int j = 0; + next[0] = 0; + for(int i = 1; i < s.size(); i++) { + while (j > 0 && s[i] != s[j]) { + j = next[j - 1]; + } + if (s[i] == s[j]) { + j++; + } + next[i] = j; + } + } + int strStr(string haystack, string needle) { + if (needle.size() == 0) { + return 0; + } + vector next(needle.size()); + getNext(&next[0], needle); + int j = 0; + for (int i = 0; i < haystack.size(); i++) { + while(j > 0 && haystack[i] != needle[j]) { + j = next[j - 1]; + } + if (haystack[i] == needle[j]) { + j++; + } + if (j == needle.size() ) { + return (i - needle.size() + 1); + } + } + return -1; + } +}; +``` +* 时间复杂度: O(n + m) +* 空间复杂度: O(m) + + +## 总结 + +我们介绍了什么是KMP,KMP可以解决什么问题,然后分析KMP算法里的next数组,知道了next数组就是前缀表,再分析为什么要是前缀表而不是什么其他表。 + +接着从给出的模式串中,我们一步一步的推导出了前缀表,得出前缀表无论是统一减一还是不减一得到的next数组仅仅是kmp的实现方式的不同。 + +其中还分析了KMP算法的时间复杂度,并且和暴力方法做了对比。 + +然后先用前缀表统一减一得到的next数组,求得文本串s里是否出现过模式串t,并给出了具体分析代码。 + +又给出了直接用前缀表作为next数组,来做匹配的实现代码。 + +可以说把KMP的每一个细微的细节都扣了出来,毫无遮掩的展示给大家了! + + +## 其他语言版本 + +### Java: +```Java +class Solution { + /** + 牺牲空间,换取最直白的暴力法 + 时间复杂度 O(n * m) + 空间 O(n + m) + */ + public int strStr(String haystack, String needle) { + // 获取 haystack 和 needle 的长度 + int n = haystack.length(), m = needle.length(); + // 将字符串转换为字符数组,方便索引操作 + char[] s = haystack.toCharArray(), p = needle.toCharArray(); + + // 遍历 haystack 字符串 + for (int i = 0; i < n - m + 1; i++) { + // 初始化匹配的指针 + int a = i, b = 0; + // 循环检查 needle 是否在当前位置开始匹配 + while (b < m && s[a] == p[b]) { + // 如果当前字符匹配,则移动指针 + a++; + b++; + } + // 如果 b 等于 m,说明 needle 已经完全匹配,返回当前位置 i + if (b == m) return i; + } + + // 如果遍历完毕仍未找到匹配的子串,则返回 -1 + return -1; + } +} +``` + +```Java +class Solution { + /** + * 基于窗口滑动的算法 + *

+ * 时间复杂度:O(m*n) + * 空间复杂度:O(1) + * 注:n为haystack的长度,m为needle的长度 + */ + public int strStr(String haystack, String needle) { + int m = needle.length(); + // 当 needle 是空字符串时我们应当返回 0 + if (m == 0) { + return 0; + } + int n = haystack.length(); + if (n < m) { + return -1; + } + int i = 0; + int j = 0; + while (i < n - m + 1) { + // 找到首字母相等 + while (i < n && haystack.charAt(i) != needle.charAt(j)) { + i++; + } + if (i == n) {// 没有首字母相等的 + return -1; + } + // 遍历后续字符,判断是否相等 + i++; + j++; + while (i < n && j < m && haystack.charAt(i) == needle.charAt(j)) { + i++; + j++; + } + if (j == m) {// 找到 + return i - j; + } else {// 未找到 + i -= j - 1; + j = 0; + } + } + return -1; + } +} +``` + +```java +// 方法一 +class Solution { + public void getNext(int[] next, String s){ + int j = -1; + next[0] = j; + for (int i = 1; i < s.length(); i++){ + while(j >= 0 && s.charAt(i) != s.charAt(j+1)){ + j=next[j]; + } + + if(s.charAt(i) == s.charAt(j+1)){ + j++; + } + next[i] = j; + } + } + public int strStr(String haystack, String needle) { + if(needle.length()==0){ + return 0; + } + + int[] next = new int[needle.length()]; + getNext(next, needle); + int j = -1; + for(int i = 0; i < haystack.length(); i++){ + while(j>=0 && haystack.charAt(i) != needle.charAt(j+1)){ + j = next[j]; + } + if(haystack.charAt(i) == needle.charAt(j+1)){ + j++; + } + if(j == needle.length()-1){ + return (i-needle.length()+1); + } + } + + return -1; + } +} +``` + +```Java +class Solution { + //前缀表(不减一)Java实现 + public int strStr(String haystack, String needle) { + if (needle.length() == 0) return 0; + int[] next = new int[needle.length()]; + getNext(next, needle); + + int j = 0; + for (int i = 0; i < haystack.length(); i++) { + while (j > 0 && needle.charAt(j) != haystack.charAt(i)) + j = next[j - 1]; + if (needle.charAt(j) == haystack.charAt(i)) + j++; + if (j == needle.length()) + return i - needle.length() + 1; + } + return -1; + + } + + private void getNext(int[] next, String s) { + int j = 0; + next[0] = 0; + for (int i = 1; i < s.length(); i++) { + while (j > 0 && s.charAt(j) != s.charAt(i)) + j = next[j - 1]; + if (s.charAt(j) == s.charAt(i)) + j++; + next[i] = j; + } + } +} +``` + +### Python3: +(版本一)前缀表(减一) + +```python +class Solution: + def getNext(self, next, s): + j = -1 + next[0] = j + for i in range(1, len(s)): + while j >= 0 and s[i] != s[j+1]: + j = next[j] + if s[i] == s[j+1]: + j += 1 + next[i] = j + + def strStr(self, haystack: str, needle: str) -> int: + if not needle: + return 0 + next = [0] * len(needle) + self.getNext(next, needle) + j = -1 + for i in range(len(haystack)): + while j >= 0 and haystack[i] != needle[j+1]: + j = next[j] + if haystack[i] == needle[j+1]: + j += 1 + if j == len(needle) - 1: + return i - len(needle) + 1 + return -1 +``` +(版本二)前缀表(不减一) + +```python +class Solution: + def getNext(self, next: List[int], s: str) -> None: + j = 0 + next[0] = 0 + for i in range(1, len(s)): + while j > 0 and s[i] != s[j]: + j = next[j - 1] + if s[i] == s[j]: + j += 1 + next[i] = j + + def strStr(self, haystack: str, needle: str) -> int: + if len(needle) == 0: + return 0 + next = [0] * len(needle) + self.getNext(next, needle) + j = 0 + for i in range(len(haystack)): + while j > 0 and haystack[i] != needle[j]: + j = next[j - 1] + if haystack[i] == needle[j]: + j += 1 + if j == len(needle): + return i - len(needle) + 1 + return -1 +``` + + +(版本三)暴力法 +```python +class Solution(object): + def strStr(self, haystack, needle): + """ + :type haystack: str + :type needle: str + :rtype: int + """ + m, n = len(haystack), len(needle) + for i in range(m): + if haystack[i:i+n] == needle: + return i + return -1 +``` +(版本四)使用 index +```python +class Solution: + def strStr(self, haystack: str, needle: str) -> int: + try: + return haystack.index(needle) + except ValueError: + return -1 +``` +(版本五)使用 find +```python +class Solution: + def strStr(self, haystack: str, needle: str) -> int: + return haystack.find(needle) + +``` + +### Go: + +```go +// 方法一:前缀表使用减1实现 + +// getNext 构造前缀表next +// params: +// next 前缀表数组 +// s 模式串 +func getNext(next []int, s string) { + j := -1 // j表示 最长相等前后缀长度 + next[0] = j + + for i := 1; i < len(s); i++ { + for j >= 0 && s[i] != s[j+1] { + j = next[j] // 回退前一位 + } + if s[i] == s[j+1] { + j++ + } + next[i] = j // next[i]是i(包括i)之前的最长相等前后缀长度 + } +} +func strStr(haystack string, needle string) int { + if len(needle) == 0 { + return 0 + } + next := make([]int, len(needle)) + getNext(next, needle) + j := -1 // 模式串的起始位置 next为-1 因此也为-1 + for i := 0; i < len(haystack); i++ { + for j >= 0 && haystack[i] != needle[j+1] { + j = next[j] // 寻找下一个匹配点 + } + if haystack[i] == needle[j+1] { + j++ + } + if j == len(needle)-1 { // j指向了模式串的末尾 + return i - len(needle) + 1 + } + } + return -1 +} +``` + +```go +// 方法二: 前缀表无减一或者右移 + +// getNext 构造前缀表next +// params: +// next 前缀表数组 +// s 模式串 +func getNext(next []int, s string) { + j := 0 + next[0] = j + for i := 1; i < len(s); i++ { + for j > 0 && s[i] != s[j] { + j = next[j-1] + } + if s[i] == s[j] { + j++ + } + next[i] = j + } +} +func strStr(haystack string, needle string) int { + n := len(needle) + if n == 0 { + return 0 + } + j := 0 + next := make([]int, n) + getNext(next, needle) + for i := 0; i < len(haystack); i++ { + for j > 0 && haystack[i] != needle[j] { + j = next[j-1] // 回退到j的前一位 + } + if haystack[i] == needle[j] { + j++ + } + if j == n { + return i - n + 1 + } + } + return -1 +} +``` + +### JavaScript: + +> 前缀表统一减一 + +```javascript +/** + * @param {string} haystack + * @param {string} needle + * @return {number} + */ +var strStr = function (haystack, needle) { + if (needle.length === 0) + return 0; + + const getNext = (needle) => { + let next = []; + let j = -1; + next.push(j); + + for (let i = 1; i < needle.length; ++i) { + while (j >= 0 && needle[i] !== needle[j + 1]) + j = next[j]; + if (needle[i] === needle[j + 1]) + j++; + next.push(j); + } + + return next; + } + + let next = getNext(needle); + let j = -1; + for (let i = 0; i < haystack.length; ++i) { + while (j >= 0 && haystack[i] !== needle[j + 1]) + j = next[j]; + if (haystack[i] === needle[j + 1]) + j++; + if (j === needle.length - 1) + return (i - needle.length + 1); + } + + return -1; +}; +``` + +> 前缀表统一不减一 + +```javascript +/** + * @param {string} haystack + * @param {string} needle + * @return {number} + */ +var strStr = function (haystack, needle) { + if (needle.length === 0) + return 0; + + const getNext = (needle) => { + let next = []; + let j = 0; + next.push(j); + + for (let i = 1; i < needle.length; ++i) { + while (j > 0 && needle[i] !== needle[j]) + j = next[j - 1]; + if (needle[i] === needle[j]) + j++; + next.push(j); + } + + return next; + } + + let next = getNext(needle); + let j = 0; + for (let i = 0; i < haystack.length; ++i) { + while (j > 0 && haystack[i] !== needle[j]) + j = next[j - 1]; + if (haystack[i] === needle[j]) + j++; + if (j === needle.length) + return (i - needle.length + 1); + } + + return -1; +}; +``` + +### TypeScript: + +> 前缀表统一减一 + +```typescript +function strStr(haystack: string, needle: string): number { + function getNext(str: string): number[] { + let next: number[] = []; + let j: number = -1; + next[0] = j; + for (let i = 1, length = str.length; i < length; i++) { + while (j >= 0 && str[i] !== str[j + 1]) { + j = next[j]; + } + if (str[i] === str[j + 1]) { + j++; + } + next[i] = j; + } + return next; + } + if (needle.length === 0) return 0; + let next: number[] = getNext(needle); + let j: number = -1; + for (let i = 0, length = haystack.length; i < length; i++) { + while (j >= 0 && haystack[i] !== needle[j + 1]) { + j = next[j]; + } + if (haystack[i] === needle[j + 1]) { + if (j === needle.length - 2) { + return i - j - 1; + } + j++; + } + } + return -1; +}; +``` + +> 前缀表不减一 + +```typescript +// 不减一版本 +function strStr(haystack: string, needle: string): number { + function getNext(str: string): number[] { + let next: number[] = []; + let j: number = 0; + next[0] = j; + for (let i = 1, length = str.length; i < length; i++) { + while (j > 0 && str[i] !== str[j]) { + j = next[j - 1]; + } + if (str[i] === str[j]) { + j++; + } + next[i] = j; + } + return next; + } + if (needle.length === 0) return 0; + let next: number[] = getNext(needle); + let j: number = 0; + for (let i = 0, length = haystack.length; i < length; i++) { + while (j > 0 && haystack[i] !== needle[j]) { + j = next[j - 1]; + } + if (haystack[i] === needle[j]) { + if (j === needle.length - 1) { + return i - j; + } + j++; + } + } + return -1; +} +``` + +### Swift: + +> 前缀表统一减一 + +```swift +func strStr(_ haystack: String, _ needle: String) -> Int { + + let s = Array(haystack), p = Array(needle) + guard p.count != 0 else { return 0 } + + // 2 pointer + var j = -1 + var next = [Int](repeating: -1, count: needle.count) + // KMP + getNext(&next, needle: p) + for i in 0 ..< s.count { + while j >= 0 && s[i] != p[j + 1] { + //不匹配之后寻找之前匹配的位置 + j = next[j] + } + if s[i] == p[j + 1] { + //匹配,双指针同时后移 + j += 1 + } + if j == (p.count - 1) { + //出现匹配字符串 + return i - p.count + 1 + } + } + return -1 +} + +//前缀表统一减一 +func getNext(_ next: inout [Int], needle: [Character]) { + + var j: Int = -1 + next[0] = j + + // i 从 1 开始 + for i in 1 ..< needle.count { + while j >= 0 && needle[i] != needle[j + 1] { + j = next[j] + } + if needle[i] == needle[j + 1] { + j += 1; + } + next[i] = j + } + print(next) +} + +``` + +> 前缀表右移 + +```swift +func strStr(_ haystack: String, _ needle: String) -> Int { + + let s = Array(haystack), p = Array(needle) + guard p.count != 0 else { return 0 } + + var j = 0 + var next = [Int].init(repeating: 0, count: p.count) + getNext(&next, p) + + for i in 0 ..< s.count { + + while j > 0 && s[i] != p[j] { + j = next[j] + } + + if s[i] == p[j] { + j += 1 + } + + if j == p.count { + return i - p.count + 1 + } + } + + return -1 + } + + // 前缀表后移一位,首位用 -1 填充 + func getNext(_ next: inout [Int], _ needle: [Character]) { + + guard needle.count > 1 else { return } + + var j = 0 + next[0] = j + + for i in 1 ..< needle.count-1 { + + while j > 0 && needle[i] != needle[j] { + j = next[j-1] + } + + if needle[i] == needle[j] { + j += 1 + } + + next[i] = j + } + next.removeLast() + next.insert(-1, at: 0) + } +``` + +> 前缀表统一不减一 +```swift + +func strStr(_ haystack: String, _ needle: String) -> Int { + + let s = Array(haystack), p = Array(needle) + guard p.count != 0 else { return 0 } + + var j = 0 + var next = [Int](repeating: 0, count: needle.count) + // KMP + getNext(&next, needle: p) + + for i in 0 ..< s.count { + while j > 0 && s[i] != p[j] { + j = next[j-1] + } + + if s[i] == p[j] { + j += 1 + } + + if j == p.count { + return i - p.count + 1 + } + } + return -1 + } + + //前缀表 + func getNext(_ next: inout [Int], needle: [Character]) { + + var j = 0 + next[0] = j + + for i in 1 ..< needle.count { + + while j>0 && needle[i] != needle[j] { + j = next[j-1] + } + + if needle[i] == needle[j] { + j += 1 + } + + next[i] = j + + } + } + +``` + +### PHP: + +> 前缀表统一减一 +```php +function strStr($haystack, $needle) { + if (strlen($needle) == 0) return 0; + $next= []; + $this->getNext($next,$needle); + + $j = -1; + for ($i = 0;$i < strlen($haystack); $i++) { // 注意i就从0开始 + while($j >= 0 && $haystack[$i] != $needle[$j + 1]) { + $j = $next[$j]; + } + if ($haystack[$i] == $needle[$j + 1]) { + $j++; + } + if ($j == (strlen($needle) - 1) ) { + return ($i - strlen($needle) + 1); + } + } + return -1; +} + +function getNext(&$next, $s){ + $j = -1; + $next[0] = $j; + for($i = 1; $i < strlen($s); $i++) { // 注意i从1开始 + while ($j >= 0 && $s[$i] != $s[$j + 1]) { + $j = $next[$j]; + } + if ($s[$i] == $s[$j + 1]) { + $j++; + } + $next[$i] = $j; + } +} +``` + +> 前缀表统一不减一 +```php +function strStr($haystack, $needle) { + if (strlen($needle) == 0) return 0; + $next= []; + $this->getNext($next,$needle); + + $j = 0; + for ($i = 0;$i < strlen($haystack); $i++) { // 注意i就从0开始 + while($j > 0 && $haystack[$i] != $needle[$j]) { + $j = $next[$j-1]; + } + if ($haystack[$i] == $needle[$j]) { + $j++; + } + if ($j == strlen($needle)) { + return ($i - strlen($needle) + 1); + } + } + return -1; +} + +function getNext(&$next, $s){ + $j = 0; + $next[0] = $j; + for($i = 1; $i < strlen($s); $i++) { // 注意i从1开始 + while ($j > 0 && $s[$i] != $s[$j]) { + $j = $next[$j-1]; + } + if ($s[$i] == $s[$j]) { + $j++; + } + $next[$i] = $j; + } +} +``` + +### Rust: + +> 前缀表统一不减一 +```Rust +impl Solution { + pub fn get_next(next: &mut Vec, s: &Vec) { + let len = s.len(); + let mut j = 0; + for i in 1..len { + while j > 0 && s[i] != s[j] { + j = next[j - 1]; + } + if s[i] == s[j] { + j += 1; + } + next[i] = j; + } + } + + pub fn str_str(haystack: String, needle: String) -> i32 { + let (haystack_len, needle_len) = (haystack.len(), needle.len()); + if haystack_len < needle_len { return -1;} + let (haystack, needle) = (haystack.chars().collect::>(), needle.chars().collect::>()); + let mut next: Vec = vec![0; haystack_len]; + Self::get_next(&mut next, &needle); + let mut j = 0; + for i in 0..haystack_len { + while j > 0 && haystack[i] != needle[j] { + j = next[j - 1]; + } + if haystack[i] == needle[j] { + j += 1; + } + if j == needle_len { + return (i - needle_len + 1) as i32; + } + } + return -1; + } +} +``` + +> 前缀表统一减一 + +```rust +impl Solution { + pub fn get_next(next_len: usize, s: &Vec) -> Vec { + let mut next = vec![-1; next_len]; + let mut j = -1; + for i in 1..s.len() { + while j >= 0 && s[(j + 1) as usize] != s[i] { + j = next[j as usize]; + } + if s[i] == s[(j + 1) as usize] { + j += 1; + } + next[i] = j; + } + next + } + pub fn str_str(haystack: String, needle: String) -> i32 { + if haystack.len() < needle.len() { + return -1; + } + let (haystack_chars, needle_chars) = ( + haystack.chars().collect::>(), + needle.chars().collect::>(), + ); + let mut j = -1; + let next = Self::get_next(needle.len(), &needle_chars); + for (i, v) in haystack_chars.into_iter().enumerate() { + while j >= 0 && v != needle_chars[(j + 1) as usize] { + j = next[j as usize]; + } + if v == needle_chars[(j + 1) as usize] { + j += 1; + } + if j == needle_chars.len() as i32 - 1 { + return (i - needle_chars.len() + 1) as i32; + } + } + -1 + } +} +``` + +>前缀表统一不减一 +```csharp +public int StrStr(string haystack, string needle) +{ + if (string.IsNullOrEmpty(needle)) + return 0; + + if (needle.Length > haystack.Length || string.IsNullOrEmpty(haystack)) + return -1; + + return KMP(haystack, needle); +} + +public int KMP(string haystack, string needle) +{ + int[] next = GetNext(needle); + int i = 0, j = 0; + while (i < haystack.Length) + { + if (haystack[i] == needle[j]) + { + i++; + j++; + } + if (j == needle.Length) + return i-j; + else if (i < haystack.Length && haystack[i] != needle[j]) + if (j != 0) + { + j = next[j - 1]; + } + else + { + i++; + } + } + return -1; +} + +public int[] GetNext(string needle) +{ + int[] next = new int[needle.Length]; + next[0] = 0; + int i = 1, j = 0; + while (i < needle.Length) + { + if (needle[i] == needle[j]) + { + next[i++] = ++j; + } + else + { + if (j == 0) + { + next[i++] = 0; + } + else + { + j = next[j - 1]; + } + } + } + return next; +} +``` + +### C: + +> 前缀表统一右移和减一 + +```c + +int *build_next(char* needle, int len) { + + int *next = (int *)malloc(len * sizeof(int)); + assert(next); // 确保分配成功 + + // 初始化next数组 + next[0] = -1; // next[0] 设置为 -1,表示没有有效前缀匹配 + if (len <= 1) { // 如果模式串长度小于等于 1,直接返回 + return next; + } + next[1] = 0; // next[1] 设置为 0,表示第一个字符没有公共前后缀 + + // 构建next数组, i 从模式串的第三个字符开始, j 指向当前匹配的最长前缀长度 + int i = 2, j = 0; + while (i < len) { + if (needle[i - 1] == needle[j]) { + j++; + next[i] = j; + i++; + } else if (j > 0) { + // 如果不匹配且 j > 0, 回退到次长匹配前缀的长度 + j = next[j]; + } else { + next[i] = 0; + i++; + } + } + return next; +} + +int strStr(char* haystack, char* needle) { + + int needle_len = strlen(needle); + int haystack_len = strlen(haystack); + + int *next = build_next(needle, needle_len); + + int i = 0, j = 0; // i 指向主串的当前起始位置, j 指向模式串的当前匹配位置 + while (i <= haystack_len - needle_len) { + if (haystack[i + j] == needle[j]) { + j++; + if (j == needle_len) { + free(next); + next = NULL + return i; + } + } else { + i += j - next[j]; // 调整主串的起始位置 + j = j > 0 ? next[j] : 0; + } + } + + free(next); + next = NULL; + return -1; +} +``` + diff --git "a/problems/0031.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" "b/problems/0031.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" new file mode 100755 index 0000000000..4bbf20fbb8 --- /dev/null +++ "b/problems/0031.\344\270\213\344\270\200\344\270\252\346\216\222\345\210\227.md" @@ -0,0 +1,269 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 31.下一个排列 + +[力扣题目链接](https://leetcode.cn/problems/next-permutation/) + +实现获取 下一个排列 的函数,算法需要将给定数字序列重新排列成字典序中下一个更大的排列。 + +如果不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。 + +必须 原地 修改,只允许使用额外常数空间。 + +示例 1: +* 输入:nums = [1,2,3] +* 输出:[1,3,2] + +示例 2: +* 输入:nums = [3,2,1] +* 输出:[1,2,3] + +示例 3: +* 输入:nums = [1,1,5] +* 输出:[1,5,1] + +示例 4: +* 输入:nums = [1] +* 输出:[1] + + +## 思路 + +一些同学可能手动写排列的顺序,都没有写对,那么写程序的话思路一定是有问题的了,我这里以1234为例子,把全排列都列出来。可以参考一下规律所在: + +``` +1 2 3 4 +1 2 4 3 +1 3 2 4 +1 3 4 2 +1 4 2 3 +1 4 3 2 +2 1 3 4 +2 1 4 3 +2 3 1 4 +2 3 4 1 +2 4 1 3 +2 4 3 1 +3 1 2 4 +3 1 4 2 +3 2 1 4 +3 2 4 1 +3 4 1 2 +3 4 2 1 +4 1 2 3 +4 1 3 2 +4 2 1 3 +4 2 3 1 +4 3 1 2 +4 3 2 1 +``` + +如图: + +以求1243为例,流程如图: + + + +对应的C++代码如下: + +```CPP +class Solution { +public: + void nextPermutation(vector& nums) { + for (int i = nums.size() - 1; i >= 0; i--) { + for (int j = nums.size() - 1; j > i; j--) { + if (nums[j] > nums[i]) { + swap(nums[j], nums[i]); + reverse(nums.begin() + i + 1, nums.end()); + return; + } + } + } + // 到这里了说明整个数组都是倒序了,反转一下便可 + reverse(nums.begin(), nums.end()); + } +}; +``` + +## 其他语言版本 + +### Java + +```java +class Solution { + public void nextPermutation(int[] nums) { + for (int i = nums.length - 1; i >= 0; i--) { + for (int j = nums.length - 1; j > i; j--) { + if (nums[j] > nums[i]) { + // 交换 + int temp = nums[i]; + nums[i] = nums[j]; + nums[j] = temp; + // [i + 1, nums.length) 内元素升序排序 + Arrays.sort(nums, i + 1, nums.length); + return; + } + } + } + Arrays.sort(nums); // 不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。 + } +} +``` +> 优化时间复杂度为O(N),空间复杂度为O(1) +```Java +class Solution { + public void nextPermutation(int[] nums) { + // 1.从后向前获取逆序区域的前一位 + int index = findIndex(nums); + // 判断数组是否处于最小组合状态 + if(index != 0){ + // 2.交换逆序区域刚好大于它的最小数字 + exchange(nums,index); + } + // 3.把原来的逆序区转为顺序 + reverse(nums,index); + } + + public static int findIndex(int [] nums){ + for(int i = nums.length-1;i>0;i--){ + if(nums[i]>nums[i-1]){ + return i; + } + } + return 0; + } + public static void exchange(int [] nums, int index){ + int head = nums[index-1]; + for(int i = nums.length-1;i>0;i--){ + if(head < nums[i]){ + nums[index-1] = nums[i]; + nums[i] = head; + break; + } + } + } + public static void reverse(int [] nums, int index){ + for(int i = index,j = nums.length-1;i直接使用sorted()会开辟新的空间并返回一个新的list,故补充一个原地反转函数 +```python +class Solution: + def nextPermutation(self, nums: List[int]) -> None: + """ + Do not return anything, modify nums in-place instead. + """ + length = len(nums) + for i in range(length - 2, -1, -1): # 从倒数第二个开始 + if nums[i]>=nums[i+1]: continue # 剪枝去重 + for j in range(length - 1, i, -1): + if nums[j] > nums[i]: + nums[j], nums[i] = nums[i], nums[j] + self.reverse(nums, i + 1, length - 1) + return + self.reverse(nums, 0, length - 1) + + def reverse(self, nums: List[int], left: int, right: int) -> None: + while left < right: + nums[left], nums[right] = nums[right], nums[left] + left += 1 + right -= 1 + +""" +265 / 265 个通过测试用例 +状态:通过 +执行用时: 36 ms +内存消耗: 14.9 MB +""" +``` + +### Go + +```go +//卡尔的解法 +func nextPermutation(nums []int) { + for i:=len(nums)-1;i>=0;i--{ + for j:=len(nums)-1;j>i;j--{ + if nums[j]>nums[i]{ + //交换 + nums[j],nums[i]=nums[i],nums[j] + reverse(nums,0+i+1,len(nums)-1) + return + } + } + } + reverse(nums,0,len(nums)-1) +} +//对目标切片指定区间的反转方法 +func reverse(a []int,begin,end int){ + for i,j:=begin,end;i= 0; i--){ + for(let j = nums.length - 1; j > i; j--){ + if(nums[j] > nums[i]){ + [nums[j],nums[i]] = [nums[i],nums[j]]; // 交换 + // 深拷贝[i + 1, nums.length)部分到新数组arr + let arr = nums.slice(i+1); + // arr升序排序 + arr.sort((a,b) => a - b); + // arr替换nums的[i + 1, nums.length)部分 + nums.splice(i+1,nums.length - i, ...arr); + return; + } + } + } + nums.sort((a,b) => a - b); // 不存在下一个更大的排列,则将数字重新排列成最小的排列(即升序排列)。 +}; + +//另一种 +var nextPermutation = function(nums) { + let i = nums.length - 2; + // 从右往左遍历拿到第一个左边小于右边的 i,此时 i 右边的数组是从右往左递增的 + while (i >= 0 && nums[i] >= nums[i+1]){ + i--; + } + if (i >= 0){ + let j = nums.length - 1; + // 从右往左遍历拿到第一个大于nums[i]的数,因为之前nums[i]是第一个小于他右边的数,所以他的右边一定有大于他的数 + while (j >= 0 && nums[j] <= nums[i]){ + j--; + } + // 交换两个数 + [nums[j], nums[i]] = [nums[i], nums[j]]; + } + // 对 i 右边的数进行交换 + // 因为 i 右边的数原来是从右往左递增的,把一个较小的值交换过来之后,仍然维持单调递增特性 + // 此时头尾交换并向中间逼近就能获得 i 右边序列的最小值 + let l = i + 1; + let r = nums.length - 1; + while (l < r){ + [nums[l], nums[r]] = [nums[r], nums[l]]; + l++; + r--; + } +}; +``` + + + diff --git "a/problems/0034.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" "b/problems/0034.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" new file mode 100755 index 0000000000..37248e4819 --- /dev/null +++ "b/problems/0034.\345\234\250\346\216\222\345\272\217\346\225\260\347\273\204\344\270\255\346\237\245\346\211\276\345\205\203\347\264\240\347\232\204\347\254\254\344\270\200\344\270\252\345\222\214\346\234\200\345\220\216\344\270\200\344\270\252\344\275\215\347\275\256.md" @@ -0,0 +1,855 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 34. 在排序数组中查找元素的第一个和最后一个位置 + +[力扣链接](https://leetcode.cn/problems/find-first-and-last-position-of-element-in-sorted-array/) + + +给定一个按照升序排列的整数数组 nums,和一个目标值 target。找出给定目标值在数组中的开始位置和结束位置。 + +如果数组中不存在目标值 target,返回 [-1, -1]。 + +进阶:你可以设计并实现时间复杂度为 $O(\log n)$ 的算法解决此问题吗? + + +示例 1: +* 输入:nums = [5,7,7,8,8,10], target = 8 +* 输出:[3,4] + +示例 2: +* 输入:nums = [5,7,7,8,8,10], target = 6 +* 输出:[-1,-1] + +示例 3: +* 输入:nums = [], target = 0 +* 输出:[-1,-1] + + +## 思路 + +这道题目如果基础不是很好,不建议大家看简短的代码,简短的代码隐藏了太多逻辑,结果就是稀里糊涂把题AC了,但是没有想清楚具体细节! + +对二分还不了解的同学先做这两题: + +* [704.二分查找](https://programmercarl.com/0704.二分查找.html) +* [35.搜索插入位置](https://programmercarl.com/0035.搜索插入位置.html) + +下面我来把所有情况都讨论一下。 + +寻找target在数组里的左右边界,有如下三种情况: + +* 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1} +* 情况二:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1} +* 情况三:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1} + +这三种情况都考虑到,说明就想的很清楚了。 + +接下来,在去寻找左边界,和右边界了。 + +采用二分法来去寻找左右边界,为了让代码清晰,我分别写两个二分来寻找左边界和右边界。 + +**刚刚接触二分搜索的同学不建议上来就想用一个二分来查找左右边界,很容易把自己绕进去,建议扎扎实实的写两个二分分别找左边界和右边界** + +### 寻找右边界 + +先来寻找右边界,至于二分查找,如果看过[704.二分查找](https://programmercarl.com/0704.二分查找.html)就会知道,二分查找中什么时候用while (left <= right),有什么时候用while (left < right),其实只要清楚**循环不变量**,很容易区分两种写法。 + +那么这里我采用while (left <= right)的写法,区间定义为[left, right],即左闭右闭的区间(如果这里有点看不懂了,强烈建议把[704.二分查找](https://programmercarl.com/0704.二分查找.html)这篇文章先看了,704题目做了之后再做这道题目就好很多了) + +确定好:计算出来的右边界是不包含target的右边界,左边界同理。 + +可以写出如下代码 + +```CPP +// 二分查找,寻找target的右边界(不包括target) +// 如果rightBorder为没有被赋值(即target在数组范围的左边,例如数组[3,3],target为2),为了处理情况一 +int getRightBorder(vector& nums, int target) { + int left = 0; + int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right] + int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况 + while (left <= right) { // 当left==right,区间[left, right]依然有效 + int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2 + if (nums[middle] > target) { + right = middle - 1; // target 在左区间,所以[left, middle - 1] + } else { // 当nums[middle] == target的时候,更新left,这样才能得到target的右边界 + left = middle + 1; + rightBorder = left; + } + } + return rightBorder; +} +``` + +### 寻找左边界 + +```CPP +// 二分查找,寻找target的左边界leftBorder(不包括target) +// 如果leftBorder没有被赋值(即target在数组范围的右边,例如数组[3,3],target为4),为了处理情况一 +int getLeftBorder(vector& nums, int target) { + int left = 0; + int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right] + int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况 + while (left <= right) { + int middle = left + ((right - left) / 2); + if (nums[middle] >= target) { // 寻找左边界,就要在nums[middle] == target的时候更新right + right = middle - 1; + leftBorder = right; + } else { + left = middle + 1; + } + } + return leftBorder; +} +``` + +### 处理三种情况 + +左右边界计算完之后,看一下主体代码,这里把上面讨论的三种情况,都覆盖了 + +```CPP +class Solution { +public: + vector searchRange(vector& nums, int target) { + int leftBorder = getLeftBorder(nums, target); + int rightBorder = getRightBorder(nums, target); + // 情况一 + if (leftBorder == -2 || rightBorder == -2) return {-1, -1}; + // 情况三 + if (rightBorder - leftBorder > 1) return {leftBorder + 1, rightBorder - 1}; + // 情况二 + return {-1, -1}; + } +private: + int getRightBorder(vector& nums, int target) { + int left = 0; + int right = nums.size() - 1; + int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况 + while (left <= right) { + int middle = left + ((right - left) / 2); + if (nums[middle] > target) { + right = middle - 1; + } else { // 寻找右边界,nums[middle] == target的时候更新left + left = middle + 1; + rightBorder = left; + } + } + return rightBorder; + } + int getLeftBorder(vector& nums, int target) { + int left = 0; + int right = nums.size() - 1; + int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况 + while (left <= right) { + int middle = left + ((right - left) / 2); + if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right + right = middle - 1; + leftBorder = right; + } else { + left = middle + 1; + } + } + return leftBorder; + } +}; +``` + +这份代码在简洁性很有大的优化空间,例如把寻找左右区间函数合并一起。 + +但拆开更清晰一些,而且把三种情况以及对应的处理逻辑完整的展现出来了。 + +## 总结 + +初学者建议大家一块一块的去分拆这道题目,正如本题解描述,想清楚三种情况之后,先专注于寻找右区间,然后专注于寻找左区间,左右根据左右区间做最后判断。 + +不要上来就想如果一起寻找左右区间,搞着搞着就会顾此失彼,绕进去拔不出来了。 + + +## 其他语言版本 + +### Java + +```java +class Solution { + int[] searchRange(int[] nums, int target) { + int leftBorder = getLeftBorder(nums, target); + int rightBorder = getRightBorder(nums, target); + // 情况一 + if (leftBorder == -2 || rightBorder == -2) return new int[]{-1, -1}; + // 情况三 + if (rightBorder - leftBorder > 1) return new int[]{leftBorder + 1, rightBorder - 1}; + // 情况二 + return new int[]{-1, -1}; + } + + int getRightBorder(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; + int rightBorder = -2; // 记录一下rightBorder没有被赋值的情况 + while (left <= right) { + int middle = left + ((right - left) / 2); + if (nums[middle] > target) { + right = middle - 1; + } else { // 寻找右边界,nums[middle] == target的时候更新left + left = middle + 1; + rightBorder = left; + } + } + return rightBorder; + } + + int getLeftBorder(int[] nums, int target) { + int left = 0; + int right = nums.length - 1; + int leftBorder = -2; // 记录一下leftBorder没有被赋值的情况 + while (left <= right) { + int middle = left + ((right - left) / 2); + if (nums[middle] >= target) { // 寻找左边界,nums[middle] == target的时候更新right + right = middle - 1; + leftBorder = right; + } else { + left = middle + 1; + } + } + return leftBorder; + } +} +``` + +```java +// 解法2 +// 1、首先,在 nums 数组中二分查找 target; +// 2、如果二分查找失败,则 binarySearch 返回 -1,表明 nums 中没有 target。此时,searchRange 直接返回 {-1, -1}; +// 3、如果二分查找成功,则 binarySearch 返回 nums 中值为 target 的一个下标。然后,通过左右滑动指针,来找到符合题意的区间 + +class Solution { + public int[] searchRange(int[] nums, int target) { + int index = binarySearch(nums, target); // 二分查找 + + if (index == -1) { // nums 中不存在 target,直接返回 {-1, -1} + return new int[] {-1, -1}; // 匿名数组 + } + // nums 中存在 target,则左右滑动指针,来找到符合题意的区间 + int left = index; + int right = index; + // 向左滑动,找左边界 + while (left - 1 >= 0 && nums[left - 1] == nums[index]) { // 防止数组越界。逻辑短路,两个条件顺序不能换 + left--; + } + // 向右滑动,找右边界 + while (right + 1 < nums.length && nums[right + 1] == nums[index]) { // 防止数组越界。 + right++; + } + return new int[] {left, right}; + } + + /** + * 二分查找 + * @param nums + * @param target + */ + public int binarySearch(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 { + right = mid - 1; // 不变量:左闭右闭区间 + } + } + return -1; // 不存在 + } +} +``` + +```java +// 解法三 +class Solution { + public int[] searchRange(int[] nums, int target) { + int left = searchLeft(nums,target); + int right = searchRight(nums,target); + return new int[]{left,right}; + } + public int searchLeft(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){ + right = mid - 1; + }else{ + left = mid + 1; + } + } + // right = left - 1 + // 如果存在答案 right是首选 + if(right>=0&&right=0&&left=0&&left=0&&right<=nums.length&&nums[right]==target){ + return right; + } + return -1; + } +} +``` + +### C# + +```csharp +public int[] SearchRange(int[] nums, int target) { + + var leftBorder = GetLeftBorder(nums, target); + var rightBorder = GetRightBorder(nums, target); + + if (leftBorder == -2 || rightBorder == -2) { + return new int[] {-1, -1}; + } + + if (rightBorder - leftBorder >=2) { + return new int[] {leftBorder + 1, rightBorder - 1}; + } + + return new int[] {-1, -1}; + +} + +public int GetLeftBorder(int[] nums, int target){ + var left = 0; + var right = nums.Length - 1; + var leftBorder = -2; + + while (left <= right) { + var mid = (left + right) / 2; + + if (target <= nums[mid]) { + right = mid - 1; + leftBorder = right; + } + else { + left = mid + 1; + } + } + + return leftBorder; +} + +public int GetRightBorder(int[] nums, int target){ + var left = 0; + var right = nums.Length - 1; + var rightBorder = -2; + + while (left <= right) { + var mid = (left + right) / 2; + + if (target >= nums[mid]) { + left = mid + 1; + rightBorder = left; + } + else { + right = mid - 1; + } + } + + return rightBorder; +} +``` + + + +### Python + +```python +class Solution: + def searchRange(self, nums: List[int], target: int) -> List[int]: + def getRightBorder(nums:List[int], target:int) -> int: + left, right = 0, len(nums)-1 + rightBoder = -2 # 记录一下rightBorder没有被赋值的情况 + while left <= right: + middle = left + (right-left) // 2 + if nums[middle] > target: + right = middle - 1 + else: # 寻找右边界,nums[middle] == target的时候更新left + left = middle + 1 + rightBoder = left + + return rightBoder + + def getLeftBorder(nums:List[int], target:int) -> int: + left, right = 0, len(nums)-1 + leftBoder = -2 # 记录一下leftBorder没有被赋值的情况 + while left <= right: + middle = left + (right-left) // 2 + if nums[middle] >= target: # 寻找左边界,nums[middle] == target的时候更新right + right = middle - 1 + leftBoder = right + else: + left = middle + 1 + return leftBoder + leftBoder = getLeftBorder(nums, target) + rightBoder = getRightBorder(nums, target) + # 情况一 + if leftBoder == -2 or rightBoder == -2: return [-1, -1] + # 情况三 + if rightBoder -leftBoder >1: return [leftBoder + 1, rightBoder - 1] + # 情况二 + return [-1, -1] +``` +```python +# 解法2 +# 1、首先,在 nums 数组中二分查找 target; +# 2、如果二分查找失败,则 binarySearch 返回 -1,表明 nums 中没有 target。此时,searchRange 直接返回 {-1, -1}; +# 3、如果二分查找成功,则 binarySearch 返回 nums 中值为 target 的一个下标。然后,通过左右滑动指针,来找到符合题意的区间 +class Solution: + def searchRange(self, nums: List[int], target: int) -> List[int]: + def binarySearch(nums:List[int], target:int) -> int: + left, right = 0, len(nums)-1 + while left<=right: # 不变量:左闭右闭区间 + middle = left + (right-left) // 2 + if nums[middle] > target: + right = middle - 1 + elif nums[middle] < target: + left = middle + 1 + else: + return middle + return -1 + index = binarySearch(nums, target) + if index == -1:return [-1, -1] # nums 中不存在 target,直接返回 {-1, -1} + # nums 中存在 target,则左右滑动指针,来找到符合题意的区间 + left, right = index, index + # 向左滑动,找左边界 + while left -1 >=0 and nums[left - 1] == target: left -=1 + # 向右滑动,找右边界 + while right+1 < len(nums) and nums[right + 1] == target: right +=1 + return [left, right] +``` +```python +# 解法3 +# 1、首先,在 nums 数组中二分查找得到第一个大于等于 target的下标(左边界)与第一个大于target的下标(右边界); +# 2、如果左边界<= 右边界,则返回 [左边界, 右边界]。否则返回[-1, -1] +class Solution: + def searchRange(self, nums: List[int], target: int) -> List[int]: + def binarySearch(nums:List[int], target:int, lower:bool) -> int: + left, right = 0, len(nums)-1 + ans = len(nums) + while left<=right: # 不变量:左闭右闭区间 + middle = left + (right-left) //2 + # lower为True,执行前半部分,找到第一个大于等于 target的下标 ,否则找到第一个大于target的下标 + if nums[middle] > target or (lower and nums[middle] >= target): + right = middle - 1 + ans = middle + else: + left = middle + 1 + return ans + + leftBorder = binarySearch(nums, target, True) # 搜索左边界 + rightBorder = binarySearch(nums, target, False) -1 # 搜索右边界 + if leftBorder<= rightBorder and rightBorder< len(nums) and nums[leftBorder] == target and nums[rightBorder] == target: + return [leftBorder, rightBorder] + return [-1, -1] +``` + +```python +# 解法4 +# 1、首先,在 nums 数组中二分查找得到第一个大于等于 target的下标leftBorder; +# 2、在 nums 数组中二分查找得到第一个大于等于 target+1的下标, 减1则得到rightBorder; +# 3、如果开始位置在数组的右边或者不存在target,则返回[-1, -1] 。否则返回[leftBorder, rightBorder] +class Solution: + def searchRange(self, nums: List[int], target: int) -> List[int]: + def binarySearch(nums:List[int], target:int) -> int: + left, right = 0, len(nums)-1 + while left<=right: # 不变量:左闭右闭区间 + middle = left + (right-left) //2 + if nums[middle] >= target: + right = middle - 1 + else: + left = middle + 1 + return left # 若存在target,则返回第一个等于target的值 + + leftBorder = binarySearch(nums, target) # 搜索左边界 + rightBorder = binarySearch(nums, target+1) -1 # 搜索右边界 + if leftBorder == len(nums) or nums[leftBorder]!= target: # 情况一和情况二 + return [-1, -1] + return [leftBorder, rightBorder] +``` + +### Rust + +```rust + +impl Solution { + pub fn search_range(nums: Vec, target: i32) -> Vec { + let right_border = Solution::get_right_border(&nums, target); + let left_border = Solution::get_left_border(&nums, target); + if right_border == -2 || left_border == -2 { + return vec![-1, -1]; + } + if right_border - left_border > 0 { + return vec![left_border, right_border - 1]; + } + vec![-1, -1] + } + + pub fn get_right_border(nums: &Vec, target: i32) -> i32 { + let mut left = 0; + let mut right = nums.len(); + let mut right_border: i32 = -2; + while left < right { + let mid = (left + right) / 2; + if nums[mid] > target { + right = mid; + } else { + left = mid + 1; + right_border = left as i32; + } + } + right_border as i32 + } + + pub fn get_left_border(nums: &Vec, target: i32) -> i32 { + let mut left = 0; + let mut right = nums.len(); + let mut left_border: i32 = -2; + while left < right { + let mid = (left + right) / 2; + if nums[mid] >= target { + right = mid; + left_border = right as i32; + } else { + left = mid + 1; + } + } + left_border as i32 + } +} +``` + +### Go + +```go +func searchRange(nums []int, target int) []int { + leftBorder := getLeft(nums, target) + rightBorder := getRight(nums, target) + // 情况一 + if leftBorder == -2 || rightBorder == -2 { + return []int{-1, -1} + } + // 情况三 + if rightBorder - leftBorder > 1 { + return []int{leftBorder + 1, rightBorder - 1} + } + // 情况二 + return []int{-1, -1} +} + +func getLeft(nums []int, target int) int { + left, right := 0, len(nums)-1 + border := -2 // 记录border没有被赋值的情况;这里不能赋值-1,target = num[0]时,会无法区分情况一和情况二 + for left <= right { // []闭区间 + mid := left + ((right - left) >> 1) + if nums[mid] >= target { // 找到第一个等于target的位置 + right = mid - 1 + border = right + } else { + left = mid + 1 + } + } + return border +} + +func getRight(nums []int, target int) int { + left, right := 0, len(nums) - 1 + border := -2 + for left <= right { + mid := left + ((right - left) >> 1) + if nums[mid] > target { + right = mid - 1 + } else { // 找到第一个大于target的位置 + left = mid + 1 + border = left + } + } + return border + +} +``` + +### JavaScript + +```js +var searchRange = function(nums, target) { + const getLeftBorder = (nums, target) => { + let left = 0, right = nums.length - 1; + let leftBorder = -2;// 记录一下leftBorder没有被赋值的情况 + while(left <= right){ + let middle = left + ((right - left) >> 1); + if(nums[middle] >= target){ // 寻找左边界,nums[middle] == target的时候更新right + right = middle - 1; + leftBorder = right; + } else { + left = middle + 1; + } + } + return leftBorder; + } + + const getRightBorder = (nums, target) => { + let left = 0, right = nums.length - 1; + let rightBorder = -2; // 记录一下rightBorder没有被赋值的情况 + while (left <= right) { + let middle = left + ((right - left) >> 1); + if (nums[middle] > target) { + right = middle - 1; + } else { // 寻找右边界,nums[middle] == target的时候更新left + left = middle + 1; + rightBorder = left; + } + } + return rightBorder; + } + + let leftBorder = getLeftBorder(nums, target); + let rightBorder = getRightBorder(nums, target); + // 情况一 + if(leftBorder === -2 || rightBorder === -2) return [-1,-1]; + // 情况三 + if (rightBorder - leftBorder > 1) return [leftBorder + 1, rightBorder - 1]; + // 情况二 + return [-1, -1]; +}; +``` + + +### TypeScript + +```typescript +function searchRange(nums: number[], target: number): number[] { + const leftBoard: number = getLeftBorder(nums, target); + const rightBoard: number = getRightBorder(nums, target); + // target 在nums区间左侧或右侧 + if (leftBoard === (nums.length - 1) || rightBoard === 0) return [-1, -1]; + // target 不存在与nums范围内 + if (rightBoard - leftBoard <= 1) return [-1, -1]; + // target 存在于nums范围内 + return [leftBoard + 1, rightBoard - 1]; +}; +// 查找第一个大于target的元素下标 +function getRightBorder(nums: number[], target: number): number { + let left: number = 0, + right: number = nums.length - 1; + // 0表示target在nums区间的左边 + let rightBoard: number = 0; + while (left <= right) { + let mid = Math.floor((left + right) / 2); + if (nums[mid] <= target) { + // 右边界一定在mid右边(不含mid) + left = mid + 1; + rightBoard = left; + } else { + // 右边界在mid左边(含mid) + right = mid - 1; + } + } + return rightBoard; +} +// 查找第一个小于target的元素下标 +function getLeftBorder(nums: number[], target: number): number { + let left: number = 0, + right: number = nums.length - 1; + // length-1表示target在nums区间的右边 + let leftBoard: number = nums.length - 1; + while (left <= right) { + let mid = Math.floor((left + right) / 2); + if (nums[mid] >= target) { + // 左边界一定在mid左边(不含mid) + right = mid - 1; + leftBoard = right; + } else { + // 左边界在mid右边(含mid) + left = mid + 1; + } + } + return leftBoard; +} +``` + + +### Scala +```scala +object Solution { + def searchRange(nums: Array[Int], target: Int): Array[Int] = { + var left = getLeftBorder(nums, target) + var right = getRightBorder(nums, target) + if (left == -2 || right == -2) return Array(-1, -1) + if (right - left > 1) return Array(left + 1, right - 1) + Array(-1, -1) + } + + // 寻找左边界 + def getLeftBorder(nums: Array[Int], target: Int): Int = { + var leftBorder = -2 + var left = 0 + var right = nums.length - 1 + while (left <= right) { + var mid = left + (right - left) / 2 + if (nums(mid) >= target) { + right = mid - 1 + leftBorder = right + } else { + left = mid + 1 + } + } + leftBorder + } + + // 寻找右边界 + def getRightBorder(nums: Array[Int], target: Int): Int = { + var rightBorder = -2 + var left = 0 + var right = nums.length - 1 + while (left <= right) { + var mid = left + (right - left) / 2 + if (nums(mid) <= target) { + left = mid + 1 + rightBorder = left + } else { + right = mid - 1 + } + } + rightBorder + } +} +``` + + +### Kotlin +```kotlin +class Solution { + fun searchRange(nums: IntArray, target: Int): IntArray { + var index = binarySearch(nums, target) + // 没找到,返回[-1, -1] + if (index == -1) return intArrayOf(-1, -1) + var left = index + var right = index + // 寻找左边界 + while (left - 1 >=0 && nums[left - 1] == target){ + left-- + } + // 寻找右边界 + while (right + 1 target) { + right = middle - 1 + } + else { + if (nums[middle] < target) { + left = middle + 1 + } + else { + return middle + } + } + } + // 没找到,返回-1 + return -1 + } +} +``` + +### C +```c +int searchLeftBorder(int *nums, int numsSize, int target) { + int left = 0, right = numsSize - 1; + // 记录leftBorder没有被赋值的情况 + int leftBorder = -1; + // 边界为[left, right] + while (left <= right) { + // 更新middle值,等同于middle = (left + right) / 2 + int middle = left + ((right - left) >> 1); + // 若当前middle所指为target,将左边界设为middle,并向左继续寻找左边界 + if (nums[middle] == target) { + leftBorder = middle; + right = middle - 1; + } else if (nums[middle] > target) { + right = middle - 1; + } else { + left = middle + 1; + } + } + return leftBorder; +} +int searchRightBorder(int *nums, int numsSize, int target) { + int left = 0, right = numsSize - 1; + // 记录rightBorder没有被赋值的情况 + int rightBorder = -1; + while (left <= right) { + int middle = left + ((right - left) >> 1); + // 若当前middle所指为target,将右边界设为middle,并向右继续寻找右边界 + if (nums[middle] == target) { + rightBorder = middle; + left = middle + 1; + } else if (nums[middle] > target) { + right = middle - 1; + } else { + left = middle + 1; + } + } + return rightBorder; +} + +int* searchRange(int* nums, int numsSize, int target, int* returnSize){ + int leftBorder = searchLeftBorder(nums, numsSize, target); + int rightBorder = searchRightBorder(nums, numsSize, target); + + // 定义返回数组及数组大小 + *returnSize = 2; + int *resNums = (int*)malloc(sizeof(int) * 2); + resNums[0] = leftBorder; + resNums[1] = rightBorder; + return resNums; +} +``` + + diff --git "a/problems/0035.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" "b/problems/0035.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" new file mode 100755 index 0000000000..b48910eef7 --- /dev/null +++ "b/problems/0035.\346\220\234\347\264\242\346\217\222\345\205\245\344\275\215\347\275\256.md" @@ -0,0 +1,549 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + + + +# 35.搜索插入位置 + +[力扣题目链接](https://leetcode.cn/problems/search-insert-position/) + +给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。 + +你可以假设数组中无重复元素。 + +示例 1: + +* 输入: [1,3,5,6], 5 +* 输出: 2 + +示例 2: + +* 输入: [1,3,5,6], 2 +* 输出: 1 + +示例 3: + +* 输入: [1,3,5,6], 7 +* 输出: 4 + +示例 4: + +* 输入: [1,3,5,6], 0 +* 输出: 0 + +## 思路 + +这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的。 + +这道题目,要在数组中插入目标值,无非是这四种情况。 + +![35_搜索插入位置3](https://file1.kamacoder.com/i/algo/20201216232148471.png) + +* 目标值在数组所有元素之前 +* 目标值等于数组中某一个元素 +* 目标值插入数组中的位置 +* 目标值在数组所有元素之后 + +这四种情况确认清楚了,就可以尝试解题了。 + +接下来我将从暴力的解法和二分法来讲解此题,也借此好好讲一讲二分查找法。 + +### 暴力解法 + +暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。 + +C++代码 + +```CPP +class Solution { +public: + int searchInsert(vector& nums, int target) { + for (int i = 0; i < nums.size(); i++) { + // 分别处理如下三种情况 + // 目标值在数组所有元素之前 + // 目标值等于数组中某一个元素 + // 目标值插入数组中的位置 + if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果 + return i; + } + } + // 目标值在数组所有元素之后的情况 + return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度 + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +效率如下: + +![35_搜索插入位置](https://file1.kamacoder.com/i/algo/20201216232127268.png) + +### 二分法 + +既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。 + + +![35_搜索插入位置4](https://file1.kamacoder.com/i/algo/202012162326354.png) + +大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件。 + +以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。** + +同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的。 + +大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,使用二分法寻找元素为5的位置,并返回其下标。 + +![35_搜索插入位置5](https://file1.kamacoder.com/i/algo/20201216232659199.png) + +二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好。 + +相信很多同学对二分查找法中边界条件处理不好。 + +例如到底是 `while(left < right)` 还是 `while(left <= right)`,到底是`right = middle`呢,还是要`right = middle - 1`呢? + +这里弄不清楚主要是因为**对区间的定义没有想清楚,这就是不变量**。 + +要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)。 + +### 二分法第一种写法 + +以这道题目来举例,以下的代码中定义 target 是在一个在左闭右闭的区间里,**也就是[left, right] (这个很重要)**。 + +这就决定了这个二分法的代码如何去写,大家看如下代码: + +**大家要仔细看注释,思考为什么要写while(left <= right), 为什么要写right = middle - 1**。 + +```CPP +class Solution { +public: + int searchInsert(vector& nums, int target) { + int n = nums.size(); + int left = 0; + int right = n - 1; // 定义target在左闭右闭的区间里,[left, right] + while (left <= right) { // 当left==right,区间[left, right]依然有效 + int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2 + if (nums[middle] > target) { + right = middle - 1; // target 在左区间,所以[left, middle - 1] + } else if (nums[middle] < target) { + left = middle + 1; // target 在右区间,所以[middle + 1, right] + } else { // nums[middle] == target + return middle; + } + } + // 分别处理如下四种情况 + // 目标值在数组所有元素之前 [0, -1] + // 目标值等于数组中某一个元素 return middle; + // 目标值插入数组中的位置 [left, right],return right + 1 + // 目标值在数组所有元素之后的情况 [left, right], 因为是右闭区间,所以 return right + 1 + return right + 1; + } +}; +``` + +* 时间复杂度:O(log n) +* 空间复杂度:O(1) + +效率如下: +![35_搜索插入位置2](https://file1.kamacoder.com/i/algo/2020121623272877.png) + +### 二分法第二种写法 + +如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) 。 + +那么二分法的边界处理方式则截然不同。 + +不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。 + +**大家要仔细看注释,思考为什么要写while (left < right), 为什么要写right = middle**。 + +```CPP +class Solution { +public: + int searchInsert(vector& nums, int target) { + int n = nums.size(); + int left = 0; + int right = n; // 定义target在左闭右开的区间里,[left, right) target + while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间 + int middle = left + ((right - left) >> 1); + if (nums[middle] > target) { + right = middle; // target 在左区间,在[left, middle)中 + } else if (nums[middle] < target) { + left = middle + 1; // target 在右区间,在 [middle+1, right)中 + } else { // nums[middle] == target + return middle; // 数组中找到目标值的情况,直接返回下标 + } + } + // 分别处理如下四种情况 + // 目标值在数组所有元素之前 [0,0) + // 目标值等于数组中某一个元素 return middle + // 目标值插入数组中的位置 [left, right) ,return right 即可 + // 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right + return right; + } +}; +``` + +* 时间复杂度:O(log n) +* 空间复杂度:O(1) + +## 总结 + +希望通过这道题目,大家会发现平时写二分法,为什么总写不好,就是因为对区间定义不清楚。 + +确定要查找的区间到底是左闭右开[left, right),还是左闭又闭[left, right],这就是不变量。 + +然后在**二分查找的循环中,坚持循环不变量的原则**,很多细节问题,自然会知道如何处理了。 + + + +## 其他语言版本 + +### Java + +```java +class Solution { + public int searchInsert(int[] nums, int target) { + int n = nums.length; + + // 定义target在左闭右闭的区间,[low, high] + int low = 0; + int high = n - 1; + + while (low <= high) { // 当low==high,区间[low, high]依然有效 + int mid = low + (high - low) / 2; // 防止溢出 + if (nums[mid] > target) { + high = mid - 1; // target 在左区间,所以[low, mid - 1] + } else if (nums[mid] < target) { + low = mid + 1; // target 在右区间,所以[mid + 1, high] + } else { + // 1. 目标值等于数组中某一个元素 return mid; + return mid; + } + } + // 2.目标值在数组所有元素之前 3.目标值插入数组中 4.目标值在数组所有元素之后 return right + 1; + return high + 1; + } +} +``` + +```java +//第二种二分法:左闭右开 +public int searchInsert(int[] nums, int target) { + int left = 0; + int right = nums.length; + while (left < right) { //左闭右开 [left, right) + int middle = left + ((right - left) >> 1); + if (nums[middle] > target) { + right = middle; // target 在左区间,在[left, middle)中 + } else if (nums[middle] < target) { + left = middle + 1; // target 在右区间,在 [middle+1, right)中 + } else { // nums[middle] == target + return middle; // 数组中找到目标值的情况,直接返回下标 + } + } + // 目标值在数组所有元素之前 [0,0) + // 目标值插入数组中的位置 [left, right) ,return right 即可 + // 目标值在数组所有元素之后的情况 [left, right),因为是右开区间,所以 return right + return right; +} +``` + + + +### C# + +```go +public int SearchInsert(int[] nums, int target) { + + var left = 0; + var right = nums.Length - 1; + + while (left <= right) { + + var curr = (left + right) / 2; + + if (nums[curr] == target) + { + return curr; + } + + if (target > nums[curr]) { + left = curr + 1; + } + else { + right = curr - 1; + } + } + + return left; +} +``` + + + +### Golang + +```go +// 第一种二分法 +func searchInsert(nums []int, target int) int { + left, right := 0, len(nums)-1 + for left <= right { + mid := left + (right-left)/2 + if nums[mid] == target { + return mid + } else if nums[mid] > target { + right = mid - 1 + } else { + left = mid + 1 + } + } + return right+1 +} +``` + +### Rust + +```rust +impl Solution { + pub fn search_insert(nums: Vec, target: i32) -> i32 { + use std::cmp::Ordering::{Equal, Greater, Less}; + let (mut left, mut right) = (0, nums.len() as i32 - 1); + while left <= right { + let mid = (left + right) / 2; + match nums[mid as usize].cmp(&target) { + Less => left = mid + 1, + Equal => return mid, + Greater => right = mid - 1, + } + } + right + 1 + } +} +``` + +### Python + +```python +# 第一种二分法: [left, right]左闭右闭区间 +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) - 1 + + while left <= right: + middle = (left + right) // 2 + + if nums[middle] < target: + left = middle + 1 + elif nums[middle] > target: + right = middle - 1 + else: + return middle + return right + 1 +``` + +```python +# 第二种二分法: [left, right)左闭右开区间 +class Solution: + def searchInsert(self, nums: List[int], target: int) -> int: + left = 0 + right = len(nums) + + while (left < right): + middle = (left + right) // 2 + + if nums[middle] > target: + right = middle + elif nums[middle] < target: + left = middle + 1 + else: + return middle + + return right +``` + +### JavaScript + +```js +var searchInsert = function (nums, target) { + let l = 0, r = nums.length - 1, ans = nums.length; + + while (l <= r) { + const mid = l + Math.floor((r - l) >> 1); + + if (target > nums[mid]) { + l = mid + 1; + } else { + ans = mid; + r = mid - 1; + } + } + + return ans; +}; +``` + +### TypeScript + +```typescript +// 第一种二分法 +function searchInsert(nums: number[], target: number): number { + const length: number = nums.length; + let left: number = 0, + right: number = length - 1; + while (left <= right) { + const mid: number = Math.floor((left + right) / 2); + if (nums[mid] < target) { + left = mid + 1; + } else if (nums[mid] === target) { + return mid; + } else { + right = mid - 1; + } + } + return right + 1; +}; +``` + +### Swift + +```swift +// 暴力法 +func searchInsert(_ nums: [Int], _ target: Int) -> Int { + for i in 0..= target { + return i + } + } + return nums.count +} + +// 二分法 +func searchInsert(_ nums: [Int], _ target: Int) -> Int { + var left = 0 + var right = nums.count - 1 + + while left <= right { + let middle = left + ((right - left) >> 1) + + if nums[middle] > target { + right = middle - 1 + }else if nums[middle] < target { + left = middle + 1 + }else if nums[middle] == target { + return middle + } + } + + return right + 1 +} +``` + +### Scala + +```scala +object Solution { + def searchInsert(nums: Array[Int], target: Int): Int = { + var left = 0 + var right = nums.length - 1 + while (left <= right) { + var mid = left + (right - left) / 2 + if (target == nums(mid)) { + return mid + } else if (target > nums(mid)) { + left = mid + 1 + } else { + right = mid - 1 + } + } + right + 1 + } +} +``` + +### PHP + +```php +// 二分法(1):[左闭右闭] +function searchInsert($nums, $target) +{ + $n = count($nums); + $l = 0; + $r = $n - 1; + while ($l <= $r) { + $mid = floor(($l + $r) / 2); + if ($nums[$mid] > $target) { + // 下次搜索在左区间:[$l,$mid-1] + $r = $mid - 1; + } else if ($nums[$mid] < $target) { + // 下次搜索在右区间:[$mid+1,$r] + $l = $mid + 1; + } else { + // 命中返回 + return $mid; + } + } + return $r + 1; +} +``` + +### C + +```c +//版本一 [left, right]左闭右闭区间 +int searchInsert(int* nums, int numsSize, int target){ + //左闭右开区间 [0 , numsSize-1] + int left =0; + int mid =0; + int right = numsSize - 1; + while(left <= right){//左闭右闭区间 所以可以 left == right + mid = left + (right - left) / 2; + if(target < nums[mid]){ + //target 在左区间 [left, mid - 1]中,原区间包含mid,右区间边界可以向左内缩 + right = mid -1; + }else if( target > nums[mid]){ + //target 在右区间 [mid + 1, right]中,原区间包含mid,左区间边界可以向右内缩 + left = mid + 1; + }else { + // nums[mid] == target ,顺利找到target,直接返回mid + return mid; + } + } + //数组中未找到target元素 + //target在数组所有元素之后,[left, right]是右闭区间,需要返回 right +1 + return right + 1; +} +``` + +```c +//版本二 [left, right]左闭右开区间 +int searchInsert(int* nums, int numsSize, int target){ + //左闭右开区间 [0 , numsSize) + int left =0; + int mid =0; + int right = numsSize; + while(left < right){//左闭右闭区间 所以 left < right + mid = left + (right - left) / 2; + if(target < nums[mid]){ + //target 在左区间 [left, mid)中,原区间没有包含mid,右区间边界不能内缩 + right = mid ; + }else if( target > nums[mid]){ + // target 在右区间 [mid+1, right)中,原区间包含mid,左区间边界可以向右内缩 + left = mid + 1; + }else { + // nums[mid] == target ,顺利找到target,直接返回mid + return mid; + } + } + //数组中未找到target元素 + //target在数组所有元素之后,[left, right)是右开区间, return right即可 + return right; +} +``` + + diff --git "a/problems/0037.\350\247\243\346\225\260\347\213\254.md" "b/problems/0037.\350\247\243\346\225\260\347\213\254.md" new file mode 100755 index 0000000000..204f0cc092 --- /dev/null +++ "b/problems/0037.\350\247\243\346\225\260\347\213\254.md" @@ -0,0 +1,893 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 如果对回溯法理论还不清楚的同学,可以先看这个视频[视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) + +# 37. 解数独 + +[力扣题目链接](https://leetcode.cn/problems/sudoku-solver/) + +编写一个程序,通过填充空格来解决数独问题。 + +一个数独的解法需遵循如下规则: +数字 1-9 在每一行只能出现一次。 +数字 1-9 在每一列只能出现一次。 +数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 +空白格用 '.' 表示。 + +![解数独](https://file1.kamacoder.com/i/algo/202011171912586.png) + +一个数独。 + +![解数独](https://file1.kamacoder.com/i/algo/20201117191340669.png) + +答案被标成红色。 + +提示: + +* 给定的数独序列只包含数字 1-9 和字符 '.' 。 +* 你可以假设给定的数独只有唯一解。 +* 给定数独永远是 9x9 形式的。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[回溯算法二维递归?解数独不过如此!| LeetCode:37. 解数独](https://www.bilibili.com/video/BV1TW4y1471V/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +棋盘搜索问题可以使用回溯法暴力搜索,只不过这次我们要做的是**二维递归**。 + +怎么做二维递归呢? + +大家已经跟着「代码随想录」刷过了如下回溯法题目,例如:[77.组合(组合问题)](https://programmercarl.com/0077.组合.html),[131.分割回文串(分割问题)](https://programmercarl.com/0131.分割回文串.html),[78.子集(子集问题)](https://programmercarl.com/0078.子集.html),[46.全排列(排列问题)](https://programmercarl.com/0046.全排列.html),以及[51.N皇后(N皇后问题)](https://programmercarl.com/0051.N皇后.html),其实这些题目都是一维递归。 + +**如果以上这几道题目没有做过的话,不建议上来就做这道题哈!** + +[N皇后问题](https://programmercarl.com/0051.N皇后.html)是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置。 + +本题就不一样了,**本题中棋盘的每一个位置都要放一个数字(而N皇后是一行只放一个皇后),并检查数字是否合法,解数独的树形结构要比N皇后更宽更深**。 + +因为这个树形结构太大了,我抽取一部分,如图所示: + +![37.解数独](https://file1.kamacoder.com/i/algo/2020111720451790-20230310131816104.png) + + +### 回溯三部曲 + +* 递归函数以及参数 + +**递归函数的返回值需要是bool类型,为什么呢?** + +因为解数独找到一个符合的条件(就在树的叶子节点上)立刻就返回,相当于找从根节点到叶子节点一条唯一路径,所以需要使用bool返回值。 + +代码如下: + +```cpp +bool backtracking(vector>& board) +``` + +* 递归终止条件 + +本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。 + +**不用终止条件会不会死循环?** + +递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件! + +**那么有没有永远填不满的情况呢?** + +这个问题我在递归单层搜索逻辑里再来讲! + +* 递归单层搜索逻辑 + +![37.解数独](https://file1.kamacoder.com/i/algo/2020111720451790-20230310131822254.png) + +在树形图中可以看出我们需要的是一个二维的递归 (一行一列) + +**一个for循环遍历棋盘的行,一个for循环遍历棋盘的列,一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!** + + +代码如下:(**详细看注释**) + +```CPP +bool backtracking(vector>& board) { + for (int i = 0; i < board.size(); i++) { // 遍历行 + for (int j = 0; j < board[0].size(); j++) { // 遍历列 + if (board[i][j] != '.') continue; + for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适 + if (isValid(i, j, k, board)) { + board[i][j] = k; // 放置k + if (backtracking(board)) return true; // 如果找到合适一组立刻返回 + board[i][j] = '.'; // 回溯,撤销k + } + } + return false; // 9个数都试完了,都不行,那么就返回false + } + } + return true; // 遍历完没有返回false,说明找到了合适棋盘位置了 +} +``` + +**注意这里return false的地方,这里放return false 是有讲究的**。 + +因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! + +那么会直接返回, **这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!** + +### 判断棋盘是否合法 + +判断棋盘是否合法有如下三个维度: + +* 同行是否重复 +* 同列是否重复 +* 9宫格里是否重复 + +代码如下: + +```CPP +bool isValid(int row, int col, char val, vector>& board) { + for (int i = 0; i < 9; i++) { // 判断行里是否重复 + if (board[row][i] == val) { + return false; + } + } + for (int j = 0; j < 9; j++) { // 判断列里是否重复 + if (board[j][col] == val) { + return false; + } + } + int startRow = (row / 3) * 3; + int startCol = (col / 3) * 3; + for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复 + for (int j = startCol; j < startCol + 3; j++) { + if (board[i][j] == val ) { + return false; + } + } + } + return true; +} +``` + +最后整体C++代码如下: + + +```CPP +class Solution { +private: +bool backtracking(vector>& board) { + for (int i = 0; i < board.size(); i++) { // 遍历行 + for (int j = 0; j < board[0].size(); j++) { // 遍历列 + if (board[i][j] == '.') { + for (char k = '1'; k <= '9'; k++) { // (i, j) 这个位置放k是否合适 + if (isValid(i, j, k, board)) { + board[i][j] = k; // 放置k + if (backtracking(board)) return true; // 如果找到合适一组立刻返回 + board[i][j] = '.'; // 回溯,撤销k + } + } + return false; // 9个数都试完了,都不行,那么就返回false + } + } + } + return true; // 遍历完没有返回false,说明找到了合适棋盘位置了 +} +bool isValid(int row, int col, char val, vector>& board) { + for (int i = 0; i < 9; i++) { // 判断行里是否重复 + if (board[row][i] == val) { + return false; + } + } + for (int j = 0; j < 9; j++) { // 判断列里是否重复 + if (board[j][col] == val) { + return false; + } + } + int startRow = (row / 3) * 3; + int startCol = (col / 3) * 3; + for (int i = startRow; i < startRow + 3; i++) { // 判断9方格里是否重复 + for (int j = startCol; j < startCol + 3; j++) { + if (board[i][j] == val ) { + return false; + } + } + } + return true; +} +public: + void solveSudoku(vector>& board) { + backtracking(board); + } +}; + +``` + +## 总结 + +解数独可以说是非常难的题目了,如果还一直停留在单层递归的逻辑中,这道题目可以让大家瞬间崩溃。 + +所以我在开篇就提到了**二维递归**,这也是我自创词汇,希望可以帮助大家理解解数独的搜索过程。 + +一波分析之后,再看代码会发现其实也不难,唯一难点就是理解**二维递归**的思维逻辑。 + +**这样,解数独这么难的问题,也被我们攻克了**。 + +**恭喜一路上坚持打卡的录友们,回溯算法已经接近尾声了,接下来就是要一波总结了**。 + + +## 其他语言版本 + + +### Java +解法一: +```java +class Solution { + public void solveSudoku(char[][] board) { + solveSudokuHelper(board); + } + + private boolean solveSudokuHelper(char[][] board){ + //「一个for循环遍历棋盘的行,一个for循环遍历棋盘的列, + // 一行一列确定下来之后,递归遍历这个位置放9个数字的可能性!」 + for (int i = 0; i < 9; i++){ // 遍历行 + for (int j = 0; j < 9; j++){ // 遍历列 + if (board[i][j] != '.'){ // 跳过原始数字 + continue; + } + for (char k = '1'; k <= '9'; k++){ // (i, j) 这个位置放k是否合适 + if (isValidSudoku(i, j, k, board)){ + board[i][j] = k; + if (solveSudokuHelper(board)){ // 如果找到合适一组立刻返回 + return true; + } + board[i][j] = '.'; + } + } + // 9个数都试完了,都不行,那么就返回false + return false; + // 因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! + // 那么会直接返回, 「这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!」 + } + } + // 遍历完没有返回false,说明找到了合适棋盘位置了 + return true; + } + + /** + * 判断棋盘是否合法有如下三个维度: + * 同行是否重复 + * 同列是否重复 + * 9宫格里是否重复 + */ + private boolean isValidSudoku(int row, int col, char val, char[][] board){ + // 同行是否重复 + for (int i = 0; i < 9; i++){ + if (board[row][i] == val){ + return false; + } + } + // 同列是否重复 + for (int j = 0; j < 9; j++){ + if (board[j][col] == val){ + return false; + } + } + // 9宫格里是否重复 + int startRow = (row / 3) * 3; + int startCol = (col / 3) * 3; + for (int i = startRow; i < startRow + 3; i++){ + for (int j = startCol; j < startCol + 3; j++){ + if (board[i][j] == val){ + return false; + } + } + } + return true; + } +} +``` +解法二(bitmap标记) +``` +class Solution{ + int[] rowBit = new int[9]; + int[] colBit = new int[9]; + int[] square9Bit = new int[9]; + + public void solveSudoku(char[][] board) { + // 1 10 11 + for (int y = 0; y < board.length; y++) { + for (int x = 0; x < board[y].length; x++) { + int numBit = 1 << (board[y][x] - '1'); + rowBit[y] ^= numBit; + colBit[x] ^= numBit; + square9Bit[(y / 3) * 3 + x / 3] ^= numBit; + } + } + backtrack(board, 0); + } + + public boolean backtrack(char[][] board, int n) { + if (n >= 81) { + return true; + } + + // 快速算出行列编号 n/9 n%9 + int row = n / 9; + int col = n % 9; + + if (board[row][col] != '.') { + return backtrack(board, n + 1); + } + + for (char c = '1'; c <= '9'; c++) { + int numBit = 1 << (c - '1'); + if (!isValid(numBit, row, col)) continue; + { + board[row][col] = c; // 当前的数字放入到数组之中, + rowBit[row] ^= numBit; // 第一行rowBit[0],第一个元素eg: 1 , 0^1=1,第一个元素:4, 100^1=101,... + colBit[col] ^= numBit; + square9Bit[(row / 3) * 3 + col / 3] ^= numBit; + } + if (backtrack(board, n + 1)) return true; + { + board[row][col] = '.'; // 不满足条件,回退成'.' + rowBit[row] &= ~numBit; // 第一行rowBit[0],第一个元素eg: 1 , 101&=~1==>101&111111110==>100 + colBit[col] &= ~numBit; + square9Bit[(row / 3) * 3 + col / 3] &= ~numBit; + } + } + return false; + } + + + boolean isValid(int numBit, int row, int col) { + // 左右 + if ((rowBit[row] & numBit) > 0) return false; + // 上下 + if ((colBit[col] & numBit) > 0) return false; + // 9宫格: 快速算出第n个九宫格,编号[0,8] , 编号=(row / 3) * 3 + col / 3 + if ((square9Bit[(row / 3) * 3 + col / 3] & numBit) > 0) return false; + return true; + } + +} + +``` +### Python + +```python +class Solution: + def solveSudoku(self, board: List[List[str]]) -> None: + """ + Do not return anything, modify board in-place instead. + """ + row_used = [set() for _ in range(9)] + col_used = [set() for _ in range(9)] + box_used = [set() for _ in range(9)] + for row in range(9): + for col in range(9): + num = board[row][col] + if num == ".": + continue + row_used[row].add(num) + col_used[col].add(num) + box_used[(row // 3) * 3 + col // 3].add(num) + self.backtracking(0, 0, board, row_used, col_used, box_used) + + def backtracking( + self, + row: int, + col: int, + board: List[List[str]], + row_used: List[List[int]], + col_used: List[List[int]], + box_used: List[List[int]], + ) -> bool: + if row == 9: + return True + + next_row, next_col = (row, col + 1) if col < 8 else (row + 1, 0) + if board[row][col] != ".": + return self.backtracking( + next_row, next_col, board, row_used, col_used, box_used + ) + + for num in map(str, range(1, 10)): + if ( + num not in row_used[row] + and num not in col_used[col] + and num not in box_used[(row // 3) * 3 + col // 3] + ): + board[row][col] = num + row_used[row].add(num) + col_used[col].add(num) + box_used[(row // 3) * 3 + col // 3].add(num) + if self.backtracking( + next_row, next_col, board, row_used, col_used, box_used + ): + return True + board[row][col] = "." + row_used[row].remove(num) + col_used[col].remove(num) + box_used[(row // 3) * 3 + col // 3].remove(num) + return False +``` + +### Go + +```go +func solveSudoku(board [][]byte) { + var backtracking func(board [][]byte) bool + backtracking = func(board [][]byte) bool { + for i := 0; i < 9; i++ { + for j := 0; j < 9; j++ { + //判断此位置是否适合填数字 + if board[i][j] != '.' { + continue + } + //尝试填1-9 + for k := '1'; k <= '9'; k++ { + if isvalid(i, j, byte(k), board) == true { //如果满足要求就填 + board[i][j] = byte(k) + if backtracking(board) == true { + return true + } + board[i][j] = '.' + } + } + return false + } + } + return true + } + backtracking(board) +} + +//判断填入数字是否满足要求 +func isvalid(row, col int, k byte, board [][]byte) bool { + for i := 0; i < 9; i++ { //行 + if board[row][i] == k { + return false + } + } + for i := 0; i < 9; i++ { //列 + if board[i][col] == k { + return false + } + } + //方格 + startrow := (row / 3) * 3 + startcol := (col / 3) * 3 + for i := startrow; i < startrow+3; i++ { + for j := startcol; j < startcol+3; j++ { + if board[i][j] == k { + return false + } + } + } + return true +} +``` + + + +### JavaScript + +```Javascript +var solveSudoku = function(board) { + function isValid(row, col, val, board) { + let len = board.length + // 行不能重复 + for(let i = 0; i < len; i++) { + if(board[row][i] === val) { + return false + } + } + // 列不能重复 + for(let i = 0; i < len; i++) { + if(board[i][col] === val) { + return false + } + } + let startRow = Math.floor(row / 3) * 3 + let startCol = Math.floor(col / 3) * 3 + + for(let i = startRow; i < startRow + 3; i++) { + for(let j = startCol; j < startCol + 3; j++) { + if(board[i][j] === val) { + return false + } + } + } + + return true + } + + function backTracking() { + for(let i = 0; i < board.length; i++) { + for(let j = 0; j < board[0].length; j++) { + if(board[i][j] !== '.') continue + for(let val = 1; val <= 9; val++) { + if(isValid(i, j, `${val}`, board)) { + board[i][j] = `${val}` + if (backTracking()) { + return true + } + + board[i][j] = `.` + } + } + return false + } + } + return true + } + backTracking(board) + return board + +}; +``` + +### TypeScript + +```typescript +/** + Do not return anything, modify board in-place instead. + */ +function isValid(col: number, row: number, val: string, board: string[][]): boolean { + let n: number = board.length; + // 列向检查 + for (let rowIndex = 0; rowIndex < n; rowIndex++) { + if (board[rowIndex][col] === val) return false; + } + // 横向检查 + for (let colIndex = 0; colIndex < n; colIndex++) { + if (board[row][colIndex] === val) return false; + } + // 九宫格检查 + const startX = Math.floor(col / 3) * 3; + const startY = Math.floor(row / 3) * 3; + for (let rowIndex = startY; rowIndex < startY + 3; rowIndex++) { + for (let colIndex = startX; colIndex < startX + 3; colIndex++) { + if (board[rowIndex][colIndex] === val) return false; + } + } + return true; +} +function solveSudoku(board: string[][]): void { + let n: number = 9; + backTracking(n, board); + function backTracking(n: number, board: string[][]): boolean { + for (let row = 0; row < n; row++) { + for (let col = 0; col < n; col++) { + if (board[row][col] === '.') { + for (let i = 1; i <= n; i++) { + if (isValid(col, row, String(i), board)) { + board[row][col] = String(i); + if (backTracking(n, board) === true) return true; + board[row][col] = '.'; + } + } + return false; + } + } + } + return true; + } +}; +``` + +### Rust + +```Rust +impl Solution { + fn is_valid(row: usize, col: usize, val: char, board: &mut Vec>) -> bool{ + for i in 0..9 { + if board[row][i] == val { return false; } + } + for j in 0..9 { + if board[j][col] == val { + return false; + } + } + let start_row = (row / 3) * 3; + let start_col = (col / 3) * 3; + for i in start_row..(start_row + 3) { + for j in start_col..(start_col + 3) { + if board[i][j] == val { return false; } + } + } + return true; + } + + fn backtracking(board: &mut Vec>) -> bool{ + for i in 0..board.len() { + for j in 0..board[0].len() { + if board[i][j] != '.' { continue; } + for k in '1'..='9' { + if Self::is_valid(i, j, k, board) { + board[i][j] = k; + if Self::backtracking(board) { return true; } + board[i][j] = '.'; + } + } + return false; + } + } + return true; + } + + pub fn solve_sudoku(board: &mut Vec>) { + Self::backtracking(board); + } +} +``` + +### C + +```C +bool isValid(char** board, int row, int col, int k) { + /* 判断当前行是否有重复元素 */ + for (int i = 0; i < 9; i++) { + if (board[i][col] == k) { + return false; + } + } + /* 判断当前列是否有重复元素 */ + for (int j = 0; j < 9; j++) { + if (board[row][j] == k) { + return false; + } + } + /* 计算当前9宫格左上角的位置 */ + int startRow = (row / 3) * 3; + int startCol = (col / 3) * 3; + /* 判断当前元素所在九宫格是否有重复元素 */ + for (int i = startRow; i < startRow + 3; i++) { + for (int j = startCol; j < startCol + 3; j++) { + if (board[i][j] == k) { + return false; + } + } + } + /* 满足条件,返回true */ + return true; +} + +bool backtracking(char** board, int boardSize, int* boardColSize) { + /* 从上到下、从左到右依次遍历输入数组 */ + for (int i = 0; i < boardSize; i++) { + for (int j = 0; j < *boardColSize; j++) { + /* 遇到数字跳过 */ + if (board[i][j] != '.') { + continue; + } + /* 依次将数组1到9填入当前位置 */ + for (int k = '1'; k <= '9'; k++) { + /* 判断当前位置是否与满足条件,是则进入下一层 */ + if (isValid(board, i, j, k)) { + board[i][j] = k; + /* 判断下一层递归之后是否找到一种解法,是则返回true */ + if (backtracking(board, boardSize, boardColSize)) { + return true; + } + /* 回溯,将当前位置清零 */ + board[i][j] = '.'; + } + } + /* 若填入的9个数均不满足条件,返回false,说明此解法无效 */ + return false; + } + } + /* 遍历完所有的棋盘,没有返回false,说明找到了解法,返回true */ + return true; +} + +void solveSudoku(char** board, int boardSize, int* boardColSize) { + bool res = backtracking(board, boardSize, boardColSize); +} +``` + +### Swift + +```swift +func solveSudoku(_ board: inout [[Character]]) { + // 判断对应格子的值是否合法 + func isValid(row: Int, col: Int, val: Character) -> Bool { + // 行中是否重复 + for i in 0 ..< 9 { + if board[row][i] == val { return false } + } + + // 列中是否重复 + for j in 0 ..< 9 { + if board[j][col] == val { return false } + } + + // 9方格内是否重复 + let startRow = row / 3 * 3 + let startCol = col / 3 * 3 + for i in startRow ..< startRow + 3 { + for j in startCol ..< startCol + 3 { + if board[i][j] == val { return false } + } + } + return true + } + + @discardableResult + func backtracking() -> Bool { + for i in 0 ..< board.count { // i:行坐标 + for j in 0 ..< board[0].count { // j:列坐标 + guard board[i][j] == "." else { continue } // 跳过已填写格子 + // 填写格子 + for val in 1 ... 9 { + let charVal = Character("\(val)") + guard isValid(row: i, col: j, val: charVal) else { continue } // 跳过不合法的 + board[i][j] = charVal // 填写 + if backtracking() { return true } + board[i][j] = "." // 回溯:擦除 + } + return false // 遍历完数字都不行 + } + } + return true // 没有不合法的,填写正确 + } + backtracking() +} +``` + +### Scala + +详细写法: + +```scala +object Solution { + + def solveSudoku(board: Array[Array[Char]]): Unit = { + backtracking(board) + } + + def backtracking(board: Array[Array[Char]]): Boolean = { + for (i <- 0 until 9) { + for (j <- 0 until 9) { + if (board(i)(j) == '.') { // 必须是为 . 的数字才放数字 + for (k <- '1' to '9') { // 这个位置放k是否合适 + if (isVaild(i, j, k, board)) { + board(i)(j) = k + if (backtracking(board)) return true // 找到了立刻返回 + board(i)(j) = '.' // 回溯 + } + } + return false // 9个数都试完了,都不行就返回false + } + } + } + true // 遍历完所有的都没返回false,说明找到了 + } + + def isVaild(x: Int, y: Int, value: Char, board: Array[Array[Char]]): Boolean = { + // 行 + for (i <- 0 until 9 ) { + if (board(i)(y) == value) { + return false + } + } + + // 列 + for (j <- 0 until 9) { + if (board(x)(j) == value) { + return false + } + } + + // 宫 + var row = (x / 3) * 3 + var col = (y / 3) * 3 + for (i <- row until row + 3) { + for (j <- col until col + 3) { + if (board(i)(j) == value) { + return false + } + } + } + + true + } +} +``` + +遵循Scala至简原则写法: + +```scala +object Solution { + + def solveSudoku(board: Array[Array[Char]]): Unit = { + backtracking(board) + } + + def backtracking(board: Array[Array[Char]]): Boolean = { + // 双重for循环 + 循环守卫 + for (i <- 0 until 9; j <- 0 until 9 if board(i)(j) == '.') { + // 必须是为 . 的数字才放数字,使用循环守卫判断该位置是否可以放置当前循环的数字 + for (k <- '1' to '9' if isVaild(i, j, k, board)) { // 这个位置放k是否合适 + board(i)(j) = k + if (backtracking(board)) return true // 找到了立刻返回 + board(i)(j) = '.' // 回溯 + } + return false // 9个数都试完了,都不行就返回false + } + true // 遍历完所有的都没返回false,说明找到了 + } + + def isVaild(x: Int, y: Int, value: Char, board: Array[Array[Char]]): Boolean = { + // 行,循环守卫进行判断 + for (i <- 0 until 9 if board(i)(y) == value) return false + // 列,循环守卫进行判断 + for (j <- 0 until 9 if board(x)(j) == value) return false + // 宫,循环守卫进行判断 + var row = (x / 3) * 3 + var col = (y / 3) * 3 + for (i <- row until row + 3; j <- col until col + 3 if board(i)(j) == value) return false + true // 最终没有返回false,就说明该位置可以填写true + } +} +``` +### C# +```csharp +public class Solution +{ + public void SolveSudoku(char[][] board) + { + BackTracking(board); + } + public bool BackTracking(char[][] board) + { + for (int i = 0; i < board.Length; i++) + { + for (int j = 0; j < board[0].Length; j++) + { + if (board[i][j] != '.') continue; + for (char k = '1'; k <= '9'; k++) + { + if (IsValid(board, i, j, k)) + { + board[i][j] = k; + if (BackTracking(board)) return true; + board[i][j] = '.'; + } + } + return false; + } + + } + return true; + } + public bool IsValid(char[][] board, int row, int col, char val) + { + for (int i = 0; i < 9; i++) + { + if (board[i][col] == val) return false; + } + for (int i = 0; i < 9; i++) + { + if (board[row][i] == val) return false; + } + int startRow = (row / 3) * 3; + int startCol = (col / 3) * 3; + for (int i = startRow; i < startRow + 3; i++) + { + for (int j = startCol; j < startCol + 3; j++) + { + if (board[i][j] == val) return false; + } + } + return true; + } +} +``` + + diff --git "a/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" "b/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" new file mode 100755 index 0000000000..d8dac0b45b --- /dev/null +++ "b/problems/0039.\347\273\204\345\220\210\346\200\273\345\222\214.md" @@ -0,0 +1,661 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 39. 组合总和 + +[力扣题目链接](https://leetcode.cn/problems/combination-sum/) + +给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 + +candidates 中的数字可以无限制重复被选取。 + +说明: + +* 所有数字(包括 target)都是正整数。 +* 解集不能包含重复的组合。 + +示例 1: + +* 输入:candidates = [2,3,6,7], target = 7, +* 所求解集为: + [ + [7], + [2,2,3] + ] + +示例 2: + +* 输入:candidates = [2,3,5], target = 8, +* 所求解集为: + [ + [2,2,2,2], + [2,3,3], + [3,5] + ] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[Leetcode:39. 组合总和讲解](https://www.bilibili.com/video/BV1KT4y1M7HJ),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了。 + +本题和[77.组合](https://programmercarl.com/0077.组合.html),[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)的区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。 + +本题搜索的过程抽象成树形结构如下: + +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367.png) +注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回! + +而在[77.组合](https://programmercarl.com/0077.组合.html)和[216.组合总和III](https://programmercarl.com/0216.组合总和III.html) 中都可以知道要递归K层,因为要取k个元素的组合。 + +### 回溯三部曲 + +* 递归函数参数 + +这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入) + +首先是题目中给出的参数,集合candidates, 和目标值target。 + +此外我还定义了int型的sum变量来统计单一结果path里的总和,其实这个sum也可以不用,用target做相应的减法就可以了,最后如何target==0就说明找到符合的结果了,但为了代码逻辑清晰,我依然用了sum。 + +**本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?** + +我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:[77.组合](https://programmercarl.com/0077.组合.html),[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)。 + +如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[17.电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html) + +**注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我在讲解排列的时候会重点介绍**。 + +代码如下: + +```CPP +vector> result; +vector path; +void backtracking(vector& candidates, int target, int sum, int startIndex) +``` + +* 递归终止条件 + +在如下树形结构中: + +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367-20230310135337214.png) + +从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target。 + +sum等于target的时候,需要收集结果,代码如下: + +```CPP +if (sum > target) { + return; +} +if (sum == target) { + result.push_back(path); + return; +} +``` + +* 单层搜索的逻辑 + +单层for循环依然是从startIndex开始,搜索candidates集合。 + +**注意本题和[77.组合](https://programmercarl.com/0077.组合.html)、[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)的一个区别是:本题元素为可重复选取的**。 + +如何重复选取呢,看代码,注释部分: + +```CPP +for (int i = startIndex; i < candidates.size(); i++) { + sum += candidates[i]; + path.push_back(candidates[i]); + backtracking(candidates, target, sum, i); // 关键点:不用i+1了,表示可以重复读取当前的数 + sum -= candidates[i]; // 回溯 + path.pop_back(); // 回溯 +} +``` + +按照[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中给出的模板,不难写出如下C++完整代码: + +```CPP +// 版本一 +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& candidates, int target, int sum, int startIndex) { + if (sum > target) { + return; + } + if (sum == target) { + result.push_back(path); + return; + } + + for (int i = startIndex; i < candidates.size(); i++) { + sum += candidates[i]; + path.push_back(candidates[i]); + backtracking(candidates, target, sum, i); // 不用i+1了,表示可以重复读取当前的数 + sum -= candidates[i]; + path.pop_back(); + } + } +public: + vector> combinationSum(vector& candidates, int target) { + result.clear(); + path.clear(); + backtracking(candidates, target, 0, 0); + return result; + } +}; +``` + +### 剪枝优化 + +在这个树形结构中: + +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367-20230310135342472.png) + +以及上面的版本一的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。 + +其实如果已经知道下一层的sum会大于target,就没有必要进入下一层递归了。 + +那么可以在for循环的搜索范围上做做文章了。 + +**对总集合排序之后,如果下一层的sum(就是本层的 sum + candidates[i])已经大于target,就可以结束本轮for循环的遍历**。 + +如图: + + +![39.组合总和1](https://file1.kamacoder.com/i/algo/20201223170809182.png) + +for循环剪枝代码如下: + +``` +for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) +``` + +整体代码如下:(注意注释的部分) + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& candidates, int target, int sum, int startIndex) { + if (sum == target) { + result.push_back(path); + return; + } + + // 如果 sum + candidates[i] > target 就终止遍历 + for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + sum += candidates[i]; + path.push_back(candidates[i]); + backtracking(candidates, target, sum, i); + sum -= candidates[i]; + path.pop_back(); + + } + } +public: + vector> combinationSum(vector& candidates, int target) { + result.clear(); + path.clear(); + sort(candidates.begin(), candidates.end()); // 需要排序 + backtracking(candidates, target, 0, 0); + return result; + } +}; +``` +* 时间复杂度: O(n * 2^n),注意这只是复杂度的上界,因为剪枝的存在,真实的时间复杂度远小于此 +* 空间复杂度: O(target) + +## 总结 + +本题和我们之前讲过的[77.组合](https://programmercarl.com/0077.组合.html)、[216.组合总和III](https://programmercarl.com/0216.组合总和III.html)有两点不同: + +* 组合没有数量要求 +* 元素可无限重复选取 + +针对这两个问题,我都做了详细的分析。 + +并且给出了对于组合问题,什么时候用startIndex,什么时候不用,并用[17.电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html)做了对比。 + +最后还给出了本题的剪枝优化,这个优化如果是初学者的话并不容易想到。 + +**在求和问题中,排序之后加剪枝是常见的套路!** + +可以看出我写的文章都会大量引用之前的文章,就是要不断作对比,分析其差异,然后给出代码解决的方法,这样才能彻底理解题目的本质与难点。 + + + + + +## 其他语言版本 + + +### Java + +```Java +// 剪枝优化 +class Solution { + public List> combinationSum(int[] candidates, int target) { + List> res = new ArrayList<>(); + Arrays.sort(candidates); // 先进行排序 + backtracking(res, new ArrayList<>(), candidates, target, 0, 0); + return res; + } + + public void backtracking(List> res, List path, int[] candidates, int target, int sum, int idx) { + // 找到了数字和为 target 的组合 + if (sum == target) { + res.add(new ArrayList<>(path)); + return; + } + + for (int i = idx; i < candidates.length; i++) { + // 如果 sum + candidates[i] > target 就终止遍历 + if (sum + candidates[i] > target) break; + path.add(candidates[i]); + backtracking(res, path, candidates, target, sum + candidates[i], i); + path.remove(path.size() - 1); // 回溯,移除路径 path 最后一个元素 + } + } +} +``` + +### Python + +回溯(版本一) + +```python +class Solution: + + def backtracking(self, candidates, target, total, startIndex, path, result): + if total > target: + return + if total == target: + result.append(path[:]) + return + + for i in range(startIndex, len(candidates)): + total += candidates[i] + path.append(candidates[i]) + self.backtracking(candidates, target, total, i, path, result) # 不用i+1了,表示可以重复读取当前的数 + total -= candidates[i] + path.pop() + + def combinationSum(self, candidates, target): + result = [] + self.backtracking(candidates, target, 0, 0, [], result) + return result + +``` + +回溯剪枝(版本一) + +```python +class Solution: + + def backtracking(self, candidates, target, total, startIndex, path, result): + if total == target: + result.append(path[:]) + return + + for i in range(startIndex, len(candidates)): + if total + candidates[i] > target: + break + total += candidates[i] + path.append(candidates[i]) + self.backtracking(candidates, target, total, i, path, result) + total -= candidates[i] + path.pop() + + def combinationSum(self, candidates, target): + result = [] + candidates.sort() # 需要排序 + self.backtracking(candidates, target, 0, 0, [], result) + return result + +``` + +回溯(版本二) + +```python +class Solution: + def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: + result =[] + self.backtracking(candidates, target, 0, [], result) + return result + def backtracking(self, candidates, target, startIndex, path, result): + if target == 0: + result.append(path[:]) + return + if target < 0: + return + for i in range(startIndex, len(candidates)): + path.append(candidates[i]) + self.backtracking(candidates, target - candidates[i], i, path, result) + path.pop() + +``` + +回溯剪枝(版本二) + +```python +class Solution: + def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]: + result =[] + candidates.sort() + self.backtracking(candidates, target, 0, [], result) + return result + def backtracking(self, candidates, target, startIndex, path, result): + if target == 0: + result.append(path[:]) + return + + for i in range(startIndex, len(candidates)): + if target - candidates[i] < 0: + break + path.append(candidates[i]) + self.backtracking(candidates, target - candidates[i], i, path, result) + path.pop() + +``` + +### Go + +主要在于递归中传递下一个数字 + +```go +var ( + res [][]int + path []int +) +func combinationSum(candidates []int, target int) [][]int { + res, path = make([][]int, 0), make([]int, 0, len(candidates)) + sort.Ints(candidates) // 排序,为剪枝做准备 + dfs(candidates, 0, target) + return res +} + +func dfs(candidates []int, start int, target int) { + if target == 0 { // target 不断减小,如果为0说明达到了目标值 + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + return + } + for i := start; i < len(candidates); i++ { + if candidates[i] > target { // 剪枝,提前返回 + break + } + path = append(path, candidates[i]) + dfs(candidates, i, target - candidates[i]) + path = path[:len(path) - 1] + } +} +``` + +### JavaScript + +```js +var combinationSum = function(candidates, target) { + const res = [], path = []; + candidates.sort((a,b)=>a-b); // 排序 + backtracking(0, 0); + return res; + function backtracking(j, sum) { + if (sum === target) { + res.push(Array.from(path)); + return; + } + for(let i = j; i < candidates.length; i++ ) { + const n = candidates[i]; + if(n > target - sum) break; + path.push(n); + sum += n; + backtracking(i, sum); + path.pop(); + sum -= n; + } + } +}; +``` + +### TypeScript + +```typescript +function combinationSum(candidates: number[], target: number): number[][] { + const resArr: number[][] = []; + function backTracking( + candidates: number[], target: number, + startIndex: number, route: number[], curSum: number + ): void { + if (curSum > target) return; + if (curSum === target) { + resArr.push(route.slice()); + return + } + for (let i = startIndex, length = candidates.length; i < length; i++) { + let tempVal: number = candidates[i]; + route.push(tempVal); + backTracking(candidates, target, i, route, curSum + tempVal); + route.pop(); + } + } + backTracking(candidates, target, 0, [], 0); + return resArr; +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn backtracking(result: &mut Vec>, path: &mut Vec, candidates: &Vec, target: i32, mut sum: i32, start_index: usize) { + if sum == target { + result.push(path.to_vec()); + return; + } + for i in start_index..candidates.len() { + if sum + candidates[i] <= target { + sum += candidates[i]; + path.push(candidates[i]); + Self::backtracking(result, path, candidates, target, sum, i); + sum -= candidates[i]; + path.pop(); + } + } + } + + pub fn combination_sum(candidates: Vec, target: i32) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + Self::backtracking(&mut result, &mut path, &candidates, target, 0, 0); + result + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; +//记录每一个和等于target的path数组长度 +int* length; + +void backTracking(int target, int index, int* candidates, int candidatesSize, int sum) { + //若sum>=target就应该终止遍历 + if(sum >= target) { + //若sum等于target,将当前的组合放入ans数组中 + if(sum == target) { + int* tempPath = (int*)malloc(sizeof(int) * pathTop); + int j; + for(j = 0; j < pathTop; j++) { + tempPath[j] = path[j]; + } + ans[ansTop] = tempPath; + length[ansTop++] = pathTop; + } + return ; + } + + int i; + for(i = index; i < candidatesSize; i++) { + //将当前数字大小加入sum + sum+=candidates[i]; + path[pathTop++] = candidates[i]; + backTracking(target, i, candidates, candidatesSize, sum); + sum-=candidates[i]; + pathTop--; + } +} + +int** combinationSum(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes){ + //初始化变量 + path = (int*)malloc(sizeof(int) * 50); + ans = (int**)malloc(sizeof(int*) * 200); + length = (int*)malloc(sizeof(int) * 200); + ansTop = pathTop = 0; + backTracking(target, 0, candidates, candidatesSize, 0); + + //设置返回的数组大小 + *returnSize = ansTop; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; i++) { + (*returnColumnSizes)[i] = length[i]; + } + return ans; +} +``` + +### Swift + +```swift +func combinationSum(_ candidates: [Int], _ target: Int) -> [[Int]] { + var result = [[Int]]() + var path = [Int]() + func backtracking(sum: Int, startIndex: Int) { + // 终止条件 + if sum == target { + result.append(path) + return + } + + let end = candidates.count + guard startIndex < end else { return } + for i in startIndex ..< end { + let sum = sum + candidates[i] // 使用局部变量隐藏回溯 + if sum > target { continue } // 剪枝 + + path.append(candidates[i]) // 处理 + backtracking(sum: sum, startIndex: i) // i不用+1以重复访问 + path.removeLast() // 回溯 + } + } + backtracking(sum: 0, startIndex: 0) + return result +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def combinationSum(candidates: Array[Int], target: Int): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + + def backtracking(sum: Int, index: Int): Unit = { + if (sum == target) { + result.append(path.toList) // 如果正好等于target,就添加到结果集 + return + } + // 应该是从当前索引开始的,而不是从0 + // 剪枝优化:添加循环守卫,当sum + c(i) <= target的时候才循环,才可以进入下一次递归 + for (i <- index until candidates.size if sum + candidates(i) <= target) { + path.append(candidates(i)) + backtracking(sum + candidates(i), i) + path = path.take(path.size - 1) + } + } + + backtracking(0, 0) + result.toList + } +} +``` +### C# +```csharp +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> CombinationSum(int[] candidates, int target) + { + BackTracking(candidates, target, 0, 0); + return res; + } + public void BackTracking(int[] candidates, int target, int start, int sum) + { + if (sum > target) return; + if (sum == target) + { + res.Add(new List(path)); + return; + } + for (int i = start; i < candidates.Length; i++) + { + sum += candidates[i]; + path.Add(candidates[i]); + BackTracking(candidates, target, i, sum); + sum -= candidates[i]; + path.RemoveAt(path.Count - 1); + } + } +} + +// 剪枝优化 +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> CombinationSum(int[] candidates, int target) + { + Array.Sort(candidates); + BackTracking(candidates, target, 0, 0); + return res; + } + public void BackTracking(int[] candidates, int target, int start, int sum) + { + if (sum > target) return; + if (sum == target) + { + res.Add(new List(path)); + return; + } + for (int i = start; i < candidates.Length && sum + candidates[i] <= target; i++) + { + sum += candidates[i]; + path.Add(candidates[i]); + BackTracking(candidates, target, i, sum); + sum -= candidates[i]; + path.RemoveAt(path.Count - 1); + } + } +} +``` + + + diff --git "a/problems/0040.\347\273\204\345\220\210\346\200\273\345\222\214II.md" "b/problems/0040.\347\273\204\345\220\210\346\200\273\345\222\214II.md" new file mode 100755 index 0000000000..0d3972662f --- /dev/null +++ "b/problems/0040.\347\273\204\345\220\210\346\200\273\345\222\214II.md" @@ -0,0 +1,805 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 40.组合总和II + +[力扣题目链接](https://leetcode.cn/problems/combination-sum-ii/) + +给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 + +candidates 中的每个数字在每个组合中只能使用一次。 + +说明: +所有数字(包括目标数)都是正整数。解集不能包含重复的组合。  + +* 示例 1: +* 输入: candidates = [10,1,2,7,6,1,5], target = 8, +* 所求解集为: +``` +[ + [1, 7], + [1, 2, 5], + [2, 6], + [1, 1, 6] +] +``` + +* 示例 2: +* 输入: candidates = [2,5,2,1,2], target = 5, +* 所求解集为: + +``` +[ +  [1,2,2], +  [5] +] +``` + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[回溯算法中的去重,树层去重树枝去重,你弄清楚了没?| LeetCode:40.组合总和II](https://www.bilibili.com/video/BV12V4y1V73A),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +这道题目和[39.组合总和](https://programmercarl.com/0039.组合总和.html)如下区别: + +1. 本题candidates 中的每个数字在每个组合中只能使用一次。 +2. 本题数组candidates的元素是有重复的,而[39.组合总和](https://programmercarl.com/0039.组合总和.html)是无重复元素的数组candidates + +最后本题和[39.组合总和](https://programmercarl.com/0039.组合总和.html)要求一样,解集不能包含重复的组合。 + +**本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合**。 + +一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时! + +所以要在搜索的过程中就去掉重复组合。 + +很多同学在去重的问题上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。 + +这个去重为什么很难理解呢,**所谓去重,其实就是使用过的元素不能重复选取。** 这么一说好像很简单! + +都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上使用过,一个维度是同一树层上使用过。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因。** + +那么问题来了,我们是要同一树层上使用过,还是同一树枝上使用过呢? + +回看一下题目,元素在同一个组合内是可以重复的,怎么重复都没事,但两个组合不能相同。 + + +**所以我们要去重的是同一树层上的“使用过”,同一树枝上的都是一个组合里的元素,不用去重**。 + +为了理解去重我们来举一个例子,candidates = [1, 1, 2], target = 3,(方便起见candidates已经排序了) + +**强调一下,树层去重的话,需要对数组排序!** + +选择过程树形结构如图所示: + +![40.组合总和II](https://file1.kamacoder.com/i/algo/20230310000918.png) + +可以看到图中,每个节点相对于 [39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)我多加了used数组,这个used数组下面会重点介绍。 + +### 回溯三部曲 + +* **递归函数参数** + +与[39.组合总和](https://programmercarl.com/0039.组合总和.html)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。 + +这个集合去重的重任就是used来完成的。 + +代码如下: + +```CPP +vector> result; // 存放组合集合 +vector path; // 符合条件的组合 +void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { +``` + +* **递归终止条件** + +与[39.组合总和](https://programmercarl.com/0039.组合总和.html)相同,终止条件为 `sum > target` 和 `sum == target`。 + +代码如下: + +```CPP +if (sum > target) { // 这个条件其实可以省略 + return; +} +if (sum == target) { + result.push_back(path); + return; +} +``` + +`sum > target` 这个条件其实可以省略,因为在递归单层遍历的时候,会有剪枝的操作,下面会介绍到。 + +* **单层搜索的逻辑** + +这里与[39.组合总和](https://programmercarl.com/0039.组合总和.html)最大的不同就是要去重了。 + +前面我们提到:要去重的是“同一树层上的使用过”,如何判断同一树层上元素(相同的元素)是否使用过了呢。 + +**如果`candidates[i] == candidates[i - 1]` 并且 `used[i - 1] == false`,就说明:前一个树枝,使用了candidates[i - 1],也就是说同一树层使用过candidates[i - 1]**。 + +此时for循环里就应该做continue的操作。 + +这块比较抽象,如图: + +![40.组合总和II1](https://file1.kamacoder.com/i/algo/20230310000954.png) + +我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下: + +* used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 +* used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + +可能有的录友想,为什么 used[i - 1] == false 就是同一树层呢,因为同一树层,used[i - 1] == false 才能表示,当前取的 candidates[i] 是从 candidates[i - 1] 回溯而来的。 + +而 used[i - 1] == true,说明是进入下一层递归,去下一个数,所以是树枝上,如图所示: + +![](https://file1.kamacoder.com/i/algo/20221021163812.png) + + +**这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!** + + +那么单层搜索的逻辑代码如下: + +```CPP +for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 + // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + // 要对同一树层使用过的元素进行跳过 + if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { + continue; + } + sum += candidates[i]; + path.push_back(candidates[i]); + used[i] = true; + backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1:这里是i+1,每个数字在每个组合中只能使用一次 + used[i] = false; + sum -= candidates[i]; + path.pop_back(); +} +``` + +**注意sum + candidates[i] <= target为剪枝操作,在[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)有讲解过!** + + +回溯三部曲分析完了,整体C++代码如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { + if (sum == target) { + result.push_back(path); + return; + } + for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 + // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + // 要对同一树层使用过的元素进行跳过 + if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false) { + continue; + } + sum += candidates[i]; + path.push_back(candidates[i]); + used[i] = true; + backtracking(candidates, target, sum, i + 1, used); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次 + used[i] = false; + sum -= candidates[i]; + path.pop_back(); + } + } + +public: + vector> combinationSum2(vector& candidates, int target) { + vector used(candidates.size(), false); + path.clear(); + result.clear(); + // 首先把给candidates排序,让其相同的元素都挨在一起。 + sort(candidates.begin(), candidates.end()); + backtracking(candidates, target, 0, 0, used); + return result; + } +}; + +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n) + +### 补充 + +这里直接用startIndex来去重也是可以的, 就不用used数组了。 + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& candidates, int target, int sum, int startIndex) { + if (sum == target) { + result.push_back(path); + return; + } + for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + // 要对同一树层使用过的元素进行跳过 + if (i > startIndex && candidates[i] == candidates[i - 1]) { + continue; + } + sum += candidates[i]; + path.push_back(candidates[i]); + backtracking(candidates, target, sum, i + 1); // 和39.组合总和的区别1,这里是i+1,每个数字在每个组合中只能使用一次 + sum -= candidates[i]; + path.pop_back(); + } + } + +public: + vector> combinationSum2(vector& candidates, int target) { + path.clear(); + result.clear(); + // 首先把给candidates排序,让其相同的元素都挨在一起。 + sort(candidates.begin(), candidates.end()); + backtracking(candidates, target, 0, 0); + return result; + } +}; + +``` + +## 总结 + +本题同样是求组合总和,但就是因为其数组candidates有重复元素,而要求不能有重复的组合,所以相对于[39.组合总和](https://programmercarl.com/0039.组合总和.html)难度提升了不少。 + +**关键是去重的逻辑,代码很简单,网上一搜一大把,但几乎没有能把这块代码含义讲明白的,基本都是给出代码,然后说这就是去重了,究竟怎么个去重法也是模棱两可**。 + +所以Carl有必要把去重的这块彻彻底底的给大家讲清楚,**就连“树层去重”和“树枝去重”都是我自创的词汇,希望对大家理解有帮助!** + +## 其他语言版本 + + +### Java +**使用标记数组** +```Java +class Solution { + LinkedList path = new LinkedList<>(); + List> ans = new ArrayList<>(); + boolean[] used; + int sum = 0; + + public List> combinationSum2(int[] candidates, int target) { + used = new boolean[candidates.length]; + // 加标志数组,用来辅助判断同层节点是否已经遍历 + Arrays.fill(used, false); + // 为了将重复的数字都放到一起,所以先进行排序 + Arrays.sort(candidates); + backTracking(candidates, target, 0); + return ans; + } + + private void backTracking(int[] candidates, int target, int startIndex) { + if (sum == target) { + ans.add(new ArrayList(path)); + } + for (int i = startIndex; i < candidates.length; i++) { + if (sum + candidates[i] > target) { + break; + } + // 出现重复节点,同层的第一个节点已经被访问过,所以直接跳过 + if (i > 0 && candidates[i] == candidates[i - 1] && !used[i - 1]) { + continue; + } + used[i] = true; + sum += candidates[i]; + path.add(candidates[i]); + // 每个节点仅能选择一次,所以从下一位开始 + backTracking(candidates, target, i + 1); + used[i] = false; + sum -= candidates[i]; + path.removeLast(); + } + } +} + +``` +**不使用标记数组** +```Java +class Solution { + List> res = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + int sum = 0; + + public List> combinationSum2( int[] candidates, int target ) { + //为了将重复的数字都放到一起,所以先进行排序 + Arrays.sort( candidates ); + backTracking( candidates, target, 0 ); + return res; + } + + private void backTracking( int[] candidates, int target, int start ) { + if ( sum == target ) { + res.add( new ArrayList<>( path ) ); + return; + } + for ( int i = start; i < candidates.length && sum + candidates[i] <= target; i++ ) { + //正确剔除重复解的办法 + //跳过同一树层使用过的元素 + if ( i > start && candidates[i] == candidates[i - 1] ) { + continue; + } + + sum += candidates[i]; + path.add( candidates[i] ); + // i+1 代表当前组内元素只选取一次 + backTracking( candidates, target, i + 1 ); + + int temp = path.getLast(); + sum -= temp; + path.removeLast(); + } + } +} +``` + +### Python +回溯 +```python +class Solution: + + + def backtracking(self, candidates, target, total, startIndex, path, result): + if total == target: + result.append(path[:]) + return + + for i in range(startIndex, len(candidates)): + if i > startIndex and candidates[i] == candidates[i - 1]: + continue + + if total + candidates[i] > target: + break + + total += candidates[i] + path.append(candidates[i]) + self.backtracking(candidates, target, total, i + 1, path, result) + total -= candidates[i] + path.pop() + + def combinationSum2(self, candidates, target): + result = [] + candidates.sort() + self.backtracking(candidates, target, 0, 0, [], result) + return result + +``` +回溯 使用used +```python +class Solution: + + + def backtracking(self, candidates, target, total, startIndex, used, path, result): + if total == target: + result.append(path[:]) + return + + for i in range(startIndex, len(candidates)): + # 对于相同的数字,只选择第一个未被使用的数字,跳过其他相同数字 + if i > startIndex and candidates[i] == candidates[i - 1] and not used[i - 1]: + continue + + if total + candidates[i] > target: + break + + total += candidates[i] + path.append(candidates[i]) + used[i] = True + self.backtracking(candidates, target, total, i + 1, used, path, result) + used[i] = False + total -= candidates[i] + path.pop() + + def combinationSum2(self, candidates, target): + used = [False] * len(candidates) + result = [] + candidates.sort() + self.backtracking(candidates, target, 0, 0, used, [], result) + return result + +``` +回溯优化 +```python +class Solution: + def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]: + candidates.sort() + results = [] + self.combinationSumHelper(candidates, target, 0, [], results) + return results + + def combinationSumHelper(self, candidates, target, index, path, results): + if target == 0: + results.append(path[:]) + return + for i in range(index, len(candidates)): + if i > index and candidates[i] == candidates[i - 1]: + continue + if candidates[i] > target: + break + path.append(candidates[i]) + self.combinationSumHelper(candidates, target - candidates[i], i + 1, path, results) + path.pop() +``` +### Go +主要在于如何在回溯中去重 + +**使用used数组** +```go +var ( + res [][]int + path []int + used []bool +) +func combinationSum2(candidates []int, target int) [][]int { + res, path = make([][]int, 0), make([]int, 0, len(candidates)) + used = make([]bool, len(candidates)) + sort.Ints(candidates) // 排序,为剪枝做准备 + dfs(candidates, 0, target) + return res +} + +func dfs(candidates []int, start int, target int) { + if target == 0 { // target 不断减小,如果为0说明达到了目标值 + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + return + } + for i := start; i < len(candidates); i++ { + if candidates[i] > target { // 剪枝,提前返回 + break + } + // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 + // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + if i > 0 && candidates[i] == candidates[i-1] && used[i-1] == false { + continue + } + path = append(path, candidates[i]) + used[i] = true + dfs(candidates, i+1, target - candidates[i]) + used[i] = false + path = path[:len(path) - 1] + } +} +``` +**不使用used数组** +```go +var ( + res [][]int + path []int +) +func combinationSum2(candidates []int, target int) [][]int { + res, path = make([][]int, 0), make([]int, 0, len(candidates)) + sort.Ints(candidates) // 排序,为剪枝做准备 + dfs(candidates, 0, target) + return res +} + +func dfs(candidates []int, start int, target int) { + if target == 0 { // target 不断减小,如果为0说明达到了目标值 + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + return + } + for i := start; i < len(candidates); i++ { + if candidates[i] > target { // 剪枝,提前返回 + break + } + // i != start 限制了这不对深度遍历到达的此值去重 + if i != start && candidates[i] == candidates[i-1] { // 去重 + continue + } + path = append(path, candidates[i]) + dfs(candidates, i+1, target - candidates[i]) + path = path[:len(path) - 1] + } +} +``` +### JavaScript + +```js +/** + * @param {number[]} candidates + * @param {number} target + * @return {number[][]} + */ +var combinationSum2 = function(candidates, target) { + const res = []; path = [], len = candidates.length; + candidates.sort((a,b)=>a-b); + backtracking(0, 0); + return res; + function backtracking(sum, i) { + if (sum === target) { + res.push(Array.from(path)); + return; + } + for(let j = i; j < len; j++) { + const n = candidates[j]; + if(j > i && candidates[j] === candidates[j-1]){ + //若当前元素和前一个元素相等 + //则本次循环结束,防止出现重复组合 + continue; + } + //如果当前元素值大于目标值-总和的值 + //由于数组已排序,那么该元素之后的元素必定不满足条件 + //直接终止当前层的递归 + if(n > target - sum) break; + path.push(n); + sum += n; + backtracking(sum, j + 1); + path.pop(); + sum -= n; + } + } +}; +``` +**使用used去重** + +```js +var combinationSum2 = function(candidates, target) { + let res = []; + let path = []; + let total = 0; + const len = candidates.length; + candidates.sort((a, b) => a - b); + let used = new Array(len).fill(false); + const backtracking = (startIndex) => { + if (total === target) { + res.push([...path]); + return; + } + for(let i = startIndex; i < len && total < target; i++) { + const cur = candidates[i]; + if (cur > target - total || (i > 0 && cur === candidates[i - 1] && !used[i - 1])) continue; + path.push(cur); + total += cur; + used[i] = true; + backtracking(i + 1); + path.pop(); + total -= cur; + used[i] = false; + } + } + backtracking(0); + return res; +}; +``` + +### TypeScript + +```typescript +function combinationSum2(candidates: number[], target: number): number[][] { + candidates.sort((a, b) => a - b); + const resArr: number[][] = []; + function backTracking( + candidates: number[], target: number, + curSum: number, startIndex: number, route: number[] + ) { + if (curSum > target) return; + if (curSum === target) { + resArr.push(route.slice()); + return; + } + for (let i = startIndex, length = candidates.length; i < length; i++) { + if (i > startIndex && candidates[i] === candidates[i - 1]) { + continue; + } + let tempVal: number = candidates[i]; + route.push(tempVal); + backTracking(candidates, target, curSum + tempVal, i + 1, route); + route.pop(); + + } + } + backTracking(candidates, target, 0, 0, []); + return resArr; +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn backtracking(result: &mut Vec>, path: &mut Vec, candidates: &Vec, target: i32, mut sum: i32, start_index: usize, used: &mut Vec) { + if sum == target { + result.push(path.to_vec()); + return; + } + for i in start_index..candidates.len() { + if sum + candidates[i] <= target { + if i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false { continue; } + sum += candidates[i]; + path.push(candidates[i]); + used[i] = true; + Self::backtracking(result, path, candidates, target, sum, i + 1, used); + used[i] = false; + sum -= candidates[i]; + path.pop(); + } + } + } + + pub fn combination_sum2(candidates: Vec, target: i32) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + let mut used: Vec = vec![false; candidates.len()]; + let mut candidates = candidates; + candidates.sort(); + Self::backtracking(&mut result, &mut path, &candidates, target, 0, 0, &mut used); + result + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; +//记录ans中每一个一维数组的大小 +int* length; +int cmp(const void* a1, const void* a2) { + return *((int*)a1) - *((int*)a2); +} + +void backTracking(int* candidates, int candidatesSize, int target, int sum, int startIndex) { + if(sum >= target) { + //若sum等于target,复制当前path进入 + if(sum == target) { + int* tempPath = (int*)malloc(sizeof(int) * pathTop); + int j; + for(j = 0; j < pathTop; j++) { + tempPath[j] = path[j]; + } + length[ansTop] = pathTop; + ans[ansTop++] = tempPath; + } + return ; + } + + int i; + for(i = startIndex; i < candidatesSize; i++) { + //对同一层树中使用过的元素跳过 + if(i > startIndex && candidates[i] == candidates[i-1]) + continue; + path[pathTop++] = candidates[i]; + sum += candidates[i]; + backTracking(candidates, candidatesSize, target, sum, i + 1); + //回溯 + sum -= candidates[i]; + pathTop--; + } +} + +int** combinationSum2(int* candidates, int candidatesSize, int target, int* returnSize, int** returnColumnSizes){ + path = (int*)malloc(sizeof(int) * 50); + ans = (int**)malloc(sizeof(int*) * 100); + length = (int*)malloc(sizeof(int) * 100); + pathTop = ansTop = 0; + //快速排序candidates,让相同元素挨到一起 + qsort(candidates, candidatesSize, sizeof(int), cmp); + + backTracking(candidates, candidatesSize, target, 0, 0); + + *returnSize = ansTop; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; i++) { + (*returnColumnSizes)[i] = length[i]; + } + return ans; +} +``` + +### Swift + +```swift +func combinationSum2(_ candidates: [Int], _ target: Int) -> [[Int]] { + // 为了方便去重复,先对集合排序 + let candidates = candidates.sorted() + var result = [[Int]]() + var path = [Int]() + func backtracking(sum: Int, startIndex: Int) { + // 终止条件 + if sum == target { + result.append(path) + return + } + + let end = candidates.count + guard startIndex < end else { return } + for i in startIndex ..< end { + if i > startIndex, candidates[i] == candidates[i - 1] { continue } // 去重复 + let sum = sum + candidates[i] // 使用局部变量隐藏回溯 + if sum > target { continue } // 剪枝 + + path.append(candidates[i]) // 处理 + backtracking(sum: sum, startIndex: i + 1) // i+1避免重复访问 + path.removeLast() // 回溯 + } + } + backtracking(sum: 0, startIndex: 0) + return result +} +``` + + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def combinationSum2(candidates: Array[Int], target: Int): List[List[Int]] = { + var res = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + var candidate = candidates.sorted + + def backtracking(sum: Int, startIndex: Int): Unit = { + if (sum == target) { + res.append(path.toList) + return + } + + for (i <- startIndex until candidate.size if sum + candidate(i) <= target) { + if (!(i > startIndex && candidate(i) == candidate(i - 1))) { + path.append(candidate(i)) + backtracking(sum + candidate(i), i + 1) + path = path.take(path.size - 1) + } + } + } + + backtracking(0, 0) + res.toList + } +} +``` +### C# +```csharp +public class Solution +{ + public List> res = new List>(); + public List path = new List(); + public IList> CombinationSum2(int[] candidates, int target) + { + + Array.Sort(candidates); + BackTracking(candidates, target, 0, 0); + return res; + } + public void BackTracking(int[] candidates, int target, int start, int sum) + { + if (sum > target) return; + if (sum == target) + { + res.Add(new List(path)); + return; + } + for (int i = start; i < candidates.Length && sum + candidates[i] <= target; i++) + { + if (i > start && candidates[i] == candidates[i - 1]) continue; + sum += candidates[i]; + path.Add(candidates[i]); + BackTracking(candidates, target, i + 1, sum); + sum -= candidates[i]; + path.RemoveAt(path.Count - 1); + } + } +} +``` + diff --git "a/problems/0042.\346\216\245\351\233\250\346\260\264.md" "b/problems/0042.\346\216\245\351\233\250\346\260\264.md" new file mode 100755 index 0000000000..c208637b2f --- /dev/null +++ "b/problems/0042.\346\216\245\351\233\250\346\260\264.md" @@ -0,0 +1,1094 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + + +# 42. 接雨水 + +[力扣题目链接](https://leetcode.cn/problems/trapping-rain-water/) + +给定 n 个非负整数表示每个宽度为 1 的柱子的高度图,计算按此排列的柱子,下雨之后能接多少雨水。 + +示例 1: + +![](https://code-thinking-1253855093.cos.ap-guangzhou.myqcloud.com/pics/20210713205038.png) + +* 输入:height = [0,1,0,2,1,0,1,3,2,1,2,1] +* 输出:6 +* 解释:上面是由数组 [0,1,0,2,1,0,1,3,2,1,2,1] 表示的高度图,在这种情况下,可以接 6 个单位的雨水(蓝色部分表示雨水)。 + +示例 2: + +* 输入:height = [4,2,0,3,2,5] +* 输出:9 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[单调栈,经典来袭!LeetCode:42.接雨水](https://www.bilibili.com/video/BV1uD4y1u75P/),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +接雨水问题在面试中还是常见题目的,有必要好好讲一讲。 + +本文深度讲解如下三种方法: + +* 暴力解法 +* 双指针优化 +* 单调栈 + +### 暴力解法 + +本题暴力解法也是也是使用双指针。 + +首先要明确,要按照行来计算,还是按照列来计算。 + +按照行来计算如图: +![42.接雨水2](https://file1.kamacoder.com/i/algo/20210402091118927.png) + +按照列来计算如图: +![42.接雨水1](https://file1.kamacoder.com/i/algo/20210402091208445.png) + +一些同学在实现的时候,很容易一会按照行来计算一会按照列来计算,这样就会越写越乱。 + +我个人倾向于按照列来计算,比较容易理解,接下来看一下按照列如何计算。 + +首先,**如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。** + +可以看出每一列雨水的高度,取决于,该列 左侧最高的柱子和右侧最高的柱子中最矮的那个柱子的高度。 + +这句话可以有点绕,来举一个理解,例如求列4的雨水高度,如图: + +![42.接雨水3](https://file1.kamacoder.com/i/algo/20210223092732301.png) + +列4 左侧最高的柱子是列3,高度为2(以下用lHeight表示)。 + +列4 右侧最高的柱子是列7,高度为3(以下用rHeight表示)。 + +列4 柱子的高度为1(以下用height表示) + +那么列4的雨水高度为 列3和列7的高度最小值减列4高度,即: min(lHeight, rHeight) - height。 + +列4的雨水高度求出来了,宽度为1,相乘就是列4的雨水体积了。 + +此时求出了列4的雨水体积。 + +一样的方法,只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。 + +首先从头遍历所有的列,并且**要注意第一个柱子和最后一个柱子不接雨水**,代码如下: + +```CPP +for (int i = 0; i < height.size(); i++) { + // 第一个柱子和最后一个柱子不接雨水 + if (i == 0 || i == height.size() - 1) continue; +} +``` + +在for循环中求左右两边最高柱子,代码如下: + +```CPP +int rHeight = height[i]; // 记录右边柱子的最高高度 +int lHeight = height[i]; // 记录左边柱子的最高高度 +for (int r = i + 1; r < height.size(); r++) { + if (height[r] > rHeight) rHeight = height[r]; +} +for (int l = i - 1; l >= 0; l--) { + if (height[l] > lHeight) lHeight = height[l]; +} +``` + +最后,计算该列的雨水高度,代码如下: + +```CPP +int h = min(lHeight, rHeight) - height[i]; +if (h > 0) sum += h; // 注意只有h大于零的时候,在统计到总和中 +``` + +整体代码如下: + +```CPP +class Solution { +public: + int trap(vector& height) { + int sum = 0; + for (int i = 0; i < height.size(); i++) { + // 第一个柱子和最后一个柱子不接雨水 + if (i == 0 || i == height.size() - 1) continue; + + int rHeight = height[i]; // 记录右边柱子的最高高度 + int lHeight = height[i]; // 记录左边柱子的最高高度 + for (int r = i + 1; r < height.size(); r++) { + if (height[r] > rHeight) rHeight = height[r]; + } + for (int l = i - 1; l >= 0; l--) { + if (height[l] > lHeight) lHeight = height[l]; + } + int h = min(lHeight, rHeight) - height[i]; + if (h > 0) sum += h; + } + return sum; + } +}; +``` + +因为每次遍历列的时候,还要向两边寻找最高的列,所以时间复杂度为O(n^2),空间复杂度为O(1)。 + +力扣后面修改了后台测试数据,所以以上暴力解法超时了。 + +### 双指针优化 + + +在暴力解法中,我们可以看到只要记录左边柱子的最高高度 和 右边柱子的最高高度,就可以计算当前位置的雨水面积,这就是通过列来计算。 + +当前列雨水面积:min(左边柱子的最高高度,记录右边柱子的最高高度) - 当前柱子高度。 + +为了得到两边的最高高度,使用了双指针来遍历,每到一个柱子都向两边遍历一遍,这其实是有重复计算的。我们把每一个位置的左边最高高度记录在一个数组上(maxLeft),右边最高高度记录在一个数组上(maxRight),这样就避免了重复计算。 + +当前位置,左边的最高高度是前一个位置的左边最高高度和本高度的最大值。 + +即从左向右遍历:maxLeft[i] = max(height[i], maxLeft[i - 1]); + +从右向左遍历:maxRight[i] = max(height[i], maxRight[i + 1]); + +代码如下: + +```CPP +class Solution { +public: + int trap(vector& height) { + if (height.size() <= 2) return 0; + vector maxLeft(height.size(), 0); + vector maxRight(height.size(), 0); + int size = maxRight.size(); + + // 记录每个柱子左边柱子最大高度 + maxLeft[0] = height[0]; + for (int i = 1; i < size; i++) { + maxLeft[i] = max(height[i], maxLeft[i - 1]); + } + // 记录每个柱子右边柱子最大高度 + maxRight[size - 1] = height[size - 1]; + for (int i = size - 2; i >= 0; i--) { + maxRight[i] = max(height[i], maxRight[i + 1]); + } + // 求和 + int sum = 0; + for (int i = 0; i < size; i++) { + int count = min(maxLeft[i], maxRight[i]) - height[i]; + if (count > 0) sum += count; + } + return sum; + } +}; +``` + +### 单调栈解法 + +关于单调栈的理论基础,单调栈适合解决什么问题,单调栈的工作过程,大家可以先看这题讲解 [739. 每日温度](https://programmercarl.com/0739.每日温度.html)。 + +单调栈就是保持栈内元素有序。和[栈与队列:单调队列](https://programmercarl.com/0239.滑动窗口最大值.html)一样,需要我们自己维持顺序,没有现成的容器可以用。 + +通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。 + +而接雨水这道题目,我们正需要寻找一个元素,右边最大元素以及左边最大元素,来计算雨水面积。 + +#### 准备工作 + +那么本题使用单调栈有如下几个问题: + +1. 首先单调栈是按照行方向来计算雨水,如图: + +![42.接雨水2](https://file1.kamacoder.com/i/algo/20210223092629946.png) + +知道这一点,后面的就可以理解了。 + +2. 使用单调栈内元素的顺序 + +从大到小还是从小到大呢? + +从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。 + +因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。 + +如图: + +![42.接雨水4](https://file1.kamacoder.com/i/algo/2021022309321229.png) + +关于单调栈的顺序给大家一个总结: [739. 每日温度](https://programmercarl.com/0739.每日温度.html) 中求一个元素右边第一个更大元素,单调栈就是递增的,[84.柱状图中最大的矩形](https://programmercarl.com/0084.柱状图中最大的矩形.html)求一个元素右边第一个更小元素,单调栈就是递减的。 + +3. 遇到相同高度的柱子怎么办。 + +遇到相同的元素,更新栈内下标,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。 + +例如 5 5 1 3 这种情况。如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。 + +**因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度**。 + +如图所示: + +![42.接雨水5](https://file1.kamacoder.com/i/algo/20210223094619398.png) + +4. 栈里要保存什么数值 + +使用单调栈,也是通过 长 * 宽 来计算雨水面积的。 + +长就是通过柱子的高度来计算,宽是通过柱子之间的下标来计算, + +那么栈里有没有必要存一个pair类型的元素,保存柱子的高度和下标呢。 + +其实不用,栈里就存放下标就行,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下标对应的高度了。 + +所以栈的定义如下: + +``` +stack st; // 存着下标,计算的时候用下标对应的柱子高度 +``` + +明确了如上几点,我们再来看处理逻辑。 + +#### 单调栈处理逻辑 + +以下操作过程其实和 [739. 每日温度](https://programmercarl.com/0739.每日温度.html) 也是一样的,建议先做 [739. 每日温度](https://programmercarl.com/0739.每日温度.html)。 + +以下逻辑主要就是三种情况 + +* 情况一:当前遍历的元素(柱子)高度小于栈顶元素的高度 height[i] < height[st.top()] +* 情况二:当前遍历的元素(柱子)高度等于栈顶元素的高度 height[i] == height[st.top()] +* 情况三:当前遍历的元素(柱子)高度大于栈顶元素的高度 height[i] > height[st.top()] + +先将下标0的柱子加入到栈中,`st.push(0);`。 栈中存放我们遍历过的元素,所以先将下标0加进来。 + +然后开始从下标1开始遍历所有的柱子,`for (int i = 1; i < height.size(); i++)`。 + +如果当前遍历的元素(柱子)高度小于栈顶元素的高度,就把这个元素加入栈中,因为栈里本来就要保持从小到大的顺序(从栈头到栈底)。 + +代码如下: + +``` +if (height[i] < height[st.top()]) st.push(i); +``` + +如果当前遍历的元素(柱子)高度等于栈顶元素的高度,要跟更新栈顶元素,因为遇到相相同高度的柱子,需要使用最右边的柱子来计算宽度。 + +代码如下: + +``` +if (height[i] == height[st.top()]) { // 例如 5 5 1 7 这种情况 + st.pop(); + st.push(i); +} +``` + +如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了,如图所示: + +![42.接雨水4](https://file1.kamacoder.com/i/algo/2021022309321229-20230310123027977.png) + +取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,下标记为mid,对应的高度为height[mid](就是图中的高度1)。 + +此时的栈顶元素st.top(),就是凹槽的左边位置,下标为st.top(),对应的高度为height[st.top()](就是图中的高度2)。 + +当前遍历的元素i,就是凹槽右边的位置,下标为i,对应的高度为height[i](就是图中的高度3)。 + +此时大家应该可以发现其实就是**栈顶和栈顶的下一个元素以及要入栈的元素,三个元素来接水!** + +那么雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度,代码为:`int h = min(height[st.top()], height[i]) - height[mid];` + +雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度),代码为:`int w = i - st.top() - 1 ;` + +当前凹槽雨水的体积就是:`h * w`。 + +求当前凹槽雨水的体积代码如下: + +```CPP +while (!st.empty() && height[i] > height[st.top()]) { // 注意这里是while,持续跟新栈顶元素 + int mid = st.top(); + st.pop(); + if (!st.empty()) { + int h = min(height[st.top()], height[i]) - height[mid]; + int w = i - st.top() - 1; // 注意减一,只求中间宽度 + sum += h * w; + } +} +``` + +关键部分讲完了,整体代码如下: + +```CPP +class Solution { +public: + int trap(vector& height) { + if (height.size() <= 2) return 0; // 可以不加 + stack st; // 存着下标,计算的时候用下标对应的柱子高度 + st.push(0); + int sum = 0; + for (int i = 1; i < height.size(); i++) { + if (height[i] < height[st.top()]) { // 情况一 + st.push(i); + } if (height[i] == height[st.top()]) { // 情况二 + st.pop(); // 其实这一句可以不加,效果是一样的,但处理相同的情况的思路却变了。 + st.push(i); + } else { // 情况三 + while (!st.empty() && height[i] > height[st.top()]) { // 注意这里是while + int mid = st.top(); + st.pop(); + if (!st.empty()) { + int h = min(height[st.top()], height[i]) - height[mid]; + int w = i - st.top() - 1; // 注意减一,只求中间宽度 + sum += h * w; + } + } + st.push(i); + } + } + return sum; + } +}; +``` + +以上代码冗余了一些,但是思路是清晰的,下面我将代码精简一下,如下: + +```CPP +class Solution { +public: + int trap(vector& height) { + stack st; + st.push(0); + int sum = 0; + for (int i = 1; i < height.size(); i++) { + while (!st.empty() && height[i] > height[st.top()]) { + int mid = st.top(); + st.pop(); + if (!st.empty()) { + int h = min(height[st.top()], height[i]) - height[mid]; + int w = i - st.top() - 1; + sum += h * w; + } + } + st.push(i); + } + return sum; + } +}; +``` + +精简之后的代码,大家就看不出去三种情况的处理了,貌似好像只处理的情况三,其实是把情况一和情况二融合了。 这样的代码不太利于理解。 + + +## 其他语言版本 + +### Java: + +暴力解法: + +```java +class Solution { + public int trap(int[] height) { + int sum = 0; + for (int i = 0; i < height.length; i++) { + // 第一个柱子和最后一个柱子不接雨水 + if (i==0 || i== height.length - 1) continue; + + int rHeight = height[i]; // 记录右边柱子的最高高度 + int lHeight = height[i]; // 记录左边柱子的最高高度 + for (int r = i+1; r < height.length; r++) { + if (height[r] > rHeight) rHeight = height[r]; + } + for (int l = i-1; l >= 0; l--) { + if(height[l] > lHeight) lHeight = height[l]; + } + int h = Math.min(lHeight, rHeight) - height[i]; + if (h > 0) sum += h; + } + return sum; + + } +} +``` + +双指针: + +```java +class Solution { + public int trap(int[] height) { + int length = height.length; + if (length <= 2) return 0; + int[] maxLeft = new int[length]; + int[] maxRight = new int[length]; + + // 记录每个柱子左边柱子最大高度 + maxLeft[0] = height[0]; + for (int i = 1; i< length; i++) maxLeft[i] = Math.max(height[i], maxLeft[i-1]); + + // 记录每个柱子右边柱子最大高度 + maxRight[length - 1] = height[length - 1]; + for(int i = length - 2; i >= 0; i--) maxRight[i] = Math.max(height[i], maxRight[i+1]); + + // 求和 + int sum = 0; + for (int i = 0; i < length; i++) { + int count = Math.min(maxLeft[i], maxRight[i]) - height[i]; + if (count > 0) sum += count; + } + return sum; + } +} +``` + +双指针优化 +```java +class Solution { + public int trap(int[] height) { + if (height.length <= 2) { + return 0; + } + // 从两边向中间寻找最值 + int maxLeft = height[0], maxRight = height[height.length - 1]; + int l = 1, r = height.length - 2; + int res = 0; + while (l <= r) { + // 不确定上一轮是左边移动还是右边移动,所以两边都需更新最值 + maxLeft = Math.max(maxLeft, height[l]); + maxRight = Math.max(maxRight, height[r]); + // 最值较小的一边所能装的水量已定,所以移动较小的一边。 + if (maxLeft < maxRight) { + res += maxLeft - height[l ++]; + } else { + res += maxRight - height[r --]; + } + } + return res; + } +} +``` + +单调栈法 + +```java +class Solution { + public int trap(int[] height){ + int size = height.length; + + if (size <= 2) return 0; + + // in the stack, we push the index of array + // using height[] to access the real height + Stack stack = new Stack(); + stack.push(0); + + int sum = 0; + for (int index = 1; index < size; index++){ + int stackTop = stack.peek(); + if (height[index] < height[stackTop]){ + stack.push(index); + }else if (height[index] == height[stackTop]){ + // 因为相等的相邻墙,左边一个是不可能存放雨水的,所以pop左边的index, push当前的index + stack.pop(); + stack.push(index); + }else{ + //pop up all lower value + int heightAtIdx = height[index]; + while (!stack.isEmpty() && (heightAtIdx > height[stackTop])){ + int mid = stack.pop(); + + if (!stack.isEmpty()){ + int left = stack.peek(); + + int h = Math.min(height[left], height[index]) - height[mid]; + int w = index - left - 1; + int hold = h * w; + if (hold > 0) sum += hold; + stackTop = stack.peek(); + } + } + stack.push(index); + } + } + + return sum; + } +} +``` + +### Python: + +暴力解法: + +```Python +class Solution: + def trap(self, height: List[int]) -> int: + res = 0 + for i in range(len(height)): + if i == 0 or i == len(height)-1: continue + lHight = height[i-1] + rHight = height[i+1] + for j in range(i-1): + if height[j] > lHight: + lHight = height[j] + for k in range(i+2,len(height)): + if height[k] > rHight: + rHight = height[k] + res1 = min(lHight,rHight) - height[i] + if res1 > 0: + res += res1 + return res +``` + +双指针: + +```python +class Solution: + def trap(self, height: List[int]) -> int: + leftheight, rightheight = [0]*len(height), [0]*len(height) + + leftheight[0]=height[0] + for i in range(1,len(height)): + leftheight[i]=max(leftheight[i-1],height[i]) + rightheight[-1]=height[-1] + for i in range(len(height)-2,-1,-1): + rightheight[i]=max(rightheight[i+1],height[i]) + + result = 0 + for i in range(0,len(height)): + summ = min(leftheight[i],rightheight[i])-height[i] + result += summ + return result +``` + +单调栈 + +```Python +class Solution: + def trap(self, height: List[int]) -> int: + # 单调栈 + ''' + 单调栈是按照 行 的方向来计算雨水 + 从栈顶到栈底的顺序:从小到大 + 通过三个元素来接水:栈顶,栈顶的下一个元素,以及即将入栈的元素 + 雨水高度是 min(凹槽左边高度, 凹槽右边高度) - 凹槽底部高度 + 雨水的宽度是 凹槽右边的下标 - 凹槽左边的下标 - 1(因为只求中间宽度) + ''' + # stack储存index,用于计算对应的柱子高度 + stack = [0] + result = 0 + for i in range(1, len(height)): + # 情况一 + if height[i] < height[stack[-1]]: + stack.append(i) + + # 情况二 + # 当当前柱子高度和栈顶一致时,左边的一个是不可能存放雨水的,所以保留右侧新柱子 + # 需要使用最右边的柱子来计算宽度 + elif height[i] == height[stack[-1]]: + stack.pop() + stack.append(i) + + # 情况三 + else: + # 抛出所有较低的柱子 + while stack and height[i] > height[stack[-1]]: + # 栈顶就是中间的柱子:储水槽,就是凹槽的地步 + mid_height = height[stack[-1]] + stack.pop() + if stack: + right_height = height[i] + left_height = height[stack[-1]] + # 两侧的较矮一方的高度 - 凹槽底部高度 + h = min(right_height, left_height) - mid_height + # 凹槽右侧下标 - 凹槽左侧下标 - 1: 只求中间宽度 + w = i - stack[-1] - 1 + # 体积:高乘宽 + result += h * w + stack.append(i) + return result + +# 单调栈压缩版 +class Solution: + def trap(self, height: List[int]) -> int: + stack = [0] + result = 0 + for i in range(1, len(height)): + while stack and height[i] > height[stack[-1]]: + mid_height = stack.pop() + if stack: + # 雨水高度是 min(凹槽左侧高度, 凹槽右侧高度) - 凹槽底部高度 + h = min(height[stack[-1]], height[i]) - height[mid_height] + # 雨水宽度是 凹槽右侧的下标 - 凹槽左侧的下标 - 1 + w = i - stack[-1] - 1 + # 累计总雨水体积 + result += h * w + stack.append(i) + return result + +``` + +### Go: + +```go +func trap(height []int) int { + var left, right, leftMax, rightMax, res int + right = len(height) - 1 + for left < right { + if height[left] < height[right] { + if height[left] >= leftMax { + leftMax = height[left] // 设置左边最高柱子 + } else { + res += leftMax - height[left] // //右边必定有柱子挡水,所以遇到所有值小于等于leftMax的,全部加入水池中 + } + left++ + } else { + if height[right] > rightMax { + rightMax = height[right] // //设置右边最高柱子 + } else { + res += rightMax - height[right] // //左边必定有柱子挡水,所以,遇到所有值小于等于rightMax的,全部加入水池 + } + right-- + } + } + return res +} +``` + +双指针解法: + +```go +func trap(height []int) int { + sum:=0 + n:=len(height) + lh:=make([]int,n) + rh:=make([]int,n) + lh[0]=height[0] + rh[n-1]=height[n-1] + for i:=1;i=0;i--{ + rh[i]=max(rh[i+1],height[i]) + } + for i:=1;i0{ + sum+=h + } + } + return sum +} +func max(a,b int)int{ + if a>b{ + return a + } + return b +} +func min(a,b int)int{ + if a height[st[len(st)-1]] { + top := st[len(st)-1] + st = st[:len(st)-1] + if len(st) != 0 { + tmp := (min(height[i], height[st[len(st)-1]]) - height[top]) * (i - st[len(st)-1] - 1) + res += tmp + } + } + st = append(st, i) + } + } + return res +} + + +func min(x, y int) int { + if x >= y { + return y + } + return x +} +``` + +单调栈压缩版: + +```go +func trap(height []int) int { + stack := make([]int, 0) + res := 0 + + // 无需事先将第一个柱子的坐标入栈,因为它会在该for循环的最后入栈 + for i := 0; i < len(height); i ++ { + // 满足栈不为空并且当前柱子高度大于栈顶对应的柱子高度的情况时 + for len(stack) > 0 && height[stack[len(stack) - 1]] < height[i] { + // 获得凹槽高度 + mid := height[stack[len(stack) - 1]] + // 凹槽坐标出栈 + stack = stack[: len(stack) - 1] + + // 如果栈不为空则此时栈顶元素为左侧柱子坐标 + if len(stack) > 0 { + // 求得雨水高度 + h := min(height[i], height[stack[len(stack) - 1]]) - mid + // 求得雨水宽度 + w := i - stack[len(stack) - 1] - 1 + res += h * w + } + } + // 如果栈为空或者当前柱子高度小于等于栈顶对应的柱子高度时入栈 + stack = append(stack, i) + } + return res +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + +### JavaScript: + +```javascript +//暴力解法 +var trap = function(height) { + const len = height.length; + let sum = 0; + for(let i = 0; i < len; i++){ + // 第一个柱子和最后一个柱子不接雨水 + if(i == 0 || i == len - 1) continue; + let rHeight = height[i]; // 记录右边柱子的最高高度 + let lHeight = height[i]; // 记录左边柱子的最高高度 + for(let r = i + 1; r < len; r++){ + if(height[r] > rHeight) rHeight = height[r]; + } + for(let l = i - 1; l >= 0; l--){ + if(height[l] > lHeight) lHeight = height[l]; + } + let h = Math.min(lHeight, rHeight) - height[i]; + if(h > 0) sum += h; + } + return sum; +}; + +//双指针 +var trap = function(height) { + const len = height.length; + if(len <= 2) return 0; + const maxLeft = new Array(len).fill(0); + const maxRight = new Array(len).fill(0); + // 记录每个柱子左边柱子最大高度 + maxLeft[0] = height[0]; + for(let i = 1; i < len; i++){ + maxLeft[i] = Math.max(height[i], maxLeft[i - 1]); + } + // 记录每个柱子右边柱子最大高度 + maxRight[len - 1] = height[len - 1]; + for(let i = len - 2; i >= 0; i--){ + maxRight[i] = Math.max(height[i], maxRight[i + 1]); + } + // 求和 + let sum = 0; + for(let i = 0; i < len; i++){ + let count = Math.min(maxLeft[i], maxRight[i]) - height[i]; + if(count > 0) sum += count; + } + return sum; +}; + +//单调栈 js数组作为栈 +var trap = function(height) { + const len = height.length; + if(len <= 2) return 0; // 可以不加 + const st = [];// 存着下标,计算的时候用下标对应的柱子高度 + st.push(0); + let sum = 0; + for(let i = 1; i < len; i++){ + if(height[i] < height[st[st.length - 1]]){ // 情况一 + st.push(i); + } + if (height[i] == height[st[st.length - 1]]) { // 情况二 + st.pop(); // 其实这一句可以不加,效果是一样的,但处理相同的情况的思路却变了。 + st.push(i); + } else { // 情况三 + while (st.length !== 0 && height[i] > height[st[st.length - 1]]) { // 注意这里是while + let mid = st[st.length - 1]; + st.pop(); + if (st.length !== 0) { + let h = Math.min(height[st[st.length - 1]], height[i]) - height[mid]; + let w = i - st[st.length - 1] - 1; // 注意减一,只求中间宽度 + sum += h * w; + } + } + st.push(i); + } + } + return sum; +}; + +//单调栈 简洁版本 只处理情况三 +var trap = function(height) { + const len = height.length; + if(len <= 2) return 0; // 可以不加 + const st = [];// 存着下标,计算的时候用下标对应的柱子高度 + st.push(0); + let sum = 0; + for(let i = 1; i < len; i++){ // 只处理的情况三,其实是把情况一和情况二融合了 + while (st.length !== 0 && height[i] > height[st[st.length - 1]]) { // 注意这里是while + let mid = st[st.length - 1]; + st.pop(); + if (st.length !== 0) { + let h = Math.min(height[st[st.length - 1]], height[i]) - height[mid]; + let w = i - st[st.length - 1] - 1; // 注意减一,只求中间宽度 + sum += h * w; + } + } + st.push(i); + } + return sum; +}; +``` + +### TypeScript: + +暴力解法: + +```typescript +function trap(height: number[]): number { + const length: number = height.length; + let resVal: number = 0; + for (let i = 0; i < length; i++) { + let leftMaxHeight: number = height[i], + rightMaxHeight: number = height[i]; + let leftIndex: number = i - 1, + rightIndex: number = i + 1; + while (leftIndex >= 0) { + if (height[leftIndex] > leftMaxHeight) + leftMaxHeight = height[leftIndex]; + leftIndex--; + } + while (rightIndex < length) { + if (height[rightIndex] > rightMaxHeight) + rightMaxHeight = height[rightIndex]; + rightIndex++; + } + resVal += Math.min(leftMaxHeight, rightMaxHeight) - height[i]; + } + return resVal; +}; +``` + +双指针: + +```typescript +function trap(height: number[]): number { + const length: number = height.length; + const leftMaxHeightDp: number[] = [], + rightMaxHeightDp: number[] = []; + leftMaxHeightDp[0] = height[0]; + rightMaxHeightDp[length - 1] = height[length - 1]; + for (let i = 1; i < length; i++) { + leftMaxHeightDp[i] = Math.max(height[i], leftMaxHeightDp[i - 1]); + } + for (let i = length - 2; i >= 0; i--) { + rightMaxHeightDp[i] = Math.max(height[i], rightMaxHeightDp[i + 1]); + } + let resVal: number = 0; + for (let i = 0; i < length; i++) { + resVal += Math.min(leftMaxHeightDp[i], rightMaxHeightDp[i]) - height[i]; + } + return resVal; +}; +``` + +单调栈: + +```typescript +function trap(height: number[]): number { + const length: number = height.length; + const stack: number[] = []; + stack.push(0); + let resVal: number = 0; + for (let i = 1; i < length; i++) { + let top = stack[stack.length - 1]; + if (height[top] > height[i]) { + stack.push(i); + } else if (height[top] === height[i]) { + stack.pop(); + stack.push(i); + } else { + while (stack.length > 0 && height[top] < height[i]) { + let mid = stack.pop(); + if (stack.length > 0) { + let left = stack[stack.length - 1]; + let h = Math.min(height[left], height[i]) - height[mid]; + let w = i - left - 1; + resVal += h * w; + top = stack[stack.length - 1]; + } + } + stack.push(i); + } + } + return resVal; +}; +``` + +### C: + +一种更简便的双指针方法: + +之前的双指针方法的原理是固定“底”的位置,往两边找比它高的“壁”,循环若干次求和。 + +我们逆向思维,把“壁”用两个初始位置在数组首末位置的指针表示,“壁”往中间推,同样可以让每个“底”都能找到最高的“壁” + +本质上就是改变了运算方向,从而减少了重复运算 + +代码如下: + +```C +int trap(int* height, int heightSize) { + int ans = 0; + int left = 0, right = heightSize - 1; //初始化两个指针到左右两边 + int leftMax = 0, rightMax = 0; //这两个值用来记录左右的“壁”的最高值 + while (left < right) { //两个指针重合就结束 + leftMax = fmax(leftMax, height[left]); + rightMax = fmax(rightMax, height[right]); + if (leftMax < rightMax) { + ans += leftMax - height[left]; //这里考虑的是下标为left的“底”能装多少水 + ++left;//指针的移动次序是这个方法的关键 + //这里左指针右移是因为左“墙”较矮,左边这一片实际情况下的盛水量是受制于这个矮的左“墙”的 + //而较高的右边在实际情况下的限制条件可能不是当前的左“墙”,比如限制条件可能是右“墙”,就能装更高的水, + } + else { + ans += rightMax - height[right]; //同理,考虑下标为right的元素 + --right; + } + } + return ans; +} +``` + +* 时间复杂度 O(n) +* 空间复杂度 O(1) + +### Rust: + +双指针 + +```rust +impl Solution { + pub fn trap(height: Vec) -> i32 { + let n = height.len(); + let mut max_left = vec![0; height.len()]; + let mut max_right = vec![0; height.len()]; + max_left.iter_mut().zip(max_right.iter_mut().rev()).enumerate().fold((0, 0), |(lm, rm), (idx, (x, y))| { + let lmax = lm.max(height[idx]); + let rmax = rm.max(height[n - 1 - idx]); + *x = lmax; *y = rmax; + (lmax, rmax) + }); + height.iter().enumerate().fold(0, |acc, (idx, x)| { + let h = max_left[idx].min(max_right[idx]); + if h > 0 { h - x + acc } else { acc } + }) + } +} +``` + +单调栈 + +```rust +impl Solution { + pub fn trap(height: Vec) -> i32 { + let mut stack = vec![]; + let mut ans = 0; + for (right_pos, &right_h) in height.iter().enumerate() { + while !stack.is_empty() && height[*stack.last().unwrap()] <= right_h { + let mid_pos = stack.pop().unwrap(); + if !stack.is_empty() { + let left_pos = *stack.last().unwrap(); + let left_h = height[left_pos]; + let top = std::cmp::min(left_h, right_h); + if top > height[mid_pos] { + ans += (top - height[mid_pos]) * (right_pos - left_pos - 1) as i32; + } + } + } + stack.push(right_pos); + } + ans + } +} +``` + +Rust + +双指针 + +```rust +impl Solution { + pub fn trap(height: Vec) -> i32 { + let n = height.len(); + let mut max_left = vec![0; height.len()]; + let mut max_right = vec![0; height.len()]; + max_left.iter_mut().zip(max_right.iter_mut().rev()).enumerate().fold((0, 0), |(lm, rm), (idx, (x, y))| { + let lmax = lm.max(height[idx]); + let rmax = rm.max(height[n - 1 - idx]); + *x = lmax; *y = rmax; + (lmax, rmax) + }); + height.iter().enumerate().fold(0, |acc, (idx, x)| { + let h = max_left[idx].min(max_right[idx]); + if h > 0 { h - x + acc } else { acc } + }) + } +} +``` + +单调栈 + +```rust +impl Solution { + pub fn trap(height: Vec) -> i32 { + let mut stack = vec![]; + let mut ans = 0; + for (right_pos, &right_h) in height.iter().enumerate() { + while !stack.is_empty() && height[*stack.last().unwrap()] <= right_h { + let mid_pos = stack.pop().unwrap(); + if !stack.is_empty() { + let left_pos = *stack.last().unwrap(); + let left_h = height[left_pos]; + let top = std::cmp::min(left_h, right_h); + if top > height[mid_pos] { + ans += (top - height[mid_pos]) * (right_pos - left_pos - 1) as i32; + } + } + } + stack.push(right_pos); + } + ans + } +} +``` + diff --git "a/problems/0045.\350\267\263\350\267\203\346\270\270\346\210\217II.md" "b/problems/0045.\350\267\263\350\267\203\346\270\270\346\210\217II.md" new file mode 100755 index 0000000000..c20cdc65e6 --- /dev/null +++ "b/problems/0045.\350\267\263\350\267\203\346\270\270\346\210\217II.md" @@ -0,0 +1,542 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +> 相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不少,做好心理准备! + +# 45.跳跃游戏 II + +[力扣题目链接](https://leetcode.cn/problems/jump-game-ii/) + +给定一个非负整数数组,你最初位于数组的第一个位置。 + +数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +你的目标是使用最少的跳跃次数到达数组的最后一个位置。 + +示例: + +- 输入: [2,3,1,1,4] +- 输出: 2 +- 解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳  1  步,然后跳  3  步到达数组的最后一个位置。 + +说明: +假设你总是可以到达数组的最后一个位置。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,最少跳几步还得看覆盖范围 | LeetCode: 45.跳跃游戏 II](https://www.bilibili.com/video/BV1Y24y1r7XZ),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +本题相对于[55.跳跃游戏](https://programmercarl.com/0055.跳跃游戏.html)还是难了不少。 + +但思路是相似的,还是要看最大覆盖范围。 + +本题要计算最少步数,那么就要想清楚什么时候步数才一定要加一呢? + +贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最少步数。 + +思路虽然是这样,但在写代码的时候还不能真的能跳多远就跳多远,那样就不知道下一步最远能跳到哪里了。 + +**所以真正解题的时候,要从覆盖范围出发,不管怎么跳,覆盖范围内一定是可以跳到的,以最小的步数增加覆盖范围,覆盖范围一旦覆盖了终点,得到的就是最少步数!** + +**这里需要统计两个覆盖范围,当前这一步的最大覆盖和下一步最大覆盖**。 + +如果移动下标达到了当前这一步的最大覆盖最远距离了,还没有到终点的话,那么就必须再走一步来增加覆盖范围,直到覆盖范围覆盖了终点。 + +如图: + +![45.跳跃游戏II](https://file1.kamacoder.com/i/algo/20201201232309103.png) + +**图中覆盖范围的意义在于,只要红色的区域,最多两步一定可以到!(不用管具体怎么跳,反正一定可以跳到)** + +### 方法一 + +从图中可以看出来,就是移动下标达到了当前覆盖的最远距离下标时,步数就要加一,来增加覆盖距离。最后的步数就是最少步数。 + +这里还是有个特殊情况需要考虑,当移动下标达到了当前覆盖的最远距离下标时 + +- 如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。 +- 如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了。 + +C++代码如下:(详细注释) + +```CPP +// 版本一 +class Solution { +public: + int jump(vector& nums) { + if (nums.size() == 1) return 0; + int curDistance = 0; // 当前覆盖最远距离下标 + int ans = 0; // 记录走的最大步数 + int nextDistance = 0; // 下一步覆盖最远距离下标 + for (int i = 0; i < nums.size(); i++) { + nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖最远距离下标 + if (i == curDistance) { // 遇到当前覆盖最远距离下标 + ans++; // 需要走下一步 + curDistance = nextDistance; // 更新当前覆盖最远距离下标(相当于加油了) + if (nextDistance >= nums.size() - 1) break; // 当前覆盖最远距到达集合终点,不用做ans++操作了,直接结束 + } + } + return ans; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + +### 方法二 + +依然是贪心,思路和方法一差不多,代码可以简洁一些。 + +**针对于方法一的特殊情况,可以统一处理**,即:移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不考虑是不是终点的情况。 + +想要达到这样的效果,只要让移动下标,最大只能移动到 nums.size - 2 的地方就可以了。 + +因为当移动下标指向 nums.size - 2 时: + +- 如果移动下标等于当前覆盖最大距离下标, 需要再走一步(即 ans++),因为最后一步一定是可以到的终点。(题目假设总是可以到达数组的最后一个位置),如图: + ![45.跳跃游戏II2](https://file1.kamacoder.com/i/algo/20201201232445286.png) + +- 如果移动下标不等于当前覆盖最大距离下标,说明当前覆盖最远距离就可以直接达到终点了,不需要再走一步。如图: + +![45.跳跃游戏II1](https://file1.kamacoder.com/i/algo/20201201232338693.png) + +代码如下: + +```CPP +// 版本二 +class Solution { +public: + int jump(vector& nums) { + int curDistance = 0; // 当前覆盖的最远距离下标 + int ans = 0; // 记录走的最大步数 + int nextDistance = 0; // 下一步覆盖的最远距离下标 + for (int i = 0; i < nums.size() - 1; i++) { // 注意这里是小于nums.size() - 1,这是关键所在 + nextDistance = max(nums[i] + i, nextDistance); // 更新下一步覆盖的最远距离下标 + if (i == curDistance) { // 遇到当前覆盖的最远距离下标 + curDistance = nextDistance; // 更新当前覆盖的最远距离下标 + ans++; + } + } + return ans; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + + +可以看出版本二的代码相对于版本一简化了不少! + +**其精髓在于控制移动下标 i 只移动到 nums.size() - 2 的位置**,所以移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不用考虑别的了。 + +## 总结 + +相信大家可以发现,这道题目相当于[55.跳跃游戏](https://programmercarl.com/0055.跳跃游戏.html)难了不止一点。 + +但代码又十分简单,贪心就是这么巧妙。 + +理解本题的关键在于:**以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点**,这个范围内最少步数一定可以跳到,不用管具体是怎么跳的,不纠结于一步究竟跳一个单位还是两个单位。 + +## 其他语言版本 + +### Java + +```Java +// 版本一 +class Solution { + public int jump(int[] nums) { + if (nums == null || nums.length == 0 || nums.length == 1) { + return 0; + } + //记录跳跃的次数 + int count=0; + //当前的覆盖最大区域 + int curDistance = 0; + //最大的覆盖区域 + int maxDistance = 0; + for (int i = 0; i < nums.length; i++) { + //在可覆盖区域内更新最大的覆盖区域 + maxDistance = Math.max(maxDistance,i+nums[i]); + //说明当前一步,再跳一步就到达了末尾 + if (maxDistance>=nums.length-1){ + count++; + break; + } + //走到当前覆盖的最大区域时,更新下一步可达的最大区域 + if (i==curDistance){ + curDistance = maxDistance; + count++; + } + } + return count; + } +} +``` + +```java +// 版本二 +class Solution { + public int jump(int[] nums) { + int result = 0; + // 当前覆盖的最远距离下标 + int end = 0; + // 下一步覆盖的最远距离下标 + int temp = 0; + for (int i = 0; i <= end && end < nums.length - 1; ++i) { + temp = Math.max(temp, i + nums[i]); + // 可达位置的改变次数就是跳跃次数 + if (i == end) { + end = temp; + result++; + } + } + return result; + } +} +``` + +### Python +贪心(版本一) +```python +class Solution: + def jump(self, nums): + if len(nums) == 1: + return 0 + + cur_distance = 0 # 当前覆盖最远距离下标 + ans = 0 # 记录走的最大步数 + next_distance = 0 # 下一步覆盖最远距离下标 + + for i in range(len(nums)): + next_distance = max(nums[i] + i, next_distance) # 更新下一步覆盖最远距离下标 + if i == cur_distance: # 遇到当前覆盖最远距离下标 + ans += 1 # 需要走下一步 + cur_distance = next_distance # 更新当前覆盖最远距离下标(相当于加油了) + if next_distance >= len(nums) - 1: # 当前覆盖最远距离达到数组末尾,不用再做ans++操作,直接结束 + break + + return ans + +``` +贪心(版本二) + +```python +class Solution: + def jump(self, nums): + cur_distance = 0 # 当前覆盖的最远距离下标 + ans = 0 # 记录走的最大步数 + next_distance = 0 # 下一步覆盖的最远距离下标 + + for i in range(len(nums) - 1): # 注意这里是小于len(nums) - 1,这是关键所在 + next_distance = max(nums[i] + i, next_distance) # 更新下一步覆盖的最远距离下标 + if i == cur_distance: # 遇到当前覆盖的最远距离下标 + cur_distance = next_distance # 更新当前覆盖的最远距离下标 + ans += 1 + + return ans + +``` +贪心(版本三) 类似‘55-跳跃游戏’写法 + +```python +class Solution: + def jump(self, nums) -> int: + if len(nums)==1: # 如果数组只有一个元素,不需要跳跃,步数为0 + return 0 + + i = 0 # 当前位置 + count = 0 # 步数计数器 + cover = 0 # 当前能够覆盖的最远距离 + + while i <= cover: # 当前位置小于等于当前能够覆盖的最远距离时循环 + for i in range(i, cover+1): # 遍历从当前位置到当前能够覆盖的最远距离之间的所有位置 + cover = max(nums[i]+i, cover) # 更新当前能够覆盖的最远距离 + if cover >= len(nums)-1: # 如果当前能够覆盖的最远距离达到或超过数组的最后一个位置,直接返回步数+1 + return count+1 + count += 1 # 每一轮遍历结束后,步数+1 + + +``` +动态规划 +```python +class Solution: + def jump(self, nums: List[int]) -> int: + result = [10**4+1] * len(nums) # 初始化结果数组,初始值为一个较大的数 + result[0] = 0 # 起始位置的步数为0 + + for i in range(len(nums)): # 遍历数组 + for j in range(nums[i] + 1): # 在当前位置能够跳跃的范围内遍历 + if i + j < len(nums): # 确保下一跳的位置不超过数组范围 + result[i + j] = min(result[i + j], result[i] + 1) # 更新到达下一跳位置的最少步数 + + return result[-1] # 返回到达最后一个位置的最少步数 + + +``` + +### Go + + +```go +/** + * @date: 2024 Jan 06 + * @time: 13:44 + * @author: Chris +**/ +// 贪心算法优化版 + +// 记录步骤规则:每超过上一次可达最大范围,需要跳跃一次,次数+1 +// 记录位置:i == lastDistance + 1 +func jump(nums []int) int { + // 根据题目规则,初始位置为nums[0] + lastDistance := 0 // 上一次覆盖范围 + curDistance := 0 // 当前覆盖范围(可达最大范围) + minStep := 0 // 记录最少跳跃次数 + + for i := 0; i < len(nums); i++ { + if i == lastDistance+1 { // 在上一次可达范围+1的位置,记录步骤 + minStep++ // 跳跃次数+1 + lastDistance = curDistance // 记录时才可以更新 + } + curDistance = max(nums[i]+i, curDistance) // 更新当前可达的最大范围 + } + return minStep +} +``` + +```go +// 贪心版本一 +func jump(nums []int) int { + n := len(nums) + if n == 1 { + return 0 + } + cur, next := 0, 0 + step := 0 + for i := 0; i < n; i++ { + next = max(nums[i]+i, next) + if i == cur { + if cur != n-1 { + step++ + cur = next + if cur >= n-1 { + return step + } + } else { + return step + } + } + } + return step +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```go +// 贪心版本二 +func jump(nums []int) int { + n := len(nums) + if n == 1 { + return 0 + } + cur, next := 0, 0 + step := 0 + for i := 0; i < n-1; i++ { + next = max(nums[i]+i, next) + if i == cur { + cur = next + step++ + } + } + return step +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +### JavaScript + +```Javascript +var jump = function(nums) { + let curIndex = 0 + let nextIndex = 0 + let steps = 0 + for(let i = 0; i < nums.length - 1; i++) { + nextIndex = Math.max(nums[i] + i, nextIndex) + if(i === curIndex) { + curIndex = nextIndex + steps++ + } + } + + return steps +}; +``` + +### TypeScript + +```typescript +function jump(nums: number[]): number { + const length: number = nums.length; + let curFarthestIndex: number = 0, + nextFarthestIndex: number = 0; + let curIndex: number = 0; + let stepNum: number = 0; + while (curIndex < length - 1) { + nextFarthestIndex = Math.max(nextFarthestIndex, curIndex + nums[curIndex]); + if (curIndex === curFarthestIndex) { + curFarthestIndex = nextFarthestIndex; + stepNum++; + } + curIndex++; + } + return stepNum; +} +``` + +### Scala + +```scala +object Solution { + def jump(nums: Array[Int]): Int = { + if (nums.length == 0) return 0 + var result = 0 // 记录走的最大步数 + var curDistance = 0 // 当前覆盖最远距离下标 + var nextDistance = 0 // 下一步覆盖最远距离下标 + for (i <- nums.indices) { + nextDistance = math.max(nums(i) + i, nextDistance) // 更新下一步覆盖最远距离下标 + if (i == curDistance) { + if (curDistance != nums.length - 1) { + result += 1 + curDistance = nextDistance + if (nextDistance >= nums.length - 1) return result + } else { + return result + } + } + } + result + } +} +``` + +### Rust + +```Rust +//版本一 +impl Solution { + pub fn jump(nums: Vec) -> i32 { + if nums.len() == 1 { + return 0; + } + let mut cur_distance = 0; + let mut ans = 0; + let mut next_distance = 0; + for (i, &n) in nums.iter().enumerate().take(nums.len() - 1) { + next_distance = (n as usize + i).max(next_distance); + if i == cur_distance { + if cur_distance < nums.len() - 1 { + ans += 1; + cur_distance = next_distance; + if next_distance >= nums.len() - 1 { + break; + }; + } else { + break; + } + } + } + ans + } +} +``` + +```Rust +//版本二 +impl Solution { + pub fn jump(nums: Vec) -> i32 { + if nums.len() == 1 { + return 0; + } + let mut cur_distance = 0; + let mut ans = 0; + let mut next_distance = 0; + for (i, &n) in nums.iter().enumerate().take(nums.len() - 1) { + next_distance = (n as usize + i).max(next_distance); + if i == cur_distance { + cur_distance = next_distance; + ans += 1; + } + } + ans + } +} +``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int jump(int* nums, int numsSize) { + if(numsSize == 1){ + return 0; + } + int count = 0; + // 记录当前能走的最远距离 + int curDistance = 0; + // 记录下一步能走的最远距离 + int nextDistance = 0; + for(int i = 0; i < numsSize; i++){ + nextDistance = max(i + nums[i], nextDistance); + // 下标到了当前的最大距离 + if(i == nextDistance){ + count++; + curDistance = nextDistance; + } + } + return count; +} +``` + +### C# + +```csharp +// 版本二 +public class Solution +{ + public int Jump(int[] nums) + { + int cur = 0, next = 0, step = 0; + for (int i = 0; i < nums.Length - 1; i++) + { + next = Math.Max(next, i + nums[i]); + if (i == cur) + { + cur = next; + step++; + } + } + return step; + } +} +``` + + diff --git "a/problems/0046.\345\205\250\346\216\222\345\210\227.md" "b/problems/0046.\345\205\250\346\216\222\345\210\227.md" new file mode 100755 index 0000000000..356f51b5a8 --- /dev/null +++ "b/problems/0046.\345\205\250\346\216\222\345\210\227.md" @@ -0,0 +1,522 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 46.全排列 + +[力扣题目链接](https://leetcode.cn/problems/permutations/) + +给定一个 没有重复 数字的序列,返回其所有可能的全排列。 + +示例: +* 输入: [1,2,3] +* 输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[组合与排列的区别,回溯算法求解的时候,有何不同?| LeetCode:46.全排列](https://www.bilibili.com/video/BV19v4y1S79W/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + + +此时我们已经学习了[77.组合问题](https://programmercarl.com/0077.组合.html)、 [131.分割回文串](https://programmercarl.com/0131.分割回文串.html)和[78.子集问题](https://programmercarl.com/0078.子集.html),接下来看一看排列问题。 + +相信这个排列问题就算是让你用for循环暴力把结果搜索出来,这个暴力也不是很好写。 + +所以正如我们在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)所讲的为什么回溯法是暴力搜索,效率这么低,还要用它? + +**因为一些问题能暴力搜出来就已经很不错了!** + +我以[1,2,3]为例,抽象成树形结构如下: + + +![全排列](https://file1.kamacoder.com/i/algo/20240803180318.png) + +### 回溯三部曲 + +* 递归函数参数 + +**首先排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方**。 + +可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。 + +但排列问题需要一个used数组,标记已经选择的元素,如图橘黄色部分所示: + +![全排列](https://file1.kamacoder.com/i/algo/20240803180318.png) + +代码如下: + +```cpp +vector> result; +vector path; +void backtracking (vector& nums, vector& used) +``` + +* 递归终止条件 + +![全排列](https://file1.kamacoder.com/i/algo/20240803180318.png) + +可以看出叶子节点,就是收割结果的地方。 + +那么什么时候,算是到达叶子节点呢? + +当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。 + +代码如下: + +```cpp +// 此时说明找到了一组 +if (path.size() == nums.size()) { + result.push_back(path); + return; +} +``` + +* 单层搜索的逻辑 + +这里和[77.组合问题](https://programmercarl.com/0077.组合.html)、[131.切割问题](https://programmercarl.com/0131.分割回文串.html)和[78.子集问题](https://programmercarl.com/0078.子集.html)最大的不同就是for循环里不用startIndex了。 + +因为排列问题,每次都要从头开始搜索,例如元素1在[1,2]中已经使用过了,但是在[2,1]中还要再使用一次1。 + +**而used数组,其实就是记录此时path里都有哪些元素使用了,一个排列里一个元素只能使用一次**。 + +代码如下: + +```cpp +for (int i = 0; i < nums.size(); i++) { + if (used[i] == true) continue; // path里已经收录的元素,直接跳过 + used[i] = true; + path.push_back(nums[i]); + backtracking(nums, used); + path.pop_back(); + used[i] = false; +} +``` + +整体C++代码如下: + +```CPP +class Solution { +public: + vector> result; + vector path; + void backtracking (vector& nums, vector& used) { + // 此时说明找到了一组 + if (path.size() == nums.size()) { + result.push_back(path); + return; + } + for (int i = 0; i < nums.size(); i++) { + if (used[i] == true) continue; // path里已经收录的元素,直接跳过 + used[i] = true; + path.push_back(nums[i]); + backtracking(nums, used); + path.pop_back(); + used[i] = false; + } + } + vector> permute(vector& nums) { + result.clear(); + path.clear(); + vector used(nums.size(), false); + backtracking(nums, used); + return result; + } +}; +``` +* 时间复杂度: O(n!) +* 空间复杂度: O(n) + +## 总结 + +大家此时可以感受出排列问题的不同: + +* 每层都是从0开始搜索而不是startIndex +* 需要used数组记录path里都放了哪些元素了 + +排列问题是回溯算法解决的经典题目,大家可以好好体会体会。 + + +## 其他语言版本 + +### Java + +```java +class Solution { + + List> result = new ArrayList<>();// 存放符合条件结果的集合 + LinkedList path = new LinkedList<>();// 用来存放符合条件结果 + boolean[] used; + public List> permute(int[] nums) { + if (nums.length == 0){ + return result; + } + used = new boolean[nums.length]; + permuteHelper(nums); + return result; + } + + private void permuteHelper(int[] nums){ + if (path.size() == nums.length){ + result.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++){ + if (used[i]){ + continue; + } + used[i] = true; + path.add(nums[i]); + permuteHelper(nums); + path.removeLast(); + used[i] = false; + } + } +} +``` + +```java +// 解法2:通过判断path中是否存在数字,排除已经选择的数字 +class Solution { + List> result = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + public List> permute(int[] nums) { + if (nums.length == 0) return result; + backtrack(nums, path); + return result; + } + public void backtrack(int[] nums, LinkedList path) { + if (path.size() == nums.length) { + result.add(new ArrayList<>(path)); + return; + } + for (int i =0; i < nums.length; i++) { + // 如果path中已有,则跳过 + if (path.contains(nums[i])) { + continue; + } + path.add(nums[i]); + backtrack(nums, path); + path.removeLast(); + } + } +} +``` + +### Python +回溯 使用used +```python +class Solution: + def permute(self, nums): + result = [] + self.backtracking(nums, [], [False] * len(nums), result) + return result + + def backtracking(self, nums, path, used, result): + if len(path) == len(nums): + result.append(path[:]) + return + for i in range(len(nums)): + if used[i]: + continue + used[i] = True + path.append(nums[i]) + self.backtracking(nums, path, used, result) + path.pop() + used[i] = False + +``` + +### Go +```Go +var ( + res [][]int + path []int + st []bool // state的缩写 +) +func permute(nums []int) [][]int { + res, path = make([][]int, 0), make([]int, 0, len(nums)) + st = make([]bool, len(nums)) + dfs(nums, 0) + return res +} + +func dfs(nums []int, cur int) { + if cur == len(nums) { + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + } + for i := 0; i < len(nums); i++ { + if !st[i] { + path = append(path, nums[i]) + st[i] = true + dfs(nums, cur + 1) + st[i] = false + path = path[:len(path)-1] + } + } +} +``` + +### JavaScript + +```js + +/** + * @param {number[]} nums + * @return {number[][]} + */ +var permute = function(nums) { + const res = [], path = []; + backtracking(nums, nums.length, []); + return res; + + function backtracking(n, k, used) { + if(path.length === k) { + res.push(Array.from(path)); + return; + } + for (let i = 0; i < k; i++ ) { + if(used[i]) continue; + path.push(n[i]); + used[i] = true; // 同支 + backtracking(n, k, used); + path.pop(); + used[i] = false; + } + } +}; + +``` + +### TypeScript + +```typescript +function permute(nums: number[]): number[][] { + const resArr: number[][] = []; + const helperSet: Set = new Set(); + backTracking(nums, []); + return resArr; + function backTracking(nums: number[], route: number[]): void { + if (route.length === nums.length) { + resArr.push([...route]); + return; + } + let tempVal: number; + for (let i = 0, length = nums.length; i < length; i++) { + tempVal = nums[i]; + if (!helperSet.has(tempVal)) { + route.push(tempVal); + helperSet.add(tempVal); + backTracking(nums, route); + route.pop(); + helperSet.delete(tempVal); + } + } + } +}; +``` + +### Rust + +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, nums: &Vec, used: &mut Vec) { + let len = nums.len(); + if path.len() == len { + result.push(path.clone()); + return; + } + for i in 0..len { + if used[i] == true { continue; } + used[i] = true; + path.push(nums[i]); + Self::backtracking(result, path, nums, used); + path.pop(); + used[i] = false; + } + } + + pub fn permute(nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + let mut used = vec![false; nums.len()]; + Self::backtracking(&mut result, &mut path, &nums, &mut used); + result + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; + +//将used中元素都设置为0 +void initialize(int* used, int usedLength) { + int i; + for(i = 0; i < usedLength; i++) { + used[i] = 0; + } +} + +//将path中元素拷贝到ans中 +void copy() { + int* tempPath = (int*)malloc(sizeof(int) * pathTop); + int i; + for(i = 0; i < pathTop; i++) { + tempPath[i] = path[i]; + } + ans[ansTop++] = tempPath; +} + +void backTracking(int* nums, int numsSize, int* used) { + //若path中元素个数等于nums元素个数,将nums放入ans中 + if(pathTop == numsSize) { + copy(); + return; + } + int i; + for(i = 0; i < numsSize; i++) { + //若当前下标中元素已使用过,则跳过当前元素 + if(used[i]) + continue; + used[i] = 1; + path[pathTop++] = nums[i]; + backTracking(nums, numsSize, used); + //回溯 + pathTop--; + used[i] = 0; + } +} + +int** permute(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){ + //初始化辅助变量 + path = (int*)malloc(sizeof(int) * numsSize); + ans = (int**)malloc(sizeof(int*) * 1000); + int* used = (int*)malloc(sizeof(int) * numsSize); + //将used数组中元素都置0 + initialize(used, numsSize); + ansTop = pathTop = 0; + + backTracking(nums, numsSize, used); + + //设置path和ans数组的长度 + *returnSize = ansTop; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; i++) { + (*returnColumnSizes)[i] = numsSize; + } + return ans; +} +``` + +### Swift + +```swift +func permute(_ nums: [Int]) -> [[Int]] { + var result = [[Int]]() + var path = [Int]() + var used = [Bool](repeating: false, count: nums.count) // 记录path中已包含的元素 + func backtracking() { + // 结束条件,收集结果 + if path.count == nums.count { + result.append(path) + return + } + + for i in 0 ..< nums.count { + if used[i] { continue } // 排除已包含的元素 + used[i] = true + path.append(nums[i]) + backtracking() + // 回溯 + path.removeLast() + used[i] = false + } + } + backtracking() + return result +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def permute(nums: Array[Int]): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + + def backtracking(used: Array[Boolean]): Unit = { + if (path.size == nums.size) { + // 如果path的长度和nums相等,那么可以添加到结果集 + result.append(path.toList) + return + } + // 添加循环守卫,只有当当前数字没有用过的情况下才进入回溯 + for (i <- nums.indices if used(i) == false) { + used(i) = true + path.append(nums(i)) + backtracking(used) // 回溯 + path.remove(path.size - 1) + used(i) = false + } + } + + backtracking(new Array[Boolean](nums.size)) // 调用方法 + result.toList // 最终返回结果集的List形式 + } +} +``` +### C# +```csharp +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> Permute(int[] nums) + { + var used = new bool[nums.Length]; + BackTracking(nums, used); + return res; + } + public void BackTracking(int[] nums, bool[] used) + { + if (path.Count == nums.Length) + { + res.Add(new List(path)); + return; + } + for (int i = 0; i < nums.Length; i++) + { + if (used[i]) continue; + used[i] = true; + path.Add(nums[i]); + BackTracking(nums, used); + used[i] = false; + path.RemoveAt(path.Count - 1); + } + } +} +``` + + + diff --git "a/problems/0047.\345\205\250\346\216\222\345\210\227II.md" "b/problems/0047.\345\205\250\346\216\222\345\210\227II.md" new file mode 100755 index 0000000000..5330997a66 --- /dev/null +++ "b/problems/0047.\345\205\250\346\216\222\345\210\227II.md" @@ -0,0 +1,555 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 47.全排列 II + +[力扣题目链接](https://leetcode.cn/problems/permutations-ii/) + +给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。 + +示例 1: + +* 输入:nums = [1,1,2] +* 输出: + [[1,1,2], + [1,2,1], + [2,1,1]] + +示例 2: + +* 输入:nums = [1,2,3] +* 输出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]] + +提示: + +* 1 <= nums.length <= 8 +* -10 <= nums[i] <= 10 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[回溯算法求解全排列,如何去重?| LeetCode:47.全排列 II](https://www.bilibili.com/video/BV1R84y1i7Tm/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目和[46.全排列](https://programmercarl.com/0046.全排列.html)的区别在与**给定一个可包含重复数字的序列**,要返回**所有不重复的全排列**。 + +这里又涉及到去重了。 + +在[40.组合总和II](https://programmercarl.com/0040.组合总和II.html) 、[90.子集II](https://programmercarl.com/0090.子集II.html)我们分别详细讲解了组合问题和子集问题如何去重。 + +那么排列问题其实也是一样的套路。 + +**还要强调的是去重一定要对元素进行排序,这样我们才方便通过相邻的节点来判断是否重复使用了**。 + +我以示例中的 [1,1,2]为例 (为了方便举例,已经排序)抽象为一棵树,去重过程如图: + +![47.全排列II1](https://file1.kamacoder.com/i/algo/20201124201331223.png) + +图中我们对同一树层,前一位(也就是nums[i-1])如果使用过,那么就进行去重。 + +**一般来说:组合问题和排列问题是在树形结构的叶子节点上收集结果,而子集问题就是取树上所有节点的结果**。 + +在[46.全排列](https://programmercarl.com/0046.全排列.html)中已经详细讲解了排列问题的写法,在[40.组合总和II](https://programmercarl.com/0040.组合总和II.html) 、[90.子集II](https://programmercarl.com/0090.子集II.html)中详细讲解了去重的写法,所以这次我就不用回溯三部曲分析了,直接给出代码,如下: + + + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking (vector& nums, vector& used) { + // 此时说明找到了一组 + if (path.size() == nums.size()) { + result.push_back(path); + return; + } + for (int i = 0; i < nums.size(); i++) { + // used[i - 1] == true,说明同一树枝nums[i - 1]使用过 + // used[i - 1] == false,说明同一树层nums[i - 1]使用过 + // 如果同一树层nums[i - 1]使用过则直接跳过 + if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { + continue; + } + if (used[i] == false) { + used[i] = true; + path.push_back(nums[i]); + backtracking(nums, used); + path.pop_back(); + used[i] = false; + } + } + } +public: + vector> permuteUnique(vector& nums) { + result.clear(); + path.clear(); + sort(nums.begin(), nums.end()); // 排序 + vector used(nums.size(), false); + backtracking(nums, used); + return result; + } +}; + +// 时间复杂度: 最差情况所有元素都是唯一的。复杂度和全排列1都是 O(n! * n) 对于 n 个元素一共有 n! 中排列方案。而对于每一个答案,我们需要 O(n) 去复制最终放到 result 数组 +// 空间复杂度: O(n) 回溯树的深度取决于我们有多少个元素 +``` +* 时间复杂度: O(n! * n) +* 空间复杂度: O(n) + +## 拓展 + +大家发现,去重最为关键的代码为: + +```cpp +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { + continue; +} +``` + +**如果改成 `used[i - 1] == true`, 也是正确的!**,去重代码如下: + +```cpp +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { + continue; +} +``` + +这是为什么呢,就是上面我刚说的,如果要对树层中前一位去重,就用`used[i - 1] == false`,如果要对树枝前一位去重用`used[i - 1] == true`。 + +**对于排列问题,树层上去重和树枝上去重,都是可以的,但是树层上去重效率更高!** + +这么说是不是有点抽象? + +来来来,我就用输入: [1,1,1] 来举一个例子。 + +树层上去重(used[i - 1] == false),的树形结构如下: + +![47.全排列II2](https://file1.kamacoder.com/i/algo/20201124201406192.png) + +树枝上去重(used[i - 1] == true)的树型结构如下: + +![47.全排列II3](https://file1.kamacoder.com/i/algo/20201124201431571.png) + +大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。 + +## 总结 + +这道题其实还是用了我们之前讲过的去重思路,但有意思的是,去重的代码中,这么写: + +```cpp +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { + continue; +} +``` + +和这么写: + +```cpp +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { + continue; +} +``` + +都是可以的,这也是很多同学做这道题目困惑的地方,知道`used[i - 1] == false`也行而`used[i - 1] == true`也行,但是就想不明白为啥。 + +所以我通过举[1,1,1]的例子,把这两个去重的逻辑分别抽象成树形结构,大家可以一目了然:为什么两种写法都可以以及哪一种效率更高! + +这里可能大家又有疑惑,既然 `used[i - 1] == false`也行而`used[i - 1] == true`也行,那为什么还要写这个条件呢? + +直接这样写 不就完事了? + +```cpp +if (i > 0 && nums[i] == nums[i - 1]) { + continue; +} +``` + +其实并不行,一定要加上 `used[i - 1] == false`或者`used[i - 1] == true`,因为 used[i - 1] 要一直是 true 或者一直是false 才可以,而不是 一会是true 一会又是false。 所以这个条件要写上。 + + +是不是豁然开朗了!! + +## 其他语言版本 + +### Java + +```java +class Solution { + //存放结果 + List> result = new ArrayList<>(); + //暂存结果 + List path = new ArrayList<>(); + + public List> permuteUnique(int[] nums) { + boolean[] used = new boolean[nums.length]; + Arrays.fill(used, false); + Arrays.sort(nums); + backTrack(nums, used); + return result; + } + + private void backTrack(int[] nums, boolean[] used) { + if (path.size() == nums.length) { + result.add(new ArrayList<>(path)); + return; + } + for (int i = 0; i < nums.length; i++) { + // used[i - 1] == true,说明同⼀树⽀nums[i - 1]使⽤过 + // used[i - 1] == false,说明同⼀树层nums[i - 1]使⽤过 + // 如果同⼀树层nums[i - 1]使⽤过则直接跳过 + if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { + continue; + } + //如果同⼀树⽀nums[i]没使⽤过开始处理 + if (used[i] == false) { + used[i] = true;//标记同⼀树⽀nums[i]使⽤过,防止同一树枝重复使用 + path.add(nums[i]); + backTrack(nums, used); + path.remove(path.size() - 1);//回溯,说明同⼀树层nums[i]使⽤过,防止下一树层重复 + used[i] = false;//回溯 + } + } + } +} +``` + +Python + +```python +class Solution: + def permuteUnique(self, nums): + nums.sort() # 排序 + result = [] + self.backtracking(nums, [], [False] * len(nums), result) + return result + + def backtracking(self, nums, path, used, result): + if len(path) == len(nums): + result.append(path[:]) + return + for i in range(len(nums)): + if (i > 0 and nums[i] == nums[i - 1] and not used[i - 1]) or used[i]: + continue + used[i] = True + path.append(nums[i]) + self.backtracking(nums, path, used, result) + path.pop() + used[i] = False + +``` + +### Go + +```go +var ( + res [][]int + path []int + st []bool // state的缩写 +) +func permuteUnique(nums []int) [][]int { + res, path = make([][]int, 0), make([]int, 0, len(nums)) + st = make([]bool, len(nums)) + sort.Ints(nums) + dfs(nums, 0) + return res +} + +func dfs(nums []int, cur int) { + if cur == len(nums) { + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + } + for i := 0; i < len(nums); i++ { + if i != 0 && nums[i] == nums[i-1] && !st[i-1] { // 去重,用st来判别是深度还是广度 + continue + } + if !st[i] { + path = append(path, nums[i]) + st[i] = true + dfs(nums, cur + 1) + st[i] = false + path = path[:len(path)-1] + } + } +} +``` + +### JavaScript + +```javascript +var permuteUnique = function (nums) { + nums.sort((a, b) => { + return a - b + }) + let result = [] + let path = [] + + function backtracing( used) { + if (path.length === nums.length) { + result.push([...path]) + return + } + for (let i = 0; i < nums.length; i++) { + if (i > 0 && nums[i] === nums[i - 1] && !used[i - 1]) { + continue + } + if (!used[i]) { + used[i] = true + path.push(nums[i]) + backtracing(used) + path.pop() + used[i] = false + } + + + } + } + backtracing([]) + return result +}; + +``` + +### TypeScript + +```typescript +function permuteUnique(nums: number[]): number[][] { + nums.sort((a, b) => a - b); + const resArr: number[][] = []; + const usedArr: boolean[] = new Array(nums.length).fill(false); + backTracking(nums, []); + return resArr; + function backTracking(nums: number[], route: number[]): void { + if (route.length === nums.length) { + resArr.push([...route]); + return; + } + for (let i = 0, length = nums.length; i < length; i++) { + if (i > 0 && nums[i] === nums[i - 1] && usedArr[i - 1] === false) continue; + if (usedArr[i] === false) { + route.push(nums[i]); + usedArr[i] = true; + backTracking(nums, route); + usedArr[i] = false; + route.pop(); + } + } + } +}; +``` + +### Swift + +```swift +func permuteUnique(_ nums: [Int]) -> [[Int]] { + let nums = nums.sorted() // 先排序,以方便相邻元素去重 + var result = [[Int]]() + var path = [Int]() + var used = [Bool](repeating: false, count: nums.count) + func backtracking() { + if path.count == nums.count { + result.append(path) + return + } + + for i in 0 ..< nums.count { + // !used[i - 1]表示同一树层nums[i - 1]使用过,直接跳过,这一步很关键! + if i > 0, nums[i] == nums[i - 1], !used[i - 1] { continue } + if used[i] { continue } + used[i] = true + path.append(nums[i]) + backtracking() + // 回溯 + path.removeLast() + used[i] = false + } + } + backtracking() + return result +} +``` + +### Rust + +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, nums: &Vec, used: &mut Vec) { + let len = nums.len(); + if path.len() == len { + result.push(path.clone()); + return; + } + for i in 0..len { + if i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false { continue; } + if used[i] == false { + used[i] = true; + path.push(nums[i]); + Self::backtracking(result, path, nums, used); + path.pop(); + used[i] = false; + } + } + } + + pub fn permute_unique(nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + let mut used = vec![false; nums.len()]; + let mut nums= nums; + nums.sort(); + Self::backtracking(&mut result, &mut path, &nums, &mut used); + result + } +} +``` + +### C + +```c +//临时数组 +int *path; +//返回数组 +int **ans; +int *used; +int pathTop, ansTop; + +//拷贝path到ans中 +void copyPath() { + int *tempPath = (int*)malloc(sizeof(int) * pathTop); + int i; + for(i = 0; i < pathTop; ++i) { + tempPath[i] = path[i]; + } + ans[ansTop++] = tempPath; +} + +void backTracking(int* used, int *nums, int numsSize) { + //若path中元素个数等于numsSize,将path拷贝入ans数组中 + if(pathTop == numsSize) + copyPath(); + int i; + for(i = 0; i < numsSize; i++) { + //若当前元素已被使用 + //或前一位元素与当前元素值相同但并未被使用 + //则跳过此分支 + if(used[i] || (i != 0 && nums[i] == nums[i-1] && used[i-1] == 0)) + continue; + + //将当前元素的使用情况设为True + used[i] = 1; + path[pathTop++] = nums[i]; + backTracking(used, nums, numsSize); + used[i] = 0; + --pathTop; + } +} + +int cmp(void* elem1, void* elem2) { + return *((int*)elem1) - *((int*)elem2); +} + +int** permuteUnique(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){ + //排序数组 + qsort(nums, numsSize, sizeof(int), cmp); + //初始化辅助变量 + pathTop = ansTop = 0; + path = (int*)malloc(sizeof(int) * numsSize); + ans = (int**)malloc(sizeof(int*) * 1000); + //初始化used辅助数组 + used = (int*)malloc(sizeof(int) * numsSize); + int i; + for(i = 0; i < numsSize; i++) { + used[i] = 0; + } + + backTracking(used, nums, numsSize); + + //设置返回的数组的长度 + *returnSize = ansTop; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int z; + for(z = 0; z < ansTop; z++) { + (*returnColumnSizes)[z] = numsSize; + } + return ans; +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def permuteUnique(nums: Array[Int]): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + var num = nums.sorted // 首先对数据进行排序 + + def backtracking(used: Array[Boolean]): Unit = { + if (path.size == num.size) { + // 如果path的size等于num了,那么可以添加到结果集 + result.append(path.toList) + return + } + // 循环守卫,当前元素没被使用过就进入循环体 + for (i <- num.indices if used(i) == false) { + // 当前索引为0,不存在和前一个数字相等可以进入回溯 + // 当前索引值和上一个索引不相等,可以回溯 + // 前一个索引对应的值没有被选,可以回溯 + // 因为Scala没有continue,只能将逻辑反过来写 + if (i == 0 || (i > 0 && num(i) != num(i - 1)) || used(i-1) == false) { + used(i) = true + path.append(num(i)) + backtracking(used) + path.remove(path.size - 1) + used(i) = false + } + } + } + + backtracking(new Array[Boolean](nums.length)) + result.toList + } +} +``` +### C# +```csharp +public class Solution +{ + public List> res = new List>(); + public List path = new List(); + public IList> PermuteUnique(int[] nums) + { + Array.Sort(nums); + BackTracking(nums, new bool[nums.Length]); + return res; + } + public void BackTracking(int[] nums, bool[] used) + { + if (nums.Length == path.Count) + { + res.Add(new List(path)); + return; + } + for (int i = 0; i < nums.Length; i++) + { + if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) continue; + if (used[i]) continue; + path.Add(nums[i]); + used[i] = true; + BackTracking(nums, used); + path.RemoveAt(path.Count - 1); + used[i] = false; + } + } +} +``` + + diff --git "a/problems/0051.N\347\232\207\345\220\216.md" "b/problems/0051.N\347\232\207\345\220\216.md" new file mode 100755 index 0000000000..d06d7798e8 --- /dev/null +++ "b/problems/0051.N\347\232\207\345\220\216.md" @@ -0,0 +1,921 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 51. N皇后 + +[力扣题目链接](https://leetcode.cn/problems/n-queens/) + +n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 + +给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。 + +每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 + +示例 1: + +![](https://file1.kamacoder.com/i/algo/20211020232201.png) + +* 输入:n = 4 +* 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] +* 解释:如上图所示,4 皇后问题存在两个不同的解法。 + +示例 2: + +* 输入:n = 1 +* 输出:[["Q"]] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[这就是传说中的N皇后? 回溯算法安排!| LeetCode:51.N皇后](https://www.bilibili.com/video/BV1Rd4y1c7Bq/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +都知道n皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列问题之后,遇到这种二维矩阵还会有点不知所措。 + +首先来看一下皇后们的约束条件: + +1. 不能同行 +2. 不能同列 +3. 不能同斜线 + +确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。 + +下面我用一个 3 * 3 的棋盘,将搜索过程抽象为一棵树,如图: + +![51.N皇后](https://file1.kamacoder.com/i/algo/20210130182532303.jpg) + +从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。 + +那么我们用皇后们的约束条件,来回溯搜索这棵树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。 + +### 回溯三部曲 + +按照我总结的如下回溯模板,我们来依次分析: + +``` +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} +``` + +* 递归函数参数 + +我依然是定义全局变量二维数组result来记录最终结果。 + +参数n是棋盘的大小,然后用row来记录当前遍历到棋盘的第几层了。 + +代码如下: + +```cpp +vector> result; +void backtracking(int n, int row, vector& chessboard) { +``` + +* 递归终止条件 + +在如下树形结构中: +![51.N皇后](https://file1.kamacoder.com/i/algo/20210130182532303-20230310122134167.jpg) + + +可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。 + +代码如下: + +```cpp +if (row == n) { + result.push_back(chessboard); + return; +} +``` + +* 单层搜索的逻辑 + +递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。 + +每次都是要从新的一行的起始位置开始搜,所以都是从0开始。 + +代码如下: + +```cpp +for (int col = 0; col < n; col++) { + if (isValid(row, col, chessboard, n)) { // 验证合法就可以放 + chessboard[row][col] = 'Q'; // 放置皇后 + backtracking(n, row + 1, chessboard); + chessboard[row][col] = '.'; // 回溯,撤销皇后 + } +} +``` + +* 验证棋盘是否合法 + +按照如下标准去重: + +1. 不能同行 +2. 不能同列 +3. 不能同斜线 (45度和135度角) + +代码如下: + +```CPP +bool isValid(int row, int col, vector& chessboard, int n) { + // 检查列 + for (int i = 0; i < row; i++) { // 这是一个剪枝 + if (chessboard[i][col] == 'Q') { + return false; + } + } + // 检查 45度角是否有皇后 + for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + // 检查 135度角是否有皇后 + for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + return true; +} +``` + +在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢? + +因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。 + +那么按照这个模板不难写出如下C++代码: + +```CPP +class Solution { +private: +vector> result; +// n 为输入的棋盘大小 +// row 是当前递归到棋盘的第几行了 +void backtracking(int n, int row, vector& chessboard) { + if (row == n) { + result.push_back(chessboard); + return; + } + for (int col = 0; col < n; col++) { + if (isValid(row, col, chessboard, n)) { // 验证合法就可以放 + chessboard[row][col] = 'Q'; // 放置皇后 + backtracking(n, row + 1, chessboard); + chessboard[row][col] = '.'; // 回溯,撤销皇后 + } + } +} +bool isValid(int row, int col, vector& chessboard, int n) { + // 检查列 + for (int i = 0; i < row; i++) { // 这是一个剪枝 + if (chessboard[i][col] == 'Q') { + return false; + } + } + // 检查 45度角是否有皇后 + for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + // 检查 135度角是否有皇后 + for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + return true; +} +public: + vector> solveNQueens(int n) { + result.clear(); + std::vector chessboard(n, std::string(n, '.')); + backtracking(n, 0, chessboard); + return result; + } +}; +``` +* 时间复杂度: O(n!) +* 空间复杂度: O(n) + + +可以看出,除了验证棋盘合法性的代码,省下来部分就是按照回溯法模板来的。 + +## 总结 + +本题是我们解决棋盘问题的第一道题目。 + +如果从来没有接触过N皇后问题的同学看着这样的题会感觉无从下手,可能知道要用回溯法,但也不知道该怎么去搜。 + +**这里我明确给出了棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了**。 + +大家可以在仔细体会体会! + + +## 其他语言补充 + +### Java + +```java +class Solution { + List> res = new ArrayList<>(); + + public List> solveNQueens(int n) { + char[][] chessboard = new char[n][n]; + for (char[] c : chessboard) { + Arrays.fill(c, '.'); + } + backTrack(n, 0, chessboard); + return res; + } + + + public void backTrack(int n, int row, char[][] chessboard) { + if (row == n) { + res.add(Array2List(chessboard)); + return; + } + + for (int col = 0;col < n; ++col) { + if (isValid (row, col, n, chessboard)) { + chessboard[row][col] = 'Q'; + backTrack(n, row+1, chessboard); + chessboard[row][col] = '.'; + } + } + + } + + + public List Array2List(char[][] chessboard) { + List list = new ArrayList<>(); + + for (char[] c : chessboard) { + list.add(String.copyValueOf(c)); + } + return list; + } + + + public boolean isValid(int row, int col, int n, char[][] chessboard) { + // 检查列 + for (int i=0; i=0 && j>=0; i--, j--) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + + // 检查135度对角线 + for (int i=row-1, j=col+1; i>=0 && j<=n-1; i--, j++) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + return true; + } +} +``` + +```java +// 方法2:使用boolean数组表示已经占用的直(斜)线 +class Solution { + List> res = new ArrayList<>(); + boolean[] usedCol, usedDiag45, usedDiag135; // boolean数组中的每个元素代表一条直(斜)线 + public List> solveNQueens(int n) { + usedCol = new boolean[n]; // 列方向的直线条数为 n + usedDiag45 = new boolean[2 * n - 1]; // 45°方向的斜线条数为 2 * n - 1 + usedDiag135 = new boolean[2 * n - 1]; // 135°方向的斜线条数为 2 * n - 1 + //用于收集结果, 元素的index表示棋盘的row,元素的value代表棋盘的column + int[] board = new int[n]; + backTracking(board, n, 0); + return res; + } + private void backTracking(int[] board, int n, int row) { + if (row == n) { + //收集结果 + List temp = new ArrayList<>(); + for (int i : board) { + char[] str = new char[n]; + Arrays.fill(str, '.'); + str[i] = 'Q'; + temp.add(new String(str)); + } + res.add(temp); + return; + } + + for (int col = 0; col < n; col++) { + if (usedCol[col] | usedDiag45[row + col] | usedDiag135[row - col + n - 1]) { + continue; + } + board[row] = col; + // 标记该列出现过 + usedCol[col] = true; + // 同一45°斜线上元素的row + col为定值, 且各不相同 + usedDiag45[row + col] = true; + // 同一135°斜线上元素row - col为定值, 且各不相同 + // row - col 值有正有负, 加 n - 1 是为了对齐零点 + usedDiag135[row - col + n - 1] = true; + // 递归 + backTracking(board, n, row + 1); + usedCol[col] = false; + usedDiag45[row + col] = false; + usedDiag135[row - col + n - 1] = false; + } + } +} +``` + +### Python + +```python +class Solution: + def solveNQueens(self, n: int) -> List[List[str]]: + result = [] # 存储最终结果的二维字符串数组 + + chessboard = ['.' * n for _ in range(n)] # 初始化棋盘 + self.backtracking(n, 0, chessboard, result) # 回溯求解 + return [[''.join(row) for row in solution] for solution in result] # 返回结果集 + + def backtracking(self, n: int, row: int, chessboard: List[str], result: List[List[str]]) -> None: + if row == n: + result.append(chessboard[:]) # 棋盘填满,将当前解加入结果集 + return + + for col in range(n): + if self.isValid(row, col, chessboard): + chessboard[row] = chessboard[row][:col] + 'Q' + chessboard[row][col+1:] # 放置皇后 + self.backtracking(n, row + 1, chessboard, result) # 递归到下一行 + chessboard[row] = chessboard[row][:col] + '.' + chessboard[row][col+1:] # 回溯,撤销当前位置的皇后 + + def isValid(self, row: int, col: int, chessboard: List[str]) -> bool: + # 检查列 + for i in range(row): + if chessboard[i][col] == 'Q': + return False # 当前列已经存在皇后,不合法 + + # 检查 45 度角是否有皇后 + i, j = row - 1, col - 1 + while i >= 0 and j >= 0: + if chessboard[i][j] == 'Q': + return False # 左上方向已经存在皇后,不合法 + i -= 1 + j -= 1 + + # 检查 135 度角是否有皇后 + i, j = row - 1, col + 1 + while i >= 0 and j < len(chessboard): + if chessboard[i][j] == 'Q': + return False # 右上方向已经存在皇后,不合法 + i -= 1 + j += 1 + + return True # 当前位置合法 + +``` + + +### Go +```Go +func solveNQueens(n int) [][]string { + var res [][]string + chessboard := make([][]string, n) + for i := 0; i < n; i++ { + chessboard[i] = make([]string, n) + } + for i := 0; i < n; i++ { + for j := 0; j < n; j++ { + chessboard[i][j] = "." + } + } + var backtrack func(int) + backtrack = func(row int) { + if row == n { + temp := make([]string, n) + for i, rowStr := range chessboard { + temp[i] = strings.Join(rowStr, "") + } + res = append(res, temp) + return + } + for i := 0; i < n; i++ { + if isValid(n, row, i, chessboard) { + chessboard[row][i] = "Q" + backtrack(row + 1) + chessboard[row][i] = "." + } + } + } + backtrack(0) + return res +} + +func isValid(n, row, col int, chessboard [][]string) bool { + for i := 0; i < row; i++ { + if chessboard[i][col] == "Q" { + return false + } + } + for i, j := row-1, col-1; i >= 0 && j >= 0; i, j = i-1, j-1 { + if chessboard[i][j] == "Q" { + return false + } + } + for i, j := row-1, col+1; i >= 0 && j < n; i, j = i-1, j+1 { + if chessboard[i][j] == "Q" { + return false + } + } + return true +} +``` + + +### JavaScript +```Javascript +/** + * @param {number} n + * @return {string[][]} + */ +var solveNQueens = function (n) { + const ans = []; + const path = []; + const matrix = new Array(n).fill(0).map(() => new Array(n).fill(".")); + // 判断是否能相互攻击 + const canAttack = (matrix, row, col) => { + let i; + let j; + // 判断正上方和正下方是否有皇后 + for (i = 0, j = col; i < n; i++) { + if (matrix[i][j] === "Q") { + return true; + } + } + // 判断正左边和正右边是否有皇后 + for (i = row, j = 0; j < n; j++) { + if (matrix[i][j] === "Q") { + return true; + } + } + // 判断左上方是否有皇后 + for (i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { + if (matrix[i][j] === "Q") { + return true; + } + } + // 判断右上方是否有皇后 + for (i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { + if (matrix[i][j] === "Q") { + return true; + } + } + return false; + }; + const backtrack = (matrix, row, col) => { + if (path.length === matrix.length) { + ans.push(path.slice()); + return; + } + for (let i = row; i < matrix.length; i++) { + for (let j = col; j < matrix.length; j++) { + // 当前位置会导致互相攻击 继续下一轮搜索 + if (canAttack(matrix, i, j)) { + continue; + } + matrix[i][j] = "Q"; + path.push(matrix[i].join("")); + // 另起一行搜索 同一行只能有一个皇后 + backtrack(matrix, i + 1, 0); + matrix[i][j] = "."; + path.pop(); + } + } + }; + backtrack(matrix, 0, 0); + return ans; +}; +``` + +### TypeScript + +```typescript +function solveNQueens(n: number): string[][] { + const board: string[][] = new Array(n).fill(0).map(_ => new Array(n).fill('.')); + const resArr: string[][] = []; + backTracking(n, 0, board); + return resArr; + function backTracking(n: number, rowNum: number, board: string[][]): void { + if (rowNum === n) { + resArr.push(transformBoard(board)); + return; + } + for (let i = 0; i < n; i++) { + if (isValid(i, rowNum, board) === true) { + board[rowNum][i] = 'Q'; + backTracking(n, rowNum + 1, board); + board[rowNum][i] = '.'; + } + } + } +}; +function isValid(col: number, row: number, board: string[][]): boolean { + const n: number = board.length; + if (col < 0 || col >= n || row < 0 || row >= n) return false; + // 检查列 + for (let row of board) { + if (row[col] === 'Q') return false; + } + // 检查45度方向 + let x: number = col, + y: number = row; + while (y >= 0 && x < n) { + if (board[y--][x++] === 'Q') return false; + } + // 检查135度方向 + x = col; + y = row; + while (x >= 0 && y >= 0) { + if (board[y--][x--] === 'Q') return false; + } + return true; +} +function transformBoard(board: string[][]): string[] { + const resArr = []; + for (let row of board) { + resArr.push(row.join('')); + } + return resArr; +} +``` + +### Swift + +```swift +func solveNQueens(_ n: Int) -> [[String]] { + var result = [[String]]() + // 棋盘,使用Character的二维数组,以便于更新元素 + var chessboard = [[Character]](repeating: [Character](repeating: ".", count: n), count: n) + // 检查棋盘是否符合N皇后 + func isVaild(row: Int, col: Int) -> Bool { + // 检查列 + for i in 0 ..< row { + if chessboard[i][col] == "Q" { return false } + } + + var i, j: Int + // 检查45度 + i = row - 1 + j = col - 1 + while i >= 0, j >= 0 { + if chessboard[i][j] == "Q" { return false } + i -= 1 + j -= 1 + } + // 检查135度 + i = row - 1 + j = col + 1 + while i >= 0, j < n { + if chessboard[i][j] == "Q" { return false } + i -= 1 + j += 1 + } + + return true + } + func backtracking(row: Int) { + if row == n { + result.append(chessboard.map { String($0) }) + } + + for col in 0 ..< n { + guard isVaild(row: row, col: col) else { continue } + chessboard[row][col] = "Q" // 放置皇后 + backtracking(row: row + 1) + chessboard[row][col] = "." // 回溯 + } + } + backtracking(row: 0) + return result +} +``` + +### Rust + +```Rust +impl Solution { + fn is_valid(row: usize, col: usize, chessboard: &mut Vec>, n: usize) -> bool { + let mut i = 0 as usize; + while i < row { + if chessboard[i][col] == 'Q' { return false; } + i += 1; + } + let (mut i, mut j) = (row as i32 - 1, col as i32 - 1); + while i >= 0 && j >= 0 { + if chessboard[i as usize][j as usize] == 'Q' { return false; } + i -= 1; + j -= 1; + } + let (mut i, mut j) = (row as i32 - 1, col as i32 + 1); + while i >= 0 && j < n as i32 { + if chessboard[i as usize][j as usize] == 'Q' { return false; } + i -= 1; + j += 1; + } + return true; + } + fn backtracking(result: &mut Vec>, n: usize, row: usize, chessboard: &mut Vec>) { + if row == n { + let mut chessboard_clone: Vec = Vec::new(); + for i in chessboard { + chessboard_clone.push(i.iter().collect::()); + } + result.push(chessboard_clone); + return; + } + for col in 0..n { + if Self::is_valid(row, col, chessboard, n) { + chessboard[row][col] = 'Q'; + Self::backtracking(result, n, row + 1, chessboard); + chessboard[row][col] = '.'; + } + } + } + pub fn solve_n_queens(n: i32) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut chessboard: Vec> = vec![vec!['.'; n as usize]; n as usize]; + Self::backtracking(&mut result, n as usize, 0, &mut chessboard); + result + } +} +``` + +### C +```c +char ***ans; +char **path; +int ansTop, pathTop; +//将path中元素复制到ans中 +void copyPath(int n) { + char **tempPath = (char**)malloc(sizeof(char*) * pathTop); + int i; + for(i = 0; i < pathTop; ++i) { + tempPath[i] = (char*)malloc(sizeof(char) * n + 1); + int j; + for(j = 0; j < n; ++j) + tempPath[i][j] = path[i][j]; + tempPath[i][j] = '\0'; + + } + ans[ansTop++] = tempPath; +} + +//判断当前位置是否有效(是否不被其它皇后影响) +int isValid(int x, int y, int n) { + int i, j; + //检查同一行以及同一列是否有效 + for(i = 0; i < n; ++i) { + if(path[y][i] == 'Q' || path[i][x] == 'Q') + return 0; + } + //下面两个for循环检查斜角45度是否有效 + i = y - 1; + j = x - 1; + while(i >= 0 && j >= 0) { + if(path[i][j] == 'Q') + return 0; + --i, --j; + } + + i = y + 1; + j = x + 1; + while(i < n && j < n) { + if(path[i][j] == 'Q') + return 0; + ++i, ++j; + } + + //下面两个for循环检查135度是否有效 + i = y - 1; + j = x + 1; + while(i >= 0 && j < n) { + if(path[i][j] == 'Q') + return 0; + --i, ++j; + } + + i = y + 1; + j = x -1; + while(j >= 0 && i < n) { + if(path[i][j] == 'Q') + return 0; + ++i, --j; + } + return 1; +} + +void backTracking(int n, int depth) { + //若path中有四个元素,将其拷贝到ans中。从当前层返回 + if(pathTop == n) { + copyPath(n); + return; + } + + //遍历横向棋盘 + int i; + for(i = 0; i < n; ++i) { + //若当前位置有效 + if(isValid(i, depth, n)) { + //在当前位置放置皇后 + path[depth][i] = 'Q'; + //path中元素数量+1 + ++pathTop; + + backTracking(n, depth + 1); + //进行回溯 + path[depth][i] = '.'; + //path中元素数量-1 + --pathTop; + } + } +} + +//初始化存储char*数组path,将path中所有元素设为'.' +void initPath(int n) { + int i, j; + for(i = 0; i < n; i++) { + //为path中每个char*开辟空间 + path[i] = (char*)malloc(sizeof(char) * n + 1); + //将path中所有字符设为'.' + for(j = 0; j < n; j++) + path[i][j] = '.'; + //在每个字符串结尾加入'\0' + path[i][j] = '\0'; + } +} + +char *** solveNQueens(int n, int* returnSize, int** returnColumnSizes){ + //初始化辅助变量 + ans = (char***)malloc(sizeof(char**) * 400); + path = (char**)malloc(sizeof(char*) * n); + ansTop = pathTop = 0; + + //初始化path数组 + initPath(n); + backTracking(n, 0); + + //设置返回数组大小 + *returnSize = ansTop; + int i; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + for(i = 0; i < ansTop; ++i) { + (*returnColumnSizes)[i] = n; + } + return ans; +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def solveNQueens(n: Int): List[List[String]] = { + var result = mutable.ListBuffer[List[String]]() + + def judge(x: Int, y: Int, maze: Array[Array[Boolean]]): Boolean = { + // 正上方 + var xx = x + while (xx >= 0) { + if (maze(xx)(y)) return false + xx -= 1 + } + // 左边 + var yy = y + while (yy >= 0) { + if (maze(x)(yy)) return false + yy -= 1 + } + // 左上方 + xx = x + yy = y + while (xx >= 0 && yy >= 0) { + if (maze(xx)(yy)) return false + xx -= 1 + yy -= 1 + } + xx = x + yy = y + // 右上方 + while (xx >= 0 && yy < n) { + if (maze(xx)(yy)) return false + xx -= 1 + yy += 1 + } + true + } + + def backtracking(row: Int, maze: Array[Array[Boolean]]): Unit = { + if (row == n) { + // 将结果转换为题目所需要的形式 + var path = mutable.ListBuffer[String]() + for (x <- maze) { + var tmp = mutable.ListBuffer[String]() + for (y <- x) { + if (y == true) tmp.append("Q") + else tmp.append(".") + } + path.append(tmp.mkString) + } + result.append(path.toList) + return + } + + for (j <- 0 until n) { + // 判断这个位置是否可以放置皇后 + if (judge(row, j, maze)) { + maze(row)(j) = true + backtracking(row + 1, maze) + maze(row)(j) = false + } + } + } + + backtracking(0, Array.ofDim[Boolean](n, n)) + result.toList + } +} +``` +### C# +```csharp +public class Solution +{ + public List> res = new(); + public IList> SolveNQueens(int n) + { + char[][] chessBoard = new char[n][]; + for (int i = 0; i < n; i++) + { + chessBoard[i] = new char[n]; + for (int j = 0; j < n; j++) + { + chessBoard[i][j] = '.'; + } + } + BackTracking(n, 0, chessBoard); + return res; + } + public void BackTracking(int n, int row, char[][] chessBoard) + { + if (row == n) + { + res.Add(chessBoard.Select(x => new string(x)).ToList()); + return; + } + for (int col = 0; col < n; col++) + { + if (IsValid(row, col, chessBoard, n)) + { + chessBoard[row][col] = 'Q'; + BackTracking(n, row + 1, chessBoard); + chessBoard[row][col] = '.'; + } + } + } + public bool IsValid(int row, int col, char[][] chessBoard, int n) + { + for (int i = 0; i < row; i++) + { + if (chessBoard[i][col] == 'Q') return false; + } + for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) + { + if (chessBoard[i][j] == 'Q') return false; + } + for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) + { + if (chessBoard[i][j] == 'Q') return false; + } + return true; + } +} +``` + + diff --git "a/problems/0052.N\347\232\207\345\220\216II.md" "b/problems/0052.N\347\232\207\345\220\216II.md" new file mode 100755 index 0000000000..6c6650ad00 --- /dev/null +++ "b/problems/0052.N\347\232\207\345\220\216II.md" @@ -0,0 +1,307 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 52. N皇后II + +题目链接:[https://leetcode.cn/problems/n-queens-ii/](https://leetcode.cn/problems/n-queens-ii/) + +n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 + +上图为 8 皇后问题的一种解法。 + + +![51n皇后](https://file1.kamacoder.com/i/algo/20200821152118456.png) + +给定一个整数 n,返回 n 皇后不同的解决方案的数量。 + +示例: + +输入: 4 + +输出: 2 + +解释: 4 皇后问题存在如下两个不同的解法。 + +解法 1 + +[ + [".Q..", +  "...Q", +  "Q...", +  "..Q."], + +解法 2 + + ["..Q.", +  "Q...", +  "...Q", +  ".Q.."] +] + +## 思路 + + +详看:[51.N皇后](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg) ,基本没有区别 + +```CPP +class Solution { +private: +int count = 0; +void backtracking(int n, int row, vector& chessboard) { + if (row == n) { + count++; + return; + } + for (int col = 0; col < n; col++) { + if (isValid(row, col, chessboard, n)) { + chessboard[row][col] = 'Q'; // 放置皇后 + backtracking(n, row + 1, chessboard); + chessboard[row][col] = '.'; // 回溯 + } + } +} +bool isValid(int row, int col, vector& chessboard, int n) { + int count = 0; + // 检查列 + for (int i = 0; i < row; i++) { // 这是一个剪枝 + if (chessboard[i][col] == 'Q') { + return false; + } + } + // 检查 45度角是否有皇后 + for (int i = row - 1, j = col - 1; i >=0 && j >= 0; i--, j--) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + // 检查 135度角是否有皇后 + for(int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) { + if (chessboard[i][j] == 'Q') { + return false; + } + } + return true; +} + +public: + int totalNQueens(int n) { + std::vector chessboard(n, std::string(n, '.')); + backtracking(n, 0, chessboard); + return count; + + } +}; +``` + +## 其他语言补充 +### JavaScript + +```javascript +var totalNQueens = function(n) { + let count = 0; + const backtracking = (n, row, chessboard) => { + if(row === n){ + count++; + return; + } + for(let col = 0; col < n; col++){ + if(isValid(row, col, chessboard, n)) { // 验证合法就可以放 + chessboard[row][col] = 'Q'; // 放置皇后 + backtracking(n, row + 1, chessboard); + chessboard[row][col] = '.'; // 回溯 + } + } + } + + const isValid = (row, col, chessboard, n) => { + // 检查列 + for(let i = 0; i < row; i++){ // 这是一个剪枝 + if(chessboard[i][col] === 'Q'){ + return false; + } + } + // 检查 45度角是否有皇后 + for(let i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--){ + if(chessboard[i][j] === 'Q'){ + return false; + } + } + // 检查 135度角是否有皇后 + for(let i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++){ + if(chessboard[i][j] === 'Q'){ + return false; + } + } + return true; + } + const chessboard = new Array(n).fill([]).map(() => new Array(n).fill('.')); + backtracking(n, 0, chessboard); + return count; +}; +``` + +### TypeScript + +```typescript +// 0-该格为空,1-该格有皇后 +type GridStatus = 0 | 1; +function totalNQueens(n: number): number { + let resCount: number = 0; + const chess: GridStatus[][] = new Array(n).fill(0) + .map(_ => new Array(n).fill(0)); + backTracking(chess, n, 0); + return resCount; + function backTracking(chess: GridStatus[][], n: number, startRowIndex: number): void { + if (startRowIndex === n) { + resCount++; + return; + } + for (let j = 0; j < n; j++) { + if (checkValid(chess, startRowIndex, j, n) === true) { + chess[startRowIndex][j] = 1; + backTracking(chess, n, startRowIndex + 1); + chess[startRowIndex][j] = 0; + } + } + } +}; +function checkValid(chess: GridStatus[][], i: number, j: number, n: number): boolean { + // 向上纵向检查 + let tempI: number = i - 1, + tempJ: number = j; + while (tempI >= 0) { + if (chess[tempI][tempJ] === 1) return false; + tempI--; + } + // 斜向左上检查 + tempI = i - 1; + tempJ = j - 1; + while (tempI >= 0 && tempJ >= 0) { + if (chess[tempI][tempJ] === 1) return false; + tempI--; + tempJ--; + } + // 斜向右上检查 + tempI = i - 1; + tempJ = j + 1; + while (tempI >= 0 && tempJ < n) { + if (chess[tempI][tempJ] === 1) return false; + tempI--; + tempJ++; + } + return true; +} +``` + +### C + +```c +//path[i]为在i行,path[i]列上存在皇后 +int *path; +int pathTop; +int answer; +//检查当前level行index列放置皇后是否合法 +int isValid(int index, int level) { + int i; + //updater为若斜角存在皇后,其所应在的列 + //用来检查左上45度是否存在皇后 + int lCornerUpdater = index - level; + //用来检查右上135度是否存在皇后 + int rCornerUpdater = index + level; + for(i = 0; i < pathTop; ++i) { + //path[i] == index检查index列是否存在皇后 + //检查斜角皇后:只要path[i] == updater,就说明当前位置不可放置皇后。 + //path[i] == lCornerUpdater检查左上角45度是否有皇后 + //path[i] == rCornerUpdater检查右上角135度是否有皇后 + if(path[i] == index || path[i] == lCornerUpdater || path[i] == rCornerUpdater) + return 0; + //更新updater指向下一行对应的位置 + ++lCornerUpdater; + --rCornerUpdater; + } + return 1; +} + +//回溯算法:level为当前皇后行数 +void backTracking(int n, int level) { + //若path中元素个数已经为n,则证明有一种解法。answer+1 + if(pathTop == n) { + ++answer; + return; + } + + int i; + for(i = 0; i < n; ++i) { + //若当前level行,i列是合法的放置位置。就将i放入path中 + if(isValid(i, level)) { + path[pathTop++] = i; + backTracking(n, level + 1); + //回溯 + --pathTop; + } + } +} + +int totalNQueens(int n){ + answer = 0; + pathTop = 0; + path = (int *)malloc(sizeof(int) * n); + + backTracking(n, 0); + + return answer; +} +``` +### Java + +```java +class Solution { + int count = 0; + public int totalNQueens(int n) { + char[][] board = new char[n][n]; + for (char[] chars : board) { + Arrays.fill(chars, '.'); + } + backTrack(n, 0, board); + return count; + } + private void backTrack(int n, int row, char[][] board) { + if (row == n) { + count++; + return; + } + for (int col = 0; col < n; col++) { + if (isValid(row, col, n, board)) { + board[row][col] = 'Q'; + backTrack(n, row + 1, board); + board[row][col] = '.'; + } + } + } + private boolean isValid(int row, int col, int n, char[][] board) { + // 检查列 + for (int i = 0; i < row; ++i) { + if (board[i][col] == 'Q') { + return false; + } + } + // 检查45度对角线 + for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) { + if (board[i][j] == 'Q') { + return false; + } + } + // 检查135度对角线 + for (int i = row - 1, j = col + 1; i >= 0 && j <= n - 1; i--, j++) { + if (board[i][j] == 'Q') { + return false; + } + } + return true; + } +} +``` + diff --git "a/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" new file mode 100755 index 0000000000..84bb5f6663 --- /dev/null +++ "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214.md" @@ -0,0 +1,492 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 53. 最大子序和 + +[力扣题目链接](https://leetcode.cn/problems/maximum-subarray/) + +给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +示例: + +- 输入: [-2,1,-3,4,-1,2,1,-5,4] +- 输出: 6 +- 解释:  连续子数组  [4,-1,2,1] 的和最大,为  6。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法的巧妙需要慢慢体会!LeetCode:53. 最大子序和](https://www.bilibili.com/video/BV1aY4y1Z7ya),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 暴力解法 + +暴力解法的思路,第一层 for 就是设置起始位置,第二层 for 循环遍历数组寻找最大值 + + +```CPP +class Solution { +public: + int maxSubArray(vector& nums) { + int result = INT32_MIN; + int count = 0; + for (int i = 0; i < nums.size(); i++) { // 设置起始位置 + count = 0; + for (int j = i; j < nums.size(); j++) { // 每次从起始位置i开始遍历寻找最大值 + count += nums[j]; + result = count > result ? count : result; + } + } + return result; + } +}; +``` +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + + +以上暴力的解法 C++勉强可以过,其他语言就不确定了。 + +### 贪心解法 + +**贪心贪的是哪里呢?** + +如果 -2 1 在一起,计算起点的时候,一定是从 1 开始计算,因为负数只会拉低总和,这就是贪心贪的地方! + +局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。 + +全局最优:选取最大“连续和” + +**局部最优的情况下,并记录最大的“连续和”,可以推出全局最优**。 + +从代码角度上来讲:遍历 nums,从头开始用 count 累积,如果 count 一旦加上 nums[i]变为负数,那么就应该从 nums[i+1]开始从 0 累积 count 了,因为已经变为负数的 count,只会拖累总和。 + +**这相当于是暴力解法中的不断调整最大子序和区间的起始位置**。 + +**那有同学问了,区间终止位置不用调整么? 如何才能得到最大“连续和”呢?** + +区间的终止位置,其实就是如果 count 取到最大值了,及时记录下来了。例如如下代码: + +``` +if (count > result) result = count; +``` + +**这样相当于是用 result 记录最大子序和区间和(变相的算是调整了终止位置)**。 + +如动画所示: + +![53.最大子序和](https://file1.kamacoder.com/i/algo/53.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C.gif) + +红色的起始位置就是贪心每次取 count 为正数的时候,开始一个区间的统计。 + +那么不难写出如下 C++代码(关键地方已经注释) + +```CPP +class Solution { +public: + int maxSubArray(vector& nums) { + int result = INT32_MIN; + int count = 0; + for (int i = 0; i < nums.size(); i++) { + count += nums[i]; + if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置) + result = count; + } + if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 + } + return result; + } +}; +``` +- 时间复杂度:O(n) +- 空间复杂度:O(1) + +当然题目没有说如果数组为空,应该返回什么,所以数组为空的话返回啥都可以了。 + +### 常见误区 + +误区一: + +不少同学认为 如果输入用例都是-1,或者 都是负数,这个贪心算法跑出来的结果是 0, 这是**又一次证明脑洞模拟不靠谱的经典案例**,建议大家把代码运行一下试一试,就知道了,也会理解 为什么 result 要初始化为最小负数了。 + +误区二: + +大家在使用贪心算法求解本题,经常陷入的误区,就是分不清,是遇到 负数就选择起始位置,还是连续和为负选择起始位置。 + +在动画演示用,大家可以发现, 4,遇到 -1 的时候,我们依然累加了,为什么呢? + +因为和为 3,只要连续和还是正数就会 对后面的元素 起到增大总和的作用。 所以只要连续和为正数我们就保留。 + +这里也会有录友疑惑,那 4 + -1 之后 不就变小了吗? 会不会错过 4 成为最大连续和的可能性? + +其实并不会,因为还有一个变量 result 一直在更新 最大的连续和,只要有更大的连续和出现,result 就更新了,那么 result 已经把 4 更新了,后面 连续和变成 3,也不会对最后结果有影响。 + +### 动态规划 + +当然本题还可以用动态规划来做,在代码随想录动态规划章节我会详细介绍,如果大家想在想看,可以直接跳转:[动态规划版本详解](https://programmercarl.com/0053.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C%EF%BC%88%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%EF%BC%89.html#%E6%80%9D%E8%B7%AF) + +那么先给出我的 dp 代码如下,有时间的录友可以提前做一做: + +```CPP +class Solution { +public: + int maxSubArray(vector& nums) { + if (nums.size() == 0) return 0; + vector dp(nums.size(), 0); // dp[i]表示包括i之前的最大连续子序列和 + dp[0] = nums[0]; + int result = dp[0]; + for (int i = 1; i < nums.size(); i++) { + dp[i] = max(dp[i - 1] + nums[i], nums[i]); // 状态转移公式 + if (dp[i] > result) result = dp[i]; // result 保存dp[i]的最大值 + } + return result; + } +}; +``` + +- 时间复杂度:O(n) +- 空间复杂度:O(n) + +## 总结 + +本题的贪心思路其实并不好想,这也进一步验证了,别看贪心理论很直白,有时候看似是常识,但贪心的题目一点都不简单! + +后续将介绍的贪心题目都挺难的,所以贪心很有意思,别小看贪心! + +## 其他语言版本 + +### Java + +```java +class Solution { + public int maxSubArray(int[] nums) { + if (nums.length == 1){ + return nums[0]; + } + int sum = Integer.MIN_VALUE; + int count = 0; + for (int i = 0; i < nums.length; i++){ + count += nums[i]; + sum = Math.max(sum, count); // 取区间累计的最大值(相当于不断确定最大子序终止位置) + if (count <= 0){ + count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 + } + } + return sum; + } +} +``` + +```java +// DP 方法 +class Solution { + public int maxSubArray(int[] nums) { + int ans = Integer.MIN_VALUE; + int[] dp = new int[nums.length]; + dp[0] = nums[0]; + ans = dp[0]; + + for (int i = 1; i < nums.length; i++){ + dp[i] = Math.max(dp[i-1] + nums[i], nums[i]); + ans = Math.max(dp[i], ans); + } + + return ans; + } +} +``` + +### Python +暴力法 +```python +class Solution: + def maxSubArray(self, nums): + result = float('-inf') # 初始化结果为负无穷大 + count = 0 + for i in range(len(nums)): # 设置起始位置 + count = 0 + for j in range(i, len(nums)): # 从起始位置i开始遍历寻找最大值 + count += nums[j] + result = max(count, result) # 更新最大值 + return result + +``` +贪心法 +```python +class Solution: + def maxSubArray(self, nums): + result = float('-inf') # 初始化结果为负无穷大 + count = 0 + for i in range(len(nums)): + count += nums[i] + if count > result: # 取区间累计的最大值(相当于不断确定最大子序终止位置) + result = count + if count <= 0: # 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 + count = 0 + return result +``` +动态规划 +```python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + dp = [0] * len(nums) + dp[0] = nums[0] + res = nums[0] + for i in range(1, len(nums)): + dp[i] = max(dp[i-1] + nums[i], nums[i]) + res = max(res, dp[i]) + return res +``` + +动态规划 + +```python +class Solution: + def maxSubArray(self, nums): + if not nums: + return 0 + dp = [0] * len(nums) # dp[i]表示包括i之前的最大连续子序列和 + dp[0] = nums[0] + result = dp[0] + for i in range(1, len(nums)): + dp[i] = max(dp[i-1]+nums[i], nums[i]) # 状态转移公式 + if dp[i] > result: + result = dp[i] # result 保存dp[i]的最大值 + return result +``` + +动态规划优化 + +```python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + max_sum = float("-inf") # 初始化结果为负无穷大,方便比较取最大值 + current_sum = 0 # 初始化当前连续和 + + for num in nums: + + # 更新当前连续和 + # 如果原本的连续和加上当前数字之后没有当前数字大,说明原本的连续和是负数,那么就直接从当前数字开始重新计算连续和 + current_sum = max(current_sum+num, num) + max_sum = max(max_sum, current_sum) # 更新结果 + + return max_sum +``` + +### Go +贪心法 +```go +func maxSubArray(nums []int) int { + max := nums[0] + count := 0 + + for i := 0; i < len(nums); i++{ + count += nums[i] + if count > max{ + max = count + } + if count < 0 { + count = 0 + } + } + return max +} +``` +动态规划 +```go +func maxSubArray(nums []int) int { + maxSum := nums[0] + for i := 1; i < len(nums); i++ { + if nums[i] + nums[i-1] > nums[i] { + nums[i] += nums[i-1] + } + if nums[i] > maxSum { + maxSum = nums[i] + } + } + return maxSum +} +``` + +### Rust + +```rust +pub fn max_sub_array(nums: Vec) -> i32 { + let mut max_sum = i32::MIN; + let mut curr = 0; + for n in nums.iter() { + curr += n; + max_sum = max_sum.max(curr); + curr = curr.max(0); + } + max_sum +} +``` + +### JavaScript: + +```Javascript +var maxSubArray = function(nums) { + let result = -Infinity + let count = 0 + for(let i = 0; i < nums.length; i++) { + count += nums[i] + if(count > result) { + result = count + } + if(count < 0) { + count = 0 + } + } + return result +}; +``` + +### C: + +贪心: + +```c +int maxSubArray(int* nums, int numsSize){ + int maxVal = INT_MIN; + int subArrSum = 0; + + int i; + for(i = 0; i < numsSize; ++i) { + subArrSum += nums[i]; + // 若当前局部和大于之前的最大结果,对结果进行更新 + maxVal = subArrSum > maxVal ? subArrSum : maxVal; + // 若当前局部和为负,对结果无益。则从nums[i+1]开始应重新计算。 + subArrSum = subArrSum < 0 ? 0 : subArrSum; + } + + return maxVal; +} +``` + +动态规划: + +```c +/** + * 解题思路:动态规划: + * 1. dp数组:dp[i]表示从0到i的子序列中最大序列和的值 + * 2. 递推公式:dp[i] = max(dp[i-1] + nums[i], nums[i]) + 若dp[i-1]<0,对最后结果无益。dp[i]则为nums[i]。 + * 3. dp数组初始化:dp[0]的最大子数组和为nums[0] + * 4. 推导顺序:从前往后遍历 + */ + +#define max(a, b) (((a) > (b)) ? (a) : (b)) + +int maxSubArray(int* nums, int numsSize){ + int dp[numsSize]; + // dp[0]最大子数组和为nums[0] + dp[0] = nums[0]; + // 若numsSize为1,应直接返回nums[0] + int subArrSum = nums[0]; + + int i; + for(i = 1; i < numsSize; ++i) { + dp[i] = max(dp[i - 1] + nums[i], nums[i]); + + // 若dp[i]大于之前记录的最大值,进行更新 + if(dp[i] > subArrSum) + subArrSum = dp[i]; + } + + return subArrSum; +} +``` + +### TypeScript + +**贪心** + +```typescript +function maxSubArray(nums: number[]): number { + let curSum: number = 0; + let resMax: number = -Infinity; + for (let i = 0, length = nums.length; i < length; i++) { + curSum += nums[i]; + resMax = Math.max(curSum, resMax); + if (curSum < 0) curSum = 0; + } + return resMax; +} +``` + +**动态规划** + +```typescript +// 动态规划 +function maxSubArray(nums: number[]): number { + const length = nums.length; + if (length === 0) return 0; + const dp: number[] = []; + dp[0] = nums[0]; + let resMax: number = nums[0]; + for (let i = 1; i < length; i++) { + dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); + resMax = Math.max(resMax, dp[i]); + } + return resMax; +} +``` + +### Scala + +**贪心** + +```scala +object Solution { + def maxSubArray(nums: Array[Int]): Int = { + var result = Int.MinValue + var count = 0 + for (i <- nums.indices) { + count += nums(i) // count累加 + if (count > result) result = count // 记录最大值 + if (count <= 0) count = 0 // 一旦count为负,则count归0 + } + result + } +} +``` + +**动态规划** + +```scala +object Solution { + def maxSubArray(nums: Array[Int]): Int = { + var dp = new Array[Int](nums.length) + var result = nums(0) + dp(0) = nums(0) + for (i <- 1 until nums.length) { + dp(i) = math.max(nums(i), dp(i - 1) + nums(i)) + result = math.max(result, dp(i)) // 更新最大值 + } + result + } +} +``` +### C# +**贪心** +```csharp +public class Solution +{ + public int MaxSubArray(int[] nums) + { + int res = Int32.MinValue; + int count = 0; + for (int i = 0; i < nums.Length; i++) + { + count += nums[i]; + res = Math.Max(res, count); + if (count < 0) count = 0; + } + return res; + } +} +``` + + diff --git "a/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" new file mode 100755 index 0000000000..ba44a36104 --- /dev/null +++ "b/problems/0053.\346\234\200\345\244\247\345\255\220\345\272\217\345\222\214\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" @@ -0,0 +1,244 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 53. 最大子序和 + +[力扣题目链接](https://leetcode.cn/problems/maximum-subarray/) + +给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +示例: +* 输入: [-2,1,-3,4,-1,2,1,-5,4] +* 输出: 6 +* 解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[看起来复杂,其实是简单动态规划 | LeetCode:53.最大子序和](https://www.bilibili.com/video/BV19V4y1F7b5),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题之前我们在讲解贪心专题的时候用贪心算法解决过一次,[贪心算法:最大子序和](https://programmercarl.com/0053.最大子序和.html)。 + +这次我们用动态规划的思路再来分析一次。 + +动规五部曲如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i]:包括下标i(以nums[i]为结尾)的最大连续子序列和为dp[i]**。 + +2. 确定递推公式 + +dp[i]只有两个方向可以推出来: + +* dp[i - 1] + nums[i],即:nums[i]加入当前连续子序列和 +* nums[i],即:从头开始计算当前连续子序列和 + +一定是取最大的,所以dp[i] = max(dp[i - 1] + nums[i], nums[i]); + +3. dp数组如何初始化 + +从递推公式可以看出来dp[i]是依赖于dp[i - 1]的状态,dp[0]就是递推公式的基础。 + +dp[0]应该是多少呢? + +根据dp[i]的定义,很明显dp[0]应为nums[0]即dp[0] = nums[0]。 + +4. 确定遍历顺序 + +递推公式中dp[i]依赖于dp[i - 1]的状态,需要从前向后遍历。 + +5. 举例推导dp数组 + +以示例一为例,输入:nums = [-2,1,-3,4,-1,2,1,-5,4],对应的dp状态如下: +![53.最大子序和(动态规划)](https://file1.kamacoder.com/i/algo/20210303104129101.png) + +**注意最后的结果可不是dp[nums.size() - 1]!** ,而是dp[6]。 + +在回顾一下dp[i]的定义:包括下标i之前的最大连续子序列和为dp[i]。 + +那么我们要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。 + +所以在递推公式的时候,可以直接选出最大的dp[i]。 + +以上动规五部曲分析完毕,完整代码如下: + +```CPP +class Solution { +public: + int maxSubArray(vector& nums) { + if (nums.size() == 0) return 0; + vector dp(nums.size()); + dp[0] = nums[0]; + int result = dp[0]; + for (int i = 1; i < nums.size(); i++) { + dp[i] = max(dp[i - 1] + nums[i], nums[i]); // 状态转移公式 + if (dp[i] > result) result = dp[i]; // result 保存dp[i]的最大值 + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + + +## 总结 + +这道题目用贪心也很巧妙,但有一点绕,需要仔细想一想,如果想回顾一下贪心就看这里吧:[贪心算法:最大子序和](https://programmercarl.com/0053.最大子序和.html) + +动规的解法还是很直接的。 + +## 其他语言版本 + +### Java: + +```java + /** + * 1.dp[i]代表当前下标对应的最大值 + * 2.递推公式 dp[i] = max (dp[i-1]+nums[i],nums[i]) res = max(res,dp[i]) + * 3.初始化 都为 0 + * 4.遍历方向,从前往后 + * 5.举例推导结果。。。 + * + * @param nums + * @return + */ + public static int maxSubArray(int[] nums) { + if (nums.length == 0) { + return 0; + } + + int res = nums[0]; + int[] dp = new int[nums.length]; + dp[0] = nums[0]; + for (int i = 1; i < nums.length; i++) { + dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); + res = res > dp[i] ? res : dp[i]; + } + return res; + } +``` +```Java +//因为dp[i]的递推公式只与前一个值有关,所以可以用一个变量代替dp数组,空间复杂度为O(1) +class Solution { + public int maxSubArray(int[] nums) { + int res = nums[0]; + int pre = nums[0]; + for(int i = 1; i < nums.length; i++) { + pre = Math.max(pre + nums[i], nums[i]); + res = Math.max(res, pre); + } + return res; + } +} +``` + +### Python: + +```python +class Solution: + def maxSubArray(self, nums: List[int]) -> int: + dp = [0] * len(nums) + dp[0] = nums[0] + result = dp[0] + for i in range(1, len(nums)): + dp[i] = max(dp[i-1] + nums[i], nums[i]) #状态转移公式 + result = max(result, dp[i]) #result 保存dp[i]的最大值 + return result +``` + +### Go: + +```Go +// solution +// 1, dp +// 2, 贪心 + +func maxSubArray(nums []int) int { + n := len(nums) + // 这里的dp[i] 表示,最大的连续子数组和,包含num[i] 元素 + dp := make([]int,n) + // 初始化,由于dp 状态转移方程依赖dp[0] + dp[0] = nums[0] + // 初始化最大的和 + mx := nums[0] + for i:=1;ib { + return a + } + return b +} +``` + +### JavaScript: + +```javascript +const maxSubArray = nums => { + // 数组长度,dp初始化 + const len = nums.length; + let dp = new Array(len).fill(0); + dp[0] = nums[0]; + // 最大值初始化为dp[0] + let max = dp[0]; + for (let i = 1; i < len; i++) { + dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]); + // 更新最大值 + max = Math.max(max, dp[i]); + } + return max; +}; +``` + +### Scala: + +```scala +object Solution { + def maxSubArray(nums: Array[Int]): Int = { + var dp = new Array[Int](nums.length) + var result = nums(0) + dp(0) = nums(0) + for (i <- 1 until nums.length) { + dp(i) = math.max(nums(i), dp(i - 1) + nums(i)) + result = math.max(result, dp(i)) // 更新最大值 + } + result + } +} +``` + +### TypeScript: + +```typescript +function maxSubArray(nums: number[]): number { + const len = nums.length + if (len === 1) return nums[0] + + const dp: number[] = new Array(len) + let resMax: number = dp[0] = nums[0] + + for (let i = 1; i < len; i++) { + dp[i] = Math.max(dp[i - 1] + nums[i], nums[i]) + // 注意值为负数的情况 + if (dp[i] > resMax) resMax = dp[i] + } + + return resMax +} +``` + + + diff --git "a/problems/0054.\350\236\272\346\227\213\347\237\251\351\230\265.md" "b/problems/0054.\350\236\272\346\227\213\347\237\251\351\230\265.md" new file mode 100755 index 0000000000..8b700c1fe8 --- /dev/null +++ "b/problems/0054.\350\236\272\346\227\213\347\237\251\351\230\265.md" @@ -0,0 +1,485 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 54.螺旋矩阵 + +[力扣题目链接](https://leetcode.cn/problems/spiral-matrix/) + +给你一个 m 行 n 列的矩阵 matrix ,请按照 顺时针螺旋顺序 ,返回矩阵中的所有元素。 + +示例1: + +输入: +[ + [ 1, 2, 3 ], + [ 4, 5, 6 ], + [ 7, 8, 9 ] +] +输出:[1,2,3,6,9,8,7,4,5] + +## 思路 + +本题解决思路继承自[59.螺旋矩阵II](https://www.programmercarl.com/0059.%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5II.html),建议看完59.螺旋矩阵II之后再看本题 + +与59.螺旋矩阵II相同的是:两者都是模拟矩形的顺时针旋转,所以核心依然是依然是坚持循环不变量,按照左闭右开的原则 + +模拟顺时针画矩阵的过程: + +* 填充上行从左到右 +* 填充右列从上到下 +* 填充下行从右到左 +* 填充左列从下到上 + +由外向内一圈一圈这么画下去,如下所示: + + +![](https://file1.kamacoder.com/i/algo/20220922102236.png) + +这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。 + +与59.螺旋矩阵II不同的是:前题中的螺旋矩阵是正方形,只有正方形的边长n一个边界条件,而本题中,需要考虑长方形的长和宽(m行和n列)两个边界条件。自然,m可以等于n,即前题可视为本题在m==n的特殊情况。 + +我们从最一般的情况开始考虑,与59.螺旋矩阵II题解对比起来,m和n的带入,主要引来两方面的差异: + +* loop的计算: + 本题的loop计算与59.螺旋矩阵II算法略微差异,因为存在rows和columns两个维度,可自行分析,loop只能取min(rows, columns),例如rows = 5, columns = 7,那loop = 5 / 7 = 2 +* mid的计算及填充: + 1、同样的原理,本题的mid计算也存在上述差异; + 2、 + 如果min(rows, columns)为偶数,则不需要在最后单独考虑矩阵最中间位置的赋值 + 如果min(rows, columns)为奇数,则矩阵最中间位置不只是[mid][mid],而是会留下来一个特殊的中间行或者中间列,具体是中间行还是中间列,要看rows和columns的大小,如果rows > columns,则是中间列,相反,则是中间行 + +代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。 + +整体C++代码如下: + +```CPP +class Solution { +public: + vector spiralOrder(vector>& matrix) { + if (matrix.size() == 0 || matrix[0].size() == 0) + return {}; + int rows = matrix.size(), columns = matrix[0].size(); + int total = rows * columns; + vector res(total); // 使用vector定义一个一维数组存放结果 + int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 + int loop = min(rows, columns) / 2; + // 本题的loop计算与59.螺旋矩阵II算法略微差异,因为存在rows和columns两个维度,可自行分析,loop只能取min(rows, columns),例如rows = 5, columns = 7,那loop = 5 / 7 = 2 + int mid = min(rows, columns) / 2; + // 1、同样的原理,本题的mid计算也存在上述差异; + // 2、 + //如果min(rows, columns)为偶数,则不需要在最后单独考虑矩阵最中间位置的赋值 + //如果min(rows, columns)为奇数,则矩阵最中间位置不只是[mid][mid],而是会留下来一个特殊的中间行或者中间列,具体是中间行还是中间列,要看rows和columns的大小,如果rows > columns,则是中间列,相反,则是中间行 + //相信这一点不好理解,建议自行画图理解 + int count = 0;// 用来给矩阵中每一个空格赋值 + int offset = 1;// 每一圈循环,需要控制每一条边遍历的长度 + int i,j; + while (loop --) { + i = startx; + j = starty; + + // 下面开始的四个for就是模拟转了一圈 + // 模拟填充上行从左到右(左闭右开) + for (j = starty; j < starty + columns - offset; j++) { + res[count++] = matrix[startx][j]; + } + // 模拟填充右列从上到下(左闭右开) + for (i = startx; i < startx + rows - offset; i++) { + res[count++] = matrix[i][j]; + } + // 模拟填充下行从右到左(左闭右开) + for (; j > starty; j--) { + res[count++] = matrix[i][j]; + } + // 模拟填充左列从下到上(左闭右开) + for (; i > startx; i--) { + res[count++] = matrix[i][starty]; + } + + // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) + startx++; + starty++; + + // offset 控制每一圈里每一条边遍历的长度 + offset += 2; + } + + // 如果min(rows, columns)为奇数的话,需要单独给矩阵最中间的位置赋值 + if (min(rows, columns) % 2) { + if(rows > columns){ + for (int i = mid; i < mid + rows - columns + 1; ++i) { + res[count++] = matrix[i][mid]; + } + + } else { + for (int i = mid; i < mid + columns - rows + 1; ++i) { + res[count++] = matrix[mid][i]; + } + } + } + return res; + } +}; +``` + +## 类似题目 + +* [59.螺旋矩阵II](https://leetcode.cn/problems/spiral-matrix-ii/) +* [剑指Offer 29.顺时针打印矩阵](https://leetcode.cn/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/) + +## 其他语言版本 + +### Java + +```java +class Solution { + public List spiralOrder(int[][] matrix) { + //存放数组的数 + List ans = new ArrayList<>(); + //列数 + int columns = matrix[0].length; + //行数 + int rows = matrix.length; + //遍历起点 + int start = 0; + //循环的次数 行数和列数中的最小值除以二 + int loop = Math.min(rows,columns) / 2; + //未遍历的中间列(行)的列(行)下标 + int mid = loop; + //终止条件 + int offSet = 1; + int i,j; + while(loop-- > 0) { + //初始化起点 + i = j = start; + + //从左往右 + for(; j < columns - offSet; j++) + ans.add(matrix[i][j]); + + //从上往下 + for(; i < rows - offSet; i++) + ans.add(matrix[i][j]); + + //从右往左 + for(; j > start; j--) + ans.add(matrix[i][j]); + + //从下往上 + for(; i > start; i--) + ans.add(matrix[i][j]); + + //每循环一次 改变起点位置 + start++; + //终止条件改变 + offSet++; + } + + //如果行和列中的最小值是奇数 则会产生中间行或者中间列没有遍历 + if(Math.min(rows,columns) % 2 != 0) { + //行大于列则产生中间列 + if(rows > columns) { + //中间列的行的最大下标的下一位的下标为mid + rows - columns + 1 + for(int k = mid; k < mid + rows - columns + 1; k++) { + ans.add(matrix[k][mid]); + } + }else {//列大于等于行则产生中间行 + //中间行的列的最大下标的下一位的下标为mid + columns - rows + 1 + for(int k = mid; k < mid + columns - rows + 1; k++) { + ans.add(matrix[mid][k]); + } + } + } + return ans; + } +} +``` + +```java +class Solution { + public List spiralOrder(int[][] matrix) { + List res = new ArrayList<>(); // 存放结果 + if (matrix.length == 0 || matrix[0].length == 0) + return res; + int rows = matrix.length, columns = matrix[0].length; + int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 + int loop = 0; // 循环次数 + int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度 + while (loop < Math.min(rows, columns) / 2) { + int i = startx; + int j = starty; + // 模拟填充上行从左到右(左闭右开) + for (; j < columns - offset; j++) { + res.add(matrix[i][j]); + } + // 模拟填充右列从上到下(左闭右开) + for (; i < rows - offset; i++) { + res.add(matrix[i][j]); + } + // 模拟填充下行从右到左(左闭右开) + for (; j > starty; j--) { + res.add(matrix[i][j]); + } + // 模拟填充左列从下到上(左闭右开) + for (; i > startx; i--) { + res.add(matrix[i][j]); + } + + // 起始位置加1 循环次数加1 并控制每条边遍历的长度 + startx++; + starty++; + offset++; + loop++; + } + + // 如果列或行中的最小值为奇数 则一定有未遍历的部分 + // 可以自行画图理解 + if (Math.min(rows, columns) % 2 == 1) { + // 当行大于列时 未遍历的部分是列 + // (startx, starty)即下一个要遍历位置 从该位置出发 遍历完未遍历的列 + // 遍历次数为rows - columns + 1 + if (rows > columns) { + for (int i = 0; i < rows - columns + 1; i++) { + res.add(matrix[startx++][starty]); + } + } else { + // 此处与上面同理 遍历完未遍历的行 + for (int i = 0; i < columns - rows + 1; i++) { + res.add(matrix[startx][starty++]); + } + } + } + + return res; + } +} +``` + +### JavaScript +``` +/** + * @param {number[][]} matrix + * @return {number[]} + */ +var spiralOrder = function(matrix) { + let m = matrix.length + let n = matrix[0].length + + let startX = startY = 0 + let i = 0 + let arr = new Array(m*n).fill(0) + let offset = 1 + let loop = mid = Math.floor(Math.min(m,n) / 2) + while (loop--) { + let row = startX + let col = startY + // --> + for (; col < n + startY - offset; col++) { + arr[i++] = matrix[row][col] + } + // down + for (; row < m + startX - offset; row++) { + arr[i++] = matrix[row][col] + } + // <-- + for (; col > startY; col--) { + arr[i++] = matrix[row][col] + } + for (; row > startX; row--) { + arr[i++] = matrix[row][col] + } + startX++ + startY++ + offset += 2 + } + if (Math.min(m, n) % 2 === 1) { + if (n > m) { + for (let j = mid; j < mid + n - m + 1; j++) { + arr[i++] = matrix[mid][j] + } + } else { + for (let j = mid; j < mid + m - n + 1; j++) { + arr[i++] = matrix[j][mid] + } + } + } + return arr +}; +``` +### Python + +```python +class Solution(object): + def spiralOrder(self, matrix): + """ + :type matrix: List[List[int]] + :rtype: List[int] + """ + if len(matrix) == 0 or len(matrix[0]) == 0 : # 判定List是否为空 + return [] + row, col = len(matrix), len(matrix[0]) # 行数,列数 + loop = min(row, col) // 2 # 循环轮数 + stx, sty = 0, 0 # 起始x,y坐标 + i, j =0, 0 + count = 0 # 计数 + offset = 1 # 每轮减少的格子数 + result = [0] * (row * col) + while loop>0 :# 左闭右开 + i, j = stx, sty + while j < col - offset : # 从左到右 + result[count] = matrix[i][j] + count += 1 + j += 1 + while i < row - offset : # 从上到下 + result[count] = matrix[i][j] + count += 1 + i += 1 + while j>sty : # 从右到左 + result[count] = matrix[i][j] + count += 1 + j -= 1 + while i>stx : # 从下到上 + result[count] = matrix[i][j] + count += 1 + i -= 1 + stx += 1 + sty += 1 + offset += 1 + loop -= 1 + if min(row, col) % 2 == 1 : # 判定是否需要填充多出来的一行 + i = stx + if row < col : + while i < stx + col - row + 1 : + result[count] = matrix[stx][i] + count += 1 + i += 1 + else : + while i < stx + row - col + 1 : + result[count] = matrix[i][stx] + count += 1 + i += 1 + return result +``` + +版本二:定义四个边界 +```python +class Solution(object): + def spiralOrder(self, matrix): + """ + :type matrix: List[List[int]] + :rtype: List[int] + """ + if not matrix: + return [] + + rows = len(matrix) + cols = len(matrix[0]) + top, bottom, left, right = 0, rows - 1, 0, cols - 1 + print_list = [] + + while top <= bottom and left <= right: + # 从左到右 + for i in range(left, right + 1): + print_list.append(matrix[top][i]) + top += 1 + + # 从上到下 + for i in range(top, bottom + 1): + print_list.append(matrix[i][right]) + right -= 1 + + # 从右到左 + if top <= bottom: + for i in range(right, left - 1, -1): + print_list.append(matrix[bottom][i]) + bottom -= 1 + + # 从下到上 + if left <= right: + for i in range(bottom, top - 1, -1): + print_list.append(matrix[i][left]) + left += 1 + + return print_list +``` + +### Go + +```go +func spiralOrder(matrix [][]int) []int { + rows := len(matrix) + if rows == 0 { + return []int{} + } + columns := len(matrix[0]) + if columns == 0 { + return []int{} + } + res := make([]int, rows * columns) + startx, starty := 0, 0 // 定义每循环一个圈的起始位置 + loop := min(rows, columns) / 2 + mid := min(rows, columns) / 2 + count := 0 // 用来给矩阵中每一个空格赋值 + offset := 1 // 每一圈循环,需要控制每一条边遍历的长度 + for loop > 0 { + i, j := startx, starty + + // 模拟填充上行从左到右(左闭右开) + for ; j < starty + columns - offset; j++ { + res[count] = matrix[startx][j] + count++ + } + // 模拟填充右列从上到下(左闭右开) + for ; i < startx + rows - offset; i++ { + res[count] = matrix[i][j] + count++ + } + // 模拟填充下行从右到左(左闭右开) + for ; j > starty; j-- { + res[count] = matrix[i][j] + count++ + } + // 模拟填充左列从下到上(左闭右开) + for ; i > startx; i-- { + res[count] = matrix[i][starty] + count++ + } + + // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) + startx++ + starty++ + + // offset 控制每一圈里每一条边遍历的长度 + offset += 2 + loop-- + } + + // 如果min(rows, columns)为奇数的话,需要单独给矩阵最中间的位置赋值 + if min(rows, columns) % 2 == 1 { + if rows > columns { + for i := mid; i < mid + rows - columns + 1; i++ { + res[count] = matrix[i][mid] + count++ + } + } else { + for i := mid; i < mid + columns - rows + 1; i++ { + res[count] = matrix[mid][i] + count++ + } + } + } + return res +} + +func min(x, y int) int { + if x < y { + return x + } + return y +} +``` + + diff --git "a/problems/0055.\350\267\263\350\267\203\346\270\270\346\210\217.md" "b/problems/0055.\350\267\263\350\267\203\346\270\270\346\210\217.md" new file mode 100755 index 0000000000..513fc2e340 --- /dev/null +++ "b/problems/0055.\350\267\263\350\267\203\346\270\270\346\210\217.md" @@ -0,0 +1,293 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 55. 跳跃游戏 + +[力扣题目链接](https://leetcode.cn/problems/jump-game/) + +给定一个非负整数数组,你最初位于数组的第一个位置。 + +数组中的每个元素代表你在该位置可以跳跃的最大长度。 + +判断你是否能够到达最后一个位置。 + +示例  1: + +- 输入: [2,3,1,1,4] +- 输出: true +- 解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 + +示例  2: + +- 输入: [3,2,1,0,4] +- 输出: false +- 解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,怎么跳跃不重要,关键在覆盖范围 | LeetCode:55.跳跃游戏](https://www.bilibili.com/video/BV1VG4y1X7kB),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +刚看到本题一开始可能想:当前位置元素如果是 3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢? + +其实跳几步无所谓,关键在于可跳的覆盖范围! + +不一定非要明确一次究竟跳几步,每次取最大的跳跃步数,这个就是可以跳跃的覆盖范围。 + +这个范围内,别管是怎么跳的,反正一定可以跳过来。 + +**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!** + +每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。 + +**贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点**。 + +局部最优推出全局最优,找不出反例,试试贪心! + +如图: + +![](https://file1.kamacoder.com/i/algo/20230203105634.png) + + +i 每次移动只能在 cover 的范围内移动,每移动一个元素,cover 得到该元素数值(新的覆盖范围)的补充,让 i 继续移动下去。 + +而 cover 每次只取 max(该元素数值补充后的范围, cover 本身范围)。 + +如果 cover 大于等于了终点下标,直接 return true 就可以了。 + +C++代码如下: + +```CPP +class Solution { +public: + bool canJump(vector& nums) { + int cover = 0; + if (nums.size() == 1) return true; // 只有一个元素,就是能达到 + for (int i = 0; i <= cover; i++) { // 注意这里是小于等于cover + cover = max(i + nums[i], cover); + if (cover >= nums.size() - 1) return true; // 说明可以覆盖到终点了 + } + return false; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + +## 总结 + +这道题目关键点在于:不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。 + +大家可以看出思路想出来了,代码还是非常简单的。 + +一些同学可能感觉,我在讲贪心系列的时候,题目和题目之间貌似没有什么联系? + +**是真的就是没什么联系,因为贪心无套路**!没有个整体的贪心框架解决一系列问题,只能是接触各种类型的题目锻炼自己的贪心思维! + +## 其他语言版本 + +### Java + +```Java +class Solution { + public boolean canJump(int[] nums) { + if (nums.length == 1) { + return true; + } + //覆盖范围, 初始覆盖范围应该是0,因为下面的迭代是从下标0开始的 + int coverRange = 0; + //在覆盖范围内更新最大的覆盖范围 + for (int i = 0; i <= coverRange; i++) { + coverRange = Math.max(coverRange, i + nums[i]); + if (coverRange >= nums.length - 1) { + return true; + } + } + return false; + } +} +``` + +### Python + +```python +class Solution: + def canJump(self, nums: List[int]) -> bool: + cover = 0 + if len(nums) == 1: return True + i = 0 + # python不支持动态修改for循环中变量,使用while循环代替 + while i <= cover: + cover = max(i + nums[i], cover) + if cover >= len(nums) - 1: return True + i += 1 + return False +``` + +```python +## for循环 +class Solution: + def canJump(self, nums: List[int]) -> bool: + cover = 0 + if len(nums) == 1: return True + for i in range(len(nums)): + if i <= cover: + cover = max(i + nums[i], cover) + if cover >= len(nums) - 1: return True + return False +``` + +```python +## 基于当前最远可到达位置判断 +class Solution: + def canJump(self, nums: List[int]) -> bool: + far = nums[0] + for i in range(len(nums)): + # 要考虑两个情况 + # 1. i <= far - 表示 当前位置i 可以到达 + # 2. i > far - 表示 当前位置i 无法到达 + if i > far: + return False + far = max(far, nums[i]+i) + # 如果循环正常结束,表示最后一个位置也可以到达,否则会在中途直接退出 + # 关键点在于,要想明白其实列表中的每个位置都是需要验证能否到达的 + return True +``` + +### Go + +```go +// 贪心 +func canJump(nums []int) bool { + cover := 0 + n := len(nums)-1 + for i := 0; i <= cover; i++ { // 每次与覆盖值比较 + cover = max(i+nums[i], cover) //每走一步都将 cover 更新为最大值 + if cover >= n { + return true + } + } + return false +} +func max(a, b int ) int { + if a > b { + return a + } + return b +} +``` + +### JavaScript + +```Javascript +var canJump = function(nums) { + if(nums.length === 1) return true + let cover = 0 + for(let i = 0; i <= cover; i++) { + cover = Math.max(cover, i + nums[i]) + if(cover >= nums.length - 1) { + return true + } + } + return false +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn can_jump(nums: Vec) -> bool { + if nums.len() == 1 { + return true; + } + let (mut i, mut cover) = (0, 0); + while i <= cover { + cover = (i + nums[i] as usize).max(cover); + if cover >= nums.len() - 1 { + return true; + } + i += 1; + } + false + } +} +``` + +### C + +```c +#define max(a, b) (((a) > (b)) ? (a) : (b)) + +bool canJump(int* nums, int numsSize){ + int cover = 0; + + int i; + // 只可能获取cover范围中的步数,所以i<=cover + for(i = 0; i <= cover; ++i) { + // 更新cover为从i出发能到达的最大值/cover的值中较大值 + cover = max(i + nums[i], cover); + + // 若更新后cover可以到达最后的元素,返回true + if(cover >= numsSize - 1) + return true; + } + + return false; +} +``` + +### TypeScript + +```typescript +function canJump(nums: number[]): boolean { + let farthestIndex: number = 0; + let cur: number = 0; + while (cur <= farthestIndex) { + farthestIndex = Math.max(farthestIndex, cur + nums[cur]); + if (farthestIndex >= nums.length - 1) return true; + cur++; + } + return false; +} +``` + +### Scala + +```scala +object Solution { + def canJump(nums: Array[Int]): Boolean = { + var cover = 0 + if (nums.length == 1) return true // 如果只有一个元素,那么必定到达 + var i = 0 + while (i <= cover) { // i表示下标,当前只能够走cover步 + cover = math.max(i + nums(i), cover) + if (cover >= nums.length - 1) return true // 说明可以覆盖到终点,直接返回 + i += 1 + } + false // 如果上面没有返回就是跳不到 + } +} +``` +### C# +```csharp +public class Solution +{ + public bool CanJump(int[] nums) + { + int cover = 0; + if (nums.Length == 1) return true; + for (int i = 0; i <= cover; i++) + { + cover = Math.Max(i + nums[i], cover); + if (cover >= nums.Length - 1) return true; + } + return false; + } +} +``` + diff --git "a/problems/0056.\345\220\210\345\271\266\345\214\272\351\227\264.md" "b/problems/0056.\345\220\210\345\271\266\345\214\272\351\227\264.md" new file mode 100755 index 0000000000..24a97f6c5a --- /dev/null +++ "b/problems/0056.\345\220\210\345\271\266\345\214\272\351\227\264.md" @@ -0,0 +1,405 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 56. 合并区间 + +[力扣题目链接](https://leetcode.cn/problems/merge-intervals/) + +给出一个区间的集合,请合并所有重叠的区间。 + +示例 1: +* 输入: intervals = [[1,3],[2,6],[8,10],[15,18]] +* 输出: [[1,6],[8,10],[15,18]] +* 解释: 区间 [1,3] 和 [2,6] 重叠, 将它们合并为 [1,6]. + +示例 2: +* 输入: intervals = [[1,4],[4,5]] +* 输出: [[1,5]] +* 解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。 +* 注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,合并区间有细节!LeetCode:56.合并区间](https://www.bilibili.com/video/BV1wx4y157nD),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +本题的本质其实还是判断重叠区间问题。 + +大家如果认真做题的话,话发现和我们刚刚讲过的[452. 用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html) 和 [435. 无重叠区间](https://programmercarl.com/0435.无重叠区间.html) 都是一个套路。 + +这几道题都是判断区间重叠,区别就是判断区间重叠后的逻辑,本题是判断区间重贴后要进行区间合并。 + +所以一样的套路,先排序,让所有的相邻区间尽可能的重叠在一起,按左边界,或者右边界排序都可以,处理逻辑稍有不同。 + +按照左边界从小到大排序之后,如果 `intervals[i][0] <= intervals[i - 1][1]` 即intervals[i]的左边界 <= intervals[i - 1]的右边界,则一定有重叠。(本题相邻区间也算重贴,所以是<=) + +这么说有点抽象,看图:(**注意图中区间都是按照左边界排序之后了**) + +![56.合并区间](https://file1.kamacoder.com/i/algo/20201223200632791.png) + +知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢? + +其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。 + +C++代码如下: + +```CPP +class Solution { +public: + vector> merge(vector>& intervals) { + vector> result; + if (intervals.size() == 0) return result; // 区间集合为空直接返回 + // 排序的参数使用了lambda表达式 + sort(intervals.begin(), intervals.end(), [](const vector& a, const vector& b){return a[0] < b[0];}); + + // 第一个区间就可以放进结果集里,后面如果重叠,在result上直接合并 + result.push_back(intervals[0]); + + for (int i = 1; i < intervals.size(); i++) { + if (result.back()[1] >= intervals[i][0]) { // 发现重叠区间 + // 合并区间,只更新右边界就好,因为result.back()的左边界一定是最小值,因为我们按照左边界排序的 + result.back()[1] = max(result.back()[1], intervals[i][1]); + } else { + result.push_back(intervals[i]); // 区间不重叠 + } + } + return result; + } +}; +``` + +* 时间复杂度: O(nlogn) +* 空间复杂度: O(logn),排序需要的空间开销 + + +## 其他语言版本 + + +### Java +```java + +/** +时间复杂度 : O(NlogN) 排序需要O(NlogN) +空间复杂度 : O(logN) java 的内置排序是快速排序 需要 O(logN)空间 + +*/ +class Solution { + public int[][] merge(int[][] intervals) { + List res = new LinkedList<>(); + //按照左边界排序 + Arrays.sort(intervals, (x, y) -> Integer.compare(x[0], y[0])); + //initial start 是最小左边界 + int start = intervals[0][0]; + int rightmostRightBound = intervals[0][1]; + for (int i = 1; i < intervals.length; i++) { + //如果左边界大于最大右边界 + if (intervals[i][0] > rightmostRightBound) { + //加入区间 并且更新start + res.add(new int[]{start, rightmostRightBound}); + start = intervals[i][0]; + rightmostRightBound = intervals[i][1]; + } else { + //更新最大右边界 + rightmostRightBound = Math.max(rightmostRightBound, intervals[i][1]); + } + } + res.add(new int[]{start, rightmostRightBound}); + return res.toArray(new int[res.size()][]); + } +} + +``` +```java +// 版本2 +class Solution { + public int[][] merge(int[][] intervals) { + LinkedList res = new LinkedList<>(); + Arrays.sort(intervals, (o1, o2) -> Integer.compare(o1[0], o2[0])); + res.add(intervals[0]); + for (int i = 1; i < intervals.length; i++) { + if (intervals[i][0] <= res.getLast()[1]) { + int start = res.getLast()[0]; + int end = Math.max(intervals[i][1], res.getLast()[1]); + res.removeLast(); + res.add(new int[]{start, end}); + } + else { + res.add(intervals[i]); + } + } + return res.toArray(new int[res.size()][]); + } +} +``` + +### Python +```python +class Solution: + def merge(self, intervals): + result = [] + if len(intervals) == 0: + return result # 区间集合为空直接返回 + + intervals.sort(key=lambda x: x[0]) # 按照区间的左边界进行排序 + + result.append(intervals[0]) # 第一个区间可以直接放入结果集中 + + for i in range(1, len(intervals)): + if result[-1][1] >= intervals[i][0]: # 发现重叠区间 + # 合并区间,只需要更新结果集最后一个区间的右边界,因为根据排序,左边界已经是最小的 + result[-1][1] = max(result[-1][1], intervals[i][1]) + else: + result.append(intervals[i]) # 区间不重叠 + + return result + +``` + +### Go +```go +func merge(intervals [][]int) [][]int { + sort.Slice(intervals, func(i, j int) bool { + return intervals[i][0] < intervals[j][0] + }) + res := make([][]int, 0, len(intervals)) + left, right := intervals[0][0], intervals[0][1] + for i := 1; i < len(intervals); i++ { + if right < intervals[i][0] { + res = append(res, []int{left, right}) + left, right = intervals[i][0], intervals[i][1] + } else { + right = max(right, intervals[i][1]) + } + } + res = append(res, []int{left, right}) // 将最后一个区间放入 + return res +} +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` +```go +// 版本2 +func merge(intervals [][]int) [][]int { + if len(intervals) == 1 { + return intervals + } + sort.Slice(intervals, func(i, j int) bool { + return intervals[i][0] < intervals[j][0] + }) + res := make([][]int, 0) + res = append(res, intervals[0]) + for i := 1; i < len(intervals); i++ { + if intervals[i][0] <= res[len(res)-1][1]{ + res[len(res)-1][1] = max56(res[len(res)-1][1],intervals[i][1]) + } else { + res = append(res, intervals[i]) + } + } + return res +} +func max56(a, b int) int { + if a > b { + return a + } + return b +} +``` + + +### JavaScript +```javascript +var merge = function (intervals) { + intervals.sort((a, b) => a[0] - b[0]); + let prev = intervals[0] + let result = [] + for(let i =0; i prev[1]){ + result.push(prev) + prev = cur + }else{ + prev[1] = Math.max(cur[1],prev[1]) + } + } + result.push(prev) + return result +}; +``` +版本二:左右区间 +```javascript +/** + * @param {number[][]} intervals + * @return {number[][]} + */ +var merge = function(intervals) { + let n = intervals.length; + if ( n < 2) return intervals; + intervals.sort((a, b) => a[0]- b[0]); + let res = [], + left = intervals[0][0], + right = intervals[0][1]; + for (let i = 1; i < n; i++) { + if (intervals[i][0] > right) { + res.push([left, right]); + left = intervals[i][0]; + right = intervals[i][1]; + } else { + right = Math.max(intervals[i][1], right); + } + } + res.push([left, right]); + return res; +}; +``` + +### TypeScript + +```typescript +function merge(intervals: number[][]): number[][] { + const resArr: number[][] = []; + intervals.sort((a, b) => a[0] - b[0]); + resArr[0] = [...intervals[0]]; // 避免修改原intervals + for (let i = 1, length = intervals.length; i < length; i++) { + let interval: number[] = intervals[i]; + let last: number[] = resArr[resArr.length - 1]; + if (interval[0] <= last[1]) { + last[1] = Math.max(interval[1], last[1]); + } else { + resArr.push([...intervals[i]]); + } + } + return resArr; +}; +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def merge(intervals: Array[Array[Int]]): Array[Array[Int]] = { + var res = mutable.ArrayBuffer[Array[Int]]() + + // 排序 + var interval = intervals.sortWith((a, b) => { + a(0) < b(0) + }) + + var left = interval(0)(0) + var right = interval(0)(1) + + for (i <- 1 until interval.length) { + if (interval(i)(0) <= right) { + left = math.min(left, interval(i)(0)) + right = math.max(right, interval(i)(1)) + } else { + res.append(Array[Int](left, right)) + left = interval(i)(0) + right = interval(i)(1) + } + } + res.append(Array[Int](left, right)) + res.toArray // 返回res的Array形式 + } +} +``` + +### Rust + +```Rust +impl Solution { + pub fn merge(mut intervals: Vec>) -> Vec> { + let mut res = vec![]; + if intervals.is_empty() { + return res; + } + intervals.sort_by_key(|a| a[0]); + res.push(intervals[0].clone()); + for interval in intervals.into_iter().skip(1) { + let res_last_ele = res.last_mut().unwrap(); + if res_last_ele[1] >= interval[0] { + res_last_ele[1] = interval[1].max(res_last_ele[1]); + } else { + res.push(interval); + } + } + res + } +} +``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +// 根据左边界进行排序 +int cmp(const void * var1, const void * var2){ + int *v1 = *(int **) var1; + int *v2 = *(int **) var2; + return v1[0] - v2[0]; +} + +int** merge(int** intervals, int intervalsSize, int* intervalsColSize, int* returnSize, int** returnColumnSizes) { + int ** result = malloc(sizeof (int *) * intervalsSize); + * returnColumnSizes = malloc(sizeof (int ) * intervalsSize); + for(int i = 0; i < intervalsSize; i++){ + result[i] = malloc(sizeof (int ) * 2); + } + qsort(intervals, intervalsSize, sizeof (int *), cmp); + int count = 0; + for(int i = 0; i < intervalsSize; i++){ + // 记录区间的左右边界 + int L = intervals[i][0], R = intervals[i][1]; + // 如果count为0或者前一区间的右区间小于此时的左边,加入结果中 + if (count == 0 || result[count - 1][1] < L) { + returnColumnSizes[0][count] = 2; + result[count][0] = L; + result[count][1] = R; + count++; + } + else{ // 更新右边界的值 + result[count - 1][1] = max(R, result[count - 1][1]); + } + } + *returnSize = count; + return result; +} +``` + + + +### C# + +```csharp +public class Solution +{ + public int[][] Merge(int[][] intervals) + { + if (intervals.Length == 0) + return intervals; + Array.Sort(intervals, (a, b) => a[0] - b[0]); + List> res = new List>(); + res.Add(intervals[0].ToList()); + for (int i = 1; i < intervals.Length; i++) + { + if (res[res.Count - 1][1] >= intervals[i][0]) + { + res[res.Count - 1][1] = Math.Max(res[res.Count - 1][1], intervals[i][1]); + } + else + { + res.Add(intervals[i].ToList()); + } + } + return res.Select(x => x.ToArray()).ToArray(); + } +} +``` + diff --git "a/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" "b/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" new file mode 100755 index 0000000000..927df1c6c1 --- /dev/null +++ "b/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" @@ -0,0 +1,829 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 59.螺旋矩阵II + +[力扣题目链接](https://leetcode.cn/problems/spiral-matrix-ii/) + +给定一个正整数 n,生成一个包含 1 到 n^2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。 + +示例: + +输入: 3 +输出: +[ + [ 1, 2, 3 ], + [ 8, 9, 4 ], + [ 7, 6, 5 ] +] + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[拿下螺旋矩阵!LeetCode:59.螺旋矩阵II](https://www.bilibili.com/video/BV1SL4y1N7mV),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这道题目可以说在面试中出现频率较高的题目,**本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。** + +要如何画出这个螺旋排列的正方形矩阵呢? + +相信很多同学刚开始做这种题目的时候,上来就是一波判断猛如虎。 + +结果运行的时候各种问题,然后开始各种修修补补,最后发现改了这里那里有问题,改了那里这里又跑不起来了。 + +大家还记得我们在这篇文章[数组:每次遇到二分法,都是一看就会,一写就废](https://programmercarl.com/0704.二分查找.html)中讲解了二分法,提到如果要写出正确的二分法一定要坚持**循环不变量原则**。 + +而求解本题依然是要坚持循环不变量原则。 + +模拟顺时针画矩阵的过程: + +* 填充上行从左到右 +* 填充右列从上到下 +* 填充下行从右到左 +* 填充左列从下到上 + +由外向内一圈一圈这么画下去。 + +可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是**一进循环深似海,从此offer是路人**。 + +这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开右闭的原则,这样这一圈才能按照统一的规则画下来。 + +那么我按照左闭右开的原则,来画一圈,大家看一下: + +![](https://file1.kamacoder.com/i/algo/20220922102236.png) + +这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。 + +这也是坚持了每条边左闭右开的原则。 + +一些同学做这道题目之所以一直写不好,代码越写越乱。 + +就是因为在画每一条边的时候,一会左开右闭,一会左闭右闭,一会又来左闭右开,岂能不乱。 + +代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。 + +整体C++代码如下: + +```CPP +class Solution { +public: + vector> generateMatrix(int n) { + vector> res(n, vector(n, 0)); // 使用vector定义一个二维数组 + int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 + int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理 + int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2) + int count = 1; // 用来给矩阵中每一个空格赋值 + int offset = 1; // 需要控制每一条边遍历的长度,每次循环右边界收缩一位 + int i,j; + while (loop --) { + i = startx; + j = starty; + + // 下面开始的四个for就是模拟转了一圈 + // 模拟填充上行从左到右(左闭右开) + for (j; j < n - offset; j++) { + res[i][j] = count++; + } + // 模拟填充右列从上到下(左闭右开) + for (i; i < n - offset; i++) { + res[i][j] = count++; + } + // 模拟填充下行从右到左(左闭右开) + for (; j > starty; j--) { + res[i][j] = count++; + } + // 模拟填充左列从下到上(左闭右开) + for (; i > startx; i--) { + res[i][j] = count++; + } + + // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) + startx++; + starty++; + + // offset 控制每一圈里每一条边遍历的长度 + offset += 1; + } + + // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值 + if (n % 2) { + res[mid][mid] = count; + } + return res; + } +}; +``` + +* 时间复杂度 O(n^2): 模拟遍历二维矩阵的时间 +* 空间复杂度 O(1) + +## 类似题目 + +* [54.螺旋矩阵](https://leetcode.cn/problems/spiral-matrix/) +* [剑指Offer 29.顺时针打印矩阵](https://leetcode.cn/problems/shun-shi-zhen-da-yin-ju-zhen-lcof/) + + + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int[][] generateMatrix(int n) { + int[][] nums = new int[n][n]; + int startX = 0, startY = 0; // 每一圈的起始点 + int offset = 1; + int count = 1; // 矩阵中需要填写的数字 + int loop = 1; // 记录当前的圈数 + int i, j; // j 代表列, i 代表行; + + while (loop <= n / 2) { + + // 顶部 + // 左闭右开,所以判断循环结束时, j 不能等于 n - offset + for (j = startY; j < n - offset; j++) { + nums[startX][j] = count++; + } + + // 右列 + // 左闭右开,所以判断循环结束时, i 不能等于 n - offset + for (i = startX; i < n - offset; i++) { + nums[i][j] = count++; + } + + // 底部 + // 左闭右开,所以判断循环结束时, j != startY + for (; j > startY; j--) { + nums[i][j] = count++; + } + + // 左列 + // 左闭右开,所以判断循环结束时, i != startX + for (; i > startX; i--) { + nums[i][j] = count++; + } + startX++; + startY++; + offset++; + loop++; + } + if (n % 2 == 1) { // n 为奇数时,单独处理矩阵中心的值 + nums[startX][startY] = count; + } + return nums; + } +} + + +``` + +### python3: + +```python +class Solution: + def generateMatrix(self, n: int) -> List[List[int]]: + nums = [[0] * n for _ in range(n)] + startx, starty = 0, 0 # 起始点 + loop, mid = n // 2, n // 2 # 迭代次数、n为奇数时,矩阵的中心点 + count = 1 # 计数 + + for offset in range(1, loop + 1) : # 每循环一层偏移量加1,偏移量从1开始 + for i in range(starty, n - offset) : # 从左至右,左闭右开 + nums[startx][i] = count + count += 1 + for i in range(startx, n - offset) : # 从上至下 + nums[i][n - offset] = count + count += 1 + for i in range(n - offset, starty, -1) : # 从右至左 + nums[n - offset][i] = count + count += 1 + for i in range(n - offset, startx, -1) : # 从下至上 + nums[i][starty] = count + count += 1 + startx += 1 # 更新起始点 + starty += 1 + + if n % 2 != 0 : # n为奇数时,填充中心点 + nums[mid][mid] = count + return nums +``` + +版本二:定义四个边界 +```python +class Solution(object): + def generateMatrix(self, n): + if n <= 0: + return [] + + # 初始化 n x n 矩阵 + matrix = [[0]*n for _ in range(n)] + + # 初始化边界和起始值 + top, bottom, left, right = 0, n-1, 0, n-1 + num = 1 + + while top <= bottom and left <= right: + # 从左到右填充上边界 + for i in range(left, right + 1): + matrix[top][i] = num + num += 1 + top += 1 + + # 从上到下填充右边界 + for i in range(top, bottom + 1): + matrix[i][right] = num + num += 1 + right -= 1 + + # 从右到左填充下边界 + + for i in range(right, left - 1, -1): + matrix[bottom][i] = num + num += 1 + bottom -= 1 + + # 从下到上填充左边界 + + for i in range(bottom, top - 1, -1): + matrix[i][left] = num + num += 1 + left += 1 + + return matrix +``` + +### JavaScript: + +```javascript + +var generateMatrix = function(n) { + let startX = startY = 0; // 起始位置 + let loop = Math.floor(n/2); // 旋转圈数 + let mid = Math.floor(n/2); // 中间位置 + let offset = 1; // 控制每一层填充元素个数 + let count = 1; // 更新填充数字 + let res = new Array(n).fill(0).map(() => new Array(n).fill(0)); + + while (loop--) { + let row = startX, col = startY; + // 上行从左到右(左闭右开) + for (; col < n - offset; col++) { + res[row][col] = count++; + } + // 右列从上到下(左闭右开) + for (; row < n - offset; row++) { + res[row][col] = count++; + } + // 下行从右到左(左闭右开) + for (; col > startY; col--) { + res[row][col] = count++; + } + // 左列做下到上(左闭右开) + for (; row > startX; row--) { + res[row][col] = count++; + } + + // 更新起始位置 + startX++; + startY++; + + // 更新offset + offset += 1; + } + // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值 + if (n % 2 === 1) { + res[mid][mid] = count; + } + return res; +}; + + + +``` + +### TypeScript: + +```typescript +function generateMatrix(n: number): number[][] { + let loopNum: number = Math.floor(n / 2); + const resArr: number[][] = new Array(n).fill(1).map(i => new Array(n)); + let chunkNum: number = n - 1; + let startX: number = 0; + let startY: number = 0; + let value: number = 1; + let x: number, y: number; + while (loopNum--) { + x = startX; + y = startY; + while (x < startX + chunkNum) { + resArr[y][x] = value; + x++; + value++; + } + while (y < startY + chunkNum) { + resArr[y][x] = value; + y++; + value++; + } + while (x > startX) { + resArr[y][x] = value; + x--; + value++; + } + while (y > startY) { + resArr[y][x] = value; + y--; + value++; + } + startX++; + startY++; + chunkNum -= 2; + } + if (n % 2 === 1) { + resArr[startX][startY] = value; + } + return resArr; +}; +``` + +### Go: + +```go +package main + +import "fmt" + +func main() { + n := 3 + fmt.Println(generateMatrix(n)) +} + +func generateMatrix(n int) [][]int { + startx, starty := 0, 0 + var loop int = n / 2 + var center int = n / 2 + count := 1 + offset := 1 + res := make([][]int, n) + for i := 0; i < n; i++ { + res[i] = make([]int, n) + } + for loop > 0 { + i, j := startx, starty + + //行数不变 列数在变 + for j = starty; j < n-offset; j++ { + res[startx][j] = count + count++ + } + //列数不变是j 行数变 + for i = startx; i < n-offset; i++ { + res[i][j] = count + count++ + } + //行数不变 i 列数变 j-- + for ; j > starty; j-- { + res[i][j] = count + count++ + } + //列不变 行变 + for ; i > startx; i-- { + res[i][j] = count + count++ + } + startx++ + starty++ + offset++ + loop-- + } + if n%2 == 1 { + res[center][center] = n * n + } + return res +} +``` + +```go +func generateMatrix(n int) [][]int { + top, bottom := 0, n-1 + left, right := 0, n-1 + num := 1 + tar := n * n + matrix := make([][]int, n) + for i := 0; i < n; i++ { + matrix[i] = make([]int, n) + } + for num <= tar { + for i := left; i <= right; i++ { + matrix[top][i] = num + num++ + } + top++ + for i := top; i <= bottom; i++ { + matrix[i][right] = num + num++ + } + right-- + for i := right; i >= left; i-- { + matrix[bottom][i] = num + num++ + } + bottom-- + for i := bottom; i >= top; i-- { + matrix[i][left] = num + num++ + } + left++ + } + return matrix +} +``` + +### Swift: + +```swift +func generateMatrix(_ n: Int) -> [[Int]] { + var result = [[Int]](repeating: [Int](repeating: 0, count: n), count: n) + + var startRow = 0 + var startColumn = 0 + var loopCount = n / 2 + let mid = n / 2 + var count = 1 + var offset = 1 + var row: Int + var column: Int + + while loopCount > 0 { + row = startRow + column = startColumn + + for c in column ..< startColumn + n - offset { + result[startRow][c] = count + count += 1 + column += 1 + } + + for r in row ..< startRow + n - offset { + result[r][column] = count + count += 1 + row += 1 + } + + for _ in startColumn ..< column { + result[row][column] = count + count += 1 + column -= 1 + } + + for _ in startRow ..< row { + result[row][column] = count + count += 1 + row -= 1 + } + + startRow += 1 + startColumn += 1 + offset += 2 + loopCount -= 1 + } + + if (n % 2) != 0 { + result[mid][mid] = count + } + return result +} +``` + +### Rust: + +```rust +impl Solution { + pub fn generate_matrix(n: i32) -> Vec> { + let mut res = vec![vec![0; n as usize]; n as usize]; + let (mut startX, mut startY, mut offset): (usize, usize, usize) = (0, 0, 1); + let mut loopIdx = n/2; + let mid: usize = loopIdx as usize; + let mut count = 1; + let (mut i, mut j): (usize, usize) = (0, 0); + while loopIdx > 0 { + i = startX; + j = startY; + + while j < (startY + (n as usize) - offset) { + res[i][j] = count; + count += 1; + j += 1; + } + + while i < (startX + (n as usize) - offset) { + res[i][j] = count; + count += 1; + i += 1; + } + + while j > startY { + res[i][j] = count; + count += 1; + j -= 1; + } + + while i > startX { + res[i][j] = count; + count += 1; + i -= 1; + } + + startX += 1; + startY += 1; + offset += 2; + loopIdx -= 1; + } + + if(n % 2 == 1) { + res[mid][mid] = count; + } + res + } +} +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer $n + * @return Integer[][] + */ + function generateMatrix($n) { + // 初始化数组 + $res = array_fill(0, $n, array_fill(0, $n, 0)); + $mid = $loop = floor($n / 2); + $startX = $startY = 0; + $offset = 1; + $count = 1; + while ($loop > 0) { + $i = $startX; + $j = $startY; + for (; $j < $startY + $n - $offset; $j++) { + $res[$i][$j] = $count++; + } + for (; $i < $startX + $n - $offset; $i++) { + $res[$i][$j] = $count++; + } + for (; $j > $startY; $j--) { + $res[$i][$j] = $count++; + } + for (; $i > $startX; $i--) { + $res[$i][$j] = $count++; + } + $startX += 1; + $startY += 1; + $offset += 2; + $loop--; + } + if ($n % 2 == 1) { + $res[$mid][$mid] = $count; + } + return $res; + } +} +``` + +### C: + +```c +int** generateMatrix(int n, int* returnSize, int** returnColumnSizes){ + //初始化返回的结果数组的大小 + *returnSize = n; + *returnColumnSizes = (int*)malloc(sizeof(int) * n); + //初始化返回结果数组ans + int** ans = (int**)malloc(sizeof(int*) * n); + int i; + for(i = 0; i < n; i++) { + ans[i] = (int*)malloc(sizeof(int) * n); + (*returnColumnSizes)[i] = n; + } + + //设置每次循环的起始位置 + int startX = 0; + int startY = 0; + //设置二维数组的中间值,若n为奇数。需要最后在中间填入数字 + int mid = n / 2; + //循环圈数 + int loop = n / 2; + //偏移数 + int offset = 1; + //当前要添加的元素 + int count = 1; + + while(loop) { + int i = startX; + int j = startY; + //模拟上侧从左到右 + for(; j < startY + n - offset; j++) { + ans[startX][j] = count++; + } + //模拟右侧从上到下 + for(; i < startX + n - offset; i++) { + ans[i][j] = count++; + } + //模拟下侧从右到左 + for(; j > startY; j--) { + ans[i][j] = count++; + } + //模拟左侧从下到上 + for(; i > startX; i--) { + ans[i][j] = count++; + } + //偏移值每次加2 + offset+=2; + //遍历起始位置每次+1 + startX++; + startY++; + loop--; + } + //若n为奇数需要单独给矩阵中间赋值 + if(n%2) + ans[mid][mid] = count; + + return ans; +} +``` +### Scala: + +```scala +object Solution { + def generateMatrix(n: Int): Array[Array[Int]] = { + var res = Array.ofDim[Int](n, n) // 定义一个n*n的二维矩阵 + var num = 1 // 标志当前到了哪个数字 + var i = 0 // 横坐标 + var j = 0 // 竖坐标 + + while (num <= n * n) { + // 向右:当j不越界,并且下一个要填的数字是空白时 + while (j < n && res(i)(j) == 0) { + res(i)(j) = num // 当前坐标等于num + num += 1 // num++ + j += 1 // 竖坐标+1 + } + i += 1 // 下移一行 + j -= 1 // 左移一列 + + // 剩下的都同上 + + // 向下 + while (i < n && res(i)(j) == 0) { + res(i)(j) = num + num += 1 + i += 1 + } + i -= 1 + j -= 1 + + // 向左 + while (j >= 0 && res(i)(j) == 0) { + res(i)(j) = num + num += 1 + j -= 1 + } + i -= 1 + j += 1 + + // 向上 + while (i >= 0 && res(i)(j) == 0) { + res(i)(j) = num + num += 1 + i -= 1 + } + i += 1 + j += 1 + } + res + } +} +``` +### C#: + +```csharp +public int[][] GenerateMatrix(int n) +{ + // 参考Carl的代码随想录里面C++的思路 + // https://www.programmercarl.com/0059.%E8%9E%BA%E6%97%8B%E7%9F%A9%E9%98%B5II.html#%E6%80%9D%E8%B7%AF + int startX = 0, startY = 0; // 定义每循环一个圈的起始位置 + int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理 + int count = 1; // 用来给矩阵每个空格赋值 + int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2) + int offset = 1;// 需要控制每一条边遍历的长度,每次循环右边界收缩一位 + + // 构建result二维数组 + int[][] result = new int[n][]; + for (int k = 0; k < n; k++) + { + result[k] = new int[n]; + } + + int i = 0, j = 0; // [i,j] + while (loop > 0) + { + i = startX; + j = startY; + // 四个For循环模拟转一圈 + // 第一排,从左往右遍历,不取最右侧的值(左闭右开) + for (; j < n - offset; j++) + { + result[i][j] = count++; + } + // 右侧的第一列,从上往下遍历,不取最下面的值(左闭右开) + for (; i < n - offset; i++) + { + result[i][j] = count++; + } + + // 最下面的第一行,从右往左遍历,不取最左侧的值(左闭右开) + for (; j > startY; j--) + { + result[i][j] = count++; + } + + // 左侧第一列,从下往上遍历,不取最左侧的值(左闭右开) + for (; i > startX; i--) + { + result[i][j] = count++; + } + // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) + startX++; + startY++; + + // offset 控制每一圈里每一条边遍历的长度 + offset++; + loop--; + } + if (n % 2 == 1) + { + // n 为奇数 + result[mid][mid] = count; + } + return result; +} +``` + +### Ruby: +```ruby +def generate_matrix(n) + result = Array.new(n) { Array.new(n, 0) } + #循环次数 + loop_times = 0 + #步长 + step = n - 1 + val = 1 + + + while loop_times < n / 2 + #模拟从左向右 + for i in 0..step - 1 + #行数不变,列数变 + result[loop_times][i+loop_times] = val + val += 1 + end + + #模拟从上到下 + for i in 0..step - 1 + #列数不变,行数变 + result[i+loop_times][n-loop_times-1] = val + val += 1 + end + + #模拟从右到左 + for i in 0..step - 1 + #行数不变,列数变 + result[n-loop_times-1][n-loop_times-i-1] = val + val += 1 + end + + #模拟从下到上 + for i in 0..step - 1 + #列数不变,行数变 + result[n-loop_times-i-1][loop_times] = val + val += 1 + end + + loop_times += 1 + step -= 2 + end + + #如果是奇数,则填充最后一个元素 + result[n/2][n/2] = n**2 if n % 2 + + return result + +end +``` + diff --git "a/problems/0062.\344\270\215\345\220\214\350\267\257\345\276\204.md" "b/problems/0062.\344\270\215\345\220\214\350\267\257\345\276\204.md" new file mode 100755 index 0000000000..ac60767dce --- /dev/null +++ "b/problems/0062.\344\270\215\345\220\214\350\267\257\345\276\204.md" @@ -0,0 +1,616 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 62.不同路径 + +[力扣题目链接](https://leetcode.cn/problems/unique-paths/) + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 + +问总共有多少条不同的路径? + +示例 1: + +![](https://file1.kamacoder.com/i/algo/20210110174033215.png) + +* 输入:m = 3, n = 7 +* 输出:28 + +示例 2: + +* 输入:m = 2, n = 3 +* 输出:3 + +解释: 从左上角开始,总共有 3 条路径可以到达右下角。 + +1. 向右 -> 向右 -> 向下 +2. 向右 -> 向下 -> 向右 +3. 向下 -> 向右 -> 向右 + + +示例 3: + +* 输入:m = 7, n = 3 +* 输出:28 + +示例 4: + +* 输入:m = 3, n = 3 +* 输出:6 + +提示: + +* 1 <= m, n <= 100 +* 题目数据保证答案小于等于 2 * 10^9 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划中如何初始化很重要!| LeetCode:62.不同路径](https://www.bilibili.com/video/BV1ve4y1x7Eu/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 深搜 + +这道题目,刚一看最直观的想法就是用图论里的深搜,来枚举出来有多少种路径。 + +注意题目中说机器人每次只能向下或者向右移动一步,那么其实**机器人走过的路径可以抽象为一棵二叉树,而叶子节点就是终点!** + +如图举例: + +![62.不同路径](https://file1.kamacoder.com/i/algo/20201209113602700.png) + +此时问题就可以转化为求二叉树叶子节点的个数,代码如下: + +```CPP +class Solution { +private: + int dfs(int i, int j, int m, int n) { + if (i > m || j > n) return 0; // 越界了 + if (i == m && j == n) return 1; // 找到一种方法,相当于找到了叶子节点 + return dfs(i + 1, j, m, n) + dfs(i, j + 1, m, n); + } +public: + int uniquePaths(int m, int n) { + return dfs(1, 1, m, n); + } +}; +``` + +**大家如果提交了代码就会发现超时了!** + +来分析一下时间复杂度,这个深搜的算法,其实就是要遍历整个二叉树。 + +这棵树的深度其实就是m+n-1(深度按从1开始计算)。 + +那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已) + +所以上面深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的。 + +### 动态规划 + +机器人从(0 , 0) 位置出发,到(m - 1, n - 1)终点。 + +按照动规五部曲来分析: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。 + + +2. 确定递推公式 + +想要求dp[i][j],只能有两个方向来推导出来,即dp[i - 1][j] 和 dp[i][j - 1]。 + +此时在回顾一下 dp[i - 1][j] 表示啥,是从(0, 0)的位置到(i - 1, j)有几条路径,dp[i][j - 1]同理。 + +那么很自然,dp[i][j] = dp[i - 1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。 + +3. dp数组的初始化 + +如何初始化呢,首先dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。 + +所以初始化代码为: + +``` +for (int i = 0; i < m; i++) dp[i][0] = 1; +for (int j = 0; j < n; j++) dp[0][j] = 1; +``` + +4. 确定遍历顺序 + +这里要看一下递推公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。 + +这样就可以保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值的。 + +5. 举例推导dp数组 + +如图所示: + +![62.不同路径1](https://file1.kamacoder.com/i/algo/20201209113631392.png) + +以上动规五部曲分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int uniquePaths(int m, int n) { + vector> dp(m, vector(n, 0)); + for (int i = 0; i < m; i++) dp[i][0] = 1; + for (int j = 0; j < n; j++) dp[0][j] = 1; + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[m - 1][n - 1]; + } +}; +``` + +* 时间复杂度:O(m × n) +* 空间复杂度:O(m × n) + +其实用一个一维数组(也可以理解是滚动数组)就可以了,但是不利于理解,可以优化点空间,建议先理解了二维,在理解一维,C++代码如下: + +```CPP +class Solution { +public: + int uniquePaths(int m, int n) { + vector dp(n); + for (int i = 0; i < n; i++) dp[i] = 1; + for (int j = 1; j < m; j++) { + for (int i = 1; i < n; i++) { + dp[i] += dp[i - 1]; + } + } + return dp[n - 1]; + } +}; +``` + +* 时间复杂度:O(m × n) +* 空间复杂度:O(n) + +### 数论方法 + +在这个图中,可以看出一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。 + +![62.不同路径](https://file1.kamacoder.com/i/algo/20201209113602700-20230310120944078.png) + +在这m + n - 2 步中,一定有 m - 1 步是要向下走的,不用管什么时候向下走。 + +那么有几种走法呢? 可以转化为,给你m + n - 2个不同的数,随便取m - 1个数,有几种取法。 + +那么这就是一个组合问题了。 + +那么答案,如图所示: + +![62.不同路径2](https://file1.kamacoder.com/i/algo/20201209113725324.png) + +**求组合的时候,要防止两个int相乘溢出!** 所以不能把算式的分子都算出来,分母都算出来再做除法。 + +例如如下代码是不行的。 + +```CPP +class Solution { +public: + int uniquePaths(int m, int n) { + int numerator = 1, denominator = 1; + int count = m - 1; + int t = m + n - 2; + while (count--) numerator *= (t--); // 计算分子,此时分子就会溢出 + for (int i = 1; i <= m - 1; i++) denominator *= i; // 计算分母 + return numerator / denominator; + } +}; + +``` + +需要在计算分子的时候,不断除以分母,代码如下: + +```CPP +class Solution { +public: + int uniquePaths(int m, int n) { + long long numerator = 1; // 分子 + int denominator = m - 1; // 分母 + int count = m - 1; + int t = m + n - 2; + while (count--) { + numerator *= (t--); + while (denominator != 0 && numerator % denominator == 0) { + numerator /= denominator; + denominator--; + } + } + return numerator; + } +}; +``` + +* 时间复杂度:O(m) +* 空间复杂度:O(1) + +**计算组合问题的代码还是有难度的,特别是处理溢出的情况!** + +## 总结 + +本文分别给出了深搜,动规,数论三种方法。 + +深搜当然是超时了,顺便分析了一下使用深搜的时间复杂度,就可以看出为什么超时了。 + +然后在给出动规的方法,依然是使用动规五部曲,这次我们就要考虑如何正确的初始化了,初始化和遍历顺序其实也很重要! + + +## 其他语言版本 + + +### Java + +```java + /** + * 1. 确定dp数组下标含义 dp[i][j] 到每一个坐标可能的路径种类 + * 2. 递推公式 dp[i][j] = dp[i-1][j] dp[i][j-1] + * 3. 初始化 dp[i][0]=1 dp[0][i]=1 初始化横竖就可 + * 4. 遍历顺序 一行一行遍历 + * 5. 推导结果 。。。。。。。。 + * + * @param m + * @param n + * @return + */ + public static int uniquePaths(int m, int n) { + int[][] dp = new int[m][n]; + //初始化 + for (int i = 0; i < m; i++) { + dp[i][0] = 1; + } + for (int i = 0; i < n; i++) { + dp[0][i] = 1; + } + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = dp[i-1][j]+dp[i][j-1]; + } + } + return dp[m-1][n-1]; + } + +``` +状态压缩 +```java +class Solution { + public int uniquePaths(int m, int n) { + // 在二维dp数组中,当前值的计算只依赖正上方和正左方,因此可以压缩成一维数组。 + int[] dp = new int[n]; + // 初始化,第一行只能从正左方跳过来,所以只有一条路径。 + Arrays.fill(dp, 1); + for (int i = 1; i < m; i ++) { + // 第一列也只有一条路,不用迭代,所以从第二列开始 + for (int j = 1; j < n; j ++) { + dp[j] += dp[j - 1]; // dp[j] = dp[j] (正上方)+ dp[j - 1] (正左方) + } + } + return dp[n - 1]; + } +} +``` + +### Python +递归 +```python +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + if m == 1 or n == 1: + return 1 + return self.uniquePaths(m - 1, n) + self.uniquePaths(m, n - 1) + +``` +动态规划(版本一) +```python +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + # 创建一个二维列表用于存储唯一路径数 + dp = [[0] * n for _ in range(m)] + + # 设置第一行和第一列的基本情况 + for i in range(m): + dp[i][0] = 1 + for j in range(n): + dp[0][j] = 1 + + # 计算每个单元格的唯一路径数 + for i in range(1, m): + for j in range(1, n): + dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + + # 返回右下角单元格的唯一路径数 + return dp[m - 1][n - 1] + +``` +动态规划(版本二) +```python +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + # 创建一个一维列表用于存储每列的唯一路径数 + dp = [1] * n + + # 计算每个单元格的唯一路径数 + for j in range(1, m): + for i in range(1, n): + dp[i] += dp[i - 1] + + # 返回右下角单元格的唯一路径数 + return dp[n - 1] +``` +数论 +```python +class Solution: + def uniquePaths(self, m: int, n: int) -> int: + numerator = 1 # 分子 + denominator = m - 1 # 分母 + count = m - 1 # 计数器,表示剩余需要计算的乘积项个数 + t = m + n - 2 # 初始乘积项 + while count > 0: + numerator *= t # 计算乘积项的分子部分 + t -= 1 # 递减乘积项 + while denominator != 0 and numerator % denominator == 0: + numerator //= denominator # 约简分子 + denominator -= 1 # 递减分母 + count -= 1 # 计数器减1,继续下一项的计算 + return numerator # 返回最终的唯一路径数 + +``` +### Go + +动态规划 +```Go +func uniquePaths(m int, n int) int { + dp := make([][]int, m) + for i := range dp { + dp[i] = make([]int, n) + dp[i][0] = 1 + } + for j := 0; j < n; j++ { + dp[0][j] = 1 + } + for i := 1; i < m; i++ { + for j := 1; j < n; j++ { + dp[i][j] = dp[i-1][j] + dp[i][j-1] + } + } + return dp[m-1][n-1] +} +``` + +数论方法 +```Go +func uniquePaths(m int, n int) int { + numerator := 1 + denominator := m - 1 + count := m - 1 + t := m + n - 2 + for count > 0 { + numerator *= t + t-- + for denominator != 0 && numerator % denominator == 0 { + numerator /= denominator + denominator-- + } + count-- + } + return numerator +} +``` + +### JavaScript + +```Javascript +var uniquePaths = function(m, n) { + const dp = Array(m).fill().map(item => Array(n)) + + for (let i = 0; i < m; ++i) { + dp[i][0] = 1 + } + + for (let i = 0; i < n; ++i) { + dp[0][i] = 1 + } + + for (let i = 1; i < m; ++i) { + for (let j = 1; j < n; ++j) { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + } + } + return dp[m - 1][n - 1] +}; +``` + +>版本二:直接将dp数值值初始化为1 + +```javascript +/** + * @param {number} m + * @param {number} n + * @return {number} + */ +var uniquePaths = function(m, n) { + let dp = new Array(m).fill(1).map(() => new Array(n).fill(1)); + // dp[i][j] 表示到达(i,j) 点的路径数 + for (let i=1; i []); + for (let i = 0; i < m; i++) { + dp[i][0] = 1; + } + for (let i = 0; i < n; i++) { + dp[0][i] = 1; + } + for (let i = 1; i < m; i++) { + for (let j = 1; j < n; j++) { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[m - 1][n - 1]; +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn unique_paths(m: i32, n: i32) -> i32 { + let (m, n) = (m as usize, n as usize); + let mut dp = vec![vec![1; n]; m]; + for i in 1..m { + for j in 1..n { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + dp[m - 1][n - 1] + } +} +``` + +### C + +```c +//初始化dp数组 +int **initDP(int m, int n) { + //动态开辟dp数组 + int **dp = (int**)malloc(sizeof(int *) * m); + int i, j; + for(i = 0; i < m; ++i) { + dp[i] = (int *)malloc(sizeof(int) * n); + } + + //从0,0到i,0只有一种走法,所以dp[i][0]都是1,同理dp[0][j]也是1 + for(i = 0; i < m; ++i) + dp[i][0] = 1; + for(j = 0; j < n; ++j) + dp[0][j] = 1; + return dp; +} + +int uniquePaths(int m, int n){ + //dp数组,dp[i][j]代表从dp[0][0]到dp[i][j]有几种走法 + int **dp = initDP(m, n); + + int i, j; + //到达dp[i][j]只能从dp[i-1][j]和dp[i][j-1]出发 + //dp[i][j] = dp[i-1][j] + dp[i][j-1] + for(i = 1; i < m; ++i) { + for(j = 1; j < n; ++j) { + dp[i][j] = dp[i-1][j] + dp[i][j-1]; + } + } + int result = dp[m-1][n-1]; + free(dp); + return result; +} +``` + +滚动数组解法: + +```c +int uniquePaths(int m, int n){ + int i, j; + + // 初始化dp数组 + int *dp = (int*)malloc(sizeof(int) * n); + for (i = 0; i < n; ++i) + dp[i] = 1; + + for (j = 1; j < m; ++j) { + for (i = 1; i < n; ++i) { + // dp[i]为二维数组解法中dp[i-1][j]。dp[i-1]为二维数组解法中dp[i][j-1] + dp[i] += dp[i - 1]; + } + } + return dp[n - 1]; +} +``` + +### Scala + +```scala +object Solution { + def uniquePaths(m: Int, n: Int): Int = { + var dp = Array.ofDim[Int](m, n) + for (i <- 0 until m) dp(i)(0) = 1 + for (j <- 1 until n) dp(0)(j) = 1 + for (i <- 1 until m; j <- 1 until n) { + dp(i)(j) = dp(i - 1)(j) + dp(i)(j - 1) + } + dp(m - 1)(n - 1) + } +} +``` + +### c# +```csharp +// 二维数组 +public class Solution +{ + public int UniquePaths(int m, int n) + { + int[,] dp = new int[m, n]; + for (int i = 0; i < m; i++) dp[i, 0] = 1; + for (int j = 0; j < n; j++) dp[0, j] = 1; + for (int i = 1; i < m; i++) + { + for (int j = 1; j < n; j++) + { + dp[i, j] = dp[i - 1, j] + dp[i, j - 1]; + } + } + return dp[m - 1, n - 1]; + } +} +``` + +```csharp +// 一维数组 +public class Solution +{ + public int UniquePaths(int m, int n) + { + int[] dp = new int[n]; + for (int i = 0; i < n; i++) + dp[i] = 1; + for (int i = 1; i < m; i++) + for (int j = 1; j < n; j++) + dp[j] += dp[j - 1]; + return dp[n - 1]; + } +} +``` + + + + diff --git "a/problems/0063.\344\270\215\345\220\214\350\267\257\345\276\204II.md" "b/problems/0063.\344\270\215\345\220\214\350\267\257\345\276\204II.md" new file mode 100755 index 0000000000..f39afe8455 --- /dev/null +++ "b/problems/0063.\344\270\215\345\220\214\350\267\257\345\276\204II.md" @@ -0,0 +1,781 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 63. 不同路径 II + +[力扣题目链接](https://leetcode.cn/problems/unique-paths-ii/) + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 + +现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? + +![](https://file1.kamacoder.com/i/algo/20210111204901338.png) + +网格中的障碍物和空位置分别用 1 和 0 来表示。 + +示例 1: + +![](https://file1.kamacoder.com/i/algo/20210111204939971.png) + +* 输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] +* 输出:2 + 解释: +* 3x3 网格的正中间有一个障碍物。 +* 从左上角到右下角一共有 2 条不同的路径: + 1. 向右 -> 向右 -> 向下 -> 向下 + 2. 向下 -> 向下 -> 向右 -> 向右 + +示例 2: + +![](https://file1.kamacoder.com/i/algo/20210111205857918.png) + +* 输入:obstacleGrid = [[0,1],[0,0]] +* 输出:1 + +提示: + +* m == obstacleGrid.length +* n == obstacleGrid[i].length +* 1 <= m, n <= 100 +* obstacleGrid[i][j] 为 0 或 1 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,这次遇到障碍了| LeetCode:63. 不同路径 II](https://www.bilibili.com/video/BV1Ld4y1k7c6/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题相对于[62.不同路径](https://programmercarl.com/0062.不同路径.html) 就是有了障碍。 + +第一次接触这种题目的同学可能会有点懵,这有障碍了,应该怎么算呢? + +[62.不同路径](https://programmercarl.com/0062.不同路径.html)中我们已经详细分析了没有障碍的情况,有障碍的话,其实就是标记对应的dp table(dp数组)保持初始值(0)就可以了。 + +动规五部曲: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。 + +2. 确定递推公式 + +递推公式和62.不同路径一样,dp[i][j] = dp[i - 1][j] + dp[i][j - 1]。 + +但这里需要注意一点,因为有了障碍,(i, j)如果就是障碍的话应该就保持初始状态(初始状态为0)。 + +所以代码为: + +```cpp +if (obstacleGrid[i][j] == 0) { // 当(i, j)没有障碍的时候,再推导dp[i][j] + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; +} +``` + +3. dp数组如何初始化 + +在[62.不同路径](https://programmercarl.com/0062.不同路径.html)不同路径中我们给出如下的初始化: + +```cpp +vector> dp(m, vector(n, 0)); // 初始值为0 +for (int i = 0; i < m; i++) dp[i][0] = 1; +for (int j = 0; j < n; j++) dp[0][j] = 1; +``` + +因为从(0, 0)的位置到(i, 0)的路径只有一条,所以dp[i][0]一定为1,dp[0][j]也同理。 + +但如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。 + +如图: + +![63.不同路径II](https://file1.kamacoder.com/i/algo/20210104114513928.png) + +下标(0, j)的初始化情况同理。 + +所以本题初始化代码为: + +```CPP +vector> dp(m, vector(n, 0)); +for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1; +for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1; +``` + +**注意代码里for循环的终止条件,一旦遇到obstacleGrid[i][0] == 1的情况就停止dp[i][0]的赋值1的操作,dp[0][j]同理** + +4. 确定遍历顺序 + +从递归公式dp[i][j] = dp[i - 1][j] + dp[i][j - 1] 中可以看出,一定是从左到右一层一层遍历,这样保证推导dp[i][j]的时候,dp[i - 1][j] 和 dp[i][j - 1]一定是有数值。 + +代码如下: + +```CPP +for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + if (obstacleGrid[i][j] == 1) continue; + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } +} +``` + + +5. 举例推导dp数组 + +拿示例1来举例如题: + +![63.不同路径II1](https://file1.kamacoder.com/i/algo/20210104114548983.png) + +对应的dp table 如图: + +![63.不同路径II2](https://file1.kamacoder.com/i/algo/20210104114610256.png) + +如果这个图看不懂,建议再理解一下递归公式,然后照着文章中说的遍历顺序,自己推导一下! + +动规五部分分析完毕,对应C++代码如下: + +```CPP +class Solution { +public: + int uniquePathsWithObstacles(vector>& obstacleGrid) { + int m = obstacleGrid.size(); + int n = obstacleGrid[0].size(); + if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) //如果在起点或终点出现了障碍,直接返回0 + return 0; + vector> dp(m, vector(n, 0)); + for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1; + for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1; + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + if (obstacleGrid[i][j] == 1) continue; + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[m - 1][n - 1]; + } +}; +``` + +* 时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度 +* 空间复杂度:O(n × m) + + +同样我们给出空间优化版本: + +```CPP +class Solution { +public: + int uniquePathsWithObstacles(vector>& obstacleGrid) { + if (obstacleGrid[0][0] == 1) + return 0; + vector dp(obstacleGrid[0].size()); + for (int j = 0; j < dp.size(); ++j) + if (obstacleGrid[0][j] == 1) + dp[j] = 0; + else if (j == 0) + dp[j] = 1; + else + dp[j] = dp[j-1]; + + for (int i = 1; i < obstacleGrid.size(); ++i) + for (int j = 0; j < dp.size(); ++j){ + if (obstacleGrid[i][j] == 1) + dp[j] = 0; + else if (j != 0) + dp[j] = dp[j] + dp[j-1]; + } + return dp.back(); + } +}; +``` + +* 时间复杂度:O(n × m),n、m 分别为obstacleGrid 长度和宽度 +* 空间复杂度:O(m) + + +## 总结 + +本题是[62.不同路径](https://programmercarl.com/0062.不同路径.html)的障碍版,整体思路大体一致。 + +但就算是做过62.不同路径,在做本题也会有感觉遇到障碍无从下手。 + +其实只要考虑到,遇到障碍dp[i][j]保持0就可以了。 + +也有一些小细节,例如:初始化的部分,很容易忽略了障碍之后应该都是0的情况。 + + + +## 其他语言版本 + +### Java + +```java +class Solution { + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + int[][] dp = new int[m][n]; + + //如果在起点或终点出现了障碍,直接返回0 + if (obstacleGrid[m - 1][n - 1] == 1 || obstacleGrid[0][0] == 1) { + return 0; + } + + for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) { + dp[i][0] = 1; + } + for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) { + dp[0][j] = 1; + } + + for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = (obstacleGrid[i][j] == 0) ? dp[i - 1][j] + dp[i][j - 1] : 0; + } + } + return dp[m - 1][n - 1]; + } +} +``` + +```java +// 空间优化版本 +class Solution { + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int m = obstacleGrid.length; + int n = obstacleGrid[0].length; + int[] dp = new int[n]; + + for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) { + dp[j] = 1; + } + + for (int i = 1; i < m; i++) { + for (int j = 0; j < n; j++) { + if (obstacleGrid[i][j] == 1) { + dp[j] = 0; + } else if (j != 0) { + dp[j] += dp[j - 1]; + } + } + } + return dp[n - 1]; + } +} +``` + + +### Python +动态规划(版本一) +```python +class Solution: + def uniquePathsWithObstacles(self, obstacleGrid): + m = len(obstacleGrid) + n = len(obstacleGrid[0]) + if obstacleGrid[m - 1][n - 1] == 1 or obstacleGrid[0][0] == 1: + return 0 + dp = [[0] * n for _ in range(m)] + for i in range(m): + if obstacleGrid[i][0] == 0: # 遇到障碍物时,直接退出循环,后面默认都是0 + dp[i][0] = 1 + else: + break + for j in range(n): + if obstacleGrid[0][j] == 0: + dp[0][j] = 1 + else: + break + for i in range(1, m): + for j in range(1, n): + if obstacleGrid[i][j] == 1: + continue + dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + return dp[m - 1][n - 1] + +``` +动态规划(版本二) +```python +class Solution: + def uniquePathsWithObstacles(self, obstacleGrid): + m = len(obstacleGrid) # 网格的行数 + n = len(obstacleGrid[0]) # 网格的列数 + + if obstacleGrid[m - 1][n - 1] == 1 or obstacleGrid[0][0] == 1: + # 如果起点或终点有障碍物,直接返回0 + return 0 + + dp = [[0] * n for _ in range(m)] # 创建一个二维列表用于存储路径数 + + # 设置起点的路径数为1 + dp[0][0] = 1 if obstacleGrid[0][0] == 0 else 0 + + # 计算第一列的路径数 + for i in range(1, m): + if obstacleGrid[i][0] == 0: + dp[i][0] = dp[i - 1][0] + + # 计算第一行的路径数 + for j in range(1, n): + if obstacleGrid[0][j] == 0: + dp[0][j] = dp[0][j - 1] + + # 计算其他位置的路径数 + for i in range(1, m): + for j in range(1, n): + if obstacleGrid[i][j] == 1: + continue + dp[i][j] = dp[i - 1][j] + dp[i][j - 1] + + return dp[m - 1][n - 1] # 返回终点的路径数 + + +``` +动态规划(版本三) + +```python +class Solution: + def uniquePathsWithObstacles(self, obstacleGrid): + if obstacleGrid[0][0] == 1: + return 0 + + dp = [0] * len(obstacleGrid[0]) # 创建一个一维列表用于存储路径数 + + # 初始化第一行的路径数 + for j in range(len(dp)): + if obstacleGrid[0][j] == 1: + dp[j] = 0 + elif j == 0: + dp[j] = 1 + else: + dp[j] = dp[j - 1] + + # 计算其他行的路径数 + for i in range(1, len(obstacleGrid)): + for j in range(len(dp)): + if obstacleGrid[i][j] == 1: + dp[j] = 0 + elif j != 0: + dp[j] = dp[j] + dp[j - 1] + + return dp[-1] # 返回最后一个元素,即终点的路径数 + +``` +动态规划(版本四) + +```python +class Solution: + def uniquePathsWithObstacles(self, obstacleGrid): + if obstacleGrid[0][0] == 1: + return 0 + + m, n = len(obstacleGrid), len(obstacleGrid[0]) + + dp = [0] * n # 创建一个一维列表用于存储路径数 + + # 初始化第一行的路径数 + for j in range(n): + if obstacleGrid[0][j] == 1: + break + dp[j] = 1 + + # 计算其他行的路径数 + for i in range(1, m): + if obstacleGrid[i][0] == 1: + dp[0] = 0 + for j in range(1, n): + if obstacleGrid[i][j] == 1: + dp[j] = 0 + else: + dp[j] += dp[j - 1] + + return dp[-1] # 返回最后一个元素,即终点的路径数 + +``` + +动态规划(版本五) + +```python +class Solution: + def uniquePathsWithObstacles(self, obstacleGrid): + if obstacleGrid[0][0] == 1: + return 0 + + m, n = len(obstacleGrid), len(obstacleGrid[0]) + + dp = [0] * n # 创建一个一维列表用于存储路径数 + + # 初始化第一行的路径数 + for j in range(n): + if obstacleGrid[0][j] == 1: + break + dp[j] = 1 + + # 计算其他行的路径数 + for i in range(1, m): + if obstacleGrid[i][0] == 1: + dp[0] = 0 + for j in range(1, n): + if obstacleGrid[i][j] == 1: + dp[j] = 0 + continue + + dp[j] += dp[j - 1] + + return dp[-1] # 返回最后一个元素,即终点的路径数 + + +``` +### Go + +```go +func uniquePathsWithObstacles(obstacleGrid [][]int) int { + m, n := len(obstacleGrid), len(obstacleGrid[0]) + //如果在起点或终点出现了障碍,直接返回0 + if obstacleGrid[m-1][n-1] == 1 || obstacleGrid[0][0] == 1 { + return 0 + } + // 定义一个dp数组 + dp := make([][]int, m) + for i, _ := range dp { + dp[i] = make([]int, n) + } + // 初始化, 如果是障碍物, 后面的就都是0, 不用循环了 + for i := 0; i < m && obstacleGrid[i][0] == 0; i++ { + dp[i][0] = 1 + } + for i := 0; i < n && obstacleGrid[0][i] == 0; i++ { + dp[0][i] = 1 + } + // dp数组推导过程 + for i := 1; i < m; i++ { + for j := 1; j < n; j++ { + // 如果obstacleGrid[i][j]这个点是障碍物, 那么dp[i][j]保持为0 + if obstacleGrid[i][j] != 1 { + // 否则我们需要计算当前点可以到达的路径数 + dp[i][j] = dp[i-1][j] + dp[i][j-1] + } + } + } + return dp[m-1][n-1] +} +``` + +### JavaScript + +```Javascript +var uniquePathsWithObstacles = function(obstacleGrid) { + const m = obstacleGrid.length + const n = obstacleGrid[0].length + const dp = Array(m).fill().map(item => Array(n).fill(0)) + + for (let i = 0; i < m && obstacleGrid[i][0] === 0; ++i) { + dp[i][0] = 1 + } + + for (let i = 0; i < n && obstacleGrid[0][i] === 0; ++i) { + dp[0][i] = 1 + } + + for (let i = 1; i < m; ++i) { + for (let j = 1; j < n; ++j) { + dp[i][j] = obstacleGrid[i][j] === 1 ? 0 : dp[i - 1][j] + dp[i][j - 1] + } + } + + return dp[m - 1][n - 1] +}; + +// 版本二:内存优化,直接以原数组为dp数组 +var uniquePathsWithObstacles = function(obstacleGrid) { + const m = obstacleGrid.length; + const n = obstacleGrid[0].length; + for (let i = 0; i < m; i++) { + for (let j = 0; j < n; j++) { + if (obstacleGrid[i][j] === 0) { + // 不是障碍物 + if (i === 0) { + // 取左边的值 + obstacleGrid[i][j] = obstacleGrid[i][j - 1] ?? 1; + } else if (j === 0) { + // 取上边的值 + obstacleGrid[i][j] = obstacleGrid[i - 1]?.[j] ?? 1; + } else { + // 取左边和上边的和 + obstacleGrid[i][j] = obstacleGrid[i - 1][j] + obstacleGrid[i][j - 1]; + } + } else { + // 如果是障碍物,则路径为0 + obstacleGrid[i][j] = 0; + } + } + } + return obstacleGrid[m - 1][n - 1]; +}; +``` + + + +### TypeScript + +```typescript +function uniquePathsWithObstacles(obstacleGrid: number[][]): number { + /** + dp[i][j]: 到达(i, j)的路径数 + dp[0][*]: 用u表示第一个障碍物下标,则u之前为1,u之后(含u)为0 + dp[*][0]: 同上 + ... + dp[i][j]: obstacleGrid[i][j] === 1 ? 0 : dp[i-1][j] + dp[i][j-1]; + */ + const m: number = obstacleGrid.length; + const n: number = obstacleGrid[0].length; + const dp: number[][] = new Array(m).fill(0).map(_ => new Array(n).fill(0)); + for (let i = 0; i < m && obstacleGrid[i][0] === 0; i++) { + dp[i][0] = 1; + } + for (let i = 0; i < n && obstacleGrid[0][i] === 0; i++) { + dp[0][i] = 1; + } + for (let i = 1; i < m; i++) { + for (let j = 1; j < n; j++) { + if (obstacleGrid[i][j] === 1) continue; + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[m - 1][n - 1]; +}; +``` + +// 版本二: dp改為使用一維陣列,從終點開始遍歷 +```typescript +function uniquePathsWithObstacles(obstacleGrid: number[][]): number { + const m = obstacleGrid.length; + const n = obstacleGrid[0].length; + + const dp: number[] = new Array(n).fill(0); + dp[n - 1] = 1; + + // 由下而上,右而左進行遍歷 + for (let i = m - 1; i >= 0; i--) { + for (let j = n - 1; j >= 0; j--) { + if (obstacleGrid[i][j] === 1) dp[j] = 0; + else dp[j] = dp[j] + (dp[j + 1] || 0); + } + } + + return dp[0]; +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn unique_paths_with_obstacles(obstacle_grid: Vec>) -> i32 { + let m: usize = obstacle_grid.len(); + let n: usize = obstacle_grid[0].len(); + if obstacle_grid[0][0] == 1 || obstacle_grid[m-1][n-1] == 1 { + return 0; + } + let mut dp = vec![vec![0; n]; m]; + for i in 0..m { + if obstacle_grid[i][0] == 1 { + break; + } + else { dp[i][0] = 1; } + } + for j in 0..n { + if obstacle_grid[0][j] == 1 { + break; + } + else { dp[0][j] = 1; } + } + for i in 1..m { + for j in 1..n { + if obstacle_grid[i][j] == 1 { + continue; + } + dp[i][j] = dp[i-1][j] + dp[i][j-1]; + } + } + dp[m-1][n-1] + } +} +``` + +空间优化: + +```rust +impl Solution { + pub fn unique_paths_with_obstacles(obstacle_grid: Vec>) -> i32 { + let mut dp = vec![0; obstacle_grid[0].len()]; + for (i, &v) in obstacle_grid[0].iter().enumerate() { + if v == 0 { + dp[i] = 1; + } else { + break; + } + } + for rows in obstacle_grid.iter().skip(1) { + for j in 0..rows.len() { + if rows[j] == 1 { + dp[j] = 0; + } else if j != 0 { + dp[j] += dp[j - 1]; + } + } + } + dp.pop().unwrap() + } +} +``` + +### C + +```c +//初始化dp数组 +int **initDP(int m, int n, int** obstacleGrid) { + int **dp = (int**)malloc(sizeof(int*) * m); + int i, j; + //初始化每一行数组 + for(i = 0; i < m; ++i) { + dp[i] = (int*)malloc(sizeof(int) * n); + } + + //先将第一行第一列设为0 + for(i = 0; i < m; ++i) { + dp[i][0] = 0; + } + for(j = 0; j < n; ++j) { + dp[0][j] = 0; + } + + //若碰到障碍,之后的都走不了。退出循环 + for(i = 0; i < m; ++i) { + if(obstacleGrid[i][0]) { + break; + } + dp[i][0] = 1; + } + for(j = 0; j < n; ++j) { + if(obstacleGrid[0][j]) + break; + dp[0][j] = 1; + } + return dp; +} + +int uniquePathsWithObstacles(int** obstacleGrid, int obstacleGridSize, int* obstacleGridColSize){ + int m = obstacleGridSize, n = *obstacleGridColSize; + //初始化dp数组 + int **dp = initDP(m, n, obstacleGrid); + + int i, j; + for(i = 1; i < m; ++i) { + for(j = 1; j < n; ++j) { + //若当前i,j位置有障碍 + if(obstacleGrid[i][j]) + //路线不同 + dp[i][j] = 0; + else + dp[i][j] = dp[i-1][j] + dp[i][j-1]; + } + } + //返回最后终点的路径个数 + return dp[m-1][n-1]; +} +``` + +空间优化版本: + +```c +int uniquePathsWithObstacles(int** obstacleGrid, int obstacleGridSize, int* obstacleGridColSize){ + int m = obstacleGridSize; + int n = obstacleGridColSize[0]; + int *dp = (int*)malloc(sizeof(int) * n); + int i, j; + + // 初始化dp为第一行起始状态。 + for (j = 0; j < n; ++j) { + if (obstacleGrid[0][j] == 1) + dp[j] = 0; + else if (j == 0) + dp[j] = 1; + else + dp[j] = dp[j - 1]; + } + + for (i = 1; i < m; ++i) { + for (j = 0; j < n; ++j) { + if (obstacleGrid[i][j] == 1) + dp[j] = 0; + // 若j为0,dp[j]表示最左边一列,无需改动 + // 此处dp[j],dp[j-1]等同于二维dp中的dp[i-1][j]和dp[i][j-1] + else if (j != 0) + dp[j] += dp[j - 1]; + } + } + + return dp[n - 1]; +} +``` + +### Scala + +```scala +object Solution { + import scala.util.control.Breaks._ + def uniquePathsWithObstacles(obstacleGrid: Array[Array[Int]]): Int = { + var (m, n) = (obstacleGrid.length, obstacleGrid(0).length) + var dp = Array.ofDim[Int](m, n) + + // 比如break、continue这些流程控制需要使用breakable + breakable( + for (i <- 0 until m) { + if (obstacleGrid(i)(0) != 1) dp(i)(0) = 1 + else break() + } + ) + breakable( + for (j <- 0 until n) { + if (obstacleGrid(0)(j) != 1) dp(0)(j) = 1 + else break() + } + ) + + for (i <- 1 until m; j <- 1 until n; if obstacleGrid(i)(j) != 1) { + dp(i)(j) = dp(i - 1)(j) + dp(i)(j - 1) + } + + dp(m - 1)(n - 1) + } +} +``` +### C# +```csharp +public class Solution +{ + public int UniquePathsWithObstacles(int[][] obstacleGrid) + { + int m = obstacleGrid.Length; + int n = obstacleGrid[0].Length; + int[,] dp = new int[m, n]; + if (obstacleGrid[0][0] == 1 || obstacleGrid[m - 1][n - 1] == 1) return 0; + for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i, 0] = 1; + for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0, j] = 1; + for (int i = 1; i < m; i++) + { + for (int j = 1; j < n; j++) + { + if (obstacleGrid[i][j] == 1) continue; + dp[i, j] = dp[i - 1, j] + dp[i, j - 1]; + } + } + return dp[m - 1, n - 1]; + } +} +``` + + diff --git "a/problems/0070.\347\210\254\346\245\274\346\242\257.md" "b/problems/0070.\347\210\254\346\245\274\346\242\257.md" new file mode 100755 index 0000000000..316fbd4f39 --- /dev/null +++ "b/problems/0070.\347\210\254\346\245\274\346\242\257.md" @@ -0,0 +1,521 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 70. 爬楼梯 + +[力扣题目链接](https://leetcode.cn/problems/climbing-stairs/) + +假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 + +每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? + +注意:给定 n 是一个正整数。 + +示例 1: +* 输入: 2 +* 输出: 2 +* 解释: 有两种方法可以爬到楼顶。 + * 1 阶 + 1 阶 + * 2 阶 + +示例 2: +* 输入: 3 +* 输出: 3 +* 解释: 有三种方法可以爬到楼顶。 + * 1 阶 + 1 阶 + 1 阶 + * 1 阶 + 2 阶 + * 2 阶 + 1 阶 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透动态规划-爬楼梯|LeetCode:70.爬楼梯)](https://www.bilibili.com/video/BV17h411h7UH),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题大家如果没有接触过的话,会感觉比较难,多举几个例子,就可以发现其规律。 + +爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。 + +那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。 + +所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。 + +我们来分析一下,动规五部曲: + +定义一个一维数组来记录不同楼层的状态 + +1. 确定dp数组以及下标的含义 + +dp[i]: 爬到第i层楼梯,有dp[i]种方法 + +2. 确定递推公式 + +如何可以推出dp[i]呢? + +从dp[i]的定义可以看出,dp[i] 可以有两个方向推出来。 + +首先是dp[i - 1],上i-1层楼梯,有dp[i - 1]种方法,那么再一步跳一个台阶不就是dp[i]了么。 + +还有就是dp[i - 2],上i-2层楼梯,有dp[i - 2]种方法,那么再一步跳两个台阶不就是dp[i]了么。 + +那么dp[i]就是 dp[i - 1]与dp[i - 2]之和! + +所以dp[i] = dp[i - 1] + dp[i - 2] 。 + +在推导dp[i]的时候,一定要时刻想着dp[i]的定义,否则容易跑偏。 + +这体现出确定dp数组以及下标的含义的重要性! + +3. dp数组如何初始化 + +再回顾一下dp[i]的定义:爬到第i层楼梯,有dp[i]种方法。 + +那么i为0,dp[i]应该是多少呢,这个可以有很多解释,但基本都是直接奔着答案去解释的。 + +例如强行安慰自己爬到第0层,也有一种方法,什么都不做也就是一种方法即:dp[0] = 1,相当于直接站在楼顶。 + +但总有点牵强的成分。 + +那还这么理解呢:我就认为跑到第0层,方法就是0啊,一步只能走一个台阶或者两个台阶,然而楼层是0,直接站楼顶上了,就是不用方法,dp[0]就应该是0. + +**其实这么争论下去没有意义,大部分解释说dp[0]应该为1的理由其实是因为dp[0]=1的话在递推的过程中i从2开始遍历本题就能过,然后就往结果上靠去解释dp[0] = 1**。 + +从dp数组定义的角度上来说,dp[0] = 0 也能说得通。 + +需要注意的是:题目中说了n是一个正整数,题目根本就没说n有为0的情况。 + +所以本题其实就不应该讨论dp[0]的初始化! + +我相信dp[1] = 1,dp[2] = 2,这个初始化大家应该都没有争议的。 + +所以我的原则是:不考虑dp[0]如何初始化,只初始化dp[1] = 1,dp[2] = 2,然后从i = 3开始递推,这样才符合dp[i]的定义。 + +4. 确定遍历顺序 + +从递推公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,遍历顺序一定是从前向后遍历的 + +5. 举例推导dp数组 + +举例当n为5的时候,dp table(dp数组)应该是这样的 + + +![70.爬楼梯](https://file1.kamacoder.com/i/algo/20210105202546299.png) + +如果代码出问题了,就把dp table 打印出来,看看究竟是不是和自己推导的一样。 + +**此时大家应该发现了,这不就是斐波那契数列么!** + +唯一的区别是,没有讨论dp[0]应该是什么,因为dp[0]在本题没有意义! + +以上五部分析完之后,C++代码如下: + +```CPP +// 版本一 +class Solution { +public: + int climbStairs(int n) { + if (n <= 1) return n; // 因为下面直接对dp[2]操作了,防止空指针 + vector dp(n + 1); + dp[1] = 1; + dp[2] = 2; + for (int i = 3; i <= n; i++) { // 注意i是从3开始的 + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +当然依然也可以,优化一下空间复杂度,代码如下: + +```CPP +// 版本二 +class Solution { +public: + int climbStairs(int n) { + if (n <= 1) return n; + int dp[3]; + dp[1] = 1; + dp[2] = 2; + for (int i = 3; i <= n; i++) { + int sum = dp[1] + dp[2]; + dp[1] = dp[2]; + dp[2] = sum; + } + return dp[2]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +后面将讲解的很多动规的题目其实都是当前状态依赖前两个,或者前三个状态,都可以做空间上的优化,**但我个人认为面试中能写出版本一就够了哈,清晰明了,如果面试官要求进一步优化空间的话,我们再去优化**。 + +因为版本一才能体现出动规的思想精髓,递推的状态变化。 + +## 拓展 + +这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。 + +这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,大家可以去卡码网去做一下 [57. 爬楼梯](https://kamacoder.com/problempage.php?pid=1067) + + +所以后续我在讲解背包问题的时候,今天这道题还会从背包问题的角度上来再讲一遍。 如果想提前看一下,可以看这篇:[70.爬楼梯完全背包版本](https://programmercarl.com/0070.%E7%88%AC%E6%A5%BC%E6%A2%AF%E5%AE%8C%E5%85%A8%E8%83%8C%E5%8C%85%E7%89%88%E6%9C%AC.html) + +这里我先给出本题的代码: + +```CPP +class Solution { +public: + int climbStairs(int n) { + vector dp(n + 1, 0); + dp[0] = 1; + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题 + if (i - j >= 0) dp[i] += dp[i - j]; + } + } + return dp[n]; + } +}; +``` + +代码中m表示最多可以爬m个台阶。 + +**以上代码不能运行哈,我主要是为了体现只要把m换成2,粘过去,就可以AC爬楼梯这道题,不信你就粘一下试试**。 + + +**此时我就发现一个绝佳的大厂面试题**,第一道题就是单纯的爬楼梯,然后看候选人的代码实现,如果把dp[0]的定义成1了,就可以发难了,为什么dp[0]一定要初始化为1,此时可能候选人就要强行给dp[0]应该是1找各种理由。那这就是一个考察点了,对dp[i]的定义理解的不深入。 + +然后可以继续发难,如果一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。这道题目leetcode上并没有原题,绝对是考察候选人算法能力的绝佳好题。 + +这一连套问下来,候选人算法能力如何,面试官心里就有数了。 + +**其实大厂面试最喜欢的问题就是这种简单题,然后慢慢变化,在小细节上考察候选人**。 + + + +## 总结 + +这道题目和[动态规划:斐波那契数](https://programmercarl.com/0509.斐波那契数.html)题目基本是一样的,但是会发现本题相比[动态规划:斐波那契数](https://programmercarl.com/0509.斐波那契数.html)难多了,为什么呢? + +关键是 [动态规划:斐波那契数](https://programmercarl.com/0509.斐波那契数.html) 题目描述就已经把动规五部曲里的递归公式和如何初始化都给出来了,剩下几部曲也自然而然的推出来了。 + +而本题,就需要逐个分析了,大家现在应该初步感受出[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)里给出的动规五部曲了。 + +简单题是用来掌握方法论的,例如昨天斐波那契的题目够简单了吧,但昨天和今天可以使用一套方法分析出来的,这就是方法论! + +所以不要轻视简单题,那种凭感觉就刷过去了,其实和没掌握区别不大,只有掌握方法论并说清一二三,才能触类旁通,举一反三哈! + + +## 其他语言版本 + + +### Java + +```java +// 常规方式 +public int climbStairs(int n) { + int[] dp = new int[n + 1]; + dp[0] = 1; + dp[1] = 1; + for (int i = 2; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; +} +``` + +```Java +// 用变量记录代替数组 +class Solution { + public int climbStairs(int n) { + if(n <= 2) return n; + int a = 1, b = 2, sum = 0; + + for(int i = 3; i <= n; i++){ + sum = a + b; // f(i - 1) + f(i - 2) + a = b; // 记录f(i - 1),即下一轮的f(i - 2) + b = sum; // 记录f(i),即下一轮的f(i - 1) + } + return b; + } +} +``` + +### Python +动态规划(版本一) +```python +# 空间复杂度为O(n)版本 +class Solution: + def climbStairs(self, n: int) -> int: + if n <= 1: + return n + + dp = [0] * (n + 1) + dp[1] = 1 + dp[2] = 2 + + for i in range(3, n + 1): + dp[i] = dp[i - 1] + dp[i - 2] + + return dp[n] + +``` +动态规划(版本二) +```python + +# 空间复杂度为O(3)版本 +class Solution: + def climbStairs(self, n: int) -> int: + if n <= 1: + return n + + dp = [0] * 3 + dp[1] = 1 + dp[2] = 2 + + for i in range(3, n + 1): + total = dp[1] + dp[2] + dp[1] = dp[2] + dp[2] = total + + return dp[2] + +``` +动态规划(版本三) +```python + +# 空间复杂度为O(1)版本 +class Solution: + def climbStairs(self, n: int) -> int: + if n <= 1: + return n + + prev1 = 1 + prev2 = 2 + + for i in range(3, n + 1): + total = prev1 + prev2 + prev1 = prev2 + prev2 = total + + return prev2 + + +``` +### Go +```Go +func climbStairs(n int) int { + if n == 1 { + return 1 + } + dp := make([]int, n+1) + dp[1] = 1 + dp[2] = 2 + for i := 3; i <= n; i++ { + dp[i] = dp[i-1] + dp[i-2] + } + return dp[n] +} +``` +### JavaScript +```Javascript +var climbStairs = function(n) { + // dp[i] 为第 i 阶楼梯有多少种方法爬到楼顶 + // dp[i] = dp[i - 1] + dp[i - 2] + let dp = [1 , 2] + for(let i = 2; i < n; i++) { + dp[i] = dp[i - 1] + dp[i - 2] + } + return dp[n - 1] +}; +``` + +### TypeScript + +> 爬2阶 + +```typescript +function climbStairs(n: number): number { + /** + dp[i]: i阶楼梯的方法种数 + dp[1]: 1; + dp[2]: 2; + ... + dp[i]: dp[i - 1] + dp[i - 2]; + */ + const dp: number[] = []; + dp[1] = 1; + dp[2] = 2; + for (let i = 3; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; +}; +``` + +> 爬m阶 + +```typescript +function climbStairs(n: number): number { + /** + 一次可以爬m阶 + dp[i]: i阶楼梯的方法种数 + dp[1]: 1; + dp[2]: 2; + dp[3]: dp[2] + dp[1]; + ... + dp[i]: dp[i - 1] + dp[i - 2] + ... + dp[max(i - m, 1)]; 从i-1加到max(i-m, 1) + */ + const m: number = 2; // 本题m为2 + const dp: number[] = new Array(n + 1).fill(0); + dp[1] = 1; + dp[2] = 2; + for (let i = 3; i <= n; i++) { + const end: number = Math.max(i - m, 1); + for (let j = i - 1; j >= end; j--) { + dp[i] += dp[j]; + } + } + return dp[n]; +}; +``` + +### C + +```c +int climbStairs(int n){ + //若n<=2,返回n + if(n <= 2) + return n; + //初始化dp数组,数组大小为n+1 + int *dp = (int *)malloc(sizeof(int) * (n + 1)); + dp[0] = 0, dp[1] = 1, dp[2] = 2; + + //从前向后遍历数组,dp[i] = dp[i-1] + dp[i-2] + int i; + for(i = 3; i <= n; ++i) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + //返回dp[n] + return dp[n]; +} +``` + +优化空间复杂度: +```c +int climbStairs(int n){ + //若n<=2,返回n + if(n <= 2) + return n; + //初始化dp数组,数组大小为3 + int *dp = (int *)malloc(sizeof(int) * 3); + dp[1] = 1, dp[2] = 2; + + //只记录前面两个台阶的状态 + int i; + for(i = 3; i <= n; ++i) { + int sum = dp[1] + dp[2]; + dp[1] = dp[2]; + dp[2] = sum; + } + //返回dp[2] + return dp[2]; +} +``` + +### Scala + +```scala +object Solution { + def climbStairs(n: Int): Int = { + if (n <= 2) return n + var dp = new Array[Int](n + 1) + dp(1) = 1 + dp(2) = 2 + for (i <- 3 to n) { + dp(i) = dp(i - 1) + dp(i - 2) + } + dp(n) + } +} +``` + +优化空间复杂度: +```scala +object Solution { + def climbStairs(n: Int): Int = { + if (n <= 2) return n + var (a, b) = (1, 2) + for (i <- 3 to n) { + var tmp = a + b + a = b + b = tmp + } + b // 最终返回b + } +} +``` + +### C# + +```csharp +public class Solution { + public int ClimbStairs(int n) { + if(n<=2) return n; + int[] dp = new int[2] { 1, 2 }; + for (int i = 3; i <= n; i++) + { + int temp = dp[0] + dp[1]; + dp[0] = dp[1]; + dp[1] = temp; + } + return dp[1]; + } +} +``` + +### Rust + +```rust +impl Solution { + pub fn climb_stairs(n: i32) -> i32 { + if n <= 1 { + return n; + } + let (mut a, mut b, mut f) = (1, 1, 0); + for _ in 2..=n { + f = a + b; + a = b; + b = f; + } + f +} +``` + +dp 数组 + +```rust +impl Solution { + pub fn climb_stairs(n: i32) -> i32 { + let n = n as usize; + let mut dp = vec![0; n + 1]; + dp[0] = 1; + dp[1] = 1; + for i in 2..=n { + dp[i] = dp[i - 1] + dp[i - 2]; + } + dp[n] + } +} +``` + + + diff --git "a/problems/0070.\347\210\254\346\245\274\346\242\257\345\256\214\345\205\250\350\203\214\345\214\205\347\211\210\346\234\254.md" "b/problems/0070.\347\210\254\346\245\274\346\242\257\345\256\214\345\205\250\350\203\214\345\214\205\347\211\210\346\234\254.md" new file mode 100755 index 0000000000..a5435ddd71 --- /dev/null +++ "b/problems/0070.\347\210\254\346\245\274\346\242\257\345\256\214\345\205\250\350\203\214\345\214\205\347\211\210\346\234\254.md" @@ -0,0 +1,252 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 70. 爬楼梯(进阶版) + +[卡码网:57. 爬楼梯](https://kamacoder.com/problempage.php?pid=1067) + +假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 + +每次你可以爬至多m (1 <= m < n)个台阶。你有多少种不同的方法可以爬到楼顶呢? + +注意:给定 n 是一个正整数。 + +输入描述:输入共一行,包含两个正整数,分别表示n, m + +输出描述:输出一个整数,表示爬到楼顶的方法数。 + +输入示例:3 2 + +输出示例:3 + +提示: + +当 m = 2,n = 3 时,n = 3 这表示一共有三个台阶,m = 2 代表你每次可以爬一个台阶或者两个台阶。 + +此时你有三种方法可以爬到楼顶。 + +* 1 阶 + 1 阶 + 1 阶段 +* 1 阶 + 2 阶 +* 2 阶 + 1 阶 + + +## 思路 + +之前讲这道题目的时候,因为还没有讲背包问题,所以就只是讲了一下爬楼梯最直接的动规方法(斐波那契)。 + +**这次终于讲到了背包问题,我选择带录友们再爬一次楼梯!** + +这道题目 我们在[动态规划:爬楼梯](https://programmercarl.com/0070.爬楼梯.html) 中已经讲过一次了,这次我又给本题加点料,力扣上没有原题,所以可以在卡码网[57. 爬楼梯](https://kamacoder.com/problempage.php?pid=1067)上来刷这道题目。 + +我们之前做的 爬楼梯 是只能至多爬两个台阶。 + +这次**改为:一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?** + +这又有难度了,这其实是一个完全背包问题。 + +1阶,2阶,.... m阶就是物品,楼顶就是背包。 + +每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。 + +问跳到楼顶有几种方法其实就是问装满背包有几种方法。 + +**此时大家应该发现这就是一个完全背包问题了!** + +和昨天的题目[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)基本就是一道题了。 + +动规五部曲分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法**。 + +2. 确定递推公式 + +在[动态规划:494.目标和](https://programmercarl.com/0494.目标和.html) 、 [动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)、[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)中我们都讲过了,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]]; + +本题呢,dp[i]有几种来源,dp[i - 1],dp[i - 2],dp[i - 3] 等等,即:dp[i - j] + +那么递推公式为:dp[i] += dp[i - j] + +3. dp数组如何初始化 + +既然递归公式是 dp[i] += dp[i - j],那么dp[0] 一定为1,dp[0]是递归中一切数值的基础所在,如果dp[0]是0的话,其他数值都是0了。 + +下标非0的dp[i]初始化为0,因为dp[i]是靠dp[i-j]累计上来的,dp[i]本身为0这样才不会影响结果 + +4. 确定遍历顺序 + +这是背包里求排列问题,即:**1、2 步 和 2、1 步都是上三个台阶,但是这两种方法不一样!** + +所以需将target放在外循环,将nums放在内循环。 + +每一步可以走多次,这是完全背包,内循环需要从前向后遍历。 + +5. 举例来推导dp数组 + +介于本题和[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)几乎是一样的,这里我就不再重复举例了。 + + +以上分析完毕,C++代码如下: +```CPP +#include +#include +using namespace std; +int main() { + int n, m; + while (cin >> n >> m) { + vector dp(n + 1, 0); + dp[0] = 1; + for (int i = 1; i <= n; i++) { // 遍历背包 + for (int j = 1; j <= m; j++) { // 遍历物品 + if (i - j >= 0) dp[i] += dp[i - j]; + } + } + cout << dp[n] << endl; + } +} +``` + +* 时间复杂度: O(n * m) +* 空间复杂度: O(n) + +代码中m表示最多可以爬m个台阶,代码中把m改成2就是 力扣:70.爬楼梯的解题思路。 + +**当然注意 力扣是 核心代码模式,卡码网是ACM模式** + + +## 总结 + +**本题看起来是一道简单题目,稍稍进阶一下其实就是一个完全背包!** + +如果我来面试的话,我就会先给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。 + +顺便再考察一下两个for循环的嵌套顺序,为什么target放外面,nums放里面。 + +这就能考察对背包问题本质的掌握程度,候选人是不是刷题背公式,一眼就看出来了。 + +这么一连套下来,如果候选人都能答出来,相信任何一位面试官都是非常满意的。 + +**本题代码不长,题目也很普通,但稍稍一进阶就可以考察完全背包,而且题目进阶的内容在leetcode上并没有原题,一定程度上就可以排除掉刷题党了,简直是面试题目的绝佳选择!** + + + + +## 其他语言版本 + +### Java: + +```Java +import java.util.Scanner; +class climbStairs{ + public static void main(String [] args){ + Scanner sc = new Scanner(System.in); + int m, n; + while (sc.hasNextInt()) { + // 从键盘输入参数,中间用空格隔开 + n = sc.nextInt(); + m = sc.nextInt(); + + // 求排列问题,先遍历背包再遍历物品 + int[] dp = new int[n + 1]; + dp[0] = 1; + for (int j = 1; j <= n; j++) { + for (int i = 1; i <= m; i++) { + if (j - i >= 0) dp[j] += dp[j - i]; + } + } + System.out.println(dp[n]); + } + } +} +``` + +### Python3: +```python3 +def climbing_stairs(n,m): + dp = [0]*(n+1) # 背包总容量 + dp[0] = 1 + # 排列题,注意循环顺序,背包在外物品在内 + for j in range(1,n+1): + for i in range(1,m+1): + if j>=i: + dp[j] += dp[j-i] # 这里i就是重量而非index + return dp[n] + +if __name__ == '__main__': + n,m = list(map(int,input().split(' '))) + print(climbing_stairs(n,m)) +``` + + +### Go: +```go +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func climbStairs(n int, m int) int { + dp := make([]int, n+1) + dp[0] = 1 + for i := 1; i <= n; i++ { + for j := 1; j <= m; j++ { + if i-j >= 0 { + dp[i] += dp[i-j] + } + } + } + return dp[n] +} + +func main() { + // 读取输入n,m + reader := bufio.NewReader(os.Stdin) + input, _ := reader.ReadString('\n') + input = strings.TrimSpace(input) + nv := strings.Split(input, " ") + n, _ := strconv.Atoi(nv[0]) + m, _ := strconv.Atoi(nv[1]) + result := climbStairs(n, m) + fmt.Println(result) +} +``` + +### JavaScript: +```javaScript +var climbStairs = function (n) { + let dp = new Array(n + 1).fill(0); + dp[0] = 1; + // 排列题,注意循环顺序,背包在外物品在内 + for (let j = 1; j <= n; j++) {//遍历背包 + for (let i = 1; i <= 2; i++) {//遍历物品 + if (j - i >= 0) dp[j] = dp[j] + dp[j - i]; + } + } + return dp[n]; +} +``` + +### TypeScript: +```typescript +var climbStairs = function (n: number): number { + let dp: number[] = new Array(n + 1).fill(0); + dp[0] = 1; + for (let j = 1; j <= n; j++) {//遍历背包 + for (let i = 1; i <= 2; i++) {//遍历物品 + if (j - i >= 0) dp[j] = dp[j] + dp[j - i]; + } + } + return dp[n]; +} +``` + +### Rust: + + diff --git "a/problems/0072.\347\274\226\350\276\221\350\267\235\347\246\273.md" "b/problems/0072.\347\274\226\350\276\221\350\267\235\347\246\273.md" new file mode 100755 index 0000000000..c4bcbb4338 --- /dev/null +++ "b/problems/0072.\347\274\226\350\276\221\350\267\235\347\246\273.md" @@ -0,0 +1,462 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 72. 编辑距离 + +[力扣题目链接](https://leetcode.cn/problems/edit-distance/) + +给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 + +你可以对一个单词进行如下三种操作: + +* 插入一个字符 +* 删除一个字符 +* 替换一个字符 + +* 示例 1: +* 输入:word1 = "horse", word2 = "ros" +* 输出:3 +* 解释: +horse -> rorse (将 'h' 替换为 'r') +rorse -> rose (删除 'r') +rose -> ros (删除 'e') + + +* 示例 2: +* 输入:word1 = "intention", word2 = "execution" +* 输出:5 +* 解释: +intention -> inention (删除 't') +inention -> enention (将 'i' 替换为 'e') +enention -> exention (将 'n' 替换为 'x') +exention -> exection (将 'n' 替换为 'c') +exection -> execution (插入 'u') + +提示: + +* 0 <= word1.length, word2.length <= 500 +* word1 和 word2 由小写英文字母组成 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划终极绝杀! LeetCode:72.编辑距离](https://www.bilibili.com/video/BV1qv4y1q78f/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +编辑距离终于来了,这道题目如果大家没有了解动态规划的话,会感觉超级复杂。 + +编辑距离是用动规来解决的经典题目,这道题目看上去好像很复杂,但用动规可以很巧妙的算出最少编辑距离。 + +接下来我依然使用动规五部曲,对本题做一个详细的分析: + + +### 1. 确定dp数组(dp table)以及下标的含义 + +**dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]**。 + +有同学问了,为啥要表示下标i-1为结尾的字符串呢,为啥不表示下标i为结尾的字符串呢? + +为什么这么定义我在 [718. 最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html) 中做了详细的讲解。 + +其实用i来表示也可以! 用i-1就是为了方便后面dp数组初始化的。 + +### 2. 确定递推公式 + +在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下: + +``` +if (word1[i - 1] == word2[j - 1]) + 不操作 +if (word1[i - 1] != word2[j - 1]) + 增 + 删 + 换 +``` + +也就是如上4种情况。 + +`if (word1[i - 1] == word2[j - 1])` 那么说明不用任何编辑,`dp[i][j]` 就应该是 `dp[i - 1][j - 1]`,即`dp[i][j] = dp[i - 1][j - 1];` + +此时可能有同学有点不明白,为啥要即`dp[i][j] = dp[i - 1][j - 1]`呢? + +那么就在回顾上面讲过的`dp[i][j]`的定义,`word1[i - 1]` 与 `word2[j - 1]`相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串`word2`的最近编辑距离`dp[i - 1][j - 1]`就是 `dp[i][j]`了。 + +在下面的讲解中,如果哪里看不懂,就回想一下`dp[i][j]`的定义,就明白了。 + +**在整个动规的过程中,最为关键就是正确理解`dp[i][j]`的定义!** + + +`if (word1[i - 1] != word2[j - 1])`,此时就需要编辑了,如何编辑呢? + +* 操作一:word1删除一个元素,那么就是以下标i - 2为结尾的word1 与 j-1为结尾的word2的最近编辑距离 再加上一个操作。 + +即 `dp[i][j] = dp[i - 1][j] + 1;` + + +* 操作二:word2删除一个元素,那么就是以下标i - 1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 再加上一个操作。 + +即 `dp[i][j] = dp[i][j - 1] + 1;` + +这里有同学发现了,怎么都是删除元素,添加元素去哪了。 + +**word2添加一个元素,相当于word1删除一个元素**,例如 `word1 = "ad" ,word2 = "a"`,`word1`删除元素`'d'` 和 `word2`添加一个元素`'d'`,变成`word1="a", word2="ad"`, 最终的操作数是一样! dp数组如下图所示意的: + +``` + a a d + +-----+-----+ +-----+-----+-----+ + | 0 | 1 | | 0 | 1 | 2 | + +-----+-----+ ===> +-----+-----+-----+ + a | 1 | 0 | a | 1 | 0 | 1 | + +-----+-----+ +-----+-----+-----+ + d | 2 | 1 | + +-----+-----+ +``` + +操作三:替换元素,`word1`替换`word1[i - 1]`,使其与`word2[j - 1]`相同,此时不用增删加元素。 + +可以回顾一下,`if (word1[i - 1] == word2[j - 1])`的时候我们的操作 是 `dp[i][j] = dp[i - 1][j - 1]` 对吧。 + +那么只需要一次替换的操作,就可以让 word1[i - 1] 和 word2[j - 1] 相同。 + +所以 `dp[i][j] = dp[i - 1][j - 1] + 1;` + +综上,当 `if (word1[i - 1] != word2[j - 1])` 时取最小的,即:`dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1;` + +递归公式代码如下: + +```CPP +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], dp[i - 1][j], dp[i][j - 1]}) + 1; +} +``` + +--- + +### 3. dp数组如何初始化 + + +再回顾一下dp[i][j]的定义: + +**dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]**。 + +那么dp[i][0] 和 dp[0][j] 表示什么呢? + +dp[i][0] :以下标i-1为结尾的字符串word1,和空字符串word2,最近编辑距离为dp[i][0]。 + +那么dp[i][0]就应该是i,对word1里的元素全部做删除操作,即:dp[i][0] = i; + +同理dp[0][j] = j; + +所以C++代码如下: + +```CPP +for (int i = 0; i <= word1.size(); i++) dp[i][0] = i; +for (int j = 0; j <= word2.size(); j++) dp[0][j] = j; +``` + + +### 4. 确定遍历顺序 + +从如下四个递推公式: + +* `dp[i][j] = dp[i - 1][j - 1]` +* `dp[i][j] = dp[i - 1][j - 1] + 1` +* `dp[i][j] = dp[i][j - 1] + 1` +* `dp[i][j] = dp[i - 1][j] + 1` + +可以看出dp[i][j]是依赖左方,上方和左上方元素的,如图: + +![72.编辑距离](https://file1.kamacoder.com/i/algo/20210114162113131.jpg) + +所以在dp矩阵中一定是从左到右从上到下去遍历。 + +代码如下: + +```CPP +for (int i = 1; i <= word1.size(); i++) { + for (int j = 1; j <= word2.size(); 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], dp[i - 1][j], dp[i][j - 1]}) + 1; + } + } +} +``` + +### 5. 举例推导dp数组 + + +以示例1为例,输入:`word1 = "horse", word2 = "ros"`为例,dp矩阵状态图如下: + +![72.编辑距离1](https://file1.kamacoder.com/i/algo/20210114162132300.jpg) + +以上动规五部分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int minDistance(string word1, string word2) { + vector> dp(word1.size() + 1, vector(word2.size() + 1, 0)); + for (int i = 0; i <= word1.size(); i++) dp[i][0] = i; + for (int j = 0; j <= word2.size(); j++) dp[0][j] = j; + for (int i = 1; i <= word1.size(); i++) { + for (int j = 1; j <= word2.size(); 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], dp[i - 1][j], dp[i][j - 1]}) + 1; + } + } + } + return dp[word1.size()][word2.size()]; + } +}; +``` +* 时间复杂度: O(n * m) +* 空间复杂度: O(n * m) + + + +## 其他语言版本 + +### Java: + +```java +public int minDistance(String word1, String word2) { + int m = word1.length(); + int n = word2.length(); + int[][] dp = new int[m + 1][n + 1]; + // 初始化 + 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++) { + // 因为dp数组有效位从1开始 + // 所以当前遍历到的字符串的位置为i-1 | j-1 + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(Math.min(dp[i - 1][j - 1], dp[i][j - 1]), dp[i - 1][j]) + 1; + } + } + } + return dp[m][n]; +} +``` + +### Python: + +```python +class Solution: + def minDistance(self, word1: str, word2: str) -> int: + dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1)] + for i in range(len(word1)+1): + dp[i][0] = i + for j in range(len(word2)+1): + dp[0][j] = j + for i in range(1, len(word1)+1): + for j in range(1, len(word2)+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], dp[i-1][j], dp[i][j-1]) + 1 + return dp[-1][-1] +``` + +### Go: + +```Go +func minDistance(word1 string, word2 string) int { + m, n := len(word1), len(word2) + dp := make([][]int, m+1) + for i := range dp { + dp[i] = make([]int, n+1) + } + for i := 0; i < m+1; i++ { + dp[i][0] = i // word1[i] 变成 word2[0], 删掉 word1[i], 需要 i 部操作 + } + for j := 0; j < n+1; j++ { + dp[0][j] = j // word1[0] 变成 word2[j], 插入 word1[j],需要 j 部操作 + } + for i := 1; i < m+1; i++ { + for j := 1; j < n+1; j++ { + if word1[i-1] == word2[j-1] { + dp[i][j] = dp[i-1][j-1] + } else { // Min(插入,删除,替换) + dp[i][j] = Min(dp[i][j-1], dp[i-1][j], dp[i-1][j-1]) + 1 + } + } + } + return dp[m][n] +} +func Min(args ...int) int { + min := args[0] + for _, item := range args { + if item < min { + min = item + } + } + return min +} +``` + +### JavaScript: + +```javascript +const minDistance = (word1, word2) => { + let dp = Array.from(Array(word1.length + 1), () => Array(word2.length+1).fill(0)); + + for(let i = 1; i <= word1.length; i++) { + dp[i][0] = i; + } + + for(let j = 1; j <= word2.length; j++) { + dp[0][j] = j; + } + + for(let i = 1; i <= word1.length; i++) { + for(let j = 1; j <= word2.length; j++) { + if(word1[i-1] === word2[j-1]) { + dp[i][j] = dp[i-1][j-1]; + } else { + dp[i][j] = Math.min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + 1); + } + } + } + + return dp[word1.length][word2.length]; +}; +``` + +### TypeScript: + +```typescript +function minDistance(word1: string, word2: string): number { + /** + dp[i][j]: word1前i个字符,word2前j个字符,最少操作数 + dp[0][0]=0:表示word1前0个字符为'', word2前0个字符为'' + */ + const length1: number = word1.length, + length2: number = word2.length; + const dp: number[][] = new Array(length1 + 1).fill(0) + .map(_ => new Array(length2 + 1).fill(0)); + for (let i = 0; i <= length1; i++) { + dp[i][0] = i; + } + for (let i = 0; i <= length2; i++) { + dp[0][i] = i; + } + for (let i = 1; i <= length1; i++) { + for (let j = 1; j <= length2; j++) { + if (word1[i - 1] === word2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min( + dp[i - 1][j], + dp[i][j - 1], + dp[i - 1][j - 1] + ) + 1; + } + } + } + return dp[length1][length2]; +}; +``` + +### C: + + +```c +int min(int num1, int num2, int num3) { + return num1 > num2 ? (num2 > num3 ? num3 : num2) : (num1 > num3 ? num3 : num1); +} + +int minDistance(char * word1, char * word2){ + int dp[strlen(word1)+1][strlen(word2)+1]; + dp[0][0] = 0; + for (int i = 1; i <= strlen(word1); i++) dp[i][0] = i; + for (int i = 1; i <= strlen(word2); i++) dp[0][i] = i; + + for (int i = 1; i <= strlen(word1); i++) { + for (int j = 1; j <= strlen(word2); 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], dp[i][j-1], dp[i-1][j]) + 1; + } + } + } + return dp[strlen(word1)][strlen(word2)]; +} +``` + +Rust: + +```rust +impl Solution { + pub fn min_distance(word1: String, word2: String) -> i32 { + let mut dp = vec![vec![0; word2.len() + 1]; word1.len() + 1]; + for i in 1..=word2.len() { + dp[0][i] = i; + } + + for (j, v) in dp.iter_mut().enumerate().skip(1) { + v[0] = j; + } + for (i, char1) in word1.chars().enumerate() { + for (j, char2) in word2.chars().enumerate() { + if char1 == char2 { + dp[i + 1][j + 1] = dp[i][j]; + continue; + } + dp[i + 1][j + 1] = dp[i][j + 1].min(dp[i + 1][j]).min(dp[i][j]) + 1; + } + } + + dp[word1.len()][word2.len()] as i32 + } +} +``` + +> 一维 dp + +```rust +impl Solution { + pub fn min_distance(word1: String, word2: String) -> i32 { + let mut dp = vec![0; word1.len() + 1]; + for (i, v) in dp.iter_mut().enumerate().skip(1) { + *v = i; + } + + for char2 in word2.chars() { + // 相当于 dp[i][0] 的初始化 + let mut pre = dp[0]; + dp[0] += 1; // j = 0, 将前 i 个字符变成空串的个数 + for (j, char1) in word1.chars().enumerate() { + let temp = dp[j + 1]; + if char1 == char2 { + dp[j + 1] = pre; + } else { + dp[j + 1] = dp[j + 1].min(dp[j]).min(pre) + 1; + } + pre = temp; + } + } + + dp[word1.len()] as i32 + } +} +``` + diff --git "a/problems/0077.\347\273\204\345\220\210.md" "b/problems/0077.\347\273\204\345\220\210.md" new file mode 100755 index 0000000000..4c9e97fd47 --- /dev/null +++ "b/problems/0077.\347\273\204\345\220\210.md" @@ -0,0 +1,876 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 第77题. 组合 + +[力扣题目链接](https://leetcode.cn/problems/combinations/ ) + +给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。 + +示例: +输入: n = 4, k = 2 +输出: +[ + [2,4], + [3,4], + [2,3], + [1,2], + [1,3], + [1,4], +] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透回溯算法-组合问题(对应力扣题目:77.组合)](https://www.bilibili.com/video/BV1ti4y1L7cv),[组合问题的剪枝操作](https://www.bilibili.com/video/BV1wi4y157er),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + + +本题是回溯法的经典题目。 + +直接的解法当然是使用for循环,例如示例中k为2,很容易想到 用两个for循环,这样就可以输出 和示例中一样的结果。 + +代码如下: + +```CPP +int n = 4; +for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + cout << i << " " << j << endl; + } +} +``` + +输入:n = 100, k = 3 +那么就三层for循环,代码如下: + +```CPP +int n = 100; +for (int i = 1; i <= n; i++) { + for (int j = i + 1; j <= n; j++) { + for (int u = j + 1; u <= n; n++) { + cout << i << " " << j << " " << u << endl; + } + } +} +``` + +**如果n为100,k为50呢,那就50层for循环,是不是开始窒息**。 + +**此时就会发现虽然想暴力搜索,但是用for循环嵌套连暴力都写不出来!** + +咋整? + +回溯搜索法来了,虽然回溯法也是暴力,但至少能写出来,不像for循环嵌套k层让人绝望。 + +那么回溯法怎么暴力搜呢? + +上面我们说了**要解决 n为100,k为50的情况,暴力写法需要嵌套50层for循环,那么回溯法就用递归来解决嵌套层数的问题**。 + +递归来做层叠嵌套(可以理解是开k层for循环),**每一次的递归中嵌套一个for循环,那么递归就可以用于解决多层嵌套循环的问题了**。 + +此时递归的层数大家应该知道了,例如:n为100,k为50的情况下,就是递归50层。 + +一些同学本来对递归就懵,回溯法中递归还要嵌套for循环,可能就直接晕倒了! + +如果脑洞模拟回溯搜索的过程,绝对可以让人窒息,所以需要抽象图形结构来进一步理解。 + +**我们在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中说到回溯法解决的问题都可以抽象为树形结构(N叉树),用树形结构来理解回溯就容易多了**。 + +那么我把组合问题抽象为如下树形结构: + +![77.组合](https://file1.kamacoder.com/i/algo/20201123195223940.png) + +可以看出这棵树,一开始集合是 1,2,3,4, 从左向右取数,取过的数,不再重复取。 + +第一次取1,集合变为2,3,4 ,因为k为2,我们只需要再取一个数就可以了,分别取2,3,4,得到集合[1,2] [1,3] [1,4],以此类推。 + +**每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围**。 + +**图中可以发现n相当于树的宽度,k相当于树的深度**。 + +那么如何在这个树上遍历,然后收集到我们要的结果集呢? + +**图中每次搜索到了叶子节点,我们就找到了一个结果**。 + +相当于只需要把达到叶子节点的结果收集起来,就可以求得 n个数中k个数的组合集合。 + +在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中我们提到了回溯法三部曲,那么我们按照回溯法三部曲开始正式讲解代码了。 + + +### 回溯法三部曲 + +* 递归函数的返回值以及参数 + +在这里要定义两个全局变量,一个用来存放符合条件单一结果,一个用来存放符合条件结果的集合。 + +代码如下: + +```cpp +vector> result; // 存放符合条件结果的集合 +vector path; // 用来存放符合条件结果 +``` + +其实不定义这两个全局变量也是可以的,把这两个变量放进递归函数的参数里,但函数里参数太多影响可读性,所以我定义全局变量了。 + +函数里一定有两个参数,既然是集合n里面取k个数,那么n和k是两个int型的参数。 + +然后还需要一个参数,为int型变量startIndex,这个参数用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。 + +为什么要有这个startIndex呢? + +**建议在[77.组合视频讲解](https://www.bilibili.com/video/BV1ti4y1L7cv)中,07:36的时候开始听,startIndex 就是防止出现重复的组合**。 + +从下图中红线部分可以看出,在集合[1,2,3,4]取1之后,下一层递归,就要在[2,3,4]中取数了,那么下一层递归如何知道从[2,3,4]中取数呢,靠的就是startIndex。 + +![77.组合2](https://file1.kamacoder.com/i/algo/20201123195328976.png) + +所以需要startIndex来记录下一层递归,搜索的起始位置。 + +那么整体代码如下: + +```cpp +vector> result; // 存放符合条件结果的集合 +vector path; // 用来存放符合条件单一结果 +void backtracking(int n, int k, int startIndex) +``` + +* 回溯函数终止条件 + +什么时候到达所谓的叶子节点了呢? + +path这个数组的大小如果达到k,说明我们找到了一个子集大小为k的组合了,在图中path存的就是根节点到叶子节点的路径。 + +如图红色部分: + +![77.组合3](https://file1.kamacoder.com/i/algo/20201123195407907.png) + +此时用result二维数组,把path保存起来,并终止本层递归。 + +所以终止条件代码如下: + +```cpp +if (path.size() == k) { + result.push_back(path); + return; +} +``` + +* 单层搜索的过程 + +回溯法的搜索过程就是一个树型结构的遍历过程,在如下图中,可以看出for循环用来横向遍历,递归的过程是纵向遍历。 + +![77.组合1](https://file1.kamacoder.com/i/algo/20201123195242899.png) + +如此我们才遍历完图中的这棵树。 + +for循环每次从startIndex开始遍历,然后用path保存取到的节点i。 + +代码如下: + +```CPP +for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历 + path.push_back(i); // 处理节点 + backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始 + path.pop_back(); // 回溯,撤销处理的节点 +} +``` + +可以看出backtracking(递归函数)通过不断调用自己一直往深处遍历,总会遇到叶子节点,遇到了叶子节点就要返回。 + +backtracking的下面部分就是回溯的操作了,撤销本次处理的结果。 + +关键地方都讲完了,组合问题C++完整代码如下: + + +```CPP +class Solution { +private: + vector> result; // 存放符合条件结果的集合 + vector path; // 用来存放符合条件结果 + void backtracking(int n, int k, int startIndex) { + if (path.size() == k) { + result.push_back(path); + return; + } + for (int i = startIndex; i <= n; i++) { + path.push_back(i); // 处理节点 + backtracking(n, k, i + 1); // 递归 + path.pop_back(); // 回溯,撤销处理的节点 + } + } +public: + vector> combine(int n, int k) { + result.clear(); // 可以不写 + path.clear(); // 可以不写 + backtracking(n, k, 1); + return result; + } +}; +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n) + + + +还记得我们在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中给出的回溯法模板么? + +如下: + +``` +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} +``` + +**对比一下本题的代码,是不是发现有点像!** 所以有了这个模板,就有解题的大体方向,不至于毫无头绪。 + +## 总结 + +组合问题是回溯法解决的经典问题,我们开始的时候给大家列举一个很形象的例子,就是n为100,k为50的话,直接想法就需要50层for循环。 + +从而引出了回溯法就是解决这种k层for循环嵌套的问题。 + +然后进一步把回溯法的搜索过程抽象为树形结构,可以直观的看出搜索的过程。 + +接着用回溯法三部曲,逐步分析了函数参数、终止条件和单层搜索的过程。 + +## 剪枝优化 + +我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。 + +在遍历的过程中有如下代码: + +```cpp +for (int i = startIndex; i <= n; i++) { + path.push_back(i); + backtracking(n, k, i + 1); + path.pop_back(); +} +``` + +这个遍历的范围是可以剪枝优化的,怎么优化呢? + +来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。 + +这么说有点抽象,如图所示: + +![77.组合4](https://file1.kamacoder.com/i/algo/20210130194335207-20230310134409532.png) + +图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。 + +**所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置**。 + +**如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了**。 + +注意代码中i,就是for循环里选择的起始位置。 + +``` +for (int i = startIndex; i <= n; i++) { +``` + +接下来看一下优化过程如下: + +1. 已经选择的元素个数:path.size(); + +2. 还需要的元素个数为: k - path.size(); + +3. 在集合n中至多要从该起始位置 : n - (k - path.size()) + 1,开始遍历 + +为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。 + +举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。 + +从2开始搜索都是合理的,可以是组合[2, 3, 4]。 + +这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。 + +所以优化之后的for循环是: + +``` +for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置 +``` + +优化后整体代码如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(int n, int k, int startIndex) { + if (path.size() == k) { + result.push_back(path); + return; + } + for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方 + path.push_back(i); // 处理节点 + backtracking(n, k, i + 1); + path.pop_back(); // 回溯,撤销处理的节点 + } + } +public: + + vector> combine(int n, int k) { + backtracking(n, k, 1); + return result; + } +}; +``` + +## 剪枝总结 + +本篇我们准对求组合问题的回溯法代码做了剪枝优化,这个优化如果不画图的话,其实不好理解,也不好讲清楚。 + +所以我依然是把整个回溯过程抽象为一棵树形结构,然后可以直观的看出,剪枝究竟是剪的哪里。 + + + +## 其他语言版本 + + + +### Java: +未剪枝优化 +```java +class Solution { + List> result= new ArrayList<>(); + LinkedList path = new LinkedList<>(); + public List> combine(int n, int k) { + backtracking(n,k,1); + return result; + } + + public void backtracking(int n,int k,int startIndex){ + if (path.size() == k){ + result.add(new ArrayList<>(path)); + return; + } + for (int i =startIndex;i<=n;i++){ + path.add(i); + backtracking(n,k,i+1); + path.removeLast(); + } + } +} +``` +剪枝优化: +```java +class Solution { + List> result = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + public List> combine(int n, int k) { + combineHelper(n, k, 1); + return result; + } + + /** + * 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex + * @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。 + */ + private void combineHelper(int n, int k, int startIndex){ + //终止条件 + if (path.size() == k){ + result.add(new ArrayList<>(path)); + return; + } + for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){ + path.add(i); + combineHelper(n, k, i + 1); + path.removeLast(); + } + } +} +``` + +### Python +未剪枝优化 +```python +class Solution: + def combine(self, n: int, k: int) -> List[List[int]]: + result = [] # 存放结果集 + self.backtracking(n, k, 1, [], result) + return result + def backtracking(self, n, k, startIndex, path, result): + if len(path) == k: + result.append(path[:]) + return + for i in range(startIndex, n + 1): # 需要优化的地方 + path.append(i) # 处理节点 + self.backtracking(n, k, i + 1, path, result) + path.pop() # 回溯,撤销处理的节点 + +``` + + +剪枝优化: + +```python +class Solution: + def combine(self, n: int, k: int) -> List[List[int]]: + result = [] # 存放结果集 + self.backtracking(n, k, 1, [], result) + return result + def backtracking(self, n, k, startIndex, path, result): + if len(path) == k: + result.append(path[:]) + return + for i in range(startIndex, n - (k - len(path)) + 2): # 优化的地方 + path.append(i) # 处理节点 + self.backtracking(n, k, i + 1, path, result) + path.pop() # 回溯,撤销处理的节点 + +``` + +### Go + +```Go +var ( + path []int + res [][]int +) + +func combine(n int, k int) [][]int { + path, res = make([]int, 0, k), make([][]int, 0) + dfs(n, k, 1) + return res +} + +func dfs(n int, k int, start int) { + if len(path) == k { // 说明已经满足了k个数的要求 + tmp := make([]int, k) + copy(tmp, path) + res = append(res, tmp) + return + } + for i := start; i <= n; i++ { // 从start开始,不往回走,避免出现重复组合 + if n - i + 1 < k - len(path) { // 剪枝 + break + } + path = append(path, i) + dfs(n, k, i+1) + path = path[:len(path)-1] + } +} +``` + +### JavaScript +未剪枝: + +```js +var combine = function (n, k) { + // 回溯法 + let result = [], + path = []; + let backtracking = (_n, _k, startIndex) => { + // 终止条件 + if (path.length === _k) { + result.push(path.slice()); + return; + } + // 循环本层集合元素 + for (let i = startIndex; i <= _n; i++) { + path.push(i); + // 递归 + backtracking(_n, _k, i + 1); + // 回溯操作 + path.pop(); + } + }; + backtracking(n, k, 1); + return result; +}; +``` + +剪枝: + +```javascript +var combine = function (n, k) { + // 回溯法 + let result = [], + path = []; + let backtracking = (_n, _k, startIndex) => { + // 终止条件 + if (path.length === _k) { + result.push(path.slice()); + return; + } + // 循环本层集合元素 + for (let i = startIndex; i <= _n - (_k - path.length) + 1; i++) { + path.push(i); + // 递归 + backtracking(_n, _k, i + 1); + // 回溯操作 + path.pop(); + } + }; + backtracking(n, k, 1); + return result; +}; +``` + +### TypeScript + +```typescript +function combine(n: number, k: number): number[][] { + let resArr: number[][] = []; + function backTracking(n: number, k: number, startIndex: number, tempArr: number[]): void { + if (tempArr.length === k) { + resArr.push(tempArr.slice()); + return; + } + for (let i = startIndex; i <= n - k + 1 + tempArr.length; i++) { + tempArr.push(i); + backTracking(n, k, i + 1, tempArr); + tempArr.pop(); + } + } + backTracking(n, k, 1, []); + return resArr; +}; +``` + +### Rust + +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, n: i32, k: i32, start_index: i32) { + let len= path.len() as i32; + if len == k{ + result.push(path.to_vec()); + return; + } + for i in start_index..= n { + path.push(i); + Self::backtracking(result, path, n, k, i+1); + path.pop(); + } + } + pub fn combine(n: i32, k: i32) -> Vec> { + let mut result = vec![]; + let mut path = vec![]; + Self::backtracking(&mut result, &mut path, n, k, 1); + result + } +} +``` + +剪枝 + +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, n: i32, k: i32, start_index: i32) { + let len= path.len() as i32; + if len == k{ + result.push(path.to_vec()); + return; + } + // 此处剪枝 + for i in start_index..= n - (k - len) + 1 { + path.push(i); + Self::backtracking(result, path, n, k, i+1); + path.pop(); + } + } + pub fn combine(n: i32, k: i32) -> Vec> { + let mut result = vec![]; + let mut path = vec![]; + Self::backtracking(&mut result, &mut path, n, k, 1); + result + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; + +void backtracking(int n, int k,int startIndex) { + //当path中元素个数为k个时,我们需要将path数组放入ans二维数组中 + if(pathTop == k) { + //path数组为我们动态申请,若直接将其地址放入二维数组,path数组中的值会随着我们回溯而逐渐变化 + //因此创建新的数组存储path中的值 + int* temp = (int*)malloc(sizeof(int) * k); + int i; + for(i = 0; i < k; i++) { + temp[i] = path[i]; + } + ans[ansTop++] = temp; + return ; + } + + int j; + for(j = startIndex; j <=n ;j++) { + //将当前结点放入path数组 + path[pathTop++] = j; + //进行递归 + backtracking(n, k, j + 1); + //进行回溯,将数组最上层结点弹出 + pathTop--; + } +} + +int** combine(int n, int k, int* returnSize, int** returnColumnSizes){ + //path数组存储符合条件的结果 + path = (int*)malloc(sizeof(int) * k); + //ans二维数组存储符合条件的结果数组的集合。(数组足够大,避免极端情况) + ans = (int**)malloc(sizeof(int*) * 10000); + pathTop = ansTop = 0; + + //回溯算法 + backtracking(n, k, 1); + //最后的返回大小为ans数组大小 + *returnSize = ansTop; + //returnColumnSizes数组存储ans二维数组对应下标中一维数组的长度(都为k) + *returnColumnSizes = (int*)malloc(sizeof(int) *(*returnSize)); + int i; + for(i = 0; i < *returnSize; i++) { + (*returnColumnSizes)[i] = k; + } + //返回ans二维数组 + return ans; +} +``` + +剪枝: + +```c +int* path; +int pathTop; +int** ans; +int ansTop; + +void backtracking(int n, int k,int startIndex) { + //当path中元素个数为k个时,我们需要将path数组放入ans二维数组中 + if(pathTop == k) { + //path数组为我们动态申请,若直接将其地址放入二维数组,path数组中的值会随着我们回溯而逐渐变化 + //因此创建新的数组存储path中的值 + int* temp = (int*)malloc(sizeof(int) * k); + int i; + for(i = 0; i < k; i++) { + temp[i] = path[i]; + } + ans[ansTop++] = temp; + return ; + } + + int j; + for(j = startIndex; j <= n- (k - pathTop) + 1;j++) { + //将当前结点放入path数组 + path[pathTop++] = j; + //进行递归 + backtracking(n, k, j + 1); + //进行回溯,将数组最上层结点弹出 + pathTop--; + } +} + +int** combine(int n, int k, int* returnSize, int** returnColumnSizes){ + //path数组存储符合条件的结果 + path = (int*)malloc(sizeof(int) * k); + //ans二维数组存储符合条件的结果数组的集合。(数组足够大,避免极端情况) + ans = (int**)malloc(sizeof(int*) * 10000); + pathTop = ansTop = 0; + + //回溯算法 + backtracking(n, k, 1); + //最后的返回大小为ans数组大小 + *returnSize = ansTop; + //returnColumnSizes数组存储ans二维数组对应下标中一维数组的长度(都为k) + *returnColumnSizes = (int*)malloc(sizeof(int) *(*returnSize)); + int i; + for(i = 0; i < *returnSize; i++) { + (*returnColumnSizes)[i] = k; + } + //返回ans二维数组 + return ans; +} +``` + +### Swift + +```swift +func combine(_ n: Int, _ k: Int) -> [[Int]] { + var path = [Int]() + var result = [[Int]]() + func backtracking(start: Int) { + // 结束条件,并收集结果 + if path.count == k { + result.append(path) + return + } + + // 单层逻辑 + // let end = n + // 剪枝优化 + let end = n - (k - path.count) + 1 + guard start <= end else { return } + for i in start ... end { + path.append(i) // 处理结点 + backtracking(start: i + 1) // 递归 + path.removeLast() // 回溯 + } + } + + backtracking(start: 1) + return result +} +``` + +### Scala + +暴力: + +```scala +object Solution { + import scala.collection.mutable // 导包 + def combine(n: Int, k: Int): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() // 存放结果集 + var path = mutable.ListBuffer[Int]() //存放符合条件的结果 + + def backtracking(n: Int, k: Int, startIndex: Int): Unit = { + if (path.size == k) { + // 如果path的size == k就达到题目要求,添加到结果集,并返回 + result.append(path.toList) + return + } + for (i <- startIndex to n) { // 遍历从startIndex到n + path.append(i) // 先把数字添加进去 + backtracking(n, k, i + 1) // 进行下一步回溯 + path = path.take(path.size - 1) // 回溯完再删除掉刚刚添加的数字 + } + } + + backtracking(n, k, 1) // 执行回溯 + result.toList // 最终返回result的List形式,return关键字可以省略 + } +} +``` + +剪枝: + +```scala +object Solution { + import scala.collection.mutable // 导包 + def combine(n: Int, k: Int): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() // 存放结果集 + var path = mutable.ListBuffer[Int]() //存放符合条件的结果 + + def backtracking(n: Int, k: Int, startIndex: Int): Unit = { + if (path.size == k) { + // 如果path的size == k就达到题目要求,添加到结果集,并返回 + result.append(path.toList) + return + } + // 剪枝优化 + for (i <- startIndex to (n - (k - path.size) + 1)) { + path.append(i) // 先把数字添加进去 + backtracking(n, k, i + 1) // 进行下一步回溯 + path = path.take(path.size - 1) // 回溯完再删除掉刚刚添加的数字 + } + } + + backtracking(n, k, 1) // 执行回溯 + result.toList // 最终返回result的List形式,return关键字可以省略 + } +} +``` + +### Ruby + +```ruby + +def combine(n, k) + result = [] + path = [] + backtracking(result, path, n, 1, k) + return result +end + +#剪枝优化 +def backtracking(result, path, n, j, k) + if path.size == k + result << path.map {|item| item} + return + end + + for i in j..(n-(k - path.size)) + 1 + #处理节点 + path << i + backtracking(result, path, n, i + 1, k) + #回溯,撤销处理过的节点 + path.pop + end +end + +``` +### C# +```csharp +// 暴力 +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> Combine(int n, int k) + { + BackTracking(n, k, 1); + return res; + } + public void BackTracking(int n, int k, int start) + { + if (path.Count == k) + { + res.Add(new List(path)); + return; + } + for (int i = start; i <= n; i++) + { + path.Add(i); + BackTracking(n, k, i + 1); + path.RemoveAt(path.Count - 1); + } + } +} +// 剪枝 +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> Combine(int n, int k) + { + BackTracking(n, k, 1); + return res; + } + public void BackTracking(int n, int k, int start) + { + if (path.Count == k) + { + res.Add(new List(path)); + return; + } + for (int i = start; i <= n - (k - path.Count) + 1; i++) + { + path.Add(i); + BackTracking(n, k, i + 1); + path.RemoveAt(path.Count - 1); + } + } +} +``` + diff --git "a/problems/0077.\347\273\204\345\220\210\344\274\230\345\214\226.md" "b/problems/0077.\347\273\204\345\220\210\344\274\230\345\214\226.md" new file mode 100755 index 0000000000..8ddc4058cc --- /dev/null +++ "b/problems/0077.\347\273\204\345\220\210\344\274\230\345\214\226.md" @@ -0,0 +1,412 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 77.组合优化 +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[组合问题的剪枝操作](https://www.bilibili.com/video/BV1wi4y157er),相信结合视频在看本篇题解,更有助于大家对本题的理解。** + +## 思路 + +在[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)中,我们通过回溯搜索法,解决了n个数中求k个数的组合问题。 + +文中的回溯法是可以剪枝优化的,本篇我们继续来看一下题目77. 组合。 + +链接:https://leetcode.cn/problems/combinations/ + +**看本篇之前,需要先看[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)**。 + +大家先回忆一下[77. 组合]给出的回溯法的代码: + +```CPP +class Solution { +private: + vector> result; // 存放符合条件结果的集合 + vector path; // 用来存放符合条件结果 + void backtracking(int n, int k, int startIndex) { + if (path.size() == k) { + result.push_back(path); + return; + } + for (int i = startIndex; i <= n; i++) { + path.push_back(i); // 处理节点 + backtracking(n, k, i + 1); // 递归 + path.pop_back(); // 回溯,撤销处理的节点 + } + } +public: + vector> combine(int n, int k) { + result.clear(); // 可以不写 + path.clear(); // 可以不写 + backtracking(n, k, 1); + return result; + } +}; +``` + +## 剪枝优化 + +我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。 + +在遍历的过程中有如下代码: + +```CPP +for (int i = startIndex; i <= n; i++) { + path.push_back(i); + backtracking(n, k, i + 1); + path.pop_back(); +} +``` + +这个遍历的范围是可以剪枝优化的,怎么优化呢? + +来举一个例子,n = 4,k = 4的话,那么第一层for循环的时候,从元素2开始的遍历都没有意义了。 在第二层for循环,从元素3开始的遍历都没有意义了。 + +这么说有点抽象,如图所示: + +![77.组合4](https://file1.kamacoder.com/i/algo/20210130194335207.png) + + +图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。 + +**所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置**。 + +**如果for循环选择的起始位置之后的元素个数 已经不足 我们需要的元素个数了,那么就没有必要搜索了**。 + +注意代码中i,就是for循环里选择的起始位置。 +```CPP +for (int i = startIndex; i <= n; i++) { +``` + +接下来看一下优化过程如下: + +1. 已经选择的元素个数:path.size(); + +2. 所需需要的元素个数为: k - path.size(); + +3. 列表中剩余元素(n-i) >= 所需需要的元素个数(k - path.size()) + +4. 在集合n中至多要从该起始位置 : i <= n - (k - path.size()) + 1,开始遍历 + +为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。 + +举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。 + +从2开始搜索都是合理的,可以是组合[2, 3, 4]。 + +这里大家想不懂的话,建议也举一个例子,就知道是不是要+1了。 + +所以优化之后的for循环是: + +```CPP +for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜索的起始位置 +``` + +优化后整体代码如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(int n, int k, int startIndex) { + if (path.size() == k) { + result.push_back(path); + return; + } + for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方 + path.push_back(i); // 处理节点 + backtracking(n, k, i + 1); + path.pop_back(); // 回溯,撤销处理的节点 + } + } +public: + + vector> combine(int n, int k) { + backtracking(n, k, 1); + return result; + } +}; +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n) + + + +## 总结 + +本篇我们针对求组合问题的回溯法代码做了剪枝优化,这个优化如果不画图的话,其实不好理解,也不好讲清楚。 + +所以我依然是把整个回溯过程抽象为一棵树形结构,然后可以直观的看出,剪枝究竟是剪的哪里。 + +**就酱,学到了就帮Carl转发一下吧,让更多的同学知道这里!** + +## 其他语言版本 + +### Java + +```java +class Solution { + List> result = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + public List> combine(int n, int k) { + combineHelper(n, k, 1); + return result; + } + + /** + * 每次从集合中选取元素,可选择的范围随着选择的进行而收缩,调整可选择的范围,就是要靠startIndex + * @param startIndex 用来记录本层递归的中,集合从哪里开始遍历(集合就是[1,...,n] )。 + */ + private void combineHelper(int n, int k, int startIndex){ + //终止条件 + if (path.size() == k){ + result.add(new ArrayList<>(path)); + return; + } + for (int i = startIndex; i <= n - (k - path.size()) + 1; i++){ + path.add(i); + combineHelper(n, k, i + 1); + path.removeLast(); + } + } +} +``` + +### Python + +```python +class Solution: + def combine(self, n: int, k: int) -> List[List[int]]: + result = [] # 存放结果集 + self.backtracking(n, k, 1, [], result) + return result + def backtracking(self, n, k, startIndex, path, result): + if len(path) == k: + result.append(path[:]) + return + for i in range(startIndex, n - (k - len(path)) + 2): # 优化的地方 + path.append(i) # 处理节点 + self.backtracking(n, k, i + 1, path, result) + path.pop() # 回溯,撤销处理的节点 + + + + +``` +### Go + +```Go +var ( + path []int + res [][]int +) + +func combine(n int, k int) [][]int { + path, res = make([]int, 0, k), make([][]int, 0) + dfs(n, k, 1) + return res +} + +func dfs(n int, k int, start int) { + if len(path) == k { + tmp := make([]int, k) + copy(tmp, path) + res = append(res, tmp) + return + } + for i := start; i <= n - (k-len(path)) + 1; i++ { + path = append(path, i) + dfs(n, k, i+1) + path = path[:len(path)-1] + } +} +``` + +### JavaScript + +```js +var combine = function(n, k) { + const res = [], path = []; + backtracking(n, k, 1); + return res; + function backtracking (n, k, i){ + const len = path.length; + if(len === k) { + res.push(Array.from(path)); + return; + } + for(let a = i; a <= n + len - k + 1; a++) { + path.push(a); + backtracking(n, k, a + 1); + path.pop(); + } + } +}; +``` + +### TypeScript + +```typescript +function combine(n: number, k: number): number[][] { + let resArr: number[][] = []; + function backTracking(n: number, k: number, startIndex: number, tempArr: number[]): void { + if (tempArr.length === k) { + resArr.push(tempArr.slice()); + return; + } + for (let i = startIndex; i <= n - k + 1 + tempArr.length; i++) { + tempArr.push(i); + backTracking(n, k, i + 1, tempArr); + tempArr.pop(); + } + } + backTracking(n, k, 1, []); + return resArr; +}; +``` + +### Rust + +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, n: i32, k: i32, start_index: i32) { + let len= path.len() as i32; + if len == k{ + result.push(path.to_vec()); + return; + } + // 此处剪枝 + for i in start_index..= n - (k - len) + 1 { + path.push(i); + Self::backtracking(result, path, n, k, i+1); + path.pop(); + } + } + pub fn combine(n: i32, k: i32) -> Vec> { + let mut result = vec![]; + let mut path = vec![]; + Self::backtracking(&mut result, &mut path, n, k, 1); + result + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; + +void backtracking(int n, int k,int startIndex) { + //当path中元素个数为k个时,我们需要将path数组放入ans二维数组中 + if(pathTop == k) { + //path数组为我们动态申请,若直接将其地址放入二维数组,path数组中的值会随着我们回溯而逐渐变化 + //因此创建新的数组存储path中的值 + int* temp = (int*)malloc(sizeof(int) * k); + int i; + for(i = 0; i < k; i++) { + temp[i] = path[i]; + } + ans[ansTop++] = temp; + return ; + } + + int j; + for(j = startIndex; j <= n- (k - pathTop) + 1;j++) { + //将当前结点放入path数组 + path[pathTop++] = j; + //进行递归 + backtracking(n, k, j + 1); + //进行回溯,将数组最上层结点弹出 + pathTop--; + } +} + +int** combine(int n, int k, int* returnSize, int** returnColumnSizes){ + //path数组存储符合条件的结果 + path = (int*)malloc(sizeof(int) * k); + //ans二维数组存储符合条件的结果数组的集合。(数组足够大,避免极端情况) + ans = (int**)malloc(sizeof(int*) * 10000); + pathTop = ansTop = 0; + + //回溯算法 + backtracking(n, k, 1); + //最后的返回大小为ans数组大小 + *returnSize = ansTop; + //returnColumnSizes数组存储ans二维数组对应下标中一维数组的长度(都为k) + *returnColumnSizes = (int*)malloc(sizeof(int) *(*returnSize)); + int i; + for(i = 0; i < *returnSize; i++) { + (*returnColumnSizes)[i] = k; + } + //返回ans二维数组 + return ans; +} +``` + +### Swift + +```swift +func combine(_ n: Int, _ k: Int) -> [[Int]] { + var path = [Int]() + var result = [[Int]]() + func backtracking(start: Int) { + // 结束条件,并收集结果 + if path.count == k { + result.append(path) + return + } + + // 单层逻辑 + // let end = n + // 剪枝优化 + let end = n - (k - path.count) + 1 + guard start <= end else { return } + for i in start ... end { + path.append(i) // 处理结点 + backtracking(start: i + 1) // 递归 + path.removeLast() // 回溯 + } + } + + backtracking(start: 1) + return result +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable // 导包 + def combine(n: Int, k: Int): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() // 存放结果集 + var path = mutable.ListBuffer[Int]() //存放符合条件的结果 + + def backtracking(n: Int, k: Int, startIndex: Int): Unit = { + if (path.size == k) { + // 如果path的size == k就达到题目要求,添加到结果集,并返回 + result.append(path.toList) + return + } + // 剪枝优化 + for (i <- startIndex to (n - (k - path.size) + 1)) { + path.append(i) // 先把数字添加进去 + backtracking(n, k, i + 1) // 进行下一步回溯 + path = path.take(path.size - 1) // 回溯完再删除掉刚刚添加的数字 + } + } + + backtracking(n, k, 1) // 执行回溯 + result.toList // 最终返回result的List形式,return关键字可以省略 + } +} +``` + + diff --git "a/problems/0078.\345\255\220\351\233\206.md" "b/problems/0078.\345\255\220\351\233\206.md" new file mode 100755 index 0000000000..844b8dc2ca --- /dev/null +++ "b/problems/0078.\345\255\220\351\233\206.md" @@ -0,0 +1,492 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 78.子集 + +[力扣题目链接](https://leetcode.cn/problems/subsets/) + +给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 + +说明:解集不能包含重复的子集。 + +示例: +输入: nums = [1,2,3] +输出: +[ + [3], +  [1], +  [2], +  [1,2,3], +  [1,3], +  [2,3], +  [1,2], +  [] +] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[回溯算法解决子集问题,树上节点都是目标集和! | LeetCode:78.子集](https://www.bilibili.com/video/BV1U84y1q7Ci),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +求子集问题和[77.组合](https://programmercarl.com/0077.组合.html)和[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)又不一样了。 + +如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,**那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!** + +其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 + +**那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!** + +有同学问了,什么时候for可以从0开始呢? + +求排列问题的时候,就要从0开始,因为集合是有序的,{1, 2} 和{2, 1}是两个集合,排列问题我们后续的文章就会讲到的。 + +以示例中nums = [1,2,3]为例把求子集抽象为树型结构,如下: + +![78.子集](https://file1.kamacoder.com/i/algo/78.%E5%AD%90%E9%9B%86.png) + +从图中红线部分,可以看出**遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合**。 + +### 回溯三部曲 + +* 递归函数参数 + +全局变量数组path为子集收集元素,二维数组result存放子集组合。(也可以放到递归函数参数里) + +递归函数参数在上面讲到了,需要startIndex。 + +代码如下: + +```cpp +vector> result; +vector path; +void backtracking(vector& nums, int startIndex) { +``` + +递归终止条件 + +从图中可以看出: + +![78.子集](https://file1.kamacoder.com/i/algo/78.%E5%AD%90%E9%9B%86.png) + +剩余集合为空的时候,就是叶子节点。 + +那么什么时候剩余集合为空呢? + +就是startIndex已经大于数组的长度了,就终止了,因为没有元素可取了,代码如下: + +```cpp +if (startIndex >= nums.size()) { + return; +} +``` + +**其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了**。 + +* 单层搜索逻辑 + +**求取子集问题,不需要任何剪枝!因为子集就是要遍历整棵树**。 + +那么单层递归逻辑代码如下: + +``` +for (int i = startIndex; i < nums.size(); i++) { + path.push_back(nums[i]); // 子集收集元素 + backtracking(nums, i + 1); // 注意从i+1开始,元素不重复取 + path.pop_back(); // 回溯 +} +``` + +根据[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)给出的回溯算法模板: + +``` +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} +``` + +可以写出如下回溯算法C++代码: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& nums, int startIndex) { + result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己 + if (startIndex >= nums.size()) { // 终止条件可以不加 + return; + } + for (int i = startIndex; i < nums.size(); i++) { + path.push_back(nums[i]); + backtracking(nums, i + 1); + path.pop_back(); + } + } +public: + vector> subsets(vector& nums) { + result.clear(); + path.clear(); + backtracking(nums, 0); + return result; + } +}; + +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n) + +在注释中,可以发现可以不写终止条件,因为本来我们就要遍历整棵树。 + +有的同学可能担心不写终止条件会不会无限递归? + +并不会,因为每次递归的下一层就是从i+1开始的。 + +## 总结 + +相信大家经过了 +* 组合问题: + * [77.组合](https://programmercarl.com/0077.组合.html) + * [回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html) + * [216.组合总和III](https://programmercarl.com/0216.组合总和III.html) + * [17.电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html) + * [39.组合总和](https://programmercarl.com/0039.组合总和.html) + * [40.组合总和II](https://programmercarl.com/0040.组合总和II.html) +* 分割问题: + * [131.分割回文串](https://programmercarl.com/0131.分割回文串.html) + * [93.复原IP地址](https://programmercarl.com/0093.复原IP地址.html) + +洗礼之后,发现子集问题还真的有点简单了,其实这就是一道标准的模板题。 + +但是要清楚子集问题和组合问题、分割问题的的区别,**子集是收集树形结构中树的所有节点的结果**。 + +**而组合问题、分割问题是收集树形结构中叶子节点的结果**。 + +## 其他语言版本 + + +### Java +```java +class Solution { + List> result = new ArrayList<>();// 存放符合条件结果的集合 + LinkedList path = new LinkedList<>();// 用来存放符合条件结果 + public List> subsets(int[] nums) { + subsetsHelper(nums, 0); + return result; + } + + private void subsetsHelper(int[] nums, int startIndex){ + result.add(new ArrayList<>(path));//「遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合」。 + if (startIndex >= nums.length){ //终止条件可不加 + return; + } + for (int i = startIndex; i < nums.length; i++){ + path.add(nums[i]); + subsetsHelper(nums, i + 1); + path.removeLast(); + } + } +} +``` + +### Python +```python +class Solution: + def subsets(self, nums): + result = [] + path = [] + self.backtracking(nums, 0, path, result) + return result + + def backtracking(self, nums, startIndex, path, result): + result.append(path[:]) # 收集子集,要放在终止添加的上面,否则会漏掉自己 + # if startIndex >= len(nums): # 终止条件可以不加 + # return + for i in range(startIndex, len(nums)): + path.append(nums[i]) + self.backtracking(nums, i + 1, path, result) + path.pop() +``` + +### Go +```Go +var ( + path []int + res [][]int +) +func subsets(nums []int) [][]int { + res, path = make([][]int, 0), make([]int, 0, len(nums)) + dfs(nums, 0) + return res +} +func dfs(nums []int, start int) { + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + + for i := start; i < len(nums); i++ { + path = append(path, nums[i]) + dfs(nums, i+1) + path = path[:len(path)-1] + } +} +``` + +### JavaScript + +```Javascript +var subsets = function(nums) { + let result = [] + let path = [] + function backtracking(startIndex) { + result.push([...path]) + for(let i = startIndex; i < nums.length; i++) { + path.push(nums[i]) + backtracking(i + 1) + path.pop() + } + } + backtracking(0) + return result +}; +``` + +### TypeScript + +```typescript +function subsets(nums: number[]): number[][] { + const resArr: number[][] = []; + backTracking(nums, 0, []); + return resArr; + function backTracking(nums: number[], startIndex: number, route: number[]): void { + resArr.push([...route]); + let length = nums.length; + if (startIndex === length) return; + for (let i = startIndex; i < length; i++) { + route.push(nums[i]); + backTracking(nums, i + 1, route); + route.pop(); + } + } +}; +``` + +### Rust + +思路一:使用本题的标准解法,递归回溯。 +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, nums: &Vec, start_index: usize) { + result.push(path.clone()); + let len = nums.len(); + // if start_index >= len { return; } + for i in start_index..len { + path.push(nums[i]); + Self::backtracking(result, path, nums, i + 1); + path.pop(); + } + } + + pub fn subsets(nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + Self::backtracking(&mut result, &mut path, &nums, 0); + result + } +} +``` +思路二:使用二进制枚举,n个元素的子集问题一共是$2^n$种情况。如果我们使用一个二进制数字,每一位根据0和1来决定是选取该元素与否,那么一共也是$2^n$的情况,正好可以一一对应,所以我们可以不使用递归,直接利用循环枚举完成子集问题。 +这种方法的优点在于效率高,不需要递归调用,并且代码容易编写。缺点则是过滤某些非法情况时会比递归方法难写一点,不过在子集问题中不存在这个问题。 +```Rust +impl Solution { + pub fn subsets(nums: Vec) -> Vec> { + let n = nums.len(); + // 预分配2^n空间 + let mut result = Vec::with_capacity(1 << n); + // 二进制枚举,2^n种情况 + for i in 0..(1 << n) { + let mut subset = Vec::new(); + for j in 0..n { + // 枚举该二进制数字的每一位 + // 如果该位是1,对应位置上的元素加入子集,否则跳过 + if i & (1 << j) != 0 { + subset.push(nums[j]); + } + } + result.push(subset); + } + result + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; +//记录二维数组中每个一维数组的长度 +int* length; +//将当前path数组复制到ans中 +void copy() { + int* tempPath = (int*)malloc(sizeof(int) * pathTop); + int i; + for(i = 0; i < pathTop; i++) { + tempPath[i] = path[i]; + } + ans = (int**)realloc(ans, sizeof(int*) * (ansTop+1)); + length[ansTop] = pathTop; + ans[ansTop++] = tempPath; +} + +void backTracking(int* nums, int numsSize, int startIndex) { + //收集子集,要放在终止添加的上面,否则会漏掉自己 + copy(); + //若startIndex大于数组大小,返回 + if(startIndex >= numsSize) { + return; + } + int j; + for(j = startIndex; j < numsSize; j++) { + //将当前下标数字放入path中 + path[pathTop++] = nums[j]; + backTracking(nums, numsSize, j+1); + //回溯 + pathTop--; + } +} + +int** subsets(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){ + //初始化辅助变量 + path = (int*)malloc(sizeof(int) * numsSize); + ans = (int**)malloc(0); + length = (int*)malloc(sizeof(int) * 1500); + ansTop = pathTop = 0; + //进入回溯 + backTracking(nums, numsSize, 0); + //设置二维数组中元素个数 + *returnSize = ansTop; + //设置二维数组中每个一维数组的长度 + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; i++) { + (*returnColumnSizes)[i] = length[i]; + } + return ans; +} +``` + +### Swift + +```swift +func subsets(_ nums: [Int]) -> [[Int]] { + var result = [[Int]]() + var path = [Int]() + func backtracking(startIndex: Int) { + // 直接收集结果 + result.append(path) + + let end = nums.count + guard startIndex < end else { return } // 终止条件 + for i in startIndex ..< end { + path.append(nums[i]) // 处理:收集元素 + backtracking(startIndex: i + 1) // 元素不重复访问 + path.removeLast() // 回溯 + } + } + backtracking(startIndex: 0) + return result +} +``` + +### Scala + +思路一: 使用本题解思路 + +```scala +object Solution { + import scala.collection.mutable + def subsets(nums: Array[Int]): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + + def backtracking(startIndex: Int): Unit = { + result.append(path.toList) // 存放结果 + if (startIndex >= nums.size) { + return + } + for (i <- startIndex until nums.size) { + path.append(nums(i)) // 添加元素 + backtracking(i + 1) + path.remove(path.size - 1) // 删除 + } + } + + backtracking(0) + result.toList + } +} +``` + +思路二: 将原问题转换为二叉树,针对每一个元素都有**选或不选**两种选择,直到遍历到最后,所有的叶子节点即为本题的答案: + +```scala +object Solution { + import scala.collection.mutable + def subsets(nums: Array[Int]): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + + def backtracking(path: mutable.ListBuffer[Int], startIndex: Int): Unit = { + if (startIndex == nums.length) { + result.append(path.toList) + return + } + path.append(nums(startIndex)) + backtracking(path, startIndex + 1) // 选择元素 + path.remove(path.size - 1) + backtracking(path, startIndex + 1) // 不选择元素 + } + + backtracking(mutable.ListBuffer[Int](), 0) + result.toList + } +} +``` +### C# +```csharp +public class Solution { + public IList> res = new List>(); + public IList path = new List(); + public IList> Subsets(int[] nums) { + BackTracking(nums, 0); + return res; + } + public void BackTracking(int[] nums, int start){ + res.Add(new List(path)); + if(start > nums.Length) return; + for (int i = start; i < nums.Length; i++) + { + path.Add(nums[i]); + BackTracking(nums, i + 1); + path.RemoveAt(path.Count - 1); + } + } +} +``` + + + diff --git "a/problems/0084.\346\237\261\347\212\266\345\233\276\344\270\255\346\234\200\345\244\247\347\232\204\347\237\251\345\275\242.md" "b/problems/0084.\346\237\261\347\212\266\345\233\276\344\270\255\346\234\200\345\244\247\347\232\204\347\237\251\345\275\242.md" new file mode 100755 index 0000000000..99fb1678e6 --- /dev/null +++ "b/problems/0084.\346\237\261\347\212\266\345\233\276\344\270\255\346\234\200\345\244\247\347\232\204\347\237\251\345\275\242.md" @@ -0,0 +1,863 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 84.柱状图中最大的矩形 + +[力扣题目链接](https://leetcode.cn/problems/largest-rectangle-in-histogram/) + +给定 n 个非负整数,用来表示柱状图中各个柱子的高度。每个柱子彼此相邻,且宽度为 1 。 + +求在该柱状图中,能够勾勒出来的矩形的最大面积。 + +![](https://file1.kamacoder.com/i/algo/20210803220437.png) + +![](https://file1.kamacoder.com/i/algo/20210803220506.png) + +* 1 <= heights.length <=10^5 +* 0 <= heights[i] <= 10^4 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[单调栈,又一次经典来袭! LeetCode:84.柱状图中最大的矩形](https://www.bilibili.com/video/BV1Ns4y1o7uB/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +本题和[42. 接雨水](https://programmercarl.com/0042.接雨水.html),是遥相呼应的两道题目,建议都要仔细做一做,原理上有很多相同的地方,但细节上又有差异,更可以加深对单调栈的理解! + +其实这两道题目先做那一道都可以,但我先写的42.接雨水的题解,所以如果没做过接雨水的话,建议先做一做接雨水,可以参考我的题解:[42. 接雨水](https://programmercarl.com/0042.接雨水.html) + +我们先来看一下暴力解法的解法: + +### 暴力解法 + +```CPP +class Solution { +public: + int largestRectangleArea(vector& heights) { + int sum = 0; + for (int i = 0; i < heights.size(); i++) { + int left = i; + int right = i; + for (; left >= 0; left--) { + if (heights[left] < heights[i]) break; + } + for (; right < heights.size(); right++) { + if (heights[right] < heights[i]) break; + } + int w = right - left - 1; + int h = heights[i]; + sum = max(sum, w * h); + } + return sum; + } +}; +``` + +如上代码并不能通过leetcode,超时了,因为时间复杂度是$O(n^2)$。 + +### 双指针解法 + +本题双指针的写法整体思路和[42. 接雨水](https://programmercarl.com/0042.接雨水.html)是一致的,但要比[42. 接雨水](https://programmercarl.com/0042.接雨水.html)难一些。 + +难就难在本题要记录记录每个柱子 左边第一个小于该柱子的下标,而不是左边第一个小于该柱子的高度。 + +所以需要循环查找,也就是下面在寻找的过程中使用了while,详细请看下面注释,整理思路在题解:[42. 接雨水](https://programmercarl.com/0042.接雨水.html)中已经介绍了。 + +```CPP +class Solution { +public: + int largestRectangleArea(vector& heights) { + vector minLeftIndex(heights.size()); + vector minRightIndex(heights.size()); + int size = heights.size(); + + // 记录每个柱子 左边第一个小于该柱子的下标 + minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环 + for (int i = 1; i < size; i++) { + int t = i - 1; + // 这里不是用if,而是不断向左寻找的过程 + while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t]; + minLeftIndex[i] = t; + } + // 记录每个柱子 右边第一个小于该柱子的下标 + minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环 + for (int i = size - 2; i >= 0; i--) { + int t = i + 1; + // 这里不是用if,而是不断向右寻找的过程 + while (t < size && heights[t] >= heights[i]) t = minRightIndex[t]; + minRightIndex[i] = t; + } + // 求和 + int result = 0; + for (int i = 0; i < size; i++) { + int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1); + result = max(sum, result); + } + return result; + } +}; +``` + +### 单调栈 + +本地单调栈的解法和接雨水的题目是遥相呼应的。 + +为什么这么说呢,[42. 接雨水](https://programmercarl.com/0042.接雨水.html)是找每个柱子左右两边第一个大于该柱子高度的柱子,而本题是找每个柱子左右两边第一个小于该柱子的柱子。 + +**这里就涉及到了单调栈很重要的性质,就是单调栈里的顺序,是从小到大还是从大到小**。 + +在题解[42. 接雨水](https://programmercarl.com/0042.接雨水.html)中我讲解了接雨水的单调栈从栈头(元素从栈头弹出)到栈底的顺序应该是从小到大的顺序。 + +那么因为本题是要找每个柱子左右两边第一个小于该柱子的柱子,所以从栈头(元素从栈头弹出)到栈底的顺序应该是从大到小的顺序! + +我来举一个例子,如图: + +![](https://file1.kamacoder.com/i/algo/20230221165730.png) + +只有栈里从大到小的顺序,才能保证栈顶元素找到左右两边第一个小于栈顶元素的柱子。 + +所以本题单调栈的顺序正好与接雨水反过来。 + +此时大家应该可以发现其实就是**栈顶和栈顶的下一个元素以及要入栈的三个元素组成了我们要求最大面积的高度和宽度** + +理解这一点,对单调栈就掌握的比较到位了。 + +除了栈内元素顺序和接雨水不同,剩下的逻辑就都差不多了,在题解[42. 接雨水](https://programmercarl.com/0042.接雨水.html)我已经对单调栈的各个方面做了详细讲解,这里就不赘述了。 + +主要就是分析清楚如下三种情况: + +* 情况一:当前遍历的元素heights[i]大于栈顶元素heights[st.top()]的情况 +* 情况二:当前遍历的元素heights[i]等于栈顶元素heights[st.top()]的情况 +* 情况三:当前遍历的元素heights[i]小于栈顶元素heights[st.top()]的情况 + +C++代码如下: + +```CPP +// 版本一 +class Solution { +public: + int largestRectangleArea(vector& heights) { + int result = 0; + stack st; + heights.insert(heights.begin(), 0); // 数组头部加入元素0 + heights.push_back(0); // 数组尾部加入元素0 + st.push(0); + + // 第一个元素已经入栈,从下标1开始 + for (int i = 1; i < heights.size(); i++) { + if (heights[i] > heights[st.top()]) { // 情况一 + st.push(i); + } else if (heights[i] == heights[st.top()]) { // 情况二 + st.pop(); // 这个可以加,可以不加,效果一样,思路不同 + st.push(i); + } else { // 情况三 + while (!st.empty() && heights[i] < heights[st.top()]) { // 注意是while + int mid = st.top(); + st.pop(); + if (!st.empty()) { + int left = st.top(); + int right = i; + int w = right - left - 1; + int h = heights[mid]; + result = max(result, w * h); + } + } + st.push(i); + } + } + return result; + } +}; + +``` + +细心的录友会发现,我在 height数组上后,都加了一个元素0, 为什么这么做呢? + +首先来说末尾为什么要加元素0? + +如果数组本身就是升序的,例如[2,4,6,8],那么入栈之后 都是单调递减,一直都没有走 情况三 计算结果的哪一步,所以最后输出的就是0了。 如图: + +![](https://file1.kamacoder.com/i/algo/20230221163936.png) + +那么结尾加一个0,就会让栈里的所有元素,走到情况三的逻辑。 + + +开头为什么要加元素0? + +如果数组本身是降序的,例如 [8,6,4,2],在 8 入栈后,6 开始与8 进行比较,此时我们得到 mid(8),right(6),但是得不到 left。 + +(mid、left,right 都是对应版本一里的逻辑) + +因为 将 8 弹出之后,栈里没有元素了,那么为了避免空栈取值,直接跳过了计算结果的逻辑。 + +之后又将6 加入栈(此时8已经弹出了),然后 就是 4 与 栈口元素 6 进行比较,周而复始,那么计算的最后结果result就是0。 如图所示: + +![](https://file1.kamacoder.com/i/algo/20230221164533.png) + +所以我们需要在 height数组前后各加一个元素0。 + + + +版本一代码精简之后: + +```CPP +// 版本二 +class Solution { +public: + int largestRectangleArea(vector& heights) { + stack st; + heights.insert(heights.begin(), 0); // 数组头部加入元素0 + heights.push_back(0); // 数组尾部加入元素0 + st.push(0); + int result = 0; + for (int i = 1; i < heights.size(); i++) { + while (heights[i] < heights[st.top()]) { + int mid = st.top(); + st.pop(); + int w = i - st.top() - 1; + int h = heights[mid]; + result = max(result, w * h); + } + st.push(i); + } + return result; + } +}; +``` + +这里我依然建议大家按部就班把版本一写出来,把情况一二三分析清楚,然后在精简代码到版本二。 直接看版本二容易忽略细节! + +## 其他语言版本 + +### Java: + +暴力解法: +```java +class Solution { + public int largestRectangleArea(int[] heights) { + int length = heights.length; + int[] minLeftIndex = new int [length]; + int[] minRightIndex = new int [length]; + // 记录左边第一个小于该柱子的下标 + minLeftIndex[0] = -1 ; + for (int i = 1; i < length; i++) { + int t = i - 1; + // 这里不是用if,而是不断向右寻找的过程 + while (t >= 0 && heights[t] >= heights[i]) t = minLeftIndex[t]; + minLeftIndex[i] = t; + } + // 记录每个柱子右边第一个小于该柱子的下标 + minRightIndex[length - 1] = length; + for (int i = length - 2; i >= 0; i--) { + int t = i + 1; + while(t < length && heights[t] >= heights[i]) t = minRightIndex[t]; + minRightIndex[i] = t; + } + // 求和 + int result = 0; + for (int i = 0; i < length; i++) { + int sum = heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1); + result = Math.max(sum, result); + } + return result; + } +} +``` + +单调栈: +```java +class Solution { + int largestRectangleArea(int[] heights) { + Stack st = new Stack(); + + // 数组扩容,在头和尾各加入一个元素 + int [] newHeights = new int[heights.length + 2]; + newHeights[0] = 0; + newHeights[newHeights.length - 1] = 0; + for (int index = 0; index < heights.length; index++){ + newHeights[index + 1] = heights[index]; + } + + heights = newHeights; + + st.push(0); + int result = 0; + // 第一个元素已经入栈,从下标1开始 + for (int i = 1; i < heights.length; i++) { + // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下标 + if (heights[i] > heights[st.peek()]) { + st.push(i); + } else if (heights[i] == heights[st.peek()]) { + st.pop(); // 这个可以加,可以不加,效果一样,思路不同 + st.push(i); + } else { + while (heights[i] < heights[st.peek()]) { // 注意是while + int mid = st.peek(); + st.pop(); + int left = st.peek(); + int right = i; + int w = right - left - 1; + int h = heights[mid]; + result = Math.max(result, w * h); + } + st.push(i); + } + } + return result; + } +} +``` +单调栈精简 +```java +class Solution { + public int largestRectangleArea(int[] heights) { + int[] newHeight = new int[heights.length + 2]; + System.arraycopy(heights, 0, newHeight, 1, heights.length); + newHeight[heights.length+1] = 0; + newHeight[0] = 0; + + Stack stack = new Stack<>(); + stack.push(0); + + int res = 0; + for (int i = 1; i < newHeight.length; i++) { + while (newHeight[i] < newHeight[stack.peek()]) { + int mid = stack.pop(); + int w = i - stack.peek() - 1; + int h = newHeight[mid]; + res = Math.max(res, w * h); + } + stack.push(i); + + } + return res; + } +} +``` + +### Python3: + +```python + +# 暴力解法(leetcode超时) +class Solution: + def largestRectangleArea(self, heights: List[int]) -> int: + # 从左向右遍历:以每一根柱子为主心骨(当前轮最高的参照物),迭代直到找到左侧和右侧各第一个矮一级的柱子 + res = 0 + + for i in range(len(heights)): + left = i + right = i + # 向左侧遍历:寻找第一个矮一级的柱子 + for _ in range(left, -1, -1): + if heights[left] < heights[i]: + break + left -= 1 + # 向右侧遍历:寻找第一个矮一级的柱子 + for _ in range(right, len(heights)): + if heights[right] < heights[i]: + break + right += 1 + + width = right - left - 1 + height = heights[i] + res = max(res, width * height) + + return res + +# 双指针 +class Solution: + def largestRectangleArea(self, heights: List[int]) -> int: + size = len(heights) + # 两个DP数列储存的均是下标index + min_left_index = [0] * size + min_right_index = [0] * size + result = 0 + + # 记录每个柱子的左侧第一个矮一级的柱子的下标 + min_left_index[0] = -1 # 初始化防止while死循环 + for i in range(1, size): + # 以当前柱子为主心骨,向左迭代寻找次级柱子 + temp = i - 1 + while temp >= 0 and heights[temp] >= heights[i]: + # 当左侧的柱子持续较高时,尝试这个高柱子自己的次级柱子(DP + temp = min_left_index[temp] + # 当找到左侧矮一级的目标柱子时 + min_left_index[i] = temp + + # 记录每个柱子的右侧第一个矮一级的柱子的下标 + min_right_index[size-1] = size # 初始化防止while死循环 + for i in range(size-2, -1, -1): + # 以当前柱子为主心骨,向右迭代寻找次级柱子 + temp = i + 1 + while temp < size and heights[temp] >= heights[i]: + # 当右侧的柱子持续较高时,尝试这个高柱子自己的次级柱子(DP + temp = min_right_index[temp] + # 当找到右侧矮一级的目标柱子时 + min_right_index[i] = temp + + for i in range(size): + area = heights[i] * (min_right_index[i] - min_left_index[i] - 1) + result = max(area, result) + + return result + +# 单调栈 +class Solution: + def largestRectangleArea(self, heights: List[int]) -> int: + # Monotonic Stack + ''' + 找每个柱子左右侧的第一个高度值小于该柱子的柱子 + 单调栈:栈顶到栈底:从大到小(每插入一个新的小数值时,都要弹出先前的大数值) + 栈顶,栈顶的下一个元素,即将入栈的元素:这三个元素组成了最大面积的高度和宽度 + 情况一:当前遍历的元素heights[i]大于栈顶元素的情况 + 情况二:当前遍历的元素heights[i]等于栈顶元素的情况 + 情况三:当前遍历的元素heights[i]小于栈顶元素的情况 + ''' + + # 输入数组首尾各补上一个0(与42.接雨水不同的是,本题原首尾的两个柱子可以作为核心柱进行最大面积尝试 + heights.insert(0, 0) + heights.append(0) + stack = [0] + result = 0 + for i in range(1, len(heights)): + # 情况一 + if heights[i] > heights[stack[-1]]: + stack.append(i) + # 情况二 + elif heights[i] == heights[stack[-1]]: + stack.pop() + stack.append(i) + # 情况三 + else: + # 抛出所有较高的柱子 + while stack and heights[i] < heights[stack[-1]]: + # 栈顶就是中间的柱子,主心骨 + mid_index = stack[-1] + stack.pop() + if stack: + left_index = stack[-1] + right_index = i + width = right_index - left_index - 1 + height = heights[mid_index] + result = max(result, width * height) + stack.append(i) + return result + +# 单调栈精简 +class Solution: + def largestRectangleArea(self, heights: List[int]) -> int: + heights.insert(0, 0) + heights.append(0) + stack = [0] + result = 0 + for i in range(1, len(heights)): + while stack and heights[i] < heights[stack[-1]]: + mid_height = heights[stack[-1]] + stack.pop() + if stack: + # area = width * height + area = (i - stack[-1] - 1) * mid_height + result = max(area, result) + stack.append(i) + return result + + + + + +``` + +### Go: + +暴力解法 + +```go +func largestRectangleArea(heights []int) int { + sum := 0 + for i := 0; i < len(heights); i++ { + left, right := i, i + for left >= 0 { + if heights[left] < heights[i] { + break + } + left-- + } + for right < len(heights) { + if heights[right] < heights[i] { + break + } + right++ + } + w := right - left - 1 + h := heights[i] + sum = max(sum, w * h) + } + return sum +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +双指针解法 + +```go +func largestRectangleArea(heights []int) int { + size := len(heights) + minLeftIndex := make([]int, size) + minRightIndex := make([]int, size) + + // 记录每个柱子 左边第一个小于该柱子的下标 + minLeftIndex[0] = -1 // 注意这里初始化,防止下面while死循环 + for i := 1; i < size; i++ { + t := i - 1 + // 这里不是用if,而是不断向左寻找的过程 + for t >= 0 && heights[t] >= heights[i] { + t = minLeftIndex[t] + } + minLeftIndex[i] = t + } + // 记录每个柱子 右边第一个小于该柱子的下标 + minRightIndex[size - 1] = size; // 注意这里初始化,防止下面while死循环 + for i := size - 2; i >= 0; i-- { + t := i + 1 + // 这里不是用if,而是不断向右寻找的过程 + for t < size && heights[t] >= heights[i] { + t = minRightIndex[t] + } + minRightIndex[i] = t + } + // 求和 + result := 0 + for i := 0; i < size; i++ { + sum := heights[i] * (minRightIndex[i] - minLeftIndex[i] - 1) + result = max(sum, result) + } + return result +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +单调栈 + +```go +func largestRectangleArea(heights []int) int { + result := 0 + heights = append([]int{0}, heights...) // 数组头部加入元素0 + heights = append(heights, 0) // 数组尾部加入元素0 + st := []int{0} + + // 第一个元素已经入栈,从下标1开始 + for i := 1; i < len(heights); i++ { + if heights[i] > heights[st[len(st)-1]] { + st = append(st, i) + } else if heights[i] == heights[st[len(st)-1]] { + st = st[:len(st)-1] + st = append(st, i) + } else { + for len(st) > 0 && heights[i] < heights[st[len(st)-1]] { + mid := st[len(st)-1] + st = st[:len(st)-1] + if len(st) > 0 { + left := st[len(st)-1] + right := i + w := right - left - 1 + h := heights[mid] + result = max(result, w * h) + } + } + st = append(st, i) + } + } + return result +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +单调栈精简 + +```go +func largestRectangleArea(heights []int) int { + max := 0 + // 使用切片实现栈 + stack := make([]int, 0) + // 数组头部加入0 + heights = append([]int{0}, heights...) + // 数组尾部加入0 + heights = append(heights, 0) + // 初始化栈,序号从0开始 + stack = append(stack, 0) + for i := 1; i < len(heights); i ++ { + // 结束循环条件为:当即将入栈元素>top元素,也就是形成非单调递增的趋势 + for heights[stack[len(stack) - 1]] > heights[i] { + // mid 是top + mid := stack[len(stack) - 1] + // 出栈 + stack = stack[0 : len(stack) - 1] + // left是top的下一位元素,i是将要入栈的元素 + left := stack[len(stack) - 1] + // 高度x宽度 + tmp := heights[mid] * (i - left - 1) + if tmp > max { + max = tmp + } + } + stack = append(stack, i) + } + return max +} +``` + +### JavaScript: + +```javascript +//双指针 js中运行速度最快 +var largestRectangleArea = function(heights) { + const len = heights.length; + const minLeftIndex = new Array(len); + const maxRightIndex = new Array(len); + // 记录每个柱子 左边第一个小于该柱子的下标 + minLeftIndex[0] = -1; // 注意这里初始化,防止下面while死循环 + for(let i = 1; i < len; i++) { + let t = i - 1; + // 这里不是用if,而是不断向左寻找的过程 + while (t >= 0 && heights[t] >= heights[i]) { + t = minLeftIndex[t]; + } + minLeftIndex[i] = t; + } + // 记录每个柱子 右边第一个小于该柱子的下标 + maxRightIndex[len - 1] = len; // 注意这里初始化,防止下面while死循环 + for(let i = len - 2; i >= 0; i--){ + let t = i + 1; + // 这里不是用if,而是不断向右寻找的过程 + while (t <= n && heights[t] > heights[i]) { + t = maxRightIndex[t]; + } + maxRightIndex[i] = t; + } + // 求和 + let maxArea = 0; + for(let i = 0; i < len; i++){ + let sum = heights[i] * (maxRightIndex[i] - minLeftIndex[i] - 1); + maxArea = Math.max(maxArea , sum); + } + return maxArea; +}; + +//单调栈 +var largestRectangleArea = function(heights) { + let maxArea = 0; + const stack = [0]; + heights.push(0); + const n = heights.length; + + for (let i = 1; i < n; i++) { + let top = stack.at(-1); + // 情况三 + if (heights[top] < heights[i]) { + stack.push(i); + } + // 情况二 + if (heights[top] === heights[i]) { + stack.pop(); // 这个可以加,可以不加,效果一样,思路不同 + stack.push(i); + } + // 情况一 + if (heights[top] > heights[i]) { + while (stack.length > 0 && heights[top] > heights[i]) { + // 栈顶元素出栈,并保存栈顶bar的索引 + const h = heights[stack.pop()]; + const left = stack.at(-1) ?? -1; + const w = i - left - 1; + // 计算面积,并取最大面积 + maxArea = Math.max(maxArea, w * h); + top = stack.at(-1); + } + stack.push(i); + } + } + return maxArea; +}; + +//单调栈 简洁 +var largestRectangleArea = function(heights) { + let maxArea = 0; + const stack = []; + heights = [0,...heights,0]; // 数组头部加入元素0 数组尾部加入元素0 + for(let i = 0; i < heights.length; i++){ // 只用考虑情况一 当前遍历的元素heights[i]小于栈顶元素heights[stack[stack.length-1]]]的情况 + while(heights[i] < heights[stack[stack.length-1]]){// 当前bar比栈顶bar矮 + const stackTopIndex = stack.pop();// 栈顶元素出栈,并保存栈顶bar的索引 + let w = i - stack[stack.length -1] - 1; + let h = heights[stackTopIndex] + // 计算面积,并取最大面积 + maxArea = Math.max(maxArea, w * h); + } + stack.push(i);// 当前bar比栈顶bar高了,入栈 + } + return maxArea; +}; +``` +### TypeScript: + +> 暴力法(会超时): + +```typescript +function largestRectangleArea(heights: number[]): number { + let resMax: number = 0; + for (let i = 0, length = heights.length; i < length; i++) { + // 左开右开 + let left: number = i - 1, + right: number = i + 1; + while (left >= 0 && heights[left] >= heights[i]) { + left--; + } + while (right < length && heights[right] >= heights[i]) { + right++; + } + resMax = Math.max(resMax, heights[i] * (right - left - 1)); + } + return resMax; +}; +``` + +> 双指针预处理: + +```typescript +function largestRectangleArea(heights: number[]): number { + const length: number = heights.length; + const leftHeightDp: number[] = [], + rightHeightDp: number[] = []; + leftHeightDp[0] = -1; + rightHeightDp[length - 1] = length; + for (let i = 1; i < length; i++) { + let j = i - 1; + while (j >= 0 && heights[i] <= heights[j]) { + j = leftHeightDp[j]; + } + leftHeightDp[i] = j; + } + for (let i = length - 2; i >= 0; i--) { + let j = i + 1; + while (j < length && heights[i] <= heights[j]) { + j = rightHeightDp[j]; + } + rightHeightDp[i] = j; + } + let resMax: number = 0; + for (let i = 0; i < length; i++) { + let area = heights[i] * (rightHeightDp[i] - leftHeightDp[i] - 1); + resMax = Math.max(resMax, area); + } + return resMax; +}; +``` + +> 单调栈: + +```typescript +function largestRectangleArea(heights: number[]): number { + heights.push(0); + const length: number = heights.length; + // 栈底->栈顶:严格单调递增 + const stack: number[] = []; + stack.push(0); + let resMax: number = 0; + for (let i = 1; i < length; i++) { + let top = stack[stack.length - 1]; + if (heights[top] < heights[i]) { + stack.push(i); + } else if (heights[top] === heights[i]) { + stack.pop(); + stack.push(i); + } else { + while (stack.length > 0 && heights[top] > heights[i]) { + let mid = stack.pop(); + let left = stack.length > 0 ? stack[stack.length - 1] : -1; + let w = i - left - 1; + let h = heights[mid]; + resMax = Math.max(resMax, w * h); + top = stack[stack.length - 1]; + } + stack.push(i); + } + } + return resMax; +}; +``` + +### Rust: + +双指针预处理 +```rust + +impl Solution { + pub fn largest_rectangle_area(v: Vec) -> i32 { + let n = v.len(); + let mut left_smaller_idx = vec![-1; n]; + let mut right_smaller_idx = vec![n as i32; n]; + for i in 1..n { + let mut mid = i as i32 - 1; + while mid >= 0 && v[mid as usize] >= v[i] { + mid = left_smaller_idx[mid as usize]; + } + left_smaller_idx[i] = mid; + } + for i in (0..n-1).rev() { + let mut mid = i + 1; + while mid < n && v[mid] >= v[i] { + mid = right_smaller_idx[mid] as usize; + } + right_smaller_idx[i] = mid as i32; + } + let mut res = 0; + for (idx, &e) in v.iter().enumerate() { + res = res.max((right_smaller_idx[idx] - left_smaller_idx[idx] - 1) * e); + } + dbg!(res) + } +} +``` + +单调栈 +```rust +impl Solution { + pub fn largest_rectangle_area1(mut v: Vec) -> i32 { + v.insert(0, 0); // 便于使第一个元素能够有左侧<=它的值 + v.push(0); // 便于在结束处理最后一个元素后清空残留在栈中的值 + let mut res = 0; + let mut stack = vec![]; // 递增的栈 + for (idx, &e) in v.iter().enumerate() { + while !stack.is_empty() && v[*stack.last().unwrap()] > e { + let pos = stack.pop().unwrap(); + let prev_pos = *stack.last().unwrap(); + let s = (idx - prev_pos - 1) as i32 * v[pos]; + res = res.max(s); + } + stack.push(idx); + } + res + } +} +``` + + diff --git "a/problems/0090.\345\255\220\351\233\206II.md" "b/problems/0090.\345\255\220\351\233\206II.md" new file mode 100755 index 0000000000..2e8945c90f --- /dev/null +++ "b/problems/0090.\345\255\220\351\233\206II.md" @@ -0,0 +1,697 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 90.子集II + +[力扣题目链接](https://leetcode.cn/problems/subsets-ii/) + +给定一个可能包含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 + +说明:解集不能包含重复的子集。 + +示例: +* 输入: [1,2,2] +* 输出: +[ + [2], + [1], + [1,2,2], + [2,2], + [1,2], + [] +] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[回溯算法解决子集问题,如何去重?| LeetCode:90.子集II](https://www.bilibili.com/video/BV1vm4y1F71J/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +做本题之前一定要先做[78.子集](https://programmercarl.com/0078.子集.html)。 + +这道题目和[78.子集](https://programmercarl.com/0078.子集.html)区别就是集合里有重复元素了,而且求取的子集要去重。 + +那么关于回溯算法中的去重问题,**在[40.组合总和II](https://programmercarl.com/0040.组合总和II.html)中已经详细讲解过了,和本题是一个套路**。 + +**剧透一下,后期要讲解的排列问题里去重也是这个套路,所以理解“树层去重”和“树枝去重”非常重要**。 + +用示例中的[1, 2, 2] 来举例,如图所示: (**注意去重需要先对集合排序**) + +![90.子集II](https://file1.kamacoder.com/i/algo/20201124195411977.png) + +从图中可以看出,同一树层上重复取2 就要过滤掉,同一树枝上就可以重复取2,因为同一树枝上元素的集合才是唯一子集! + +本题就是其实就是[回溯算法:求子集问题!](https://programmercarl.com/0078.子集.html)的基础上加上了去重,去重我们在[回溯算法:求组合总和(三)](https://programmercarl.com/0040.组合总和II.html)也讲过了,所以我就直接给出代码了: + +C++代码如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& nums, int startIndex, vector& used) { + result.push_back(path); + for (int i = startIndex; i < nums.size(); i++) { + // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 + // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + // 而我们要对同一树层使用过的元素进行跳过 + if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { + continue; + } + path.push_back(nums[i]); + used[i] = true; + backtracking(nums, i + 1, used); + used[i] = false; + path.pop_back(); + } + } + +public: + vector> subsetsWithDup(vector& nums) { + result.clear(); + path.clear(); + vector used(nums.size(), false); + sort(nums.begin(), nums.end()); // 去重需要排序 + backtracking(nums, 0, used); + return result; + } +}; +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n) + + +使用set去重的版本。 +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& nums, int startIndex) { + result.push_back(path); + unordered_set uset; + for (int i = startIndex; i < nums.size(); i++) { + if (uset.find(nums[i]) != uset.end()) { + continue; + } + uset.insert(nums[i]); + path.push_back(nums[i]); + backtracking(nums, i + 1); + path.pop_back(); + } + } + +public: + vector> subsetsWithDup(vector& nums) { + result.clear(); + path.clear(); + sort(nums.begin(), nums.end()); // 去重需要排序 + backtracking(nums, 0); + return result; + } +}; +``` + +## 补充 + +本题也可以不使用used数组来去重,因为递归的时候下一个startIndex是i+1而不是0。 + +如果要是全排列的话,每次要从0开始遍历,为了跳过已入栈的元素,需要使用used。 + +代码如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& nums, int startIndex) { + result.push_back(path); + for (int i = startIndex; i < nums.size(); i++) { + // 而我们要对同一树层使用过的元素进行跳过 + if (i > startIndex && nums[i] == nums[i - 1] ) { // 注意这里使用i > startIndex + continue; + } + path.push_back(nums[i]); + backtracking(nums, i + 1); + path.pop_back(); + } + } + +public: + vector> subsetsWithDup(vector& nums) { + result.clear(); + path.clear(); + sort(nums.begin(), nums.end()); // 去重需要排序 + backtracking(nums, 0); + return result; + } +}; +``` + +## 总结 + +其实这道题目的知识点,我们之前都讲过了,如果之前讲过的子集问题和去重问题都掌握的好,这道题目应该分分钟AC。 + + +## 其他语言版本 + + +### Java +使用used数组 +```java +class Solution { + List> result = new ArrayList<>();// 存放符合条件结果的集合 + LinkedList path = new LinkedList<>();// 用来存放符合条件结果 + boolean[] used; + public List> subsetsWithDup(int[] nums) { + if (nums.length == 0){ + result.add(path); + return result; + } + Arrays.sort(nums); + used = new boolean[nums.length]; + subsetsWithDupHelper(nums, 0); + return result; + } + + private void subsetsWithDupHelper(int[] nums, int startIndex){ + result.add(new ArrayList<>(path)); + if (startIndex >= nums.length){ + return; + } + for (int i = startIndex; i < nums.length; i++){ + if (i > 0 && nums[i] == nums[i - 1] && !used[i - 1]){ + continue; + } + path.add(nums[i]); + used[i] = true; + subsetsWithDupHelper(nums, i + 1); + path.removeLast(); + used[i] = false; + } + } +} +``` + +不使用used数组 +```java +class Solution { + + List> res = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + + public List> subsetsWithDup( int[] nums ) { + Arrays.sort( nums ); + subsetsWithDupHelper( nums, 0 ); + return res; + } + + + private void subsetsWithDupHelper( int[] nums, int start ) { + res.add( new ArrayList<>( path ) ); + + for ( int i = start; i < nums.length; i++ ) { + // 跳过当前树层使用过的、相同的元素 + if ( i > start && nums[i - 1] == nums[i] ) { + continue; + } + path.add( nums[i] ); + subsetsWithDupHelper( nums, i + 1 ); + path.removeLast(); + } + } + +} +``` + + + +### Python3 + +回溯 利用used数组去重 +```python +class Solution: + def subsetsWithDup(self, nums): + result = [] + path = [] + used = [False] * len(nums) + nums.sort() # 去重需要排序 + self.backtracking(nums, 0, used, path, result) + return result + + def backtracking(self, nums, startIndex, used, path, result): + result.append(path[:]) # 收集子集 + for i in range(startIndex, len(nums)): + # used[i - 1] == True,说明同一树枝 nums[i - 1] 使用过 + # used[i - 1] == False,说明同一树层 nums[i - 1] 使用过 + # 而我们要对同一树层使用过的元素进行跳过 + if i > 0 and nums[i] == nums[i - 1] and not used[i - 1]: + continue + path.append(nums[i]) + used[i] = True + self.backtracking(nums, i + 1, used, path, result) + used[i] = False + path.pop() + +``` + +回溯 利用集合去重 + +```python +class Solution: + def subsetsWithDup(self, nums): + result = [] + path = [] + nums.sort() # 去重需要排序 + self.backtracking(nums, 0, path, result) + return result + + def backtracking(self, nums, startIndex, path, result): + result.append(path[:]) # 收集子集 + uset = set() + for i in range(startIndex, len(nums)): + if nums[i] in uset: + continue + uset.add(nums[i]) + path.append(nums[i]) + self.backtracking(nums, i + 1, path, result) + path.pop() + +``` + +回溯 利用递归的时候下一个startIndex是i+1而不是0去重 + +```python +class Solution: + def subsetsWithDup(self, nums): + result = [] + path = [] + nums.sort() # 去重需要排序 + self.backtracking(nums, 0, path, result) + return result + + def backtracking(self, nums, startIndex, path, result): + result.append(path[:]) # 收集子集 + for i in range(startIndex, len(nums)): + # 而我们要对同一树层使用过的元素进行跳过 + if i > startIndex and nums[i] == nums[i - 1]: + continue + path.append(nums[i]) + self.backtracking(nums, i + 1, path, result) + path.pop() + + +``` +### Go + +使用used数组 +```Go +var ( + result [][]int + path []int +) + +func subsetsWithDup(nums []int) [][]int { + result = make([][]int, 0) + path = make([]int, 0) + used := make([]bool, len(nums)) + sort.Ints(nums) // 去重需要排序 + backtracing(nums, 0, used) + return result +} + +func backtracing(nums []int, startIndex int, used []bool) { + tmp := make([]int, len(path)) + copy(tmp, path) + result = append(result, tmp) + for i := startIndex; i < len(nums); i++ { + // used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 + // used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + // 而我们要对同一树层使用过的元素进行跳过 + if i > 0 && nums[i] == nums[i-1] && used[i-1] == false { + continue + } + path = append(path, nums[i]) + used[i] = true + backtracing(nums, i + 1, used) + path = path[:len(path)-1] + used[i] = false + } +} +``` + +不使用used数组 +```Go +var ( + path []int + res [][]int +) +func subsetsWithDup(nums []int) [][]int { + path, res = make([]int, 0, len(nums)), make([][]int, 0) + sort.Ints(nums) + dfs(nums, 0) + return res +} + +func dfs(nums []int, start int) { + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + + for i := start; i < len(nums); i++ { + if i != start && nums[i] == nums[i-1] { + continue + } + path = append(path, nums[i]) + dfs(nums, i+1) + path = path[:len(path)-1] + } +} +``` + + +### JavaScript + +```Javascript + +var subsetsWithDup = function(nums) { + let result = [] + let path = [] + let sortNums = nums.sort((a, b) => { + return a - b + }) + function backtracing(startIndex, sortNums) { + result.push([...path]) + if(startIndex > nums.length - 1) { + return + } + for(let i = startIndex; i < nums.length; i++) { + if(i > startIndex && nums[i] === nums[i - 1]) { + continue + } + path.push(nums[i]) + backtracing(i + 1, sortNums) + path.pop() + } + } + backtracing(0, sortNums) + return result +}; + +``` + +### TypeScript + +```typescript +function subsetsWithDup(nums: number[]): number[][] { + nums.sort((a, b) => a - b); + const resArr: number[][] = []; + backTraking(nums, 0, []); + return resArr; + function backTraking(nums: number[], startIndex: number, route: number[]): void { + resArr.push([...route]); + let length: number = nums.length; + if (startIndex === length) return; + for (let i = startIndex; i < length; i++) { + if (i > startIndex && nums[i] === nums[i - 1]) continue; + route.push(nums[i]); + backTraking(nums, i + 1, route); + route.pop(); + } + } +}; +``` + +set去重版本: +```typescript +// 使用set去重版本 +function subsetsWithDup(nums: number[]): number[][] { + const result: number[][] = []; + const path: number[] = []; + // 去重之前先排序 + nums.sort((a, b) => a - b); + function backTracking(startIndex: number) { + // 收集结果 + result.push([...path]) + // 此处不返回也可以因为,每次递归都会使startIndex + 1,当这个数大到nums.length的时候就不会进入递归了。 + if (startIndex === nums.length) { + return + } + // 定义每一个树层的set集合 + const set: Set = new Set() + for (let i = startIndex; i < nums.length; i++) { + // 去重 + if (set.has(nums[i])) { + continue + } + set.add(nums[i]) + path.push(nums[i]) + backTracking(i + 1) + // 回溯 + path.pop() + } + } + backTracking(0) + return result +}; +``` +### Rust + +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, nums: &Vec, start_index: usize, used: &mut Vec) { + result.push(path.clone()); + let len = nums.len(); + // if start_index >= len { return; } + for i in start_index..len { + if i > 0 && nums[i] == nums[i - 1] && !used[i - 1] { continue; } + path.push(nums[i]); + used[i] = true; + Self::backtracking(result, path, nums, i + 1, used); + used[i] = false; + path.pop(); + } + } + + pub fn subsets_with_dup(mut nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + let mut used = vec![false; nums.len()]; + nums.sort(); + Self::backtracking(&mut result, &mut path, &nums, 0, &mut used); + result + } +} +``` + +set 去重版本 + +```rust +use std::collections::HashSet; +impl Solution { + pub fn subsets_with_dup(mut nums: Vec) -> Vec> { + let mut res = HashSet::new(); + let mut path = vec![]; + nums.sort(); + Self::backtracking(&nums, &mut path, &mut res, 0); + res.into_iter().collect() + } + + pub fn backtracking( + nums: &Vec, + path: &mut Vec, + res: &mut HashSet>, + start_index: usize, + ) { + res.insert(path.clone()); + for i in start_index..nums.len() { + path.push(nums[i]); + Self::backtracking(nums, path, res, i + 1); + path.pop(); + } + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; +//负责存放二维数组中每个数组的长度 +int* lengths; +//快排cmp函数 +int cmp(const void* a, const void* b) { + return *((int*)a) - *((int*)b); +} + +//复制函数,将当前path中的元素复制到ans中。同时记录path长度 +void copy() { + int* tempPath = (int*)malloc(sizeof(int) * pathTop); + int i; + for(i = 0; i < pathTop; i++) { + tempPath[i] = path[i]; + } + ans = (int**)realloc(ans, sizeof(int*) * (ansTop + 1)); + lengths[ansTop] = pathTop; + ans[ansTop++] = tempPath; +} + +void backTracking(int* nums, int numsSize, int startIndex, int* used) { + //首先将当前path复制 + copy(); + //若startIndex大于数组最后一位元素的位置,返回 + if(startIndex >= numsSize) + return ; + + int i; + for(i = startIndex; i < numsSize; i++) { + //对同一树层使用过的元素进行跳过 + if(i > 0 && nums[i] == nums[i-1] && used[i-1] == false) + continue; + path[pathTop++] = nums[i]; + used[i] = true; + backTracking(nums, numsSize, i + 1, used); + used[i] = false; + pathTop--; + } +} + +int** subsetsWithDup(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){ + //声明辅助变量 + path = (int*)malloc(sizeof(int) * numsSize); + ans = (int**)malloc(0); + lengths = (int*)malloc(sizeof(int) * 1500); + int* used = (int*)malloc(sizeof(int) * numsSize); + pathTop = ansTop = 0; + + //排序后查重才能生效 + qsort(nums, numsSize, sizeof(int), cmp); + backTracking(nums, numsSize, 0, used); + + //设置一维数组和二维数组的返回大小 + *returnSize = ansTop; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; i++) { + (*returnColumnSizes)[i] = lengths[i]; + } + return ans; +} +``` + +### Swift + +```swift +func subsetsWithDup(_ nums: [Int]) -> [[Int]] { + let nums = nums.sorted() + var result = [[Int]]() + var path = [Int]() + func backtracking(startIndex: Int) { + // 直接收集结果 + result.append(path) + + let end = nums.count + guard startIndex < end else { return } // 终止条件 + for i in startIndex ..< end { + if i > startIndex, nums[i] == nums[i - 1] { continue } // 跳过重复元素 + path.append(nums[i]) // 处理:收集元素 + backtracking(startIndex: i + 1) // 元素不重复访问 + path.removeLast() // 回溯 + } + } + backtracking(startIndex: 0) + return result +} +``` + +### Scala + +不使用used数组: + +```scala +object Solution { + import scala.collection.mutable + def subsetsWithDup(nums: Array[Int]): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + var num = nums.sorted // 排序 + + def backtracking(startIndex: Int): Unit = { + result.append(path.toList) + if (startIndex >= num.size){ + return + } + for (i <- startIndex until num.size) { + // 同一树层重复的元素不进入回溯 + if (!(i > startIndex && num(i) == num(i - 1))) { + path.append(num(i)) + backtracking(i + 1) + path.remove(path.size - 1) + } + } + } + + backtracking(0) + result.toList + } +} +``` + +使用Set去重: +```scala +object Solution { + import scala.collection.mutable + def subsetsWithDup(nums: Array[Int]): List[List[Int]] = { + var result = mutable.Set[List[Int]]() + var num = nums.sorted + def backtracking(path: mutable.ListBuffer[Int], startIndex: Int): Unit = { + if (startIndex == num.length) { + result.add(path.toList) + return + } + path.append(num(startIndex)) + backtracking(path, startIndex + 1) // 选择 + path.remove(path.size - 1) + backtracking(path, startIndex + 1) // 不选择 + } + + backtracking(mutable.ListBuffer[Int](), 0) + + result.toList + } +} +``` +### C# +```csharp +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> SubsetsWithDup(int[] nums) + { + Array.Sort(nums); + BackTracking(nums, 0); + return res; + } + public void BackTracking(int[] nums, int start) + { + res.Add(new List(path)); + for (int i = start; i < nums.Length; i++) + { + if (i > start && nums[i] == nums[i - 1]) continue; + path.Add(nums[i]); + BackTracking(nums, i + 1); + path.RemoveAt(path.Count - 1); + } + } +} +``` + + diff --git "a/problems/0093.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" "b/problems/0093.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" new file mode 100755 index 0000000000..6fa732d0c1 --- /dev/null +++ "b/problems/0093.\345\244\215\345\216\237IP\345\234\260\345\235\200.md" @@ -0,0 +1,876 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 93.复原IP地址 + +[力扣题目链接](https://leetcode.cn/problems/restore-ip-addresses/) + +给定一个只包含数字的字符串,复原它并返回所有可能的 IP 地址格式。 + +有效的 IP 地址 正好由四个整数(每个整数位于 0 到 255 之间组成,且不能含有前导 0),整数之间用 '.' 分隔。 + +例如:"0.1.2.201" 和 "192.168.1.1" 是 有效的 IP 地址,但是 "0.011.255.245"、"192.168.1.312" 和 "192.168@1.1" 是 无效的 IP 地址。 + +示例 1: +* 输入:s = "25525511135" +* 输出:["255.255.11.135","255.255.111.35"] + +示例 2: +* 输入:s = "0000" +* 输出:["0.0.0.0"] + +示例 3: +* 输入:s = "1111" +* 输出:["1.1.1.1"] + +示例 4: +* 输入:s = "010010" +* 输出:["0.10.0.10","0.100.1.0"] + +示例 5: +* 输入:s = "101023" +* 输出:["1.0.10.23","1.0.102.3","10.1.0.23","10.10.2.3","101.0.2.3"] + +提示: +* 0 <= s.length <= 3000 +* s 仅由数字组成 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[回溯算法如何分割字符串并判断是合法IP?| LeetCode:93.复原IP地址](https://www.bilibili.com/video/BV1XP4y1U73i/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +做这道题目之前,最好先把[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)这个做了。 + +这道题目相信大家刚看的时候,应该会一脸茫然。 + +其实只要意识到这是切割问题,**切割问题就可以使用回溯搜索法把所有可能性搜出来**,和刚做过的[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)就十分类似了。 + +切割问题可以抽象为树型结构,如图: + + +![93.复原IP地址](https://file1.kamacoder.com/i/algo/20201123203735933.png) + + +### 回溯三部曲 + +* 递归参数 + +在[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)中我们就提到切割问题类似组合问题。 + +startIndex一定是需要的,因为不能重复分割,记录下一层递归分割的起始位置。 + +本题我们还需要一个变量pointNum,记录添加逗点的数量。 + +所以代码如下: + +```cpp +vector result;// 记录结果 +// startIndex: 搜索的起始位置,pointNum:添加逗点的数量 +void backtracking(string& s, int startIndex, int pointNum) { +``` + +* 递归终止条件 + +终止条件和[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)情况就不同了,本题明确要求只会分成4段,所以不能用切割线切到最后作为终止条件,而是分割的段数作为终止条件。 + +pointNum表示逗点数量,pointNum为3说明字符串分成了4段了。 + +然后验证一下第四段是否合法,如果合法就加入到结果集里 + +代码如下: + +```cpp +if (pointNum == 3) { // 逗点数量为3时,分隔结束 + // 判断第四段子字符串是否合法,如果合法就放进result中 + if (isValid(s, startIndex, s.size() - 1)) { + result.push_back(s); + } + return; +} +``` + +* 单层搜索的逻辑 + +在[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)中已经讲过在循环遍历中如何截取子串。 + +在`for (int i = startIndex; i < s.size(); i++)`循环中 [startIndex, i] 这个区间就是截取的子串,需要判断这个子串是否合法。 + +如果合法就在字符串后面加上符号`.`表示已经分割。 + +如果不合法就结束本层循环,如图中剪掉的分支: + + +![93.复原IP地址](https://file1.kamacoder.com/i/algo/20201123203735933-20230310132314109.png) + +然后就是递归和回溯的过程: + +递归调用时,下一层递归的startIndex要从i+2开始(因为需要在字符串中加入了分隔符`.`),同时记录分割符的数量pointNum 要 +1。 + +回溯的时候,就将刚刚加入的分隔符`.` 删掉就可以了,pointNum也要-1。 + +代码如下: + +```CPP +for (int i = startIndex; i < s.size(); i++) { + if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法 + s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点 + pointNum++; + backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2 + pointNum--; // 回溯 + s.erase(s.begin() + i + 1); // 回溯删掉逗点 + } else break; // 不合法,直接结束本层循环 +} +``` + +### 判断子串是否合法 + +最后就是在写一个判断段位是否是有效段位了。 + +主要考虑到如下三点: + +* 段位以0为开头的数字不合法 +* 段位里有非正整数字符不合法 +* 段位如果大于255了不合法 + +代码如下: + +```CPP +// 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法 +bool isValid(const string& s, int start, int end) { + if (start > end) { + return false; + } + if (s[start] == '0' && start != end) { // 0开头的数字不合法 + return false; + } + int num = 0; + for (int i = start; i <= end; i++) { + if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法 + return false; + } + num = num * 10 + (s[i] - '0'); + if (num > 255) { // 如果大于255了不合法 + return false; + } + } + return true; +} +``` + + +根据[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)给出的回溯算法模板: + +``` +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} +``` + +可以写出如下回溯算法C++代码: + +```CPP +class Solution { +private: + vector result;// 记录结果 + // startIndex: 搜索的起始位置,pointNum:添加逗点的数量 + void backtracking(string& s, int startIndex, int pointNum) { + if (pointNum == 3) { // 逗点数量为3时,分隔结束 + // 判断第四段子字符串是否合法,如果合法就放进result中 + if (isValid(s, startIndex, s.size() - 1)) { + result.push_back(s); + } + return; + } + for (int i = startIndex; i < s.size(); i++) { + if (isValid(s, startIndex, i)) { // 判断 [startIndex,i] 这个区间的子串是否合法 + s.insert(s.begin() + i + 1 , '.'); // 在i的后面插入一个逗点 + pointNum++; + backtracking(s, i + 2, pointNum); // 插入逗点之后下一个子串的起始位置为i+2 + pointNum--; // 回溯 + s.erase(s.begin() + i + 1); // 回溯删掉逗点 + } else break; // 不合法,直接结束本层循环 + } + } + // 判断字符串s在左闭右闭区间[start, end]所组成的数字是否合法 + bool isValid(const string& s, int start, int end) { + if (start > end) { + return false; + } + if (s[start] == '0' && start != end) { // 0开头的数字不合法 + return false; + } + int num = 0; + for (int i = start; i <= end; i++) { + if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法 + return false; + } + num = num * 10 + (s[i] - '0'); + if (num > 255) { // 如果大于255了不合法 + return false; + } + } + return true; + } +public: + vector restoreIpAddresses(string s) { + result.clear(); + if (s.size() < 4 || s.size() > 12) return result; // 算是剪枝了 + backtracking(s, 0, 0); + return result; + } +}; + +``` +* 时间复杂度: O(3^4),IP地址最多包含4个数字,每个数字最多有3种可能的分割方式,则搜索树的最大深度为4,每个节点最多有3个子节点。 +* 空间复杂度: O(n) + +## 总结 + +在[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)中我列举的分割字符串的难点,本题都覆盖了。 + +而且本题还需要操作字符串添加逗号作为分隔符,并验证区间的合法性。 + +可以说是[131.分割回文串](https://programmercarl.com/0131.分割回文串.html)的加强版。 + +在本文的树形结构图中,我已经把详细的分析思路都画了出来,相信大家看了之后一定会思路清晰不少! + + + +## 其他语言版本 + +### Java + +```java +class Solution { + List result = new ArrayList<>(); + + public List restoreIpAddresses(String s) { + if (s.length() > 12) return result; // 算是剪枝了 + backTrack(s, 0, 0); + return result; + } + + // startIndex: 搜索的起始位置, pointNum:添加逗点的数量 + private void backTrack(String s, int startIndex, int pointNum) { + if (pointNum == 3) {// 逗点数量为3时,分隔结束 + // 判断第四段⼦字符串是否合法,如果合法就放进result中 + if (isValid(s,startIndex,s.length()-1)) { + result.add(s); + } + return; + } + for (int i = startIndex; i < s.length(); i++) { + if (isValid(s, startIndex, i)) { + s = s.substring(0, i + 1) + "." + s.substring(i + 1); //在str的后⾯插⼊⼀个逗点 + pointNum++; + backTrack(s, i + 2, pointNum);// 插⼊逗点之后下⼀个⼦串的起始位置为i+2 + pointNum--;// 回溯 + s = s.substring(0, i + 1) + s.substring(i + 2);// 回溯删掉逗点 + } else { + break; + } + } + } + + // 判断字符串s在左闭⼜闭区间[start, end]所组成的数字是否合法 + private Boolean isValid(String s, int start, int end) { + if (start > end) { + return false; + } + if (s.charAt(start) == '0' && start != end) { // 0开头的数字不合法 + return false; + } + int num = 0; + for (int i = start; i <= end; i++) { + if (s.charAt(i) > '9' || s.charAt(i) < '0') { // 遇到⾮数字字符不合法 + return false; + } + num = num * 10 + (s.charAt(i) - '0'); + if (num > 255) { // 如果⼤于255了不合法 + return false; + } + } + return true; + } +} +//方法一:但使用stringBuilder,故优化时间、空间复杂度,因为向字符串插入字符时无需复制整个字符串,从而减少了操作的时间复杂度,也不用开新空间存subString,从而减少了空间复杂度。 +class Solution { + List result = new ArrayList<>(); + public List restoreIpAddresses(String s) { + StringBuilder sb = new StringBuilder(s); + backTracking(sb, 0, 0); + return result; + } + private void backTracking(StringBuilder s, int startIndex, int dotCount){ + if(dotCount == 3){ + if(isValid(s, startIndex, s.length() - 1)){ + result.add(s.toString()); + } + return; + } + for(int i = startIndex; i < s.length(); i++){ + if(isValid(s, startIndex, i)){ + s.insert(i + 1, '.'); + backTracking(s, i + 2, dotCount + 1); + s.deleteCharAt(i + 1); + }else{ + break; + } + } + } + //[start, end] + private boolean isValid(StringBuilder s, int start, int end){ + if(start > end) + return false; + if(s.charAt(start) == '0' && start != end) + return false; + int num = 0; + for(int i = start; i <= end; i++){ + int digit = s.charAt(i) - '0'; + num = num * 10 + digit; + if(num > 255) + return false; + } + return true; + } +} + +//方法二:比上面的方法时间复杂度低,更好地剪枝,优化时间复杂度 +class Solution { + List result = new ArrayList(); + StringBuilder stringBuilder = new StringBuilder(); + + public List restoreIpAddresses(String s) { + restoreIpAddressesHandler(s, 0, 0); + return result; + } + + // number表示stringbuilder中ip段的数量 + public void restoreIpAddressesHandler(String s, int start, int number) { + // 如果start等于s的长度并且ip段的数量是4,则加入结果集,并返回 + if (start == s.length() && number == 4) { + result.add(stringBuilder.toString()); + return; + } + // 如果start等于s的长度但是ip段的数量不为4,或者ip段的数量为4但是start小于s的长度,则直接返回 + if (start == s.length() || number == 4) { + return; + } + // 剪枝:ip段的长度最大是3,并且ip段处于[0,255] + for (int i = start; i < s.length() && i - start < 3 && Integer.parseInt(s.substring(start, i + 1)) >= 0 + && Integer.parseInt(s.substring(start, i + 1)) <= 255; i++) { + if (i + 1 - start > 1 && s.charAt(start) - '0' == 0) { + break; + } + stringBuilder.append(s.substring(start, i + 1)); + // 当stringBuilder里的网段数量小于3时,才会加点;如果等于3,说明已经有3段了,最后一段不需要再加点 + if (number < 3) { + stringBuilder.append("."); + } + number++; + restoreIpAddressesHandler(s, i + 1, number); + number--; + // 删除当前stringBuilder最后一个网段,注意考虑点的数量的问题 + stringBuilder.delete(start + number, i + number + 2); + } + } +} +``` + + +### Python + +回溯(版本一) +```python +class Solution: + def restoreIpAddresses(self, s: str) -> List[str]: + result = [] + self.backtracking(s, 0, 0, "", result) + return result + + def backtracking(self, s, start_index, point_num, current, result): + if point_num == 3: # 逗点数量为3时,分隔结束 + if self.is_valid(s, start_index, len(s) - 1): # 判断第四段子字符串是否合法 + current += s[start_index:] # 添加最后一段子字符串 + result.append(current) + return + + for i in range(start_index, len(s)): + if self.is_valid(s, start_index, i): # 判断 [start_index, i] 这个区间的子串是否合法 + sub = s[start_index:i + 1] + self.backtracking(s, i + 1, point_num + 1, current + sub + '.', result) + else: + break + + def is_valid(self, s, start, end): + if start > end: + return False + if s[start] == '0' and start != end: # 0开头的数字不合法 + return False + num = 0 + for i in range(start, end + 1): + if not s[i].isdigit(): # 遇到非数字字符不合法 + return False + num = num * 10 + int(s[i]) + if num > 255: # 如果大于255了不合法 + return False + return True + +``` +回溯(版本二) + +```python +class Solution: + def restoreIpAddresses(self, s: str) -> List[str]: + results = [] + self.backtracking(s, 0, [], results) + return results + + def backtracking(self, s, index, path, results): + if index == len(s) and len(path) == 4: + results.append('.'.join(path)) + return + + if len(path) > 4: # 剪枝 + return + + for i in range(index, min(index + 3, len(s))): + if self.is_valid(s, index, i): + sub = s[index:i+1] + path.append(sub) + self.backtracking(s, i+1, path, results) + path.pop() + + def is_valid(self, s, start, end): + if start > end: + return False + if s[start] == '0' and start != end: # 0开头的数字不合法 + return False + num = int(s[start:end+1]) + return 0 <= num <= 255 + +回溯(版本三) + +```python +class Solution: + def restoreIpAddresses(self, s: str) -> List[str]: + result = [] + self.backtracking(s, 0, [], result) + return result + + def backtracking(self, s, startIndex, path, result): + if startIndex == len(s): + result.append('.'.join(path[:])) + return + + for i in range(startIndex, min(startIndex+3, len(s))): + # 如果 i 往后遍历了,并且当前地址的第一个元素是 0 ,就直接退出 + if i > startIndex and s[startIndex] == '0': + break + # 比如 s 长度为 5,当前遍历到 i = 3 这个元素 + # 因为还没有执行任何操作,所以此时剩下的元素数量就是 5 - 3 = 2 ,即包括当前的 i 本身 + # path 里面是当前包含的子串,所以有几个元素就表示储存了几个地址 + # 所以 (4 - len(path)) * 3 表示当前路径至多能存放的元素个数 + # 4 - len(path) 表示至少要存放的元素个数 + if (4 - len(path)) * 3 < len(s) - i or 4 - len(path) > len(s) - i: + break + if i - startIndex == 2: + if not int(s[startIndex:i+1]) <= 255: + break + path.append(s[startIndex:i+1]) + self.backtracking(s, i+1, path, result) + path.pop() +``` + +### Go + +```go +var ( + path []string + res []string +) +func restoreIpAddresses(s string) []string { + path, res = make([]string, 0, len(s)), make([]string, 0) + dfs(s, 0) + return res +} +func dfs(s string, start int) { + if len(path) == 4 { // 够四段后就不再继续往下递归 + if start == len(s) { + str := strings.Join(path, ".") + res = append(res, str) + } + return + } + for i := start; i < len(s); i++ { + if i != start && s[start] == '0' { // 含有前导 0,无效 + break + } + str := s[start : i+1] + num, _ := strconv.Atoi(str) + if num >= 0 && num <= 255 { + path = append(path, str) // 符合条件的就进入下一层 + dfs(s, i+1) + path = path[:len(path) - 1] + } else { // 如果不满足条件,再往后也不可能满足条件,直接退出 + break + } + } +} +``` + +### JavaScript + +```js +/** + * @param {string} s + * @return {string[]} + */ +var restoreIpAddresses = function(s) { + const res = [], path = []; + backtracking(0, 0) + return res; + function backtracking(i) { + const len = path.length; + if(len > 4) return; + if(len === 4 && i === s.length) { + res.push(path.join(".")); + return; + } + for(let j = i; j < s.length; j++) { + const str = s.slice(i, j + 1); + if(str.length > 3 || +str > 255) break; + if(str.length > 1 && str[0] === "0") break; + path.push(str); + backtracking(j + 1); + path.pop() + } + } +}; +``` + +### TypeScript + +```typescript +function isValidIpSegment(str: string): boolean { + let resBool: boolean = true; + let tempVal: number = Number(str); + if ( + str.length === 0 || isNaN(tempVal) || + tempVal > 255 || tempVal < 0 || + (str.length > 1 && str[0] === '0') + ) { + resBool = false; + } + return resBool; +} +function restoreIpAddresses(s: string): string[] { + const resArr: string[] = []; + backTracking(s, 0, []); + return resArr; + function backTracking(s: string, startIndex: number, route: string[]): void { + let length: number = s.length; + if (route.length === 4 && startIndex >= length) { + resArr.push(route.join('.')); + return; + } + if (route.length === 4 || startIndex >= length) return; + let tempStr: string = ''; + for (let i = startIndex + 1; i <= Math.min(length, startIndex + 3); i++) { + tempStr = s.slice(startIndex, i); + if (isValidIpSegment(tempStr)) { + route.push(s.slice(startIndex, i)); + backTracking(s, i, route); + route.pop(); + } + } + } +}; +``` + +### Rust + +```Rust +impl Solution { + fn is_valid(s: &[char], start: usize, end: usize) -> bool { + if start > end { + return false; + } + if s[start] == '0' && start != end { + return false; + } + let mut num = 0; + for &c in s.iter().take(end + 1).skip(start) { + if !('0'..='9').contains(&c) { + return false; + } + if let Some(digit) = c.to_digit(10) { + num = num * 10 + digit; + } + if num > 255 { + return false; + } + } + true + } + + fn backtracking(result: &mut Vec, s: &mut Vec, start_index: usize, mut point_num: usize) { + let len = s.len(); + if point_num == 3 { + if Self::is_valid(s, start_index, len - 1) { + result.push(s.iter().collect::()); + } + return; + } + for i in start_index..len { + if Self::is_valid(s, start_index, i) { + point_num += 1; + s.insert(i + 1, '.'); + Self::backtracking(result, s, i + 2, point_num); + point_num -= 1; + s.remove(i + 1); + } else { break; } + } + } + + pub fn restore_ip_addresses(s: String) -> Vec { + let mut result: Vec = Vec::new(); + let len = s.len(); + if len < 4 || len > 12 { return result; } + let mut s = s.chars().collect::>(); + Self::backtracking(&mut result, &mut s, 0, 0); + result + } + +} +``` + +### C +```c +//记录结果 +char** result; +int resultTop; +//记录应该加入'.'的位置 +int segments[3]; +int isValid(char* s, int start, int end) { + if(start > end) + return 0; + if (s[start] == '0' && start != end) { // 0开头的数字不合法 + return false; + } + int num = 0; + for (int i = start; i <= end; i++) { + if (s[i] > '9' || s[i] < '0') { // 遇到非数字字符不合法 + return false; + } + num = num * 10 + (s[i] - '0'); + if (num > 255) { // 如果大于255了不合法 + return false; + } + } + return true; +} + +//startIndex为起始搜索位置,pointNum为'.'对象 +void backTracking(char* s, int startIndex, int pointNum) { + //若'.'数量为3,分隔结束 + if(pointNum == 3) { + //若最后一段字符串符合要求,将当前的字符串放入result种 + if(isValid(s, startIndex, strlen(s) - 1)) { + char* tempString = (char*)malloc(sizeof(char) * strlen(s) + 4); + int j; + //记录添加字符时tempString的下标 + int count = 0; + //记录添加字符时'.'的使用数量 + int count1 = 0; + for(j = 0; j < strlen(s); j++) { + tempString[count++] = s[j]; + //若'.'的使用数量小于3且当前下标等于'.'下标,添加'.'到数组 + if(count1 < 3 && j == segments[count1]) { + tempString[count++] = '.'; + count1++; + } + } + tempString[count] = 0; + //扩容result数组 + result = (char**)realloc(result, sizeof(char*) * (resultTop + 1)); + result[resultTop++] = tempString; + } + return ; + } + + int i; + for(i = startIndex; i < strlen(s); i++) { + if(isValid(s, startIndex, i)) { + //记录应该添加'.'的位置 + segments[pointNum] = i; + backTracking(s, i + 1, pointNum + 1); + } + else { + break; + } + } +} + +char ** restoreIpAddresses(char * s, int* returnSize){ + result = (char**)malloc(0); + resultTop = 0; + backTracking(s, 0, 0); + *returnSize = resultTop; + return result; +} +``` + +### Swift + +```swift +// 判断区间段是否合法 +func isValid(s: [Character], start: Int, end: Int) -> Bool { + guard start <= end, start >= 0, end < s.count else { return false } // 索引不合法 + if start != end, s[start] == "0" { return false } // 以0开头的多位数字不合法 + var num = 0 + for i in start ... end { + let c = s[i] + guard c >= "0", c <= "9" else { return false } // 非数字不合法 + let value = c.asciiValue! - ("0" as Character).asciiValue! + num = num * 10 + Int(value) + guard num <= 255 else { return false } // 大于255不合法 + } + return true +} +func restoreIpAddresses(_ s: String) -> [String] { + var s = Array(s) // 转换成字符数组以便于比较 + var result = [String]() // 结果 + func backtracking(startIndex: Int, pointCount: Int) { + guard startIndex < s.count else { return } // 索引不合法 + // 结束条件 + if pointCount == 3 { + // 最后一段也合法,则收集结果 + if isValid(s: s, start: startIndex, end: s.count - 1) { + result.append(String(s)) + } + return + } + + for i in startIndex ..< s.count { + // 判断[starIndex, i]子串是否合法,合法则插入“.”,否则结束本层循环 + if isValid(s: s, start: startIndex, end: i) { + s.insert(".", at: i + 1) // 子串后面插入“.” + backtracking(startIndex: i + 2, pointCount: pointCount + 1) // 注意这里时跳2位,且通过pointCount + 1局部变量隐藏了pointCount的回溯 + s.remove(at: i + 1) // 回溯 + } else { + break + } + } + } + backtracking(startIndex: 0, pointCount: 0) + return result +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def restoreIpAddresses(s: String): List[String] = { + var result = mutable.ListBuffer[String]() + if (s.size < 4 || s.length > 12) return result.toList + var path = mutable.ListBuffer[String]() + + // 判断IP中的一个字段是否为正确的 + def isIP(sub: String): Boolean = { + if (sub.size > 1 && sub(0) == '0') return false + if (sub.toInt > 255) return false + true + } + + def backtracking(startIndex: Int): Unit = { + if (startIndex >= s.size) { + if (path.size == 4) { + result.append(path.mkString(".")) // mkString方法可以把集合里的数据以指定字符串拼接 + return + } + return + } + // subString + for (i <- startIndex until startIndex + 3 if i < s.size) { + var subString = s.substring(startIndex, i + 1) + if (isIP(subString)) { // 如果合法则进行下一轮 + path.append(subString) + backtracking(i + 1) + path = path.take(path.size - 1) + } + } + } + + backtracking(0) + result.toList + } +} +``` +### C# +```csharp +public class Solution +{ + public IList res = new List(); + public IList RestoreIpAddresses(string s) + { + if (s.Length < 4 || s.Length > 12) return res; + BackTracking(s, 0, 0); + return res; + } + public void BackTracking(string s, int start, int pointSum) + { + if (pointSum == 3) + { + if (IsValid(s, start, s.Length - 1)) + { + res.Add(s); + } + return; + } + for (int i = start; i < s.Length; i++) + { + if (IsValid(s, start, i)) + { + s = s.Insert(i + 1, "."); + BackTracking(s, i + 2, pointSum + 1); + s = s.Remove(i + 1, 1); + } + else break; + } + } + public bool IsValid(string s, int start, int end) + { + if (start > end) return false; + if (s[start] == '0' && start != end) return false; + int num = 0; + for (int i = start; i <= end; i++) + { + if (s[i] > '9' || s[i] < '0') return false; + num = num * 10 + s[i] - '0'; + if (num > 255) return false; + } + return true; + } +} +``` + + + diff --git "a/problems/0096.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0096.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" new file mode 100755 index 0000000000..e5bc2b6b65 --- /dev/null +++ "b/problems/0096.\344\270\215\345\220\214\347\232\204\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,348 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 96.不同的二叉搜索树 + +[力扣题目链接](https://leetcode.cn/problems/unique-binary-search-trees/) + +给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种? + +示例: + +![](https://file1.kamacoder.com/i/algo/20210113161941835.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划找到子状态之间的关系很重要!| LeetCode:96.不同的二叉搜索树](https://www.bilibili.com/video/BV1eK411o7QA/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目描述很简短,但估计大部分同学看完都是懵懵的状态,这得怎么统计呢? + +关于什么是二叉搜索树,我们之前在讲解二叉树专题的时候已经详细讲解过了,也可以看看这篇[二叉树:二叉搜索树登场!](https://programmercarl.com/0700.二叉搜索树中的搜索.html)再回顾一波。 + +了解了二叉搜索树之后,我们应该先举几个例子,画画图,看看有没有什么规律,如图: + +![96.不同的二叉搜索树](https://file1.kamacoder.com/i/algo/20210107093106367.png) + +n为1的时候有一棵树,n为2有两棵树,这个是很直观的。 + +![96.不同的二叉搜索树1](https://file1.kamacoder.com/i/algo/20210107093129889.png) + +来看看n为3的时候,有哪几种情况。 + +当1为头结点的时候,其右子树有两个节点,看这两个节点的布局,是不是和 n 为2的时候两棵树的布局是一样的啊! + +(可能有同学问了,这布局不一样啊,节点数值都不一样。别忘了我们就是求不同树的数量,并不用把搜索树都列出来,所以不用关心其具体数值的差异) + +当3为头结点的时候,其左子树有两个节点,看这两个节点的布局,是不是和n为2的时候两棵树的布局也是一样的啊! + +当2为头结点的时候,其左右子树都只有一个节点,布局是不是和n为1的时候只有一棵树的布局也是一样的啊! + +发现到这里,其实我们就找到了重叠子问题了,其实也就是发现可以通过dp[1] 和 dp[2] 来推导出来dp[3]的某种方式。 + +思考到这里,这道题目就有眉目了。 + +dp[3],就是 元素1为头结点搜索树的数量 + 元素2为头结点搜索树的数量 + 元素3为头结点搜索树的数量 + +元素1为头结点搜索树的数量 = 右子树有2个元素的搜索树数量 * 左子树有0个元素的搜索树数量 + +元素2为头结点搜索树的数量 = 右子树有1个元素的搜索树数量 * 左子树有1个元素的搜索树数量 + +元素3为头结点搜索树的数量 = 右子树有0个元素的搜索树数量 * 左子树有2个元素的搜索树数量 + +有2个元素的搜索树数量就是dp[2]。 + +有1个元素的搜索树数量就是dp[1]。 + +有0个元素的搜索树数量就是dp[0]。 + +所以dp[3] = dp[2] * dp[0] + dp[1] * dp[1] + dp[0] * dp[2] + +如图所示: + +![96.不同的二叉搜索树2](https://file1.kamacoder.com/i/algo/20210107093226241.png) + + +此时我们已经找到递推关系了,那么可以用动规五部曲再系统分析一遍。 + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i] : 1到i为节点组成的二叉搜索树的个数为dp[i]**。 + +也可以理解是i个不同元素节点组成的二叉搜索树的个数为dp[i] ,都是一样的。 + +以下分析如果想不清楚,就来回想一下dp[i]的定义 + +2. 确定递推公式 + +在上面的分析中,其实已经看出其递推关系, dp[i] += dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] + +j相当于是头结点的元素,从1遍历到i为止。 + +所以递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量 + +3. dp数组如何初始化 + +初始化,只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。 + +那么dp[0]应该是多少呢? + +从定义上来讲,空节点也是一棵二叉树,也是一棵二叉搜索树,这是可以说得通的。 + +从递归公式上来讲,dp[以j为头结点左子树节点数量] * dp[以j为头结点右子树节点数量] 中以j为头结点左子树节点数量为0,也需要dp[以j为头结点左子树节点数量] = 1, 否则乘法的结果就都变成0了。 + +所以初始化dp[0] = 1 + +4. 确定遍历顺序 + +首先一定是遍历节点数,从递归公式:dp[i] += dp[j - 1] * dp[i - j]可以看出,节点数为i的状态是依靠 i之前节点数的状态。 + +那么遍历i里面每一个数作为头结点的状态,用j来遍历。 + +代码如下: + +```CPP +for (int i = 1; i <= n; i++) { + for (int j = 1; j <= i; j++) { + dp[i] += dp[j - 1] * dp[i - j]; + } +} +``` + +5. 举例推导dp数组 + +n为5时候的dp数组状态如图: + +![96.不同的二叉搜索树3](https://file1.kamacoder.com/i/algo/20210107093253987.png) + +当然如果自己画图举例的话,基本举例到n为3就可以了,n为4的时候,画图已经比较麻烦了。 + +**我这里列到了n为5的情况,是为了方便大家 debug代码的时候,把dp数组打出来,看看哪里有问题**。 + +综上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int numTrees(int n) { + vector dp(n + 1); + dp[0] = 1; + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= i; j++) { + dp[i] += dp[j - 1] * dp[i - j]; + } + } + return dp[n]; + } +}; +``` + +* 时间复杂度:$O(n^2)$ +* 空间复杂度:$O(n)$ + +大家应该发现了,我们分析了这么多,最后代码却如此简单! + +## 总结 + +这道题目虽然在力扣上标记是中等难度,但可以算是困难了! + +首先这道题想到用动规的方法来解决,就不太好想,需要举例,画图,分析,才能找到递推的关系。 + +然后难点就是确定递推公式了,如果把递推公式想清楚了,遍历顺序和初始化,就是自然而然的事情了。 + +可以看出我依然还是用动规五部曲来进行分析,会把题目的方方面面都覆盖到! + +**而且具体这五部分析是我自己平时总结的经验,找不出来第二个的,可能过一阵子 其他题解也会有动规五部曲了**。 + +当时我在用动规五部曲讲解斐波那契的时候,一些录友和我反应,感觉讲复杂了。 + +其实当时我一直强调简单题是用来练习方法论的,并不能因为简单我就代码一甩,简单解释一下就完事了。 + +可能当时一些同学不理解,现在大家应该感受方法论的重要性了,加油💪 + +## 其他语言版本 + + +### Java + +```Java +class Solution { + public int numTrees(int n) { + //初始化 dp 数组 + int[] dp = new int[n + 1]; + //初始化0个节点和1个节点的情况 + dp[0] = 1; + dp[1] = 1; + for (int i = 2; i <= n; i++) { + for (int j = 1; j <= i; j++) { + //对于第i个节点,需要考虑1作为根节点直到i作为根节点的情况,所以需要累加 + //一共i个节点,对于根节点j时,左子树的节点个数为j-1,右子树的节点个数为i-j + dp[i] += dp[j - 1] * dp[i - j]; + } + } + return dp[n]; + } +} +``` + +### Python + +```python +class Solution: + def numTrees(self, n: int) -> int: + dp = [0] * (n + 1) # 创建一个长度为n+1的数组,初始化为0 + dp[0] = 1 # 当n为0时,只有一种情况,即空树,所以dp[0] = 1 + for i in range(1, n + 1): # 遍历从1到n的每个数字 + for j in range(1, i + 1): # 对于每个数字i,计算以i为根节点的二叉搜索树的数量 + dp[i] += dp[j - 1] * dp[i - j] # 利用动态规划的思想,累加左子树和右子树的组合数量 + return dp[n] # 返回以1到n为节点的二叉搜索树的总数量 + +``` + +### Go + +```Go +func numTrees(n int)int{ + dp := make([]int, n+1) + dp[0] = 1 + for i := 1; i <= n; i++ { + for j := 1; j <= i; j++ { + dp[i] += dp[j-1] * dp[i-j] + } + } + return dp[n] +} +``` + +### JavaScript + +```Javascript +const numTrees =(n) => { + let dp = new Array(n+1).fill(0); + dp[0] = 1; + dp[1] = 1; + + for(let i = 2; i <= n; i++) { + for(let j = 1; j <= i; j++) { + dp[i] += dp[j-1] * dp[i-j]; + } + } + + return dp[n]; +}; +``` + +### TypeScript + +```typescript +function numTrees(n: number): number { + /** + dp[i]: i个节点对应的种树 + dp[0]: -1; 无意义; + dp[1]: 1; + ... + dp[i]: 2 * dp[i - 1] + + (dp[1] * dp[i - 2] + dp[2] * dp[i - 3] + ... + dp[i - 2] * dp[1]); 从1加到i-2 + */ + const dp: number[] = []; + dp[0] = -1; // 表示无意义 + dp[1] = 1; + for (let i = 2; i <= n; i++) { + dp[i] = 2 * dp[i - 1]; + for (let j = 1, end = i - 1; j < end; j++) { + dp[i] += dp[j] * dp[end - j]; + } + } + return dp[n]; +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn num_trees(n: i32) -> i32 { + let n = n as usize; + let mut dp = vec![0; n + 1]; + dp[0] = 1; + for i in 1..=n { + for j in 1..=i { + dp[i] += dp[j - 1] * dp[i - j]; + } + } + dp[n] + } +} +``` + +### C + +```c +//开辟dp数组 +int *initDP(int n) { + int *dp = (int *)malloc(sizeof(int) * (n + 1)); + int i; + for(i = 0; i <= n; ++i) + dp[i] = 0; + return dp; +} + +int numTrees(int n){ + //开辟dp数组 + int *dp = initDP(n); + //将dp[0]设为1 + dp[0] = 1; + + int i, j; + for(i = 1; i <= n; ++i) { + for(j = 1; j <= i; ++j) { + //递推公式:dp[i] = dp[i] + 根为j时左子树种类个数 * 根为j时右子树种类个数 + dp[i] += dp[j - 1] * dp[i - j]; + } + } + + return dp[n]; +} +``` + +### Scala + +```scala +object Solution { + def numTrees(n: Int): Int = { + var dp = new Array[Int](n + 1) + dp(0) = 1 + for (i <- 1 to n) { + for (j <- 1 to i) { + dp(i) += dp(j - 1) * dp(i - j) + } + } + dp(n) + } +} +``` +### C# +```csharp +public class Solution +{ + public int NumTrees(int n) + { + int[] dp = new int[n + 1]; + dp[0] = 1; + for (int i = 1; i <= n; i++) + { + for (int j = 1; j <= i; j++) + { + dp[i] += dp[j - 1] * dp[i - j]; + } + } + return dp[n]; + } +} +``` + diff --git "a/problems/0098.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0098.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" new file mode 100755 index 0000000000..990d3c8413 --- /dev/null +++ "b/problems/0098.\351\252\214\350\257\201\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,807 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 98.验证二叉搜索树 + +[力扣题目链接](https://leetcode.cn/problems/validate-binary-search-tree/) + + +给定一个二叉树,判断其是否是一个有效的二叉搜索树。 + +假设一个二叉搜索树具有如下特征: + +* 节点的左子树只包含小于当前节点的数。 +* 节点的右子树只包含大于当前节点的数。 +* 所有左子树和右子树自身必须也是二叉搜索树。 + +![98.验证二叉搜索树](https://file1.kamacoder.com/i/algo/20230310000750.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[你对二叉搜索树了解的还不够! | LeetCode:98.验证二叉搜索树](https://www.bilibili.com/video/BV18P411n7Q4),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。 + +有了这个特性,**验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。** + +### 递归法 + +可以递归中序遍历将二叉搜索树转变成一个数组,代码如下: + +```CPP +vector vec; +void traversal(TreeNode* root) { + if (root == NULL) return; + traversal(root->left); + vec.push_back(root->val); // 将二叉搜索树转换为有序数组 + traversal(root->right); +} +``` + +然后只要比较一下,这个数组是否是有序的,**注意二叉搜索树中不能有重复元素**。 + +```CPP +traversal(root); +for (int i = 1; i < vec.size(); i++) { + // 注意要小于等于,搜索树里不能有相同元素 + if (vec[i] <= vec[i - 1]) return false; +} +return true; +``` + +整体代码如下: + +```CPP +class Solution { +private: + vector vec; + void traversal(TreeNode* root) { + if (root == NULL) return; + traversal(root->left); + vec.push_back(root->val); // 将二叉搜索树转换为有序数组 + traversal(root->right); + } +public: + bool isValidBST(TreeNode* root) { + vec.clear(); // 不加这句在leetcode上也可以过,但最好加上 + traversal(root); + for (int i = 1; i < vec.size(); i++) { + // 注意要小于等于,搜索树里不能有相同元素 + if (vec[i] <= vec[i - 1]) return false; + } + return true; + } +}; +``` + +以上代码中,我们把二叉树转变为数组来判断,是最直观的,但其实不用转变成数组,可以在递归遍历的过程中直接判断是否有序。 + + +这道题目比较容易陷入两个陷阱: + +* 陷阱1 + +**不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**。 + +写出了类似这样的代码: + +```CPP +if (root->val > root->left->val && root->val < root->right->val) { + return true; +} else { + return false; +} +``` + +**我们要比较的是 左子树所有节点小于中间节点,右子树所有节点大于中间节点**。所以以上代码的判断逻辑是错误的。 + +例如: [10,5,15,null,null,6,20] 这个case: + +![二叉搜索树](https://file1.kamacoder.com/i/algo/20230310000824.png) + +节点10大于左节点5,小于右节点15,但右子树里出现了一个6 这就不符合了! + +* 陷阱2 + +样例中最小节点 可能是int的最小值,如果这样使用最小的int来比较也是不行的。 + +此时可以初始化比较元素为longlong的最小值。 + +问题可以进一步演进:如果样例中根节点的val 可能是longlong的最小值 又要怎么办呢?文中会解答。 + +了解这些陷阱之后我们来看一下代码应该怎么写: + +递归三部曲: + +* 确定递归函数,返回值以及参数 + +要定义一个longlong的全局变量,用来比较遍历的节点是否有序,因为后台测试数据中有int最小值,所以定义为longlong的类型,初始化为longlong最小值。 + +注意递归函数要有bool类型的返回值, 我们在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html) 中讲了,只有寻找某一条边(或者一个节点)的时候,递归函数会有bool类型的返回值。 + +其实本题是同样的道理,我们在寻找一个不符合条件的节点,如果没有找到这个节点就遍历了整个树,如果找到不符合的节点了,立刻返回。 + +代码如下: + +```CPP +long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值 +bool isValidBST(TreeNode* root) +``` + +* 确定终止条件 + +如果是空节点 是不是二叉搜索树呢? + +是的,二叉搜索树也可以为空! + +代码如下: + +```CPP +if (root == NULL) return true; +``` + +* 确定单层递归的逻辑 + +中序遍历,一直更新maxVal,一旦发现maxVal >= root->val,就返回false,注意元素相同时候也要返回false。 + +代码如下: + +```CPP +bool left = isValidBST(root->left); // 左 + +// 中序遍历,验证遍历的元素是不是从小到大 +if (maxVal < root->val) maxVal = root->val; // 中 +else return false; + +bool right = isValidBST(root->right); // 右 +return left && right; +``` + +整体代码如下: + +```CPP +class Solution { +public: + long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值 + bool isValidBST(TreeNode* root) { + if (root == NULL) return true; + + bool left = isValidBST(root->left); + // 中序遍历,验证遍历的元素是不是从小到大 + if (maxVal < root->val) maxVal = root->val; + else return false; + bool right = isValidBST(root->right); + + return left && right; + } +}; +``` + +以上代码是因为后台数据有int最小值测试用例,所以都把maxVal改成了longlong最小值。 + +如果测试数据中有 longlong的最小值,怎么办? + +不可能在初始化一个更小的值了吧。 建议避免 初始化最小值,如下方法取到最左面节点的数值来比较。 + +代码如下: + +```CPP +class Solution { +public: + TreeNode* pre = NULL; // 用来记录前一个节点 + bool isValidBST(TreeNode* root) { + if (root == NULL) return true; + bool left = isValidBST(root->left); + + if (pre != NULL && pre->val >= root->val) return false; + pre = root; // 记录前一个节点 + + bool right = isValidBST(root->right); + return left && right; + } +}; +``` + +最后这份代码看上去整洁一些,思路也清晰。 + +### 迭代法 + +可以用迭代法模拟二叉树中序遍历,对前中后序迭代法生疏的同学可以看这两篇[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html),[二叉树:前中后序迭代方式统一写法](https://programmercarl.com/二叉树的统一迭代法.html) + +迭代法中序遍历稍加改动就可以了,代码如下: + +```CPP +class Solution { +public: + bool isValidBST(TreeNode* root) { + stack st; + TreeNode* cur = root; + TreeNode* pre = NULL; // 记录前一个节点 + while (cur != NULL || !st.empty()) { + if (cur != NULL) { + st.push(cur); + cur = cur->left; // 左 + } else { + cur = st.top(); // 中 + st.pop(); + if (pre != NULL && cur->val <= pre->val) + return false; + pre = cur; //保存前一个访问的结点 + + cur = cur->right; // 右 + } + } + return true; + } +}; +``` + +在[二叉树:二叉搜索树登场!](https://programmercarl.com/0700.二叉搜索树中的搜索.html)中我们分明写出了痛哭流涕的简洁迭代法,怎么在这里不行了呢,因为本题是要验证二叉搜索树啊。 + +## 总结 + +这道题目是一个简单题,但对于没接触过的同学还是有难度的。 + +所以初学者刚开始学习算法的时候,看到简单题目没有思路很正常,千万别怀疑自己智商,学习过程都是这样的,大家智商都差不多。 + +只要把基本类型的题目都做过,总结过之后,思路自然就开阔了,加油💪 + + +## 其他语言版本 + + +### Java + +```Java +//使用統一迭代法 +class Solution { + public boolean isValidBST(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode pre = null; + if(root != null) + stack.add(root); + while(!stack.isEmpty()){ + TreeNode curr = stack.peek(); + if(curr != null){ + stack.pop(); + if(curr.right != null) + stack.add(curr.right); + stack.add(curr); + stack.add(null); + if(curr.left != null) + stack.add(curr.left); + }else{ + stack.pop(); + TreeNode temp = stack.pop(); + if(pre != null && pre.val >= temp.val) + return false; + pre = temp; + } + } + return true; + } +} +``` +```Java +class Solution { + // 递归 + TreeNode max; + public boolean isValidBST(TreeNode root) { + if (root == null) { + return true; + } + // 左 + boolean left = isValidBST(root.left); + if (!left) { + return false; + } + // 中 + if (max != null && root.val <= max.val) { + return false; + } + max = root; + // 右 + boolean right = isValidBST(root.right); + return right; + } +} + +class Solution { + // 迭代 + public boolean isValidBST(TreeNode root) { + if (root == null) { + return true; + } + Stack stack = new Stack<>(); + TreeNode pre = null; + while (root != null || !stack.isEmpty()) { + while (root != null) { + stack.push(root); + root = root.left;// 左 + } + // 中,处理 + TreeNode pop = stack.pop(); + if (pre != null && pop.val <= pre.val) { + return false; + } + pre = pop; + + root = pop.right;// 右 + } + return true; + } +} + +// 简洁实现·递归解法 +class Solution { + public boolean isValidBST(TreeNode root) { + return validBST(Long.MIN_VALUE, Long.MAX_VALUE, root); + } + boolean validBST(long lower, long upper, TreeNode root) { + if (root == null) return true; + if (root.val <= lower || root.val >= upper) return false; + return validBST(lower, root.val, root.left) && validBST(root.val, upper, root.right); + } +} +// 简洁实现·中序遍历 +class Solution { + private long prev = Long.MIN_VALUE; + public boolean isValidBST(TreeNode root) { + if (root == null) { + return true; + } + if (!isValidBST(root.left)) { + return false; + } + if (root.val <= prev) { // 不满足二叉搜索树条件 + return false; + } + prev = root.val; + return isValidBST(root.right); + } +} +``` + +### Python + +递归法(版本一)利用中序递增性质,转换成数组 +```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 __init__(self): + self.vec = [] + + def traversal(self, root): + if root is None: + return + self.traversal(root.left) + self.vec.append(root.val) # 将二叉搜索树转换为有序数组 + self.traversal(root.right) + + def isValidBST(self, root): + self.vec = [] # 清空数组 + self.traversal(root) + for i in range(1, len(self.vec)): + # 注意要小于等于,搜索树里不能有相同元素 + if self.vec[i] <= self.vec[i - 1]: + return False + return True + +``` + +递归法(版本二)设定极小值,进行比较 + +```python +class Solution: + def __init__(self): + self.maxVal = float('-inf') # 因为后台测试数据中有int最小值 + + def isValidBST(self, root): + if root is None: + return True + + left = self.isValidBST(root.left) + # 中序遍历,验证遍历的元素是不是从小到大 + if self.maxVal < root.val: + self.maxVal = root.val + else: + return False + right = self.isValidBST(root.right) + + return left and right + +``` +递归法(版本三)直接取该树的最小值 +```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 __init__(self): + self.pre = None # 用来记录前一个节点 + + def isValidBST(self, root): + if root is None: + return True + + left = self.isValidBST(root.left) + + if self.pre is not None and self.pre.val >= root.val: + return False + self.pre = root # 记录前一个节点 + + right = self.isValidBST(root.right) + return left and right + + + +``` +迭代法 +```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 isValidBST(self, root): + stack = [] + cur = root + pre = None # 记录前一个节点 + while cur is not None or len(stack) > 0: + if cur is not None: + stack.append(cur) + cur = cur.left # 左 + else: + cur = stack.pop() # 中 + if pre is not None and cur.val <= pre.val: + return False + pre = cur # 保存前一个访问的结点 + cur = cur.right # 右 + return True + +``` + + +### Go + +```Go +func isValidBST(root *TreeNode) bool { + // 二叉搜索树也可以是空树 + if root == nil { + return true + } + // 由题目中的数据限制可以得出min和max + return check(root,math.MinInt64,math.MaxInt64) +} + +func check(node *TreeNode,min,max int64) bool { + if node == nil { + return true + } + + if min >= int64(node.Val) || max <= int64(node.Val) { + return false + } + // 分别对左子树和右子树递归判断,如果左子树和右子树都符合则返回true + return check(node.Right,int64(node.Val),max) && check(node.Left,min,int64(node.Val)) +} +``` +```go +// 中序遍历解法 +func isValidBST(root *TreeNode) bool { + // 保存上一个指针 + var prev *TreeNode + var travel func(node *TreeNode) bool + travel = func(node *TreeNode) bool { + if node == nil { + return true + } + leftRes := travel(node.Left) + // 当前值小于等于前一个节点的值,返回false + if prev != nil && node.Val <= prev.Val { + return false + } + prev = node + rightRes := travel(node.Right) + return leftRes && rightRes + } + return travel(root) +} +``` + +### JavaScript + +辅助数组解决 + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @return {boolean} + */ +var isValidBST = function (root) { + let arr = []; + const buildArr = (root) => { + if (root) { + buildArr(root.left); + arr.push(root.val); + buildArr(root.right); + } + } + buildArr(root); + for (let i = 1; i < arr.length; ++i) { + if (arr[i] <= arr[i - 1]) + return false; + } + return true; +}; +``` + +递归中解决 + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @return {boolean} + */ +let pre = null; +var isValidBST = function (root) { + let pre = null; + const inOrder = (root) => { + if (root === null) + return true; + let left = inOrder(root.left); + + if (pre !== null && pre.val >= root.val) + return false; + pre = root; + + let right = inOrder(root.right); + return left && right; + } + return inOrder(root); +}; +``` + +> 迭代法: + +```JavaScript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @return {boolean} + */ +let pre = null; +var isValidBST = function (root) { + const queue = []; + let cur = root; + let pre = null; + while (cur !== null || queue.length !== 0) { + if (cur !== null) { + queue.push(cur); + cur = cur.left; + } else { + cur = queue.pop(); + if (pre !== null && cur.val <= pre.val) { + return false; + } + pre = cur; + cur = cur.right; + } + } + return true; +}; +``` + +### TypeScript + +> 辅助数组解决: + +```typescript +function isValidBST(root: TreeNode | null): boolean { + const traversalArr: number[] = []; + function inorderTraverse(root: TreeNode | null): void { + if (root === null) return; + inorderTraverse(root.left); + traversalArr.push(root.val); + inorderTraverse(root.right); + } + inorderTraverse(root); + for (let i = 0, length = traversalArr.length; i < length - 1; i++) { + if (traversalArr[i] >= traversalArr[i + 1]) return false; + } + return true; +}; +``` + +> 递归中解决: + +```typescript +function isValidBST(root: TreeNode | null): boolean { + let maxVal = -Infinity; + function inorderTraverse(root: TreeNode | null): boolean { + if (root === null) return true; + let leftValid: boolean = inorderTraverse(root.left); + if (!leftValid) return false; + if (maxVal < root.val) { + maxVal = root.val + } else { + return false; + } + let rightValid: boolean = inorderTraverse(root.right); + return leftValid && rightValid; + } + return inorderTraverse(root); +}; +``` + +> 迭代法: + +```TypeScript +function isValidBST(root: TreeNode | null): boolean { + const queue: TreeNode[] = []; + let cur: TreeNode | null = root; + let pre: TreeNode | null = null; + while (cur !== null || queue.length !== 0) { + if (cur !== null) { + queue.push(cur); + cur = cur.left; + } else { + cur = queue.pop()!; + if (pre !== null && cur!.val <= pre.val) { + return false; + } + pre = cur; + cur = cur!.right; + } + } + return true; +} +``` + +### Scala + +辅助数组解决: +```scala +object Solution { + import scala.collection.mutable + def isValidBST(root: TreeNode): Boolean = { + var arr = new mutable.ArrayBuffer[Int]() + // 递归中序遍历二叉树,将节点添加到arr + def traversal(node: TreeNode): Unit = { + if (node == null) return + traversal(node.left) + arr.append(node.value) + traversal(node.right) + } + traversal(root) + // 这个数组如果是升序就代表是二叉搜索树 + for (i <- 1 until arr.size) { + if (arr(i) <= arr(i - 1)) return false + } + true + } +} +``` + +递归中解决: +```scala +object Solution { + def isValidBST(root: TreeNode): Boolean = { + var flag = true + var preValue:Long = Long.MinValue // 这里要使用Long类型 + + def traversal(node: TreeNode): Unit = { + if (node == null || flag == false) return + traversal(node.left) + if (node.value > preValue) preValue = node.value + else flag = false + traversal(node.right) + } + traversal(root) + flag + } +} +``` + +### Rust + +递归: + +```rust +impl Solution { + pub fn is_valid_bst(root: Option>>) -> bool { + Self::valid_bst(i64::MIN, i64::MAX, root) + } + pub fn valid_bst(low: i64, upper: i64, root: Option>>) -> bool { + if root.is_none() { + return true; + } + let root = root.as_ref().unwrap().borrow(); + if root.val as i64 <= low || root.val as i64 >= upper { + return false; + } + Self::valid_bst(low, root.val as i64, root.left.clone()) + && Self::valid_bst(root.val as i64, upper, root.right.clone()) + } +} +``` + +辅助数组: + +```rust +impl Solution { + pub fn is_valid_bst(root: Option>>) -> bool { + let mut vec = vec![]; + Self::valid_bst(root, &mut vec); + for i in 1..vec.len() { + if vec[i] <= vec[i - 1] { + return false; + } + } + true + } + pub fn valid_bst(root: Option>>, mut v: &mut Vec) { + if root.is_none() { + return; + } + let node = root.as_ref().unwrap().borrow(); + Self::valid_bst(node.left.clone(), v); + v.push(node.val as i64); + Self::valid_bst(node.right.clone(), v); + } +} +``` +### C# +```csharp +// 递归 +public long val = Int64.MinValue; +public bool IsValidBST(TreeNode root) +{ + if (root == null) return true; + bool left = IsValidBST(root.left); + if (root.val > val) val = root.val; + else return false; + bool right = IsValidBST(root.right); + return left && right; +} +``` + + diff --git "a/problems/0100.\347\233\270\345\220\214\347\232\204\346\240\221.md" "b/problems/0100.\347\233\270\345\220\214\347\232\204\346\240\221.md" new file mode 100755 index 0000000000..df1b55a462 --- /dev/null +++ "b/problems/0100.\347\233\270\345\220\214\347\232\204\346\240\221.md" @@ -0,0 +1,340 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 100. 相同的树 + +[力扣题目链接](https://leetcode.cn/problems/same-tree/) + +给定两个二叉树,编写一个函数来检验它们是否相同。 + +如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 + +![](https://file1.kamacoder.com/i/algo/20210726172932.png) + +![](https://file1.kamacoder.com/i/algo/20210726173011.png) + + +## 思路 + +在[101.对称二叉树](https://programmercarl.com/0101.对称二叉树.html)中,我们讲到对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了**其实我们要比较的是两个树(这两个树是根节点的左右子树)**,所以在递归遍历的过程中,也是要同时遍历两棵树。 + +理解这一本质之后,就会发现,求二叉树是否对称,和求二叉树是否相同几乎是同一道题目。 + +**如果没有读过[二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)这一篇,请认真读完再做这道题,就会有感觉了。** + +递归三部曲中: + +1. 确定递归函数的参数和返回值 + +我们要比较的是两个树是否是相互相同的,参数也就是两个树的根节点。 + +返回值自然是bool类型。 + +代码如下: +``` +bool compare(TreeNode* tree1, TreeNode* tree2) +``` + +分析过程同[101.对称二叉树](https://programmercarl.com/0101.对称二叉树.html)。 + +2. 确定终止条件 + +**要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。** + +节点为空的情况有: + +* tree1为空,tree2不为空,不对称,return false +* tree1不为空,tree2为空,不对称 return false +* tree1,tree2都为空,对称,返回true + +此时已经排除掉了节点为空的情况,那么剩下的就是tree1和tree2不为空的时候: + +* tree1、tree2都不为空,比较节点数值,不相同就return false + +此时tree1、tree2节点不为空,且数值也不相同的情况我们也处理了。 + +代码如下: +```CPP +if (tree1 == NULL && tree2 != NULL) return false; +else if (tree1 != NULL && tree2 == NULL) return false; +else if (tree1 == NULL && tree2 == NULL) return true; +else if (tree1->val != tree2->val) return false; // 注意这里我没有使用else +``` + +分析过程同[101.对称二叉树](https://programmercarl.com/0101.对称二叉树.html) + +3. 确定单层递归的逻辑 + +* 比较二叉树是否相同 :传入的是tree1的左孩子,tree2的右孩子。 +* 如果左右都相同就返回true ,有一侧不相同就返回false 。 + +代码如下: + +```CPP +bool left = compare(tree1->left, tree2->left); // 左子树:左、 右子树:左 +bool right = compare(tree1->right, tree2->right); // 左子树:右、 右子树:右 +bool isSame = left && right; // 左子树:中、 右子树:中(逻辑处理) +return isSame; +``` +最后递归的C++整体代码如下: + +```CPP +class Solution { +public: + bool compare(TreeNode* tree1, TreeNode* tree2) { + if (tree1 == NULL && tree2 != NULL) return false; + else if (tree1 != NULL && tree2 == NULL) return false; + else if (tree1 == NULL && tree2 == NULL) return true; + else if (tree1->val != tree2->val) return false; // 注意这里我没有使用else + + // 此时就是:左右节点都不为空,且数值相同的情况 + // 此时才做递归,做下一层的判断 + bool left = compare(tree1->left, tree2->left); // 左子树:左、 右子树:左 + bool right = compare(tree1->right, tree2->right); // 左子树:右、 右子树:右 + bool isSame = left && right; // 左子树:中、 右子树:中(逻辑处理) + return isSame; + + } + bool isSameTree(TreeNode* p, TreeNode* q) { + return compare(p, q); + } +}; +``` + + +**我给出的代码并不简洁,但是把每一步判断的逻辑都清楚的描绘出来了。** + +如果上来就看网上各种简洁的代码,看起来真的很简单,但是很多逻辑都掩盖掉了,而题解可能也没有把掩盖掉的逻辑说清楚。 + +**盲目的照着抄,结果就是:发现这是一道“简单题”,稀里糊涂的就过了,但是真正的每一步判断逻辑未必想到清楚。** + +当然我可以把如上代码整理如下: + +### 递归 + +```CPP +class Solution { +public: + bool compare(TreeNode* left, TreeNode* right) { + if (left == NULL && right != NULL) return false; + else if (left != NULL && right == NULL) return false; + else if (left == NULL && right == NULL) return true; + else if (left->val != right->val) return false; + else return compare(left->left, right->left) && compare(left->right, right->right); + + } + bool isSameTree(TreeNode* p, TreeNode* q) { + return compare(p, q); + } +}; +``` + +### 迭代法 + +```CPP +class Solution { +public: + + bool isSameTree(TreeNode* p, TreeNode* q) { + if (p == NULL && q == NULL) return true; + if (p == NULL || q == NULL) return false; + queue que; + que.push(p); // 添加根节点p + que.push(q); // 添加根节点q + while (!que.empty()) { // + TreeNode* leftNode = que.front(); que.pop(); + TreeNode* rightNode = que.front(); que.pop(); + if (!leftNode && !rightNode) { // 若p的节点与q的节点都为空 + continue; + } + // 若p的节点与q的节点有一个为空或p的节点的值与q节点不同 + if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { + return false; + } + que.push(leftNode->left); // 添加p节点的左子树节点 + que.push(rightNode->left); // 添加q节点的左子树节点 + que.push(leftNode->right); // 添加p节点的右子树节点 + que.push(rightNode->right); // 添加q节点的右子树节点 + } + return true; + } +}; +``` + +## 其他语言版本 + +### Java: + +```java +// 递归法 +class Solution { + public boolean isSameTree(TreeNode p, TreeNode q) { + if (p == null && q == null) return true; + else if (q == null || p == null) return false; + else if (q.val != p.val) return false; + return isSameTree(q.left, p.left) && isSameTree(q.right, p.right); + } +} +``` + +```java +// 迭代法 +class Solution { + public boolean isSameTree(TreeNode p, TreeNode q) { + if(p == null && q == null) return true; + if(p == null || q == null) return false; + Queue que= new LinkedList(); + que.offer(p); + que.offer(q); + while(!que.isEmpty()){ + TreeNode leftNode = que.poll(); + TreeNode rightNode = que.poll(); + if(leftNode == null && rightNode == null) continue; + if(leftNode == null || rightNode== null || leftNode.val != rightNode.val) return false; + que.offer(leftNode.left); + que.offer(rightNode.left); + que.offer(leftNode.right); + que.offer(rightNode.right); + } + return true; + } +} +``` +### Python: + +```python +# 递归法 +class Solution: + def isSameTree(self, p: TreeNode, q: TreeNode) -> bool: + if not p and not q: return True + elif not p or not q: return False + elif p.val != q.val: return False + return self.isSameTree(p.left, q.left) and self.isSameTree(p.right, q.right) +``` + +```python +# 迭代法 +class Solution: + def isSameTree(self, p: TreeNode, q: TreeNode) -> bool: + if not p and not q: return True + if not p or not q: return False + que = collections.deque() + que.append(p) + que.append(q) + while que: + leftNode = que.popleft() + rightNode = que.popleft() + if not leftNode and not rightNode: continue + if not leftNode or not rightNode or leftNode.val != rightNode.val: return False + que.append(leftNode.left) + que.append(rightNode.left) + que.append(leftNode.right) + que.append(rightNode.right) + return True +``` +### Go: + +> 递归法 +```go +func isSameTree(p *TreeNode, q *TreeNode) bool { + if p != nil && q == nil { + return false + } + if p == nil && q != nil { + return false + } + if p == nil && q == nil { + return true + } + if p.Val != q.Val { + return false + } + Left := isSameTree(p.Left, q.Left) + Right := isSameTree(p.Right, q.Right) + return Left && Right +} +``` + +### JavaScript: + +> 递归法 + +```javascript +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); +}; +``` +> 迭代法 + +```javascript +var isSameTree = (p, q) => { + const queue = [{ p, q }]; + // 这是用{ } 解决了null的问题! + while (queue.length) { + const cur = queue.shift(); + if (cur.p == null && cur.q == null) continue; + if (cur.p == null || cur.q == null) return false; + if (cur.p.val != cur.q.val) return false; + queue.push({ + p: cur.p.left, + q: cur.q.left + }, { + p: cur.p.right, + q: cur.q.right + }); + } + return true; +}; +``` + +### TypeScript: + +> 递归法-先序遍历 + +```typescript +function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean { + 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); +}; +``` + +> 迭代法-层序遍历 + +```typescript +function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean { + const queue1: (TreeNode | null)[] = [], + queue2: (TreeNode | null)[] = []; + queue1.push(p); + queue2.push(q); + while (queue1.length > 0 && queue2.length > 0) { + const node1 = queue1.shift(), + node2 = queue2.shift(); + if (node1 === null && node2 === null) continue; + if ( + (node1 === null || node2 === null) || + node1!.val !== node2!.val + ) return false; + queue1.push(node1!.left); + queue1.push(node1!.right); + queue2.push(node2!.left); + queue2.push(node2!.right); + } + return true; +}; +``` + + + + + diff --git "a/problems/0101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" "b/problems/0101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" new file mode 100755 index 0000000000..24e9e2684e --- /dev/null +++ "b/problems/0101.\345\257\271\347\247\260\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,947 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 101. 对称二叉树 + +[力扣题目链接](https://leetcode.cn/problems/symmetric-tree/) + +给定一个二叉树,检查它是否是镜像对称的。 + +![101. 对称二叉树](https://file1.kamacoder.com/i/algo/20210203144607387.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[同时操作两个二叉树 | LeetCode:101. 对称二叉树](https://www.bilibili.com/video/BV1ue4y1Y7Mf), 相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +**首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!** + +对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了**其实我们要比较的是两个树(这两个树是根节点的左右子树)**,所以在递归遍历的过程中,也是要同时遍历两棵树。 + +那么如何比较呢? + +比较的是两个子树的里侧和外侧的元素是否相等。如图所示: + +![101. 对称二叉树1](https://file1.kamacoder.com/i/algo/20210203144624414.png) + +那么遍历的顺序应该是什么样的呢? + +本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。 + +**正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。** + +但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。 + +其实后序也可以理解为是一种回溯,当然这是题外话,讲回溯的时候会重点讲的。 + +说到这大家可能感觉我有点啰嗦,哪有这么多道理,上来就干就完事了。别急,我说的这些在下面的代码讲解中都有身影。 + +那么我们先来看看递归法的代码应该怎么写。 + +### 递归法 + +递归三部曲 + +1. 确定递归函数的参数和返回值 + +因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。 + +返回值自然是bool类型。 + +代码如下: +``` +bool compare(TreeNode* left, TreeNode* right) +``` + +2. 确定终止条件 + +要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。 + +节点为空的情况有:(**注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点**) + +* 左节点为空,右节点不为空,不对称,return false +* 左不为空,右为空,不对称 return false +* 左右都为空,对称,返回true + +此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空: + +* 左右都不为空,比较节点数值,不相同就return false + +此时左右节点不为空,且数值也不相同的情况我们也处理了。 + +代码如下: +```CPP +if (left == NULL && right != NULL) return false; +else if (left != NULL && right == NULL) return false; +else if (left == NULL && right == NULL) return true; +else if (left->val != right->val) return false; // 注意这里我没有使用else +``` + +注意上面最后一种情况,我没有使用else,而是else if, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。 + +3. 确定单层递归的逻辑 + +此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。 + + +* 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。 +* 比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。 +* 如果左右都对称就返回true ,有一侧不对称就返回false 。 + +代码如下: + +```CPP +bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右 +bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左 +bool isSame = outside && inside; // 左子树:中、 右子树:中(逻辑处理) +return isSame; +``` + +如上代码中,我们可以看出使用的遍历方式,左子树左右中,右子树右左中,所以我把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。 + +最后递归的C++整体代码如下: + +```CPP +class Solution { +public: + bool compare(TreeNode* left, TreeNode* right) { + // 首先排除空节点的情况 + if (left == NULL && right != NULL) return false; + else if (left != NULL && right == NULL) return false; + else if (left == NULL && right == NULL) return true; + // 排除了空节点,再排除数值不相同的情况 + else if (left->val != right->val) return false; + + // 此时就是:左右节点都不为空,且数值相同的情况 + // 此时才做递归,做下一层的判断 + bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右 + bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左 + bool isSame = outside && inside; // 左子树:中、 右子树:中 (逻辑处理) + return isSame; + + } + bool isSymmetric(TreeNode* root) { + if (root == NULL) return true; + return compare(root->left, root->right); + } +}; +``` + +**我给出的代码并不简洁,但是把每一步判断的逻辑都清楚的描绘出来了。** + +如果上来就看网上各种简洁的代码,看起来真的很简单,但是很多逻辑都掩盖掉了,而题解可能也没有把掩盖掉的逻辑说清楚。 + +**盲目的照着抄,结果就是:发现这是一道“简单题”,稀里糊涂的就过了,但是真正的每一步判断逻辑未必想到清楚。** + +当然我可以把如上代码整理如下: +```CPP +class Solution { +public: + bool compare(TreeNode* left, TreeNode* right) { + if (left == NULL && right != NULL) return false; + else if (left != NULL && right == NULL) return false; + else if (left == NULL && right == NULL) return true; + else if (left->val != right->val) return false; + else return compare(left->left, right->right) && compare(left->right, right->left); + + } + bool isSymmetric(TreeNode* root) { + if (root == NULL) return true; + return compare(root->left, root->right); + } +}; +``` + +**这个代码就很简洁了,但隐藏了很多逻辑,条理不清晰,而且递归三部曲,在这里完全体现不出来。** + +**所以建议大家做题的时候,一定要想清楚逻辑,每一步做什么。把题目所有情况想到位,相应的代码写出来之后,再去追求简洁代码的效果。** + +### 迭代法 + +这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。 + +这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(**注意这不是层序遍历**) + +#### 使用队列 + +通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等,如动画所示: + +![101.对称二叉树](https://file1.kamacoder.com/i/algo/101.%E5%AF%B9%E7%A7%B0%E4%BA%8C%E5%8F%89%E6%A0%91.gif) + + + +如下的条件判断和递归的逻辑是一样的。 + +代码如下: + +```CPP +class Solution { +public: + bool isSymmetric(TreeNode* root) { + if (root == NULL) return true; + queue que; + que.push(root->left); // 将左子树头结点加入队列 + que.push(root->right); // 将右子树头结点加入队列 + + while (!que.empty()) { // 接下来就要判断这两个树是否相互翻转 + TreeNode* leftNode = que.front(); que.pop(); + TreeNode* rightNode = que.front(); que.pop(); + if (!leftNode && !rightNode) { // 左节点为空、右节点为空,此时说明是对称的 + continue; + } + + // 左右一个节点不为空,或者都不为空但数值不相同,返回false + if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { + return false; + } + que.push(leftNode->left); // 加入左节点左孩子 + que.push(rightNode->right); // 加入右节点右孩子 + que.push(leftNode->right); // 加入左节点右孩子 + que.push(rightNode->left); // 加入右节点左孩子 + } + return true; + } +}; +``` + +#### 使用栈 + +细心的话,其实可以发现,这个迭代法,其实是把左右两个子树要比较的元素顺序放进一个容器,然后成对成对的取出来进行比较,那么其实使用栈也是可以的。 + +只要把队列原封不动的改成栈就可以了,我下面也给出了代码。 + +```CPP +class Solution { +public: + bool isSymmetric(TreeNode* root) { + if (root == NULL) return true; + stack st; // 这里改成了栈 + st.push(root->left); + st.push(root->right); + while (!st.empty()) { + TreeNode* rightNode = st.top(); st.pop(); + TreeNode* leftNode = st.top(); st.pop(); + if (!leftNode && !rightNode) { + continue; + } + if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { + return false; + } + st.push(leftNode->left); + st.push(rightNode->right); + st.push(leftNode->right); + st.push(rightNode->left); + } + return true; + } +}; +``` + +## 总结 + +这次我们又深度剖析了一道二叉树的“简单题”,大家会发现,真正的把题目搞清楚其实并不简单,leetcode上accept了和真正掌握了还是有距离的。 + +我们介绍了递归法和迭代法,递归依然通过递归三部曲来解决了这道题目,如果只看精简的代码根本看不出来递归三部曲是如何解题的。 + +在迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,知道这一本质之后就发现,用队列,用栈,甚至用数组,都是可以的。 + +如果已经做过这道题目的同学,读完文章可以再去看看这道题目,思考一下,会有不一样的发现! + +## 相关题目推荐 + +这两道题目基本和本题是一样的,只要稍加修改就可以AC。 + +* [100.相同的树](https://leetcode.cn/problems/same-tree/) +* [572.另一个树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) + +## 其他语言版本 + +### Java: + +```Java + /** + * 递归法 + */ + public boolean isSymmetric1(TreeNode root) { + return compare(root.left, root.right); + } + + private boolean compare(TreeNode left, TreeNode right) { + + if (left == null && right != null) { + return false; + } + if (left != null && right == null) { + return false; + } + + if (left == null && right == null) { + return true; + } + if (left.val != right.val) { + return false; + } + // 比较外侧 + boolean compareOutside = compare(left.left, right.right); + // 比较内侧 + boolean compareInside = compare(left.right, right.left); + return compareOutside && compareInside; + } + + /** + * 迭代法 + * 使用双端队列,相当于两个栈 + */ + public boolean isSymmetric2(TreeNode root) { + Deque deque = new LinkedList<>(); + deque.offerFirst(root.left); + deque.offerLast(root.right); + while (!deque.isEmpty()) { + TreeNode leftNode = deque.pollFirst(); + TreeNode rightNode = deque.pollLast(); + if (leftNode == null && rightNode == null) { + continue; + } +// if (leftNode == null && rightNode != null) { +// return false; +// } +// if (leftNode != null && rightNode == null) { +// return false; +// } +// if (leftNode.val != rightNode.val) { +// return false; +// } + // 以上三个判断条件合并 + if (leftNode == null || rightNode == null || leftNode.val != rightNode.val) { + return false; + } + deque.offerFirst(leftNode.left); + deque.offerFirst(leftNode.right); + deque.offerLast(rightNode.right); + deque.offerLast(rightNode.left); + } + return true; + } + + /** + * 迭代法 + * 使用普通队列 + */ + public boolean isSymmetric3(TreeNode root) { + Queue deque = new LinkedList<>(); + deque.offer(root.left); + deque.offer(root.right); + while (!deque.isEmpty()) { + TreeNode leftNode = deque.poll(); + TreeNode rightNode = deque.poll(); + if (leftNode == null && rightNode == null) { + continue; + } +// if (leftNode == null && rightNode != null) { +// return false; +// } +// if (leftNode != null && rightNode == null) { +// return false; +// } +// if (leftNode.val != rightNode.val) { +// return false; +// } + // 以上三个判断条件合并 + if (leftNode == null || rightNode == null || leftNode.val != rightNode.val) { + return false; + } + // 这里顺序与使用Deque不同 + deque.offer(leftNode.left); + deque.offer(rightNode.right); + deque.offer(leftNode.right); + deque.offer(rightNode.left); + } + return true; + } + +``` + +### Python: + +递归法: +```python +class Solution: + def isSymmetric(self, root: TreeNode) -> bool: + if not root: + return True + return self.compare(root.left, root.right) + + def compare(self, left, right): + #首先排除空节点的情况 + if left == None and right != None: return False + elif left != None and right == None: return False + elif left == None and right == None: return True + #排除了空节点,再排除数值不相同的情况 + elif left.val != right.val: return False + + #此时就是:左右节点都不为空,且数值相同的情况 + #此时才做递归,做下一层的判断 + outside = self.compare(left.left, right.right) #左子树:左、 右子树:右 + inside = self.compare(left.right, right.left) #左子树:右、 右子树:左 + isSame = outside and inside #左子树:中、 右子树:中 (逻辑处理) + return isSame +``` + +迭代法: 使用队列 +```python +import collections +class Solution: + def isSymmetric(self, root: TreeNode) -> bool: + if not root: + return True + queue = collections.deque() + queue.append(root.left) #将左子树头结点加入队列 + queue.append(root.right) #将右子树头结点加入队列 + while queue: #接下来就要判断这这两个树是否相互翻转 + leftNode = queue.popleft() + rightNode = queue.popleft() + if not leftNode and not rightNode: #左节点为空、右节点为空,此时说明是对称的 + continue + + #左右一个节点不为空,或者都不为空但数值不相同,返回false + if not leftNode or not rightNode or leftNode.val != rightNode.val: + return False + queue.append(leftNode.left) #加入左节点左孩子 + queue.append(rightNode.right) #加入右节点右孩子 + queue.append(leftNode.right) #加入左节点右孩子 + queue.append(rightNode.left) #加入右节点左孩子 + return True +``` + +迭代法:使用栈 +```python +class Solution: + def isSymmetric(self, root: TreeNode) -> bool: + if not root: + return True + st = [] #这里改成了栈 + st.append(root.left) + st.append(root.right) + while st: + rightNode = st.pop() + leftNode = st.pop() + if not leftNode and not rightNode: + continue + if not leftNode or not rightNode or leftNode.val != rightNode.val: + return False + st.append(leftNode.left) + st.append(rightNode.right) + st.append(leftNode.right) + st.append(rightNode.left) + return True +``` + +层次遍历 +```python +class Solution: + def isSymmetric(self, root: TreeNode) -> bool: + if not root: + return True + + queue = collections.deque([root.left, root.right]) + + while queue: + level_size = len(queue) + + if level_size % 2 != 0: + return False + + level_vals = [] + for i in range(level_size): + node = queue.popleft() + if node: + level_vals.append(node.val) + queue.append(node.left) + queue.append(node.right) + else: + level_vals.append(None) + + if level_vals != level_vals[::-1]: + return False + + return True +``` + +### Go: + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +// 递归 +func defs(left *TreeNode, right *TreeNode) bool { + if left == nil && right == nil { + return true; + }; + if left == nil || right == nil { + return false; + }; + if left.Val != right.Val { + return false; + } + return defs(left.Left, right.Right) && defs(right.Left, left.Right); +} + +func isSymmetric(root *TreeNode) bool { + return defs(root.Left, root.Right); +} + + +// 迭代 +func isSymmetric(root *TreeNode) bool { + var queue []*TreeNode; + if root != nil { + queue = append(queue, root.Left, root.Right); + } + for len(queue) > 0 { + left := queue[0]; + right := queue[1]; + queue = queue[2:]; + if left == nil && right == nil { + continue; + } + if left == nil || right == nil || left.Val != right.Val { + return false; + }; + queue = append(queue, left.Left, right.Right, right.Left, left.Right); + } + return true; +} +``` + +### JavaScript: + +递归判断是否为对称二叉树: +```javascript +var isSymmetric = function(root) { + // 使用递归遍历左右子树 递归三部曲 + // 1. 确定递归的参数 root.left root.right和返回值true false + const compareNode = function(left, right) { + // 2. 确定终止条件 空的情况 + if(left === null && right !== null || left !== null && right === null) { + return false; + } else if(left === null && right === null) { + return true; + } else if(left.val !== right.val) { + return false; + } + // 3. 确定单层递归逻辑 + let outSide = compareNode(left.left, right.right); + let inSide = compareNode(left.right, right.left); + return outSide && inSide; + } + if(root === null) { + return true; + } + return compareNode(root.left, root.right); +}; +``` + +队列实现迭代判断是否为对称二叉树: +```javascript +var isSymmetric = function(root) { + // 迭代方法判断是否是对称二叉树 + // 首先判断root是否为空 + if(root === null) { + return true; + } + let queue = []; + queue.push(root.left); + queue.push(root.right); + while(queue.length) { + let leftNode = queue.shift(); //左节点 + let rightNode = queue.shift(); //右节点 + if(leftNode === null && rightNode === null) { + continue; + } + if(leftNode === null || rightNode === null || leftNode.val !== rightNode.val) { + return false; + } + queue.push(leftNode.left); //左节点左孩子入队 + queue.push(rightNode.right); //右节点右孩子入队 + queue.push(leftNode.right); //左节点右孩子入队 + queue.push(rightNode.left); //右节点左孩子入队 + } + + return true; +}; +``` + +栈实现迭代判断是否为对称二叉树: +```javascript +var isSymmetric = function(root) { + // 迭代方法判断是否是对称二叉树 + // 首先判断root是否为空 + if(root === null) { + return true; + } + let stack = []; + stack.push(root.left); + stack.push(root.right); + while(stack.length) { + let rightNode = stack.pop(); //左节点 + let leftNode=stack.pop(); //右节点 + if(leftNode === null && rightNode === null) { + continue; + } + if(leftNode === null || rightNode === null || leftNode.val !== rightNode.val) { + return false; + } + stack.push(leftNode.left); //左节点左孩子入队 + stack.push(rightNode.right); //右节点右孩子入队 + stack.push(leftNode.right); //左节点右孩子入队 + stack.push(rightNode.left); //右节点左孩子入队 + } + + return true; +}; +``` + +### TypeScript: + +> 递归法 + +```typescript +function isSymmetric(root: TreeNode | null): boolean { + function recur(node1: TreeNode | null, node2: TreeNode | null): boolean { + if (node1 === null && node2 === null) return true; + if (node1 === null || node2 === null) return false; + if (node1.val !== node2.val) return false + let isSym1: boolean = recur(node1.left, node2.right); + let isSym2: boolean = recur(node1.right, node2.left); + return isSym1 && isSym2 + } + if (root === null) return true; + return recur(root.left, root.right); +}; +``` + +> 迭代法 + +```typescript +// 迭代法(队列) +function isSymmetric(root: TreeNode | null): boolean { + let helperQueue: (TreeNode | null)[] = []; + let tempNode1: TreeNode | null, + tempNode2: TreeNode | null; + if (root !== null) { + helperQueue.push(root.left); + helperQueue.push(root.right); + } + while (helperQueue.length > 0) { + tempNode1 = helperQueue.shift()!; + tempNode2 = helperQueue.shift()!; + if (tempNode1 === null && tempNode2 === null) continue; + if (tempNode1 === null || tempNode2 === null) return false; + if (tempNode1.val !== tempNode2.val) return false; + helperQueue.push(tempNode1.left); + helperQueue.push(tempNode2.right); + helperQueue.push(tempNode1.right); + helperQueue.push(tempNode2.left); + } + return true; +} + +// 迭代法(栈) +function isSymmetric(root: TreeNode | null): boolean { + let helperStack: (TreeNode | null)[] = []; + let tempNode1: TreeNode | null, + tempNode2: TreeNode | null; + if (root !== null) { + helperStack.push(root.left); + helperStack.push(root.right); + } + while (helperStack.length > 0) { + tempNode1 = helperStack.pop()!; + tempNode2 = helperStack.pop()!; + if (tempNode1 === null && tempNode2 === null) continue; + if (tempNode1 === null || tempNode2 === null) return false; + if (tempNode1.val !== tempNode2.val) return false; + helperStack.push(tempNode1.left); + helperStack.push(tempNode2.right); + helperStack.push(tempNode1.right); + helperStack.push(tempNode2.left); + } + return true; +}; +``` + +### Swift: + +> 递归 +```swift +func isSymmetric(_ root: TreeNode?) -> Bool { + return _isSymmetric(root?.left, right: root?.right) +} +func _isSymmetric(_ left: TreeNode?, right: TreeNode?) -> Bool { + // 首先排除空节点情况 + if left == nil && right == nil { + return true + } else if left == nil && right != nil { + return false + } else if left != nil && right == nil { + return false + } else if left!.val != right!.val { + // 进而排除数值不相等的情况 + return false + } + + // left 和 right 都不为空, 且数值也相等就递归 + let inSide = _isSymmetric(left!.right, right: right!.left) + let outSide = _isSymmetric(left!.left, right: right!.right) + return inSide && outSide +} +``` + +> 迭代 - 使用队列 +```swift +func isSymmetric2(_ root: TreeNode?) -> Bool { + guard let root = root else { + return true + } + var queue = [TreeNode?]() + queue.append(root.left) + queue.append(root.right) + while !queue.isEmpty { + let left = queue.removeFirst() + let right = queue.removeFirst() + if left == nil && right == nil { + continue + } + if left == nil || right == nil || left?.val != right?.val { + return false + } + queue.append(left!.left) + queue.append(right!.right) + queue.append(left!.right) + queue.append(right!.left) + } + return true +} +``` + +> 迭代 - 使用栈 +```swift +func isSymmetric3(_ root: TreeNode?) -> Bool { + guard let root = root else { + return true + } + var stack = [TreeNode?]() + stack.append(root.left) + stack.append(root.right) + while !stack.isEmpty { + let left = stack.removeLast() + let right = stack.removeLast() + + if left == nil && right == nil { + continue + } + if left == nil || right == nil || left?.val != right?.val { + return false + } + stack.append(left!.left) + stack.append(right!.right) + stack.append(left!.right) + stack.append(right!.left) + } + return true +} +``` + +### Scala: + +> 递归: +```scala +object Solution { + def isSymmetric(root: TreeNode): Boolean = { + if (root == null) return true // 如果等于空直接返回true + + def compare(left: TreeNode, right: TreeNode): Boolean = { + if (left == null && right == null) true // 如果左右都为空,则为true + else if (left == null && right != null) false // 如果左空右不空,不对称,返回false + else if (left != null && right == null) false // 如果左不空右空,不对称,返回false + // 如果左右的值相等,并且往下递归 + else left.value == right.value && compare(left.left, right.right) && compare(left.right, right.left) + } + + // 分别比较左子树和右子树 + compare(root.left, root.right) + } +} +``` +> 迭代 - 使用栈 +```scala +object Solution { + + import scala.collection.mutable + + def isSymmetric(root: TreeNode): Boolean = { + if (root == null) return true + + val cache = mutable.Stack[(TreeNode, TreeNode)]((root.left, root.right)) + + while (cache.nonEmpty) { + cache.pop() match { + case (null, null) => + case (_, null) => return false + case (null, _) => return false + case (left, right) => + if (left.value != right.value) return false + cache.push((left.left, right.right)) + cache.push((left.right, right.left)) + } + } + true + } +} +``` +> 迭代 - 使用队列 +```scala +object Solution { + + import scala.collection.mutable + + def isSymmetric(root: TreeNode): Boolean = { + if (root == null) return true + + val cache = mutable.Queue[(TreeNode, TreeNode)]((root.left, root.right)) + + while (cache.nonEmpty) { + cache.dequeue() match { + case (null, null) => + case (_, null) => return false + case (null, _) => return false + case (left, right) => + if (left.value != right.value) return false + cache.enqueue((left.left, right.right)) + cache.enqueue((left.right, right.left)) + } + } + true + } +} +``` + +### Rust: + +递归: +```rust +impl Solution { + pub fn is_symmetric(root: Option>>) -> bool { + Self::recur( + &root.as_ref().unwrap().borrow().left, + &root.as_ref().unwrap().borrow().right, + ) + } + pub fn recur( + left: &Option>>, + right: &Option>>, + ) -> bool { + match (left, right) { + (None, None) => true, + (Some(n1), Some(n2)) => { + return n1.borrow().val == n2.borrow().val + && Self::recur(&n1.borrow().left, &n2.borrow().right) + && Self::recur(&n1.borrow().right, &n2.borrow().left) + } + _ => false, + } + } +} +``` + +迭代: +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn is_symmetric(root: Option>>) -> bool { + let mut queue = VecDeque::new(); + if let Some(node) = root { + queue.push_back(node.borrow().left.clone()); + queue.push_back(node.borrow().right.clone()); + } + while !queue.is_empty() { + let (n1, n2) = (queue.pop_front().unwrap(), queue.pop_front().unwrap()); + match (n1.clone(), n2.clone()) { + (None, None) => continue, + (Some(n1), Some(n2)) => { + if n1.borrow().val != n2.borrow().val { + return false; + } + } + _ => return false, + }; + queue.push_back(n1.as_ref().unwrap().borrow().left.clone()); + queue.push_back(n2.as_ref().unwrap().borrow().right.clone()); + queue.push_back(n1.unwrap().borrow().right.clone()); + queue.push_back(n2.unwrap().borrow().left.clone()); + } + true + } +} +``` +### C# +```csharp +// 递归 +public bool IsSymmetric(TreeNode root) +{ + if (root == null) return true; + return Compare(root.left, root.right); +} +public bool Compare(TreeNode left, TreeNode right) +{ + if(left == null && right != null) return false; + else if(left != null && right == null ) return false; + else if(left == null && right == null) return true; + else if(left.val != right.val) return false; + + var outside = Compare(left.left, right.right); + var inside = Compare(left.right, right.left); + + return outside&&inside; +} +``` +``` C# +// 迭代法 +public bool IsSymmetric(TreeNode root) +{ + if (root == null) return true; + var st = new Stack(); + st.Push(root.left); + st.Push(root.right); + while (st.Count != 0) + { + var left = st.Pop(); + var right = st.Pop(); + if (left == null && right == null) + continue; + + if ((left == null || right == null || (left.val != right.val))) + return false; + + st.Push(left.left); + st.Push(right.right); + st.Push(left.right); + st.Push(right.left); + } + return true; +} +``` + + + diff --git "a/problems/0102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" "b/problems/0102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" new file mode 100755 index 0000000000..819153be97 --- /dev/null +++ "b/problems/0102.\344\272\214\345\217\211\346\240\221\347\232\204\345\261\202\345\272\217\351\201\215\345\216\206.md" @@ -0,0 +1,3601 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 二叉树层序遍历登场! + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[讲透二叉树的层序遍历 | 广度优先搜索 | LeetCode:102.二叉树的层序遍历](https://www.bilibili.com/video/BV1GY4y1u7b2),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +学会二叉树的层序遍历,可以一口气打完以下十题: + +* [102.二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) +* [107.二叉树的层次遍历II](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/) +* [199.二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) +* [637.二叉树的层平均值](https://leetcode.cn/problems/average-of-levels-in-binary-tree/) +* [429.N叉树的层序遍历](https://leetcode.cn/problems/n-ary-tree-level-order-traversal/) +* [515.在每个树行中找最大值](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/) +* [116.填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) +* [117.填充每个节点的下一个右侧节点指针II](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/) +* [104.二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) +* [111.二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) + + + +![我要打十个](https://file1.kamacoder.com/i/algo/%E6%88%91%E8%A6%81%E6%89%93%E5%8D%81%E4%B8%AA.gif) + + + + +## 102.二叉树的层序遍历 + +[力扣题目链接](https://leetcode.cn/problems/binary-tree-level-order-traversal/) + +给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 + +![102.二叉树的层序遍历](https://file1.kamacoder.com/i/algo/20210203144842988.png) + +### 思路 + +我们之前讲过了三篇关于二叉树的深度优先遍历的文章: + +* [二叉树:前中后序递归法](https://programmercarl.com/二叉树的递归遍历.html) +* [二叉树:前中后序迭代法](https://programmercarl.com/二叉树的迭代遍历.html) +* [二叉树:前中后序迭代方式统一写法](https://programmercarl.com/二叉树的统一迭代法.html) + +接下来我们再来介绍二叉树的另一种遍历方式:层序遍历。 + +层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。 + +需要借用一个辅助数据结构即队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** + +**而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。** + +使用队列实现二叉树广度优先遍历,动画如下: + +![102二叉树的层序遍历](https://file1.kamacoder.com/i/algo/102%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.gif) + +这样就实现了层序从左到右遍历二叉树。 + +代码如下:**这份代码也可以作为二叉树层序遍历的模板,打十个就靠它了**。 + +c++代码如下: + +```CPP +class Solution { +public: + vector> levelOrder(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector> result; + while (!que.empty()) { + int size = que.size(); + vector vec; + // 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的 + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + vec.push_back(node->val); + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + result.push_back(vec); + } + return result; + } +}; +``` + +```CPP +# 递归法 +class Solution { +public: + void order(TreeNode* cur, vector>& result, int depth) + { + if (cur == nullptr) return; + if (result.size() == depth) result.push_back(vector()); + result[depth].push_back(cur->val); + order(cur->left, result, depth + 1); + order(cur->right, result, depth + 1); + } + vector> levelOrder(TreeNode* root) { + vector> result; + int depth = 0; + order(root, result, depth); + return result; + } +}; +``` + +### 其他语言版本 + +#### Java: + +```Java +// 102.二叉树的层序遍历 +class Solution { + public List> resList = new ArrayList>(); + + public List> levelOrder(TreeNode root) { + //checkFun01(root,0); + checkFun02(root); + + return resList; + } + + //BFS--递归方式 + public void checkFun01(TreeNode node, Integer deep) { + if (node == null) return; + deep++; + + if (resList.size() < deep) { + //当层级增加时,list的Item也增加,利用list的索引值进行层级界定 + List item = new ArrayList(); + resList.add(item); + } + resList.get(deep - 1).add(node.val); + + checkFun01(node.left, deep); + checkFun01(node.right, deep); + } + + //BFS--迭代方式--借助队列 + public void checkFun02(TreeNode node) { + if (node == null) return; + Queue que = new LinkedList(); + que.offer(node); + + while (!que.isEmpty()) { + List itemList = new ArrayList(); + int len = que.size(); + + while (len > 0) { + TreeNode tmpNode = que.poll(); + itemList.add(tmpNode.val); + + if (tmpNode.left != null) que.offer(tmpNode.left); + if (tmpNode.right != null) que.offer(tmpNode.right); + len--; + } + + resList.add(itemList); + } + + } +} +``` + +#### Python: + + +```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 levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]: + if not root: + return [] + queue = collections.deque([root]) + result = [] + while queue: + level = [] + for _ in range(len(queue)): + cur = queue.popleft() + level.append(cur.val) + if cur.left: + queue.append(cur.left) + if cur.right: + queue.append(cur.right) + result.append(level) + return result +``` +```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 levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]: + if not root: + return [] + + levels = [] + + def traverse(node, level): + if not node: + return + + if len(levels) == level: + levels.append([]) + + levels[level].append(node.val) + traverse(node.left, level + 1) + traverse(node.right, level + 1) + + traverse(root, 0) + return levels + +``` + +#### Go: + +```go +/** +102. 二叉树的递归遍历 + */ +func levelOrder(root *TreeNode) [][]int { + arr := [][]int{} + + depth := 0 + + var order func(root *TreeNode, depth int) + + order = func(root *TreeNode, depth int) { + if root == nil { + return + } + if len(arr) == depth { + arr = append(arr, []int{}) + } + arr[depth] = append(arr[depth], root.Val) + + order(root.Left, depth+1) + order(root.Right, depth+1) + } + + order(root, depth) + + return arr +} +``` + +```go +/** +102. 二叉树的层序遍历 使用container包 + */ +func levelOrder(root *TreeNode) [][]int { + res := [][]int{} + if root == nil{//防止为空 + return res + } + queue := list.New() + queue.PushBack(root) + + var tmpArr []int + + for queue.Len() > 0 { + length := queue.Len() //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) //出队列 + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + tmpArr = append(tmpArr, node.Val) //将值加入本层切片中 + } + res = append(res, tmpArr) //放入结果集 + tmpArr = []int{} //清空层的数据 + } + + return res +} + +/** + 102. 二叉树的层序遍历 使用切片 +*/ +func levelOrder(root *TreeNode) [][]int { + res := make([][]int, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + level := make([]int, 0) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + level = append(level, node.Val) + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + res = append(res, level) + } + return res +} + +/** +102. 二叉树的层序遍历:使用切片模拟队列,易理解 + */ +func levelOrder(root *TreeNode) (res [][]int) { + if root == nil { + return + } + + curLevel := []*TreeNode{root} // 存放当前层节点 + for len(curLevel) > 0 { + nextLevel := []*TreeNode{} // 准备通过当前层生成下一层 + vals := []int{} + + for _, node := range curLevel { + vals = append(vals, node.Val) // 收集当前层的值 + // 收集下一层的节点 + if node.Left != nil { + nextLevel = append(nextLevel, node.Left) + } + if node.Right != nil { + nextLevel = append(nextLevel, node.Right) + } + } + res = append(res, vals) + curLevel = nextLevel // 将下一层变成当前层 + } + + return +} +``` + +#### JavaScript: + +```javascript +var levelOrder = function(root) { + //二叉树的层序遍历 + let res = [], queue = []; + queue.push(root); + if(root === null) { + return res; + } + while(queue.length !== 0) { + // 记录当前层级节点数 + let length = queue.length; + //存放每一层的节点 + let curLevel = []; + for(let i = 0;i < length; i++) { + let node = queue.shift(); + curLevel.push(node.val); + // 存放当前层下一层的节点 + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + //把每一层的结果放到结果数组 + res.push(curLevel); + } + return res; +}; + +``` + +#### TypeScript: + +```typescript +function levelOrder(root: TreeNode | null): number[][] { + let helperQueue: TreeNode[] = []; + let res: number[][] = []; + let tempArr: number[] = []; + if (root !== null) helperQueue.push(root); + let curNode: TreeNode; + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + curNode = helperQueue.shift()!; + tempArr.push(curNode.val); + if (curNode.left !== null) { + helperQueue.push(curNode.left); + } + if (curNode.right !== null) { + helperQueue.push(curNode.right); + } + } + res.push(tempArr); + tempArr = []; + } + return res; +}; +``` + +#### Swift: + +```swift +func levelOrder(_ root: TreeNode?) -> [[Int]] { + var result = [[Int]]() + guard let root = root else { return result } + // 表示一层 + var queue = [root] + while !queue.isEmpty { + let count = queue.count + var subarray = [Int]() + for _ in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + subarray.append(node.val) + // 下一层 + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node) } + } + result.append(subarray) + } + + return result +} +``` + +#### Scala: + +```scala +// 102.二叉树的层序遍历 +object Solution { + import scala.collection.mutable + def levelOrder(root: TreeNode): List[List[Int]] = { + val res = mutable.ListBuffer[List[Int]]() + if (root == null) return res.toList + val queue = mutable.Queue[TreeNode]() // 声明一个队列 + queue.enqueue(root) // 把根节点加入queue + while (!queue.isEmpty) { + val tmp = mutable.ListBuffer[Int]() + val len = queue.size // 求出len的长度 + for (i <- 0 until len) { // 从0到当前队列长度的所有节点都加入到结果集 + val curNode = queue.dequeue() + tmp.append(curNode.value) + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + res.append(tmp.toList) + } + res.toList + } +} +``` + +#### Rust: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +use std::collections::VecDeque; +impl Solution { + pub fn level_order(root: Option>>) -> Vec> { + let mut res = vec![]; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + let mut temp = vec![]; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + temp.push(node.borrow().val); + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + res.push(temp); + } + res + } +} +``` +### C# +```csharp +public IList> LevelOrder(TreeNode root) +{ + var res = new List>(); + var que = new Queue(); + if (root == null) return res; + que.Enqueue(root); + while (que.Count != 0) + { + var size = que.Count; + var vec = new List(); + for (int i = 0; i < size; i++) + { + var cur = que.Dequeue(); + vec.Add(cur.val); + if (cur.left != null) que.Enqueue(cur.left); + if (cur.right != null) que.Enqueue(cur.right); + } + res.Add(vec); + + + } + return res; +} +``` + + +**此时我们就掌握了二叉树的层序遍历了,那么如下九道力扣上的题目,只需要修改模板的两三行代码(不能再多了),便可打倒!** + + +## 107.二叉树的层次遍历 II + +[力扣题目链接](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/) + +给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) + +![107.二叉树的层次遍历II](https://file1.kamacoder.com/i/algo/20210203151058308.png) + +### 思路 + +相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。 + +C++代码: + +```CPP +class Solution { +public: + vector> levelOrderBottom(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector> result; + while (!que.empty()) { + int size = que.size(); + vector vec; + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + vec.push_back(node->val); + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + result.push_back(vec); + } + reverse(result.begin(), result.end()); // 在这里反转一下数组即可 + return result; + + } +}; +``` + +### 其他语言版本 + +#### Python: + +```python +class Solution: + """二叉树层序遍历II迭代解法""" + +# 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 levelOrderBottom(self, root: TreeNode) -> List[List[int]]: + if not root: + return [] + queue = collections.deque([root]) + result = [] + while queue: + level = [] + for _ in range(len(queue)): + cur = queue.popleft() + level.append(cur.val) + if cur.left: + queue.append(cur.left) + if cur.right: + queue.append(cur.right) + result.append(level) + return result[::-1] +``` + +#### Java: + +```java +// 107. 二叉树的层序遍历 II +public class N0107 { + + /** + * 解法:队列,迭代。 + * 层序遍历,再翻转数组即可。 + */ + public List> solution1(TreeNode root) { + List> list = new ArrayList<>(); + Deque que = new LinkedList<>(); + + if (root == null) { + return list; + } + + que.offerLast(root); + while (!que.isEmpty()) { + List levelList = new ArrayList<>(); + + int levelSize = que.size(); + for (int i = 0; i < levelSize; i++) { + TreeNode peek = que.peekFirst(); + levelList.add(que.pollFirst().val); + + if (peek.left != null) { + que.offerLast(peek.left); + } + if (peek.right != null) { + que.offerLast(peek.right); + } + } + list.add(levelList); + } + + List> result = new ArrayList<>(); + for (int i = list.size() - 1; i >= 0; i-- ) { + result.add(list.get(i)); + } + + return result; + } +} +``` + +```java +/** + * 思路和模板相同, 对收集答案的方式做了优化, 最后不需要反转 + */ +class Solution { + public List> levelOrderBottom(TreeNode root) { + // 利用链表可以进行 O(1) 头部插入, 这样最后答案不需要再反转 + LinkedList> ans = new LinkedList<>(); + + Queue q = new LinkedList<>(); + + if (root != null) q.offer(root); + + while (!q.isEmpty()) { + int size = q.size(); + + List temp = new ArrayList<>(); + + for (int i = 0; i < size; i ++) { + TreeNode node = q.poll(); + + temp.add(node.val); + + if (node.left != null) q.offer(node.left); + + if (node.right != null) q.offer(node.right); + } + + // 新遍历到的层插到头部, 这样就满足按照层次反序的要求 + ans.addFirst(temp); + } + + return ans; + } + +``` + +#### Go: + +```GO +/** +107. 二叉树的层序遍历 II + */ +func levelOrderBottom(root *TreeNode) [][]int { + queue := list.New() + res := [][]int{} + if root == nil{ + return res + } + queue.PushBack(root) + + for queue.Len() > 0 { + length := queue.Len() + tmp := []int{} + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + tmp = append(tmp, node.Val) + } + res=append(res, tmp) + } + + //反转结果集 + for i:=0; i 0 { + size := len(queue) + level := make([]int, 0) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + level = append(level, node.Val) + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + res = append(res, level) + } + l, r := 0, len(res)-1 + for l < r { + res[l], res[r] = res[r], res[l] + l++ + r-- + } + return res +} +``` + +#### JavaScript: + +```javascript +var levelOrderBottom = function (root) { + let res = [], + queue = []; + queue.push(root); + while (queue.length && root !== null) { + // 存放当前层级节点数组 + let curLevel = []; + // 计算当前层级节点数量 + let length = queue.length; + while (length--) { + let node = queue.shift(); + // 把当前层节点存入curLevel数组 + curLevel.push(node.val); + // 把下一层级的左右节点存入queue队列 + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + // 从数组前头插入值,避免最后反转数组,减少运算时间 + res.unshift(curLevel); + } + return res; +}; + +``` + +#### TypeScript: + +```typescript +function levelOrderBottom(root: TreeNode | null): number[][] { + let helperQueue: TreeNode[] = []; + let resArr: number[][] = []; + let tempArr: number[] = []; + let tempNode: TreeNode; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + tempArr.push(tempNode.val); + if (tempNode.left !== null) helperQueue.push(tempNode.left); + if (tempNode.right !== null) helperQueue.push(tempNode.right); + } + resArr.push(tempArr); + tempArr = []; + } + return resArr.reverse(); +}; +``` + +#### Swift: + +```swift +func levelOrderBottom(_ root: TreeNode?) -> [[Int]] { + // 表示一层 + var queue = [TreeNode]() + if let node = root { queue.append(node) } + var result = [[Int]]() + while !queue.isEmpty { + let count = queue.count + var subarray = [Int]() + for _ in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + subarray.append(node.val) + // 下一层 + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node)} + } + result.append(subarray) + } + + return result.reversed() +} +``` + +#### Scala: + +```scala +// 107.二叉树的层次遍历II +object Solution { + import scala.collection.mutable + def levelOrderBottom(root: TreeNode): List[List[Int]] = { + val res = mutable.ListBuffer[List[Int]]() + if (root == null) return res.toList + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + val tmp = mutable.ListBuffer[Int]() + val len = queue.size + for (i <- 0 until len) { + val curNode = queue.dequeue() + tmp.append(curNode.value) + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + res.append(tmp.toList) + } + // 最后翻转一下 + res.reverse.toList + } + + +``` + +#### Rust: + +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn level_order_bottom(root: Option>>) -> Vec> { + let mut res = vec![]; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + let mut temp = vec![]; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + temp.push(node.borrow().val); + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + res.push(temp); + } + res.into_iter().rev().collect() + } +} +``` +### C# +```csharp +public IList> LevelOrderBottom(TreeNode root) +{ + var res = new List>(); + var que = new Queue(); + if (root == null) return res; + que.Enqueue(root); + while (que.Count != 0) + { + var size = que.Count; + var vec = new List(); + for (int i = 0; i < size; i++) + { + var cur = que.Dequeue(); + vec.Add(cur.val); + if (cur.left != null) que.Enqueue(cur.left); + if (cur.right != null) que.Enqueue(cur.right); + } + res.Add(vec); + } + res.Reverse(); + return res; +} +``` + +## 199.二叉树的右视图 + +[力扣题目链接](https://leetcode.cn/problems/binary-tree-right-side-view/) + +给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 + +![199.二叉树的右视图](https://file1.kamacoder.com/i/algo/20210203151307377.png) + +### 思路 + +层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。 + +C++代码: + +```CPP +class Solution { +public: + vector rightSideView(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector result; + while (!que.empty()) { + int size = que.size(); + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + if (i == (size - 1)) result.push_back(node->val); // 将每一层的最后元素放入result数组中 + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + } + return result; + } +}; +``` + +### 其他语言版本 + +#### Python: + +```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 rightSideView(self, root: TreeNode) -> List[int]: + if not root: + return [] + + queue = collections.deque([root]) + right_view = [] + + while queue: + level_size = len(queue) + + for i in range(level_size): + node = queue.popleft() + + if i == level_size - 1: + right_view.append(node.val) + + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + + return right_view +``` + +#### Java: + +```java +// 199.二叉树的右视图 +public class N0199 { + /** + * 解法:队列,迭代。 + * 每次返回每层的最后一个字段即可。 + * + * 小优化:每层右孩子先入队。代码略。 + */ + public List rightSideView(TreeNode root) { + List list = new ArrayList<>(); + Deque que = new LinkedList<>(); + + if (root == null) { + return list; + } + + que.offerLast(root); + while (!que.isEmpty()) { + int levelSize = que.size(); + + for (int i = 0; i < levelSize; i++) { + TreeNode poll = que.pollFirst(); + + if (poll.left != null) { + que.addLast(poll.left); + } + if (poll.right != null) { + que.addLast(poll.right); + } + + if (i == levelSize - 1) { + list.add(poll.val); + } + } + } + + return list; + } +} +``` + +#### Go: + +```GO +/** +199. 二叉树的右视图 + */ +func rightSideView(root *TreeNode) []int { + if root == nil { + return nil + } + res := make([]int, 0) + queue := list.New() + queue.PushBack(root) + + for queue.Len() > 0 { + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + // 取每层的最后一个元素,添加到结果集中 + if i == length-1 { + res = append(res, node.Val) + } + } + } + return res +} +``` + +```GO +// 使用切片作为队列 +func rightSideView(root *TreeNode) []int { + res := make([]int, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + if i == size-1 { + res = append(res, node.Val) + } + } + } + return res +} +``` + +#### JavaScript: + +```javascript +var rightSideView = function(root) { + //二叉树右视图 只需要把每一层最后一个节点存储到res数组 + let res = [], queue = []; + queue.push(root); + + while(queue.length && root!==null) { + // 记录当前层级节点个数 + let length = queue.length; + while(length--) { + let node = queue.shift(); + // length长度为0的时候表明到了层级最后一个节点 + if(!length) { + res.push(node.val); + } + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + + return res; +}; +``` + +#### TypeScript: + +```typescript +function rightSideView(root: TreeNode | null): number[] { + let helperQueue: TreeNode[] = []; + let resArr: number[] = []; + let tempNode: TreeNode; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + if (i === length - 1) resArr.push(tempNode.val); + if (tempNode.left !== null) helperQueue.push(tempNode.left); + if (tempNode.right !== null) helperQueue.push(tempNode.right); + } + } + return resArr; +}; +``` + +#### Swift: + +```swift +func rightSideView(_ root: TreeNode?) -> [Int] { + // 表示一层 + var queue = [TreeNode]() + if let node = root { queue.append(node) } + var result = [Int]() + while !queue.isEmpty { + let count = queue.count + for i in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + if i == count - 1 { result.append(node.val) } + + // 下一层 + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node) } + } + } + + return result +} +``` + +#### Scala: + +```scala +// 199.二叉树的右视图 +object Solution { + import scala.collection.mutable + def rightSideView(root: TreeNode): List[Int] = { + val res = mutable.ListBuffer[Int]() + if (root == null) return res.toList + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + val len = queue.size + var curNode: TreeNode = null + for (i <- 0 until len) { + curNode = queue.dequeue() + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + res.append(curNode.value) // 把最后一个节点的值加入解集 + } + res.toList // 最后需要把res转换为List,return关键字可以省略 + } +} +``` + +#### Rust: + +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn right_side_view(root: Option>>) -> Vec { + let mut res = vec![]; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + let len = queue.len(); + for i in 0..len { + let node = queue.pop_front().unwrap().unwrap(); + if i == len - 1 { + res.push(node.borrow().val); + } + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + } + res + } +} +``` + +#### C#: + +```C# 199.二叉树的右视图 +public class Solution +{ + public IList RightSideView(TreeNode root) + { + var result = new List(); + Queue queue = new(); + + if (root != null) + { + queue.Enqueue(root); + } + while (queue.Count > 0) + { + int count = queue.Count; + int lastValue = count - 1; + for (int i = 0; i < count; i++) + { + var currentNode = queue.Dequeue(); + if (i == lastValue) + { + result.Add(currentNode.val); + } + + // lastValue == i == count -1 : left 先于 right 进入Queue + if (currentNode.left != null) queue.Enqueue(currentNode.left); + if (currentNode.right != null) queue.Enqueue(currentNode.right); + + //// lastValue == i == 0: right 先于 left 进入Queue + // if(currentNode.right !=null ) queue.Enqueue(currentNode.right); + // if(currentNode.left !=null ) queue.Enqueue(currentNode.left); + } + } + + return result; + } +} +``` + +## 637.二叉树的层平均值 + +[力扣题目链接](https://leetcode.cn/problems/average-of-levels-in-binary-tree/) + +给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。 + +![637.二叉树的层平均值](https://file1.kamacoder.com/i/algo/20210203151350500.png) + +### 思路 + +本题就是层序遍历的时候把一层求个总和再取一个均值。 + +C++代码: + +```CPP +class Solution { +public: + vector averageOfLevels(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector result; + while (!que.empty()) { + int size = que.size(); + double sum = 0; // 统计每一层的和 + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + sum += node->val; + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + result.push_back(sum / size); // 将每一层均值放进结果集 + } + return result; + } +}; + +``` + +### 其他语言版本 + +#### Python: + +```python +class Solution: + """二叉树层平均值迭代解法""" + +# 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 averageOfLevels(self, root: TreeNode) -> List[float]: + if not root: + return [] + + queue = collections.deque([root]) + averages = [] + + while queue: + size = len(queue) + level_sum = 0 + + for i in range(size): + node = queue.popleft() + + + level_sum += node.val + + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + + averages.append(level_sum / size) + + return averages +``` + +#### Java: + +```java +// 637. 二叉树的层平均值 +public class N0637 { + + /** + * 解法:队列,迭代。 + * 每次返回每层的最后一个字段即可。 + */ + public List averageOfLevels(TreeNode root) { + List list = new ArrayList<>(); + Deque que = new LinkedList<>(); + + if (root == null) { + return list; + } + + que.offerLast(root); + while (!que.isEmpty()) { + + int levelSize = que.size(); + double levelSum = 0.0; + for (int i = 0; i < levelSize; i++) { + TreeNode poll = que.pollFirst(); + + levelSum += poll.val; + + if (poll.left != null) { + que.addLast(poll.left); + } + if (poll.right != null) { + que.addLast(poll.right); + } + } + list.add(levelSum / levelSize); + } + return list; + } +} +``` + +#### Go: + +```GO +/** +637. 二叉树的层平均值 + */ +func averageOfLevels(root *TreeNode) []float64 { + if root == nil { + // 防止为空 + return nil + } + res := make([]float64, 0) + queue := list.New() + queue.PushBack(root) + + var sum int + for queue.Len() > 0 { + //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + // 当前层元素求和 + sum += node.Val + } + // 计算每层的平均值,将结果添加到响应结果中 + res = append(res, float64(sum)/float64(length)) + sum = 0 // 清空该层的数据 + } + return res +} +``` + +```GO +// 使用切片作为队列 +func averageOfLevels(root *TreeNode) []float64 { + res := make([]float64, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + sum := 0 + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + sum += node.Val + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + res = append(res, float64(sum)/float64(size)) + } + return res +} +``` + +#### JavaScript: + +```javascript +var averageOfLevels = function(root) { + let res = [], + queue = []; + queue.push(root); + while (queue.length) { + // 每一层节点个数; + let lengthLevel = queue.length, + len = queue.length, + // sum记录每一层的和; + sum = 0; + while (lengthLevel--) { + const node = queue.shift(); + sum += node.val; + // 队列存放下一层节点 + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + // 求平均值 + res.push(sum / len); + } + return res; +}; +``` + +#### TypeScript: + +```typescript +function averageOfLevels(root: TreeNode | null): number[] { + let helperQueue: TreeNode[] = []; + let resArr: number[] = []; + let total: number = 0; + let tempNode: TreeNode; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + let length = helperQueue.length; + for (let i = 0; i < length; i++) { + tempNode = helperQueue.shift()!; + total += tempNode.val; + if (tempNode.left) helperQueue.push(tempNode.left); + if (tempNode.right) helperQueue.push(tempNode.right); + } + resArr.push(total / length); + total = 0; + } + return resArr; +}; +``` + +#### Swift: + +```swift +func averageOfLevels(_ root: TreeNode?) -> [Double] { + // 表示一层 + var queue = [TreeNode]() + if let node = root { queue.append(node) } + var result = [Double]() + while !queue.isEmpty { + let count = queue.count + var sum = 0 + for _ in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + sum += node.val + + // 下一层 + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node) } + } + result.append(Double(sum) / Double(count)) + } + + return result +} +``` + +#### Scala: + +```scala +// 637.二叉树的层平均值 +object Solution { + import scala.collection.mutable + def averageOfLevels(root: TreeNode): Array[Double] = { + val res = mutable.ArrayBuffer[Double]() + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + var sum = 0.0 + var len = queue.size + for (i <- 0 until len) { + var curNode = queue.dequeue() + sum += curNode.value // 累加该层的值 + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + res.append(sum / len) // 平均值即为sum/len + } + res.toArray // 最后需要转换为Array,return关键字可以省略 + } +} +``` + +#### Rust: + +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn average_of_levels(root: Option>>) -> Vec { + let mut res = vec![]; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + let len = queue.len(); + let mut sum = 0; + for _ in 0..len { + let node = queue.pop_front().unwrap().unwrap(); + sum += node.borrow().val; + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + res.push((sum as f64) / len as f64); + } + res + } +} +``` + +#### C#: + +```C# 二叉树的层平均值 +public class Solution { + public IList AverageOfLevels(TreeNode root) { + var result= new List(); + Queue queue = new(); + if(root !=null) queue.Enqueue(root); + + while (queue.Count > 0) + { + int count = queue.Count; + double value=0; + for (int i = 0; i < count; i++) + { + var curentNode=queue.Dequeue(); + value += curentNode.val; + if (curentNode.left!=null) queue.Enqueue(curentNode.left); + if (curentNode.right!=null) queue.Enqueue(curentNode.right); + } + result.Add(value/count); + } + + return result; + } +} + +``` + +## 429.N叉树的层序遍历 + +[力扣题目链接](https://leetcode.cn/problems/n-ary-tree-level-order-traversal/) + +给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。 + +例如,给定一个 3叉树 : + +![429. N叉树的层序遍历](https://file1.kamacoder.com/i/algo/20210203151439168.png) + +返回其层序遍历: + +[ + [1], + [3,2,4], + [5,6] +] + +### 思路 + +这道题依旧是模板题,只不过一个节点有多个孩子了 + +C++代码: + +```CPP +class Solution { +public: + vector> levelOrder(Node* root) { + queue que; + if (root != NULL) que.push(root); + vector> result; + while (!que.empty()) { + int size = que.size(); + vector vec; + for (int i = 0; i < size; i++) { + Node* node = que.front(); + que.pop(); + vec.push_back(node->val); + for (int i = 0; i < node->children.size(); i++) { // 将节点孩子加入队列 + if (node->children[i]) que.push(node->children[i]); + } + } + result.push_back(vec); + } + return result; + + } +}; +``` + +### 其他语言版本 + +#### Python: + +```python +""" +# Definition for a Node. +class Node: + def __init__(self, val=None, children=None): + self.val = val + self.children = children +""" + +class Solution: + def levelOrder(self, root: 'Node') -> List[List[int]]: + if not root: + return [] + + result = [] + queue = collections.deque([root]) + + while queue: + level_size = len(queue) + level = [] + + for _ in range(level_size): + node = queue.popleft() + level.append(node.val) + + for child in node.children: + queue.append(child) + + result.append(level) + + return result +``` + +```python +# LeetCode 429. N-ary Tree Level Order Traversal +# 递归法 +class Solution: + def levelOrder(self, root: 'Node') -> List[List[int]]: + if not root: return [] + result=[] + def traversal(root,depth): + if len(result)==depth:result.append([]) + result[depth].append(root.val) + if root.children: + for i in range(len(root.children)):traversal(root.children[i],depth+1) + + traversal(root,0) + return result +``` + +#### Java: + +```java +// 429. N 叉树的层序遍历 +public class N0429 { + /** + * 解法1:队列,迭代。 + */ + public List> levelOrder(Node root) { + List> list = new ArrayList<>(); + Deque que = new LinkedList<>(); + + if (root == null) { + return list; + } + + que.offerLast(root); + while (!que.isEmpty()) { + int levelSize = que.size(); + List levelList = new ArrayList<>(); + + for (int i = 0; i < levelSize; i++) { + Node poll = que.pollFirst(); + + levelList.add(poll.val); + + List children = poll.children; + if (children == null || children.size() == 0) { + continue; + } + for (Node child : children) { + if (child != null) { + que.offerLast(child); + } + } + } + list.add(levelList); + } + + return list; + } + + class Node { + public int val; + public List children; + + public Node() {} + + public Node(int _val) { + val = _val; + } + + public Node(int _val, List _children) { + val = _val; + children = _children; + } + } +} +``` + +#### Go: + +```GO +/** +429. N 叉树的层序遍历 + */ + +func levelOrder(root *Node) [][]int { + queue := list.New() + res := [][]int{} //结果集 + if root == nil{ + return res + } + queue.PushBack(root) + for queue.Len() > 0 { + length := queue.Len() //记录当前层的数量 + var tmp []int + for T := 0; T < length; T++ { //该层的每个元素:一添加到该层的结果集中;二找到该元素的下层元素加入到队列中,方便下次使用 + myNode := queue.Remove(queue.Front()).(*Node) + tmp = append(tmp, myNode.Val) + for i := 0; i < len(myNode.Children); i++ { + queue.PushBack(myNode.Children[i]) + } + } + res = append(res, tmp) + } + + return res +} +``` + +```GO +// 使用切片作为队列 +func levelOrder(root *Node) [][]int { + res := make([][]int, 0) + if root == nil { + return res + } + queue := make([]*Node, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + level := make([]int, 0) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + level = append(level, node.Val) + if len(node.Children) > 0 { + queue = append(queue, node.Children...) + } + } + res = append(res, level) + } + return res +} +``` + +#### JavaScript: + +```JavaScript +var levelOrder = function(root) { + //每一层可能有2个以上,所以不再使用node.left node.right + let res = [], queue = []; + queue.push(root); + + while(queue.length && root!==null) { + //记录每一层节点个数还是和二叉树一致 + let length = queue.length; + //存放每层节点 也和二叉树一致 + let curLevel = []; + while(length--) { + let node = queue.shift(); + curLevel.push(node.val); + + //这里不再是 ndoe.left node.right 而是循坏node.children + for(let item of node.children){ + item && queue.push(item); + } + } + res.push(curLevel); + } + + return res; +}; +``` + +#### TypeScript: + +```typescript +function levelOrder(root: Node | null): number[][] { + let helperQueue: Node[] = []; + let resArr: number[][] = []; + let tempArr: number[] = []; + if (root !== null) helperQueue.push(root); + let curNode: Node; + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + curNode = helperQueue.shift()!; + tempArr.push(curNode.val); + helperQueue.push(...curNode.children); + } + resArr.push(tempArr); + tempArr = []; + } + return resArr; +}; +``` + +#### Swift: + +```swift +func levelOrder(_ root: Node?) -> [[Int]] { + // 表示一层 + var queue = [Node]() + if let node = root { queue.append(node) } + var result = [[Int]]() + while !queue.isEmpty { + let count = queue.count + var subarray = [Int]() + for _ in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + subarray.append(node.val) + // 下一层 + for node in node.children { queue.append(node) } + } + result.append(subarray) + } + + return result +} +``` + +#### Scala: + +```scala +// 429.N叉树的层序遍历 +object Solution { + import scala.collection.mutable + def levelOrder(root: Node): List[List[Int]] = { + val res = mutable.ListBuffer[List[Int]]() + if (root == null) return res.toList + val queue = mutable.Queue[Node]() + queue.enqueue(root) // 根节点入队 + while (!queue.isEmpty) { + val tmp = mutable.ListBuffer[Int]() // 存储每层节点 + val len = queue.size + for (i <- 0 until len) { + val curNode = queue.dequeue() + tmp.append(curNode.value) // 将该节点的值加入tmp + // 循环遍历该节点的子节点,加入队列 + for (child <- curNode.children) { + queue.enqueue(child) + } + } + res.append(tmp.toList) // 将该层的节点放到结果集 + } + res.toList + } +} +``` + +#### Rust: + +```rust +pub struct Solution; +#[derive(Debug, PartialEq, Eq)] +pub struct Node { + pub val: i32, + pub children: Vec>>>, +} + +impl Node { + #[inline] + pub fn new(val: i32) -> Node { + Node { + val, + children: vec![], + } + } +} + +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn level_order(root: Option>>) -> Vec> { + let mut res = vec![]; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + let mut temp = vec![]; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + temp.push(node.borrow().val); + if !node.borrow().children.is_empty() { + for n in node.borrow().children.clone() { + queue.push_back(n); + } + } + } + res.push(temp) + } + res + } +} +``` + +## 515.在每个树行中找最大值 + +[力扣题目链接](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/) + +您需要在二叉树的每一行中找到最大的值。 + +![515.在每个树行中找最大值](https://file1.kamacoder.com/i/algo/20210203151532153.png) + +### 思路 + +层序遍历,取每一层的最大值 + +C++代码: + +```CPP +class Solution { +public: + vector largestValues(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector result; + while (!que.empty()) { + int size = que.size(); + int maxValue = INT_MIN; // 取每一层的最大值 + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + maxValue = node->val > maxValue ? node->val : maxValue; + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + result.push_back(maxValue); // 把最大值放进数组 + } + return result; + } +}; +``` + +### 其他语言版本 + +#### Python: + +```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 largestValues(self, root: TreeNode) -> List[int]: + if not root: + return [] + + result = [] + queue = collections.deque([root]) + + while queue: + level_size = len(queue) + max_val = float('-inf') + + for _ in range(level_size): + node = queue.popleft() + max_val = max(max_val, node.val) + + if node.left: + queue.append(node.left) + + if node.right: + queue.append(node.right) + + result.append(max_val) + + return result +``` + +#### Java: + +```java +class Solution { + public List largestValues(TreeNode root) { + if(root == null){ + return Collections.emptyList(); + } + List result = new ArrayList(); + Queue queue = new LinkedList(); + queue.offer(root); + while(!queue.isEmpty()){ + int max = Integer.MIN_VALUE; + for(int i = queue.size(); i > 0; i--){ + TreeNode node = queue.poll(); + max = Math.max(max, node.val); + if(node.left != null) queue.offer(node.left); + if(node.right != null) queue.offer(node.right); + } + result.add(max); + } + return result; + } +} +``` + +#### Go: + +```GO +/** +515. 在每个树行中找最大值 + */ +func largestValues(root *TreeNode) []int { + if root == nil { + //防止为空 + return nil + } + queue := list.New() + queue.PushBack(root) + ans := make([]int, 0) + temp := math.MinInt64 + // 层序遍历 + for queue.Len() > 0 { + //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode)//出队列 + // 比较当前层中的最大值和新遍历的元素大小,取两者中大值 + temp = max(temp, node.Val) + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + } + ans = append(ans, temp) + temp = math.MinInt64 + } + return ans +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +```GO +// 使用切片作为队列 +func largestValues(root *TreeNode) []int { + res := make([]int, 0) + if root == nil { + return res + } + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + maxValue := math.MinInt64 + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Val > maxValue { + maxValue = node.Val + } + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + res = append(res, maxValue) + } + return res +} +``` + +#### JavaScript: + +```javascript +var largestValues = function (root) { + let res = [], + queue = []; + queue.push(root); + if (root === null) { + return res; + } + while (queue.length) { + let lengthLevel = queue.length, + // 初始值设为负无穷大 + max = -Infinity; + while (lengthLevel--) { + const node = queue.shift(); + // 在当前层中找到最大值 + max = Math.max(max, node.val); + // 找到下一层的节点 + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + res.push(max); + } + return res; +}; +``` + +#### TypeScript: + +```typescript +function largestValues(root: TreeNode | null): number[] { + let helperQueue: TreeNode[] = []; + let resArr: number[] = []; + let tempNode: TreeNode; + let max: number = 0; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + if (i === 0) { + max = tempNode.val; + } else { + max = max > tempNode.val ? max : tempNode.val; + } + if (tempNode.left) helperQueue.push(tempNode.left); + if (tempNode.right) helperQueue.push(tempNode.right); + } + resArr.push(max); + } + return resArr; +}; +``` + +#### Swift: + +```swift +func largestValues(_ root: TreeNode?) -> [Int] { + // 表示一层 + var queue = [TreeNode]() + if let node = root { queue.append(node) } + var result = [Int]() + while !queue.isEmpty { + let count = queue.count + var max = queue[0].val + for _ in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + if node.val > max { max = node.val } + + // 下一层 + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node) } + } + result.append(max) + } + + return result +} +``` + +#### Scala: + +```scala +// 515.在每个树行中找最大值 +object Solution { + import scala.collection.mutable + def largestValues(root: TreeNode): List[Int] = { + val res = mutable.ListBuffer[Int]() + if (root == null) return res.toList + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + var max = Int.MinValue // 初始化max为系统最小值 + val len = queue.size + for (i <- 0 until len) { + val curNode = queue.dequeue() + max = math.max(max, curNode.value) // 对比求解最大值 + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + res.append(max) // 将最大值放入结果集 + } + res.toList + } +} +``` + +#### Rust: + +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn largest_values(root: Option>>) -> Vec { + let mut res = vec![]; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + let mut max = i32::MIN; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + max = max.max(node.borrow().val); + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + res.push(max); + } + res + } +} +``` + +## 116.填充每个节点的下一个右侧节点指针 + +[力扣题目链接](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) + +给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: + +```cpp +struct Node { + int val; + Node *left; + Node *right; + Node *next; +} +``` + + +填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 + +初始状态下,所有 next 指针都被设置为 NULL。 + +![116.填充每个节点的下一个右侧节点指针](https://file1.kamacoder.com/i/algo/20210203152044855.jpg) + +### 思路 + +本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了 + +C++代码: + +```CPP +class Solution { +public: + Node* connect(Node* root) { + queue que; + if (root != NULL) que.push(root); + while (!que.empty()) { + int size = que.size(); + // vector vec; + Node* nodePre; + Node* node; + for (int i = 0; i < size; i++) { + if (i == 0) { + nodePre = que.front(); // 取出一层的头结点 + que.pop(); + node = nodePre; + } else { + node = que.front(); + que.pop(); + nodePre->next = node; // 本层前一个节点next指向本节点 + nodePre = nodePre->next; + } + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + nodePre->next = NULL; // 本层最后一个节点指向NULL + } + return root; + + } +}; +``` + +### 其他语言版本 + +#### Java: + +```java +class Solution { + public Node connect(Node root) { + Queue tmpQueue = new LinkedList(); + if (root != null) tmpQueue.add(root); + + while (tmpQueue.size() != 0){ + int size = tmpQueue.size(); + + Node cur = tmpQueue.poll(); + if (cur.left != null) tmpQueue.add(cur.left); + if (cur.right != null) tmpQueue.add(cur.right); + + for (int index = 1; index < size; index++){ + Node next = tmpQueue.poll(); + if (next.left != null) tmpQueue.add(next.left); + if (next.right != null) tmpQueue.add(next.right); + + cur.next = next; + cur = next; + } + } + + return root; + } +} +``` + +#### Python: + +```python +""" +# Definition for a Node. +class Node: + def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = 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 root + + queue = collections.deque([root]) + + while queue: + level_size = len(queue) + prev = None + + for i in range(level_size): + node = queue.popleft() + + if prev: + prev.next = node + + prev = node + + if node.left: + queue.append(node.left) + + if node.right: + queue.append(node.right) + + return root +``` + +#### Go: + +```GO +/** +116. 填充每个节点的下一个右侧节点指针 +117. 填充每个节点的下一个右侧节点指针 II + */ + +func connect(root *Node) *Node { + if root == nil { //防止为空 + return root + } + queue := list.New() + queue.PushBack(root) + tmpArr := make([]*Node, 0) + for queue.Len() > 0 { + length := queue.Len() //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*Node) //出队列 + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + tmpArr = append(tmpArr, node) //将值加入本层切片中 + } + if len(tmpArr) > 1 { + // 遍历每层元素,指定next + for i := 0; i < len(tmpArr)-1; i++ { + tmpArr[i].Next = tmpArr[i+1] + } + } + tmpArr = []*Node{} //清空层的数据 + } + return root +} + +``` + +```GO +// 使用切片作为队列 +func connect(root *Node) *Node { + if root == nil { + return root + } + queue := make([]*Node, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[i] + if i != size - 1 { + queue[i].Next = queue[i+1] + } + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + queue = queue[size:] + } + return root +} +``` + +#### JavaScript: + +```javascript +/** + * // Definition for a Node. + * function Node(val, left, right, next) { + * this.val = val === undefined ? null : val; + * this.left = left === undefined ? null : left; + * this.right = right === undefined ? null : right; + * this.next = next === undefined ? null : next; + * }; + */ + +/** + * @param {Node} root + * @return {Node} + */ +var connect = function(root) { + if (root === null) return root; + let queue = [root]; + while (queue.length) { + let n = queue.length; + for (let i = 0; i < n; i++) { + let node = queue.shift(); + if (i < n-1) { + node.next = queue[0]; + } + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + return root; +}; + +``` + +#### TypeScript: + +```typescript +function connect(root: Node | null): Node | null { + let helperQueue: Node[] = []; + let preNode: Node, curNode: Node; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + if (i === 0) { + preNode = helperQueue.shift()!; + } else { + curNode = helperQueue.shift()!; + preNode.next = curNode; + preNode = curNode; + } + if (preNode.left) helperQueue.push(preNode.left); + if (preNode.right) helperQueue.push(preNode.right); + } + preNode.next = null; + } + return root; +}; +``` + +#### Swift: + +```swift +func connect(_ root: Node?) -> Node? { + // 表示一层 + var queue = [Node]() + if let node = root { queue.append(node) } + while !queue.isEmpty { + let count = queue.count + var current, previous: Node! + for i in 0 ..< count { + // 当前层 + if i == 0 { + previous = queue.removeFirst() + current = previous + } else { + current = queue.removeFirst() + previous.next = current + previous = current + } + + // 下一层 + if let node = current.left { queue.append(node) } + if let node = current.right { queue.append(node) } + } + previous.next = nil + } + + return root +} +``` + +#### Scala: + +```scala +// 116.填充每个节点的下一个右侧节点指针 +object Solution { + import scala.collection.mutable + + def connect(root: Node): Node = { + if (root == null) return root + val queue = mutable.Queue[Node]() + queue.enqueue(root) + while (!queue.isEmpty) { + val len = queue.size + val tmp = mutable.ListBuffer[Node]() + for (i <- 0 until len) { + val curNode = queue.dequeue() + tmp.append(curNode) + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + // 处理next指针 + for (i <- 0 until tmp.size - 1) { + tmp(i).next = tmp(i + 1) + } + tmp(tmp.size-1).next = null + } + root + } +} +``` + +## 117.填充每个节点的下一个右侧节点指针II + +[力扣题目链接](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/) + +### 思路 + +这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道 + +C++代码: + +```CPP +class Solution { +public: + Node* connect(Node* root) { + queue que; + if (root != NULL) que.push(root); + while (!que.empty()) { + int size = que.size(); + vector vec; + Node* nodePre; + Node* node; + for (int i = 0; i < size; i++) { + if (i == 0) { + nodePre = que.front(); // 取出一层的头结点 + que.pop(); + node = nodePre; + } else { + node = que.front(); + que.pop(); + nodePre->next = node; // 本层前一个节点next指向本节点 + nodePre = nodePre->next; + } + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + nodePre->next = NULL; // 本层最后一个节点指向NULL + } + return root; + } +}; +``` + +### 其他语言版本 + +#### Java: + +```java +// 二叉树之层次遍历 +class Solution { + public Node connect(Node root) { + Queue queue = new LinkedList<>(); + if (root != null) { + queue.add(root); + } + while (!queue.isEmpty()) { + int size = queue.size(); + Node node = null; + Node nodePre = null; + + for (int i = 0; i < size; i++) { + if (i == 0) { + nodePre = queue.poll(); // 取出本层头一个节点 + node = nodePre; + } else { + node = queue.poll(); + nodePre.next = node; // 本层前一个节点 next 指向当前节点 + nodePre = nodePre.next; + } + if (node.left != null) { + queue.add(node.left); + } + if (node.right != null) { + queue.add(node.right); + } + } + nodePre.next = null; // 本层最后一个节点 next 指向 null + } + return root; + } +} +``` + +#### Python: + +```python +# 层序遍历解法 +""" +# Definition for a Node. +class Node: + def __init__(self, val: int = 0, left: 'Node' = None, right: 'Node' = None, next: 'Node' = 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 root + + queue = collections.deque([root]) + + while queue: + level_size = len(queue) + prev = None + + for i in range(level_size): + node = queue.popleft() + + if prev: + prev.next = node + + prev = node + + if node.left: + queue.append(node.left) + + if node.right: + queue.append(node.right) + + return root + +``` + +#### Go: + +```GO +/** +116. 填充每个节点的下一个右侧节点指针 +117. 填充每个节点的下一个右侧节点指针 II + */ + +func connect(root *Node) *Node { + if root == nil { //防止为空 + return root + } + queue := list.New() + queue.PushBack(root) + tmpArr := make([]*Node, 0) + for queue.Len() > 0 { + length := queue.Len() //保存当前层的长度,然后处理当前层(十分重要,防止添加下层元素影响判断层中元素的个数) + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*Node) //出队列 + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + tmpArr = append(tmpArr, node) //将值加入本层切片中 + } + if len(tmpArr) > 1 { + // 遍历每层元素,指定next + for i := 0; i < len(tmpArr)-1; i++ { + tmpArr[i].Next = tmpArr[i+1] + } + } + tmpArr = []*Node{} //清空层的数据 + } + return root +} +``` + +```GO +// 使用切片作为队列 +func connect(root *Node) *Node { + if root == nil { + return root + } + queue := make([]*Node, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[i] + if i != size - 1 { + queue[i].Next = queue[i+1] + } + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + queue = queue[size:] + } + return root +} +``` + +#### JavaScript: + +```javascript +/** + * // Definition for a Node. + * function Node(val, left, right, next) { + * this.val = val === undefined ? null : val; + * this.left = left === undefined ? null : left; + * this.right = right === undefined ? null : right; + * this.next = next === undefined ? null : next; + * }; + */ + +/** + * @param {Node} root + * @return {Node} + */ +var connect = function(root) { + if (root === null) { + return null; + } + let queue = [root]; + while (queue.length > 0) { + let n = queue.length; + for (let i=0; i 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + if (i === 0) { + preNode = helperQueue.shift()!; + } else { + curNode = helperQueue.shift()!; + preNode.next = curNode; + preNode = curNode; + } + if (preNode.left) helperQueue.push(preNode.left); + if (preNode.right) helperQueue.push(preNode.right); + } + preNode.next = null; + } + return root; +}; +``` + +#### Swift: + +```swift +func connect(_ root: Node?) -> Node? { + // 表示一层 + var queue = [Node]() + if let node = root { queue.append(node) } + while !queue.isEmpty { + let count = queue.count + var current, previous: Node! + for i in 0 ..< count { + // 当前层 + if i == 0 { + previous = queue.removeFirst() + current = previous + } else { + current = queue.removeFirst() + previous.next = current + previous = current + } + + // 下一层 + if let node = current.left { queue.append(node) } + if let node = current.right { queue.append(node) } + } + previous.next = nil + } + + return root +} +``` + +#### Scala: + +```scala +// 117.填充每个节点的下一个右侧节点指针II +object Solution { + import scala.collection.mutable + + def connect(root: Node): Node = { + if (root == null) return root + val queue = mutable.Queue[Node]() + queue.enqueue(root) + while (!queue.isEmpty) { + val len = queue.size + val tmp = mutable.ListBuffer[Node]() + for (i <- 0 until len) { + val curNode = queue.dequeue() + tmp.append(curNode) + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + // 处理next指针 + for (i <- 0 until tmp.size - 1) { + tmp(i).next = tmp(i + 1) + } + tmp(tmp.size-1).next = null + } + root + } +} +``` + +## 104.二叉树的最大深度 + +[力扣题目链接](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) + +给定一个二叉树,找出其最大深度。 + +二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 + +说明: 叶子节点是指没有子节点的节点。 + +示例: + +给定二叉树 [3,9,20,null,null,15,7], + +![104. 二叉树的最大深度](https://file1.kamacoder.com/i/algo/20210203153031914-20230310134849764.png) + +返回它的最大深度 3 。 + +### 思路 + +使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。 + +在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度,如图所示: + +![层序遍历](https://file1.kamacoder.com/i/algo/20200810193056585-20230310134854803.png) + +所以这道题的迭代法就是一道模板题,可以使用二叉树层序遍历的模板来解决的。 + +C++代码如下: + +```CPP +class Solution { +public: + int maxDepth(TreeNode* root) { + if (root == NULL) return 0; + int depth = 0; + queue que; + que.push(root); + while(!que.empty()) { + int size = que.size(); + depth++; // 记录深度 + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + } + return depth; + } +}; +``` + +### 其他语言版本 + +#### Java: + +```Java +class Solution { + public int maxDepth(TreeNode root) { + if (root == null) return 0; + Queue que = new LinkedList<>(); + que.offer(root); + int depth = 0; + while (!que.isEmpty()) + { + int len = que.size(); + while (len > 0) + { + TreeNode node = que.poll(); + if (node.left != null) que.offer(node.left); + if (node.right != null) que.offer(node.right); + len--; + } + depth++; + } + return depth; + } +} +``` + +#### Python: + +```python 3 +# 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 maxDepth(self, root: TreeNode) -> int: + if not root: + return 0 + + depth = 0 + queue = collections.deque([root]) + + while queue: + depth += 1 + for _ in range(len(queue)): + node = queue.popleft() + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + + return depth + +``` + +#### Go: + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func maxDepth(root *TreeNode) int { + ans := 0 + if root == nil { + return 0 + } + queue := list.New() + queue.PushBack(root) + for queue.Len() > 0 { + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + } + ans++//记录深度,其他的是层序遍历的板子 + } + return ans +} +``` + +```go +// 使用切片作为队列 +func maxDepth(root *TreeNode) int { + if root == nil { + return 0 + } + depth := 0 + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + } + depth++ + } + return depth +} +``` + +#### JavaScript: + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @return {number} + */ +var maxDepth = function (root) { + // 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 + let max = 0, + queue = [root]; + if (root === null) { + return max; + } + while (queue.length) { + max++; + let length = queue.length; + while (length--) { + let node = queue.shift(); + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + return max; +}; +``` + +#### TypeScript: + +```typescript +function maxDepth(root: TreeNode | null): number { + let helperQueue: TreeNode[] = []; + let resDepth: number = 0; + let tempNode: TreeNode; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + resDepth++; + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + if (tempNode.left) helperQueue.push(tempNode.left); + if (tempNode.right) helperQueue.push(tempNode.right); + } + } + return resDepth; +}; +``` + +#### Swift: + +```swift +func maxDepth(_ root: TreeNode?) -> Int { + guard root != nil else { return 0 } + var depth = 0 + var queue = [TreeNode]() + queue.append(root!) + while !queue.isEmpty { + let count = queue.count + depth += 1 + for _ in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + + // 下一层 + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node) } + } + } + + return depth +} +``` + +#### Scala: + +```scala +// 104.二叉树的最大深度 +object Solution { + import scala.collection.mutable + def maxDepth(root: TreeNode): Int = { + if (root == null) return 0 + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + var depth = 0 + while (!queue.isEmpty) { + val len = queue.length + depth += 1 + for (i <- 0 until len) { + val curNode = queue.dequeue() + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + } + depth + } +} +``` + +#### Rust: + +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn max_depth(root: Option>>) -> i32 { + let mut queue = VecDeque::new(); + let mut res = 0; + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + res += 1; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + } + res + } +} +``` + +## 111.二叉树的最小深度 + +[力扣题目链接](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) + +### 思路 + +相对于 104.二叉树的最大深度 ,本题还也可以使用层序遍历的方式来解决,思路是一样的。 + +**需要注意的是,只有当左右孩子都为空的时候,才说明遍历的最低点了。如果其中一个孩子为空则不是最低点** + +代码如下:(详细注释) + +```CPP +class Solution { +public: + int minDepth(TreeNode* root) { + if (root == NULL) return 0; + int depth = 0; + queue que; + que.push(root); + while(!que.empty()) { + int size = que.size(); + depth++; // 记录最小深度 + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + if (!node->left && !node->right) { // 当左右孩子都为空的时候,说明是最低点的一层了,退出 + return depth; + } + } + } + return depth; + } +}; +``` + +### 其他语言版本 + +#### Java: + +```java +class Solution { + public int minDepth(TreeNode root){ + if (root == null) { + return 0; + } + Queue queue = new LinkedList<>(); + queue.offer(root); + int depth = 0; + while (!queue.isEmpty()){ + int size = queue.size(); + depth++; + TreeNode cur = null; + for (int i = 0; i < size; i++) { + cur = queue.poll(); + //如果当前节点的左右孩子都为空,直接返回最小深度 + if (cur.left == null && cur.right == null){ + return depth; + } + if (cur.left != null) queue.offer(cur.left); + if (cur.right != null) queue.offer(cur.right); + } + } + return depth; + } +} +``` + +#### Python: + +```python 3 +# 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 minDepth(self, root: TreeNode) -> int: + if not root: + return 0 + depth = 0 + queue = collections.deque([root]) + + while queue: + depth += 1 + for _ in range(len(queue)): + node = queue.popleft() + + if not node.left and not node.right: + return depth + + if node.left: + queue.append(node.left) + + if node.right: + queue.append(node.right) + + return depth +``` + +#### Go: + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func minDepth(root *TreeNode) int { + ans := 0 + if root == nil { + return 0 + } + queue := list.New() + queue.PushBack(root) + for queue.Len() > 0 { + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if node.Left == nil && node.Right == nil { //当前节点没有左右节点,则代表此层是最小层 + return ans+1 //返回当前层 ans代表是上一层 + } + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + } + ans++//记录层数 + } + + return ans +} +``` + +```go +// 使用切片作为队列 +func minDepth(root *TreeNode) int { + if root == nil { + return 0 + } + depth := 0 + queue := make([]*TreeNode, 0) + queue = append(queue, root) + for len(queue) > 0 { + size := len(queue) + depth++ + for i := 0; i < size; i++ { + node := queue[0] + queue = queue[1:] + if node.Left != nil { + queue = append(queue, node.Left) + } + if node.Right != nil { + queue = append(queue, node.Right) + } + if node.Left == nil && node.Right == nil { + return depth + } + } + } + return depth +} +``` + +#### JavaScript: + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @return {number} + */ +var minDepth = function(root) { + if (root === null) return 0; + let queue = [root]; + let depth = 0; + while (queue.length) { + let n = queue.length; + depth++; + for (let i=0; i 0) { + resMin++; + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + if (tempNode.left === null && tempNode.right === null) return resMin; + if (tempNode.left !== null) helperQueue.push(tempNode.left); + if (tempNode.right !== null) helperQueue.push(tempNode.right); + } + } + return resMin; +}; +``` + +#### Swift: + +```swift +func minDepth(_ root: TreeNode?) -> Int { + guard root != nil else { return 0 } + var depth = 0 + var queue = [root!] + while !queue.isEmpty { + let count = queue.count + depth += 1 + for _ in 0 ..< count { + // 当前层 + let node = queue.removeFirst() + if node.left == nil, node.right == nil { // 遇到叶子结点则返回 + return depth + } + + // 下一层 + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node) } + } + } + return depth +} +``` + +#### Scala: + +```scala +// 111.二叉树的最小深度 +object Solution { + import scala.collection.mutable + def minDepth(root: TreeNode): Int = { + if (root == null) return 0 + var depth = 0 + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + depth += 1 + val len = queue.size + for (i <- 0 until len) { + val curNode = queue.dequeue() + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + if (curNode.left == null && curNode.right == null) return depth + } + } + depth + } +} +``` + +#### Rust: + +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn min_depth(root: Option>>) -> i32 { + let mut res = 0; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + res += 1; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + if node.borrow().left.is_none() && node.borrow().right.is_none() { + return res; + } + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + } + res + } +} +``` + +## 总结 + +二叉树的层序遍历,**就是图论中的广度优先搜索在二叉树中的应用**,需要借助队列来实现(此时又发现队列的一个应用了)。 + +来吧,一口气打十个: + +* [102.二叉树的层序遍历](https://leetcode.cn/problems/binary-tree-level-order-traversal/) +* [107.二叉树的层次遍历II](https://leetcode.cn/problems/binary-tree-level-order-traversal-ii/) +* [199.二叉树的右视图](https://leetcode.cn/problems/binary-tree-right-side-view/) +* [637.二叉树的层平均值](https://leetcode.cn/problems/binary-tree-right-side-view/) +* [429.N叉树的层序遍历](https://leetcode.cn/problems/n-ary-tree-level-order-traversal/) +* [515.在每个树行中找最大值](https://leetcode.cn/problems/find-largest-value-in-each-tree-row/) +* [116.填充每个节点的下一个右侧节点指针](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) +* [117.填充每个节点的下一个右侧节点指针II](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node-ii/) +* [104.二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) +* [111.二叉树的最小深度](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) + +**致敬叶师傅!** + + diff --git "a/problems/0104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" "b/problems/0104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" new file mode 100755 index 0000000000..52d6d0e5fd --- /dev/null +++ "b/problems/0104.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\244\247\346\267\261\345\272\246.md" @@ -0,0 +1,1195 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 104.二叉树的最大深度 + +[力扣题目链接](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) + +给定一个二叉树,找出其最大深度。 + +二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 + +说明: 叶子节点是指没有子节点的节点。 + +示例: +给定二叉树 [3,9,20,null,null,15,7], + + +![104. 二叉树的最大深度](https://file1.kamacoder.com/i/algo/20210203153031914-20230310121809902.png) + +返回它的最大深度 3 。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[二叉树的高度和深度有啥区别?究竟用什么遍历顺序?很多录友搞不懂 | 104.二叉树的最大深度](https://www.bilibili.com/video/BV1Gd4y1V75u),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +看完本篇可以一起做了如下两道题目: + +* [104.二叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-binary-tree/) +* [559.n叉树的最大深度](https://leetcode.cn/problems/maximum-depth-of-n-ary-tree/) + + +### 递归法 + +本题可以使用前序(中左右),也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序求的是高度。 + +* 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始) +* 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数或者节点数(取决于高度从0开始还是从1开始) + +**而根节点的高度就是二叉树的最大深度**,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度。 + +这一点其实是很多同学没有想清楚的,很多题解同样没有讲清楚。 + +我先用后序遍历(左右中)来计算树的高度。 + +1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型。 + +代码如下: +```CPP +int getdepth(TreeNode* node) +``` + +2. 确定终止条件:如果为空节点的话,就返回0,表示高度为0。 + +代码如下: +```CPP +if (node == NULL) return 0; +``` + +3. 确定单层递归的逻辑:先求它的左子树的深度,再求右子树的深度,最后取左右深度最大的数值 再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。 + +代码如下: + +```CPP +int leftdepth = getdepth(node->left); // 左 +int rightdepth = getdepth(node->right); // 右 +int depth = 1 + max(leftdepth, rightdepth); // 中 +return depth; +``` + +所以整体c++代码如下: + +```CPP +class Solution { +public: + int getdepth(TreeNode* node) { + if (node == NULL) return 0; + int leftdepth = getdepth(node->left); // 左 + int rightdepth = getdepth(node->right); // 右 + int depth = 1 + max(leftdepth, rightdepth); // 中 + return depth; + } + int maxDepth(TreeNode* root) { + return getdepth(root); + } +}; +``` + +代码精简之后c++代码如下: +```CPP +class Solution { +public: + int maxDepth(TreeNode* root) { + if (root == null) return 0; + return 1 + max(maxDepth(root->left), maxDepth(root->right)); + } +}; + +``` + +**精简之后的代码根本看不出是哪种遍历方式,也看不出递归三部曲的步骤,所以如果对二叉树的操作还不熟练,尽量不要直接照着精简代码来学。** + + +本题当然也可以使用前序,代码如下:(**充分表现出求深度回溯的过程**) + +```CPP +class Solution { +public: + int result; + void getdepth(TreeNode* node, int depth) { + result = depth > result ? depth : result; // 中 + + if (node->left == NULL && node->right == NULL) return ; + + if (node->left) { // 左 + depth++; // 深度+1 + getdepth(node->left, depth); + depth--; // 回溯,深度-1 + } + if (node->right) { // 右 + depth++; // 深度+1 + getdepth(node->right, depth); + depth--; // 回溯,深度-1 + } + return ; + } + int maxDepth(TreeNode* root) { + result = 0; + if (root == NULL) return result; + getdepth(root, 1); + return result; + } +}; +``` + +**可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!** + +注意以上代码是为了把细节体现出来,简化一下代码如下: + +```CPP +class Solution { +public: + int result; + void getdepth(TreeNode* node, int depth) { + result = depth > result ? depth : result; // 中 + if (node->left == NULL && node->right == NULL) return ; + if (node->left) { // 左 + getdepth(node->left, depth + 1); + } + if (node->right) { // 右 + getdepth(node->right, depth + 1); + } + return ; + } + int maxDepth(TreeNode* root) { + result = 0; + if (root == 0) return result; + getdepth(root, 1); + return result; + } +}; +``` + +### 迭代法 + +使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。 + +在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度,如图所示: + + +![层序遍历](https://file1.kamacoder.com/i/algo/20200810193056585.png) + +所以这道题的迭代法就是一道模板题,可以使用二叉树层序遍历的模板来解决的。 + +如果对层序遍历还不清楚的话,可以看这篇:[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html) + +c++代码如下: + +```CPP +class Solution { +public: + int maxDepth(TreeNode* root) { + if (root == NULL) return 0; + int depth = 0; + queue que; + que.push(root); + while(!que.empty()) { + int size = que.size(); + depth++; // 记录深度 + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + } + return depth; + } +}; +``` + +那么我们可以顺便解决一下n叉树的最大深度问题 + +## 相关题目推荐 + +### 559.n叉树的最大深度 + +[力扣题目链接](https://leetcode.cn/problems/maximum-depth-of-n-ary-tree/) + +给定一个 n 叉树,找到其最大深度。 + +最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。 + +例如,给定一个 3叉树 : + +![559.n叉树的最大深度](https://file1.kamacoder.com/i/algo/2021020315313214.png) + +我们应返回其最大深度,3。 + +### 思路 + +依然可以提供递归法和迭代法,来解决这个问题,思路是和二叉树思路一样的,直接给出代码如下: + +#### 递归法 + +c++代码: + +```CPP +class Solution { +public: + int maxDepth(Node* root) { + if (root == 0) return 0; + int depth = 0; + for (int i = 0; i < root->children.size(); i++) { + depth = max (depth, maxDepth(root->children[i])); + } + return depth + 1; + } +}; +``` +#### 迭代法 + +依然是层序遍历,代码如下: + +```CPP +class Solution { +public: + int maxDepth(Node* root) { + queue que; + if (root != NULL) que.push(root); + int depth = 0; + while (!que.empty()) { + int size = que.size(); + depth++; // 记录深度 + for (int i = 0; i < size; i++) { + Node* node = que.front(); + que.pop(); + for (int j = 0; j < node->children.size(); j++) { + if (node->children[j]) que.push(node->children[j]); + } + } + } + return depth; + } +}; +``` + +## 其他语言版本 + +### Java: + +104.二叉树的最大深度 + +```java +class Solution { + /** + * 递归法 + */ + public int maxDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftDepth = maxDepth(root.left); + int rightDepth = maxDepth(root.right); + return Math.max(leftDepth, rightDepth) + 1; + } +} +``` + +```java +class Solution { + /** + * 递归法(求深度法) + */ + //定义最大深度 + int maxnum = 0; + + public int maxDepth(TreeNode root) { + ans(root,0); + return maxnum; + } + + //递归求解最大深度 + void ans(TreeNode tr,int tmp){ + if(tr==null) return; + tmp++; + maxnum = maxnum deque = new LinkedList<>(); + deque.offer(root); + int depth = 0; + while (!deque.isEmpty()) { + int size = deque.size(); + depth++; + for (int i = 0; i < size; i++) { + TreeNode node = deque.poll(); + if (node.left != null) { + deque.offer(node.left); + } + if (node.right != null) { + deque.offer(node.right); + } + } + } + return depth; + } +} +``` + +559.n叉树的最大深度 + +```java +class Solution { + /*递归法,后序遍历求root节点的高度*/ + public int maxDepth(Node root) { + if (root == null) return 0; + + int depth = 0; + if (root.children != null){ + for (Node child : root.children){ + depth = Math.max(depth, maxDepth(child)); + } + } + + return depth + 1; //中节点 + } +} +``` + +```java +class Solution { + /** + * 迭代法,使用层序遍历 + */ + public int maxDepth(Node root) { + if (root == null) return 0; + int depth = 0; + Queue que = new LinkedList<>(); + que.offer(root); + while (!que.isEmpty()) + { + depth ++; + int len = que.size(); + while (len > 0) + { + Node node = que.poll(); + for (int i = 0; i < node.children.size(); i++) + if (node.children.get(i) != null) + que.offer(node.children.get(i)); + len--; + } + } + return depth; + } +} +``` + +### Python : + +104.二叉树的最大深度 + +递归法: +```python +class Solution: + def maxdepth(self, root: treenode) -> int: + return self.getdepth(root) + + def getdepth(self, node): + if not node: + return 0 + leftheight = self.getdepth(node.left) #左 + rightheight = self.getdepth(node.right) #右 + height = 1 + max(leftheight, rightheight) #中 + return height +``` + +递归法:精简代码 +```python +class Solution: + def maxdepth(self, root: treenode) -> int: + if not root: + return 0 + return 1 + max(self.maxdepth(root.left), self.maxdepth(root.right)) +``` + +层序遍历迭代法: +```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 maxDepth(self, root: TreeNode) -> int: + if not root: + return 0 + + depth = 0 + queue = collections.deque([root]) + + while queue: + depth += 1 + for _ in range(len(queue)): + node = queue.popleft() + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + + return depth + +``` + +559.n叉树的最大深度 + +递归法: +```python +class Solution: + def maxDepth(self, root: 'Node') -> int: + if not root: + return 0 + + max_depth = 1 + + for child in root.children: + max_depth = max(max_depth, self.maxDepth(child) + 1) + + return max_depth +``` + +迭代法: +```python +""" +# Definition for a Node. +class Node: + def __init__(self, val=None, children=None): + self.val = val + self.children = children +""" + +class Solution: + def maxDepth(self, root: TreeNode) -> int: + if not root: + return 0 + + depth = 0 + queue = collections.deque([root]) + + while queue: + depth += 1 + for _ in range(len(queue)): + node = queue.popleft() + for child in node.children: + queue.append(child) + + return depth + +``` + +使用栈 +```python +""" +# Definition for a Node. +class Node: + def __init__(self, val=None, children=None): + self.val = val + self.children = children +""" + +class Solution: + def maxDepth(self, root: 'Node') -> int: + if not root: + return 0 + + max_depth = 0 + + stack = [(root, 1)] + + while stack: + node, depth = stack.pop() + max_depth = max(max_depth, depth) + for child in node.children: + stack.append((child, depth + 1)) + + return max_depth +``` + +### Go: + +104.二叉树的最大深度 + +```go +/** + * definition for a binary tree node. + * type treenode struct { + * val int + * left *treenode + * right *treenode + * } + */ +func max (a, b int) int { + if a > b { + return a; + } + return b; +} +// 递归 +func maxdepth(root *treenode) int { + if root == nil { + return 0; + } + return max(maxdepth(root.left), maxdepth(root.right)) + 1; +} + +// 遍历 +func maxdepth(root *treenode) int { + levl := 0; + queue := make([]*treenode, 0); + if root != nil { + queue = append(queue, root); + } + for l := len(queue); l > 0; { + for ;l > 0;l-- { + node := queue[0]; + if node.left != nil { + queue = append(queue, node.left); + } + if node.right != nil { + queue = append(queue, node.right); + } + queue = queue[1:]; + } + levl++; + l = len(queue); + } + return levl; +} + +``` + +559. n叉树的最大深度 + +```go +func maxDepth(root *Node) int { + if root == nil { + return 0 + } + q := list.New() + q.PushBack(root) + depth := 0 + for q.Len() > 0 { + n := q.Len() + for i := 0; i < n; i++ { + node := q.Remove(q.Front()).(*Node) + for j := range node.Children { + q.PushBack(node.Children[j]) + } + } + depth++ + } + return depth +} +``` + +### JavaScript : + +104.二叉树的最大深度 + +```javascript +var maxdepth = function(root) { + if (root === null) return 0; + return 1 + Math.max(maxdepth(root.left), maxdepth(root.right)) +}; +``` + +二叉树最大深度递归遍历 +```javascript +var maxdepth = function(root) { + //使用递归的方法 递归三部曲 + //1. 确定递归函数的参数和返回值 + const getdepth = function(node) { + //2. 确定终止条件 + if(node === null) { + return 0; + } + //3. 确定单层逻辑 + let leftdepth = getdepth(node.left); + let rightdepth = getdepth(node.right); + let depth = 1 + Math.max(leftdepth, rightdepth); + return depth; + } + return getdepth(root); +}; +``` + +二叉树最大深度层级遍历 +```javascript +var maxDepth = function(root) { + if(!root) return 0 + let count = 0 + const queue = [root] + while(queue.length) { + let size = queue.length + /* 层数+1 */ + count++ + while(size--) { + let node = queue.shift(); + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + return count +}; +``` + +559.n叉树的最大深度 + +N叉树的最大深度 递归写法 +```js +var maxDepth = function(root) { + if(!root) return 0 + let depth = 0 + for(let node of root.children) { + depth = Math.max(depth, maxDepth(node)) + } + return depth + 1 +} +``` + +N叉树的最大深度 层序遍历 +```js +var maxDepth = function(root) { + if(!root) return 0 + let count = 0 + let queue = [root] + while(queue.length) { + let size = queue.length + count++ + while(size--) { + let node = queue.shift() + for (let item of node.children) { + item && queue.push(item); + } + } + } + return count +}; +``` + +### TypeScript: + +104.二叉树的最大深度 + +```typescript +// 后续遍历(自下而上) +function maxDepth(root: TreeNode | null): number { + if (root === null) return 0; + return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1; +}; + +// 前序遍历(自上而下) +function maxDepth(root: TreeNode | null): number { + function recur(node: TreeNode | null, count: number) { + if (node === null) { + resMax = resMax > count ? resMax : count; + return; + } + recur(node.left, count + 1); + recur(node.right, count + 1); + } + let resMax: number = 0; + let count: number = 0; + recur(root, count); + return resMax; +}; + +// 层序遍历(迭代法) +function maxDepth(root: TreeNode | null): number { + let helperQueue: TreeNode[] = []; + let resDepth: number = 0; + let tempNode: TreeNode; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + resDepth++; + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + if (tempNode.left) helperQueue.push(tempNode.left); + if (tempNode.right) helperQueue.push(tempNode.right); + } + } + return resDepth; +}; +``` + +559.n叉树的最大深度 + +```typescript +// 后续遍历(自下而上) +function maxDepth(root: Node | null): number { + if (root === null) return 0 + let depth = 0 + for (let i = 0; i < root.children.length; i++) { + depth = Math.max(depth, maxDepth(root.children[i])) + } + return depth + 1 +} + +// 前序遍历(自上而下) +function maxDepth(root: Node | null): number { + if (root === null) return 0 + + let depth: number = 0 + const queue: Array = [] + queue.push(root) + + while (queue.length > 0) { + let len = queue.length + depth++ + for (let i = 0; i < len; i++) { + // 当前层遍历 + let curNode: Node | null = queue.shift()! + for (let j = 0; j < curNode.children.length; j++) { + if (curNode.children[j]) queue.push(curNode.children[j]) + } + } + } + return depth +} + + +``` + +### C: + +104.二叉树的最大深度 + +二叉树最大深度递归 +```c +int maxDepth(struct TreeNode* root){ + //若传入结点为NULL,返回0 + if(!root) + return 0; + + //求出左子树深度 + int left = maxDepth(root->left); + //求出右子树深度 + int right = maxDepth(root->right); + //求出左子树深度和右子树深度的较大值 + int max = left > right ? left : right; + //返回较大值+1(1为当前层数) + return max + 1; +} +``` +二叉树最大深度迭代 +```c +int maxDepth(struct TreeNode* root){ + //若传入根节点为NULL,返回0 + if(!root) + return 0; + + int depth = 0; + //开辟队列空间 + struct TreeNode** queue = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 6000); + int queueFront = 0; + int queueEnd = 0; + + //将根结点入队 + queue[queueEnd++] = root; + + int queueSize; + //求出当前队列中元素个数 + while(queueSize = queueEnd - queueFront) { + int i; + //若当前队列中结点有左右子树,则将它们的左右子树入队 + for(i = 0; i < queueSize; i++) { + struct TreeNode* tempNode = queue[queueFront + i]; + if(tempNode->left) + queue[queueEnd++] = tempNode->left; + if(tempNode->right) + queue[queueEnd++] = tempNode->right; + } + //更新队头下标 + queueFront += queueSize; + //深度+1 + depth++; + } + return depth; +} +``` +二叉树最大深度迭代——后序遍历实现 +```c +int maxDepth(struct TreeNode *root) +{ + if(root == NULL) + return 0; + struct TreeNode *stack[10000] = {}; + int top = -1; + struct TreeNode *p = root, *r = NULL; // r指向上一个被访问的结点 + int depth = 0, maxDepth = -1; + while(p != NULL || top >= 0) + { + if(p != NULL) + { + stack[++top] = p; + depth++; + p = p->left; + } + else + { + p = stack[top]; + if(p->right != NULL && p->right != r) // 右子树未被访问 + p = p->right; + else + { + if(depth >= maxDepth) maxDepth = depth; + p = stack[top--]; + depth--; + r = p; + p = NULL; + } + } + } + return maxDepth; +} +``` +### Swift: + +104.二叉树的最大深度 + +```swift +// 递归 - 后序 +func maxDepth1(_ root: TreeNode?) -> Int { + return _maxDepth1(root) +} +func _maxDepth1(_ root: TreeNode?) -> Int { + if root == nil { + return 0 + } + let leftDepth = _maxDepth1(root!.left) + let rightDepth = _maxDepth1(root!.right) + return 1 + max(leftDepth, rightDepth) +} + +// 层序 +func maxDepth(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + var queue = [TreeNode]() + queue.append(root) + var res: Int = 0 + while !queue.isEmpty { + res += 1 + for _ in 0 ..< queue.count { + let node = queue.removeFirst() + if let left = node.left { + queue.append(left) + } + if let right = node.right { + queue.append(right) + } + } + } + return res +} +``` + +559.n叉树的最大深度 + +```swift +// 递归 +func maxDepth(_ root: Node?) -> Int { + guard let root = root else { + return 0 + } + var depth = 0 + for node in root.children { + depth = max(depth, maxDepth(node)) + } + return depth + 1 +} + +// 迭代-层序遍历 +func maxDepth1(_ root: Node?) -> Int { + guard let root = root else { + return 0 + } + var depth = 0 + var queue = [Node]() + queue.append(root) + while !queue.isEmpty { + let size = queue.count + depth += 1 + for _ in 0 ..< size { + let node = queue.removeFirst() + for child in node.children { + queue.append(child) + } + } + } + return depth +} +``` + +### Scala: + +104.二叉树的最大深度 + +递归法: +```scala +object Solution { + def maxDepth(root: TreeNode): Int = { + def process(curNode: TreeNode): Int = { + if (curNode == null) return 0 + // 递归左节点和右节点,返回最大的,最后+1 + math.max(process(curNode.left), process(curNode.right)) + 1 + } + // 调用递归方法,return关键字可以省略 + process(root) + } +} +``` + +迭代法: +```scala +object Solution { + import scala.collection.mutable + def maxDepth(root: TreeNode): Int = { + var depth = 0 + if (root == null) return depth + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + val len = queue.size + for (i <- 0 until len) { + val curNode = queue.dequeue() + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + depth += 1 // 只要有层次就+=1 + } + depth + } +} +``` + +559.n叉树的最大深度 + +递归法: +```scala +object Solution { + def maxDepth(root: Node): Int = { + if (root == null) return 0 + var depth = 0 + for (node <- root.children) { + depth = math.max(depth, maxDepth(node)) + } + depth + 1 + } +} +``` + +迭代法: (层序遍历) +```scala +object Solution { + import scala.collection.mutable + def maxDepth(root: Node): Int = { + if (root == null) return 0 + var depth = 0 + val queue = mutable.Queue[Node]() + queue.enqueue(root) + while (!queue.isEmpty) { + val len = queue.size + depth += 1 + for (i <- 0 until len) { + val curNode = queue.dequeue() + for (node <- curNode.children) queue.enqueue(node) + } + } + depth + } +} +``` + +### Rust: +0104.二叉树的最大深度 + +递归: +```rust +impl Solution { + pub fn max_depth(root: Option>>) -> i32 { + if root.is_none() { + return 0; + } + std::cmp::max( + Self::max_depth(root.clone().unwrap().borrow().left.clone()), + Self::max_depth(root.unwrap().borrow().right.clone()), + ) + 1 + } +} +``` + +迭代: +```rust +impl Solution { + pub fn max_depth(root: Option>>) -> i32 { + if root.is_none(){ + return 0; + } + let mut max_depth: i32 = 0; + let mut stack = vec![root.unwrap()]; + while !stack.is_empty() { + let num = stack.len(); + for _i in 0..num{ + let top = stack.remove(0); + if top.borrow_mut().left.is_some(){ + stack.push(top.borrow_mut().left.take().unwrap()); + } + if top.borrow_mut().right.is_some(){ + stack.push(top.borrow_mut().right.take().unwrap()); + } + } + max_depth+=1; + } + max_depth + } +``` +### C# + +0104.二叉树的最大深度 + +```csharp +// 递归法 +public int MaxDepth(TreeNode root) { + if(root == null) return 0; + + int leftDepth = MaxDepth(root.left); + int rightDepth = MaxDepth(root.right); + + return 1 + Math.Max(leftDepth, rightDepth); +} +``` +```csharp +// 前序遍历 +int result = 0; +public int MaxDepth(TreeNode root) +{ + if (root == null) return result; + GetDepth(root, 1); + return result; +} +public void GetDepth(TreeNode root, int depth) +{ + result = depth > result ? depth : result; + if (root.left == null && root.right == null) return; + + if (root.left != null) + GetDepth(root.left, depth + 1); + if (root.right != null) + GetDepth(root.right, depth + 1); + return; +} +``` +```csharp +// 迭代法 +public int MaxDepth(TreeNode root) +{ + int depth = 0; + Queue que = new(); + if (root == null) return depth; + que.Enqueue(root); + while (que.Count != 0) + { + int size = que.Count; + depth++; + for (int i = 0; i < size; i++) + { + var node = que.Dequeue(); + if (node.left != null) que.Enqueue(node.left); + if (node.right != null) que.Enqueue(node.right); + } + } + return depth; +} +``` + +559.n叉树的最大深度 +递归法 +```csharp + /* + 递归法 + */ + public class Solution { + public int MaxDepth(Node root) { + int res = 0; + /* 终止条件 */ + if(root == null){ + return 0; + } + + /* logic */ + // 遍历当前节点的子节点 + for (int i = 0; i < root.children.Count; i++) + { + res = Math.Max(res, MaxDepth(root.children[i])); + } + return res + 1; + } + } + // @lc code=end +``` + 迭代法(层序遍历) +```csharp + /* + 迭代法 + */ + public class Solution + { + public int MaxDepth(Node root) + { + Queue que = new Queue(); // 使用泛型队列存储节点 + + int res = 0; + + if(root != null){ + que.Enqueue(root); // 将根节点加入队列 + } + while (que.Count > 0) + { + int size = que.Count; // 获取当前层的节点数 + res++; // 深度加一 + + for (int i = 0; i < size; i++) + { + // 每一层的遍历 + + var curNode = que.Dequeue(); // 取出队列中的节点 + for (int j = 0; j < curNode.children.Count; j++) + { + if (curNode.children[j] != null) + { + que.Enqueue(curNode.children[j]); // 将子节点加入队列 + } + } + } + } + + return res; // 返回树的最大深度 + + } + } +``` + + + diff --git "a/problems/0106.\344\273\216\344\270\255\345\272\217\344\270\216\345\220\216\345\272\217\351\201\215\345\216\206\345\272\217\345\210\227\346\236\204\351\200\240\344\272\214\345\217\211\346\240\221.md" "b/problems/0106.\344\273\216\344\270\255\345\272\217\344\270\216\345\220\216\345\272\217\351\201\215\345\216\206\345\272\217\345\210\227\346\236\204\351\200\240\344\272\214\345\217\211\346\240\221.md" new file mode 100755 index 0000000000..5253325835 --- /dev/null +++ "b/problems/0106.\344\273\216\344\270\255\345\272\217\344\270\216\345\220\216\345\272\217\351\201\215\345\216\206\345\272\217\345\210\227\346\236\204\351\200\240\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,1351 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +看完本文,可以一起解决如下两道题目 + +* 106.从中序与后序遍历序列构造二叉树 +* 105.从前序与中序遍历序列构造二叉树 + +# 106.从中序与后序遍历序列构造二叉树 + +[力扣题目链接](https://leetcode.cn/problems/construct-binary-tree-from-inorder-and-postorder-traversal/) + +根据一棵树的中序遍历与后序遍历构造二叉树。 + +注意: +你可以假设树中没有重复的元素。 + +例如,给出 + +* 中序遍历 inorder = [9,3,15,20,7] +* 后序遍历 postorder = [9,15,7,20,3] + 返回如下的二叉树: + +![106. 从中序与后序遍历序列构造二叉树1](https://file1.kamacoder.com/i/algo/20210203154316774.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[坑很多!来看看你掉过几次坑 | LeetCode:106.从中序与后序遍历序列构造二叉树](https://www.bilibili.com/video/BV1vW4y1i7dn),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来再切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。 + +如果让我们肉眼看两个序列,画一棵二叉树的话,应该分分钟都可以画出来。 + +流程如图: + +![106.从中序与后序遍历序列构造二叉树](https://file1.kamacoder.com/i/algo/20210203154249860.png) + +那么代码应该怎么写呢? + +说到一层一层切割,就应该想到了递归。 + +来看一下一共分几步: + +* 第一步:如果数组大小为零的话,说明是空节点了。 + +* 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。 + +* 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点 + +* 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组) + +* 第五步:切割后序数组,切成后序左数组和后序右数组 + +* 第六步:递归处理左区间和右区间 + +不难写出如下代码:(先把框架写出来) + +```CPP +TreeNode* traversal (vector& inorder, vector& postorder) { + + // 第一步 + if (postorder.size() == 0) return NULL; + + // 第二步:后序遍历数组最后一个元素,就是当前的中间节点 + int rootValue = postorder[postorder.size() - 1]; + TreeNode* root = new TreeNode(rootValue); + + // 叶子节点 + if (postorder.size() == 1) return root; + + // 第三步:找切割点 + int delimiterIndex; + for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; + } + + // 第四步:切割中序数组,得到 中序左数组和中序右数组 + // 第五步:切割后序数组,得到 后序左数组和后序右数组 + + // 第六步 + root->left = traversal(中序左数组, 后序左数组); + root->right = traversal(中序右数组, 后序右数组); + + return root; +} +``` + +**难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。** + +此时应该注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。 + +**在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭右闭,必然乱套!** + +我在[数组:每次遇到二分法,都是一看就会,一写就废](https://programmercarl.com/0035.搜索插入位置.html)和[数组:这个循环可以转懵很多人!](https://programmercarl.com/0059.螺旋矩阵II.html)中都强调过循环不变量的重要性,在二分查找以及螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。 + + +首先要切割中序数组,为什么先切割中序数组呢? + +切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必要先切割中序数组。 + +中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割,如下代码中我坚持左闭右开的原则: + + +```CPP +// 找到中序遍历的切割点 +int delimiterIndex; +for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; +} + +// 左闭右开区间:[0, delimiterIndex) +vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); +// [delimiterIndex + 1, end) +vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); +``` + +接下来就要切割后序数组了。 + +首先后序数组的最后一个元素指定不能要了,这是切割点 也是 当前二叉树中间节点的元素,已经用了。 + +后序数组的切割点怎么找? + +后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。 + +**此时有一个很重的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。** + +中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。 + +代码如下: + +```CPP +// postorder 舍弃末尾元素,因为这个元素就是中间节点,已经用过了 +postorder.resize(postorder.size() - 1); + +// 左闭右开,注意这里使用了左中序数组大小作为切割点:[0, leftInorder.size) +vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); +// [leftInorder.size(), end) +vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); +``` + +此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。 + +接下来可以递归了,代码如下: + +```CPP +root->left = traversal(leftInorder, leftPostorder); +root->right = traversal(rightInorder, rightPostorder); +``` + +完整代码如下: + +```CPP +class Solution { +private: + TreeNode* traversal (vector& inorder, vector& postorder) { + if (postorder.size() == 0) return NULL; + + // 后序遍历数组最后一个元素,就是当前的中间节点 + int rootValue = postorder[postorder.size() - 1]; + TreeNode* root = new TreeNode(rootValue); + + // 叶子节点 + if (postorder.size() == 1) return root; + + // 找到中序遍历的切割点 + int delimiterIndex; + for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; + } + + // 切割中序数组 + // 左闭右开区间:[0, delimiterIndex) + vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); + // [delimiterIndex + 1, end) + vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); + + // postorder 舍弃末尾元素 + postorder.resize(postorder.size() - 1); + + // 切割后序数组 + // 依然左闭右开,注意这里使用了左中序数组大小作为切割点 + // [0, leftInorder.size) + vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); + // [leftInorder.size(), end) + vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); + + root->left = traversal(leftInorder, leftPostorder); + root->right = traversal(rightInorder, rightPostorder); + + return root; + } +public: + TreeNode* buildTree(vector& inorder, vector& postorder) { + if (inorder.size() == 0 || postorder.size() == 0) return NULL; + return traversal(inorder, postorder); + } +}; + +``` + +相信大家自己就算是思路清晰, 代码写出来一定是各种问题,所以一定要加日志来调试,看看是不是按照自己思路来切割的,不要大脑模拟,那样越想越糊涂。 + +加了日志的代码如下:(加了日志的代码不要在leetcode上提交,容易超时) + + +```CPP +class Solution { +private: + TreeNode* traversal (vector& inorder, vector& postorder) { + if (postorder.size() == 0) return NULL; + + int rootValue = postorder[postorder.size() - 1]; + TreeNode* root = new TreeNode(rootValue); + + if (postorder.size() == 1) return root; + + int delimiterIndex; + for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; + } + + vector leftInorder(inorder.begin(), inorder.begin() + delimiterIndex); + vector rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end() ); + + postorder.resize(postorder.size() - 1); + + vector leftPostorder(postorder.begin(), postorder.begin() + leftInorder.size()); + vector rightPostorder(postorder.begin() + leftInorder.size(), postorder.end()); + + // 以下为日志 + cout << "----------" << endl; + + cout << "leftInorder :"; + for (int i : leftInorder) { + cout << i << " "; + } + cout << endl; + + cout << "rightInorder :"; + for (int i : rightInorder) { + cout << i << " "; + } + cout << endl; + + cout << "leftPostorder :"; + for (int i : leftPostorder) { + cout << i << " "; + } + cout << endl; + cout << "rightPostorder :"; + for (int i : rightPostorder) { + cout << i << " "; + } + cout << endl; + + root->left = traversal(leftInorder, leftPostorder); + root->right = traversal(rightInorder, rightPostorder); + + return root; + } +public: + TreeNode* buildTree(vector& inorder, vector& postorder) { + if (inorder.size() == 0 || postorder.size() == 0) return NULL; + return traversal(inorder, postorder); + } +}; +``` + +**此时应该发现了,如上的代码性能并不好,因为每层递归定义了新的vector(就是数组),既耗时又耗空间,但上面的代码是最好理解的,为了方便读者理解,所以用如上的代码来讲解。** + +下面给出用下标索引写出的代码版本:(思路是一样的,只不过不用重复定义vector了,每次用下标索引来分割) + +```CPP +class Solution { +private: + // 中序区间:[inorderBegin, inorderEnd),后序区间[postorderBegin, postorderEnd) + TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& postorder, int postorderBegin, int postorderEnd) { + if (postorderBegin == postorderEnd) return NULL; + + int rootValue = postorder[postorderEnd - 1]; + TreeNode* root = new TreeNode(rootValue); + + if (postorderEnd - postorderBegin == 1) return root; + + int delimiterIndex; + for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; + } + // 切割中序数组 + // 左中序区间,左闭右开[leftInorderBegin, leftInorderEnd) + int leftInorderBegin = inorderBegin; + int leftInorderEnd = delimiterIndex; + // 右中序区间,左闭右开[rightInorderBegin, rightInorderEnd) + int rightInorderBegin = delimiterIndex + 1; + int rightInorderEnd = inorderEnd; + + // 切割后序数组 + // 左后序区间,左闭右开[leftPostorderBegin, leftPostorderEnd) + int leftPostorderBegin = postorderBegin; + int leftPostorderEnd = postorderBegin + delimiterIndex - inorderBegin; // 终止位置是 需要加上 中序区间的大小size + // 右后序区间,左闭右开[rightPostorderBegin, rightPostorderEnd) + int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin); + int rightPostorderEnd = postorderEnd - 1; // 排除最后一个元素,已经作为节点了 + + root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd); + root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd); + + return root; + } +public: + TreeNode* buildTree(vector& inorder, vector& postorder) { + if (inorder.size() == 0 || postorder.size() == 0) return NULL; + // 左闭右开的原则 + return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size()); + } +}; +``` + +那么这个版本写出来依然要打日志进行调试,打日志的版本如下:(**该版本不要在leetcode上提交,容易超时**) + +```CPP +class Solution { +private: + TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& postorder, int postorderBegin, int postorderEnd) { + if (postorderBegin == postorderEnd) return NULL; + + int rootValue = postorder[postorderEnd - 1]; + TreeNode* root = new TreeNode(rootValue); + + if (postorderEnd - postorderBegin == 1) return root; + + int delimiterIndex; + for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; + } + // 切割中序数组 + // 左中序区间,左闭右开[leftInorderBegin, leftInorderEnd) + int leftInorderBegin = inorderBegin; + int leftInorderEnd = delimiterIndex; + // 右中序区间,左闭右开[rightInorderBegin, rightInorderEnd) + int rightInorderBegin = delimiterIndex + 1; + int rightInorderEnd = inorderEnd; + + // 切割后序数组 + // 左后序区间,左闭右开[leftPostorderBegin, leftPostorderEnd) + int leftPostorderBegin = postorderBegin; + int leftPostorderEnd = postorderBegin + delimiterIndex - inorderBegin; // 终止位置是 需要加上 中序区间的大小size + // 右后序区间,左闭右开[rightPostorderBegin, rightPostorderEnd) + int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin); + int rightPostorderEnd = postorderEnd - 1; // 排除最后一个元素,已经作为节点了 + + cout << "----------" << endl; + cout << "leftInorder :"; + for (int i = leftInorderBegin; i < leftInorderEnd; i++) { + cout << inorder[i] << " "; + } + cout << endl; + + cout << "rightInorder :"; + for (int i = rightInorderBegin; i < rightInorderEnd; i++) { + cout << inorder[i] << " "; + } + cout << endl; + + cout << "leftpostorder :"; + for (int i = leftPostorderBegin; i < leftPostorderEnd; i++) { + cout << postorder[i] << " "; + } + cout << endl; + + cout << "rightpostorder :"; + for (int i = rightPostorderBegin; i < rightPostorderEnd; i++) { + cout << postorder[i] << " "; + } + cout << endl; + + root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd); + root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd); + + return root; + } +public: + TreeNode* buildTree(vector& inorder, vector& postorder) { + if (inorder.size() == 0 || postorder.size() == 0) return NULL; + return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size()); + } +}; +``` + +## 相关题目推荐 + +### 105.从前序与中序遍历序列构造二叉树 + +[力扣题目链接](https://leetcode.cn/problems/construct-binary-tree-from-preorder-and-inorder-traversal/) + +根据一棵树的前序遍历与中序遍历构造二叉树。 + +注意: +你可以假设树中没有重复的元素。 + +例如,给出 + +前序遍历 preorder = [3,9,20,15,7] +中序遍历 inorder = [9,3,15,20,7] +返回如下的二叉树: + +![105. 从前序与中序遍历序列构造二叉树](https://file1.kamacoder.com/i/algo/20210203154626672.png) + +### 思路 + +本题和106是一样的道理。 + +我就直接给出代码了。 + +带日志的版本C++代码如下: (**带日志的版本仅用于调试,不要在leetcode上提交,会超时**) + +```CPP +class Solution { +private: + TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& preorder, int preorderBegin, int preorderEnd) { + if (preorderBegin == preorderEnd) return NULL; + + int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0 + TreeNode* root = new TreeNode(rootValue); + + if (preorderEnd - preorderBegin == 1) return root; + + int delimiterIndex; + for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; + } + // 切割中序数组 + // 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd) + int leftInorderBegin = inorderBegin; + int leftInorderEnd = delimiterIndex; + // 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd) + int rightInorderBegin = delimiterIndex + 1; + int rightInorderEnd = inorderEnd; + + // 切割前序数组 + // 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd) + int leftPreorderBegin = preorderBegin + 1; + int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 终止位置是起始位置加上中序左区间的大小size + // 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd) + int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin); + int rightPreorderEnd = preorderEnd; + + cout << "----------" << endl; + cout << "leftInorder :"; + for (int i = leftInorderBegin; i < leftInorderEnd; i++) { + cout << inorder[i] << " "; + } + cout << endl; + + cout << "rightInorder :"; + for (int i = rightInorderBegin; i < rightInorderEnd; i++) { + cout << inorder[i] << " "; + } + cout << endl; + + cout << "leftPreorder :"; + for (int i = leftPreorderBegin; i < leftPreorderEnd; i++) { + cout << preorder[i] << " "; + } + cout << endl; + + cout << "rightPreorder :"; + for (int i = rightPreorderBegin; i < rightPreorderEnd; i++) { + cout << preorder[i] << " "; + } + cout << endl; + + + root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd); + root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd); + + return root; + } + +public: + TreeNode* buildTree(vector& preorder, vector& inorder) { + if (inorder.size() == 0 || preorder.size() == 0) return NULL; + return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size()); + + } +}; +``` + +105.从前序与中序遍历序列构造二叉树,最后版本,C++代码: + +```CPP +class Solution { +private: + TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& preorder, int preorderBegin, int preorderEnd) { + if (preorderBegin == preorderEnd) return NULL; + + int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0 + TreeNode* root = new TreeNode(rootValue); + + if (preorderEnd - preorderBegin == 1) return root; + + int delimiterIndex; + for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; + } + // 切割中序数组 + // 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd) + int leftInorderBegin = inorderBegin; + int leftInorderEnd = delimiterIndex; + // 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd) + int rightInorderBegin = delimiterIndex + 1; + int rightInorderEnd = inorderEnd; + + // 切割前序数组 + // 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd) + int leftPreorderBegin = preorderBegin + 1; + int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 终止位置是起始位置加上中序左区间的大小size + // 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd) + int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin); + int rightPreorderEnd = preorderEnd; + + root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd); + root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd); + + return root; + } + +public: + TreeNode* buildTree(vector& preorder, vector& inorder) { + if (inorder.size() == 0 || preorder.size() == 0) return NULL; + + // 参数坚持左闭右开的原则 + return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size()); + } +}; +``` + +## 思考题 + +前序和中序可以唯一确定一棵二叉树。 + +后序和中序可以唯一确定一棵二叉树。 + +那么前序和后序可不可以唯一确定一棵二叉树呢? + +**前序和后序不能唯一确定一棵二叉树!**,因为没有中序遍历无法确定左右部分,也就是无法分割。 + +举一个例子: + +![106.从中序与后序遍历序列构造二叉树2](https://file1.kamacoder.com/i/algo/20210203154720326.png) + +tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 + +tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 + +那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树! + +所以前序和后序不能唯一确定一棵二叉树! + +## 总结 + +之前我们讲的二叉树题目都是各种遍历二叉树,这次开始构造二叉树了,思路其实比较简单,但是真正代码实现出来并不容易。 + +所以要避免眼高手低,踏实地把代码写出来。 + +我同时给出了添加日志的代码版本,因为这种题目是不太容易写出来调一调就能过的,所以一定要把流程日志打出来,看看符不符合自己的思路。 + +大家遇到这种题目的时候,也要学会打日志来调试(如何打日志有时候也是个技术活),不要脑动模拟,脑动模拟很容易越想越乱。 + +最后我还给出了为什么前序和中序可以唯一确定一棵二叉树,后序和中序可以唯一确定一棵二叉树,而前序和后序却不行。 + +认真研究完本篇,相信大家对二叉树的构造会清晰很多。 + + + +## 其他语言版本 + +### Java + +106.从中序与后序遍历序列构造二叉树 + +```java +class Solution { + Map map; // 方便根据数值查找位置 + public TreeNode buildTree(int[] inorder, int[] postorder) { + map = new HashMap<>(); + for (int i = 0; i < inorder.length; i++) { // 用map保存中序序列的数值对应位置 + map.put(inorder[i], i); + } + + return findNode(inorder, 0, inorder.length, postorder,0, postorder.length); // 前闭后开 + } + + public TreeNode findNode(int[] inorder, int inBegin, int inEnd, int[] postorder, int postBegin, int postEnd) { + // 参数里的范围都是前闭后开 + if (inBegin >= inEnd || postBegin >= postEnd) { // 不满足左闭右开,说明没有元素,返回空树 + return null; + } + int rootIndex = map.get(postorder[postEnd - 1]); // 找到后序遍历的最后一个元素在中序遍历中的位置 + TreeNode root = new TreeNode(inorder[rootIndex]); // 构造结点 + int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定后序数列的个数 + root.left = findNode(inorder, inBegin, rootIndex, + postorder, postBegin, postBegin + lenOfLeft); + root.right = findNode(inorder, rootIndex + 1, inEnd, + postorder, postBegin + lenOfLeft, postEnd - 1); + + return root; + } +} +``` +```java +class Solution { + public TreeNode buildTree(int[] inorder, int[] postorder) { + if(postorder.length == 0 || inorder.length == 0) + return null; + return buildHelper(inorder, 0, inorder.length, postorder, 0, postorder.length); + + } + private TreeNode buildHelper(int[] inorder, int inorderStart, int inorderEnd, int[] postorder, int postorderStart, int postorderEnd){ + if(postorderStart == postorderEnd) + return null; + int rootVal = postorder[postorderEnd - 1]; + TreeNode root = new TreeNode(rootVal); + int middleIndex; + for (middleIndex = inorderStart; middleIndex < inorderEnd; middleIndex++){ + if(inorder[middleIndex] == rootVal) + break; + } + + int leftInorderStart = inorderStart; + int leftInorderEnd = middleIndex; + int rightInorderStart = middleIndex + 1; + int rightInorderEnd = inorderEnd; + + + int leftPostorderStart = postorderStart; + int leftPostorderEnd = postorderStart + (middleIndex - inorderStart); + int rightPostorderStart = leftPostorderEnd; + int rightPostorderEnd = postorderEnd - 1; + root.left = buildHelper(inorder, leftInorderStart, leftInorderEnd, postorder, leftPostorderStart, leftPostorderEnd); + root.right = buildHelper(inorder, rightInorderStart, rightInorderEnd, postorder, rightPostorderStart, rightPostorderEnd); + + return root; + } +} +``` +105.从前序与中序遍历序列构造二叉树 + +```java +class Solution { + Map map; + public TreeNode buildTree(int[] preorder, int[] inorder) { + map = new HashMap<>(); + for (int i = 0; i < inorder.length; i++) { // 用map保存中序序列的数值对应位置 + map.put(inorder[i], i); + } + + return findNode(preorder, 0, preorder.length, inorder, 0, inorder.length); // 前闭后开 + } + + public TreeNode findNode(int[] preorder, int preBegin, int preEnd, int[] inorder, int inBegin, int inEnd) { + // 参数里的范围都是前闭后开 + if (preBegin >= preEnd || inBegin >= inEnd) { // 不满足左闭右开,说明没有元素,返回空树 + return null; + } + int rootIndex = map.get(preorder[preBegin]); // 找到前序遍历的第一个元素在中序遍历中的位置 + TreeNode root = new TreeNode(inorder[rootIndex]); // 构造结点 + int lenOfLeft = rootIndex - inBegin; // 保存中序左子树个数,用来确定前序数列的个数 + root.left = findNode(preorder, preBegin + 1, preBegin + lenOfLeft + 1, + inorder, inBegin, rootIndex); + root.right = findNode(preorder, preBegin + lenOfLeft + 1, preEnd, + inorder, rootIndex + 1, inEnd); + + return root; + } +} +``` + +### Python + +105.从前序与中序遍历序列构造二叉树 + +```python +class Solution: + def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode: + # 第一步: 特殊情况讨论: 树为空. 或者说是递归终止条件 + if not preorder: + return None + + # 第二步: 前序遍历的第一个就是当前的中间节点. + root_val = preorder[0] + root = TreeNode(root_val) + + # 第三步: 找切割点. + separator_idx = inorder.index(root_val) + + # 第四步: 切割inorder数组. 得到inorder数组的左,右半边. + inorder_left = inorder[:separator_idx] + inorder_right = inorder[separator_idx + 1:] + + # 第五步: 切割preorder数组. 得到preorder数组的左,右半边. + # ⭐️ 重点1: 中序数组大小一定跟前序数组大小是相同的. + preorder_left = preorder[1:1 + len(inorder_left)] + preorder_right = preorder[1 + len(inorder_left):] + + # 第六步: 递归 + root.left = self.buildTree(preorder_left, inorder_left) + root.right = self.buildTree(preorder_right, inorder_right) + # 第七步: 返回答案 + return root +``` + +106.从中序与后序遍历序列构造二叉树 + +```python +class Solution: + def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode: + # 第一步: 特殊情况讨论: 树为空. (递归终止条件) + if not postorder: + return None + + # 第二步: 后序遍历的最后一个就是当前的中间节点. + root_val = postorder[-1] + root = TreeNode(root_val) + + # 第三步: 找切割点. + separator_idx = inorder.index(root_val) + + # 第四步: 切割inorder数组. 得到inorder数组的左,右半边. + inorder_left = inorder[:separator_idx] + inorder_right = inorder[separator_idx + 1:] + + # 第五步: 切割postorder数组. 得到postorder数组的左,右半边. + # ⭐️ 重点1: 中序数组大小一定跟后序数组大小是相同的. + postorder_left = postorder[:len(inorder_left)] + postorder_right = postorder[len(inorder_left): len(postorder) - 1] + + # 第六步: 递归 + root.left = self.buildTree(inorder_left, postorder_left) + root.right = self.buildTree(inorder_right, postorder_right) + # 第七步: 返回答案 + return root +``` + +### Go + +106 从中序与后序遍历序列构造二叉树 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var ( + hash map[int]int +) +func buildTree(inorder []int, postorder []int) *TreeNode { + hash = make(map[int]int) + for i, v := range inorder { // 用map保存中序序列的数值对应位置 + hash[v] = i + } + // 以左闭右闭的原则进行切分 + root := rebuild(inorder, postorder, len(postorder)-1, 0, len(inorder)-1) + return root +} +// rootIdx表示根节点在后序数组中的索引,l, r 表示在中序数组中的前后切分点 +func rebuild(inorder []int, postorder []int, rootIdx int, l, r int) *TreeNode { + if l > r { // 说明没有元素,返回空树 + return nil + } + if l == r { // 只剩唯一一个元素,直接返回 + return &TreeNode{Val : inorder[l]} + } + rootV := postorder[rootIdx] // 根据后序数组找到根节点的值 + rootIn := hash[rootV] // 找到根节点在对应的中序数组中的位置 + root := &TreeNode{Val : rootV} // 构造根节点 + // 重建左节点和右节点 + root.Left = rebuild(inorder, postorder, rootIdx-(r-rootIn)-1, l, rootIn-1) + root.Right = rebuild(inorder, postorder, rootIdx-1, rootIn+1, r) + return root +} +``` + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func buildTree(inorder []int, postorder []int) *TreeNode { + if len(postorder) == 0 { + return nil + } + + // 后序遍历数组最后一个元素,就是当前的中间节点 + rootValue := postorder[len(postorder)-1] + root := &TreeNode{Val:rootValue} + + // 叶子结点 + if len(postorder) == 1 { + return root + } + + // 找到中序遍历的切割点 + var delimiterIndex int + for delimiterIndex = 0; delimiterIndex < len(inorder); delimiterIndex++ { + if inorder[delimiterIndex] == rootValue { + break; + } + } + + // 切割中序数组 + // 左闭右开区间:[0, delimiterIndex) + leftInorder := inorder[:delimiterIndex] + // [delimiterIndex + 1, end) + rightInorder := inorder[delimiterIndex+1:] + + // postorder 舍弃末尾元素 + postorder = postorder[:len(postorder)-1] + + // 切割后序数组 + // 依然左闭右开,注意这里使用了左中序数组大小作为切割点 + // [0, len(leftInorder)) + leftPostorder := postorder[:len(leftInorder)] + // [len(leftInorder), end) + rightPostorder := postorder[len(leftInorder):] + + root.Left = buildTree(leftInorder, leftPostorder) + root.Right = buildTree(rightInorder, rightPostorder) + + return root +} +``` + +105 从前序与中序遍历序列构造二叉树 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var ( + hash map[int]int +) +func buildTree(preorder []int, inorder []int) *TreeNode { + hash = make(map[int]int, len(inorder)) + for i, v := range inorder { + hash[v] = i + } + root := build(preorder, inorder, 0, 0, len(inorder)-1) // l, r 表示构造的树在中序遍历数组中的范围 + return root +} +func build(pre []int, in []int, root int, l, r int) *TreeNode { + if l > r { + return nil + } + rootVal := pre[root] // 找到本次构造的树的根节点 + index := hash[rootVal] // 根节点在中序数组中的位置 + node := &TreeNode {Val: rootVal} + node.Left = build(pre, in, root + 1, l, index-1) + node.Right = build(pre, in, root + (index-l) + 1, index+1, r) + return node +} +``` + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func buildTree(preorder []int, inorder []int) *TreeNode { + if len(preorder) == 0 { + return nil + } + + // 前序遍历数组第一个元素,就是当前的中间节点 + rootValue := preorder[0] + root := &TreeNode{Val:rootValue} + + // 叶子结点 + if len(preorder) == 1 { + return root + } + + // 找到中序遍历的切割点 + var delimiterIndex int + for delimiterIndex = 0; delimiterIndex < len(inorder); delimiterIndex++ { + if inorder[delimiterIndex] == rootValue { + break + } + } + + // 切割中序数组 + // 左闭右开区间:[0, delimiterIndex) + leftInorder := inorder[:delimiterIndex] + // [delimiterIndex + 1, end) + rightInorder := inorder[delimiterIndex+1:] + + // preorder 舍弃首位元素 + preorder = preorder[1:] + + // 切割前序数组 + // 依然左闭右开,注意这里使用了左中序数组大小作为切割点 + // [0, len(leftInorder)) + leftPreorder := preorder[:len(leftInorder)] + // [len(leftInorder), end) + rightPreorder := preorder[len(leftInorder):] + + root.Left = buildTree(leftPreorder, leftInorder) + root.Right = buildTree(rightPreorder, rightInorder) + + return root +} +``` + + +### JavaScript + +```javascript +var buildTree = function(inorder, postorder) { + if (!inorder.length) return null; + const rootVal = postorder.pop(); // 从后序遍历的数组中获取中间节点的值, 即数组最后一个值 + let rootIndex = inorder.indexOf(rootVal); // 获取中间节点在中序遍历中的下标 + const root = new TreeNode(rootVal); // 创建中间节点 + root.left = buildTree(inorder.slice(0, rootIndex), postorder.slice(0, rootIndex)); // 创建左节点 + root.right = buildTree(inorder.slice(rootIndex + 1), postorder.slice(rootIndex)); // 创建右节点 + return root; +}; +``` + +从前序与中序遍历序列构造二叉树 + +```javascript +var buildTree = function(preorder, inorder) { + if (!preorder.length) return null; + const rootVal = preorder.shift(); // 从前序遍历的数组中获取中间节点的值, 即数组第一个值 + const index = inorder.indexOf(rootVal); // 获取中间节点在中序遍历中的下标 + const root = new TreeNode(rootVal); // 创建中间节点 + root.left = buildTree(preorder.slice(0, index), inorder.slice(0, index)); // 创建左节点 + root.right = buildTree(preorder.slice(index), inorder.slice(index + 1)); // 创建右节点 + return root; +}; +``` + +### TypeScript + +> 106.从中序与后序遍历序列构造二叉树 + +**创建新数组:** + +```typescript +function buildTree(inorder: number[], postorder: number[]): TreeNode | null { + if (postorder.length === 0) return null; + const rootVal: number = postorder.pop()!; + const rootValIndex: number = inorder.indexOf(rootVal); + const rootNode: TreeNode = new TreeNode(rootVal); + rootNode.left = buildTree(inorder.slice(0, rootValIndex), postorder.slice(0, rootValIndex)); + rootNode.right = buildTree(inorder.slice(rootValIndex + 1), postorder.slice(rootValIndex)); + return rootNode; +}; +``` + +**使用数组索引:** + +```typescript +function buildTree(inorder: number[], postorder: number[]): TreeNode | null { + function recur( + inorder: number[], postorder: number[], + inBegin: number, inEnd: number, + postBegin: number, postEnd: number + ): TreeNode | null { + if (postBegin === postEnd) return null; + const rootVal: number = postorder[postEnd - 1]!; + const rootValIndex: number = inorder.indexOf(rootVal, inBegin); + const rootNode: TreeNode = new TreeNode(rootVal); + + const leftInorderBegin: number = inBegin; + const leftInorderEnd: number = rootValIndex; + const rightInorderBegin: number = rootValIndex + 1; + const rightInorderEnd: number = inEnd; + + const leftPostorderBegin: number = postBegin; + const leftPostorderEnd: number = postBegin + rootValIndex - inBegin; + const rightPostorderBegin: number = leftPostorderEnd; + const rightPostorderEnd: number = postEnd - 1; + + rootNode.left = recur( + inorder, postorder, + leftInorderBegin, leftInorderEnd, + leftPostorderBegin, leftPostorderEnd + ); + rootNode.right = recur( + inorder, postorder, + rightInorderBegin, rightInorderEnd, + rightPostorderBegin, rightPostorderEnd + ); + return rootNode; + } + return recur(inorder, postorder, 0, inorder.length, 0, inorder.length); +}; +``` + +> 105.从前序与中序遍历序列构造二叉树 + +**新建数组:** + +```typescript +function buildTree(preorder: number[], inorder: number[]): TreeNode | null { + if (preorder.length === 0) return null; + const rootVal: number = preorder[0]; + const rootNode: TreeNode = new TreeNode(rootVal); + const rootValIndex: number = inorder.indexOf(rootVal); + rootNode.left = buildTree(preorder.slice(1, rootValIndex + 1), inorder.slice(0, rootValIndex)); + rootNode.right = buildTree(preorder.slice(rootValIndex + 1), inorder.slice(rootValIndex + 1)); + return rootNode; +}; +``` + +**使用数组索引:** + +```typescript +function buildTree(preorder: number[], inorder: number[]): TreeNode | null { + function recur( + preorder: number[], inorder: number[], + preBegin: number, preEnd: number, + inBegin: number, inEnd: number + ): TreeNode | null { + if (preBegin === preEnd) return null; + const rootVal: number = preorder[preBegin]; + const rootNode: TreeNode = new TreeNode(rootVal); + const rootValIndex: number = inorder.indexOf(rootVal, inBegin); + + const leftPreBegin: number = preBegin + 1; + const leftPreEnd: number = preBegin + rootValIndex - inBegin + 1; + const leftInBegin: number = inBegin; + const leftInEnd: number = rootValIndex; + + const rightPreBegin: number = leftPreEnd; + const rightPreEnd: number = preEnd; + const rightInBegin: number = rootValIndex + 1; + const rightInEnd: number = inEnd; + + rootNode.left = recur(preorder, inorder, leftPreBegin, leftPreEnd, leftInBegin, leftInEnd); + rootNode.right = recur(preorder, inorder, rightPreBegin, rightPreEnd, rightInBegin, rightInEnd); + return rootNode; + }; + return recur(preorder, inorder, 0, preorder.length, 0, inorder.length); +}; +``` + +### C + +106 从中序与后序遍历序列构造二叉树 + +```c +int linearSearch(int* arr, int arrSize, int key) { + int i; + for(i = 0; i < arrSize; i++) { + if(arr[i] == key) + return i; + } + return -1; +} + +struct TreeNode* buildTree(int* inorder, int inorderSize, int* postorder, int postorderSize){ + //若中序遍历数组中没有元素,则返回NULL + if(!inorderSize) + return NULL; + //创建一个新的结点,将node的val设置为后序遍历的最后一个元素 + struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); + node->val = postorder[postorderSize - 1]; + + //通过线性查找找到中间结点在中序数组中的位置 + int index = linearSearch(inorder, inorderSize, postorder[postorderSize - 1]); + + //左子树数组大小为index + //右子树的数组大小为数组大小减index减1(减的1为中间结点) + int rightSize = inorderSize - index - 1; + node->left = buildTree(inorder, index, postorder, index); + node->right = buildTree(inorder + index + 1, rightSize, postorder + index, rightSize); + return node; +} +``` + +105 从前序与中序遍历序列构造二叉树 + +```c +struct TreeNode* buildTree(int* preorder, int preorderSize, int* inorder, int inorderSize){ + // 递归结束条件:传入的数组大小为0 + if(!preorderSize) + return NULL; + + // 1.找到前序遍历数组的第一个元素, 创建结点。左右孩子设置为NULL。 + int rootValue = preorder[0]; + struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode)); + root->val = rootValue; + root->left = NULL; + root->right = NULL; + + // 2.若前序遍历数组的大小为1,返回该结点 + if(preorderSize == 1) + return root; + + // 3.根据该结点切割中序遍历数组,将中序遍历数组分割成左右两个数组。算出他们的各自大小 + int index; + for(index = 0; index < inorderSize; index++) { + if(inorder[index] == rootValue) + break; + } + int leftNum = index; + int rightNum = inorderSize - index - 1; + + int* leftInorder = inorder; + int* rightInorder = inorder + leftNum + 1; + + // 4.根据中序遍历数组左右数组的各子大小切割前序遍历数组。也分为左右数组 + int* leftPreorder = preorder+1; + int* rightPreorder = preorder + 1 + leftNum; + + // 5.递归进入左右数组,将返回的结果作为根结点的左右孩子 + root->left = buildTree(leftPreorder, leftNum, leftInorder, leftNum); + root->right = buildTree(rightPreorder, rightNum, rightInorder, rightNum); + + // 6.返回根节点 + return root; +} +``` + +### Swift + +105 从前序与中序遍历序列构造二叉树 + +```swift +class Solution { + func buildTree(_ preorder: [Int], _ inorder: [Int]) -> TreeNode? { + return helper(preorder: preorder, + preorderBegin: 0, + preorderEnd: preorder.count, + inorder: inorder, + inorderBegin: 0, + inorderEnd: inorder.count) + } + + func helper(preorder: [Int], preorderBegin: Int, preorderEnd: Int, inorder: [Int], inorderBegin: Int, inorderEnd: Int) -> TreeNode? { + if preorderBegin == preorderEnd { + return nil + } + + // 前序遍历数组的第一个元素作为分割点 + let rootValue = preorder[preorderBegin] + let root = TreeNode(rootValue) + + + if preorderEnd - preorderBegin == 1 { + return root + } + + var index = 0 // 从中序遍历数组中找到根节点的下标 + if let ind = inorder.firstIndex(of: rootValue) { + index = ind + } + + // 递归 + root.left = helper(preorder: preorder, + preorderBegin: preorderBegin + 1, + preorderEnd: preorderBegin + 1 + index - inorderBegin, + inorder: inorder, + inorderBegin: inorderBegin, + inorderEnd: index) + root.right = helper(preorder: preorder, + preorderBegin: preorderBegin + 1 + index - inorderBegin, + preorderEnd: preorderEnd, + inorder: inorder, + inorderBegin: index + 1, + inorderEnd: inorderEnd) + return root + } +} +``` + +106 从中序与后序遍历序列构造二叉树 + +```swift +class Solution_0106 { + func buildTree(inorder: [Int], inorderBegin: Int, inorderEnd: Int, postorder: [Int], postorderBegin: Int, postorderEnd: Int) -> TreeNode? { + if postorderEnd - postorderBegin < 1 { + return nil + } + + // 后序遍历数组的最后一个元素作为分割点 + let rootValue = postorder[postorderEnd - 1] + let root = TreeNode(rootValue) + + if postorderEnd - postorderBegin == 1 { + return root + } + + // 从中序遍历数组中找到根节点的下标 + var delimiterIndex = 0 + if let index = inorder.firstIndex(of: rootValue) { + delimiterIndex = index + } + + root.left = buildTree(inorder: inorder, + inorderBegin: inorderBegin, + inorderEnd: delimiterIndex, + postorder: postorder, + postorderBegin: postorderBegin, + postorderEnd: postorderBegin + (delimiterIndex - inorderBegin)) + + root.right = buildTree(inorder: inorder, + inorderBegin: delimiterIndex + 1, + inorderEnd: inorderEnd, + postorder: postorder, + postorderBegin: postorderBegin + (delimiterIndex - inorderBegin), + postorderEnd: postorderEnd - 1) + return root + } +} +``` + +### Scala + +106 从中序与后序遍历序列构造二叉树 + +```scala +object Solution { + def buildTree(inorder: Array[Int], postorder: Array[Int]): TreeNode = { + // 1、如果长度为0,则直接返回null + var len = inorder.size + if (len == 0) return null + // 2、后序数组的最后一个元素是当前根元素 + var rootValue = postorder(len - 1) + var root: TreeNode = new TreeNode(rootValue, null, null) + if (len == 1) return root // 如果数组只有一个节点,就直接返回 + // 3、在中序数组中找到切割点的索引 + var delimiterIndex: Int = inorder.indexOf(rootValue) + // 4、切分数组往下迭代 + root.left = buildTree(inorder.slice(0, delimiterIndex), postorder.slice(0, delimiterIndex)) + root.right = buildTree(inorder.slice(delimiterIndex + 1, len), postorder.slice(delimiterIndex, len - 1)) + root // 返回root,return关键字可以省略 + } +} +``` + +105 从前序与中序遍历序列构造二叉树 + +```scala +object Solution { + def buildTree(preorder: Array[Int], inorder: Array[Int]): TreeNode = { + // 1、如果长度为0,直接返回空 + var len = inorder.size + if (len == 0) return null + // 2、前序数组的第一个元素是当前子树根节点 + var rootValue = preorder(0) + var root = new TreeNode(rootValue, null, null) + if (len == 1) return root // 如果数组元素只有一个,那么返回根节点 + // 3、在中序数组中,找到切割点 + var delimiterIndex = inorder.indexOf(rootValue) + + // 4、切分数组往下迭代 + root.left = buildTree(preorder.slice(1, delimiterIndex + 1), inorder.slice(0, delimiterIndex)) + root.right = buildTree(preorder.slice(delimiterIndex + 1, preorder.size), inorder.slice(delimiterIndex + 1, len)) + + root + } +} +``` + +### Rust + +106 从中序与后序遍历序列构造二叉树 + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn build_tree(inorder: Vec, postorder: Vec) -> Option>> { + if inorder.is_empty() { + return None; + } + let mut postorder = postorder; + let root = postorder.pop().unwrap(); + let index = inorder.iter().position(|&x| x == root).unwrap(); + let mut root = TreeNode::new(root); + root.left = Self::build_tree(inorder[..index].to_vec(), postorder[..index].to_vec()); + root.right = Self::build_tree(inorder[index + 1..].to_vec(), postorder[index..].to_vec()); + Some(Rc::new(RefCell::new(root))) + } +} +``` + +105 从前序与中序遍历序列构造二叉树 + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn build_tree(preorder: Vec, inorder: Vec) -> Option>> { + if preorder.is_empty() { + return None; + } + let root = preorder[0]; + let index = inorder.iter().position(|&x| x == root).unwrap(); + let mut root = TreeNode::new(root); + root.left = Self::build_tree(preorder[1..index + 1].to_vec(), inorder[0..index].to_vec()); + root.right = Self::build_tree( + preorder[index + 1..].to_vec(), + inorder[index + 1..].to_vec(), + ); + Some(Rc::new(RefCell::new(root))) + } +} +``` +### C# +```csharp +public TreeNode BuildTree(int[] inorder, int[] postorder) +{ + if (inorder.Length == 0 || postorder.Length == null) return null; + int rootValue = postorder.Last(); + TreeNode root = new TreeNode(rootValue); + int delimiterIndex = Array.IndexOf(inorder, rootValue); + root.left = BuildTree(inorder.Take(delimiterIndex).ToArray(), postorder.Take(delimiterIndex).ToArray()); + root.right = BuildTree(inorder.Skip(delimiterIndex + 1).ToArray(), postorder.Skip(delimiterIndex).Take(inorder.Length - delimiterIndex - 1).ToArray()); + return root; +} +``` + + diff --git "a/problems/0108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" new file mode 100755 index 0000000000..2df1c2615b --- /dev/null +++ "b/problems/0108.\345\260\206\346\234\211\345\272\217\346\225\260\347\273\204\350\275\254\346\215\242\344\270\272\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,562 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 构造二叉搜索树,一不小心就平衡了 + +# 108.将有序数组转换为二叉搜索树 + +[力扣题目链接](https://leetcode.cn/problems/convert-sorted-array-to-binary-search-tree/) + +将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。 + +本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。 + +示例: + + +![108.将有序数组转换为二叉搜索树](https://file1.kamacoder.com/i/algo/20201022164420763.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[构造平衡二叉搜索树!| LeetCode:108.将有序数组转换为二叉搜索树](https://www.bilibili.com/video/BV1uR4y1X7qL?share_source=copy_web),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +做这道题目之前大家可以了解一下这几道: + +* [106.从中序与后序遍历序列构造二叉树](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) +* [654.最大二叉树](https://programmercarl.com/0654.最大二叉树.html)中其实已经讲过了,如果根据数组构造一棵二叉树。 +* [701.二叉搜索树中的插入操作](https://programmercarl.com/0701.二叉搜索树中的插入操作.html) +* [450.删除二叉搜索树中的节点](https://programmercarl.com/0450.删除二叉搜索树中的节点.html) + + +进入正题: + +题目中说要转换为一棵高度平衡二叉搜索树。为什么强调要平衡呢? + +因为只要给我们一个有序数组,如果不强调平衡,都可以以线性结构来构造二叉搜索树。 + +例如 有序数组[-10,-3,0,5,9] 就可以构造成这样的二叉搜索树,如图。 + +![](https://file1.kamacoder.com/i/algo/20220930173553.png) + +上图中,是符合二叉搜索树的特性吧,如果要这么做的话,是不是本题意义就不大了,所以才强调是平衡二叉搜索树。 + +其实数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取。**所以想构成不平衡的二叉树是自找麻烦**。 + + +在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)和[二叉树:构造一棵最大的二叉树](https://programmercarl.com/0654.最大二叉树.html)中其实已经讲过了,如果根据数组构造一棵二叉树。 + +**本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间**。 + +本题其实要比[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) 和 [二叉树:构造一棵最大的二叉树](https://programmercarl.com/0654.最大二叉树.html)简单一些,因为有序数组构造二叉搜索树,寻找分割点就比较容易了。 + +分割点就是数组中间位置的节点。 + +那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个? + +取哪一个都可以,只不过构成了不同的平衡二叉搜索树。 + +例如:输入:[-10,-3,0,5,9] + +如下两棵树,都是这个数组的平衡二叉搜索树: + +![108.将有序数组转换为二叉搜索树](https://file1.kamacoder.com/i/algo/108.将有序数组转换为二叉搜索树.png) + +如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。 + +**这也是题目中强调答案不是唯一的原因。 理解这一点,这道题目算是理解到位了**。 + +### 递归 + +递归三部曲: + +* 确定递归函数返回值及其参数 + +删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。 + +相信大家如果仔细看了[二叉树:搜索树中的插入操作](https://programmercarl.com/0701.二叉搜索树中的插入操作.html)和[二叉树:搜索树中的删除操作](https://programmercarl.com/0450.删除二叉搜索树中的节点.html),一定会对递归函数返回值的作用深有感触。 + +那么本题要构造二叉树,依然用递归函数的返回值来构造中节点的左右孩子。 + +再来看参数,首先是传入数组,然后就是左下标left和右下标right,我们在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中提过,在构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。 + +所以代码如下: + +``` +// 左闭右闭区间[left, right] +TreeNode* traversal(vector& nums, int left, int right) +``` + +这里注意,**我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到我们讲过的循环不变量**。 + +在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html),[35.搜索插入位置](https://programmercarl.com/0035.搜索插入位置.html) 和[59.螺旋矩阵II](https://programmercarl.com/0059.螺旋矩阵II.html)都详细讲过循环不变量。 + + +* 确定递归终止条件 + +这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。 + +代码如下: + +``` +if (left > right) return nullptr; +``` + +* 确定单层递归的逻辑 + +首先取数组中间元素的位置,不难写出`int mid = (left + right) / 2;`,**这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在[二分法](https://programmercarl.com/0035.搜索插入位置.html)中尤其需要注意!** + +所以可以这么写:`int mid = left + ((right - left) / 2);` + +但本题leetcode的测试数据并不会越界,所以怎么写都可以。但需要有这个意识! + +取了中间位置,就开始以中间位置的元素构造节点,代码:`TreeNode* root = new TreeNode(nums[mid]);`。 + +接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。 + +最后返回root节点,单层递归整体代码如下: + +``` +int mid = left + ((right - left) / 2); +TreeNode* root = new TreeNode(nums[mid]); +root->left = traversal(nums, left, mid - 1); +root->right = traversal(nums, mid + 1, right); +return root; +``` + +这里`int mid = left + ((right - left) / 2);`的写法相当于是如果数组长度为偶数,中间位置有两个元素,取靠左边的。 + +* 递归整体代码如下: + +```CPP +class Solution { +private: + TreeNode* traversal(vector& nums, int left, int right) { + if (left > right) return nullptr; + int mid = left + ((right - left) / 2); + TreeNode* root = new TreeNode(nums[mid]); + root->left = traversal(nums, left, mid - 1); + root->right = traversal(nums, mid + 1, right); + return root; + } +public: + TreeNode* sortedArrayToBST(vector& nums) { + TreeNode* root = traversal(nums, 0, nums.size() - 1); + return root; + } +}; +``` + +**注意:在调用traversal的时候传入的left和right为什么是0和nums.size() - 1,因为定义的区间为左闭右闭**。 + + +### 迭代法 + +迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下标,一个队列放右区间下标。 + +模拟的就是不断分割的过程,C++代码如下:(我已经详细注释) + +```CPP +class Solution { +public: + TreeNode* sortedArrayToBST(vector& nums) { + if (nums.size() == 0) return nullptr; + + TreeNode* root = new TreeNode(0); // 初始根节点 + queue nodeQue; // 放遍历的节点 + queue leftQue; // 保存左区间下标 + queue rightQue; // 保存右区间下标 + nodeQue.push(root); // 根节点入队列 + leftQue.push(0); // 0为左区间下标初始位置 + rightQue.push(nums.size() - 1); // nums.size() - 1为右区间下标初始位置 + + while (!nodeQue.empty()) { + TreeNode* curNode = nodeQue.front(); + nodeQue.pop(); + int left = leftQue.front(); leftQue.pop(); + int right = rightQue.front(); rightQue.pop(); + int mid = left + ((right - left) / 2); + + curNode->val = nums[mid]; // 将mid对应的元素给中间节点 + + if (left <= mid - 1) { // 处理左区间 + curNode->left = new TreeNode(0); + nodeQue.push(curNode->left); + leftQue.push(left); + rightQue.push(mid - 1); + } + + if (right >= mid + 1) { // 处理右区间 + curNode->right = new TreeNode(0); + nodeQue.push(curNode->right); + leftQue.push(mid + 1); + rightQue.push(right); + } + } + return root; + } +}; +``` + +## 总结 + +**在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) 和 [二叉树:构造一棵最大的二叉树](https://programmercarl.com/0654.最大二叉树.html)之后,我们顺理成章的应该构造一下二叉搜索树了,一不小心还是一棵平衡二叉搜索树**。 + +其实思路也是一样的,不断中间分割,然后递归处理左区间,右区间,也可以说是分治。 + +此时相信大家应该对通过递归函数的返回值来增删二叉树很熟悉了,这也是常规操作。 + +在定义区间的过程中我们又一次强调了循环不变量的重要性。 + +最后依然给出迭代的方法,其实就是模拟取中间元素,然后不断分割去构造二叉树的过程。 + + +## 其他语言版本 + + +### Java + +递归: 左闭右开 [left,right) +```Java +class Solution { + public TreeNode sortedArrayToBST(int[] nums) { + return sortedArrayToBST(nums, 0, nums.length); + } + + public TreeNode sortedArrayToBST(int[] nums, int left, int right) { + if (left >= right) { + return null; + } + if (right - left == 1) { + return new TreeNode(nums[left]); + } + int mid = left + (right - left) / 2; + TreeNode root = new TreeNode(nums[mid]); + root.left = sortedArrayToBST(nums, left, mid); + root.right = sortedArrayToBST(nums, mid + 1, right); + return root; + } +} + +``` + +递归: 左闭右闭 [left,right] +```java +class Solution { + public TreeNode sortedArrayToBST(int[] nums) { + TreeNode root = traversal(nums, 0, nums.length - 1); + return root; + } + + // 左闭右闭区间[left, right] + private TreeNode traversal(int[] nums, int left, int right) { + if (left > right) return null; + + int mid = left + ((right - left) >> 1); + TreeNode root = new TreeNode(nums[mid]); + root.left = traversal(nums, left, mid - 1); + root.right = traversal(nums, mid + 1, right); + return root; + } +} +``` + +迭代: 左闭右闭 [left,right] +```java +class Solution { + public TreeNode sortedArrayToBST(int[] nums) { + if (nums.length == 0) return null; + + //根节点初始化 + TreeNode root = new TreeNode(-1); + Queue nodeQueue = new LinkedList<>(); + Queue leftQueue = new LinkedList<>(); + Queue rightQueue = new LinkedList<>(); + + // 根节点入队列 + nodeQueue.offer(root); + // 0为左区间下标初始位置 + leftQueue.offer(0); + // nums.size() - 1为右区间下标初始位置 + rightQueue.offer(nums.length - 1); + + while (!nodeQueue.isEmpty()) { + TreeNode currNode = nodeQueue.poll(); + int left = leftQueue.poll(); + int right = rightQueue.poll(); + int mid = left + ((right - left) >> 1); + + // 将mid对应的元素给中间节点 + currNode.val = nums[mid]; + + // 处理左区间 + if (left <= mid - 1) { + currNode.left = new TreeNode(-1); + nodeQueue.offer(currNode.left); + leftQueue.offer(left); + rightQueue.offer(mid - 1); + } + + // 处理右区间 + if (right >= mid + 1) { + currNode.right = new TreeNode(-1); + nodeQueue.offer(currNode.right); + leftQueue.offer(mid + 1); + rightQueue.offer(right); + } + } + return root; + } +} +``` + +### Python +递归法 +```python +class Solution: + def traversal(self, nums: List[int], left: int, right: int) -> TreeNode: + if left > right: + return None + + mid = left + (right - left) // 2 + root = TreeNode(nums[mid]) + root.left = self.traversal(nums, left, mid - 1) + root.right = self.traversal(nums, mid + 1, right) + return root + + def sortedArrayToBST(self, nums: List[int]) -> TreeNode: + root = self.traversal(nums, 0, len(nums) - 1) + return root + +``` +递归 精简(自身调用) +```python +class Solution: + def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]: + if not nums: + return + mid = len(nums) // 2 + root = TreeNode(nums[mid]) + root.left = self.sortedArrayToBST(nums[:mid]) + root.right = self.sortedArrayToBST(nums[mid + 1 :]) + return root +``` + +迭代法 +```python +from collections import deque + +class Solution: + def sortedArrayToBST(self, nums: List[int]) -> TreeNode: + if len(nums) == 0: + return None + + root = TreeNode(0) # 初始根节点 + nodeQue = deque() # 放遍历的节点 + leftQue = deque() # 保存左区间下标 + rightQue = deque() # 保存右区间下标 + + nodeQue.append(root) # 根节点入队列 + leftQue.append(0) # 0为左区间下标初始位置 + rightQue.append(len(nums) - 1) # len(nums) - 1为右区间下标初始位置 + + while nodeQue: + curNode = nodeQue.popleft() + left = leftQue.popleft() + right = rightQue.popleft() + mid = left + (right - left) // 2 + + curNode.val = nums[mid] # 将mid对应的元素给中间节点 + + if left <= mid - 1: # 处理左区间 + curNode.left = TreeNode(0) + nodeQue.append(curNode.left) + leftQue.append(left) + rightQue.append(mid - 1) + + if right >= mid + 1: # 处理右区间 + curNode.right = TreeNode(0) + nodeQue.append(curNode.right) + leftQue.append(mid + 1) + rightQue.append(right) + + return root + +``` + +### Go + +递归(隐含回溯) + +```go +func sortedArrayToBST(nums []int) *TreeNode { + if len(nums) == 0 { //终止条件,最后数组为空则可以返回 + return nil + } + idx := len(nums)/2 + root := &TreeNode{Val: nums[idx]} + + root.Left = sortedArrayToBST(nums[:idx]) + root.Right = sortedArrayToBST(nums[idx+1:]) + + return root +} +``` + +### JavaScript +递归 + +```javascript +var sortedArrayToBST = function (nums) { + const buildTree = (Arr, left, right) => { + if (left > right) + return null; + + let mid = Math.floor(left + (right - left) / 2); + + let root = new TreeNode(Arr[mid]); + root.left = buildTree(Arr, left, mid - 1); + root.right = buildTree(Arr, mid + 1, right); + return root; + } + return buildTree(nums, 0, nums.length - 1); +}; +``` +迭代 +```JavaScript +var sortedArrayToBST = function(nums) { + if(nums.length===0) { + return null; + } + let root = new TreeNode(0); //初始根节点 + let nodeQue = [root]; //放遍历的节点,并初始化 + let leftQue = [0]; //放左区间的下标,初始化 + let rightQue = [nums.length-1]; // 放右区间的下标 + + while(nodeQue.length) { + let curNode = nodeQue.pop(); + let left = leftQue.pop(); + let right = rightQue.pop(); + let mid = left + Math.floor((right-left)/2); + + curNode.val = nums[mid]; //将下标为mid的元素给中间节点 + +// 处理左区间 + if(left <= mid-1) { + curNode.left = new TreeNode(0); + nodeQue.push(curNode.left); + leftQue.push(left); + rightQue.push(mid-1); + } + +// 处理右区间 + if(right >= mid+1) { + curNode.right = new TreeNode(0); + nodeQue.push(curNode.right); + leftQue.push(mid+1); + rightQue.push(right); + } + } + return root; +}; +``` +### TypeScript + +```typescript +function sortedArrayToBST(nums: number[]): TreeNode | null { + function recur(nums: number[], left: number, right: number): TreeNode | null { + if (left > right) return null; + let mid: number = Math.floor((left + right) / 2); + const root: TreeNode = new TreeNode(nums[mid]); + root.left = recur(nums, left, mid - 1); + root.right = recur(nums, mid + 1, right); + return root; + } + return recur(nums, 0, nums.length - 1); +}; +``` + +### C + +递归 +```c +struct TreeNode* traversal(int* nums, int left, int right) { + if (left > right) + return NULL; + int mid = left + ((right - left) / 2); + struct TreeNode* root = (struct TreeNode*)malloc(sizeof(struct TreeNode)); + root->val = nums[mid]; + root->left = traversal(nums, left, mid - 1); + root->right = traversal(nums, mid + 1, right); + return root; +} + +struct TreeNode* sortedArrayToBST(int* nums, int numsSize) { + struct TreeNode* root = traversal(nums, 0, numsSize - 1); + return root; +} +``` + +### Scala + +递归: + +```scala +object Solution { + def sortedArrayToBST(nums: Array[Int]): TreeNode = { + def buildTree(left: Int, right: Int): TreeNode = { + if (left > right) return null // 当left大于right的时候,返回空 + // 最中间的节点是当前节点 + var mid = left + (right - left) / 2 + var curNode = new TreeNode(nums(mid)) + curNode.left = buildTree(left, mid - 1) + curNode.right = buildTree(mid + 1, right) + curNode + } + buildTree(0, nums.size - 1) + } +} +``` + +### Rust + +递归: + +```rust +impl Solution { + pub fn sorted_array_to_bst(nums: Vec) -> Option>> { + if nums.is_empty() { + return None; + } + let index = nums.len() / 2; + let mut root = TreeNode::new(nums[index]); + + root.left = Self::sorted_array_to_bst(nums[..index].to_vec()); + root.right = Self::sorted_array_to_bst(nums[index + 1..].to_vec()); + Some(Rc::new(RefCell::new(root))) + } +} +``` +### C# +```csharp +// 递归 +public TreeNode SortedArrayToBST(int[] nums) +{ + return Traversal(nums, 0, nums.Length - 1); +} +public TreeNode Traversal(int[] nums, int left, int right) +{ + if (left > right) return null; + int mid = left + (right - left) / 2; + TreeNode node = new TreeNode(nums[mid]); + node.left = Traversal(nums, left, mid - 1); + node.right = Traversal(nums, mid + 1, right); + return node; +} +``` + + + diff --git "a/problems/0110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" "b/problems/0110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" new file mode 100755 index 0000000000..d5b100ae80 --- /dev/null +++ "b/problems/0110.\345\271\263\350\241\241\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,998 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +> 求高度还是求深度,你搞懂了不? + +# 110.平衡二叉树 + +[力扣题目链接](https://leetcode.cn/problems/balanced-binary-tree/) + +给定一个二叉树,判断它是否是高度平衡的二叉树。 + +本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。 + +示例 1: + +给定二叉树 [3,9,20,null,null,15,7] + +![110.平衡二叉树](https://file1.kamacoder.com/i/algo/2021020315542230.png) + +返回 true 。 + +示例 2: + +给定二叉树 [1,2,2,3,3,null,null,4,4] + +![110.平衡二叉树1](https://file1.kamacoder.com/i/algo/20210203155447919.png) + +返回 false 。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[后序遍历求高度,高度判断是否平衡 | LeetCode:110.平衡二叉树](https://www.bilibili.com/video/BV1Ug411S7my),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 题外话 + +咋眼一看这道题目和[104.二叉树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)很像,其实有很大区别。 + +这里强调一波概念: + +* 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。 +* 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。 + +但leetcode中强调的深度和高度很明显是按照节点来计算的,如图: + +![110.平衡二叉树2](https://file1.kamacoder.com/i/algo/20210203155515650.png) + +关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。 + +因为求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中) + +有的同学一定疑惑,为什么[104.二叉树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)中求的是二叉树的最大深度,也用的是后序遍历。 + +**那是因为代码的逻辑其实是求的根节点的高度,而根节点的高度就是这棵树的最大深度,所以才可以使用后序遍历。** + +在[104.二叉树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)中,如果真正求取二叉树的最大深度,代码应该写成如下:(前序遍历) + +```CPP +class Solution { +public: + int result; + void getDepth(TreeNode* node, int depth) { + result = depth > result ? depth : result; // 中 + + if (node->left == NULL && node->right == NULL) return ; + + if (node->left) { // 左 + depth++; // 深度+1 + getDepth(node->left, depth); + depth--; // 回溯,深度-1 + } + if (node->right) { // 右 + depth++; // 深度+1 + getDepth(node->right, depth); + depth--; // 回溯,深度-1 + } + return ; + } + int maxDepth(TreeNode* root) { + result = 0; + if (root == NULL) return result; + getDepth(root, 1); + return result; + } +}; +``` + +**可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!** + +注意以上代码是为了把细节体现出来,简化一下代码如下: + +```CPP +class Solution { +public: + int result; + void getDepth(TreeNode* node, int depth) { + result = depth > result ? depth : result; // 中 + if (node->left == NULL && node->right == NULL) return ; + if (node->left) { // 左 + getDepth(node->left, depth + 1); + } + if (node->right) { // 右 + getDepth(node->right, depth + 1); + } + return ; + } + int maxDepth(TreeNode* root) { + result = 0; + if (root == 0) return result; + getDepth(root, 1); + return result; + } +}; +``` + +## 本题思路 + +### 递归 + +此时大家应该明白了既然要求比较高度,必然是要后序遍历。 + +递归三步曲分析: + +1. 明确递归函数的参数和返回值 + +参数:当前传入节点。 +返回值:以当前传入节点为根节点的树的高度。 + +那么如何标记左右子树是否差值大于1呢? + +如果当前传入节点为根节点的二叉树已经不是二叉平衡树了,还返回高度的话就没有意义了。 + +所以如果已经不是二叉平衡树了,可以返回-1 来标记已经不符合平衡树的规则了。 + +代码如下: + + +```CPP +// -1 表示已经不是平衡二叉树了,否则返回值是以该节点为根节点树的高度 +int getHeight(TreeNode* node) +``` + +2. 明确终止条件 + +递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的树高度为0 + +代码如下: + +```CPP +if (node == NULL) { + return 0; +} +``` + +3. 明确单层递归的逻辑 + +如何判断以当前传入节点为根节点的二叉树是否是平衡二叉树呢?当然是其左子树高度和其右子树高度的差值。 + +分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则返回-1,表示已经不是二叉平衡树了。 + +代码如下: + +```CPP +int leftHeight = getHeight(node->left); // 左 +if (leftHeight == -1) return -1; +int rightHeight = getHeight(node->right); // 右 +if (rightHeight == -1) return -1; + +int result; +if (abs(leftHeight - rightHeight) > 1) { // 中 + result = -1; +} else { + result = 1 + max(leftHeight, rightHeight); // 以当前节点为根节点的树的最大高度 +} + +return result; +``` + +代码精简之后如下: + +```CPP +int leftHeight = getHeight(node->left); +if (leftHeight == -1) return -1; +int rightHeight = getHeight(node->right); +if (rightHeight == -1) return -1; +return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight); +``` + +此时递归的函数就已经写出来了,这个递归的函数传入节点指针,返回以该节点为根节点的二叉树的高度,如果不是二叉平衡树,则返回-1。 + +getHeight整体代码如下: + +```CPP +int getHeight(TreeNode* node) { + if (node == NULL) { + return 0; + } + int leftHeight = getHeight(node->left); + if (leftHeight == -1) return -1; + int rightHeight = getHeight(node->right); + if (rightHeight == -1) return -1; + return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight); +} +``` + +最后本题整体递归代码如下: + +```CPP +class Solution { +public: + // 返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1 + int getHeight(TreeNode* node) { + if (node == NULL) { + return 0; + } + int leftHeight = getHeight(node->left); + if (leftHeight == -1) return -1; + int rightHeight = getHeight(node->right); + if (rightHeight == -1) return -1; + return abs(leftHeight - rightHeight) > 1 ? -1 : 1 + max(leftHeight, rightHeight); + } + bool isBalanced(TreeNode* root) { + return getHeight(root) == -1 ? false : true; + } +}; +``` + +### 迭代 + +在[104.二叉树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)中我们可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。 + +本题的迭代方式可以先定义一个函数,专门用来求高度。 + +这个函数通过栈模拟的后序遍历找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度) + +代码如下: + +```CPP +// cur节点的最大深度,就是cur的高度 +int getDepth(TreeNode* cur) { + stack st; + if (cur != NULL) st.push(cur); + int depth = 0; // 记录深度 + int result = 0; + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + st.push(node); // 中 + st.push(NULL); + depth++; + if (node->right) st.push(node->right); // 右 + if (node->left) st.push(node->left); // 左 + + } else { + st.pop(); + node = st.top(); + st.pop(); + depth--; + } + result = result > depth ? result : depth; + } + return result; +} +``` + +然后再用栈来模拟后序遍历,遍历每一个节点的时候,再去判断左右孩子的高度是否符合,代码如下: + +```CPP +bool isBalanced(TreeNode* root) { + stack st; + if (root == NULL) return true; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); // 中 + st.pop(); + if (abs(getDepth(node->left) - getDepth(node->right)) > 1) { // 判断左右孩子高度是否符合 + return false; + } + if (node->right) st.push(node->right); // 右(空节点不入栈) + if (node->left) st.push(node->left); // 左(空节点不入栈) + } + return true; +} +``` + +整体代码如下: + +```CPP +class Solution { +private: + int getDepth(TreeNode* cur) { + stack st; + if (cur != NULL) st.push(cur); + int depth = 0; // 记录深度 + int result = 0; + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + st.push(node); // 中 + st.push(NULL); + depth++; + if (node->right) st.push(node->right); // 右 + if (node->left) st.push(node->left); // 左 + + } else { + st.pop(); + node = st.top(); + st.pop(); + depth--; + } + result = result > depth ? result : depth; + } + return result; + } + +public: + bool isBalanced(TreeNode* root) { + stack st; + if (root == NULL) return true; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); // 中 + st.pop(); + if (abs(getDepth(node->left) - getDepth(node->right)) > 1) { + return false; + } + if (node->right) st.push(node->right); // 右(空节点不入栈) + if (node->left) st.push(node->left); // 左(空节点不入栈) + } + return true; + } +}; +``` + +当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。 + +虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。 + +**例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!** + +因为对于回溯算法已经是非常复杂的递归了,如果再用迭代的话,就是自己给自己找麻烦,效率也并不一定高。 + +## 总结 + +通过本题可以了解求二叉树深度 和 二叉树高度的差异,求深度适合用前序遍历,而求高度适合用后序遍历。 + +本题迭代法其实有点复杂,大家可以有一个思路,也不一定说非要写出来。 + +但是递归方式是一定要掌握的! + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + /** + * 递归法 + */ + public boolean isBalanced(TreeNode root) { + return getHeight(root) != -1; + } + + private int getHeight(TreeNode root) { + if (root == null) { + return 0; + } + int leftHeight = getHeight(root.left); + if (leftHeight == -1) { + return -1; + } + int rightHeight = getHeight(root.right); + if (rightHeight == -1) { + return -1; + } + // 左右子树高度差大于1,return -1表示已经不是平衡树了 + if (Math.abs(leftHeight - rightHeight) > 1) { + return -1; + } + return Math.max(leftHeight, rightHeight) + 1; + } +} + +class Solution { + /** + * 迭代法,效率较低,计算高度时会重复遍历 + * 时间复杂度:O(n^2) + */ + public boolean isBalanced(TreeNode root) { + if (root == null) { + return true; + } + Stack stack = new Stack<>(); + TreeNode pre = null; + while (root!= null || !stack.isEmpty()) { + while (root != null) { + stack.push(root); + root = root.left; + } + TreeNode inNode = stack.peek(); + // 右结点为null或已经遍历过 + if (inNode.right == null || inNode.right == pre) { + // 比较左右子树的高度差,输出 + if (Math.abs(getHeight(inNode.left) - getHeight(inNode.right)) > 1) { + return false; + } + stack.pop(); + pre = inNode; + root = null;// 当前结点下,没有要遍历的结点了 + } else { + root = inNode.right;// 右结点还没遍历,遍历右结点 + } + } + return true; + } + + /** + * 层序遍历,求结点的高度 + */ + public int getHeight(TreeNode root) { + if (root == null) { + return 0; + } + Deque deque = new LinkedList<>(); + deque.offer(root); + int depth = 0; + while (!deque.isEmpty()) { + int size = deque.size(); + depth++; + for (int i = 0; i < size; i++) { + TreeNode poll = deque.poll(); + if (poll.left != null) { + deque.offer(poll.left); + } + if (poll.right != null) { + deque.offer(poll.right); + } + } + } + return depth; + } +} + +class Solution { + /** + * 优化迭代法,针对暴力迭代法的getHeight方法做优化,利用TreeNode.val来保存当前结点的高度,这样就不会有重复遍历 + * 获取高度算法时间复杂度可以降到O(1),总的时间复杂度降为O(n)。 + * 时间复杂度:O(n) + */ + public boolean isBalanced(TreeNode root) { + if (root == null) { + return true; + } + Stack stack = new Stack<>(); + TreeNode pre = null; + while (root != null || !stack.isEmpty()) { + while (root != null) { + stack.push(root); + root = root.left; + } + TreeNode inNode = stack.peek(); + // 右结点为null或已经遍历过 + if (inNode.right == null || inNode.right == pre) { + // 输出 + if (Math.abs(getHeight(inNode.left) - getHeight(inNode.right)) > 1) { + return false; + } + stack.pop(); + pre = inNode; + root = null;// 当前结点下,没有要遍历的结点了 + } else { + root = inNode.right;// 右结点还没遍历,遍历右结点 + } + } + return true; + } + + /** + * 求结点的高度 + */ + public int getHeight(TreeNode root) { + if (root == null) { + return 0; + } + int leftHeight = root.left != null ? root.left.val : 0; + int rightHeight = root.right != null ? root.right.val : 0; + int height = Math.max(leftHeight, rightHeight) + 1; + root.val = height;// 用TreeNode.val来保存当前结点的高度 + return height; + } +} +``` + +### Python: + +递归法: + +```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 isBalanced(self, root: TreeNode) -> bool: + if self.get_height(root) != -1: + return True + else: + return False + + def get_height(self, root: TreeNode) -> int: + # Base Case + if not root: + return 0 + # 左 + if (left_height := self.get_height(root.left)) == -1: + return -1 + # 右 + if (right_height := self.get_height(root.right)) == -1: + return -1 + # 中 + if abs(left_height - right_height) > 1: + return -1 + else: + return 1 + max(left_height, right_height) +``` +递归法精简版: + +```python +class Solution: + def isBalanced(self, root: Optional[TreeNode]) -> bool: + return self.get_hight(root) != -1 + def get_hight(self, node): + if not node: + return 0 + left = self.get_hight(node.left) + right = self.get_hight(node.right) + if left == -1 or right == -1 or abs(left - right) > 1: + return -1 + return max(left, right) + 1 +``` + + +迭代法: + +```python +class Solution: + def getDepth(self, cur): + st = [] + if cur is not None: + st.append(cur) + depth = 0 + result = 0 + while st: + node = st[-1] + if node is not None: + st.pop() + st.append(node) # 中 + st.append(None) + depth += 1 + if node.right: + st.append(node.right) # 右 + if node.left: + st.append(node.left) # 左 + + else: + node = st.pop() + st.pop() + depth -= 1 + result = max(result, depth) + return result + + def isBalanced(self, root): + st = [] + if root is None: + return True + st.append(root) + while st: + node = st.pop() # 中 + if abs(self.getDepth(node.left) - self.getDepth(node.right)) > 1: + return False + if node.right: + st.append(node.right) # 右(空节点不入栈) + if node.left: + st.append(node.left) # 左(空节点不入栈) + return True + +``` + +迭代法精简版: + +```python +class Solution: + def isBalanced(self, root: Optional[TreeNode]) -> bool: + if not root: + return True + + height_map = {} + stack = [root] + while stack: + node = stack.pop() + if node: + stack.append(node) # 中 + stack.append(None) + # 采用数组进行迭代,先将右节点加入,保证左节点能够先出栈 + if node.right: # 右 + stack.append(node.right) + if node.left: # 左 + stack.append(node.left) + else: + real_node = stack.pop() + left, right = height_map.get(real_node.left, 0), height_map.get(real_node.right, 0) + if abs(left - right) > 1: + return False + height_map[real_node] = 1 + max(left, right) + return True +``` +### Go: + +递归法 + +```Go +func isBalanced(root *TreeNode) bool { + h := getHeight(root) + if h == -1 { + return false + } + return true +} +// 返回以该节点为根节点的二叉树的高度,如果不是平衡二叉树了则返回-1 +func getHeight(root *TreeNode) int { + if root == nil { + return 0 + } + l, r := getHeight(root.Left), getHeight(root.Right) + if l == -1 || r == -1 { + return -1 + } + if l - r > 1 || r - l > 1 { + return -1 + } + return max(l, r) + 1 +} +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +迭代法 + +```Go +func isBalanced(root *TreeNode) bool { + st := make([]*TreeNode, 0) + if root == nil { + return true + } + st = append(st, root) + for len(st) > 0 { + node := st[len(st)-1] + st = st[:len(st)-1] + if math.Abs(float64(getDepth(node.Left)) - float64(getDepth(node.Right))) > 1 { + return false + } + if node.Right != nil { + st = append(st, node.Right) + } + if node.Left != nil { + st = append(st, node.Left) + } + } + return true +} + +func getDepth(cur *TreeNode) int { + st := make([]*TreeNode, 0) + if cur != nil { + st = append(st, cur) + } + depth := 0 + result := 0 + for len(st) > 0 { + node := st[len(st)-1] + if node != nil { + st = st[:len(st)-1] + st = append(st, node, nil) + depth++ + if node.Right != nil { + st = append(st, node.Right) + } + if node.Left != nil { + st = append(st, node.Left) + } + } else { + st = st[:len(st)-1] + node = st[len(st)-1] + st = st[:len(st)-1] + depth-- + } + if result < depth { + result = depth + } + } + return result +} +``` + +### JavaScript: + +递归法: + +```javascript +var isBalanced = function(root) { + //还是用递归三部曲 + 后序遍历 左右中 当前左子树右子树高度相差大于1就返回-1 + // 1. 确定递归函数参数以及返回值 + const getDepth = function(node) { + // 2. 确定递归函数终止条件 + if(node === null) return 0; + // 3. 确定单层递归逻辑 + let leftDepth = getDepth(node.left); //左子树高度 + // 当判定左子树不为平衡二叉树时,即可直接返回-1 + if(leftDepth === -1) return -1; + let rightDepth = getDepth(node.right); //右子树高度 + // 当判定右子树不为平衡二叉树时,即可直接返回-1 + if(rightDepth === -1) return -1; + if(Math.abs(leftDepth - rightDepth) > 1) { + return -1; + } else { + return 1 + Math.max(leftDepth, rightDepth); + } + } + return !(getDepth(root) === -1); +}; +``` + +迭代法: + +```javascript +// 获取当前节点的高度 +var getHeight = function (curNode) { + let queue = []; + if (curNode !== null) queue.push(curNode); // 压入当前元素 + let depth = 0, res = 0; + while (queue.length) { + let node = queue[queue.length - 1]; // 取出栈顶 + if (node !== null) { + queue.pop(); + queue.push(node); // 中 + queue.push(null); + depth++; + node.right && queue.push(node.right); // 右 + node.left && queue.push(node.left); // 左 + } else { + queue.pop(); + node = queue[queue.length - 1]; + queue.pop(); + depth--; + } + res = res > depth ? res : depth; + } + return res; +} +var isBalanced = function (root) { + if (root === null) return true; + let queue = [root]; + while (queue.length) { + let node = queue[queue.length - 1]; // 取出栈顶 + queue.pop(); + if (Math.abs(getHeight(node.left) - getHeight(node.right)) > 1) { + return false; + } + node.right && queue.push(node.right); + node.left && queue.push(node.left); + } + return true; +}; +``` + +### TypeScript: + +```typescript +// 递归法 +function isBalanced(root: TreeNode | null): boolean { + function getDepth(root: TreeNode | null): number { + if (root === null) return 0; + let leftDepth: number = getDepth(root.left); + if (leftDepth === -1) return -1; + let rightDepth: number = getDepth(root.right); + if (rightDepth === -1) return -1; + if (Math.abs(leftDepth - rightDepth) > 1) return -1; + return 1 + Math.max(leftDepth, rightDepth); + } + return getDepth(root) !== -1; +}; +``` + +### C: + +递归法: + +```c +int getDepth(struct TreeNode* node) { + //如果结点不存在,返回0 + if(!node) + return 0; + //求出右子树深度 + int rightDepth = getDepth(node->right); + //求出左子树深度 + int leftDepth = getDepth(node->left); + //返回左右子树中的较大值+1 + return rightDepth > leftDepth ? rightDepth + 1 : leftDepth + 1; +} + +bool isBalanced(struct TreeNode* root) { + //递归结束条件为:传入结点为NULL,返回True + if(!root) + return 1; + //求出左右子树的深度 + int leftDepth = getDepth(root->left); + int rightDepth = getDepth(root->right); + int diff; + //若左右子树绝对值差距大于1,返回False + if((diff = leftDepth - rightDepth) > 1 || diff < -1) + return 0; + //检查左右子树是否为平衡二叉树 + return isBalanced(root->right) && isBalanced(root->left); +} +``` + +迭代法: + +```c +//计算结点深度 +int getDepth(struct TreeNode* node) { + //开辟栈空间 + struct TreeNode** stack = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 10000); + int stackTop = 0; + //若传入结点存在,将其入栈。若不存在,函数直接返回0 + if(node) + stack[stackTop++] = node; + int result = 0; + int depth = 0; + + //当栈中有元素时,进行迭代遍历 + while(stackTop) { + //取出栈顶元素 + struct TreeNode* tempNode = stack[--stackTop]; + //若栈顶元素非NULL,则将深度+1 + if(tempNode) { + depth++; + //将栈顶元素再次入栈,添加NULL表示此结点已被遍历 + stack[stackTop++] = tempNode; + stack[stackTop++] = NULL; + //若栈顶元素有左右孩子,则将孩子结点入栈 + if(tempNode->left) + stack[stackTop++] = tempNode->left; + if(tempNode->right) + stack[stackTop++] = tempNode->right; + //更新结果 + result = result > depth ? result : depth; + } + else { + //若为NULL,则代表当前结点已被遍历,深度-1 + tempNode = stack[--stackTop]; + depth--; + } + } + + return result; +} + +bool isBalanced(struct TreeNode* root){ + //开辟栈空间 + struct TreeNode** stack = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 10000); + int stackTop = 0; + + //若根节点不存在,返回True + if(!root) + return 1; + + //将根节点入栈 + stack[stackTop++] = root; + //当栈中有元素时,进行遍历 + while(stackTop) { + //将栈顶元素出栈 + struct TreeNode* node = stack[--stackTop]; + //计算左右子树的深度 + int diff = getDepth(node->right) - getDepth(node->left); + //若深度的绝对值大于1,返回False + if(diff > 1 || diff < -1) + return 0; + //如果栈顶结点有左右结点,将左右结点入栈 + if(node->left) + stack[stackTop++] = node->left; + if(node->right) + stack[stackTop++] = node->right; + } + //若二叉树遍历结束后没有返回False,则返回True + return 1; +} +``` + +### Swift: + +>递归 + +```swift +func isBalanced(_ root: TreeNode?) -> Bool { + // -1 已经不是平衡二叉树 + return getHeight(root) == -1 ? false : true +} +func getHeight(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + let leftHeight = getHeight(root.left) + if leftHeight == -1 { + return -1 + } + let rightHeight = getHeight(root.right) + if rightHeight == -1 { + return -1 + } + if abs(leftHeight - rightHeight) > 1 { + return -1 + } else { + return 1 + max(leftHeight, rightHeight) + } +} +``` + +### Rust: + +递归 + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn is_balanced(root: Option>>) -> bool { + Self::get_depth(root) != -1 + } + pub fn get_depth(root: Option>>) -> i32 { + if root.is_none() { + return 0; + } + let right = Self::get_depth(root.as_ref().unwrap().borrow().left.clone()); + let left = Self::get_depth(root.unwrap().borrow().right.clone()); + if right == -1 { + return -1; + } + if left == -1 { + return -1; + } + if (right - left).abs() > 1 { + return -1; + } + + 1 + right.max(left) + } +} +``` +### C# +```csharp +public bool IsBalanced(TreeNode root) +{ + return GetHeight(root) == -1 ? false : true; +} +public int GetHeight(TreeNode root) +{ + if (root == null) return 0; + int left = GetHeight(root.left); + if (left == -1) return -1; + int right = GetHeight(root.right); + if (right == -1) return -1; + int res; + if (Math.Abs(left - right) > 1) + { + res = -1; + } + else + { + res = 1 + Math.Max(left, right); + } + return res; +} +``` + + diff --git "a/problems/0111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" "b/problems/0111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" new file mode 100755 index 0000000000..e1ee42657c --- /dev/null +++ "b/problems/0111.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\345\260\217\346\267\261\345\272\246.md" @@ -0,0 +1,752 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 和求最大深度一个套路? + +# 111.二叉树的最小深度 + +[力扣题目链接](https://leetcode.cn/problems/minimum-depth-of-binary-tree/) + +给定一个二叉树,找出其最小深度。 + +最小深度是从根节点到最近叶子节点的最短路径上的节点数量。 + +说明: 叶子节点是指没有子节点的节点。 + +示例: + +给定二叉树 [3,9,20,null,null,15,7], + + +![111.二叉树的最小深度1](https://file1.kamacoder.com/i/algo/2021020315582586.png) + +返回它的最小深度 2. + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[看起来好像做过,一写就错! | LeetCode:111.二叉树的最小深度](https://www.bilibili.com/video/BV1QD4y1B7e2),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +看完了这篇[104.二叉树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html),再来看看如何求最小深度。 + +直觉上好像和求最大深度差不多,其实还是差不少的。 + +本题依然是前序遍历和后序遍历都可以,前序求的是深度,后序求的是高度。 + +* 二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数或者节点数(取决于深度从0开始还是从1开始) +* 二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数或者节点数(取决于高度从0开始还是从1开始) + +那么使用后序遍历,其实求的是根节点到叶子节点的最小距离,就是求高度的过程,不过这个最小距离 也同样是最小深度。 + +以下讲解中遍历顺序上依然采用后序遍历(因为要比较递归返回之后的结果,本文我也给出前序遍历的写法)。 + +本题还有一个误区,在处理节点的过程中,最大深度很容易理解,最小深度就不那么好理解,如图: + +![111.二叉树的最小深度](https://file1.kamacoder.com/i/algo/111.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6.png) + +这就重新审题了,题目中说的是:**最小深度是从根节点到最近叶子节点的最短路径上的节点数量。**注意是**叶子节点**。 + +什么是叶子节点,左右孩子都为空的节点才是叶子节点! + +### 递归法 + +来来来,一起递归三部曲: + +1. 确定递归函数的参数和返回值 + +参数为要传入的二叉树根节点,返回的是int类型的深度。 + +代码如下: + +```CPP +int getDepth(TreeNode* node) +``` + +2. 确定终止条件 + +终止条件也是遇到空节点返回0,表示当前节点的高度为0。 + +代码如下: + +```CPP +if (node == NULL) return 0; +``` + +3. 确定单层递归的逻辑 + +这块和求最大深度可就不一样了,一些同学可能会写如下代码: +```CPP +int leftDepth = getDepth(node->left); +int rightDepth = getDepth(node->right); +int result = 1 + min(leftDepth, rightDepth); +return result; +``` + +这个代码就犯了此图中的误区: + +![111.二叉树的最小深度](https://file1.kamacoder.com/i/algo/111.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E6%9C%80%E5%B0%8F%E6%B7%B1%E5%BA%A6.png) + +如果这么求的话,没有左孩子的分支会算为最短深度。 + +所以,如果左子树为空,右子树不为空,说明最小深度是 1 + 右子树的深度。 + +反之,右子树为空,左子树不为空,最小深度是 1 + 左子树的深度。 最后如果左右子树都不为空,返回左右子树深度最小值 + 1 。 + +代码如下: + +```CPP +int leftDepth = getDepth(node->left); // 左 +int rightDepth = getDepth(node->right); // 右 + // 中 +// 当一个左子树为空,右不为空,这时并不是最低点 +if (node->left == NULL && node->right != NULL) {  +    return 1 + rightDepth; +}    +// 当一个右子树为空,左不为空,这时并不是最低点 +if (node->left != NULL && node->right == NULL) {  +    return 1 + leftDepth; +} +int result = 1 + min(leftDepth, rightDepth); +return result; +``` + +遍历的顺序为后序(左右中),可以看出:**求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。** + +整体递归代码如下: +```CPP +class Solution { +public: + int getDepth(TreeNode* node) { + if (node == NULL) return 0; + int leftDepth = getDepth(node->left); // 左 + int rightDepth = getDepth(node->right); // 右 + // 中 + // 当一个左子树为空,右不为空,这时并不是最低点 + if (node->left == NULL && node->right != NULL) {  +     return 1 + rightDepth; + }    + // 当一个右子树为空,左不为空,这时并不是最低点 + if (node->left != NULL && node->right == NULL) {  +     return 1 + leftDepth; + } + int result = 1 + min(leftDepth, rightDepth); + return result; + } + + int minDepth(TreeNode* root) { + return getDepth(root); + } +}; +``` + +精简之后代码如下: + +```CPP +class Solution { +public: + int minDepth(TreeNode* root) { + if (root == NULL) return 0; + if (root->left == NULL && root->right != NULL) { + return 1 + minDepth(root->right); + } + if (root->left != NULL && root->right == NULL) { + return 1 + minDepth(root->left); + } + return 1 + min(minDepth(root->left), minDepth(root->right)); + } +}; +``` + +**精简之后的代码根本看不出是哪种遍历方式,所以依然还要强调一波:如果对二叉树的操作还不熟练,尽量不要直接照着精简代码来学。** + +前序遍历的方式: + +```CPP +class Solution { +private: + int result; + void getdepth(TreeNode* node, int depth) { + // 函数递归终止条件 + if (node == nullptr) { + return; + } + // 中,处理逻辑:判断是不是叶子结点 + if (node -> left == nullptr && node->right == nullptr) { + result = min(result, depth); + } + if (node->left) { // 左 + getdepth(node->left, depth + 1); + } + if (node->right) { // 右 + getdepth(node->right, depth + 1); + } + return ; + } + +public: + int minDepth(TreeNode* root) { + if (root == nullptr) { + return 0; + } + result = INT_MAX; + getdepth(root, 1); + return result; + } +}; +``` + +### 迭代法 + +相对于[104.二叉树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html),本题还可以使用层序遍历的方式来解决,思路是一样的。 + +如果对层序遍历还不清楚的话,可以看这篇:[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html) + +**需要注意的是,只有当左右孩子都为空的时候,才说明遍历到最低点了。如果其中一个孩子不为空则不是最低点** + +代码如下:(详细注释) + +```CPP +class Solution { +public: + + int minDepth(TreeNode* root) { + if (root == NULL) return 0; + int depth = 0; + queue que; + que.push(root); + while(!que.empty()) { + int size = que.size(); + depth++; // 记录最小深度 + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + if (!node->left && !node->right) { // 当左右孩子都为空的时候,说明是最低点的一层了,退出 + return depth; + } + } + } + return depth; + } +}; +``` + + +## 其他语言版本 + + +### Java: + +```Java +class Solution { + /** + * 递归法,相比求MaxDepth要复杂点 + * 因为最小深度是从根节点到最近**叶子节点**的最短路径上的节点数量 + */ + public int minDepth(TreeNode root) { + if (root == null) { + return 0; + } + int leftDepth = minDepth(root.left); + int rightDepth = minDepth(root.right); + if (root.left == null) { + return rightDepth + 1; + } + if (root.right == null) { + return leftDepth + 1; + } + // 左右结点都不为null + return Math.min(leftDepth, rightDepth) + 1; + } +} +``` + +```java +class Solution { + /** + * 递归法(思路来自二叉树最大深度的递归法) + * 该题求最小深度,最小深度为根节点到叶子节点的深度,所以在迭代到每个叶子节点时更新最小值。 + */ + int depth = 0; + // 定义最小深度,初始化最大值 + int minDepth = Integer.MAX_VALUE; + public int minDepth(TreeNode root) { + dep(root); + return minDepth == Integer.MAX_VALUE ? 0 : minDepth; + } + void dep(TreeNode root){ + if(root == null) return ; + // 递归开始,深度增加 + depth++; + dep(root.left); + dep(root.right); + // 该位置表示递归到叶子节点了,需要更新最小深度minDepth + if(root.left == null && root.right == null) + minDepth = Math.min(minDepth , depth); + // 递归结束,深度减小 + depth--; + } +} +``` + +```Java +class Solution { + /** + * 迭代法,层序遍历 + */ + public int minDepth(TreeNode root) { + if (root == null) { + return 0; + } + Deque deque = new LinkedList<>(); + deque.offer(root); + int depth = 0; + while (!deque.isEmpty()) { + int size = deque.size(); + depth++; + for (int i = 0; i < size; i++) { + TreeNode poll = deque.poll(); + if (poll.left == null && poll.right == null) { + // 是叶子结点,直接返回depth,因为从上往下遍历,所以该值就是最小值 + return depth; + } + if (poll.left != null) { + deque.offer(poll.left); + } + if (poll.right != null) { + deque.offer(poll.right); + } + } + } + return depth; + } +} +``` + +### Python : + +递归法(版本一) + +```python +class Solution: + def getDepth(self, node): + if node is None: + return 0 + leftDepth = self.getDepth(node.left) # 左 + rightDepth = self.getDepth(node.right) # 右 + + # 当一个左子树为空,右不为空,这时并不是最低点 + if node.left is None and node.right is not None: + return 1 + rightDepth + + # 当一个右子树为空,左不为空,这时并不是最低点 + if node.left is not None and node.right is None: + return 1 + leftDepth + + result = 1 + min(leftDepth, rightDepth) + return result + + def minDepth(self, root): + return self.getDepth(root) + +``` +递归法(版本二) + +```python +class Solution: + def minDepth(self, root): + if root is None: + return 0 + if root.left is None and root.right is not None: + return 1 + self.minDepth(root.right) + if root.left is not None and root.right is None: + return 1 + self.minDepth(root.left) + return 1 + min(self.minDepth(root.left), self.minDepth(root.right)) + + +``` +递归法(版本三)前序 + +```python +class Solution: + def __init__(self): + self.result = float('inf') + + def getDepth(self, node, depth): + if node is None: + return + if node.left is None and node.right is None: + self.result = min(self.result, depth) + if node.left: + self.getDepth(node.left, depth + 1) + if node.right: + self.getDepth(node.right, depth + 1) + + def minDepth(self, root): + if root is None: + return 0 + self.getDepth(root, 1) + return self.result + + +``` +迭代法 + +```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 minDepth(self, root: TreeNode) -> int: + if not root: + return 0 + depth = 0 + queue = collections.deque([root]) + + while queue: + depth += 1 + for _ in range(len(queue)): + node = queue.popleft() + + if not node.left and not node.right: + return depth + + if node.left: + queue.append(node.left) + + if node.right: + queue.append(node.right) + + return depth +``` + +### Go: + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func min(a, b int) int { + if a < b { + return a; + } + return b; +} +// 递归 +func minDepth(root *TreeNode) int { + if root == nil { + return 0; + } + if root.Left == nil && root.Right != nil { + return 1 + minDepth(root.Right); + } + if root.Right == nil && root.Left != nil { + return 1 + minDepth(root.Left); + } + return min(minDepth(root.Left), minDepth(root.Right)) + 1; +} + +// 迭代 + +func minDepth(root *TreeNode) int { + dep := 0; + queue := make([]*TreeNode, 0); + if root != nil { + queue = append(queue, root); + } + for l := len(queue); l > 0; { + dep++; + for ; l > 0; l-- { + node := queue[0]; + if node.Left == nil && node.Right == nil { + return dep; + } + if node.Left != nil { + queue = append(queue, node.Left); + } + if node.Right != nil { + queue = append(queue, node.Right); + } + queue = queue[1:]; + } + l = len(queue); + } + return dep; +} +``` + + +### JavaScript: + +递归法: + +```javascript +/** + * @param {TreeNode} root + * @return {number} + */ +var minDepth1 = function(root) { + if(!root) return 0; + // 到叶子节点 返回 1 + if(!root.left && !root.right) return 1; + // 只有右节点时 递归右节点 + if(!root.left) return 1 + minDepth(root.right); + // 只有左节点时 递归左节点 + if(!root.right) return 1 + minDepth(root.left); + return Math.min(minDepth(root.left), minDepth(root.right)) + 1; +}; +``` + +迭代法: + +```javascript +/** +* @param {TreeNode} root +* @return {number} +*/ +var minDepth = function(root) { + if(!root) return 0; + const queue = [root]; + let dep = 0; + while(true) { + let size = queue.length; + dep++; + while(size--){ + const node = queue.shift(); + // 到第一个叶子节点 返回 当前深度 + if(!node.left && !node.right) return dep; + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } +}; +``` + +### TypeScript: + +> 递归法 + +```typescript +function minDepth(root: TreeNode | null): number { + if (root === null) return 0; + if (root.left !== null && root.right === null) { + return 1 + minDepth(root.left); + } + if (root.left === null && root.right !== null) { + return 1 + minDepth(root.right); + } + return 1 + Math.min(minDepth(root.left), minDepth(root.right)); +} +``` + +> 迭代法 + +```typescript +function minDepth(root: TreeNode | null): number { + let helperQueue: TreeNode[] = []; + let resMin: number = 0; + let tempNode: TreeNode; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + resMin++; + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + if (tempNode.left === null && tempNode.right === null) return resMin; + if (tempNode.left !== null) helperQueue.push(tempNode.left); + if (tempNode.right !== null) helperQueue.push(tempNode.right); + } + } + return resMin; +}; +``` + +### Swift: + +> 递归 +```Swift +func minDepth(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + if root.left == nil && root.right != nil { + return 1 + minDepth(root.right) + } + if root.left != nil && root.right == nil { + return 1 + minDepth(root.left) + } + return 1 + min(minDepth(root.left), minDepth(root.right)) +} +``` + +> 迭代 +```Swift +func minDepth(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + var res = 0 + var queue = [TreeNode]() + queue.append(root) + while !queue.isEmpty { + res += 1 + for _ in 0 ..< queue.count { + let node = queue.removeFirst() + if node.left == nil && node.right == nil { + return res + } + if let left = node.left { + queue.append(left) + } + if let right = node.right { + queue.append(right) + } + } + } + return res +} +``` + + +### Scala: + +递归法: +```scala +object Solution { + def minDepth(root: TreeNode): Int = { + if (root == null) return 0 + if (root.left == null && root.right != null) return 1 + minDepth(root.right) + if (root.left != null && root.right == null) return 1 + minDepth(root.left) + // 如果两侧都不为空,则取最小值,return关键字可以省略 + 1 + math.min(minDepth(root.left), minDepth(root.right)) + } +} +``` + +迭代法: +```scala +object Solution { + import scala.collection.mutable + def minDepth(root: TreeNode): Int = { + if (root == null) return 0 + var depth = 0 + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + depth += 1 + val len = queue.size + for (i <- 0 until len) { + val curNode = queue.dequeue() + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + if (curNode.left == null && curNode.right == null) return depth + } + } + depth + } +} +``` + +### Rust: + +```rust +impl Solution { + // 递归 + pub fn min_depth(root: Option>>) -> i32 { + if let Some(node) = root { + match (node.borrow().left.clone(), node.borrow().right.clone()) { + (Some(n1), None) => 1 + Self::min_depth(Some(n1)), + (None, Some(n2)) => 1 + Self::min_depth(Some(n2)), + (Some(n1), Some(n2)) => { + 1 + std::cmp::min(Self::min_depth(Some(n1)), Self::min_depth(Some(n2))) + } + _ => 1, + } + } else { + 0 + } + } + + // 迭代 + // 需要 use std::collections::VecDeque; + pub fn min_depth(root: Option>>) -> i32 { + let mut res = 0; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + res += 1; + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + if node.borrow().left.is_none() && node.borrow().right.is_none() { + return res; + } + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + } + res + } +} +``` +### C# +```csharp +// 递归 +public int MinDepth(TreeNode root) +{ + if (root == null) return 0; + int left = MinDepth(root.left); + int right = MinDepth(root.right); + if (root.left == null && root.right != null) + return 1+right; + else if(root.left!=null && root.right == null) + return 1+left; + + int res = 1 + Math.Min(left, right); + return res; +} +``` +```csharp +// 迭代 +public int MinDepth(TreeNode root) +{ + if (root == null) return 0; + int depth = 0; + var que = new Queue(); + que.Enqueue(root); + while (que.Count > 0) + { + int size = que.Count; + depth++; + for (int i = 0; i < size; i++) + { + var node = que.Dequeue(); + if (node.left != null) + que.Enqueue(node.left); + if (node.right != null) + que.Enqueue(node.right); + if (node.left == null && node.right == null) + return depth; + } + } + return depth; +} +``` + diff --git "a/problems/0112.\350\267\257\345\276\204\346\200\273\345\222\214.md" "b/problems/0112.\350\267\257\345\276\204\346\200\273\345\222\214.md" new file mode 100755 index 0000000000..73795bcfc9 --- /dev/null +++ "b/problems/0112.\350\267\257\345\276\204\346\200\273\345\222\214.md" @@ -0,0 +1,1625 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 112. 路径总和 + +[力扣题目链接](https://leetcode.cn/problems/path-sum/) + +给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。 + +说明: 叶子节点是指没有子节点的节点。 + +示例: +给定如下二叉树,以及目标和 sum = 22, + +![](https://file1.kamacoder.com/i/algo/20230407210247.png) + +返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[拿不准的遍历顺序,搞不清的回溯过程,我太难了! | LeetCode:112. 路径总和](https://www.bilibili.com/video/BV19t4y1L7CR),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +相信很多同学都会疑惑,递归函数什么时候要有返回值,什么时候没有返回值,特别是有的时候递归函数返回类型为bool类型。 + +那么接下来我通过详细讲解如下两道题,来回答这个问题: + +* [112.路径总和](https://leetcode.cn/problems/path-sum/) +* [113.路径总和ii](https://leetcode.cn/problems/path-sum-ii/) + +这道题我们要遍历从根节点到叶子节点的路径看看总和是不是目标和。 + +### 递归 + +可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树 + +1. 确定递归函数的参数和返回类型 + +参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。 + +再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点: + +* 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii) +* 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在[236. 二叉树的最近公共祖先](https://programmercarl.com/0236.二叉树的最近公共祖先.html)中介绍) +* 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况) + +而本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢? + +如图所示: + +![112.路径总和](https://file1.kamacoder.com/i/algo/2021020316051216.png) + +图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。 + +所以代码如下: + +```CPP +bool traversal(treenode* cur, int count) // 注意函数的返回类型 +``` + + +2. 确定终止条件 + +首先计数器如何统计这一条路径的和呢? + +不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。 + +如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。 + +如果遍历到了叶子节点,count不为0,就是没找到。 + +递归终止条件代码如下: + +```CPP +if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0 +if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回 +``` + +3. 确定单层递归的逻辑 + +因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。 + +递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。 + +代码如下: + +```cpp +if (cur->left) { // 左 (空节点不遍历) + // 遇到叶子节点返回true,则直接返回true + if (traversal(cur->left, count - cur->left->val)) return true; // 注意这里有回溯的逻辑 +} +if (cur->right) { // 右 (空节点不遍历) + // 遇到叶子节点返回true,则直接返回true + if (traversal(cur->right, count - cur->right->val)) return true; // 注意这里有回溯的逻辑 +} +return false; +``` + +以上代码中是包含着回溯的,没有回溯,如何后撤重新找另一条路径呢。 + +回溯隐藏在`traversal(cur->left, count - cur->left->val)`这里, 因为把`count - cur->left->val` 直接作为参数传进去,函数结束,count的数值没有改变。 + +为了把回溯的过程体现出来,可以改为如下代码: + +```cpp +if (cur->left) { // 左 + count -= cur->left->val; // 递归,处理节点; + if (traversal(cur->left, count)) return true; + count += cur->left->val; // 回溯,撤销处理结果 +} +if (cur->right) { // 右 + count -= cur->right->val; + if (traversal(cur->right, count)) return true; + count += cur->right->val; +} +return false; +``` + + +整体代码如下: + +```cpp +class Solution { +private: + bool traversal(TreeNode* cur, int count) { + if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0 + if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回 + + if (cur->left) { // 左 + count -= cur->left->val; // 递归,处理节点; + if (traversal(cur->left, count)) return true; + count += cur->left->val; // 回溯,撤销处理结果 + } + if (cur->right) { // 右 + count -= cur->right->val; // 递归,处理节点; + if (traversal(cur->right, count)) return true; + count += cur->right->val; // 回溯,撤销处理结果 + } + return false; + } + +public: + bool hasPathSum(TreeNode* root, int sum) { + if (root == NULL) return false; + return traversal(root, sum - root->val); + } +}; +``` + +以上代码精简之后如下: + +```cpp +class Solution { +public: + bool hasPathSum(TreeNode* root, int sum) { + if (!root) return false; + if (!root->left && !root->right && sum == root->val) { + return true; + } + return hasPathSum(root->left, sum - root->val) || hasPathSum(root->right, sum - root->val); + } +}; +``` + +**是不是发现精简之后的代码,已经完全看不出分析的过程了,所以我们要把题目分析清楚之后,再追求代码精简。** 这一点我已经强调很多次了! + + +### 迭代 + +如果使用栈模拟递归的话,那么如果做回溯呢? + +**此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。** + +c++就我们用pair结构来存放这个栈里的元素。 + +定义为:`pair` pair<节点指针,路径数值> + +这个为栈里的一个元素。 + +如下代码是使用栈模拟的前序遍历,如下:(详细注释) + +```cpp +class solution { + +public: + bool haspathsum(TreeNode* root, int sum) { + if (root == null) return false; + // 此时栈里要放的是pair<节点指针,路径数值> + stack> st; + st.push(pair(root, root->val)); + while (!st.empty()) { + pair node = st.top(); + st.pop(); + // 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true + if (!node.first->left && !node.first->right && sum == node.second) return true; + + // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if (node.first->right) { + st.push(pair(node.first->right, node.second + node.first->right->val)); + } + + // 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if (node.first->left) { + st.push(pair(node.first->left, node.second + node.first->left->val)); + } + } + return false; + } +}; +``` + +如果大家完全理解了本题的递归方法之后,就可以顺便把leetcode上113. 路径总和ii做了。 + +## 相关题目推荐 + +### 113. 路径总和ii + +[力扣题目链接](https://leetcode.cn/problems/path-sum-ii/) + +给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。 + +说明: 叶子节点是指没有子节点的节点。 + +示例: +给定如下二叉树,以及目标和 sum = 22, + + +![113.路径总和ii1.png](https://file1.kamacoder.com/i/algo/20210203160854654.png) + +### 思路 + + +113.路径总和ii要遍历整个树,找到所有路径,**所以递归函数不要返回值!** + +如图: + +![113.路径总和ii](https://file1.kamacoder.com/i/algo/20210203160922745.png) + + +为了尽可能的把细节体现出来,我写出如下代码(**这份代码并不简洁,但是逻辑非常清晰**) + +```cpp +class solution { +private: + vector> result; + vector path; + // 递归函数不需要返回值,因为我们要遍历整个树 + void traversal(TreeNode* cur, int count) { + if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点且找到了和为sum的路径 + result.push_back(path); + return; + } + + if (!cur->left && !cur->right) return ; // 遇到叶子节点而没有找到合适的边,直接返回 + + if (cur->left) { // 左 (空节点不遍历) + path.push_back(cur->left->val); + count -= cur->left->val; + traversal(cur->left, count); // 递归 + count += cur->left->val; // 回溯 + path.pop_back(); // 回溯 + } + if (cur->right) { // 右 (空节点不遍历) + path.push_back(cur->right->val); + count -= cur->right->val; + traversal(cur->right, count); // 递归 + count += cur->right->val; // 回溯 + path.pop_back(); // 回溯 + } + return ; + } + +public: + vector> pathSum(TreeNode* root, int sum) { + result.clear(); + path.clear(); + if (root == NULL) return result; + path.push_back(root->val); // 把根节点放进路径 + traversal(root, sum - root->val); + return result; + } +}; +``` + +至于113. 路径总和ii 的迭代法我并没有写,用迭代方式记录所有路径比较麻烦,也没有必要,如果大家感兴趣的话,可以再深入研究研究。 + +### 总结 + +本篇通过leetcode上112. 路径总和 和 113. 路径总和ii 详细的讲解了 递归函数什么时候需要返回值,什么不需要返回值。 + +这两道题目是掌握这一知识点非常好的题目,大家看完本篇文章再去做题,就会感受到搜索整棵树和搜索某一路径的差别。 + +对于112. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了! + + + + +## 其他语言版本 + +### Java + +0112.路径总和 + +```java +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { + if (root == null) { + return false; + } + targetSum -= root.val; + // 叶子结点 + if (root.left == null && root.right == null) { + return targetSum == 0; + } + if (root.left != null) { + boolean left = hasPathSum(root.left, targetSum); + if (left) { // 已经找到,提前返回 + return true; + } + } + if (root.right != null) { + boolean right = hasPathSum(root.right, targetSum); + if (right) { // 已经找到,提前返回 + return true; + } + } + return false; + } +} + +// lc112 简洁方法 +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { + + if (root == null) return false; // 为空退出 + + // 叶子节点判断是否符合 + if (root.left == null && root.right == null) return root.val == targetSum; + + // 求两侧分支的路径和 + return hasPathSum(root.left, targetSum - root.val) || hasPathSum(root.right, targetSum - root.val); + } +} +``` + +迭代 + +```java +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { + if(root == null) return false; + Stack stack1 = new Stack<>(); + Stack stack2 = new Stack<>(); + stack1.push(root); + stack2.push(root.val); + while(!stack1.isEmpty()) { + int size = stack1.size(); + + for(int i = 0; i < size; i++) { + TreeNode node = stack1.pop(); + int sum = stack2.pop(); + + // 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true + if(node.left == null && node.right == null && sum == targetSum) { + return true; + } + // 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if(node.right != null){ + stack1.push(node.right); + stack2.push(sum + node.right.val); + } + // 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if(node.left != null) { + stack1.push(node.left); + stack2.push(sum + node.left.val); + } + } + } + return false; + } +} +``` +```Java +class Solution { + public boolean hasPathSum(TreeNode root, int targetSum) { + Stack treeNodeStack = new Stack<>(); + Stack sumStack = new Stack<>(); + + if(root == null) + return false; + treeNodeStack.add(root); + sumStack.add(root.val); + + while(!treeNodeStack.isEmpty()){ + TreeNode curr = treeNodeStack.peek(); + int tempsum = sumStack.pop(); + if(curr != null){ + treeNodeStack.pop(); + treeNodeStack.add(curr); + treeNodeStack.add(null); + sumStack.add(tempsum); + if(curr.right != null){ + treeNodeStack.add(curr.right); + sumStack.add(tempsum + curr.right.val); + } + if(curr.left != null){ + treeNodeStack.add(curr.left); + sumStack.add(tempsum + curr.left.val); + } + }else{ + treeNodeStack.pop(); + TreeNode temp = treeNodeStack.pop(); + if(temp.left == null && temp.right == null && tempsum == targetSum) + return true; + } + } + return false; + } +} +``` + +0113.路径总和-ii + +```java +class Solution { + public List> pathSum(TreeNode root, int targetSum) { + List> res = new ArrayList<>(); + if (root == null) return res; // 非空判断 + + List path = new LinkedList<>(); + preOrderDfs(root, targetSum, res, path); + return res; + } + + public void preOrderDfs(TreeNode root, int targetSum, List> res, List path) { + path.add(root.val); + // 遇到了叶子节点 + if (root.left == null && root.right == null) { + // 找到了和为 targetsum 的路径 + if (targetSum - root.val == 0) { + res.add(new ArrayList<>(path)); + } + return; // 如果和不为 targetsum,返回 + } + + if (root.left != null) { + preOrderDfs(root.left, targetSum - root.val, res, path); + path.remove(path.size() - 1); // 回溯 + } + if (root.right != null) { + preOrderDfs(root.right, targetSum - root.val, res, path); + path.remove(path.size() - 1); // 回溯 + } + } +} +``` + +```java +// 解法2 +class Solution { + List> result; + LinkedList path; + public List> pathSum (TreeNode root,int targetSum) { + result = new LinkedList<>(); + path = new LinkedList<>(); + travesal(root, targetSum); + return result; + } + private void travesal(TreeNode root, int count) { + if (root == null) return; + path.offer(root.val); + count -= root.val; + if (root.left == null && root.right == null && count == 0) { + result.add(new LinkedList<>(path)); + } + travesal(root.left, count); + travesal(root.right, count); + path.removeLast(); // 回溯 + } +} +``` +```java +// 解法3 DFS统一迭代法 +class Solution { + public List> pathSum(TreeNode root, int targetSum) { + List> result = new ArrayList<>(); + Stack nodeStack = new Stack<>(); + Stack sumStack = new Stack<>(); + Stack> pathStack = new Stack<>(); + if(root == null) + return result; + nodeStack.add(root); + sumStack.add(root.val); + pathStack.add(new ArrayList<>()); + + while(!nodeStack.isEmpty()){ + TreeNode currNode = nodeStack.peek(); + int currSum = sumStack.pop(); + ArrayList currPath = pathStack.pop(); + if(currNode != null){ + nodeStack.pop(); + nodeStack.add(currNode); + nodeStack.add(null); + sumStack.add(currSum); + currPath.add(currNode.val); + pathStack.add(new ArrayList(currPath)); + if(currNode.right != null){ + nodeStack.add(currNode.right); + sumStack.add(currSum + currNode.right.val); + pathStack.add(new ArrayList(currPath)); + } + if(currNode.left != null){ + nodeStack.add(currNode.left); + sumStack.add(currSum + currNode.left.val); + pathStack.add(new ArrayList(currPath)); + } + }else{ + nodeStack.pop(); + TreeNode temp = nodeStack.pop(); + if(temp.left == null && temp.right == null && currSum == targetSum) + result.add(new ArrayList(currPath)); + } + } + return result; + } +} +``` + +### Python + +0112.路径总和 + +(版本一) 递归 +```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 traversal(self, cur: TreeNode, count: int) -> bool: + if not cur.left and not cur.right and count == 0: # 遇到叶子节点,并且计数为0 + return True + if not cur.left and not cur.right: # 遇到叶子节点直接返回 + return False + + if cur.left: # 左 + count -= cur.left.val + if self.traversal(cur.left, count): # 递归,处理节点 + return True + count += cur.left.val # 回溯,撤销处理结果 + + if cur.right: # 右 + count -= cur.right.val + if self.traversal(cur.right, count): # 递归,处理节点 + return True + count += cur.right.val # 回溯,撤销处理结果 + + return False + + def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + if root is None: + return False + return self.traversal(root, targetSum - root.val) +``` + +(版本二) 递归 + 精简 +```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 hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + if not root: + return False + if not root.left and not root.right and targetSum == root.val: + return True + return self.hasPathSum(root.left, targetSum - root.val) or self.hasPathSum(root.right, targetSum - root.val) + +``` +(版本三) 迭代 +```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 hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool: + if not root: + return False + # 此时栈里要放的是pair<节点指针,路径数值> + st = [(root, root.val)] + while st: + node, path_sum = st.pop() + # 如果该节点是叶子节点了,同时该节点的路径数值等于sum,那么就返回true + if not node.left and not node.right and path_sum == sum: + return True + # 右节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if node.right: + st.append((node.right, path_sum + node.right.val)) + # 左节点,压进去一个节点的时候,将该节点的路径数值也记录下来 + if node.left: + st.append((node.left, path_sum + node.left.val)) + return False + + + +``` + + + +0113.路径总和-ii + +(版本一) 递归 +```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 __init__(self): + self.result = [] + self.path = [] + + def traversal(self, cur, count): + if not cur.left and not cur.right and count == 0: # 遇到了叶子节点且找到了和为sum的路径 + self.result.append(self.path[:]) + return + + if not cur.left and not cur.right: # 遇到叶子节点而没有找到合适的边,直接返回 + return + + if cur.left: # 左 (空节点不遍历) + self.path.append(cur.left.val) + count -= cur.left.val + self.traversal(cur.left, count) # 递归 + count += cur.left.val # 回溯 + self.path.pop() # 回溯 + + if cur.right: # 右 (空节点不遍历) + self.path.append(cur.right.val) + count -= cur.right.val + self.traversal(cur.right, count) # 递归 + count += cur.right.val # 回溯 + self.path.pop() # 回溯 + + return + + def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]: + self.result.clear() + self.path.clear() + if not root: + return self.result + self.path.append(root.val) # 把根节点放进路径 + self.traversal(root, targetSum - root.val) + return self.result +``` + +(版本二) 递归 + 精简 +```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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]: + + result = [] + self.traversal(root, targetSum, [], result) + return result + def traversal(self,node, count, path, result): + if not node: + return + path.append(node.val) + count -= node.val + if not node.left and not node.right and count == 0: + result.append(list(path)) + self.traversal(node.left, count, path, result) + self.traversal(node.right, count, path, result) + path.pop() +``` +(版本三) 迭代 +```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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]: + if not root: + return [] + stack = [(root, [root.val])] + res = [] + while stack: + node, path = stack.pop() + if not node.left and not node.right and sum(path) == targetSum: + res.append(path) + if node.right: + stack.append((node.right, path + [node.right.val])) + if node.left: + stack.append((node.left, path + [node.left.val])) + return res + + + +``` +### Go + +112. 路径总和 + +```go +//递归法 +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func hasPathSum(root *TreeNode, targetSum int) bool { + if root == nil { + return false + } + return traversal(root, targetSum - root.Val) +} + +func traversal(cur *TreeNode, count int) bool { + if cur.Left == nil && cur.Right == nil && count == 0 { + return true + } + if cur.Left == nil && cur.Right == nil { + return false + } + if cur.Left != nil { + count -= cur.Left.Val + if traversal(cur.Left, count) { + return true + } + count += cur.Left.Val + } + if cur.Right != nil { + count -= cur.Right.Val + if traversal(cur.Right, count) { + return true + } + count += cur.Right.Val + } + return false +} +``` + +```go +//递归法精简 +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func hasPathSum(root *TreeNode, targetSum int) bool { + if root == nil { + return false + } + + targetSum -= root.Val // 将targetSum在遍历每层的时候都减去本层节点的值 + if root.Left == nil && root.Right == nil && targetSum == 0 { // 如果剩余的targetSum为0, 则正好就是符合的结果 + return true + } + return hasPathSum(root.Left, targetSum) || hasPathSum(root.Right, targetSum) // 否则递归找 +} +``` + +113. 路径总和 II + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func pathSum(root *TreeNode, targetSum int) [][]int { + result := make([][]int, 0) + traverse(root, &result, new([]int), targetSum) + return result +} + +func traverse(node *TreeNode, result *[][]int, currPath *[]int, targetSum int) { + if node == nil { // 这个判空也可以挪到递归遍历左右子树时去判断 + return + } + + targetSum -= node.Val // 将targetSum在遍历每层的时候都减去本层节点的值 + *currPath = append(*currPath, node.Val) // 把当前节点放到路径记录里 + + if node.Left == nil && node.Right == nil && targetSum == 0 { // 如果剩余的targetSum为0, 则正好就是符合的结果 + // 不能直接将currPath放到result里面, 因为currPath是共享的, 每次遍历子树时都会被修改 + pathCopy := make([]int, len(*currPath)) + for i, element := range *currPath { + pathCopy[i] = element + } + *result = append(*result, pathCopy) // 将副本放到结果集里 + } + + traverse(node.Left, result, currPath, targetSum) + traverse(node.Right, result, currPath, targetSum) + *currPath = (*currPath)[:len(*currPath)-1] // 当前节点遍历完成, 从路径记录里删除掉 +} +``` + +### JavaScript + +0112.路径总和 + +**递归** + +```javascript +/** + * @param {treenode} root + * @param {number} targetsum + * @return {boolean} + */ +let haspathsum = function (root, targetsum) { + // 递归法 + const traversal = (node, cnt) => { + // 遇到叶子节点,并且计数为0 + if (cnt === 0 && !node.left && !node.right) return true; + // 遇到叶子节点而没有找到合适的边(计数不为0),直接返回 + if (!node.left && !node.right) return false; + + // 左(空节点不遍历).遇到叶子节点返回true,则直接返回true + if (node.left && traversal(node.left, cnt - node.left.val)) return true; + // 右(空节点不遍历) + if (node.right && traversal(node.right, cnt - node.right.val)) return true; + return false; + }; + if (!root) return false; + return traversal(root, targetsum - root.val); + + // 精简代码: + // if (!root) return false; + // if (!root.left && !root.right && targetsum === root.val) return true; + // return haspathsum(root.left, targetsum - root.val) || haspathsum(root.right, targetsum - root.val); +}; +``` + +**迭代** + +```javascript +let hasPathSum = function(root, targetSum) { + if(root === null) return false; + let nodeArr = [root]; + let valArr = [0]; + while(nodeArr.length) { + let curNode = nodeArr.shift(); + let curVal = valArr.shift(); + curVal += curNode.val; + // 为叶子结点,且和等于目标数,返回true + if (curNode.left === null && curNode.right === null && curVal === targetSum) { + return true; + } + // 左节点,将当前的数值也对应记录下来 + if (curNode.left) { + nodeArr.push(curNode.left); + valArr.push(curVal); + } + // 右节点,将当前的数值也对应记录下来 + if (curNode.right) { + nodeArr.push(curNode.right); + valArr.push(curVal); + } + } + return false; +}; +``` + +0113.路径总和-ii + +**递归** + +```javascript +let pathsum = function (root, targetsum) { + // 递归法 + // 要遍历整个树找到所有路径,所以递归函数不需要返回值, 与112不同 + const res = []; + const travelsal = (node, cnt, path) => { + // 遇到了叶子节点且找到了和为sum的路径 + if (cnt === 0 && !node.left && !node.right) { + res.push([...path]); // 不能写res.push(path), 要深拷贝 + return; + } + if (!node.left && !node.right) return; // 遇到叶子节点而没有找到合适的边,直接返回 + // 左 (空节点不遍历) + if (node.left) { + path.push(node.left.val); + travelsal(node.left, cnt - node.left.val, path); // 递归 + path.pop(); // 回溯 + } + // 右 (空节点不遍历) + if (node.right) { + path.push(node.right.val); + travelsal(node.right, cnt - node.right.val, path); // 递归 + path.pop(); // 回溯 + } + return; + }; + if (!root) return res; + travelsal(root, targetsum - root.val, [root.val]); // 把根节点放进路径 + return res; +}; +``` + +**递归 精简版** + +```javascript +var pathsum = function(root, targetsum) { + //递归方法 + let respath = [],curpath = []; + // 1. 确定递归函数参数 + const traveltree = function(node,count) { + curpath.push(node.val); + count -= node.val; + if(node.left === null && node.right === null && count === 0) { + respath.push([...curpath]); + } + node.left && traveltree(node.left, count); + node.right && traveltree(node.right, count); + let cur = curpath.pop(); + count -= cur; + } + if(root === null) { + return respath; + } + travelTree(root, targetSum); + return resPath; +}; +``` + +**迭代** + +```javascript +let pathSum = function(root, targetSum) { + if(root === null) return []; + let nodeArr = [root]; + let resArr = []; // 记录符合目标和的返回路径 + let tempArr = [[]]; // 对应路径 + let countArr = [0]; //对应和 + while(nodeArr.length) { + let curNode = nodeArr.shift(); + let curVal = countArr.shift(); + let curNodeArr = tempArr.shift(); + curVal += curNode.val; + curNodeArr.push(curNode.val); + // 为叶子结点,且和等于目标数,将此次结果数组push进返回数组中 + if (curNode.left === null && curNode.right === null && curVal === targetSum) { + resArr.push(curNodeArr); + } + // 左节点,将当前的和及对应路径也对应记录下来 + if (curNode.left) { + nodeArr.push(curNode.left); + countArr.push(curVal); + tempArr.push([...curNodeArr]); + } + // 右节点,将当前的和及对应路径也对应记录下来 + if (curNode.right) { + nodeArr.push(curNode.right); + countArr.push(curVal); + tempArr.push([...curNodeArr]); + } + } + return resArr; +}; +``` + +### TypeScript + +0112.路径总和 + +**递归法:** + +```typescript +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { + function recur(node: TreeNode, sum: number): boolean { + console.log(sum); + if ( + node.left === null && + node.right === null && + sum === 0 + ) return true; + if (node.left !== null) { + sum -= node.left.val; + if (recur(node.left, sum) === true) return true; + sum += node.left.val; + } + if (node.right !== null) { + sum -= node.right.val; + if (recur(node.right, sum) === true) return true; + sum += node.right.val; + } + return false; + } + if (root === null) return false; + return recur(root, targetSum - root.val); +}; +``` + +**递归法(精简版):** + +```typescript +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { + if (root === null) return false; + targetSum -= root.val; + if ( + root.left === null && + root.right === null && + targetSum === 0 + ) return true; + return hasPathSum(root.left, targetSum) || + hasPathSum(root.right, targetSum); +}; +``` + +**迭代法:** + +```typescript +function hasPathSum(root: TreeNode | null, targetSum: number): boolean { + type Pair = { + node: TreeNode, // 当前节点 + sum: number // 根节点到当前节点的路径数值总和 + } + + const helperStack: Pair[] = []; + if (root !== null) helperStack.push({ node: root, sum: root.val }); + let tempPair: Pair; + while (helperStack.length > 0) { + tempPair = helperStack.pop()!; + if ( + tempPair.node.left === null && + tempPair.node.right === null && + tempPair.sum === targetSum + ) return true; + if (tempPair.node.right !== null) { + helperStack.push({ + node: tempPair.node.right, + sum: tempPair.sum + tempPair.node.right.val + }); + } + if (tempPair.node.left !== null) { + helperStack.push({ + node: tempPair.node.left, + sum: tempPair.sum + tempPair.node.left.val + }); + } + } + return false; +}; +``` + +0112.路径总和 ii + +**递归法:** + +```typescript +function pathSum(root: TreeNode | null, targetSum: number): number[][] { + function recur(node: TreeNode, sumGap: number, routeArr: number[]): void { + if ( + node.left === null && + node.right === null && + sumGap === 0 + ) resArr.push([...routeArr]); + if (node.left !== null) { + sumGap -= node.left.val; + routeArr.push(node.left.val); + recur(node.left, sumGap, routeArr); + sumGap += node.left.val; + routeArr.pop(); + } + if (node.right !== null) { + sumGap -= node.right.val; + routeArr.push(node.right.val); + recur(node.right, sumGap, routeArr); + sumGap += node.right.val; + routeArr.pop(); + } + } + const resArr: number[][] = []; + if (root === null) return resArr; + const routeArr: number[] = []; + routeArr.push(root.val); + recur(root, targetSum - root.val, routeArr); + return resArr; +}; +``` + +### Swift + +0112.路径总和 + +**递归** + +```swift +func hasPathSum(_ root: TreeNode?, _ targetSum: Int) -> Bool { + guard let root = root else { + return false + } + + return traversal(root, targetSum - root.val) +} + +func traversal(_ cur: TreeNode?, _ count: Int) -> Bool { + if cur?.left == nil && cur?.right == nil && count == 0 { + return true + } + + if cur?.left == nil && cur?.right == nil { + return false + } + + if let leftNode = cur?.left { + if traversal(leftNode, count - leftNode.val) { + return true + } + } + + if let rightNode = cur?.right { + if traversal(rightNode, count - rightNode.val) { + return true + } + } + + return false +} +``` + +**迭代** + +```swift +func hasPathSum(_ root: TreeNode?, _ targetSum: Int) -> Bool { + guard let root = root else { + return false + } + + var stack = Array<(TreeNode, Int)>() + stack.append((root, root.val)) + + while !stack.isEmpty { + let node = stack.removeLast() + + if node.0.left == nil && node.0.right == nil && targetSum == node.1 { + return true + } + + if let rightNode = node.0.right { + stack.append((rightNode, node.1 + rightNode.val)) + } + + if let leftNode = node.0.left { + stack.append((leftNode, node.1 + leftNode.val)) + } + } + + return false +} +``` + +0113.路径总和 II + +**递归** + +```swift +var result = [[Int]]() +var path = [Int]() +func pathSum(_ root: TreeNode?, _ targetSum: Int) -> [[Int]] { + result.removeAll() + path.removeAll() + guard let root = root else { + return result + } + path.append(root.val) + traversal(root, count: targetSum - root.val) + return result + +} + +func traversal(_ cur: TreeNode?, count: Int) { + var count = count + // 遇到了叶子节点且找到了和为targetSum的路径 + if cur?.left == nil && cur?.right == nil && count == 0 { + result.append(path) + return + } + + // 遇到叶子节点而没有找到合适的边,直接返回 + if cur?.left == nil && cur?.right == nil{ + return + } + + if let leftNode = cur?.left { + path.append(leftNode.val) + count -= leftNode.val + traversal(leftNode, count: count)// 递归 + count += leftNode.val// 回溯 + path.removeLast()// 回溯 + } + + if let rightNode = cur?.right { + path.append(rightNode.val) + count -= rightNode.val + traversal(rightNode, count: count)// 递归 + count += rightNode.val// 回溯 + path.removeLast()// 回溯 + } + return +} +``` + +### C + +0112.路径总和 + +递归法: + +```c +bool hasPathSum(struct TreeNode* root, int targetSum){ + // 递归结束条件:若当前节点不存在,返回false + if(!root) + return false; + // 若当前节点为叶子节点,且targetSum-root的值为0。(当前路径上的节点值的和满足条件)返回true + if(!root->right && !root->left && targetSum == root->val) + return true; + + // 查看左子树和右子树的所有节点是否满足条件 + return hasPathSum(root->right, targetSum - root->val) || hasPathSum(root->left, targetSum - root->val); +} +``` + +迭代法: + +```c +// 存储一个节点以及当前的和 +struct Pair { + struct TreeNode* node; + int sum; +}; + +bool hasPathSum(struct TreeNode* root, int targetSum){ + struct Pair stack[1000]; + int stackTop = 0; + + // 若root存在,则将节点和值封装成一个pair入栈 + if(root) { + struct Pair newPair = {root, root->val}; + stack[stackTop++] = newPair; + } + + // 当栈不为空时 + while(stackTop) { + // 出栈栈顶元素 + struct Pair topPair = stack[--stackTop]; + // 若栈顶元素为叶子节点,且和为targetSum时,返回true + if(!topPair.node->left && !topPair.node->right && topPair.sum == targetSum) + return true; + + // 若当前栈顶节点有左右孩子,计算和并入栈 + if(topPair.node->left) { + struct Pair newPair = {topPair.node->left, topPair.sum + topPair.node->left->val}; + stack[stackTop++] = newPair; + } + if(topPair.node->right) { + struct Pair newPair = {topPair.node->right, topPair.sum + topPair.node->right->val}; + stack[stackTop++] = newPair; + } + } + return false; +} +``` + +0113.路径总和 II + +```c +int** ret; +int* path; +int* colSize; +int retTop; +int pathTop; + +void traversal(const struct TreeNode* const node, int count) { + // 若当前节点为叶子节点 + if(!node->right && !node->left) { + // 若当前path上的节点值总和等于targetSum。 + if(count == 0) { + // 复制当前path + int *curPath = (int*)malloc(sizeof(int) * pathTop); + memcpy(curPath, path, sizeof(int) * pathTop); + // 记录当前path的长度为pathTop + colSize[retTop] = pathTop; + // 将当前path加入到ret数组中 + ret[retTop++] = curPath; + } + return; + } + + // 若节点有左/右孩子 + if(node->left) { + // 将左孩子的值加入path中 + path[pathTop++] = node->left->val; + traversal(node->left, count - node->left->val); + // 回溯 + pathTop--; + } + if(node->right) { + // 将右孩子的值加入path中 + path[pathTop++] = node->right->val; + traversal(node->right, count - node->right->val); + // 回溯 + --pathTop; + } +} + +int** pathSum(struct TreeNode* root, int targetSum, int* returnSize, int** returnColumnSizes){ + // 初始化数组 + ret = (int**)malloc(sizeof(int*) * 1000); + path = (int*)malloc(sizeof(int*) * 1000); + colSize = (int*)malloc(sizeof(int) * 1000); + retTop = pathTop = 0; + *returnSize = 0; + + // 若根节点不存在,返回空的ret + if(!root) + return ret; + // 将根节点加入到path中 + path[pathTop++] = root->val; + traversal(root, targetSum - root->val); + + // 设置返回ret数组大小,以及其中每个一维数组元素的长度 + *returnSize = retTop; + *returnColumnSizes = colSize; + + return ret; +} +``` + +### Scala + +0112.路径总和 + +**递归:** + +```scala +object Solution { + def hasPathSum(root: TreeNode, targetSum: Int): Boolean = { + if(root == null) return false + var res = false + + def traversal(curNode: TreeNode, sum: Int): Unit = { + if (res) return // 如果直接标记为true了,就没有往下递归的必要了 + if (curNode.left == null && curNode.right == null && sum == targetSum) { + res = true + return + } + // 往下递归 + if (curNode.left != null) traversal(curNode.left, sum + curNode.left.value) + if (curNode.right != null) traversal(curNode.right, sum + curNode.right.value) + } + + traversal(root, root.value) + res // return关键字可以省略 + } +} +``` + +**迭代:** + +```scala +object Solution { + import scala.collection.mutable + def hasPathSum(root: TreeNode, targetSum: Int): Boolean = { + if (root == null) return false + val stack = mutable.Stack[(TreeNode, Int)]() + stack.push((root, root.value)) // 将根节点元素放入stack + while (!stack.isEmpty) { + val curNode = stack.pop() // 取出栈顶元素 + // 如果遇到叶子节点,看当前的值是否等于targetSum,等于则返回true + if (curNode._1.left == null && curNode._1.right == null && curNode._2 == targetSum) { + return true + } + if (curNode._1.right != null) stack.push((curNode._1.right, curNode._2 + curNode._1.right.value)) + if (curNode._1.left != null) stack.push((curNode._1.left, curNode._2 + curNode._1.left.value)) + } + false //如果没有返回true,即可返回false,return关键字可以省略 + } +} +``` + +0113.路径总和 II + +**递归:** + +```scala +object Solution { + import scala.collection.mutable.ListBuffer + def pathSum(root: TreeNode, targetSum: Int): List[List[Int]] = { + val res = ListBuffer[List[Int]]() + if (root == null) return res.toList + val path = ListBuffer[Int](); + + def traversal(cur: TreeNode, count: Int): Unit = { + if (cur.left == null && cur.right == null && count == 0) { + res.append(path.toList) + return + } + if (cur.left != null) { + path.append(cur.left.value) + traversal(cur.left, count - cur.left.value) + path.remove(path.size - 1) + } + if (cur.right != null) { + path.append(cur.right.value) + traversal(cur.right, count - cur.right.value) + path.remove(path.size - 1) + } + } + + path.append(root.value) + traversal(root, targetSum - root.value) + res.toList + } +} +``` + +### Rust + +0112.路径总和 + +递归: + +```rust +use std::rc::Rc; +use std::cell::RefCell; +impl Solution { + pub fn has_path_sum(root: Option>>, target_sum: i32) -> bool { + if root.is_none() { + return false; + } + let node = root.unwrap(); + if node.borrow().left.is_none() && node.borrow().right.is_none() { + return node.borrow().val == target_sum; + } + return Self::has_path_sum(node.borrow().left.clone(), target_sum - node.borrow().val) + || Self::has_path_sum(node.borrow().right.clone(), target_sum - node.borrow().val); + } +} +``` + +迭代: + +```rust +use std::rc::Rc; +use std::cell::RefCell; +impl Solution { + pub fn has_path_sum(root: Option>>, target_sum: i32) -> bool { + let mut stack = vec![]; + if let Some(node) = root { + stack.push((node.borrow().val, node.to_owned())); + } + while !stack.is_empty() { + let (value, node) = stack.pop().unwrap(); + if node.borrow().left.is_none() && node.borrow().right.is_none() && value == target_sum + { + return true; + } + if node.borrow().left.is_some() { + if let Some(r) = node.borrow().left.as_ref() { + stack.push((r.borrow().val + value, r.to_owned())); + } + } + if node.borrow().right.is_some() { + if let Some(r) = node.borrow().right.as_ref() { + stack.push((r.borrow().val + value, r.to_owned())); + } + } + } + false + } +``` + +0113.路径总和-ii + +```rust +impl Solution { + pub fn path_sum(root: Option>>, target_sum: i32) -> Vec> { + let mut res = vec![]; + let mut route = vec![]; + if root.is_none() { + return res; + } else { + route.push(root.as_ref().unwrap().borrow().val); + } + Self::recur( + &root, + target_sum - root.as_ref().unwrap().borrow().val, + &mut res, + &mut route, + ); + res + } + + pub fn recur( + root: &Option>>, + sum: i32, + res: &mut Vec>, + route: &mut Vec, + ) { + let node = root.as_ref().unwrap().borrow(); + if node.left.is_none() && node.right.is_none() && sum == 0 { + res.push(route.to_vec()); + return; + } + if node.left.is_some() { + let left = node.left.as_ref().unwrap(); + route.push(left.borrow().val); + Self::recur(&node.left, sum - left.borrow().val, res, route); + route.pop(); + } + if node.right.is_some() { + let right = node.right.as_ref().unwrap(); + route.push(right.borrow().val); + Self::recur(&node.right, sum - right.borrow().val, res, route); + route.pop(); + } + } +} + +``` +### C# +```csharp +// 0112.路径总和 +// 递归 +public bool HasPathSum(TreeNode root, int targetSum) +{ + if (root == null) return false; + if (root.left == null && root.right == null && targetSum == root.val) return true; + return HasPathSum(root.left, targetSum - root.val) || HasPathSum(root.right, targetSum - root.val); +} +``` +0113.路径总和: +```csharp +/* + * @lc app=leetcode id=113 lang=csharp + * 0113.路径总和 II + * [113] Path Sum II + * 递归法 + */ +public class Solution { + private List> result = new List>(); + private List path = new List(); + + // Main function to find paths with the given sum + public IList> PathSum(TreeNode root, int targetSum) { + result.Clear(); + path.Clear(); + if (root == null) return result.Select(list => list as IList).ToList(); + path.Add(root.val); // Add the root node to the path + traversal(root, targetSum - root.val); // Start the traversal + return result.Select(list => list as IList).ToList(); + } + + // Recursive function to traverse the tree and find paths + private void traversal(TreeNode node, int count) { + // If a leaf node is reached and the target sum is achieved + if (node.left == null && node.right == null && count == 0) { + result.Add(new List(path)); // Add a copy of the path to the result + return; + } + + // If a leaf node is reached and the target sum is not achieved, or if it's not a leaf node + if (node.left == null && node.right == null) return; + + // Traverse the left subtree + if (node.left != null) { + path.Add(node.left.val); + count -= node.left.val; + traversal(node.left, count); // Recursive call + count += node.left.val; // Backtrack + path.RemoveAt(path.Count - 1); // Backtrack + } + + // Traverse the right subtree + if (node.right != null) { + path.Add(node.right.val); + count -= node.right.val; + traversal(node.right, count); // Recursive call + count += node.right.val; // Backtrack + path.RemoveAt(path.Count - 1); // Backtrack + } + } +} + +// @lc code=end + +``` + + diff --git "a/problems/0115.\344\270\215\345\220\214\347\232\204\345\255\220\345\272\217\345\210\227.md" "b/problems/0115.\344\270\215\345\220\214\347\232\204\345\255\220\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..499bf100e2 --- /dev/null +++ "b/problems/0115.\344\270\215\345\220\214\347\232\204\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,376 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 115.不同的子序列 + +[力扣题目链接](https://leetcode.cn/problems/distinct-subsequences/) + +给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 + +字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是) + +题目数据保证答案符合 32 位带符号整数范围。 + +![115.不同的子序列示例](https://file1.kamacoder.com/i/algo/115.不同的子序列示例.jpg) + +提示: + +* 0 <= s.length, t.length <= 1000 +* s 和 t 由英文字母组成 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划之子序列,为了编辑距离做铺垫 | LeetCode:115.不同的子序列](https://www.bilibili.com/video/BV1fG4y1m75Q/),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这道题目如果不是子序列,而是要求连续序列的,那就可以考虑用KMP。 + +这道题目相对于72. 编辑距离,简单了不少,因为本题相当于只有删除操作,不用考虑替换增加之类的。 + +但相对于刚讲过的[动态规划:392.判断子序列](https://programmercarl.com/0392.判断子序列.html)就有难度了,这道题目双指针法可就做不了了,来看看动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。 + +为什么i-1,j-1 这么定义我在 [718. 最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html) 中做了详细的讲解。 + +2. 确定递推公式 + +这一类问题,基本是要分析两种情况 + +* s[i - 1] 与 t[j - 1]相等 +* s[i - 1] 与 t[j - 1] 不相等 + +当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。 + +一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。即不需要考虑当前s子串和t子串的最后一位字母,所以只需要 dp[i-1][j-1]。 + +一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。 + +**这里可能有录友不明白了,为什么还要考虑 不用s[i - 1]来匹配,都相同了指定要匹配啊**。 + +例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。 + +当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。 + +所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; + +当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配(就是模拟在s中删除这个元素),即:dp[i - 1][j] + +所以递推公式为:dp[i][j] = dp[i - 1][j]; + +这里可能有录友还疑惑,为什么只考虑 “不用s[i - 1]来匹配” 这种情况, 不考虑 “不用t[j - 1]来匹配” 的情况呢。 + +这里大家要明确,我们求的是 s 中有多少个 t,而不是 求t中有多少个s,所以只考虑 s中删除元素的情况,即 不用s[i - 1]来匹配 的情况。 + +3. dp数组如何初始化 + +从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j] 是从上方和左上方推导而来,如图:,那么 dp[i][0] 和dp[0][j]是一定要初始化的。 + +![](https://file1.kamacoder.com/i/algo/20221222165412.png) + +每次当初始化的时候,都要回顾一下dp[i][j]的定义,不要凭感觉初始化。 + +dp[i][0]表示什么呢? + +dp[i][0] 表示:以i-1为结尾的s可以随便删除元素,出现空字符串的个数。 + +那么dp[i][0]一定都是1,因为也就是把以i-1为结尾的s,删除所有元素,出现空字符串的个数就是1。 + +再来看dp[0][j],dp[0][j]:空字符串s可以随便删除元素,出现以j-1为结尾的字符串t的个数。 + +那么dp[0][j]一定都是0,s如论如何也变成不了t。 + +最后就要看一个特殊位置了,即:dp[0][0] 应该是多少。 + +dp[0][0]应该是1,空字符串s,可以删除0个元素,变成空字符串t。 + +初始化分析完毕,代码如下: + +```CPP +vector> dp(s.size() + 1, vector(t.size() + 1)); +for (int i = 0; i <= s.size(); i++) dp[i][0] = 1; +for (int j = 1; j <= t.size(); j++) dp[0][j] = 0; // 其实这行代码可以和dp数组初始化的时候放在一起,但我为了凸显初始化的逻辑,所以还是加上了。 + +``` + +4. 确定遍历顺序 + +从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][j]都是根据左上方和正上方推出来的。 + +![](https://file1.kamacoder.com/i/algo/20221222165412.png) + +所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。 + +代码如下: + +```CPP +for (int i = 1; i <= s.size(); i++) { + for (int j = 1; j <= t.size(); j++) { + if (s[i - 1] == t[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; + } else { + dp[i][j] = dp[i - 1][j]; + } + } +} +``` + +5. 举例推导dp数组 + +以s:"baegg",t:"bag"为例,推导dp数组状态如下: + +![115.不同的子序列](https://file1.kamacoder.com/i/algo/115.%E4%B8%8D%E5%90%8C%E7%9A%84%E5%AD%90%E5%BA%8F%E5%88%97.jpg) + +如果写出来的代码怎么改都通过不了,不妨把dp数组打印出来,看一看,是不是这样的。 + + +动规五部曲分析完毕,代码如下: + +```CPP +class Solution { +public: + int numDistinct(string s, string t) { + vector> dp(s.size() + 1, vector(t.size() + 1)); + for (int i = 0; i < s.size(); i++) dp[i][0] = 1; + for (int j = 1; j < t.size(); j++) dp[0][j] = 0; + for (int i = 1; i <= s.size(); i++) { + for (int j = 1; j <= t.size(); j++) { + if (s[i - 1] == t[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; + } else { + dp[i][j] = dp[i - 1][j]; + } + } + } + return dp[s.size()][t.size()]; + } +}; +``` + +* 时间复杂度: O(n * m) +* 空间复杂度: O(n * m) + + + +## 其他语言版本 + +### Java: + +```java +class Solution { + public int numDistinct(String s, String t) { + int[][] dp = new int[s.length() + 1][t.length() + 1]; + for (int i = 0; i < s.length() + 1; i++) { + dp[i][0] = 1; + } + + for (int i = 1; i < s.length() + 1; i++) { + for (int j = 1; j < t.length() + 1; j++) { + if (s.charAt(i - 1) == t.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; + }else{ + dp[i][j] = dp[i - 1][j]; + } + } + } + + return dp[s.length()][t.length()]; + } +} +``` + +### Python: + +```python +class Solution: + def numDistinct(self, s: str, t: str) -> int: + dp = [[0] * (len(t)+1) for _ in range(len(s)+1)] + for i in range(len(s)): + dp[i][0] = 1 + for j in range(1, len(t)): + dp[0][j] = 0 + for i in range(1, len(s)+1): + for j in range(1, len(t)+1): + if s[i-1] == t[j-1]: + dp[i][j] = dp[i-1][j-1] + dp[i-1][j] + else: + dp[i][j] = dp[i-1][j] + return dp[-1][-1] +``` + +### Python3: + +```python +class SolutionDP2: + """ + 既然dp[i]只用到dp[i - 1]的状态, + 我们可以通过缓存dp[i - 1]的状态来对dp进行压缩, + 减少空间复杂度。 + (原理等同同于滚动数组) + """ + + def numDistinct(self, s: str, t: str) -> int: + n1, n2 = len(s), len(t) + if n1 < n2: + return 0 + + dp = [0 for _ in range(n2 + 1)] + dp[0] = 1 + + for i in range(1, n1 + 1): + # 必须深拷贝 + # 不然prev[i]和dp[i]是同一个地址的引用 + prev = dp.copy() + # 剪枝,保证s的长度大于等于t + # 因为对于任意i,i > n1, dp[i] = 0 + # 没必要跟新状态。 + end = i if i < n2 else n2 + for j in range(1, end + 1): + if s[i - 1] == t[j - 1]: + dp[j] = prev[j - 1] + prev[j] + else: + dp[j] = prev[j] + return dp[-1] +``` + +### Go: + +```go +func numDistinct(s string, t string) int { + dp:= make([][]int,len(s)+1) + for i:=0;i { + let dp = Array.from(Array(s.length + 1), () => Array(t.length +1).fill(0)); + + for(let i = 0; i <=s.length; i++) { + dp[i][0] = 1; + } + + for(let i = 1; i <= s.length; i++) { + for(let j = 1; j<= t.length; j++) { + if(s[i-1] === t[j-1]) { + dp[i][j] = dp[i-1][j-1] + dp[i-1][j]; + } else { + dp[i][j] = dp[i-1][j] + } + } + } + + return dp[s.length][t.length]; +}; +``` + +### TypeScript: + +```typescript +function numDistinct(s: string, t: string): number { + /** + dp[i][j]: s前i个字符,t前j个字符,s子序列中t出现的个数 + dp[0][0]=1, 表示s前0个字符为'',t前0个字符为'' + */ + const sLen: number = s.length, + tLen: number = t.length; + const dp: number[][] = new Array(sLen + 1).fill(0) + .map(_ => new Array(tLen + 1).fill(0)); + for (let m = 0; m < sLen; m++) { + dp[m][0] = 1; + } + for (let i = 1; i <= sLen; i++) { + for (let j = 1; j <= tLen; j++) { + if (s[i - 1] === t[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; + } else { + dp[i][j] = dp[i - 1][j]; + } + } + } + return dp[sLen][tLen]; +}; +``` + +### Rust: + +```rust +impl Solution { + pub fn num_distinct(s: String, t: String) -> i32 { + if s.len() < t.len() { + return 0; + } + let mut dp = vec![vec![0; s.len() + 1]; t.len() + 1]; + // i = 0, t 为空字符串,s 作为子序列的个数为 1(删除 s 所有元素) + dp[0] = vec![1; s.len() + 1]; + for (i, char_t) in t.chars().enumerate() { + for (j, char_s) in s.chars().enumerate() { + if char_t == char_s { + // t 的前 i 个字符在 s 的前 j 个字符中作为子序列的个数 + dp[i + 1][j + 1] = dp[i][j] + dp[i + 1][j]; + continue; + } + dp[i + 1][j + 1] = dp[i + 1][j]; + } + } + dp[t.len()][s.len()] + } +} +``` + +> 滚动数组 + +```rust +impl Solution { + pub fn num_distinct(s: String, t: String) -> i32 { + if s.len() < t.len() { + return 0; + } + let (s, t) = (s.into_bytes(), t.into_bytes()); + // 对于 t 为空字符串,s 作为子序列的个数为 1(删除 s 所有元素) + let mut dp = vec![1; s.len() + 1]; + for char_t in t { + // dp[i - 1][j - 1],dp[j + 1] 更新之前的值 + let mut pre = dp[0]; + // 当开始遍历 t,s 的前 0 个字符无法包含任意子序列 + dp[0] = 0; + for (j, &char_s) in s.iter().enumerate() { + let temp = dp[j + 1]; + if char_t == char_s { + dp[j + 1] = pre + dp[j]; + } else { + dp[j + 1] = dp[j]; + } + pre = temp; + } + } + dp[s.len()] + } +} +``` + + + diff --git "a/problems/0116.\345\241\253\345\205\205\346\257\217\344\270\252\350\212\202\347\202\271\347\232\204\344\270\213\344\270\200\344\270\252\345\217\263\344\276\247\350\212\202\347\202\271\346\214\207\351\222\210.md" "b/problems/0116.\345\241\253\345\205\205\346\257\217\344\270\252\350\212\202\347\202\271\347\232\204\344\270\213\344\270\200\344\270\252\345\217\263\344\276\247\350\212\202\347\202\271\346\214\207\351\222\210.md" new file mode 100755 index 0000000000..88d3abc93e --- /dev/null +++ "b/problems/0116.\345\241\253\345\205\205\346\257\217\344\270\252\350\212\202\347\202\271\347\232\204\344\270\213\344\270\200\344\270\252\345\217\263\344\276\247\350\212\202\347\202\271\346\214\207\351\222\210.md" @@ -0,0 +1,490 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 116. 填充每个节点的下一个右侧节点指针 + +[力扣题目链接](https://leetcode.cn/problems/populating-next-right-pointers-in-each-node/) + +给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: + +``` +struct Node { + int val; + Node *left; + Node *right; + Node *next; +} +``` + +填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 + +初始状态下,所有 next 指针都被设置为 NULL。 + +进阶: +* 你只能使用常量级额外空间。 +* 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 + +![](https://file1.kamacoder.com/i/algo/20210727143202.png) + +## 思路 + +注意题目提示内容,: +* 你只能使用常量级额外空间。 +* 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 + +基本上就是要求使用递归了,迭代的方式一定会用到栈或者队列。 + +### 递归 + +一想用递归怎么做呢,虽然层序遍历是最直观的,但是递归的方式确实不好想。 + +如图,假如当前操作的节点是cur: + + + +最关键的点是可以通过上一层递归 搭出来的线,进行本次搭线。 + +图中cur节点为元素4,那么搭线的逻辑代码:(**注意注释中操作1和操作2和图中的对应关系**) + +```CPP +if (cur->left) cur->left->next = cur->right; // 操作1 +if (cur->right) { + if (cur->next) cur->right->next = cur->next->left; // 操作2 + else cur->right->next = NULL; +} +``` + +理解到这里,使用前序遍历,那么不难写出如下代码: + + +```CPP +class Solution { +private: + void traversal(Node* cur) { + if (cur == NULL) return; + // 中 + if (cur->left) cur->left->next = cur->right; // 操作1 + if (cur->right) { + if (cur->next) cur->right->next = cur->next->left; // 操作2 + else cur->right->next = NULL; + } + traversal(cur->left); // 左 + traversal(cur->right); // 右 + } +public: + Node* connect(Node* root) { + traversal(root); + return root; + } +}; +``` + +### 迭代(层序遍历) + +本题使用层序遍历是最为直观的,如果对层序遍历不了解,看这篇:[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html)。 + +遍历每一行的时候,如果不是最后一个Node,则指向下一个Node;如果是最后一个Node,则指向nullptr。 + +代码如下: + +```CPP +class Solution { +public: + Node* connect(Node* root) { + queue que; + if (root != nullptr) que.push(root); + while (!que.empty()) { + int size = que.size(); + for (int i = 0; i < size; ++i) { + Node* node = que.front(); + que.pop(); + if (i != size - 1) { + node->next = que.front(); //如果不是最后一个Node 则指向下一个Node + } else node->next = nullptr; //如果是最后一个Node 则指向nullptr + if (node->left != nullptr) que.push(node->left); + if (node->right != nullptr) que.push(node->right); + } + } + return root; + } +}; +``` + +## 其他语言版本 + +### Java + +```java +// 递归法 +class Solution { + public void traversal(Node cur) { + if (cur == null) return; + if (cur.left != null) cur.left.next = cur.right; // 操作1 + if (cur.right != null) { + if(cur.next != null) cur.right.next = cur.next.left; //操作2 + else cur.right.next = null; + } + traversal(cur.left); // 左 + traversal(cur.right); //右 + } + public Node connect(Node root) { + traversal(root); + return root; + } +} +``` +```java +// 迭代法 +class Solution { + public Node connect(Node root) { + if (root == null) return root; + Queue que = new LinkedList(); + que.offer(root); + Node nodePre = null; + Node node = null; + while (!que.isEmpty()) { + int size = que.size(); + for (int i=0; i queue = new LinkedList<>(); + + queue.add(root); + + while (!queue.isEmpty()) { + int size = queue.size(); + + // 每层的第一个节点 + Node cur = queue.poll(); + if (cur.left != null) { + queue.add(cur.left); + } + if (cur.right != null) { + queue.add(cur.right); + } + + // 因为已经移除了每层的第一个节点,所以将 0 改为 1 + while (size-- > 1) { + Node next = queue.poll(); + + if (next.left != null) { + queue.add(next.left); + } + if (next.right != null) { + queue.add(next.right); + } + + // 当前节点指向同层的下一个节点 + cur.next = next; + // 更新当前节点 + cur = next; + } + + // 每层的最后一个节点不指向 null 在力扣也能过 + cur.next = null; + } + + return root; + } +} +``` + +### Python + +```python +# 递归法 +class Solution: + def connect(self, root: 'Node') -> 'Node': + def traversal(cur: 'Node') -> 'Node': + if not cur: return [] + if cur.left: cur.left.next = cur.right # 操作1 + if cur.right: + if cur.next: + cur.right.next = cur.next.left # 操作2 + else: + cur.right.next = None + traversal(cur.left) # 左 + traversal(cur.right) # 右 + traversal(root) + return root +``` +```python +# 迭代法 +class Solution: + def connect(self, root: 'Node') -> 'Node': + if not root: return + res = [] + queue = [root] + while queue: + size = len(queue) + for i in range(size): # 开始每一层的遍历 + if i==0: + nodePre = queue.pop(0) # 记录一层的头结点 + node = nodePre + else: + node = queue.pop(0) + nodePre.next = node # 本层前一个节点next指向本节点 + nodePre = nodePre.next + if node.left: queue.append(node.left) + if node.right: queue.append(node.right) + nodePre.next = None # 本层最后一个节点指向None + return root +``` +### Go +```go +// 迭代法 +func connect(root *Node) *Node { + if root == nil { + return root + } + stack := make([]*Node, 0) + stack = append(stack, root) + for len(stack) > 0 { + n := len(stack) // 记录当前层节点个数 + for i := 0; i < n; i++ { + node := stack[0] // 依次弹出节点 + stack = stack[1:] + if i == n - 1 { // 如果是这层最右的节点,next指向nil + node.Next = nil + } else { + node.Next = stack[0] // 如果不是最右的节点,next指向右边的节点 + } + if node.Left != nil { // 如果存在左子节点,放入栈中 + stack = append(stack, node.Left) + } + if node.Right != nil { // 如果存在右子节点,放入栈中 + stack = append(stack, node.Right) + } + } + } + return root +} +``` +```go +// 常量级额外空间,使用next +func connect(root *Node) *Node { + if root == nil { + return root + } + for cur := root; cur.Left != nil; cur = cur.Left { // 遍历每层最左边的节点 + for node := cur; node != nil; node = node.Next { // 当前层从左到右遍历 + node.Left.Next = node.Right // 左子节点next指向右子节点 + if node.Next != nil { //如果node next有值,右子节点指向next节点的左子节点 + node.Right.Next = node.Next.Left + } + + } + } + return root +} +``` + +### JavaScript + +```js +const connect = root => { + if (!root) return root; + // 根节点入队 + const Q = [root]; + while (Q.length) { + const len = Q.length; + // 遍历这一层的所有节点 + for (let i = 0; i < len; i++) { + // 队头出队 + const node = Q.shift(); + // 连接 + if (i < len - 1) { + // 新的队头是node的右边元素 + node.next = Q[0]; + } + // 队头左节点有值,放入队列 + node.left && Q.push(node.left); + // 队头右节点有值,放入队列 + node.right && Q.push(node.right); + } + } + return root; +}; +``` + +### TypeScript + +(注:命名空间‘Node’与typescript中内置类型冲突,这里改成了‘NodePro’) + +> 递归法: + +```typescript +class NodePro { + val: number + left: NodePro | null + right: NodePro | null + next: NodePro | null + constructor(val?: number, left?: NodePro, right?: NodePro, next?: NodePro) { + this.val = (val === undefined ? 0 : val) + this.left = (left === undefined ? null : left) + this.right = (right === undefined ? null : right) + this.next = (next === undefined ? null : next) + } +} + +function connect(root: NodePro | null): NodePro | null { + if (root === null) return null; + root.next = null; + recur(root); + return root; +}; +function recur(node: NodePro): void { + if (node.left === null || node.right === null) return; + node.left.next = node.right; + node.right.next = node.next && node.next.left; + recur(node.left); + recur(node.right); +} +``` + +> 迭代法: + +```typescript +class NodePro { + val: number + left: NodePro | null + right: NodePro | null + next: NodePro | null + constructor(val?: number, left?: NodePro, right?: NodePro, next?: NodePro) { + this.val = (val === undefined ? 0 : val) + this.left = (left === undefined ? null : left) + this.right = (right === undefined ? null : right) + this.next = (next === undefined ? null : next) + } +} + +function connect(root: NodePro | null): NodePro | null { + if (root === null) return null; + const queue: NodePro[] = []; + queue.push(root); + while (queue.length > 0) { + for (let i = 0, length = queue.length; i < length; i++) { + const curNode: NodePro = queue.shift()!; + if (i === length - 1) { + curNode.next = null; + } else { + curNode.next = queue[0]; + } + if (curNode.left !== null) queue.push(curNode.left); + if (curNode.right !== null) queue.push(curNode.right); + } + } + return root; +}; +``` + +```csharp +//递归 +public class Solution { + public Node Connect(Node root) { + if (root == null) { + return null; + } + + ConnectNodes(root.left, root.right); + + return root; + } + + private void ConnectNodes(Node node1, Node node2) { + if (node1 == null || node2 == null) { + return; + } + + // 将左子节点的 next 指向右子节点 + node1.next = node2; + + // 递归连接当前节点的左右子节点 + ConnectNodes(node1.left, node1.right); + ConnectNodes(node2.left, node2.right); + + // 连接跨越父节点的两个子树 + ConnectNodes(node1.right, node2.left); + } +} + + +// 迭代 +public class Solution +{ + public Node Connect(Node root) + { + Queue que = new Queue(); + + if (root != null) + { + que.Enqueue(root); + } + + while (que.Count > 0) + { + + var queSize = que.Count; + for (int i = 0; i < queSize; i++) + { + var cur = que.Dequeue(); + + // 当这个节点不是这一层的最后的节点 + if (i != queSize - 1) + { + // 当前节点指向下一个节点 + cur.next = que.Peek(); + } + // 否则指向空 + else + { + cur.next = null; + } + + if (cur.left != null) + { + que.Enqueue(cur.left); + } + if (cur.right != null) + { + que.Enqueue(cur.right); + } + } + } + + return root; + } +} +``` + + + + diff --git "a/problems/0121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" "b/problems/0121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" new file mode 100755 index 0000000000..d12cbf2fe2 --- /dev/null +++ "b/problems/0121.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272.md" @@ -0,0 +1,627 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 121. 买卖股票的最佳时机 + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock/) + +给定一个数组 prices ,它的第 i 个元素 prices[i] 表示一支给定股票第 i 天的价格。 + +你只能选择 某一天 买入这只股票,并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。 + +返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润,返回 0 。 + +* 示例 1: +* 输入:[7,1,5,3,6,4] +* 输出:5 +解释:在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格;同时,你不能在买入前卖出股票。 + +* 示例 2: +* 输入:prices = [7,6,4,3,1] +* 输出:0 +解释:在这种情况下, 没有交易完成, 所以最大利润为 0。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划之 LeetCode:121.买卖股票的最佳时机1](https://www.bilibili.com/video/BV1Xe4y1u77q),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 暴力 + +这道题目最直观的想法,就是暴力,找最优间距了。 + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int result = 0; + for (int i = 0; i < prices.size(); i++) { + for (int j = i + 1; j < prices.size(); j++){ + result = max(result, prices[j] - prices[i]); + } + } + return result; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + +当然该方法超时了。 + +### 贪心 + +因为股票就买卖一次,那么贪心的想法很自然就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。 + +C++代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int low = INT_MAX; + int result = 0; + for (int i = 0; i < prices.size(); i++) { + low = min(low, prices[i]); // 取最左最小价格 + result = max(result, prices[i] - low); // 直接取最大区间利润 + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +### 动态规划 + +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][0] 表示第i天持有股票所得最多现金 ,**这里可能有同学疑惑,本题中只能买卖一次,持有股票之后哪还有现金呢?** + +其实一开始现金是0,那么加入第i天买入股票现金就是 -prices[i], 这是一个负数。 + +dp[i][1] 表示第i天不持有股票所得最多现金 + +**注意这里说的是“持有”,“持有”不代表就是当天“买入”!也有可能是昨天就买入了,今天保持持有的状态** + +很多同学把“持有”和“买入”没区分清楚。 + +在下面递推公式分析中,我会进一步讲解。 + +2. 确定递推公式 + +如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 +* 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] +* 第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i] + +那么dp[i][0]应该选所得现金最大的,所以dp[i][0] = max(dp[i - 1][0], -prices[i]); + +如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来 +* 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] +* 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0] + +同样dp[i][1]取最大的,dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); + +这样递推公式我们就分析完了 + +3. dp数组如何初始化 + +由递推公式 dp[i][0] = max(dp[i - 1][0], -prices[i]); 和 dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]);可以看出 + +其基础都是要从dp[0][0]和dp[0][1]推导出来。 + +那么dp[0][0]表示第0天持有股票,此时的持有股票就一定是买入股票了,因为不可能有前一天推出来,所以dp[0][0] -= prices[0]; + +dp[0][1]表示第0天不持有股票,不持有股票那么现金就是0,所以dp[0][1] = 0; + +4. 确定遍历顺序 + +从递推公式可以看出dp[i]都是由dp[i - 1]推导出来的,那么一定是从前向后遍历。 + +5. 举例推导dp数组 + +以示例1,输入:[7,1,5,3,6,4]为例,dp数组状态如下: + + +![121.买卖股票的最佳时机](https://file1.kamacoder.com/i/algo/20210224225642465.png) + + +dp[5][1]就是最终结果。 + +为什么不是dp[5][0]呢? + +**因为本题中不持有股票状态所得金钱一定比持有股票状态得到的多!** + +以上分析完毕,C++代码如下: + +```CPP +// 版本一 +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + if (len == 0) return 0; + vector> dp(len, vector(2)); + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i][0] = max(dp[i - 1][0], -prices[i]); + dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); + } + return dp[len - 1][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。 + +``` +dp[i][0] = max(dp[i - 1][0], -prices[i]); +dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); +``` + +那么我们只需要记录 当前天的dp状态和前一天的dp状态就可以了,可以使用滚动数组来节省空间,代码如下: + +```CPP +// 版本二 +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + vector> dp(2, vector(2)); // 注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]); + dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]); + } + return dp[(len - 1) % 2][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +这里能写出版本一就可以了,版本二虽然原理都一样,但是想直接写出版本二还是有点麻烦,容易自己给自己找bug。 + +所以建议是先写出版本一,然后在版本一的基础上优化成版本二,而不是直接就写出版本二。 + +## 其他语言版本 + +### Java: + +> 贪心法: + +```java +class Solution { + public int maxProfit(int[] prices) { + // 找到一个最小的购入点 + int low = Integer.MAX_VALUE; + // res不断更新,直到数组循环完毕 + int res = 0; + for(int i = 0; i < prices.length; i++){ + low = Math.min(prices[i], low); + res = Math.max(prices[i] - low, res); + } + return res; + } +} +``` +> 动态规划:版本一 + +```java +// 解法1 +class Solution { + public int maxProfit(int[] prices) { + if (prices == null || prices.length == 0) return 0; + int length = prices.length; + // dp[i][0]代表第i天持有股票的最大收益 + // dp[i][1]代表第i天不持有股票的最大收益 + int[][] dp = new int[length][2]; + int result = 0; + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for (int i = 1; i < length; i++) { + dp[i][0] = Math.max(dp[i - 1][0], -prices[i]); + dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]); + } + return dp[length - 1][1]; + } +} +``` +> 动态规划:版本二(使用二維數組(和卡哥思路一致),下面還有使用一維滾動數組的更優化版本) + +```Java +class Solution { + public int maxProfit(int[] prices) { + int len = prices.length; + int dp[][] = new int[2][2]; + + dp[0][0] = - prices[0]; + dp[0][1] = 0; + + for (int i = 1; i < len; i++){ + dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], - prices[i]); + dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]); + } + return dp[(len - 1) % 2][1]; + } +} +``` + +> 动态规划:版本二(使用一維數組) + +``` java +class Solution { + public int maxProfit(int[] prices) { + int[] dp = new int[2]; + // 记录一次交易,一次交易有买入卖出两种状态 + // 0代表持有,1代表卖出 + dp[0] = -prices[0]; + dp[1] = 0; + // 可以参考斐波那契问题的优化方式 + // 我们从 i=1 开始遍历数组,一共有 prices.length 天, + // 所以是 i<=prices.length + for (int i = 1; i <= prices.length; i++) { + // 前一天持有;或当天买入 + dp[0] = Math.max(dp[0], -prices[i - 1]); + // 如果 dp[0] 被更新,那么 dp[1] 肯定会被更新为正数的 dp[1] + // 而不是 dp[0]+prices[i-1]==0 的0, + // 所以这里使用会改变的dp[0]也是可以的 + // 当然 dp[1] 初始值为 0 ,被更新成 0 也没影响 + // 前一天卖出;或当天卖出, 当天要卖出,得前一天持有才行 + dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]); + } + return dp[1]; + } +} +``` + +### Python: + +> 贪心法: +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + low = float("inf") + result = 0 + for i in range(len(prices)): + low = min(low, prices[i]) #取最左最小价格 + result = max(result, prices[i] - low) #直接取最大区间利润 + return result +``` + +> 动态规划:版本一 +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + length = len(prices) + if length == 0: + return 0 + dp = [[0] * 2 for _ in range(length)] + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i in range(1, length): + dp[i][0] = max(dp[i-1][0], -prices[i]) + dp[i][1] = max(dp[i-1][1], prices[i] + dp[i-1][0]) + return dp[-1][1] +``` + +> 动态规划:版本二 +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + length = len(prices) + dp = [[0] * 2 for _ in range(2)] #注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i in range(1, length): + dp[i % 2][0] = max(dp[(i-1) % 2][0], -prices[i]) + dp[i % 2][1] = max(dp[(i-1) % 2][1], prices[i] + dp[(i-1) % 2][0]) + return dp[(length-1) % 2][1] +``` + +> 动态规划:版本三 +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + length = len(prices) + dp0, dp1 = -prices[0], 0 #注意这里只维护两个常量,因为dp0的更新不受dp1的影响 + for i in range(1, length): + dp1 = max(dp1, dp0 + prices[i]) + dp0 = max(dp0, -prices[i]) + return dp1 +``` + +### Go: + +> 贪心法: +```Go +func maxProfit(prices []int) int { + min := prices[0] + res := 0 + for i := 1; i < len(prices); i++ { + if prices[i] - min > res { + res = prices[i]-min + } + if min > prices[i] { + min = prices[i] + } + } + return res +} +``` + +> 动态规划:版本一 +```Go +func maxProfit(prices []int) int { + length := len(prices) + if length == 0{return 0} + dp := make([][]int,length) + for i := 0; i < length; i++ { + dp[i] = make([]int, 2) + } + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i := 1; i < length; i++ { + dp[i][0] = max(dp[i-1][0], -prices[i]) + dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) + } + return dp[length-1][1] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +> 动态规划:版本二 +```Go +func maxProfit(prices []int) int { + dp := [2][2]int{} + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i := 1; i < len(prices); i++ { + dp[i%2][0] = max(dp[(i-1)%2][0], -prices[i]) + dp[i%2][1] = max(dp[(i-1)%2][1], dp[(i-1)%2][0]+prices[i]) + } + + return dp[(len(prices)-1)%2][1] +} + +func max(a, b int) int { + if a > b{ + return a + } + + return b +} +``` + +### JavaScript: + +> 动态规划 + +```javascript +const maxProfit = prices => { + const len = prices.length; + // 创建dp数组 + const dp = new Array(len).fill([0, 0]); + // dp数组初始化 + dp[0] = [-prices[0], 0]; + for (let i = 1; i < len; i++) { + // 更新dp[i] + dp[i] = [ + Math.max(dp[i - 1][0], -prices[i]), + Math.max(dp[i - 1][1], prices[i] + dp[i - 1][0]), + ]; + } + return dp[len - 1][1]; +}; +``` + +> 贪心法 + +```javascript +var maxProfit = function(prices) { + let lowerPrice = prices[0];// 重点是维护这个最小值(贪心的思想) + let profit = 0; + for(let i = 0; i < prices.length; i++){ + lowerPrice = Math.min(lowerPrice, prices[i]);// 贪心地选择左面的最小价格 + profit = Math.max(profit, prices[i] - lowerPrice);// 遍历一趟就可以获得最大利润 + } + return profit; +}; +``` + +### TypeScript: + +> 贪心法 + +```typescript +function maxProfit(prices: number[]): number { + if (prices.length === 0) return 0; + let buy: number = prices[0]; + let profitMax: number = 0; + for (let i = 1, length = prices.length; i < length; i++) { + profitMax = Math.max(profitMax, prices[i] - buy); + buy = Math.min(prices[i], buy); + } + return profitMax; +}; +``` + +> 动态规划:版本一 + +```typescript +function maxProfit(prices: number[]): number { + /** + dp[i][0]: 第i天持有股票的最大现金 + dp[i][1]: 第i天不持有股票的最大现金 + */ + const length = prices.length; + if (length === 0) return 0; + const dp: number[][] = []; + dp[0] = [-prices[0], 0]; + for (let i = 1; i < length; i++) { + dp[i] = []; + dp[i][0] = Math.max(dp[i - 1][0], -prices[i]); + dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]); + } + return dp[length - 1][1]; +}; +``` + +> 动态规划:版本二 + +```typescript +// dp[i][0] 表示第i天持有股票所得最多现金 +// dp[i][1] 表示第i天不持有股票所得最多现金 +function maxProfit(prices: number[]): number { + const dp:number[][] = Array(2).fill(0).map(item => Array(2)); + dp[0][0] = -prices[0]; + dp[0][1] = 0; + + for (let i = 1; i < prices.length; i++) { + dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], -prices[i]); + dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]); + } + + // 返回不持有股票的最大现金 + return dp[(prices.length-1) % 2][1]; +}; +``` + +### C#: + +> 贪心法 + +```csharp +public class Solution +{ + public int MaxProfit(int[] prices) + { + int min = Int32.MaxValue; + int res = 0; + for (int i = 0; i < prices.Length; i++) + { + min = Math.Min(prices[i], min); + res = Math.Max(prices[i] - min, res); + } + return res; + } +} +``` + +> 动态规划 + +```csharp +public class Solution +{ + public int MaxProfit(int[] prices) + { + int[] dp = new int[2]; + int size = prices.Length; + (dp[0], dp[1]) = (-prices[0], 0); + for (int i = 0; i < size; i++) + { + dp[0] = Math.Max(dp[0], -prices[i]); + dp[1] = Math.Max(dp[1], dp[0]+prices[i]); + } + return dp[1]; + } +} +``` + +### C: + +> 贪心 + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int maxProfit(int* prices, int pricesSize) { + int low = INT_MIN; + int result = 0; + for(int i = 0; i < pricesSize; i++){ + low = min(low, prices[i]); + result = max(result, prices[i] - low); + } + return result; +} +``` + +> 动态规划 + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int maxProfit(int* prices, int pricesSize){ + if(pricesSize == 0){ + return 0; + } + // dp初始化 + int ** dp = malloc(sizeof (int *) * pricesSize); + for(int i = 0; i < pricesSize; i++){ + dp[i] = malloc(sizeof (int ) * 2); + } + // 下标0表示持有股票的情况下的最大现金,下标1表示不持有股票的情况下获得的最大现金 + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for(int i = 1; i < pricesSize; i++){ + dp[i][0] = max(dp[i - 1][0], - prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); + } + return dp[pricesSize - 1][1]; +} +``` + + + +### Rust: + +> 贪心 + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let (mut low, mut res) = (i32::MAX, 0); + for p in prices { + low = p.min(low); + res = res.max(p - low); + } + res + } +} +``` + +> 动态规划 + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut dp = vec![-prices[0], 0]; + for p in prices { + dp[0] = dp[0].max(-p); + dp[1] = dp[1].max(dp[0] + p); + } + dp[1] + } +} +``` + + diff --git "a/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" new file mode 100755 index 0000000000..0da4241931 --- /dev/null +++ "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II.md" @@ -0,0 +1,423 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 122.买卖股票的最佳时机 II + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) + +给定一个数组,它的第  i 个元素是一支给定股票第 i 天的价格。 + +设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 + +注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + +示例 1: + +- 输入: [7,1,5,3,6,4] +- 输出: 7 +- 解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 + +示例 2: + +- 输入: [1,2,3,4,5] +- 输出: 4 +- 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 + +示例  3: + +- 输入: [7,6,4,3,1] +- 输出: 0 +- 解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 + +提示: + +- 1 <= prices.length <= 3 \* 10 ^ 4 +- 0 <= prices[i] <= 10 ^ 4 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法也能解决股票问题!LeetCode:122.买卖股票最佳时机 II](https://www.bilibili.com/video/BV1ev4y1C7na),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +本题首先要清楚两点: + +- 只有一只股票! +- 当前只有买股票或者卖股票的操作 + +想获得利润至少要两天为一个交易单元。 + +### 贪心算法 + +这道题目可能我们只会想,选一个低的买入,再选个高的卖,再选一个低的买入.....循环反复。 + +**如果想到其实最终利润是可以分解的,那么本题就很容易了!** + +如何分解呢? + +假如第 0 天买入,第 3 天卖出,那么利润为:prices[3] - prices[0]。 + +相当于(prices[3] - prices[2]) + (prices[2] - prices[1]) + (prices[1] - prices[0])。 + +**此时就是把利润分解为每天为单位的维度,而不是从 0 天到第 3 天整体去考虑!** + +那么根据 prices 可以得到每天的利润序列:(prices[i] - prices[i - 1]).....(prices[1] - prices[0])。 + +如图: + +![122.买卖股票的最佳时机II](https://file1.kamacoder.com/i/algo/2020112917480858-20230310134659477.png) + +一些同学陷入:第一天怎么就没有利润呢,第一天到底算不算的困惑中。 + +第一天当然没有利润,至少要第二天才会有利润,所以利润的序列比股票序列少一天! + +从图中可以发现,其实我们需要收集每天的正利润就可以,**收集正利润的区间,就是股票买卖的区间,而我们只需要关注最终利润,不需要记录区间**。 + +那么只收集正利润就是贪心所贪的地方! + +**局部最优:收集每天的正利润,全局最优:求得最大利润**。 + +局部最优可以推出全局最优,找不出反例,试一试贪心! + +对应 C++代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int result = 0; + for (int i = 1; i < prices.size(); i++) { + result += max(prices[i] - prices[i - 1], 0); + } + return result; + } +}; +``` + +- 时间复杂度:O(n) +- 空间复杂度:O(1) + +### 动态规划 + +动态规划将在下一个系列详细讲解,本题解先给出我的 C++代码(带详细注释),想先学习的话,可以看本篇:[122.买卖股票的最佳时机II(动态规划)](https://programmercarl.com/0122.%E4%B9%B0%E5%8D%96%E8%82%A1%E7%A5%A8%E7%9A%84%E6%9C%80%E4%BD%B3%E6%97%B6%E6%9C%BAII%EF%BC%88%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%EF%BC%89.html#%E6%80%9D%E8%B7%AF) + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + // dp[i][1]第i天持有的最多现金 + // dp[i][0]第i天持有股票后的最多现金 + int n = prices.size(); + vector> dp(n, vector(2, 0)); + dp[0][0] -= prices[0]; // 持股票 + for (int i = 1; i < n; i++) { + // 第i天持股票所剩最多现金 = max(第i-1天持股票所剩现金, 第i-1天持现金-买第i天的股票) + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + // 第i天持有最多现金 = max(第i-1天持有的最多现金,第i-1天持有股票的最多现金+第i天卖出股票) + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); + } + return max(dp[n - 1][0], dp[n - 1][1]); + } +}; +``` + +- 时间复杂度:$O(n)$ +- 空间复杂度:$O(n)$ + +## 总结 + +股票问题其实是一个系列的,属于动态规划的范畴,因为目前在讲解贪心系列,所以股票问题会在之后的动态规划系列中详细讲解。 + +**可以看出有时候,贪心往往比动态规划更巧妙,更好用,所以别小看了贪心算法**。 + +**本题中理解利润拆分是关键点!** 不要整块的去看,而是把整体利润拆为每天的利润。 + +一旦想到这里了,很自然就会想到贪心了,即:只收集每天的正利润,最后稳稳的就是最大利润了。 + +## 其他语言版本 + +### Java: + +贪心: + +```java +// 贪心思路 +class Solution { + public int maxProfit(int[] prices) { + int result = 0; + for (int i = 1; i < prices.length; i++) { + result += Math.max(prices[i] - prices[i - 1], 0); + } + return result; + } +} +``` + +动态规划: + +```java +class Solution { // 动态规划 + public int maxProfit(int[] prices) { + // [天数][是否持有股票] + int[][] dp = new int[prices.length][2]; + + // base case + dp[0][0] = 0; + dp[0][1] = -prices[0]; + + for (int i = 1; i < prices.length; i++) { + // dp公式 + 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[prices.length - 1][0]; + } +} +``` + +### Python: + +贪心: + +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + result = 0 + for i in range(1, len(prices)): + result += max(prices[i] - prices[i - 1], 0) + return result +``` + +动态规划: + +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + length = len(prices) + dp = [[0] * 2 for _ in range(length)] + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i in range(1, length): + dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]) #注意这里是和121. 买卖股票的最佳时机唯一不同的地方 + dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) + return dp[-1][1] +``` + +### Go: + +贪心算法 + +```go +func maxProfit(prices []int) int { + var sum int + for i := 1; i < len(prices); i++ { + // 累加每次大于0的交易 + if prices[i] - prices[i-1] > 0 { + sum += prices[i] - prices[i-1] + } + } + return sum +} +``` + +动态规划 + +```go +func maxProfit(prices []int) int { + dp := make([][]int, len(prices)) + for i := 0; i < len(dp); i++ { + dp[i] = make([]int, 2) + } + // dp[i][0]表示在状态i不持有股票的现金,dp[i][1]为持有股票的现金 + dp[0][0], dp[0][1] = 0, -prices[0] + for i := 1; i < len(prices); i++ { + dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]) + dp[i][1] = max(dp[i-1][0] - prices[i], dp[i-1][1]) + } + return dp[len(prices)-1][0] + +} +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +### JavaScript: + +贪心 + +```Javascript +var maxProfit = function(prices) { + let result = 0 + for(let i = 1; i < prices.length; i++) { + result += Math.max(prices[i] - prices[i - 1], 0) + } + return result +}; +``` + +动态规划 + +```javascript +const maxProfit = (prices) => { + let dp = Array.from(Array(prices.length), () => Array(2).fill(0)); + // dp[i][0] 表示第i天持有股票所得现金。 + // dp[i][1] 表示第i天不持有股票所得最多现金 + dp[0][0] = 0 - prices[0]; + dp[0][1] = 0; + for (let i = 1; i < prices.length; i++) { + // 如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 + // 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] + // 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i] + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + + // 在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来 + // 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] + // 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0] + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i]); + } + + return dp[prices.length - 1][1]; +}; +``` + +### TypeScript: + +贪心 +```typescript +function maxProfit(prices: number[]): number { + let resProfit: number = 0; + for (let i = 1, length = prices.length; i < length; i++) { + resProfit += Math.max(prices[i] - prices[i - 1], 0); + } + return resProfit; +} +``` + +动态规划 +```typescript +function maxProfit(prices: number[]): number { + const dp = Array(prices.length) + .fill(0) + .map(() => Array(2).fill(0)) + dp[0][0] = -prices[0] + for (let i = 1; i < prices.length; 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], dp[i - 1][0] + prices[i]) + } + return dp[prices.length - 1][1] +} +``` + +### Rust: + +贪心: + +```Rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut result = 0; + for i in 1..prices.len() { + result += (prices[i] - prices[i - 1]).max(0); + } + result + } +} +``` + +动态规划: + +```Rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut dp = vec![vec![0; 2]; prices.len()]; + dp[0][0] = -prices[0]; + for i in 1..prices.len() { + dp[i][0] = dp[i - 1][0].max(dp[i - 1][1] - prices[i]); + dp[i][1] = dp[i - 1][1].max(dp[i - 1][0] + prices[i]); + } + dp[prices.len() - 1][1] + } +} +``` + +### C: + +贪心: + +```c +int maxProfit(int* prices, int pricesSize){ + int result = 0; + int i; + //从第二个元素开始遍历数组,与之前的元素进行比较 + for(i = 1; i < pricesSize; ++i) { + //若该元素比前面元素大,则说明有利润。代表买入 + if(prices[i] > prices[i-1]) + result+= prices[i]-prices[i-1]; + } + return result; +} +``` + +动态规划: + +```c +#define max(a, b) (((a) > (b)) ? (a) : (b)) + +int maxProfit(int* prices, int pricesSize){ + int dp[pricesSize][2]; + dp[0][0] = 0 - prices[0]; + dp[0][1] = 0; + + int i; + for(i = 1; i < pricesSize; ++i) { + // dp[i][0]为i-1天持股的钱数/在第i天用i-1天的钱买入的最大值。 + // 若i-1天持股,且第i天买入股票比i-1天持股时更亏,说明应在i-1天时持股 + dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]); + //dp[i][1]为i-1天不持股钱数/在第i天卖出所持股票dp[i-1][0] + prices[i]的最大值 + dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]); + } + // 返回在最后一天不持股时的钱数(将股票卖出后钱最大化) + return dp[pricesSize - 1][1]; +} +``` + +### Scala: + +贪心: + +```scala +object Solution { + def maxProfit(prices: Array[Int]): Int = { + var result = 0 + for (i <- 1 until prices.length) { + if (prices(i) > prices(i - 1)) { + result += prices(i) - prices(i - 1) + } + } + result + } +} +``` +### C# +```csharp +public class Solution +{ + public int MaxProfit(int[] prices) + { + int res = 0; + for (int i = 0; i < prices.Length - 1; i++) + { + res += Math.Max(0, prices[i + 1] - prices[i]); + } + return res; + } +} +``` + + diff --git "a/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" new file mode 100755 index 0000000000..d8cb308b7e --- /dev/null +++ "b/problems/0122.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272II\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" @@ -0,0 +1,477 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 122.买卖股票的最佳时机II + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-ii/) + +给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。 + +设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。 + +注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + + +* 示例 1: +* 输入: [7,1,5,3,6,4] +* 输出: 7 +解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 + +* 示例 2: +* 输入: [1,2,3,4,5] +* 输出: 4 +解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 + +* 示例 3: +* 输入: [7,6,4,3,1] +* 输出: 0 +解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 + +提示: +* 1 <= prices.length <= 3 * 10 ^ 4 +* 0 <= prices[i] <= 10 ^ 4 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,股票问题第二弹 | LeetCode:122.买卖股票的最佳时机II](https://www.bilibili.com/video/BV1D24y1Q7Ls),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题我们在讲解贪心专题的时候就已经讲解过了[贪心算法:买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html),只不过没有深入讲解动态规划的解法,那么这次我们再好好分析一下动规的解法。 + + +本题和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)的唯一区别是本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票) + +**在动规五部曲中,这个区别主要是体现在递推公式上,其他都和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)一样一样的**。 + +所以我们重点讲一讲递推公式。 + +这里重申一下dp数组的含义: + +* dp[i][0] 表示第i天持有股票所得现金。 +* dp[i][1] 表示第i天不持有股票所得最多现金 + + +如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 +* 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] +* 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i] + +**注意这里和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况**。 + +在[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。 + +而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。 + +那么第i天持有股票即dp[i][0],如果是第i天买入股票,所得现金就是昨天不持有股票的所得现金 减去 今天的股票价格 即:dp[i - 1][1] - prices[i]。 + +再来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来 +* 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] +* 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金即:prices[i] + dp[i - 1][0] + +**注意这里和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)就是一样的逻辑,卖出股票收获利润(可能是负值)天经地义!** + +代码如下:(注意代码中的注释,标记了和121.买卖股票的最佳时机唯一不同的地方) + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + vector> dp(len, vector(2, 0)); + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 注意这里是和121. 买卖股票的最佳时机唯一不同的地方。 + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); + } + return dp[len - 1][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +大家可以本题和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)的代码几乎一样,唯一的区别在: + +``` +dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); +``` + +**这正是因为本题的股票可以买卖多次!** 所以买入股票的时候,可能会有之前买卖的利润即:dp[i - 1][1],所以dp[i - 1][1] - prices[i]。 + +想到到这一点,对这两道题理解的就比较深刻了。 + +这里我依然给出滚动数组的版本,C++代码如下: + +```CPP +// 版本二 +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + vector> dp(2, vector(2)); // 注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; 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], prices[i] + dp[(i - 1) % 2][0]); + } + return dp[(len - 1) % 2][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + + + +## 其他语言版本 + +### Java: + +```java +// 动态规划 +class Solution + // 实现1:二维数组存储 + // 可以将每天持有与否的情况分别用 dp[i][0] 和 dp[i][1] 来进行存储 + // 时间复杂度:O(n),空间复杂度:O(n) + public int maxProfit(int[] prices) { + int n = prices.length; + int[][] dp = new int[n][2]; // 创建二维数组存储状态 + dp[0][0] = 0; // 初始状态 + dp[0][1] = -prices[0]; + for (int i = 1; i < n; ++i) { + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] + prices[i]); // 第 i 天,没有股票 + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); // 第 i 天,持有股票 + } + return dp[n - 1][0]; // 卖出股票收益高于持有股票收益,因此取[0] + } +} +``` +```java +//DP using 2*2 Array (下方還有使用一維滾動數組的更優化版本) +class Solution { + public int maxProfit(int[] prices) { + int dp[][] = new int [2][2]; + //dp[i][0]: holding the stock + //dp[i][1]: not holding the stock + dp[0][0] = - prices[0]; + dp[0][1] = 0; + + for(int i = 1; i < prices.length; i++){ + dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]); + dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i]); + } + return dp[(prices.length - 1) % 2][1]; + } +} +``` +```java +// 优化空间 +class Solution { + public int maxProfit(int[] prices) { + int[] dp = new int[2]; + // 0表示持有,1表示卖出 + dp[0] = -prices[0]; + dp[1] = 0; + for(int i = 1; i <= prices.length; i++){ + // 前一天持有; 既然不限制交易次数,那么再次买股票时,要加上之前的收益 + dp[0] = Math.max(dp[0], dp[1] - prices[i-1]); + // 前一天卖出; 或当天卖出,当天卖出,得先持有 + dp[1] = Math.max(dp[1], dp[0] + prices[i-1]); + } + return dp[1]; + } +} +``` + +### Python: + +> 版本一: +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + length = len(prices) + dp = [[0] * 2 for _ in range(length)] + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i in range(1, length): + dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]) #注意这里是和121. 买卖股票的最佳时机唯一不同的地方 + dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i]) + return dp[-1][1] +``` + +> 版本二: +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + length = len(prices) + dp = [[0] * 2 for _ in range(2)] #注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i in range(1, length): + 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) % 2][0] + prices[i]) + return dp[(length-1) % 2][1] +``` + +### Go: + +```go +// 买卖股票的最佳时机Ⅱ 动态规划 +// 时间复杂度:O(n) 空间复杂度:O(n) +func maxProfit(prices []int) int { + dp := make([][]int, len(prices)) + status := make([]int, len(prices) * 2) + for i := range dp { + dp[i] = status[:2] + status = status[2:] + } + dp[0][0] = -prices[0] + + for i := 1; i < len(prices); i++ { + 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[len(prices) - 1][1] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```go +// 动态规划 版本二 滚动数组 +func maxProfit(prices []int) int { + dp := [2][2]int{} // 注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] = -prices[0] + dp[0][1] = 0 + for i := 1; i < len(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)%2][0] + prices[i]) + } + return dp[(len(prices)-1)%2][1] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +### JavaScript: + +```javascript +// 方法一:动态规划(dp 数组) +const maxProfit = (prices) => { + let dp = Array.from(Array(prices.length), () => Array(2).fill(0)); + // dp[i][0] 表示第i天持有股票所得现金。 + // dp[i][1] 表示第i天不持有股票所得最多现金 + dp[0][0] = 0 - prices[0]; + dp[0][1] = 0; + for(let i = 1; i < prices.length; i++) { + // 如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 + // 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] + // 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i] + dp[i][0] = Math.max(dp[i-1][0], dp[i-1][1] - prices[i]); + + // 在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来 + // 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] + // 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0] + dp[i][1] = Math.max(dp[i-1][1], dp[i-1][0] + prices[i]); + } + + return dp[prices.length -1][1]; +}; + +// 方法二:动态规划(滚动数组) +const maxProfit = (prices) => { + // 滚动数组 + // have: 第i天持有股票最大收益; notHave: 第i天不持有股票最大收益 + let n = prices.length, + have = -prices[0], + notHave = 0; + for (let i = 1; i < n; i++) { + have = Math.max(have, notHave - prices[i]); + notHave = Math.max(notHave, have + prices[i]); + } + // 最终手里不持有股票才能保证收益最大化 + return notHave; +} +``` + +### TypeScript: + +> 动态规划 + +```typescript +function maxProfit(prices: number[]): number { + /** + dp[i][0]: 第i天持有股票 + dp[i][1]: 第i天不持有股票 + */ + const length: number = prices.length; + if (length === 0) return 0; + const dp: number[][] = new Array(length).fill(0).map(_ => []); + dp[0] = [-prices[0], 0]; + for (let i = 1; i < length; 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], dp[i - 1][0] + prices[i]); + } + return dp[length - 1][1]; +}; +``` + +> 贪心法 + +```typescript +function maxProfit(prices: number[]): number { + let resProfit: number = 0; + for (let i = 1, length = prices.length; i < length; i++) { + if (prices[i] > prices[i - 1]) { + resProfit += prices[i] - prices[i - 1]; + } + } + return resProfit; +}; +``` + +### C#: + +> 贪心法 + +```csharp +public class Solution +{ + public int MaxProfit(int[] prices) + { + int res = 0; + for (int i = 1; i < prices.Length; i++) + res += Math.Max(0, prices[i] - prices[i-1]); + return res; + } +} +``` + +> 动态规划 + +```csharp +public class Solution +{ + public int MaxProfit(int[] prices) + { + int[] dp = new int[2]; + dp[0] = -prices[0]; + + for (int i = 1; i < prices.Length; i++) + { + dp[0] = dp[0]>dp[1] - prices[i]?dp[0]:dp[1] - prices[i]; + dp[1] = dp[1] > dp[0] + prices[i] ? dp[1] : dp[0] + prices[i]; + } + return dp[1]; + } +} +``` + +### C: + +> 动态规划 + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int maxProfit(int* prices, int pricesSize){ + int **dp = malloc(sizeof (int *) * pricesSize); + for (int i = 0; i < pricesSize; ++i) { + dp[i] = malloc(sizeof (int ) * 2); + } + // 0表示持有该股票所得最大,1表示不持有所得最大 + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for (int i = 1; i < pricesSize; ++i) { + 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[pricesSize - 1][1]; +} +``` + +> 贪心 + +```c +int maxProfit(int* prices, int pricesSize) { + if(pricesSize == 0){ + return 0; + } + int result = 0; + for(int i = 1; i < pricesSize; i++){ + // 如果今天股票价格大于昨天,代表有利润 + if(prices[i] > prices[i - 1]){ + result += prices[i] - prices[i - 1]; + } + } + return result; +} +``` + + + +### Rust: + +> 贪心 + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut result = 0; + for i in 1..prices.len() { + result += (prices[i] - prices[i - 1]).max(0); + } + result + } +} +``` + +>动态规划 + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut dp = vec![vec![0; 2]; prices.len()]; + dp[0][0] = -prices[0]; + for i in 1..prices.len() { + dp[i][0] = dp[i - 1][0].max(dp[i - 1][1] - prices[i]); + dp[i][1] = dp[i - 1][1].max(dp[i - 1][0] + prices[i]); + } + dp[prices.len() - 1][1] + } +} +``` + +> 优化 + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let mut dp = vec![-prices[0], 0]; + for p in prices { + dp[0] = dp[0].max(dp[1] - p); + dp[1] = dp[1].max(dp[0] + p); + } + dp[1] + } +} +``` + + diff --git "a/problems/0123.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272III.md" "b/problems/0123.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272III.md" new file mode 100755 index 0000000000..063477cb5a --- /dev/null +++ "b/problems/0123.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272III.md" @@ -0,0 +1,565 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 123.买卖股票的最佳时机III + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iii/) + + +给定一个数组,它的第 i 个元素是一支给定的股票在第 i 天的价格。 + +设计一个算法来计算你所能获取的最大利润。你最多可以完成 两笔 交易。 + +注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + +* 示例 1: +* 输入:prices = [3,3,5,0,0,3,1,4] +* 输出:6 +解释:在第 4 天(股票价格 = 0)的时候买入,在第 6 天(股票价格 = 3)的时候卖出,这笔交易所能获得利润 = 3-0 = 3 。随后,在第 7 天(股票价格 = 1)的时候买入,在第 8 天 (股票价格 = 4)的时候卖出,这笔交易所能获得利润 = 4-1 = 3。 + +* 示例 2: +* 输入:prices = [1,2,3,4,5] +* 输出:4 +解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 + +* 示例 3: +* 输入:prices = [7,6,4,3,1] +* 输出:0 +解释:在这个情况下, 没有交易完成, 所以最大利润为0。 + +* 示例 4: +* 输入:prices = [1] +输出:0 + +提示: + +* 1 <= prices.length <= 10^5 +* 0 <= prices[i] <= 10^5 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,股票至多买卖两次,怎么求? | LeetCode:123.买卖股票最佳时机III](https://www.bilibili.com/video/BV1WG411K7AR),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + + +这道题目相对 [121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) 和 [122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html) 难了不少。 + +关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。 + +接来下我用动态规划五部曲详细分析一下: + +1. 确定dp数组以及下标的含义 + +一天一共就有五个状态, + +0. 没有操作 (其实我们也可以不设置这个状态) +1. 第一次持有股票 +2. 第一次不持有股票 +3. 第二次持有股票 +4. 第二次不持有股票 + +dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。 + +需要注意:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。 + +例如 dp[i][1] ,并不是说 第i天一定买入股票,有可能 第 i-1天 就买入了,那么 dp[i][1] 延续买入股票的这个状态。 + +2. 确定递推公式 + + +达到dp[i][1]状态,有两个具体操作: + +* 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i] +* 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1] + +那么dp[i][1]究竟选 dp[i-1][0] - prices[i],还是dp[i - 1][1]呢? + +一定是选最大的,所以 dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]); + +同理dp[i][2]也有两个操作: + +* 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i] +* 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2] + +所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]) + +同理可推出剩下状态部分: + +dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + +dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + + +3. dp数组如何初始化 + +第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0; + +第0天做第一次买入的操作,dp[0][1] = -prices[0]; + +第0天做第一次卖出的操作,这个初始值应该是多少呢? + +此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0; + +第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢? + +第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后再买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。 + +所以第二次买入操作,初始化为:dp[0][3] = -prices[0]; + +同理第二次卖出初始化dp[0][4] = 0; + +4. 确定遍历顺序 + +从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。 + +5. 举例推导dp数组 + +以输入[1,2,3,4,5]为例 + + +![123.买卖股票的最佳时机III](https://file1.kamacoder.com/i/algo/20201228181724295-20230310134201291.png) + +大家可以看到红色框为最后两次卖出的状态。 + +现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。如果想不明白的录友也可以这么理解:如果第一次卖出已经是最大值了,那么我们可以在当天立刻买入再立刻卖出。所以dp[4][4]已经包含了dp[4][2]的情况。也就是说第二次卖出手里所剩的钱一定是最多的。 + +所以最终最大利润是dp[4][4] + +以上五部都分析完了,不难写出如下代码: + +```CPP +// 版本一 +class Solution { +public: + int maxProfit(vector& prices) { + if (prices.size() == 0) return 0; + vector> dp(prices.size(), vector(5, 0)); + dp[0][1] = -prices[0]; + dp[0][3] = -prices[0]; + for (int i = 1; i < prices.size(); i++) { + dp[i][0] = dp[i - 1][0]; + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]); + dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + } + return dp[prices.size() - 1][4]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n × 5) + +当然,大家可以看到力扣官方题解里的一种优化空间写法,我这里给出对应的C++版本: + +```CPP +// 版本二 +class Solution { +public: + int maxProfit(vector& prices) { + if (prices.size() == 0) return 0; + vector dp(5, 0); + dp[1] = -prices[0]; + dp[3] = -prices[0]; + for (int i = 1; i < prices.size(); i++) { + dp[1] = max(dp[1], dp[0] - prices[i]); + dp[2] = max(dp[2], dp[1] + prices[i]); + dp[3] = max(dp[3], dp[2] - prices[i]); + dp[4] = max(dp[4], dp[3] + prices[i]); + } + return dp[4]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +大家会发现dp[2]利用的是当天的dp[1]。 但结果也是对的。 + +我来简单解释一下: + +dp[1] = max(dp[1], dp[0] - prices[i]); 如果dp[1]取dp[1],即保持买入股票的状态,那么 dp[2] = max(dp[2], dp[1] + prices[i]);中dp[1] + prices[i] 就是今天卖出。 + +如果dp[1]取dp[0] - prices[i],今天买入股票,那么dp[2] = max(dp[2], dp[1] + prices[i]);中的dp[1] + prices[i]相当于是今天再卖出股票,一买一卖收益为0,对所得现金没有影响。相当于今天买入股票又卖出股票,等于没有操作,保持昨天卖出股票的状态了。 + +**这种写法看上去简单,其实思路很绕,不建议大家这么写,这么思考,很容易把自己绕进去!** + +对于本题,把版本一的写法研究明白,足以! + +## 拓展 + +其实我们可以不设置,‘0. 没有操作’ 这个状态,因为没有操作,手上的现金自然就是0, 正如我们在 [121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) 和 [122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html) 也没有设置这一状态是一样的。 + +代码如下: + +``` CPP +// 版本三 +class Solution { +public: + int maxProfit(vector& prices) { + if (prices.size() == 0) return 0; + vector> dp(prices.size(), vector(5, 0)); + dp[0][1] = -prices[0]; + dp[0][3] = -prices[0]; + for (int i = 1; i < prices.size(); i++) { + dp[i][1] = max(dp[i - 1][1], 0 - prices[i]); + dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]); + dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + } + return dp[prices.size() - 1][4]; + } +}; +``` + +## 其他语言版本 + +### Java: + +```java +// 版本一 +class Solution { + public int maxProfit(int[] prices) { + int len = prices.length; + // 边界判断, 题目中 length >= 1, 所以可省去 + if (prices.length == 0) return 0; + + /* + * 定义 5 种状态: + * 0: 没有操作, 1: 第一次买入, 2: 第一次卖出, 3: 第二次买入, 4: 第二次卖出 + */ + int[][] dp = new int[len][5]; + dp[0][1] = -prices[0]; + // 初始化第二次买入的状态是确保 最后结果是最多两次买卖的最大利润 + dp[0][3] = -prices[0]; + + for (int i = 1; i < len; i++) { + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]); + dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + } + + return dp[len - 1][4]; + } +} + +// 版本二: 空间优化 +class Solution { + public int maxProfit(int[] prices) { + int[] dp = new int[4]; + // 存储两次交易的状态就行了 + // dp[0]代表第一次交易的买入 + dp[0] = -prices[0]; + // dp[1]代表第一次交易的卖出 + dp[1] = 0; + // dp[2]代表第二次交易的买入 + dp[2] = -prices[0]; + // dp[3]代表第二次交易的卖出 + dp[3] = 0; + for(int i = 1; i <= prices.length; i++){ + // 要么保持不变,要么没有就买,有了就卖 + dp[0] = Math.max(dp[0], -prices[i-1]); + dp[1] = Math.max(dp[1], dp[0]+prices[i-1]); + // 这已经是第二次交易了,所以得加上前一次交易卖出去的收获 + dp[2] = Math.max(dp[2], dp[1]-prices[i-1]); + dp[3] = Math.max(dp[3], dp[2]+ prices[i-1]); + } + return dp[3]; + } +} +``` + +### Python: + +> 版本一: +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + if len(prices) == 0: + return 0 + dp = [[0] * 5 for _ in range(len(prices))] + dp[0][1] = -prices[0] + dp[0][3] = -prices[0] + for i in range(1, len(prices)): + dp[i][0] = dp[i-1][0] + dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) + dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]) + dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i]) + dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i]) + return dp[-1][4] +``` + +> 版本二: +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + if len(prices) == 0: + return 0 + dp = [0] * 5 + dp[1] = -prices[0] + dp[3] = -prices[0] + for i in range(1, len(prices)): + dp[1] = max(dp[1], dp[0] - prices[i]) + dp[2] = max(dp[2], dp[1] + prices[i]) + dp[3] = max(dp[3], dp[2] - prices[i]) + dp[4] = max(dp[4], dp[3] + prices[i]) + return dp[4] +``` + +### Go: + +> 版本一 + +```go +func maxProfit(prices []int) int { + dp := make([][]int, len(prices)) + for i := 0; i < len(prices); i++ { + dp[i] = make([]int, 5) + } + dp[0][0] = 0 + dp[0][1] = -prices[0] + dp[0][2] = 0 + dp[0][3] = -prices[0] + dp[0][4] = 0 + for i := 1; i < len(prices); i++ { + dp[i][0] = dp[i-1][0] + dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]) + dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]) + dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i]) + dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i]) + } + return dp[len(prices)-1][4] +} +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +> 版本二 + +```go +func maxProfit(prices []int) int { + if len(prices) == 0 { + return 0 + } + dp := make([]int, 5) + dp[1] = -prices[0] + dp[3] = -prices[0] + for i := 1; i < len(prices); i++ { + dp[1] = max(dp[1], dp[0] - prices[i]) + dp[2] = max(dp[2], dp[1] + prices[i]) + dp[3] = max(dp[3], dp[2] - prices[i]) + dp[4] = max(dp[4], dp[3] + prices[i]) + } + return dp[4] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +> 版本三 + +```go +func maxProfit(prices []int) int { + if len(prices) == 0 { + return 0 + } + dp := make([][5]int, len(prices)) + dp[0][1] = -prices[0] + dp[0][3] = -prices[0] + for i := 1; i < len(prices); i++ { + dp[i][1] = max(dp[i-1][1], 0 - prices[i]) + dp[i][2] = max(dp[i-1][2], dp[i-1][1] + prices[i]) + dp[i][3] = max(dp[i-1][3], dp[i-1][2] - prices[i]) + dp[i][4] = max(dp[i-1][4], dp[i-1][3] + prices[i]) + } + return dp[len(prices)-1][4] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +> 版本四:一维 dp 易懂版本 + +```go +func maxProfit(prices []int) int { + dp := make([]int, 4) + dp[0] = -prices[0] + dp[2] = -prices[0] + + for _, price := range prices[1:] { + dc := slices.Clone(dp) // 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + dp[0] = max(dc[0], -price) + dp[1] = max(dc[1], dc[0] + price) + dp[2] = max(dc[2], dc[1] - price) + dp[3] = max(dc[3], dc[2] + price) + } + + return dp[3] +} +``` + +### JavaScript: + +> 版本一: + +```javascript +const maxProfit = prices => { + const len = prices.length; + const dp = new Array(len).fill(0).map(x => new Array(5).fill(0)); + dp[0][1] = -prices[0]; + dp[0][3] = -prices[0]; + for (let i = 1; i < len; i++) { + dp[i][0] = dp[i - 1][0]; + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]); + dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + } + return dp[len - 1][4]; +}; +``` + +> 版本二: + +```javascript +const maxProfit = prices => { + const len = prices.length; + const dp = new Array(5).fill(0); + dp[1] = -prices[0]; + dp[3] = -prices[0]; + for (let i = 1; i < len; i++) { + dp[1] = Math.max(dp[1], dp[0] - prices[i]); + dp[2] = Math.max(dp[2], dp[1] + prices[i]); + dp[3] = Math.max(dp[3], dp[2] - prices[i]); + dp[4] = Math.max(dp[4], dp[3] + prices[i]); + } + return dp[4]; +}; +``` + +### TypeScript: + +> 版本一 + +```typescript +function maxProfit(prices: number[]): number { + /** + dp[i][0]: 无操作; + dp[i][1]: 第一次买入; + dp[i][2]: 第一次卖出; + dp[i][3]: 第二次买入; + dp[i][4]: 第二次卖出; + */ + const length: number = prices.length; + if (length === 0) return 0; + const dp: number[][] = new Array(length).fill(0) + .map(_ => new Array(5).fill(0)); + dp[0][1] = -prices[0]; + dp[0][3] = -prices[0]; + for (let i = 1; i < length; i++) { + dp[i][0] = dp[i - 1][0]; + dp[i][1] = Math.max(dp[i - 1][1], -prices[i]); + dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1] + prices[i]); + dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + dp[i][4] = Math.max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + } + return Math.max(dp[length - 1][2], dp[length - 1][4]); +}; +``` + +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int maxProfit(int* prices, int pricesSize) { + int buy1 = prices[0], buy2 = prices[0]; + int profit1 = 0, profit2 = 0; + for (int i = 0; i < pricesSize; ++i) { + // 寻找最低点买入 + buy1 = min(buy1, prices[i]); + // 找到第一次交易的最大盈利,并不断维护这一最大值 + profit1 = max(profit1, prices[i] - buy1); + + // 寻找第二次交易的最低投资点,并且考虑前一次交易的成本 + // 当前价格 - 第一次操作的盈利=新的投入成本( + // 为了让盈利最大,要寻找最小的成本) + buy2 = min(buy2, prices[i] - profit1); + // 第二次卖出后的盈利:当前价格减去成本,不断维护这一最大的总利润 + profit2 = max(profit2, prices[i] - buy2); + } + return profit2; +} +``` + + + +### Rust: + +> 版本一 + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + /* + * 定义 5 种状态: + * 0: 没有操作, 1: 第一次买入, 2: 第一次卖出, 3: 第二次买入, 4: 第二次卖出 + */ + let mut dp = vec![vec![0; 5]; prices.len()]; + dp[0][1] = -prices[0]; + dp[0][3] = -prices[0]; + + for (i, &p) in prices.iter().enumerate().skip(1) { + // 不操作 + // dp[i][0] = dp[i - 1][0]; + dp[i][1] = dp[i - 1][1].max(-p); + dp[i][2] = dp[i - 1][2].max(dp[i - 1][1] + p); + dp[i][3] = dp[i - 1][3].max(dp[i - 1][2] - p); + dp[i][4] = dp[i - 1][4].max(dp[i - 1][3] + p); + } + + dp[prices.len() - 1][4] + } +} +``` + +> 版本二(绕) + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + let (mut one_buy, mut one_sale, mut two_buy, mut two_sale) = (-prices[0], 0, -prices[0], 0); + + for p in prices { + one_buy = one_buy.max(-p); + one_sale = one_sale.max(p + one_buy); + two_buy = two_buy.max(one_sale - p); + two_sale = two_sale.max(two_buy + p); + } + two_sale + } +} +``` + + diff --git "a/problems/0127.\345\215\225\350\257\215\346\216\245\351\276\231.md" "b/problems/0127.\345\215\225\350\257\215\346\216\245\351\276\231.md" new file mode 100755 index 0000000000..0204606056 --- /dev/null +++ "b/problems/0127.\345\215\225\350\257\215\346\216\245\351\276\231.md" @@ -0,0 +1,360 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 127. 单词接龙 + +[力扣题目链接](https://leetcode.cn/problems/word-ladder/) + +字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列: +* 序列中第一个单词是 beginWord 。 +* 序列中最后一个单词是 endWord 。 +* 每次转换只能改变一个字母。 +* 转换过程中的中间单词必须是字典 wordList 中的单词。 +* 给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。 + + +示例 1: + +* 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log","cog"] +* 输出:5 +* 解释:一个最短转换序列是 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 返回它的长度 5。 + +示例 2: +* 输入:beginWord = "hit", endWord = "cog", wordList = ["hot","dot","dog","lot","log"] +* 输出:0 +* 解释:endWord "cog" 不在字典中,所以无法进行转换。 + + +## 思路 + +以示例1为例,从这个图中可以看出 hit 到 cog的路线,不止一条,有三条,一条是最短的长度为5,两条长度为6。 + +![](https://file1.kamacoder.com/i/algo/20210827175432.png) + +本题只需要求出最短路径的长度就可以了,不用找出路径。 + +所以这道题要解决两个问题: + +* 图中的线是如何连在一起的 +* 起点和终点的最短路径长度 + +首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个,所以判断点与点之间的关系,要自己判断是不是差一个字符,如果差一个字符,那就是有链接。 + +然后就是求起点和终点的最短路径长度,**这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径**。因为广搜就是以起点中心向四周扩散的搜索。 + +**本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路**。 而广搜只要达到终点,一定是最短路。 + +另外需要有一个注意点: + +* 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环! +* 本题给出集合是数组型的,可以转成set结构,查找更快一些 + +C++代码如下:(详细注释) + +```CPP +class Solution { +public: + int ladderLength(string beginWord, string endWord, vector& wordList) { + // 将vector转成unordered_set,提高查询速度 + unordered_set wordSet(wordList.begin(), wordList.end()); + // 如果endWord没有在wordSet出现,直接返回0 + if (wordSet.find(endWord) == wordSet.end()) return 0; + // 记录word是否访问过 + unordered_map visitMap; // + // 初始化队列 + queue que; + que.push(beginWord); + // 初始化visitMap + visitMap.insert(pair(beginWord, 1)); + + while(!que.empty()) { + string word = que.front(); + que.pop(); + int path = visitMap[word]; // 这个word的路径长度 + for (int i = 0; i < word.size(); i++) { + string newWord = word; // 用一个新单词替换word,因为每次置换一个字母 + for (int j = 0 ; j < 26; j++) { + newWord[i] = j + 'a'; + if (newWord == endWord) return path + 1; // 找到了end,返回path+1 + // wordSet出现了newWord,并且newWord没有被访问过 + if (wordSet.find(newWord) != wordSet.end() + && visitMap.find(newWord) == visitMap.end()) { + // 添加访问信息 + visitMap.insert(pair(newWord, path + 1)); + que.push(newWord); + } + } + } + } + return 0; + } +}; +``` + +当然本题也可以用双向BFS,就是从头尾两端进行搜索,大家感兴趣,可以自己去实现,这里就不再做详细讲解了。 + +## 其他语言版本 + +### Java + +```java +public int ladderLength(String beginWord, String endWord, List wordList) { + HashSet wordSet = new HashSet<>(wordList); //转换为hashset 加快速度 + if (wordSet.size() == 0 || !wordSet.contains(endWord)) { //特殊情况判断 + return 0; + } + Queue queue = new LinkedList<>(); //bfs 队列 + queue.offer(beginWord); + Map map = new HashMap<>(); //记录单词对应路径长度 + map.put(beginWord, 1); + + while (!queue.isEmpty()) { + String word = queue.poll(); //取出队头单词 + int path = map.get(word); //获取到该单词的路径长度 + for (int i = 0; i < word.length(); i++) { //遍历单词的每个字符 + char[] chars = word.toCharArray(); //将单词转换为char array,方便替换 + for (char k = 'a'; k <= 'z'; k++) { //从'a' 到 'z' 遍历替换 + chars[i] = k; //替换第i个字符 + String newWord = String.valueOf(chars); //得到新的字符串 + if (newWord.equals(endWord)) { //如果新的字符串值与endWord一致,返回当前长度+1 + return path + 1; + } + if (wordSet.contains(newWord) && !map.containsKey(newWord)) { //如果新单词在set中,但是没有访问过 + map.put(newWord, path + 1); //记录单词对应的路径长度 + queue.offer(newWord);//加入队尾 + } + } + } + } + return 0; //未找到 +} +``` + +```java +// Java 双向BFS +class Solution { + // 判断单词之间是否之差了一个字母 + public boolean isValid(String currentWord, String chooseWord) { + int count = 0; + for (int i = 0; i < currentWord.length(); i++) + if (currentWord.charAt(i) != chooseWord.charAt(i)) ++count; + return count == 1; + } + + public int ladderLength(String beginWord, String endWord, List wordList) { + if (!wordList.contains(endWord)) return 0; // 如果 endWord 不在 wordList 中,那么无法成功转换,返回 0 + + // ansLeft 记录从 beginWord 开始 BFS 时能组成的单词数目 + // ansRight 记录从 endWord 开始 BFS 时能组成的单词数目 + int ansLeft = 0, ansRight = 0; + + // queueLeft 表示从 beginWord 开始 BFS 时使用的队列 + // queueRight 表示从 endWord 开始 BFS 时使用的队列 + Queue queueLeft = new ArrayDeque<>(), queueRight = new ArrayDeque<>(); + queueLeft.add(beginWord); + queueRight.add(endWord); + + // 从 beginWord 开始 BFS 时把遍历到的节点存入 hashSetLeft 中 + // 从 endWord 开始 BFS 时把遍历到的节点存入 hashSetRight 中 + Set hashSetLeft = new HashSet<>(), hashSetRight = new HashSet<>(); + hashSetLeft.add(beginWord); + hashSetRight.add(endWord); + + // 只要有一个队列为空,说明 beginWord 无法转换到 endWord + while (!queueLeft.isEmpty() && !queueRight.isEmpty()) { + ++ansLeft; + int size = queueLeft.size(); + for (int i = 0; i < size; i++) { + String currentWord = queueLeft.poll(); + // 只要 hashSetRight 中存在 currentWord,说明从 currentWord 可以转换到 endWord + if (hashSetRight.contains(currentWord)) return ansRight + ansLeft; + for (String chooseWord : wordList) { + if (hashSetLeft.contains(chooseWord) || !isValid(currentWord, chooseWord)) continue; + hashSetLeft.add(chooseWord); + queueLeft.add(chooseWord); + } + } + ++ansRight; + size = queueRight.size(); + for (int i = 0; i < size; i++) { + String currentWord = queueRight.poll(); + // 只要 hashSetLeft 中存在 currentWord,说明从 currentWord 可以转换到 beginWord + if (hashSetLeft.contains(currentWord)) return ansLeft + ansRight; + for (String chooseWord : wordList) { + if (hashSetRight.contains(chooseWord) || !isValid(currentWord, chooseWord)) continue; + hashSetRight.add(chooseWord); + queueRight.add(chooseWord); + } + } + } + return 0; + } +} +``` + +### Python + +```python +class Solution: + def ladderLength(self, beginWord: str, endWord: str, wordList: List[str]) -> int: + wordSet = set(wordList) + if len(wordSet)== 0 or endWord not in wordSet: + return 0 + mapping = {beginWord:1} + queue = deque([beginWord]) + while queue: + word = queue.popleft() + path = mapping[word] + for i in range(len(word)): + word_list = list(word) + for j in range(26): + word_list[i] = chr(ord('a')+j) + newWord = "".join(word_list) + if newWord == endWord: + return path+1 + if newWord in wordSet and newWord not in mapping: + mapping[newWord] = path+1 + queue.append(newWord) + return 0 +``` +### Go +```go +func ladderLength(beginWord string, endWord string, wordList []string) int { + wordMap, que, depth := getWordMap(wordList, beginWord), []string{beginWord}, 0 + for len(que) > 0 { + depth++ + qLen := len(que) // 单词的长度 + for i := 0; i < qLen; i++ { + word := que[0] + que = que[1:] // 首位单词出队 + candidates := getCandidates(word) + for _, candidate := range candidates { + if _, exist := wordMap[candidate]; exist { // 用生成的结果集去查询 + if candidate == endWord { + return depth + 1 + } + delete(wordMap, candidate) // 删除集合中的用过的结果 + que = append(que, candidate) + } + } + } + } + return 0 +} + + +// 获取单词Map为后续的查询增加速度 +func getWordMap(wordList []string, beginWord string) map[string]int { + wordMap := make(map[string]int) + for i, word := range wordList { + if _, exist := wordMap[word]; !exist { + if word != beginWord { + wordMap[word] = i + } + } + } + return wordMap +} + +// 用26个英文字母分别替换掉各个位置的字母,生成一个结果集 +func getCandidates(word string) []string { + var res []string + for i := 0; i < 26; i++ { + for j := 0; j < len(word); j++ { + if word[j] != byte(int('a')+i) { + res = append(res, word[:j]+string(int('a')+i)+word[j+1:]) + } + } + } + return res +} +``` + +### JavaScript +```javascript +var ladderLength = function(beginWord, endWord, wordList) { + // 将wordList转成Set,提高查询速度 + const wordSet = new Set(wordList); + // Set元素个数为0 或者 endWord没有在wordSet出现,直接返回0 + if (wordSet.size === 0 || !wordSet.has(endWord)) return 0; + // 记录word是否访问过 + const visitMap = new Map();// + // 初始化队列 + const queue = []; + queue.push(beginWord); + // 初始化visitMap + visitMap.set(beginWord, 1); + + while(queue.length !== 0){ + let word = queue.shift(); // 删除队首元素,将它的值存放在word + let path = visitMap.get(word); // 这个word的路径长度 + for(let i = 0; i < word.length; i++){ // 遍历单词的每个字符 + for (let c = 97; c <= 122; c++) { // 对应26个字母ASCII值 从'a' 到 'z' 遍历替换 + // 拼串得到新的字符串 + let newWord = word.slice(0, i) + String.fromCharCode(c) + word.slice(i + 1); + if(newWord === endWord) return path + 1; // 找到了end,返回path+1 + // wordSet出现了newWord,并且newWord没有被访问过 + if(wordSet.has(newWord) && !visitMap.has(newWord)) { + // 添加访问信息 + visitMap.set(newWord, path + 1); + queue.push(newWord); + } + } + } + } + return 0; +}; +``` + +### TypeScript +```typescript +function ladderLength( + beginWord: string, + endWord: string, + wordList: string[] +): number { + const words = new Set(wordList); + if (!words.has(endWord)) return 0; + if (beginWord.length === 1) return 2; + let current = new Set([beginWord]); + let rightcurrent = new Set([endWord]); + words.delete(endWord); + let step = 1; + while (current.size) { + if (current.size > rightcurrent.size) { + [current, rightcurrent] = [rightcurrent, current]; + } + const temp: Set = new Set(); + for (const word of current) { + for (const right of rightcurrent) { + if (diffonechar(word, right)) { + return step + 1; + } + } + for (const other of words) { + if (diffonechar(other, word)) { + temp.add(other); + + words.delete(other); + } + } + } + if (temp.size === 0) return 0; + current = temp; + step = step + 1; + } + return 0; +} + +function diffonechar(word1: string, word2: string): boolean { + let changes = 0; + for (let i = 0; i < word1.length; i++) { + if (word1[i] != word2[i]) changes += 1; + } + return changes === 1; +} +``` + + diff --git "a/problems/0129.\346\261\202\346\240\271\345\210\260\345\217\266\345\255\220\350\212\202\347\202\271\346\225\260\345\255\227\344\271\213\345\222\214.md" "b/problems/0129.\346\261\202\346\240\271\345\210\260\345\217\266\345\255\220\350\212\202\347\202\271\346\225\260\345\255\227\344\271\213\345\222\214.md" new file mode 100755 index 0000000000..1568a49469 --- /dev/null +++ "b/problems/0129.\346\261\202\346\240\271\345\210\260\345\217\266\345\255\220\350\212\202\347\202\271\346\225\260\345\255\227\344\271\213\345\222\214.md" @@ -0,0 +1,383 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 129. 求根节点到叶节点数字之和 + +[力扣题目链接](https://leetcode.cn/problems/sum-root-to-leaf-numbers/) + +## 思路 + +本题和[113.路径总和II](https://programmercarl.com/0112.路径总和.html#_113-路径总和ii)是类似的思路,做完这道题,可以顺便把[113.路径总和II](https://programmercarl.com/0112.路径总和.html#_113-路径总和ii) 和 [112.路径总和](https://programmercarl.com/0112.路径总和.html#_112-路径总和) 做了。 + +结合112.路径总和 和 113.路径总和II,我在讲了[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html),如果大家对二叉树递归函数什么时候需要返回值很迷茫,可以看一下。 + +接下来在看本题,就简单多了,本题其实需要使用回溯,但一些同学可能都不知道自己用了回溯,在[二叉树:以为使用了递归,其实还隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html)中,我详细讲解了二叉树的递归中,如何使用了回溯。 + +接下来我们来看题: + +首先思路很明确,就是要遍历整个树把更节点到叶子节点组成的数字相加。 + +那么先按递归三部曲来分析: + +### 递归三部曲 + +如果对递归三部曲不了解的话,可以看这里:[二叉树:前中后递归详解](https://programmercarl.com/二叉树的递归遍历.html) + +* 确定递归函数返回值及其参数 + +这里我们要遍历整个二叉树,且需要要返回值做逻辑处理,所有返回值为void,在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html)中,详细讲解了返回值问题。 + +参数只需要把根节点传入,此时还需要定义两个全局遍历,一个是result,记录最终结果,一个是vector path。 + +**为什么用vector类型(就是数组)呢? 因为用vector方便我们做回溯!** + +所以代码如下: + +``` +int result; +vector path; +void traversal(TreeNode* cur) +``` + +* 确定终止条件 + +递归什么时候终止呢? + +当然是遇到叶子节点,此时要收集结果了,通知返回本层递归,因为单条路径的结果使用vector,我们需要一个函数vectorToInt把vector转成int。 + +终止条件代码如下: + +``` +if (!cur->left && !cur->right) { // 遇到了叶子节点 + result += vectorToInt(path); + return; +} +``` + +这里vectorToInt函数就是把数组转成int,代码如下: + +```CPP +int vectorToInt(const vector& vec) { + int sum = 0; + for (int i = 0; i < vec.size(); i++) { + sum = sum * 10 + vec[i]; + } + return sum; +} +``` + + +* 确定递归单层逻辑 + +本题其实采用前中后序都不无所谓, 因为也没有中间几点的处理逻辑。 + +这里主要是当左节点不为空,path收集路径,并递归左孩子,右节点同理。 + +**但别忘了回溯**。 + +如图: + + + + +代码如下: + +```CPP + // 中 +if (cur->left) { // 左 (空节点不遍历) + path.push_back(cur->left->val); + traversal(cur->left); // 递归 + path.pop_back(); // 回溯 +} +if (cur->right) { // 右 (空节点不遍历) + path.push_back(cur->right->val); + traversal(cur->right); // 递归 + path.pop_back(); // 回溯 +} +``` + +这里要注意回溯和递归要永远在一起,一个递归,对应一个回溯,是一对一的关系,有的同学写成如下代码: + +```CPP +if (cur->left) { // 左 (空节点不遍历) + path.push_back(cur->left->val); + traversal(cur->left); // 递归 +} +if (cur->right) { // 右 (空节点不遍历) + path.push_back(cur->right->val); + traversal(cur->right); // 递归 +} +path.pop_back(); // 回溯 +``` +**把回溯放在花括号外面了,世界上最遥远的距离,是你在花括号里,而我在花括号外!** 这就不对了。 + +整体C++代码 + +关键逻辑分析完了,整体C++代码如下: + +```CPP +class Solution { +private: + int result; + vector path; + // 把vector转化为int + int vectorToInt(const vector& vec) { + int sum = 0; + for (int i = 0; i < vec.size(); i++) { + sum = sum * 10 + vec[i]; + } + return sum; + } + void traversal(TreeNode* cur) { + if (!cur->left && !cur->right) { // 遇到了叶子节点 + result += vectorToInt(path); + return; + } + + if (cur->left) { // 左 (空节点不遍历) + path.push_back(cur->left->val); // 处理节点 + traversal(cur->left); // 递归 + path.pop_back(); // 回溯,撤销 + } + if (cur->right) { // 右 (空节点不遍历) + path.push_back(cur->right->val); // 处理节点 + traversal(cur->right); // 递归 + path.pop_back(); // 回溯,撤销 + } + return ; + } +public: + int sumNumbers(TreeNode* root) { + path.clear(); + if (root == nullptr) return 0; + path.push_back(root->val); + traversal(root); + return result; + } +}; +``` + +## 总结 + +过于简洁的代码,很容易让初学者忽视了本题中回溯的精髓,甚至作者本身都没有想清楚自己用了回溯。 + +**我这里提供的代码把整个回溯过程充分体现出来,希望可以帮助大家看的明明白白!** + + +## 其他语言版本 + +### Java: + +```java +class Solution { + List path = new ArrayList<>(); + int res = 0; + public int sumNumbers(TreeNode root) { + // 如果节点为0,那么就返回0 + if (root == null) return 0; + // 首先将根节点放到集合中 + path.add(root.val); + // 开始递归 + recur(root); + return res; + } + + public void recur(TreeNode root){ + if (root.left == null && root.right == null) { + // 当是叶子节点的时候,开始处理 + res += listToInt(path); + return; + } + + if (root.left != null){ + // 注意有回溯 + path.add(root.left.val); + recur(root.left); + path.remove(path.size() - 1); + } + if (root.right != null){ + // 注意有回溯 + path.add(root.right.val); + recur(root.right); + path.remove(path.size() - 1); + } + return; + } + public int listToInt(List path){ + int sum = 0; + for (Integer num:path){ + // sum * 10 表示进位 + sum = sum * 10 + num; + } + return sum; + } +} +``` + +### Python: + +```python +class Solution: + def sumNumbers(self, root: TreeNode) -> int: + res = 0 + path = [] + def backtrace(root): + nonlocal res + if not root: return # 节点空则返回 + path.append(root.val) + if not root.left and not root.right: # 遇到了叶子节点 + res += get_sum(path) + if root.left: # 左子树不空 + backtrace(root.left) + if root.right: # 右子树不空 + backtrace(root.right) + path.pop() + + def get_sum(arr): + s = 0 + for i in range(len(arr)): + s = s * 10 + arr[i] + return s + + backtrace(root) + return res +``` +### Go: + +```go +func sumNumbers(root *TreeNode) int { + sum := 0 + dfs(root, root.Val, &sum) + return sum +} + +func dfs(root *TreeNode, tmpSum int, sum *int) { + if root.Left == nil && root.Right == nil { + *sum += tmpSum + } else { + if root.Left != nil { + dfs(root.Left, tmpSum*10 + root.Left.Val, sum) + } + if root.Right != nil { + dfs(root.Right, tmpSum*10 + root.Right.Val, sum) + } + } +} +``` + + + +### JavaScript: + +```javascript +var sumNumbers = function(root) { + const listToInt = path => { + let sum = 0; + for(let num of path){ + // sum * 10 表示进位 + sum = sum * 10 + num; + } + return sum; + } + const recur = root =>{ + if (root.left == null && root.right == null) { + // 当是叶子节点的时候,开始处理 + res += listToInt(path); + return; + } + + if (root.left != null){ + // 注意有回溯 + path.push(root.left.val); + recur(root.left); + path.pop(); + } + if (root.right != null){ + // 注意有回溯 + path.push(root.right.val); + recur(root.right); + path.pop(); + } + return; + }; + const path = new Array(); + let res = 0; + // 如果节点为0,那么就返回0 + if (root == null) return 0; + // 首先将根节点放到集合中 + path.push(root.val); + // 开始递归 + recur(root); + return res; +}; +``` + +### TypeScript: + +```typescript +function sumNumbers(root: TreeNode | null): number { + if (root === null) return 0; + // 记录最终结果 + let resTotal: number = 0; + // 记录路径中遇到的节点值 + const route: number[] = []; + // 递归初始值 + route.push(root.val); + recur(root, route); + return resTotal; + function recur(node: TreeNode, route: number[]): void { + if (node.left === null && node.right === null) { + resTotal += listToSum(route); + return; + } + if (node.left !== null) { + route.push(node.left.val); + recur(node.left, route); + route.pop(); + }; + if (node.right !== null) { + route.push(node.right.val); + recur(node.right, route); + route.pop(); + }; + } + function listToSum(nums: number[]): number { + // 数组求和 + return Number(nums.join('')); + } +}; +``` + +### C: + +```c +//sum记录总和 +int sum; +void traverse(struct TreeNode *node, int val) { + //更新val为根节点到当前节点的和 + val = val * 10 + node->val; + //若当前节点为叶子节点,记录val + if(!node->left && !node->right) { + sum+=val; + return; + } + //若有左/右节点,遍历左/右节点 + if(node->left) + traverse(node->left, val); + if(node->right) + traverse(node->right, val); +} + +int sumNumbers(struct TreeNode* root){ + sum = 0; + + traverse(root, 0); + + return sum; +} +``` + + diff --git "a/problems/0130.\350\242\253\345\233\264\347\273\225\347\232\204\345\214\272\345\237\237.md" "b/problems/0130.\350\242\253\345\233\264\347\273\225\347\232\204\345\214\272\345\237\237.md" new file mode 100755 index 0000000000..10d6585c4c --- /dev/null +++ "b/problems/0130.\350\242\253\345\233\264\347\273\225\347\232\204\345\214\272\345\237\237.md" @@ -0,0 +1,793 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 130. 被围绕的区域 + +[题目链接](https://leetcode.cn/problems/surrounded-regions/) + +给你一个 m x n 的矩阵 board ,由若干字符 'X' 和 'O' ,找到所有被 'X' 围绕的区域,并将这些区域里所有的 'O' 用 'X' 填充。 + +![](https://file1.kamacoder.com/i/algo/20220901104745.png) + +* 输入:board = [["X","X","X","X"],["X","O","O","X"],["X","X","O","X"],["X","O","X","X"]] +* 输出:[["X","X","X","X"],["X","X","X","X"],["X","X","X","X"],["X","O","X","X"]] +* 解释:被围绕的区间不会存在于边界上,换句话说,任何边界上的 'O' 都不会被填充为 'X'。 任何不在边界上,或不与边界上的 'O' 相连的 'O' 最终都会被填充为 'X'。如果两个元素在水平或垂直方向相邻,则称它们是“相连”的。 + +## 思路 + +这道题目和1020. 飞地的数量正好反过来了,[1020. 飞地的数量](https://programmercarl.com/1020.%E9%A3%9E%E5%9C%B0%E7%9A%84%E6%95%B0%E9%87%8F.html)是求 地图中间的空格数,而本题是要把地图中间的'O'都改成'X'。 + +那么两题在思路上也是差不多的。 + +依然是从地图周边出发,将周边空格相邻的'O'都做上标记,然后在遍历一遍地图,遇到 'O' 且没做过标记的,那么都是地图中间的'O',全部改成'X'就行。 + +有的录友可能想,我在定义一个 visited 二维数组,单独标记周边的'O',然后遍历地图的时候同时对 数组board 和 数组visited 进行判断,是否'O'改成'X'。 + +这样做其实就有点麻烦了,不用额外定义空间了,标记周边的'O',可以直接改board的数值为其他特殊值。 + +步骤一:深搜或者广搜将地图周边的'O'全部改成'A',如图所示: + +![图一](https://file1.kamacoder.com/i/algo/20220902102337.png) + +步骤二:在遍历地图,将'O'全部改成'X'(地图中间的'O'改成了'X'),将'A'改回'O'(保留的地图周边的'O'),如图所示: + +![图二](https://file1.kamacoder.com/i/algo/20220902102831.png) + +整体C++代码如下,以下使用dfs实现,其实遍历方式dfs,bfs都是可以的。 + +```CPP +class Solution { +private: + int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 + void dfs(vector>& board, int x, int y) { + board[x][y] = 'A'; + for (int i = 0; i < 4; i++) { // 向四个方向遍历 + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + // 超过边界 + if (nextx < 0 || nextx >= board.size() || nexty < 0 || nexty >= board[0].size()) continue; + // 不符合条件,不继续遍历 + if (board[nextx][nexty] == 'X' || board[nextx][nexty] == 'A') continue; + dfs (board, nextx, nexty); + } + return; + } + +public: + void solve(vector>& board) { + int n = board.size(), m = board[0].size(); + // 步骤一: + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (board[i][0] == 'O') dfs(board, i, 0); + if (board[i][m - 1] == 'O') dfs(board, i, m - 1); + } + + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (board[0][j] == 'O') dfs(board, 0, j); + if (board[n - 1][j] == 'O') dfs(board, n - 1, j); + } + // 步骤二: + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (board[i][j] == 'O') board[i][j] = 'X'; + if (board[i][j] == 'A') board[i][j] = 'O'; + } + } + } +}; +``` + +## 其他语言版本 + +### Java + +```Java +// 广度优先遍历 +// 使用 visited 数组进行标记 +class Solution { + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; // 四个方向 + + public void solve(char[][] board) { + // rowSize:行的长度,colSize:列的长度 + int rowSize = board.length, colSize = board[0].length; + boolean[][] visited = new boolean[rowSize][colSize]; + Queue queue = new ArrayDeque<>(); + // 从左侧边,和右侧边遍历 + for (int row = 0; row < rowSize; row++) { + if (board[row][0] == 'O') { + visited[row][0] = true; + queue.add(new int[]{row, 0}); + } + if (board[row][colSize - 1] == 'O') { + visited[row][colSize - 1] = true; + queue.add(new int[]{row, colSize - 1}); + } + } + // 从上边和下边遍历,在对左侧边和右侧边遍历时我们已经遍历了矩阵的四个角 + // 所以在遍历上边和下边时可以不用遍历四个角 + for (int col = 1; col < colSize - 1; col++) { + if (board[0][col] == 'O') { + visited[0][col] = true; + queue.add(new int[]{0, col}); + } + if (board[rowSize - 1][col] == 'O') { + visited[rowSize - 1][col] = true; + queue.add(new int[]{rowSize - 1, col}); + } + } + // 广度优先遍历,把没有被 'X' 包围的 'O' 进行标记 + while (!queue.isEmpty()) { + int[] current = queue.poll(); + for (int[] pos: position) { + int row = current[0] + pos[0], col = current[1] + pos[1]; + // 如果范围越界、位置已被访问过、该位置的值不是 'O',就直接跳过 + if (row < 0 || row >= rowSize || col < 0 || col >= colSize) continue; + if (visited[row][col] || board[row][col] != 'O') continue; + visited[row][col] = true; + queue.add(new int[]{row, col}); + } + } + // 遍历数组,把没有被标记的 'O' 修改成 'X' + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + if (board[row][col] == 'O' && !visited[row][col]) board[row][col] = 'X'; + } + } + } +} +``` +```Java +// 广度优先遍历 +// 直接修改 board 的值为其他特殊值 +class Solution { + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; // 四个方向 + + public void solve(char[][] board) { + // rowSize:行的长度,colSize:列的长度 + int rowSize = board.length, colSize = board[0].length; + Queue queue = new ArrayDeque<>(); + // 从左侧边,和右侧边遍历 + for (int row = 0; row < rowSize; row++) { + if (board[row][0] == 'O') + queue.add(new int[]{row, 0}); + if (board[row][colSize - 1] == 'O') + queue.add(new int[]{row, colSize - 1}); + } + // 从上边和下边遍历,在对左侧边和右侧边遍历时我们已经遍历了矩阵的四个角 + // 所以在遍历上边和下边时可以不用遍历四个角 + for (int col = 1; col < colSize - 1; col++) { + if (board[0][col] == 'O') + queue.add(new int[]{0, col}); + if (board[rowSize - 1][col] == 'O') + queue.add(new int[]{rowSize - 1, col}); + } + // 广度优先遍历,把没有被 'X' 包围的 'O' 修改成特殊值 + while (!queue.isEmpty()) { + int[] current = queue.poll(); + board[current[0]][current[1]] = 'A'; + for (int[] pos: position) { + int row = current[0] + pos[0], col = current[1] + pos[1]; + // 如果范围越界、该位置的值不是 'O',就直接跳过 + if (row < 0 || row >= rowSize || col < 0 || col >= colSize) continue; + if (board[row][col] != 'O') continue; + queue.add(new int[]{row, col}); + } + } + // 遍历数组,把 'O' 修改成 'X',特殊值修改成 'O' + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + if (board[row][col] == 'A') board[row][col] = 'O'; + else if (board[row][col] == 'O') board[row][col] = 'X'; + } + } + } +} +``` +```Java +//BFS(使用helper function) +class Solution { + int[][] dir ={{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; + public void solve(char[][] board) { + for(int i = 0; i < board.length; i++){ + if(board[i][0] == 'O') bfs(board, i, 0); + if(board[i][board[0].length - 1] == 'O') bfs(board, i, board[0].length - 1); + } + + for(int j = 1 ; j < board[0].length - 1; j++){ + if(board[0][j] == 'O') bfs(board, 0, j); + if(board[board.length - 1][j] == 'O') bfs(board, board.length - 1, j); + } + + for(int i = 0; i < board.length; i++){ + for(int j = 0; j < board[0].length; j++){ + if(board[i][j] == 'O') board[i][j] = 'X'; + if(board[i][j] == 'A') board[i][j] = 'O'; + } + } + } + private void bfs(char[][] board, int x, int y){ + Queue que = new LinkedList<>(); + board[x][y] = 'A'; + que.offer(x); + que.offer(y); + + while(!que.isEmpty()){ + int currX = que.poll(); + int currY = que.poll(); + + for(int i = 0; i < 4; i++){ + int nextX = currX + dir[i][0]; + int nextY = currY + dir[i][1]; + + if(nextX < 0 || nextY < 0 || nextX >= board.length || nextY >= board[0].length) + continue; + if(board[nextX][nextY] == 'X'|| board[nextX][nextY] == 'A') + continue; + bfs(board, nextX, nextY); + } + } + } +} + +``` + +```Java +// 深度优先遍历 +// 使用 visited 数组进行标记 +class Solution { + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; // 四个方向 + + public void dfs(char[][] board, int row, int col, boolean[][] visited) { + for (int[] pos: position) { + int nextRow = row + pos[0], nextCol = col + pos[1]; + // 位置越界 + if (nextRow < 0 || nextRow >= board.length || nextCol < 0 || nextCol >= board[0].length) + continue; + // 位置已被访问过、新位置值不是 'O' + if (visited[nextRow][nextCol] || board[nextRow][nextCol] != 'O') continue; + visited[nextRow][nextCol] = true; + dfs(board, nextRow, nextCol, visited); + } + } + + public void solve(char[][] board) { + int rowSize = board.length, colSize = board[0].length; + boolean[][] visited = new boolean[rowSize][colSize]; + // 从左侧遍、右侧遍遍历 + for (int row = 0; row < rowSize; row++) { + if (board[row][0] == 'O' && !visited[row][0]) { + visited[row][0] = true; + dfs(board, row, 0, visited); + } + if (board[row][colSize - 1] == 'O' && !visited[row][colSize - 1]) { + visited[row][colSize - 1] = true; + dfs(board, row, colSize - 1, visited); + } + } + // 从上边和下边遍历,在对左侧边和右侧边遍历时我们已经遍历了矩阵的四个角 + // 所以在遍历上边和下边时可以不用遍历四个角 + for (int col = 1; col < colSize - 1; col++) { + if (board[0][col] == 'O' && !visited[0][col]) { + visited[0][col] = true; + dfs(board, 0, col, visited); + } + if (board[rowSize - 1][col] == 'O' && !visited[rowSize - 1][col]) { + visited[rowSize - 1][col] = true; + dfs(board, rowSize - 1, col, visited); + } + } + // 遍历数组,把没有被标记的 'O' 修改成 'X' + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + if (board[row][col] == 'O' && !visited[row][col]) board[row][col] = 'X'; + } + } + } +} +``` +```Java +// 深度优先遍历 +// // 直接修改 board 的值为其他特殊值 +class Solution { + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; // 四个方向 + + public void dfs(char[][] board, int row, int col) { + for (int[] pos: position) { + int nextRow = row + pos[0], nextCol = col + pos[1]; + // 位置越界 + if (nextRow < 0 || nextRow >= board.length || nextCol < 0 || nextCol >= board[0].length) + continue; + // 新位置值不是 'O' + if (board[nextRow][nextCol] != 'O') continue; + board[nextRow][nextCol] = 'A'; // 修改为特殊值 + dfs(board, nextRow, nextCol); + } + } + + public void solve(char[][] board) { + int rowSize = board.length, colSize = board[0].length; + // 从左侧遍、右侧遍遍历 + for (int row = 0; row < rowSize; row++) { + if (board[row][0] == 'O') { + board[row][0] = 'A'; + dfs(board, row, 0); + } + if (board[row][colSize - 1] == 'O') { + board[row][colSize - 1] = 'A'; + dfs(board, row, colSize - 1); + } + } + // 从上边和下边遍历,在对左侧边和右侧边遍历时我们已经遍历了矩阵的四个角 + // 所以在遍历上边和下边时可以不用遍历四个角 + for (int col = 1; col < colSize - 1; col++) { + if (board[0][col] == 'O') { + board[0][col] = 'A'; + dfs(board, 0, col); + } + if (board[rowSize - 1][col] == 'O') { + board[rowSize - 1][col] = 'A'; + dfs(board, rowSize - 1, col); + } + } + // 遍历数组,把 'O' 修改成 'X',特殊值修改成 'O' + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + if (board[row][col] == 'O') board[row][col] = 'X'; + else if (board[row][col] == 'A') board[row][col] = 'O'; + } + } + } +} +``` +```java +//DFS(有終止條件) +class Solution { + int[][] dir ={{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; + public void solve(char[][] board) { + + for(int i = 0; i < board.length; i++){ + if(board[i][0] == 'O') dfs(board, i, 0); + if(board[i][board[0].length - 1] == 'O') dfs(board, i, board[0].length - 1); + } + + for(int j = 1 ; j < board[0].length - 1; j++){ + if(board[0][j] == 'O') dfs(board, 0, j); + if(board[board.length - 1][j] == 'O') dfs(board, board.length - 1, j); + } + + for(int i = 0; i < board.length; i++){ + for(int j = 0; j < board[0].length; j++){ + if(board[i][j] == 'O') board[i][j] = 'X'; + if(board[i][j] == 'A') board[i][j] = 'O'; + } + } + } + + private void dfs(char[][] board, int x, int y){ + if(board[x][y] == 'X'|| board[x][y] == 'A') + return; + board[x][y] = 'A'; + for(int i = 0; i < 4; i++){ + int nextX = x + dir[i][0]; + int nextY = y + dir[i][1]; + + if(nextX < 0 || nextY < 0 || nextX >= board.length || nextY >= board[0].length) + continue; + // if(board[nextX][nextY] == 'X'|| board[nextX][nextY] == 'A') + // continue; + dfs(board, nextX, nextY); + } + } +} +``` +### Python3 + +```Python +// 深度优先遍历 +class Solution: + dir_list = [(0, 1), (0, -1), (1, 0), (-1, 0)] + def solve(self, board: List[List[str]]) -> None: + """ + Do not return anything, modify board in-place instead. + """ + row_size = len(board) + column_size = len(board[0]) + visited = [[False] * column_size for _ in range(row_size)] + # 从边缘开始,将边缘相连的O改成A。然后遍历所有,将A改成O,O改成X + # 第一行和最后一行 + for i in range(column_size): + if board[0][i] == "O" and not visited[0][i]: + self.dfs(board, 0, i, visited) + if board[row_size-1][i] == "O" and not visited[row_size-1][i]: + self.dfs(board, row_size-1, i, visited) + + # 第一列和最后一列 + for i in range(1, row_size-1): + if board[i][0] == "O" and not visited[i][0]: + self.dfs(board, i, 0, visited) + if board[i][column_size-1] == "O" and not visited[i][column_size-1]: + self.dfs(board, i, column_size-1, visited) + + for i in range(row_size): + for j in range(column_size): + if board[i][j] == "A": + board[i][j] = "O" + elif board[i][j] == "O": + board[i][j] = "X" + + + def dfs(self, board, x, y, visited): + if visited[x][y] or board[x][y] == "X": + return + visited[x][y] = True + board[x][y] = "A" + for i in range(4): + new_x = x + self.dir_list[i][0] + new_y = y + self.dir_list[i][1] + if new_x >= len(board) or new_y >= len(board[0]) or new_x < 0 or new_y < 0: + continue + self.dfs(board, new_x, new_y, visited) + +``` + +### JavaScript +```JavaScript +/** + * @description 深度搜索优先 + * @param {character[][]} board + * @return {void} Do not return anything, modify board in-place instead. + */ +function solve(board) { + const dir = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + const [rowSize, colSize] = [board.length, board[0].length]; + + function dfs(board, x, y) { + board[x][y] = 'A'; + for (let i = 0; i < 4; i++) { + const nextX = dir[i][0] + x; + const nextY = dir[i][1] + y; + if (nextX < 0 || nextX >= rowSize || nextY < 0 || nextY >= colSize) { + continue; + } + if (board[nextX][nextY] === 'O') { + dfs(board, nextX, nextY); + } + } + } + + for (let i = 0; i < rowSize; i++) { + if (board[i][0] === 'O') { + dfs(board, i, 0); + } + if (board[i][colSize - 1] === 'O') { + dfs(board, i, colSize - 1); + } + } + + for (let i = 1; i < colSize - 1; i++) { + if (board[0][i] === 'O') { + dfs(board, 0, i); + } + if (board[rowSize - 1][i] === 'O') { + dfs(board, rowSize - 1, i); + } + } + + for (let i = 0; i < rowSize; i++) { + for (let k = 0; k < colSize; k++) { + if (board[i][k] === 'A') { + board[i][k] = 'O'; + } else if (board[i][k] === 'O') { + board[i][k] = 'X'; + } + } + } +} + +/** + * @description 广度搜索优先 + * @param {character[][]} board + * @return {void} Do not return anything, modify board in-place instead. + */ +function solve(board) { + const dir = [[-1, 0], [1, 0], [0, -1], [0, 1]]; + const [rowSize, colSize] = [board.length, board[0].length]; + + function bfs(board, x, y) { + board[x][y] = 'A'; + const stack = [y, x]; + + while (stack.length !== 0) { + const top = [stack.pop(), stack.pop()]; + for (let i = 0; i < 4; i++) { + const nextX = dir[i][0] + top[0]; + const nextY = dir[i][1] + top[1]; + + if (nextX < 0 || nextX >= rowSize || nextY < 0 || nextY >= colSize) { + continue; + } + + if (board[nextX][nextY] === 'O') { + board[nextX][nextY] = 'A'; + stack.push(nextY, nextX); + } + } + } + + for (let i = 0; i < 4; i++) { + const nextX = dir[i][0] + x; + const nextY = dir[i][1] + y; + if (nextX < 0 || nextX >= rowSize || nextY < 0 || nextY >= colSize) { + continue; + } + if (board[nextX][nextY] === 'O') { + dfs(board, nextX, nextY); + } + } + } + + for (let i = 0; i < rowSize; i++) { + if (board[i][0] === 'O') { + bfs(board, i, 0); + } + if (board[i][colSize - 1] === 'O') { + bfs(board, i, colSize - 1); + } + } + + for (let i = 1; i < colSize - 1; i++) { + if (board[0][i] === 'O') { + bfs(board, 0, i); + } + if (board[rowSize - 1][i] === 'O') { + bfs(board, rowSize - 1, i); + } + } + + for (let i = 0; i < rowSize; i++) { + for (let k = 0; k < colSize; k++) { + if (board[i][k] === 'A') { + board[i][k] = 'O'; + } else if (board[i][k] === 'O') { + board[i][k] = 'X'; + } + } + } +} +``` + +### Go + +dfs: + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} + +func solve(board [][]byte) { + rows, cols := len(board), len(board[0]) + // 列 + for i := 0; i < rows; i++ { + if board[i][0] == 'O' { + dfs(board, i, 0) + } + if board[i][cols-1] == 'O' { + dfs(board, i, cols-1) + } + } + // 行 + for j := 0; j < cols; j++ { + if board[0][j] == 'O' { + dfs(board, 0, j) + } + if board[rows-1][j] == 'O' { + dfs(board, rows-1, j) + } + } + + for _, r := range board { + for j, c := range r { + if c == 'A' { + r[j] = 'O' + continue + } + if c == 'O' { + r[j] = 'X' + } + } + } +} + +func dfs(board [][]byte, i, j int) { + board[i][j] = 'A' + for _, d := range DIRECTIONS { + x, y := i+d[0], j+d[1] + if x < 0 || x >= len(board) || y < 0 || y >= len(board[0]) { + continue + } + if board[x][y] == 'O' { + dfs(board, x, y) + } + } +} +``` + +bfs: + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} + +func solve(board [][]byte) { + rows, cols := len(board), len(board[0]) + // 列 + for i := 0; i < rows; i++ { + if board[i][0] == 'O' { + bfs(board, i, 0) + } + if board[i][cols-1] == 'O' { + bfs(board, i, cols-1) + } + } + // 行 + for j := 0; j < cols; j++ { + if board[0][j] == 'O' { + bfs(board, 0, j) + } + if board[rows-1][j] == 'O' { + bfs(board, rows-1, j) + } + } + + for _, r := range board { + for j, c := range r { + if c == 'A' { + r[j] = 'O' + continue + } + if c == 'O' { + r[j] = 'X' + } + } + } +} + +func bfs(board [][]byte, i, j int) { + queue := [][]int{{i, j}} + board[i][j] = 'A' + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + for _, d := range DIRECTIONS { + x, y := cur[0]+d[0], cur[1]+d[1] + if x < 0 || x >= len(board) || y < 0 || y >= len(board[0]) { + continue + } + if board[x][y] == 'O' { + board[x][y] = 'A' + queue = append(queue, []int{x, y}) + } + } + } +} +``` + +### Rust + +bfs: + +```rust +impl Solution { + const DIRECTIONS: [(isize, isize); 4] = [(0, 1), (0, -1), (1, 0), (-1, 0)]; + pub fn solve(board: &mut Vec>) { + let (rows, cols) = (board.len(), board[0].len()); + // 列 + for i in 0..rows { + if board[i][0] == 'O' { + Self::dfs(board, i, 0); + } + if board[i][cols - 1] == 'O' { + Self::dfs(board, i, cols - 1); + } + } + //行 + for j in 0..cols { + if board[0][j] == 'O' { + Self::dfs(board, 0, j); + } + if board[rows - 1][j] == 'O' { + Self::dfs(board, rows - 1, j); + } + } + + for v in board.iter_mut() { + for c in v.iter_mut() { + if *c == 'A' { + *c = 'O'; + continue; + } + if *c == 'O' { + *c = 'X'; + } + } + } + } + + pub fn dfs(board: &mut [Vec], i: usize, j: usize) { + board[i][j] = 'A'; + for (d1, d2) in Self::DIRECTIONS { + let (x, y) = (i as isize + d1, j as isize + d2); + if x < 0 || x >= board.len() as isize || y < 0 || y >= board[0].len() as isize { + continue; + } + let (x, y) = (x as usize, y as usize); + if board[x][y] == 'O' { + Self::dfs(board, x, y); + } + } + } +} +``` + +bfs: + +```rust +use std::collections::VecDeque; +impl Solution { + const DIRECTIONS: [(isize, isize); 4] = [(0, 1), (0, -1), (1, 0), (-1, 0)]; + pub fn solve(board: &mut Vec>) { + let (rows, cols) = (board.len(), board[0].len()); + // 列 + for i in 0..rows { + if board[i][0] == 'O' { + Self::bfs(board, i, 0); + } + if board[i][cols - 1] == 'O' { + Self::bfs(board, i, cols - 1); + } + } + //行 + for j in 0..cols { + if board[0][j] == 'O' { + Self::bfs(board, 0, j); + } + if board[rows - 1][j] == 'O' { + Self::bfs(board, rows - 1, j); + } + } + + for v in board.iter_mut() { + for c in v.iter_mut() { + if *c == 'A' { + *c = 'O'; + continue; + } + if *c == 'O' { + *c = 'X'; + } + } + } + } + + pub fn bfs(board: &mut [Vec], i: usize, j: usize) { + let mut queue = VecDeque::from([(i, j)]); + board[i][j] = 'A'; + while let Some((i, j)) = queue.pop_front() { + for (d1, d2) in Self::DIRECTIONS { + let (x, y) = (i as isize + d1, j as isize + d2); + if x < 0 || x >= board.len() as isize || y < 0 || y >= board[0].len() as isize { + continue; + } + let (x, y) = (x as usize, y as usize); + if board[x][y] == 'O' { + board[x][y] = 'A'; + queue.push_back((x, y)); + } + } + } + } +} +``` + + diff --git "a/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" "b/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" new file mode 100755 index 0000000000..c76f1ce2f1 --- /dev/null +++ "b/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" @@ -0,0 +1,1007 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 切割问题其实是一种组合问题! + +# 131.分割回文串 + +[力扣题目链接](https://leetcode.cn/problems/palindrome-partitioning/) + +给定一个字符串 s,将 s 分割成一些子串,使每个子串都是回文串。 + +返回 s 所有可能的分割方案。 + +示例: +输入: "aab" +输出: +[ + ["aa","b"], + ["a","a","b"] +] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[131.分割回文串](https://www.bilibili.com/video/BV1c54y1e7k6),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题这涉及到两个关键问题: + +1. 切割问题,有不同的切割方式 +2. 判断回文 + +相信这里不同的切割方式可以搞懵很多同学了。 + +这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。 + +一些同学可能想不清楚 回溯究竟是如何切割字符串呢? + +我们来分析一下切割,**其实切割问题类似组合问题**。 + +例如对于字符串abcdef: + +* 组合问题:选取一个a之后,在bcdef中再去选取第二个,选取b之后在cdef中再选取第三个.....。 +* 切割问题:切割一个a之后,在bcdef中再去切割第二段,切割b之后在cdef中再切割第三段.....。 + +感受出来了不? + +所以切割问题,也可以抽象为一棵树形结构,如图: + +![131.分割回文串](https://file1.kamacoder.com/i/algo/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) + +递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。 + +此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。 + +### 回溯三部曲 + +* 递归函数参数 + +全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里) + +本题递归函数参数还需要startIndex,因为切割过的地方,不能重复切割,和组合问题也是保持一致的。 + +在[回溯算法:求组合总和(二)](https://programmercarl.com/0039.组合总和.html)中我们深入探讨了组合问题什么时候需要startIndex,什么时候不需要startIndex。 + +代码如下: + +```CPP +vector> result; +vector path; // 放已经回文的子串 +void backtracking (const string& s, int startIndex) { +``` + +* 递归函数终止条件 + +![131.分割回文串](https://file1.kamacoder.com/i/algo/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) + +从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止条件。 + +**那么在代码里什么是切割线呢?** + +在处理组合问题的时候,递归参数需要传入startIndex,表示下一轮递归遍历的起始位置,这个startIndex就是切割线。 + +所以终止条件代码如下: + +```CPP +void backtracking (const string& s, int startIndex) { + // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 + if (startIndex >= s.size()) { + result.push_back(path); + return; + } +} +``` + +* 单层搜索的逻辑 + +**来看看在递归循环中如何截取子串呢?** + +在`for (int i = startIndex; i < s.size(); i++)`循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。 + +首先判断这个子串是不是回文,如果是回文,就加入在`vector path`中,path用来记录切割过的回文子串。 + +代码如下: + +```CPP +for (int i = startIndex; i < s.size(); i++) { + if (isPalindrome(s, startIndex, i)) { // 是回文子串 + // 获取[startIndex,i]在s中的子串 + string str = s.substr(startIndex, i - startIndex + 1); + path.push_back(str); + } else { // 如果不是则直接跳过 + continue; + } + backtracking(s, i + 1); // 寻找i+1为起始位置的子串 + path.pop_back(); // 回溯过程,弹出本次已经添加的子串 +} +``` + +**注意切割过的位置,不能重复切割,所以,backtracking(s, i + 1); 传入下一层的起始位置为i + 1**。 + +### 判断回文子串 + +最后我们看一下回文子串要如何判断了,判断一个字符串是否是回文。 + +可以使用双指针法,一个指针从前向后,一个指针从后向前,如果前后指针所指向的元素是相等的,就是回文字符串了。 + +那么判断回文的C++代码如下: + +```CPP + bool isPalindrome(const string& s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + if (s[i] != s[j]) { + return false; + } + } + return true; + } +``` + +如果大家对双指针法有生疏了,传送门:[双指针法:总结篇!](https://programmercarl.com/双指针总结.html) + +此时关键代码已经讲解完毕,整体代码如下(详细注释了) + +根据Carl给出的回溯算法模板: + +```CPP +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} + +``` + +不难写出如下代码: + +```CPP +class Solution { +private: + vector> result; + vector path; // 放已经回文的子串 + void backtracking (const string& s, int startIndex) { + // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 + if (startIndex >= s.size()) { + result.push_back(path); + return; + } + for (int i = startIndex; i < s.size(); i++) { + if (isPalindrome(s, startIndex, i)) { // 是回文子串 + // 获取[startIndex,i]在s中的子串 + string str = s.substr(startIndex, i - startIndex + 1); + path.push_back(str); + } else { // 不是回文,跳过 + continue; + } + backtracking(s, i + 1); // 寻找i+1为起始位置的子串 + path.pop_back(); // 回溯过程,弹出本次已经添加的子串 + } + } + bool isPalindrome(const string& s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + if (s[i] != s[j]) { + return false; + } + } + return true; + } +public: + vector> partition(string s) { + result.clear(); + path.clear(); + backtracking(s, 0); + return result; + } +}; +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n^2) + +## 优化 + +上面的代码还存在一定的优化空间, 在于如何更高效的计算一个子字符串是否是回文字串。上述代码```isPalindrome```函数运用双指针的方法来判定对于一个字符串```s```, 给定起始下标和终止下标, 截取出的子字符串是否是回文字串。但是其中有一定的重复计算存在: + +例如给定字符串```"abcde"```, 在已知```"bcd"```不是回文字串时, 不再需要去双指针操作```"abcde"```而可以直接判定它一定不是回文字串。 + +具体来说, 给定一个字符串`s`, 长度为```n```, 它成为回文字串的充分必要条件是```s[0] == s[n-1]```且```s[1:n-1]```是回文字串。 + +大家如果熟悉动态规划这种算法的话, 我们可以高效地事先一次性计算出, 针对一个字符串```s```, 它的任何子串是否是回文字串, 然后在我们的回溯函数中直接查询即可, 省去了双指针移动判定这一步骤. + +具体参考代码如下: + +```CPP +class Solution { +private: + vector> result; + vector path; // 放已经回文的子串 + vector> isPalindrome; // 放事先计算好的是否回文子串的结果 + void backtracking (const string& s, int startIndex) { + // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 + if (startIndex >= s.size()) { + result.push_back(path); + return; + } + for (int i = startIndex; i < s.size(); i++) { + if (isPalindrome[startIndex][i]) { // 是回文子串 + // 获取[startIndex,i]在s中的子串 + string str = s.substr(startIndex, i - startIndex + 1); + path.push_back(str); + } else { // 不是回文,跳过 + continue; + } + backtracking(s, i + 1); // 寻找i+1为起始位置的子串 + path.pop_back(); // 回溯过程,弹出本次已经添加的子串 + } + } + void computePalindrome(const string& s) { + // isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串 + isPalindrome.resize(s.size(), vector(s.size(), false)); // 根据字符串s, 刷新布尔矩阵的大小 + for (int i = s.size() - 1; i >= 0; i--) { + // 需要倒序计算, 保证在i行时, i+1行已经计算好了 + for (int j = i; j < s.size(); j++) { + if (j == i) {isPalindrome[i][j] = true;} + else if (j - i == 1) {isPalindrome[i][j] = (s[i] == s[j]);} + else {isPalindrome[i][j] = (s[i] == s[j] && isPalindrome[i+1][j-1]);} + } + } + } +public: + vector> partition(string s) { + result.clear(); + path.clear(); + computePalindrome(s); + backtracking(s, 0); + return result; + } +}; + +``` + +## 总结 + +这道题目在leetcode上是中等,但可以说是hard的题目了,但是代码其实就是按照模板的样子来的。 + +那么难究竟难在什么地方呢? + +**我列出如下几个难点:** + +* 切割问题可以抽象为组合问题 +* 如何模拟那些切割线 +* 切割问题中递归如何终止 +* 在递归循环中如何截取子串 +* 如何判断回文 + +**我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力**。 + +一些同学可能遇到题目比较难,但是不知道题目难在哪里,反正就是很难。其实这样还是思维不够清晰,这种总结的能力需要多接触多锻炼。 + +**本题我相信很多同学主要卡在了第一个难点上:就是不知道如何切割,甚至知道要用回溯法,也不知道如何用。也就是没有体会到按照求组合问题的套路就可以解决切割**。 + +如果意识到这一点,算是重大突破了。接下来就可以对着模板照葫芦画瓢。 + +**但接下来如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了**。 + +关于模拟切割线,其实就是index是上一层已经确定了的分割线,i是这一层试图寻找的新分割线 + +除了这些难点,**本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1**。 + +所以本题应该是一道hard题目了。 + +**可能刷过这道题目的录友都没感受到自己原来克服了这么多难点,就把这道题目AC了**,这应该叫做无招胜有招,人码合一。 + + + +## 其他语言版本 + + +### Java +```Java +class Solution { + //保持前几题一贯的格式, initialization + List> res = new ArrayList<>(); + List cur = new ArrayList<>(); + public List> partition(String s) { + backtracking(s, 0, new StringBuilder()); + return res; + } + private void backtracking(String s, int start, StringBuilder sb){ + //因为是起始位置一个一个加的,所以结束时start一定等于s.length,因为进入backtracking时一定末尾也是回文,所以cur是满足条件的 + if (start == s.length()){ + //注意创建一个新的copy + res.add(new ArrayList<>(cur)); + return; + } + //像前两题一样从前往后搜索,如果发现回文,进入backtracking,起始位置后移一位,循环结束照例移除cur的末位 + for (int i = start; i < s.length(); i++){ + sb.append(s.charAt(i)); + if (check(sb)){ + cur.add(sb.toString()); + backtracking(s, i + 1, new StringBuilder()); + cur.remove(cur.size() -1 ); + } + } + } + + //helper method, 检查是否是回文 + private boolean check(StringBuilder sb){ + for (int i = 0; i < sb.length()/ 2; i++){ + if (sb.charAt(i) != sb.charAt(sb.length() - 1 - i)){return false;} + } + return true; + } +} +``` + +### Java +回溯+动态规划优化回文串判断 +```Java +class Solution { + List> result; + LinkedList path; + boolean[][] dp; + + public List> partition(String s) { + result = new ArrayList<>(); + char[] str = s.toCharArray(); + path = new LinkedList<>(); + dp = new boolean[str.length + 1][str.length + 1]; + isPalindrome(str); + backtracking(s, 0); + return result; + } + + public void backtracking(String str, int startIndex) { + if (startIndex >= str.length()) { + //如果起始位置大于s的大小,说明找到了一组分割方案 + result.add(new ArrayList<>(path)); + } else { + for (int i = startIndex; i < str.length(); ++i) { + if (dp[startIndex][i]) { + //是回文子串,进入下一步递归 + //先将当前子串保存入path + path.addLast(str.substring(startIndex, i + 1)); + //起始位置后移,保证不重复 + backtracking(str, i + 1); + path.pollLast(); + } else { + //不是回文子串,跳过 + continue; + } + } + } + } + + //通过动态规划判断是否是回文串,参考动态规划篇 52 回文子串 + public void isPalindrome(char[] str) { + for (int i = 0; i <= str.length; ++i) { + dp[i][i] = true; + } + for (int i = 1; i < str.length; ++i) { + for (int j = i; j >= 0; --j) { + if (str[j] == str[i]) { + if (i - j <= 1) { + dp[j][i] = true; + } else if (dp[j + 1][i - 1]) { + dp[j][i] = true; + } + } + } + } + } +} +``` + +### Python +回溯 基本版 +```python +class Solution: + + def partition(self, s: str) -> List[List[str]]: + ''' + 递归用于纵向遍历 + for循环用于横向遍历 + 当切割线迭代至字符串末尾,说明找到一种方法 + 类似组合问题,为了不重复切割同一位置,需要start_index来做标记下一轮递归的起始位置(切割线) + ''' + result = [] + self.backtracking(s, 0, [], result) + return result + + def backtracking(self, s, start_index, path, result ): + # Base Case + if start_index == len(s): + result.append(path[:]) + return + + # 单层递归逻辑 + for i in range(start_index, len(s)): + # 此次比其他组合题目多了一步判断: + # 判断被截取的这一段子串([start_index, i])是否为回文串 + if self.is_palindrome(s, start_index, i): + path.append(s[start_index:i+1]) + self.backtracking(s, i+1, path, result) # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串 + path.pop() # 回溯 + + + def is_palindrome(self, s: str, start: int, end: int) -> bool: + i: int = start + j: int = end + while i < j: + if s[i] != s[j]: + return False + i += 1 + j -= 1 + return True +``` +回溯+优化判定回文函数 +```python +class Solution: + + def partition(self, s: str) -> List[List[str]]: + result = [] + self.backtracking(s, 0, [], result) + return result + + def backtracking(self, s, start_index, path, result ): + # Base Case + if start_index == len(s): + result.append(path[:]) + return + + # 单层递归逻辑 + for i in range(start_index, len(s)): + # 若反序和正序相同,意味着这是回文串 + if s[start_index: i + 1] == s[start_index: i + 1][::-1]: + path.append(s[start_index:i+1]) + self.backtracking(s, i+1, path, result) # 递归纵向遍历:从下一处进行切割,判断其余是否仍为回文串 + path.pop() # 回溯 + +``` +回溯+高效判断回文子串 +```python +class Solution: + def partition(self, s: str) -> List[List[str]]: + result = [] + isPalindrome = [[False] * len(s) for _ in range(len(s))] # 初始化isPalindrome矩阵 + self.computePalindrome(s, isPalindrome) + self.backtracking(s, 0, [], result, isPalindrome) + return result + + def backtracking(self, s, startIndex, path, result, isPalindrome): + if startIndex >= len(s): + result.append(path[:]) + return + + for i in range(startIndex, len(s)): + if isPalindrome[startIndex][i]: # 是回文子串 + substring = s[startIndex:i + 1] + path.append(substring) + self.backtracking(s, i + 1, path, result, isPalindrome) # 寻找i+1为起始位置的子串 + path.pop() # 回溯过程,弹出本次已经添加的子串 + + def computePalindrome(self, s, isPalindrome): + for i in range(len(s) - 1, -1, -1): # 需要倒序计算,保证在i行时,i+1行已经计算好了 + for j in range(i, len(s)): + if j == i: + isPalindrome[i][j] = True + elif j - i == 1: + isPalindrome[i][j] = (s[i] == s[j]) + else: + isPalindrome[i][j] = (s[i] == s[j] and isPalindrome[i+1][j-1]) +``` +回溯+使用all函数判断回文子串 +```python +class Solution: + def partition(self, s: str) -> List[List[str]]: + result = [] + self.partition_helper(s, 0, [], result) + return result + + def partition_helper(self, s, start_index, path, result): + if start_index == len(s): + result.append(path[:]) + return + + for i in range(start_index + 1, len(s) + 1): + sub = s[start_index:i] + if self.isPalindrome(sub): + path.append(sub) + self.partition_helper(s, i, path, result) + path.pop() + + def isPalindrome(self, s): + return all(s[i] == s[len(s) - 1 - i] for i in range(len(s) // 2)) + +``` +### Go +回溯 基本版 +```go +var ( + path []string // 放已经回文的子串 + res [][]string +) +func partition(s string) [][]string { + path, res = make([]string, 0), make([][]string, 0) + dfs(s, 0) + return res +} + +func dfs(s string, start int) { + if start == len(s) { // 如果起始位置等于s的大小,说明已经找到了一组分割方案了 + tmp := make([]string, len(path)) + copy(tmp, path) + res = append(res, tmp) + return + } + for i := start; i < len(s); i++ { + str := s[start : i+1] + if isPalindrome(str) { // 是回文子串 + path = append(path, str) + dfs(s, i+1) // 寻找i+1为起始位置的子串 + path = path[:len(path)-1] // 回溯过程,弹出本次已经添加的子串 + } + } +} + +func isPalindrome(s string) bool { + for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { + if s[i] != s[j] { + return false + } + } + return true +} +``` + +回溯+动态规划优化回文串判断 +```go +var ( + result [][]string + path []string // 放已经回文的子串 + isPalindrome [][]bool // 放事先计算好的是否回文子串的结果 +) + +func partition(s string) [][]string { + result = make([][]string, 0) + path = make([]string, 0) + computePalindrome(s) + backtracing(s, 0) + return result +} + +func backtracing(s string, startIndex int) { + // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 + if startIndex >= len(s) { + tmp := make([]string, len(path)) + copy(tmp, path) + result = append(result, tmp) + return + } + for i := startIndex; i < len(s); i++ { + if isPalindrome[startIndex][i] { // 是回文子串 + // 获取[startIndex,i]在s中的子串 + path = append(path, s[startIndex:i+1]) + } else { // 不是回文,跳过 + continue + } + backtracing(s, i + 1) // 寻找i+1为起始位置的子串 + path = path[:len(path)-1] // 回溯过程,弹出本次已经添加的子串 + } +} + +func computePalindrome(s string) { + // isPalindrome[i][j] 代表 s[i:j](双边包括)是否是回文字串 + isPalindrome = make([][]bool, len(s)) + for i := 0; i < len(isPalindrome); i++ { + isPalindrome[i] = make([]bool, len(s)) + } + for i := len(s)-1; i >= 0; i-- { + // 需要倒序计算, 保证在i行时, i+1行已经计算好了 + for j := i; j < len(s); j++ { + if j == i { + isPalindrome[i][j] = true + } else if j - i == 1 { + isPalindrome[i][j] = s[i] == s[j] + } else { + isPalindrome[i][j] = s[i] == s[j] && isPalindrome[i+1][j-1] + } + } + } +} +``` + +### JavaScript + +```js +/** + * @param {string} s + * @return {string[][]} + */ +const isPalindrome = (s, l, r) => { + for (let i = l, j = r; i < j; i++, j--) { + if(s[i] !== s[j]) return false; + } + return true; +} + +var partition = function(s) { + const res = [], path = [], len = s.length; + backtracking(0); + return res; + function backtracking(startIndex) { + if(startIndex >= len) { + res.push(Array.from(path)); + return; + } + for(let i = startIndex; i < len; i++) { + if(!isPalindrome(s, startIndex, i)) continue; + path.push(s.slice(startIndex, i + 1)); + backtracking(i + 1); + path.pop(); + } + } +}; +``` + +### TypeScript + +```typescript +function partition(s: string): string[][] { + const res: string[][] = [] + const path: string[] = [] + const isHuiwen = ( + str: string, + startIndex: number, + endIndex: number + ): boolean => { + for (; startIndex < endIndex; startIndex++, endIndex--) { + if (str[startIndex] !== str[endIndex]) { + return false + } + } + return true + } + const rec = (str: string, index: number): void => { + if (index >= str.length) { + res.push([...path]) + return + } + for (let i = index; i < str.length; i++) { + if (!isHuiwen(str, index, i)) { + continue + } + path.push(str.substring(index, i + 1)) + rec(str, i + 1) + path.pop() + } + } + rec(s, 0) + return res +}; +``` + +### C + +```c +char** path; +int pathTop; +char*** ans; +int ansTop = 0; +int* ansSize; + +//将path中的字符串全部复制到ans中 +void copy() { + //创建一个临时tempPath保存path中的字符串 + char** tempPath = (char**)malloc(sizeof(char*) * pathTop); + int i; + for(i = 0; i < pathTop; i++) { + tempPath[i] = path[i]; + } + //保存tempPath + ans[ansTop] = tempPath; + //将当前path的长度(pathTop)保存在ansSize中 + ansSize[ansTop++] = pathTop; +} + +//判断字符串是否为回文字符串 +bool isPalindrome(char* str, int startIndex, int endIndex) { + //双指针法:当endIndex(右指针)的值比startIndex(左指针)大时进行遍历 + while(endIndex >= startIndex) { + //若左指针和右指针指向元素不一样,返回False + if(str[endIndex--] != str[startIndex++]) + return 0; + } + return 1; +} + +//切割从startIndex到endIndex子字符串 +char* cutString(char* str, int startIndex, int endIndex) { + //开辟字符串的空间 + char* tempString = (char*)malloc(sizeof(char) * (endIndex - startIndex + 2)); + int i; + int index = 0; + //复制子字符串 + for(i = startIndex; i <= endIndex; i++) + tempString[index++] = str[i]; + //用'\0'作为字符串结尾 + tempString[index] = '\0'; + return tempString; +} + +void backTracking(char* str, int strLen, int startIndex) { + if(startIndex >= strLen) { + //将path拷贝到ans中 + copy(); + return ; + } + + int i; + for(i = startIndex; i < strLen; i++) { + //若从subString到i的子串是回文字符串,将其放入path中 + if(isPalindrome(str, startIndex, i)) { + path[pathTop++] = cutString(str, startIndex, i); + } + //若从startIndex到i的子串不为回文字符串,跳过这一层 + else { + continue; + } + //递归判断下一层 + backTracking(str, strLen, i + 1); + //回溯,将path中最后一位元素弹出 + pathTop--; + } +} + +char*** partition(char* s, int* returnSize, int** returnColumnSizes){ + int strLen = strlen(s); + //因为path中的字符串最多为strLen个(即单个字符的回文字符串),所以开辟strLen个char*空间 + path = (char**)malloc(sizeof(char*) * strLen); + //存放path中的数组结果 + ans = (char***)malloc(sizeof(char**) * 40000); + //存放ans数组中每一个char**数组的长度 + ansSize = (int*)malloc(sizeof(int) * 40000); + ansTop = pathTop = 0; + + //回溯函数 + backTracking(s, strLen, 0); + + //将ansTop设置为ans数组的长度 + *returnSize = ansTop; + //设置ans数组中每一个数组的长度 + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; ++i) { + (*returnColumnSizes)[i] = ansSize[i]; + } + return ans; +} +``` + +### Swift + +```swift +func partition(_ s: String) -> [[String]] { + // 把字符串转为字符数组以便于通过索引访问和取子串 + let s = Array(s) + // 使用双指针法判断子串是否回文 + func isPalindrome(start: Int, end: Int) -> Bool { + var start = start, end = end + while start < end { + if s[start] != s[end] { return false } + start += 1 + end -= 1 + } + return true + } + + var result = [[String]]() + var path = [String]() // 切割方案 + func backtracking(startIndex: Int) { + // 终止条件,收集结果 + guard startIndex < s.count else { + result.append(path) + return + } + + for i in startIndex ..< s.count { + // 回文则收集,否则跳过 + guard isPalindrome(start: startIndex, end: i) else { continue } + let substring = String(s[startIndex ... i]) + path.append(substring) // 处理 + backtracking(startIndex: i + 1) // 寻找下一个起始位置的子串 + if !path.isEmpty { path.removeLast() } // 回溯 + } + } + backtracking(startIndex: 0) + return result +} +``` + +### Rust + +**回溯+函数判断回文串** +```Rust +impl Solution { + pub fn partition(s: String) -> Vec> { + let mut ret = vec![]; + let mut path = vec![]; + let sub_str: Vec = s.chars().collect(); + + Self::backtracing(&sub_str, 0, &mut ret, &mut path); + + ret + } + + fn backtracing(sub_str: &Vec, start: usize, ret: &mut Vec>, path: &mut Vec) { + //如果起始位置大于s的大小,说明找到了一组分割方案 + if start >= sub_str.len() { + ret.push(path.clone()); + return; + } + + for i in start..sub_str.len() { + if !Self::is_palindrome(sub_str, start, i) { + continue; + } + //如果是回文子串,则记录 + let s: String = sub_str[start..i+1].into_iter().collect(); + path.push(s); + + //起始位置后移,保证不重复 + Self::backtracing(sub_str, i+1, ret, path); + path.pop(); + } + + } + + fn is_palindrome(s: &Vec, start: usize, end: usize) -> bool { + let (mut start, mut end) = (start, end); + + while start < end { + if s[start] != s[end] { + return false; + } + + start += 1; + end -= 1; + } + + true + } +} +``` +**回溯+动态规划预处理判断回文串** +```Rust +impl Solution { + pub fn backtracking(is_palindrome: &Vec>, result: &mut Vec>, path: &mut Vec, s: &Vec, start_index: usize) { + let len = s.len(); + if start_index >= len { + result.push(path.to_vec()); + return; + } + for i in start_index..len { + if is_palindrome[start_index][i] { path.push(s[start_index..=i].iter().collect::()); } else { continue; } + Self::backtracking(is_palindrome, result, path, s, i + 1); + path.pop(); + } + } + + pub fn partition(s: String) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + let s = s.chars().collect::>(); + let len: usize = s.len(); + // 使用动态规划预先打表 + // 当且仅当其为空串(i>j),或其长度为1(i=j),或者首尾字符相同且(s[i+1..j−1])时为回文串 + let mut is_palindrome = vec![vec![true; len]; len]; + for i in (0..len).rev() { + for j in (i + 1)..len { + is_palindrome[i][j] = s[i] == s[j] && is_palindrome[i + 1][j - 1]; + } + } + Self::backtracking(&is_palindrome, &mut result, &mut path, &s, 0); + result + } +} +``` + + +### Scala + +```scala +object Solution { + + import scala.collection.mutable + + def partition(s: String): List[List[String]] = { + var result = mutable.ListBuffer[List[String]]() + var path = mutable.ListBuffer[String]() + + // 判断字符串是否回文 + def isPalindrome(start: Int, end: Int): Boolean = { + var (left, right) = (start, end) + while (left < right) { + if (s(left) != s(right)) return false + left += 1 + right -= 1 + } + true + } + + // 回溯算法 + def backtracking(startIndex: Int): Unit = { + if (startIndex >= s.size) { + result.append(path.toList) + return + } + // 添加循环守卫,如果当前分割是回文子串则进入回溯 + for (i <- startIndex until s.size if isPalindrome(startIndex, i)) { + path.append(s.substring(startIndex, i + 1)) + backtracking(i + 1) + path = path.take(path.size - 1) + } + } + + backtracking(0) + result.toList + } +} +``` +### CSharp +```csharp +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> Partition(string s) + { + BackTracking(s, 0); + return res; + } + public void BackTracking(string s, int start) + { + if (start >= s.Length) + { + res.Add(new List(path)); + return; + } + + for (int i = start; i < s.Length; i++) + { + if (IsPalindrome(s, start, i)) + { + path.Add(s.Substring(start, i - start + 1)); + } + else + { + continue; + } + BackTracking(s, i + 1); + path.RemoveAt(path.Count - 1); + } + } + public bool IsPalindrome(string s, int start, int end) + { + for (int i = start, j = end; i < j; i++, j--) + { + if (s[i] != s[j]) + return false; + } + return true; + } +} +``` + + + diff --git "a/problems/0132.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262II.md" "b/problems/0132.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262II.md" new file mode 100755 index 0000000000..8bbfa4ee10 --- /dev/null +++ "b/problems/0132.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262II.md" @@ -0,0 +1,373 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 132. 分割回文串 II + +[力扣题目链接](https://leetcode.cn/problems/palindrome-partitioning-ii/) + +给你一个字符串 s,请你将 s 分割成一些子串,使每个子串都是回文。 + +返回符合要求的 最少分割次数 。 + +示例 1: + +输入:s = "aab" +输出:1 +解释:只需一次分割就可将 s 分割成 ["aa","b"] 这样两个回文子串。 + +示例 2: +输入:s = "a" +输出:0 + +示例 3: +输入:s = "ab" +输出:1 + + +提示: + +* 1 <= s.length <= 2000 +* s 仅由小写英文字母组成 + +## 思路 + +我们在讲解回溯法系列的时候,讲过了这道题目[回溯算法:131.分割回文串](https://programmercarl.com/0131.分割回文串.html)。 + +本题呢其实也可以使用回溯法,只不过会超时!(通过记忆化回溯,也可以过,感兴趣的同学可以自行研究一下) + +我们来讲一讲如何使用动态规划,来解决这道题目。 + +关于回文子串,两道题目题目大家是一定要掌握的。 + +* [动态规划:647. 回文子串](https://programmercarl.com/0647.回文子串.html) +* 5.最长回文子串 和 647.回文子串基本一样的 + +这两道题目是回文子串的基础题目,本题也要用到相关的知识点。 + +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i]:范围是[0, i]的回文子串,最少分割次数是dp[i]。 + +2. 确定递推公式 + +来看一下由什么可以推出dp[i]。 + +如果要对长度为[0, i]的子串进行分割,分割点为j。 + +那么如果分割后,区间[j + 1, i]是回文子串,那么dp[i] 就等于 dp[j] + 1。 + +这里可能有同学就不明白了,为什么只看[j + 1, i]区间,不看[0, j]区间是不是回文子串呢? + +那么在回顾一下dp[i]的定义: 范围是[0, i]的回文子串,最少分割次数是dp[i]。 + +[0, j]区间的最小切割数量,我们已经知道了就是dp[j]。 + +此时就找到了递推关系,当切割点j在[0, i] 之间时候,dp[i] = dp[j] + 1; + +本题是要找到最少分割次数,所以遍历j的时候要取最小的dp[i]。 + +**所以最后递推公式为:dp[i] = min(dp[i], dp[j] + 1);** + +注意这里不是要 dp[j] + 1 和 dp[i]去比较,而是要在遍历j的过程中取最小的dp[i]! + +可以有dp[j] + 1推出,当[j + 1, i] 为回文子串 + +3. dp数组如何初始化 + +首先来看一下dp[0]应该是多少。 + +dp[i]: 范围是[0, i]的回文子串,最少分割次数是dp[i]。 + +那么dp[0]一定是0,长度为1的字符串最小分割次数就是0。这个是比较直观的。 + +在看一下非零下标的dp[i]应该初始化为多少? + +在递推公式dp[i] = min(dp[i], dp[j] + 1) 中我们可以看出每次要取最小的dp[i]。 + +那么非零下标的dp[i]就应该初始化为一个最大数,这样递推公式在计算结果的时候才不会被初始值覆盖! + +如果非零下标的dp[i]初始化为0,在那么在递推公式中,所有数值将都是零。 + +非零下标的dp[i]初始化为一个最大数。 + +代码如下: + +```CPP +vector dp(s.size(), INT_MAX); +dp[0] = 0; +``` + +其实也可以这样初始化,更具dp[i]的定义,dp[i]的最大值其实就是i,也就是把每个字符分割出来。 + +所以初始化代码也可以为: +```CPP +vector dp(s.size()); +for (int i = 0; i < s.size(); i++) dp[i] = i; +``` + +4. 确定遍历顺序 + +根据递推公式:dp[i] = min(dp[i], dp[j] + 1); + +j是在[0,i]之间,所以遍历i的for循环一定在外层,这里遍历j的for循环在内层才能通过 计算过的dp[j]数值推导出dp[i]。 + +代码如下: + +```CPP +for (int i = 1; i < s.size(); i++) { + if (isPalindromic[0][i]) { // 判断是不是回文子串 + dp[i] = 0; + continue; + } + for (int j = 0; j < i; j++) { + if (isPalindromic[j + 1][i]) { + dp[i] = min(dp[i], dp[j] + 1); + } + } +} +``` + +大家会发现代码里有一个isPalindromic函数,这是一个二维数组isPalindromic[i][j],记录[i, j]是不是回文子串。 + +那么这个isPalindromic[i][j]是怎么的代码的呢? + +就是其实这两道题目的代码: + + +* [647. 回文子串](https://programmercarl.com/0647.回文子串.html) +* 5.最长回文子串 + +所以先用一个二维数组来保存整个字符串的回文情况。 + +代码如下: + +```CPP +vector> isPalindromic(s.size(), vector(s.size(), false)); +for (int i = s.size() - 1; i >= 0; i--) { + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j] && (j - i <= 1 || isPalindromic[i + 1][j - 1])) { + isPalindromic[i][j] = true; + } + } +} +``` + +5. 举例推导dp数组 + +以输入:"aabc" 为例: + +![132.分割回文串II](https://file1.kamacoder.com/i/algo/20210124182218844.jpg) + +以上分析完毕,代码如下: + +```CPP +class Solution { +public: + int minCut(string s) { + vector> isPalindromic(s.size(), vector(s.size(), false)); + for (int i = s.size() - 1; i >= 0; i--) { + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j] && (j - i <= 1 || isPalindromic[i + 1][j - 1])) { + isPalindromic[i][j] = true; + } + } + } + // 初始化 + vector dp(s.size(), 0); + for (int i = 0; i < s.size(); i++) dp[i] = i; + + for (int i = 1; i < s.size(); i++) { + if (isPalindromic[0][i]) { + dp[i] = 0; + continue; + } + for (int j = 0; j < i; j++) { + if (isPalindromic[j + 1][i]) { + dp[i] = min(dp[i], dp[j] + 1); + } + } + } + return dp[s.size() - 1]; + + } +}; +``` + + +## 其他语言版本 + +### Java + +```java +class Solution { + + public int minCut(String s) { + if(null == s || "".equals(s)){ + return 0; + } + int len = s.length(); + // 1. + // 记录子串[i..j]是否是回文串 + boolean[][] isPalindromic = new boolean[len][len]; + // 从下到上,从左到右 + for(int i = len - 1; i >= 0; i--){ + for(int j = i; j < len; j++){ + if(s.charAt(i) == s.charAt(j)){ + if(j - i <= 1){ + isPalindromic[i][j] = true; + } else{ + isPalindromic[i][j] = isPalindromic[i + 1][j - 1]; + } + } else{ + isPalindromic[i][j] = false; + } + } + } + + // 2. + // dp[i] 表示[0..i]的最小分割次数 + int[] dp = new int[len]; + for(int i = 0; i < len; i++){ + //初始考虑最坏的情况。 1个字符分割0次, len个字符分割 len - 1次 + dp[i] = i; + } + + for(int i = 1; i < len; i++){ + if(isPalindromic[0][i]){ + // s[0..i]是回文了,那 dp[i] = 0, 一次也不用分割 + dp[i] = 0; + continue; + } + for(int j = 0; j < i; j++){ + // 按文中的思路,不清楚就拿 "ababa" 为例,先写出 isPalindromic 数组,再进行求解 + if(isPalindromic[j + 1][i]){ + dp[i] = Math.min(dp[i], dp[j] + 1); + } + } + } + return dp[len - 1]; + } +} +``` + +### Python + +```python +class Solution: + def minCut(self, s: str) -> int: + + isPalindromic=[[False]*len(s) for _ in range(len(s))] + + for i in range(len(s)-1,-1,-1): + for j in range(i,len(s)): + if s[i]!=s[j]: + isPalindromic[i][j] = False + elif j-i<=1 or isPalindromic[i+1][j-1]: + isPalindromic[i][j] = True + + # print(isPalindromic) + dp=[sys.maxsize]*len(s) + dp[0]=0 + + for i in range(1,len(s)): + if isPalindromic[0][i]: + dp[i]=0 + continue + for j in range(0,i): + if isPalindromic[j+1][i]==True: + dp[i]=min(dp[i], dp[j]+1) + return dp[-1] +``` + +### Go + +```go +func minCut(s string) int { + isValid := make([][]bool, len(s)) + for i := 0; i < len(isValid); i++ { + isValid[i] = make([]bool, len(s)) + isValid[i][i] = true + } + for i := len(s) - 1; i >= 0; i-- { + for j := i + 1; j < len(s); j++ { + if s[i] == s[j] && (isValid[i + 1][j - 1] || j - i == 1) { + isValid[i][j] = true + } + } + } + + dp := make([]int, len(s)) + for i := 0; i < len(s); i++ { + dp[i] = math.MaxInt + } + for i := 0; i < len(s); i++ { + if isValid[0][i] { + dp[i] = 0 + continue + } + for j := 0; j < i; j++ { + if isValid[j + 1][i] { + dp[i] = min(dp[i], dp[j] + 1) + } + } + } + return dp[len(s) - 1] +} + +func min(i, j int) int { + if i < j { + return i + } else { + return j + } +} +``` + +### JavaScript + +```js +var minCut = function(s) { + const len = s.length; + // 二维数组isPalindromic来保存整个字符串的回文情况 + const isPalindromic = new Array(len).fill(false).map(() => new Array(len).fill(false)); + for(let i = len - 1; i >= 0; i--){ + for(let j = i; j < len; j++){ + if(s[i] === s[j] && (j - i <= 1 || isPalindromic[i + 1][j - 1])){ + isPalindromic[i][j] = true; + } + } + } + // dp[i]:范围是[0, i]的回文子串,最少分割次数是dp[i] + const dp = new Array(len).fill(0); + for(let i = 0; i < len; i++) dp[i] = i; // 初始化 dp[i]的最大值其实就是i,也就是把每个字符分割出来 + for(let i = 1; i < len; i++){ + if(isPalindromic[0][i]){ // 判断是不是回文子串 + dp[i] = 0; + continue; + } + /* + 如果要对长度为[0, i]的子串进行分割,分割点为j。 + 那么如果分割后,区间[j + 1, i]是回文子串,那么dp[i] 就等于 dp[j] + 1。 + 这里可能有同学就不明白了,为什么只看[j + 1, i]区间,不看[0, j]区间是不是回文子串呢? + 那么在回顾一下dp[i]的定义: 范围是[0, i]的回文子串,最少分割次数是dp[i]。 + [0, j]区间的最小切割数量,我们已经知道了就是dp[j]。 + 此时就找到了递推关系,当切割点j在[0, i] 之间时候,dp[i] = dp[j] + 1; + 本题是要找到最少分割次数,所以遍历j的时候要取最小的dp[i]。dp[i] = Math.min(dp[i], dp[j] + 1); + */ + for(let j = 0; j < i; j++){ + if(isPalindromic[j + 1][i]){ + dp[i] = Math.min(dp[i], dp[j] + 1); + } + } + } + return dp[len - 1]; +}; +``` + + + diff --git "a/problems/0134.\345\212\240\346\262\271\347\253\231.md" "b/problems/0134.\345\212\240\346\262\271\347\253\231.md" new file mode 100755 index 0000000000..5c8b0c3cc8 --- /dev/null +++ "b/problems/0134.\345\212\240\346\262\271\347\253\231.md" @@ -0,0 +1,709 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 134. 加油站 + +[力扣题目链接](https://leetcode.cn/problems/gas-station/) + +在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。 + +你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。 + +如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。 + +说明:  + +* 如果题目有解,该答案即为唯一答案。 +* 输入数组均为非空数组,且长度相同。 +* 输入数组中的元素均为非负数。 + +示例 1: +输入: +* gas = [1,2,3,4,5] +* cost = [3,4,5,1,2] + +输出: 3 +解释: +* 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 +* 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 +* 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 +* 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 +* 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 +* 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 +* 因此,3 可为起始索引。 + +示例 2: +输入: +* gas = [2,3,4] +* cost = [3,4,3] + +* 输出: -1 +* 解释: +你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油。开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油。开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油。你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。因此,无论怎样,你都不可能绕环路行驶一周。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,得这么加油才能跑完全程!LeetCode :134.加油站](https://www.bilibili.com/video/BV1jA411r7WX),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +### 暴力方法 + +暴力的方法很明显就是O(n^2)的,遍历每一个加油站为起点的情况,模拟一圈。 + +如果跑了一圈,中途没有断油,而且最后油量大于等于0,说明这个起点是ok的。 + +暴力的方法思路比较简单,但代码写起来也不是很容易,关键是要模拟跑一圈的过程。 + +**for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,要善于使用while!** + +C++代码如下: + +```CPP +class Solution { +public: + int canCompleteCircuit(vector& gas, vector& cost) { + for (int i = 0; i < cost.size(); i++) { + int rest = gas[i] - cost[i]; // 记录剩余油量 + int index = (i + 1) % cost.size(); + while (rest > 0 && index != i) { // 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了) + rest += gas[index] - cost[index]; + index = (index + 1) % cost.size(); + } + // 如果以i为起点跑一圈,剩余油量>=0,返回该起始位置 + if (rest >= 0 && index == i) return i; + } + return -1; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + + +### 贪心算法(方法一) + +直接从全局进行贪心选择,情况如下: + +* 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的 +* 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。 + +* 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能把这个负数填平,能把这个负数填平的节点就是出发节点。 + +C++代码如下: + +```CPP +class Solution { +public: + int canCompleteCircuit(vector& gas, vector& cost) { + int curSum = 0; + int min = INT_MAX; // 从起点出发,油箱里的油量最小值 + for (int i = 0; i < gas.size(); i++) { + int rest = gas[i] - cost[i]; + curSum += rest; + if (curSum < min) { + min = curSum; + } + } + if (curSum < 0) return -1; // 情况1 + if (min >= 0) return 0; // 情况2 + // 情况3 + for (int i = gas.size() - 1; i >= 0; i--) { + int rest = gas[i] - cost[i]; + min += rest; + if (min >= 0) { + return i; + } + } + return -1; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +**其实我不认为这种方式是贪心算法,因为没有找出局部最优,而是直接从全局最优的角度上思考问题**。 + +但这种解法又说不出是什么方法,这就是一个从全局角度选取最优解的模拟操作。 + +所以对于本解法是贪心,我持保留意见! + +但不管怎么说,解法毕竟还是巧妙的,不用过于执着于其名字称呼。 + +### 贪心算法(方法二) + +可以换一个思路,首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。 + +每个加油站的剩余量rest[i]为gas[i] - cost[i]。 + +i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i]区间都不能作为起始位置,因为这个区间选择任何一个位置作为起点,到i这里都会断油,那么起始位置从i+1算起,再从0计算curSum。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20230117165628.png) + +那么为什么一旦[0,i] 区间和为负数,起始位置就可以是i+1呢,i+1后面就不会出现更大的负数? + +如果出现更大的负数,就是更新i,那么起始位置又变成新的i+1了。 + +那有没有可能 [0,i] 区间 选某一个作为起点,累加到 i这里 curSum是不会小于零呢? 如图: + +![](https://file1.kamacoder.com/i/algo/20230117170703.png) + +如果 curSum<0 说明 区间和1 + 区间和2 < 0, 那么 假设从上图中的位置开始计数curSum不会小于0的话,就是 区间和2>0。 + +区间和1 + 区间和2 < 0 同时 区间和2>0,只能说明区间和1 < 0, 那么就会从假设的箭头初就开始从新选择起始位置了。 + + +**那么局部最优:当前累加rest[i]的和curSum一旦小于0,起始位置至少要是i+1,因为从i之前开始一定不行。全局最优:找到可以跑一圈的起始位置**。 + +局部最优可以推出全局最优,找不出反例,试试贪心! + +C++代码如下: + +```CPP +class Solution { +public: + int canCompleteCircuit(vector& gas, vector& cost) { + int curSum = 0; + int totalSum = 0; + int start = 0; + for (int i = 0; i < gas.size(); i++) { + curSum += gas[i] - cost[i]; + totalSum += gas[i] - cost[i]; + if (curSum < 0) { // 当前累加rest[i]和 curSum一旦小于0 + start = i + 1; // 起始位置更新为i+1 + curSum = 0; // curSum从0开始 + } + } + if (totalSum < 0) return -1; // 说明怎么走都不可能跑一圈了 + return start; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +**说这种解法为贪心算法,才是有理有据的,因为全局最优解是根据局部最优推导出来的**。 + +## 总结 + +对于本题首先给出了暴力解法,暴力解法模拟跑一圈的过程其实比较考验代码技巧的,要对while使用的很熟练。 + +然后给出了两种贪心算法,对于第一种贪心方法,其实我认为就是一种直接从全局选取最优的模拟操作,思路还是很巧妙的,值得学习一下。 + +对于第二种贪心方法,才真正体现出贪心的精髓,用局部最优可以推出全局最优,进而求得起始位置。 + + + +## 其他语言版本 + + +### Java +```java +// 解法1 +class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int sum = 0; + int min = 0; + for (int i = 0; i < gas.length; i++) { + sum += (gas[i] - cost[i]); + min = Math.min(sum, min); + } + + if (sum < 0) return -1; + if (min >= 0) return 0; + + for (int i = gas.length - 1; i > 0; i--) { + min += (gas[i] - cost[i]); + if (min >= 0) return i; + } + + return -1; + } +} +``` +```java +// 解法2 +class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int curSum = 0; + int totalSum = 0; + int index = 0; + for (int i = 0; i < gas.length; i++) { + curSum += gas[i] - cost[i]; + totalSum += gas[i] - cost[i]; + if (curSum < 0) { + index = (i + 1) % gas.length ; + curSum = 0; + } + } + if (totalSum < 0) return -1; + return index; + } +} +``` +``` +// 解法3 +class Solution { + public int canCompleteCircuit(int[] gas, int[] cost) { + int tank = 0; // 当前油量 + int totalGas = 0; // 总加油量 + int totalCost = 0; // 总油耗 + int start = 0; // 起点 + for (int i = 0; i < gas.length; i++) { + totalGas += gas[i]; + totalCost += cost[i]; + + tank += gas[i] - cost[i]; + if (tank < 0) { // tank 变为负数 意味着 从0到i之间出发都不能顺利环路一周,因为在此i点必会没油 + tank = 0; // reset tank,类似于题目53.最大子树和reset sum + start = i + 1; // 起点变为i点往后一位 + } + } + if (totalCost > totalGas) return -1; + return start; + } +} +``` + +### Python +暴力法 +```python + +class Solution: + def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: + for i in range(len(cost)): + rest = gas[i] - cost[i] # 记录剩余油量 + index = (i + 1) % len(cost) # 下一个加油站的索引 + + while rest > 0 and index != i: # 模拟以i为起点行驶一圈(如果有rest==0,那么答案就不唯一了) + rest += gas[index] - cost[index] # 更新剩余油量 + index = (index + 1) % len(cost) # 更新下一个加油站的索引 + + if rest >= 0 and index == i: # 如果以i为起点跑一圈,剩余油量>=0,并且回到起始位置 + return i # 返回起始位置i + + return -1 # 所有起始位置都无法环绕一圈,返回-1 + +``` +贪心(版本一) +```python +class Solution: + def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: + curSum = 0 # 当前累计的剩余油量 + minFuel = float('inf') # 从起点出发,油箱里的油量最小值 + + for i in range(len(gas)): + rest = gas[i] - cost[i] + curSum += rest + if curSum < minFuel: + minFuel = curSum + + if curSum < 0: + return -1 # 情况1:整个行程的总消耗大于总供给,无法完成一圈 + + if minFuel >= 0: + return 0 # 情况2:从起点出发到任何一个加油站时油箱的剩余油量都不会小于0,可以从起点出发完成一圈 + + for i in range(len(gas) - 1, -1, -1): + rest = gas[i] - cost[i] + minFuel += rest + if minFuel >= 0: + return i # 情况3:找到一个位置使得从该位置出发油箱的剩余油量不会小于0,返回该位置的索引 + + return -1 # 无法完成一圈 + +``` +贪心(版本二) +```python +class Solution: + def canCompleteCircuit(self, gas: List[int], cost: List[int]) -> int: + curSum = 0 # 当前累计的剩余油量 + totalSum = 0 # 总剩余油量 + start = 0 # 起始位置 + + for i in range(len(gas)): + curSum += gas[i] - cost[i] + totalSum += gas[i] - cost[i] + + if curSum < 0: # 当前累计剩余油量curSum小于0 + start = i + 1 # 起始位置更新为i+1 + curSum = 0 # curSum重新从0开始累计 + + if totalSum < 0: + return -1 # 总剩余油量totalSum小于0,说明无法环绕一圈 + return start + + +``` + +### Go + +贪心算法(方法一) +```go +func canCompleteCircuit(gas []int, cost []int) int { + curSum := 0 + min := math.MaxInt64 + for i := 0; i < len(gas); i++ { + rest := gas[i] - cost[i] + curSum += rest + if curSum < min { + min = curSum + } + } + if curSum < 0 { + return -1 + } + if min >= 0 { + return 0 + } + for i := len(gas) - 1; i > 0; i-- { + rest := gas[i] - cost[i] + min += rest + if min >= 0 { + return i + } + } + return -1 +} +``` + +贪心算法(方法二) +```go +func canCompleteCircuit(gas []int, cost []int) int { + curSum := 0 + totalSum := 0 + start := 0 + for i := 0; i < len(gas); i++ { + curSum += gas[i] - cost[i] + totalSum += gas[i] - cost[i] + if curSum < 0 { + start = i+1 + curSum = 0 + } + } + if totalSum < 0 { + return -1 + } + return start +} +``` + +### JavaScript +暴力: +```js +var canCompleteCircuit = function(gas, cost) { + for(let i = 0; i < cost.length; i++) { + let rest = gas[i] - cost[i] //记录剩余油量 + // 以i为起点行驶一圈,index为下一个目的地 + let index = (i + 1) % cost.length + while(rest > 0 && index !== i) { + rest += gas[index] - cost[index] + index = (index + 1) % cost.length + } + if(rest >= 0 && index === i) return i + } + return -1 +}; +``` +解法一: +```js +var canCompleteCircuit = function(gas, cost) { + let curSum = 0 + let min = Infinity + for(let i = 0; i < gas.length; i++) { + let rest = gas[i] - cost[i] + curSum += rest + if(curSum < min) { + min = curSum + } + } + if(curSum < 0) return -1 //1.总油量 小于 总消耗量 + if(min >= 0) return 0 //2. 说明油箱里油没断过 + //3. 从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点 + for(let i = gas.length -1; i >= 0; i--) { + let rest = gas[i] - cost[i] + min += rest + if(min >= 0) { + return i + } + } + return -1 +} +``` +解法二: +```Javascript +var canCompleteCircuit = function(gas, cost) { + const gasLen = gas.length + let start = 0 + let curSum = 0 + let totalSum = 0 + + for(let i = 0; i < gasLen; i++) { + curSum += gas[i] - cost[i] + totalSum += gas[i] - cost[i] + if(curSum < 0) { + curSum = 0 + start = i + 1 + } + } + + if(totalSum < 0) return -1 + + return start +}; +``` + +### TypeScript + +**暴力法:** + +```typescript +function canCompleteCircuit(gas: number[], cost: number[]): number { + for (let i = 0, length = gas.length; i < length; i++) { + let curSum: number = 0; + let index: number = i; + while (curSum >= 0 && index < i + length) { + let tempIndex: number = index % length; + curSum += gas[tempIndex] - cost[tempIndex]; + index++; + } + if (index === i + length && curSum >= 0) return i; + } + return -1; +}; +``` + +**解法二:** + +```typescript +function canCompleteCircuit(gas: number[], cost: number[]): number { + let total: number = 0; + let curGas: number = 0; + let tempDiff: number = 0; + let resIndex: number = 0; + for (let i = 0, length = gas.length; i < length; i++) { + tempDiff = gas[i] - cost[i]; + total += tempDiff; + curGas += tempDiff; + if (curGas < 0) { + resIndex = i + 1; + curGas = 0; + } + } + if (total < 0) return -1; + return resIndex; +}; +``` + +### Rust + +贪心算法:方法一 + +```Rust +impl Solution { + pub fn can_complete_circuit(gas: Vec, cost: Vec) -> i32 { + let mut cur_sum = 0; + let mut min = i32::MAX; + for i in 0..gas.len() { + let rest = gas[i] - cost[i]; + cur_sum += rest; + if cur_sum < min { min = cur_sum; } + } + if cur_sum < 0 { return -1; } + if min > 0 { return 0; } + for i in (0..gas.len()).rev() { + let rest = gas[i] - cost[i]; + min += rest; + if min >= 0 { return i as i32; } + } + -1 + } +} +``` + +贪心算法:方法二 + +```Rust +impl Solution { + pub fn can_complete_circuit(gas: Vec, cost: Vec) -> i32 { + let mut cur_sum = 0; + let mut total_sum = 0; + let mut start = 0; + for i in 0..gas.len() { + cur_sum += gas[i] - cost[i]; + total_sum += gas[i] - cost[i]; + if cur_sum < 0 { + start = i + 1; + cur_sum = 0; + } + } + if total_sum < 0 { return -1; } + start as i32 + } +} +``` + + +### C + +贪心算法:方法一 + + +```c +int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize){ + int curSum = 0; + int i; + int min = INT_MAX; + //遍历整个数组。计算出每站的用油差。并将其与最小累加量比较 + for(i = 0; i < gasSize; i++) { + int diff = gas[i] - cost[i]; + curSum += diff; + if(curSum < min) + min = curSum; + } + //若汽油总数为负数,代表无法跑完一环。返回-1 + if(curSum < 0) + return -1; + //若min大于等于0,说明每一天加油量比用油量多。因此从0出发即可 + if(min >= 0) + return 0; + //若累加最小值为负,则找到一个非零元素(加油量大于出油量)出发。返回坐标 + for(i = gasSize - 1; i >= 0; i--) { + min+=(gas[i]-cost[i]); + if(min >= 0) + return i; + } + //逻辑上不会返回这个0 + return 0; +} +``` + +贪心算法:方法二 +```c +int canCompleteCircuit(int* gas, int gasSize, int* cost, int costSize){ + int curSum = 0; + int totalSum = 0; + int start = 0; + + int i; + for(i = 0; i < gasSize; ++i) { + // 当前i站中加油量与耗油量的差 + int diff = gas[i] - cost[i]; + + curSum += diff; + totalSum += diff; + + // 若0到i的加油量都为负,则开始位置应为i+1 + if(curSum < 0) { + curSum = 0; + // 当i + 1 == gasSize时,totalSum < 0(此时i为gasSize - 1),油车不可能返回原点 + start = i + 1; + } + } + + // 若总和小于0,加油车无论如何都无法返回原点。返回-1 + if(totalSum < 0) + return -1; + + return start; +} +``` + +### Scala + +暴力解法: + +```scala +object Solution { + def canCompleteCircuit(gas: Array[Int], cost: Array[Int]): Int = { + for (i <- cost.indices) { + var rest = gas(i) - cost(i) + var index = (i + 1) % cost.length // index为i的下一个节点 + while (rest > 0 && i != index) { + rest += (gas(index) - cost(index)) + index = (index + 1) % cost.length + } + if (rest >= 0 && index == i) return i + } + -1 + } +} +``` + +贪心算法,方法一: + +```scala +object Solution { + def canCompleteCircuit(gas: Array[Int], cost: Array[Int]): Int = { + var curSum = 0 + var min = Int.MaxValue + for (i <- gas.indices) { + var rest = gas(i) - cost(i) + curSum += rest + min = math.min(min, curSum) + } + if (curSum < 0) return -1 // 情况1: gas的总和小于cost的总和,不可能到达终点 + if (min >= 0) return 0 // 情况2: 最小值>=0,从0号出发可以直接到达 + // 情况3: min为负值,从后向前看,能把min填平的节点就是出发节点 + for (i <- gas.length - 1 to 0 by -1) { + var rest = gas(i) - cost(i) + min += rest + if (min >= 0) return i + } + -1 + } +} +``` + +贪心算法,方法二: + +```scala +object Solution { + def canCompleteCircuit(gas: Array[Int], cost: Array[Int]): Int = { + var curSum = 0 + var totalSum = 0 + var start = 0 + for (i <- gas.indices) { + curSum += (gas(i) - cost(i)) + totalSum += (gas(i) - cost(i)) + if (curSum < 0) { + start = i + 1 // 起始位置更新 + curSum = 0 // curSum从0开始 + } + } + if (totalSum < 0) return -1 // 说明怎么走不可能跑一圈 + start + } +} +``` +### C# +```csharp +// 贪心算法,方法二 +public class Solution +{ + public int CanCompleteCircuit(int[] gas, int[] cost) + { + int curSum = 0, totalSum = 0, start = 0; + for (int i = 0; i < gas.Length; i++) + { + curSum += gas[i] - cost[i]; + totalSum += gas[i] - cost[i]; + if (curSum < 0) + { + start = i + 1; + curSum = 0; + } + } + if (totalSum < 0) return -1; + return start; + } +} +``` + + diff --git "a/problems/0135.\345\210\206\345\217\221\347\263\226\346\236\234.md" "b/problems/0135.\345\210\206\345\217\221\347\263\226\346\236\234.md" new file mode 100755 index 0000000000..9701f0f0c1 --- /dev/null +++ "b/problems/0135.\345\210\206\345\217\221\347\263\226\346\236\234.md" @@ -0,0 +1,401 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 135. 分发糖果 + +[力扣题目链接](https://leetcode.cn/problems/candy/) + +老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。 + +你需要按照以下要求,帮助老师给这些孩子分发糖果: + +* 每个孩子至少分配到 1 个糖果。 +* 相邻的孩子中,评分高的孩子必须获得更多的糖果。 + +那么这样下来,老师至少需要准备多少颗糖果呢? + +示例 1: +* 输入: [1,0,2] +* 输出: 5 +* 解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。 + +示例 2: +* 输入: [1,2,2] +* 输出: 4 +* 解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。第三个孩子只得到 1 颗糖果,这已满足上述两个条件。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,两者兼顾很容易顾此失彼!LeetCode:135.分发糖果](https://www.bilibili.com/video/BV1ev4y1r7wN),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,**如果两边一起考虑一定会顾此失彼**。 + + +先确定右边评分大于左边的情况(也就是从前向后遍历) + +此时局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果 + +局部最优可以推出全局最优。 + +如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1 + +代码如下: + +```CPP +// 从前向后 +for (int i = 1; i < ratings.size(); i++) { + if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1; +} +``` + +如图: + + +![135.分发糖果](https://file1.kamacoder.com/i/algo/20201117114916878.png) + +再确定左孩子大于右孩子的情况(从后向前遍历) + +遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢? + +因为 rating[5]与rating[4]的比较 要利用上 rating[5]与rating[6]的比较结果,所以 要从后向前遍历。 + +如果从前向后遍历,rating[5]与rating[4]的比较 就不能用上 rating[5]与rating[6]的比较结果了 。如图: + +![](https://file1.kamacoder.com/i/algo/20230202102044.png) + +**所以确定左孩子大于右孩子的情况一定要从后向前遍历!** + + +如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。 + +那么又要贪心了,局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量既大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。 + +局部最优可以推出全局最优。 + +所以就取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,**candyVec[i]只有取最大的才能既保持对左边candyVec[i - 1]的糖果多,也比右边candyVec[i + 1]的糖果多**。 + +如图: + + +![135.分发糖果1](https://file1.kamacoder.com/i/algo/20201117115658791.png) + +所以该过程代码如下: + +```CPP +// 从后向前 +for (int i = ratings.size() - 2; i >= 0; i--) { + if (ratings[i] > ratings[i + 1] ) { + candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1); + } +} +``` + +整体代码如下: +```CPP +class Solution { +public: + int candy(vector& ratings) { + vector candyVec(ratings.size(), 1); + // 从前向后 + for (int i = 1; i < ratings.size(); i++) { + if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1; + } + // 从后向前 + for (int i = ratings.size() - 2; i >= 0; i--) { + if (ratings[i] > ratings[i + 1] ) { + candyVec[i] = max(candyVec[i], candyVec[i + 1] + 1); + } + } + // 统计结果 + int result = 0; + for (int i = 0; i < candyVec.size(); i++) result += candyVec[i]; + return result; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(n) + + + +## 总结 + +这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。 + +那么本题我采用了两次贪心的策略: + +* 一次是从左到右遍历,只比较右边孩子评分比左边大的情况。 +* 一次是从右到左遍历,只比较左边孩子评分比右边大的情况。 + +这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。 + + + +## 其他语言版本 + + +### Java +```java +class Solution { + /** + 分两个阶段 + 1、起点下标1 从左往右,只要 右边 比 左边 大,右边的糖果=左边 + 1 + 2、起点下标 ratings.length - 2 从右往左, 只要左边 比 右边 大,此时 左边的糖果应该 取本身的糖果数(符合比它左边大) 和 右边糖果数 + 1 二者的最大值,这样才符合 它比它左边的大,也比它右边大 + */ + public int candy(int[] ratings) { + int len = ratings.length; + int[] candyVec = new int[len]; + candyVec[0] = 1; + for (int i = 1; i < len; i++) { + candyVec[i] = (ratings[i] > ratings[i - 1]) ? candyVec[i - 1] + 1 : 1; + } + + for (int i = len - 2; i >= 0; i--) { + if (ratings[i] > ratings[i + 1]) { + candyVec[i] = Math.max(candyVec[i], candyVec[i + 1] + 1); + } + } + + int ans = 0; + for (int num : candyVec) { + ans += num; + } + return ans; + } +} +``` + +### Python +```python +class Solution: + def candy(self, ratings: List[int]) -> int: + n = len(ratings) + candies = [1] * n + + # Forward pass: handle cases where right rating is higher than left + for i in range(1, n): + if ratings[i] > ratings[i - 1]: + candies[i] = candies[i - 1] + 1 + + # Backward pass: handle cases where left rating is higher than right + for i in range(n - 2, -1, -1): + if ratings[i] > ratings[i + 1]: + candies[i] = max(candies[i], candies[i + 1] + 1) + + return sum(candies) + +``` + +### Go +```go +func candy(ratings []int) int { + /**先确定一边,再确定另外一边 + 1.先从左到右,当右边的大于左边的就加1 + 2.再从右到左,当左边的大于右边的就再加1 + **/ + need := make([]int, len(ratings)) + sum := 0 + // 初始化(每个人至少一个糖果) + for i := 0; i < len(ratings); i++ { + need[i] = 1 + } + // 1.先从左到右,当右边的大于左边的就加1 + for i := 0; i < len(ratings) - 1; i++ { + if ratings[i] < ratings[i+1] { + need[i+1] = need[i] + 1 + } + } + // 2.再从右到左,当左边的大于右边的就右边加1,但要花费糖果最少,所以需要做下判断 + for i := len(ratings)-1; i > 0; i-- { + if ratings[i-1] > ratings[i] { + need[i-1] = findMax(need[i-1], need[i]+1) + } + } + //计算总共糖果 + for i := 0; i < len(ratings); i++ { + sum += need[i] + } + return sum +} +func findMax(num1 int, num2 int) int { + if num1 > num2 { + return num1 + } + return num2 +} +``` + +### JavaScript +```Javascript +var candy = function(ratings) { + let candys = new Array(ratings.length).fill(1) + + for(let i = 1; i < ratings.length; i++) { + if(ratings[i] > ratings[i - 1]) { + candys[i] = candys[i - 1] + 1 + } + } + + for(let i = ratings.length - 2; i >= 0; i--) { + if(ratings[i] > ratings[i + 1]) { + candys[i] = Math.max(candys[i], candys[i + 1] + 1) + } + } + + let count = candys.reduce((a, b) => { + return a + b + }) + + return count +}; +``` + + +### Rust +```rust +pub fn candy(ratings: Vec) -> i32 { + let mut candies = vec![1i32; ratings.len()]; + for i in 1..ratings.len() { + if ratings[i - 1] < ratings[i] { + candies[i] = candies[i - 1] + 1; + } + } + + for i in (0..ratings.len()-1).rev() { + if ratings[i] > ratings[i + 1] { + candies[i] = candies[i].max(candies[i + 1] + 1); + } + } + candies.iter().sum() +} +``` + +### C +```c +#define max(a, b) (((a) > (b)) ? (a) : (b)) + +int *initCandyArr(int size) { + int *candyArr = (int*)malloc(sizeof(int) * size); + + int i; + for(i = 0; i < size; ++i) + candyArr[i] = 1; + + return candyArr; +} + +int candy(int* ratings, int ratingsSize){ + // 初始化数组,每个小孩开始至少有一颗糖 + int *candyArr = initCandyArr(ratingsSize); + + int i; + // 先判断右边是否比左边评分高。若是,右边孩子的糖果为左边孩子+1(candyArr[i] = candyArr[i - 1] + 1) + for(i = 1; i < ratingsSize; ++i) { + if(ratings[i] > ratings[i - 1]) + candyArr[i] = candyArr[i - 1] + 1; + } + + // 再判断左边评分是否比右边高。 + // 若是,左边孩子糖果为右边孩子糖果+1/自己所持糖果最大值。(若糖果已经比右孩子+1多,则不需要更多糖果) + // 举例:ratings为[1, 2, 3, 1]。此时评分为3的孩子在判断右边比左边大后为3,虽然它比最末尾的1(ratings[3])大,但是candyArr[3]为1。所以不必更新candyArr[2] + for(i = ratingsSize - 2; i >= 0; --i) { + if(ratings[i] > ratings[i + 1]) + candyArr[i] = max(candyArr[i], candyArr[i + 1] + 1); + } + + // 求出糖果之和 + int result = 0; + for(i = 0; i < ratingsSize; ++i) { + result += candyArr[i]; + } + return result; +} +``` + +### TypeScript + +```typescript +function candy(ratings: number[]): number { + const candies: number[] = []; + candies[0] = 1; + // 保证右边高分孩子一定比左边低分孩子发更多的糖果 + for (let i = 1, length = ratings.length; i < length; i++) { + if (ratings[i] > ratings[i - 1]) { + candies[i] = candies[i - 1] + 1; + } else { + candies[i] = 1; + } + } + // 保证左边高分孩子一定比右边低分孩子发更多的糖果 + for (let i = ratings.length - 2; i >= 0; i--) { + if (ratings[i] > ratings[i + 1]) { + candies[i] = Math.max(candies[i], candies[i + 1] + 1); + } + } + return candies.reduce((pre, cur) => pre + cur); +}; +``` + +### Scala + +```scala +object Solution { + def candy(ratings: Array[Int]): Int = { + var candyVec = new Array[Int](ratings.length) + for (i <- candyVec.indices) candyVec(i) = 1 + // 从前向后 + for (i <- 1 until candyVec.length) { + if (ratings(i) > ratings(i - 1)) { + candyVec(i) = candyVec(i - 1) + 1 + } + } + + // 从后向前 + for (i <- (candyVec.length - 2) to 0 by -1) { + if (ratings(i) > ratings(i + 1)) { + candyVec(i) = math.max(candyVec(i), candyVec(i + 1) + 1) + } + } + + candyVec.sum // 求和 + } +} +``` +### C# +```csharp +public class Solution +{ + public int Candy(int[] ratings) + { + int[] candies = new int[ratings.Length]; + for (int i = 0; i < candies.Length; i++) + { + candies[i] = 1; + } + for (int i = 1; i < ratings.Length; i++) + { + if (ratings[i] > ratings[i - 1]) + { + candies[i] = candies[i - 1] + 1; + } + } + for (int i = ratings.Length - 2; i >= 0; i--) + { + if (ratings[i] > ratings[i + 1]) + { + candies[i] = Math.Max(candies[i], candies[i + 1] + 1); + } + } + return candies.Sum(); + } +} +``` + + + diff --git "a/problems/0139.\345\215\225\350\257\215\346\213\206\345\210\206.md" "b/problems/0139.\345\215\225\350\257\215\346\213\206\345\210\206.md" new file mode 100755 index 0000000000..2015cb90c1 --- /dev/null +++ "b/problems/0139.\345\215\225\350\257\215\346\213\206\345\210\206.md" @@ -0,0 +1,565 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 139.单词拆分 + +[力扣题目链接](https://leetcode.cn/problems/word-break/) + +给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。 + +说明: + +拆分时可以重复使用字典中的单词。 + +你可以假设字典中没有重复的单词。 + +示例 1: +* 输入: s = "leetcode", wordDict = ["leet", "code"] +* 输出: true +* 解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。 + +示例 2: +* 输入: s = "applepenapple", wordDict = ["apple", "pen"] +* 输出: true +* 解释: 返回 true 因为 "applepenapple" 可以被拆分成 "apple pen apple"。 +* 注意你可以重复使用字典中的单词。 + +示例 3: +* 输入: s = "catsandog", wordDict = ["cats", "dog", "sand", "and", "cat"] +* 输出: false + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[你的背包如何装满?| LeetCode:139.单词拆分](https://www.bilibili.com/video/BV1pd4y147Rh/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +看到这道题目的时候,大家应该回想起我们之前讲解回溯法专题的时候,讲过的一道题目[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html),就是枚举字符串的所有分割情况。 + +[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html):是枚举分割后的所有子串,判断是否回文。 + +本道是枚举分割所有字符串,判断是否在字典里出现过。 + +那么这里我也给出回溯法C++代码: + +```CPP +class Solution { +private: + bool backtracking (const string& s, const unordered_set& wordSet, int startIndex) { + if (startIndex >= s.size()) { + return true; + } + for (int i = startIndex; i < s.size(); i++) { + string word = s.substr(startIndex, i - startIndex + 1); + if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, i + 1)) { + return true; + } + } + return false; + } +public: + bool wordBreak(string s, vector& wordDict) { + unordered_set wordSet(wordDict.begin(), wordDict.end()); + return backtracking(s, wordSet, 0); + } +}; +``` + +* 时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割 +* 空间复杂度:O(n),算法递归系统调用栈的空间 + +那么以上代码很明显要超时了,超时的数据如下: + +``` +"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" +["a","aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa"] +``` + +递归的过程中有很多重复计算,可以使用数组保存一下递归过程中计算的结果。 + +这个叫做记忆化递归,这种方法我们之前已经提过很多次了。 + +使用memory数组保存每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。 + +C++代码如下: + +```CPP +class Solution { +private: + bool backtracking (const string& s, + const unordered_set& wordSet, + vector& memory, + int startIndex) { + if (startIndex >= s.size()) { + return true; + } + // 如果memory[startIndex]不是初始值了,直接使用memory[startIndex]的结果 + if (!memory[startIndex]) return memory[startIndex]; + for (int i = startIndex; i < s.size(); i++) { + string word = s.substr(startIndex, i - startIndex + 1); + if (wordSet.find(word) != wordSet.end() && backtracking(s, wordSet, memory, i + 1)) { + return true; + } + } + memory[startIndex] = false; // 记录以startIndex开始的子串是不可以被拆分的 + return false; + } +public: + bool wordBreak(string s, vector& wordDict) { + unordered_set wordSet(wordDict.begin(), wordDict.end()); + vector memory(s.size(), 1); // -1 表示初始化状态 + return backtracking(s, wordSet, memory, 0); + } +}; +``` + +这个时间复杂度其实也是:O(2^n)。只不过对于上面那个超时测试用例优化效果特别明显。 + +**这个代码就可以AC了,当然回溯算法不是本题的主菜,背包才是!** + +### 背包问题 + +单词就是物品,字符串s就是背包,单词能否组成字符串s,就是问物品能不能把背包装满。 + +拆分时可以重复使用字典中的单词,说明就是一个完全背包! + +动规五部曲分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[i] : 字符串长度为i的话,dp[i]为true,表示可以拆分为一个或多个在字典中出现的单词**。 + +2. 确定递推公式 + +如果确定dp[j] 是true,且 [j, i] 这个区间的子串出现在字典里,那么dp[i]一定是true。(j < i )。 + +所以递推公式是 if([j, i] 这个区间的子串出现在字典里 && dp[j]是true) 那么 dp[i] = true。 + +3. dp数组如何初始化 + +从递推公式中可以看出,dp[i] 的状态依靠 dp[j]是否为true,那么dp[0]就是递推的根基,dp[0]一定要为true,否则递推下去后面都都是false了。 + +那么dp[0]有没有意义呢? + +dp[0]表示如果字符串为空的话,说明出现在字典里。 + +但题目中说了“给定一个非空字符串 s” 所以测试数据中不会出现i为0的情况,那么dp[0]初始为true完全就是为了推导公式。 + +下标非0的dp[i]初始化为false,只要没有被覆盖说明都是不可拆分为一个或多个在字典中出现的单词。 + +4. 确定遍历顺序 + +题目中说是拆分为一个或多个在字典中出现的单词,所以这是完全背包。 + +还要讨论两层for循环的前后顺序。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +我在这里做一个总结: + +求组合数:[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) +求排列数:[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和.html)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html) +求最小数:[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)、[动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html) + +而本题其实我们求的是排列数,为什么呢。 拿 s = "applepenapple", wordDict = ["apple", "pen"] 举例。 + +"apple", "pen" 是物品,那么我们要求 物品的组合一定是 "apple" + "pen" + "apple" 才能组成 "applepenapple"。 + +"apple" + "apple" + "pen" 或者 "pen" + "apple" + "apple" 是不可以的,那么我们就是强调物品之间顺序。 + +所以说,本题一定是 先遍历 背包,再遍历物品。 + +5. 举例推导dp[i] + +以输入: s = "leetcode", wordDict = ["leet", "code"]为例,dp状态如图: + + +![139.单词拆分](https://file1.kamacoder.com/i/algo/20210202162652727.jpg) + +dp[s.size()]就是最终结果。 + +动规五部曲分析完毕,C++代码如下: + +```CPP +class Solution { +public: + bool wordBreak(string s, vector& wordDict) { + unordered_set wordSet(wordDict.begin(), wordDict.end()); + vector dp(s.size() + 1, false); + dp[0] = true; + for (int i = 1; i <= s.size(); i++) { // 遍历背包 + for (int j = 0; j < i; j++) { // 遍历物品 + string word = s.substr(j, i - j); //substr(起始位置,截取的个数) + if (wordSet.find(word) != wordSet.end() && dp[j]) { + dp[i] = true; + } + } + } + return dp[s.size()]; + } +}; +``` + +* 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度) +* 空间复杂度:O(n) + +## 拓展 + +关于遍历顺序,再给大家讲一下为什么 先遍历物品再遍历背包不行。 + +这里可以给出先遍历物品再遍历背包的代码: + +```CPP +class Solution { +public: + bool wordBreak(string s, vector& wordDict) { + unordered_set wordSet(wordDict.begin(), wordDict.end()); + vector dp(s.size() + 1, false); + dp[0] = true; + for (int j = 0; j < wordDict.size(); j++) { // 物品 + for (int i = wordDict[j].size(); i <= s.size(); i++) { // 背包 + string word = s.substr(i - wordDict[j].size(), wordDict[j].size()); + // cout << word << endl; + if ( word == wordDict[j] && dp[i - wordDict[j].size()]) { + dp[i] = true; + } + // for (int k = 0; k <= s.size(); k++) cout << dp[k] << " "; //这里打印 dp数组的情况 + // cout << endl; + } + } + return dp[s.size()]; + + } +}; +``` + +使用用例:s = "applepenapple", wordDict = ["apple", "pen"],对应的dp数组状态如下: + +![](https://file1.kamacoder.com/i/algo/20240809155103.png) + +最后dp[s.size()] = 0 即 dp[13] = 0 ,而不是1,因为先用 "apple" 去遍历的时候,dp[8]并没有被赋值为1 (还没用"pen"),所以 dp[13]也不能变成1。 + +除非是先用 "apple" 遍历一遍,再用 "pen" 遍历,此时 dp[8]已经是1,最后再用 "apple" 去遍历,dp[13]才能是1。 + +如果大家对这里不理解,建议可以把我上面给的代码,拿去力扣上跑一跑,把dp数组打印出来,对着递推公式一步一步去看,思路就清晰了。 + +## 总结 + +本题和我们之前讲解回溯专题的[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html)非常像,所以我也给出了对应的回溯解法。 + +稍加分析,便可知道本题是完全背包,是求能否组成背包,而且这里要求物品是要有顺序的。 + +## 其他语言版本 + +### Java: + +```java +class Solution { + public boolean wordBreak(String s, List wordDict) { + HashSet set = new HashSet<>(wordDict); + boolean[] valid = new boolean[s.length() + 1]; + valid[0] = true; + + for (int i = 1; i <= s.length(); i++) { + for (int j = 0; j < i && !valid[i]; j++) { + if (set.contains(s.substring(j, i)) && valid[j]) { + valid[i] = true; + } + } + } + + return valid[s.length()]; + } +} + +// 另一种思路的背包算法 +class Solution { + public boolean wordBreak(String s, List wordDict) { + boolean[] dp = new boolean[s.length() + 1]; + dp[0] = true; + + for (int i = 1; i <= s.length(); i++) { + for (String word : wordDict) { + int len = word.length(); + if (i >= len && dp[i - len] && word.equals(s.substring(i - len, i))) { + dp[i] = true; + break; + } + } + } + + return dp[s.length()]; + } +} + +// 回溯法+记忆化 +class Solution { + private Set set; + private int[] memo; + public boolean wordBreak(String s, List wordDict) { + memo = new int[s.length()]; + set = new HashSet<>(wordDict); + return backtracking(s, 0); + } + + public boolean backtracking(String s, int startIndex) { + // System.out.println(startIndex); + if (startIndex == s.length()) { + return true; + } + if (memo[startIndex] == -1) { + return false; + } + + for (int i = startIndex; i < s.length(); i++) { + String sub = s.substring(startIndex, i + 1); + // 拆分出来的单词无法匹配 + if (!set.contains(sub)) { + continue; + } + boolean res = backtracking(s, i + 1); + if (res) return true; + } + // 这里是关键,找遍了startIndex~s.length()也没能完全匹配,标记从startIndex开始不能找到 + memo[startIndex] = -1; + return false; + } +} +``` + +### Python: + +回溯 +```python +class Solution: + def backtracking(self, s: str, wordSet: set[str], startIndex: int) -> bool: + # 边界情况:已经遍历到字符串末尾,返回True + if startIndex >= len(s): + return True + + # 遍历所有可能的拆分位置 + for i in range(startIndex, len(s)): + word = s[startIndex:i + 1] # 截取子串 + if word in wordSet and self.backtracking(s, wordSet, i + 1): + # 如果截取的子串在字典中,并且后续部分也可以被拆分成单词,返回True + return True + + # 无法进行有效拆分,返回False + return False + + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + wordSet = set(wordDict) # 转换为哈希集合,提高查找效率 + return self.backtracking(s, wordSet, 0) + +``` +DP(版本一) +```python +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + wordSet = set(wordDict) + n = len(s) + dp = [False] * (n + 1) # dp[i] 表示字符串的前 i 个字符是否可以被拆分成单词 + dp[0] = True # 初始状态,空字符串可以被拆分成单词 + + for i in range(1, n + 1): # 遍历背包 + for j in range(i): # 遍历单词 + if dp[j] and s[j:i] in wordSet: + dp[i] = True # 如果 s[0:j] 可以被拆分成单词,并且 s[j:i] 在单词集合中存在,则 s[0:i] 可以被拆分成单词 + break + + return dp[n] + + +``` +DP(版本二) + +```python +class Solution: + def wordBreak(self, s: str, wordDict: List[str]) -> bool: + dp = [False]*(len(s) + 1) + dp[0] = True + # 遍历背包 + for j in range(1, len(s) + 1): + # 遍历单词 + for word in wordDict: + if j >= len(word): + dp[j] = dp[j] or (dp[j - len(word)] and word == s[j - len(word):j]) + return dp[len(s)] +``` +DP(剪枝) + +```python +class Solution(object): + def wordBreak(self, s, wordDict): + + # 先对单词按长度排序 + wordDict.sort(key=lambda x: len(x)) + n = len(s) + dp = [False] * (n + 1) + dp[0] = True + # 遍历背包 + for i in range(1, n + 1): + # 遍历单词 + for word in wordDict: + # 简单的 “剪枝” + if len(word) > i: + break + dp[i] = dp[i] or (dp[i - len(word)] and s[i - len(word): i] == word) + return dp[-1] + +``` + + +### Go: + +```Go +func wordBreak(s string,wordDict []string) bool { + wordDictSet := make(map[string]bool) + for _, w := range wordDict { + wordDictSet[w] = true + } + dp := make([]bool, len(s)+1) + dp[0] = true + for i := 1; i <= len(s); i++ { + for j := 0; j < i; j++ { + if dp[j] && wordDictSet[s[j:i]] { + dp[i] = true + break + } + } + } + return dp[len(s)] +} +// 转化为 求装满背包s的前几位字符的方式有几种 +func wordBreak(s string, wordDict []string) bool { + // 装满背包s的前几位字符的方式有几种 + dp := make([]int, len(s)+1) + dp[0] = 1 + for i := 0; i <= len(s); i++ { // 背包 + for j := 0; j < len(wordDict); j++ { // 物品 + if i >= len(wordDict[j]) && wordDict[j] == s[i-len(wordDict[j]):i] { + dp[i] += dp[i-len(wordDict[j])] + } + } + } + + return dp[len(s)] > 0 +} +``` + +### JavaScript: + +```javascript +const wordBreak = (s, wordDict) => { + + let dp = Array(s.length + 1).fill(false); + dp[0] = true; + + for(let i = 0; i <= s.length; i++){ + for(let j = 0; j < wordDict.length; j++) { + if(i >= wordDict[j].length) { + if(s.slice(i - wordDict[j].length, i) === wordDict[j] && dp[i - wordDict[j].length]) { + dp[i] = true + } + } + } + } + + return dp[s.length]; +} +``` + +### TypeScript: + +> 动态规划 + +```typescript +function wordBreak(s: string, wordDict: string[]): boolean { + const dp: boolean[] = new Array(s.length + 1).fill(false); + dp[0] = true; + for (let i = 1; i <= s.length; i++) { + for (let j = 0; j < i; j++) { + const tempStr: string = s.slice(j, i); + if (wordDict.includes(tempStr) && dp[j] === true) { + dp[i] = true; + break; + } + } + } + return dp[s.length]; +}; +``` + +> 记忆化回溯 + +```typescript +function wordBreak(s: string, wordDict: string[]): boolean { + // 只需要记忆结果为false的情况 + const memory: boolean[] = []; + return backTracking(s, wordDict, 0, memory); + function backTracking(s: string, wordDict: string[], startIndex: number, memory: boolean[]): boolean { + if (startIndex >= s.length) return true; + if (memory[startIndex] === false) return false; + for (let i = startIndex + 1, length = s.length; i <= length; i++) { + const str: string = s.slice(startIndex, i); + if (wordDict.includes(str) && backTracking(s, wordDict, i, memory)) + return true; + } + memory[startIndex] = false; + return false; + } +}; +``` + +### C + +```c +bool wordBreak(char* s, char** wordDict, int wordDictSize) { + int len = strlen(s); + // 初始化 + bool dp[len + 1]; + memset(dp, false, sizeof (dp)); + dp[0] = true; + for (int i = 1; i < len + 1; ++i) { + for(int j = 0; j < wordDictSize; j++){ + int wordLen = strlen(wordDict[j]); + // 分割点是由i和字典单词长度决定 + int k = i - wordLen; + if(k < 0){ + continue; + } + // 这里注意要限制长度,故用strncmp + dp[i] = (dp[k] && !strncmp(s + k, wordDict[j], wordLen)) || dp[i]; + } + } + return dp[len]; +} +``` + + + +### Rust: + +```rust +impl Solution { + pub fn word_break(s: String, word_dict: Vec) -> bool { + let mut dp = vec![false; s.len() + 1]; + dp[0] = true; + for i in 1..=s.len() { + for j in 0..i { + if word_dict.iter().any(|word| *word == s[j..i]) && dp[j] { + dp[i] = true; + } + } + } + dp[s.len()] + } +} +``` + diff --git "a/problems/0141.\347\216\257\345\275\242\351\223\276\350\241\250.md" "b/problems/0141.\347\216\257\345\275\242\351\223\276\350\241\250.md" new file mode 100755 index 0000000000..d3583ba866 --- /dev/null +++ "b/problems/0141.\347\216\257\345\275\242\351\223\276\350\241\250.md" @@ -0,0 +1,160 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 141. 环形链表 + +[力扣题目链接](https://leetcode.cn/problems/linked-list-cycle/submissions/) + +给定一个链表,判断链表中是否有环。 + +如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。 + +如果链表中存在环,则返回 true 。 否则,返回 false 。 + +![](https://file1.kamacoder.com/i/algo/20210727173600.png) + +## 思路 + +可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 + +为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢? + +首先第一点: **fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。** + +那么来看一下,**为什么fast指针和slow指针一定会相遇呢?** + +可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 + +会发现最终都是这种情况, 如下图: + + + +fast和slow各自再走一步, fast和slow就相遇了 + +这是因为fast是走两步,slow是走一步,**其实相对于slow来说,fast是一个节点一个节点的靠近slow的**,所以fast一定可以和slow重合。 + +动画如下: + + +![141.环形链表](https://file1.kamacoder.com/i/algo/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif) + + +C++代码如下 + +```CPP +class Solution { +public: + bool hasCycle(ListNode *head) { + ListNode* fast = head; + ListNode* slow = head; + while(fast != NULL && fast->next != NULL) { + slow = slow->next; + fast = fast->next->next; + // 快慢指针相遇,说明有环 + if (slow == fast) return true; + } + return false; + } +}; +``` + +## 扩展 + +做完这道题目,可以在做做[142.环形链表II](https://programmercarl.com/0142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II.html),不仅仅要找环,还要找环的入口。 + + + +## 其他语言版本 + +### Java: + +```java +public class Solution { + public boolean hasCycle(ListNode head) { + ListNode fast = head; + ListNode slow = head; + // 空链表、单节点链表一定不会有环 + while (fast != null && fast.next != null) { + fast = fast.next.next; // 快指针,一次移动两步 + slow = slow.next; // 慢指针,一次移动一步 + if (fast == slow) { // 快慢指针相遇,表明有环 + return true; + } + } + return false; // 正常走到链表末尾,表明没有环 + } +} +``` + +### Python: + +```python +class Solution: + def hasCycle(self, head: ListNode) -> bool: + if not head: return False + slow, fast = head, head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + if fast == slow: + return True + return False +``` + +### Go: + +```go +func hasCycle(head *ListNode) bool { + if head==nil{ + return false + } //空链表一定不会有环 + fast:=head + slow:=head //快慢指针 + for fast.Next!=nil&&fast.Next.Next!=nil{ + fast=fast.Next.Next + slow=slow.Next + if fast==slow{ + return true //快慢指针相遇则有环 + } + } + return false +} +``` + +### JavaScript: + +```js +var hasCycle = function(head) { + let fast = head; + let slow = head; + // 空链表、单节点链表一定不会有环 + while(fast != null && fast.next != null){ + fast = fast.next.next; // 快指针,一次移动两步 + slow = slow.next; // 慢指针,一次移动一步 + if(fast === slow) return true; // 快慢指针相遇,表明有环 + } + return false; // 正常走到链表末尾,表明没有环 +}; +``` + +### TypeScript: + +```typescript +function hasCycle(head: ListNode | null): boolean { + let slowNode: ListNode | null = head, + fastNode: ListNode | null = head; + while (fastNode !== null && fastNode.next !== null) { + slowNode = slowNode!.next; + fastNode = fastNode.next.next; + if (slowNode === fastNode) return true; + } + return false; +}; +``` + + + + + diff --git "a/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" "b/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" new file mode 100755 index 0000000000..4fd81ef0f5 --- /dev/null +++ "b/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" @@ -0,0 +1,465 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + + +> 找到有没有环已经很不容易了,还要让我找到环的入口? + + +# 142.环形链表II + +[力扣题目链接](https://leetcode.cn/problems/linked-list-cycle-ii/) + +题意: +给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 + +为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 + +**说明**:不允许修改给定的链表。 + +![循环链表](https://file1.kamacoder.com/i/algo/20200816110112704.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[把环形链表讲清楚!| LeetCode:142.环形链表II](https://www.bilibili.com/video/BV1if4y1d7ob),相信结合视频再看本篇题解,更有助于大家对链表的理解。** + +## 思路 + + +这道题目,不仅考察对链表的操作,而且还需要一些数学运算。 + +主要考察两知识点: + +* 判断链表是否环 +* 如果有环,如何找到这个环的入口 + +### 判断链表是否有环 + +可以使用快慢指针法,分别定义 fast 和 slow 指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 + +为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢 + +首先第一点:**fast指针一定先进入环中,如果fast指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。** + +那么来看一下,**为什么fast指针和slow指针一定会相遇呢?** + +可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 + +会发现最终都是这种情况, 如下图: + +![142环形链表1](https://file1.kamacoder.com/i/algo/20210318162236720.png) + + +fast和slow各自再走一步, fast和slow就相遇了 + +这是因为fast是走两步,slow是走一步,**其实相对于slow来说,fast是一个节点一个节点的靠近slow的**,所以fast一定可以和slow重合。 + +动画如下: + +![141.环形链表](https://file1.kamacoder.com/i/algo/141.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8.gif) + + +### 如果有环,如何找到这个环的入口 + +**此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。** + +假设从头结点到环形入口节点 的节点数为x。 +环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 +从相遇节点 再到环形入口节点节点数为 z。 如图所示: + +![](https://file1.kamacoder.com/i/algo/20220925103433.png) + +那么相遇时: +slow指针走过的节点数为: `x + y`, +fast指针走过的节点数:` x + y + n (y + z)`,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。 + +因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2: + +`(x + y) * 2 = x + y + n (y + z)` + +两边消掉一个(x+y): `x + y = n (y + z) ` + +因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。 + +所以要求x ,将x单独放在左面:`x = n (y + z) - y` , + +再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:`x = (n - 1) (y + z) + z ` 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。 + +这个公式说明什么呢? + +先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。 + +当 n为1的时候,公式就化解为 `x = z`, + +这就意味着,**从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点**。 + + +也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。 + +让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。 + +动画如下: + +![142.环形链表II(求入口)](https://file1.kamacoder.com/i/algo/142.%E7%8E%AF%E5%BD%A2%E9%93%BE%E8%A1%A8II%EF%BC%88%E6%B1%82%E5%85%A5%E5%8F%A3%EF%BC%89.gif) + + +那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。 + +其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。 + +代码如下: + +```CPP +/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * ListNode *next; + * ListNode(int x) : val(x), next(NULL) {} + * }; + */ +class Solution { +public: + ListNode *detectCycle(ListNode *head) { + ListNode* fast = head; + ListNode* slow = head; + while(fast != NULL && fast->next != NULL) { + slow = slow->next; + fast = fast->next->next; + // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇 + if (slow == fast) { + ListNode* index1 = fast; + ListNode* index2 = head; + while (index1 != index2) { + index1 = index1->next; + index2 = index2->next; + } + return index2; // 返回环的入口 + } + } + return NULL; + } +}; +``` + +* 时间复杂度: O(n),快慢指针相遇前,指针走的次数小于链表长度,快慢指针相遇后,两个index指针走的次数也小于链表长度,总体为走的次数小于 2n +* 空间复杂度: O(1) + +### 补充 + +在推理过程中,大家可能有一个疑问就是:**为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?** + +即文章[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中如下的地方: + +![142环形链表5](https://file1.kamacoder.com/i/algo/20210318165123581.png) + + +首先slow进环的时候,fast一定是先进环来了。 + +如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子: + +![142环形链表3](https://file1.kamacoder.com/i/algo/2021031816503266.png) + +可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。 + +重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图: + +![142环形链表4](https://file1.kamacoder.com/i/algo/2021031816515727.png) + +那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。 + +因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 + +**也就是说slow一定没有走到环入口3,而fast已经到环入口3了**。 + +这说明什么呢? + +**在slow开始走的那一环已经和fast相遇了**。 + +那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,**fast相对于slow是一次移动一个节点,所以不可能跳过去**。 + +好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)的补充。 + +## 总结 + +这次可以说把环形链表这道题目的各个细节,完完整整的证明了一遍,说这是全网最详细讲解不为过吧。 + + +## 其他语言版本 + +### Java: + +```java +public class Solution { + public ListNode detectCycle(ListNode head) { + ListNode slow = head; + ListNode fast = head; + while (fast != null && fast.next != null) { + slow = slow.next; + fast = fast.next.next; + if (slow == fast) {// 有环 + ListNode index1 = fast; + ListNode index2 = head; + // 两个指针,从头结点和相遇结点,各走一步,直到相遇,相遇点即为环入口 + while (index1 != index2) { + index1 = index1.next; + index2 = index2.next; + } + return index1; + } + } + return null; + } +} +``` + +### Python: + +```python +(版本一)快慢指针法 +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, x): +# self.val = x +# self.next = None + + +class Solution: + def detectCycle(self, head: ListNode) -> ListNode: + slow = head + fast = head + + while fast and fast.next: + slow = slow.next + fast = fast.next.next + + # If there is a cycle, the slow and fast pointers will eventually meet + if slow == fast: + # Move one of the pointers back to the start of the list + slow = head + while slow != fast: + slow = slow.next + fast = fast.next + return slow + # If there is no cycle, return None + return None +``` +```python +(版本二)集合法 +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, x): +# self.val = x +# self.next = None + + +class Solution: + def detectCycle(self, head: ListNode) -> ListNode: + visited = set() + + while head: + if head in visited: + return head + visited.add(head) + head = head.next + + return None +``` +### Go: + +```go +func detectCycle(head *ListNode) *ListNode { + slow, fast := head, head + for fast != nil && fast.Next != nil { + slow = slow.Next + fast = fast.Next.Next + if slow == fast { + for slow != head { + slow = slow.Next + head = head.Next + } + return head + } + } + return nil +} +``` + +### JavaScript + +```js +// 两种循环实现方式 +/** + * @param {ListNode} head + * @return {ListNode} + */ +// 先判断是否是环形链表 +var detectCycle = function(head) { + if(!head || !head.next) return null; + let slow =head.next, fast = head.next.next; + while(fast && fast.next && fast!== slow) { + slow = slow.next; + fast = fast.next.next; + } + if(!fast || !fast.next ) return null; + slow = head; + while (fast !== slow) { + slow = slow.next; + fast = fast.next; + } + return slow; +}; + +var detectCycle = function(head) { + if(!head || !head.next) return null; + let slow =head.next, fast = head.next.next; + while(fast && fast.next) { + slow = slow.next; + fast = fast.next.next; + if(fast == slow) { + slow = head; + while (fast !== slow) { + slow = slow.next; + fast = fast.next; + } + return slow; + } + } + return null; +}; +``` + +### TypeScript: + +```typescript +function detectCycle(head: ListNode | null): ListNode | null { + let slowNode: ListNode | null = head, + fastNode: ListNode | null = head; + while (fastNode !== null && fastNode.next !== null) { + slowNode = slowNode!.next; + fastNode = fastNode.next.next; + if (slowNode === fastNode) { + slowNode = head; + while (slowNode !== fastNode) { + slowNode = slowNode!.next; + fastNode = fastNode!.next; + } + return slowNode; + } + } + return null; +}; +``` + +### Swift: + +```swift +class Solution { + func detectCycle(_ head: ListNode?) -> ListNode? { + var slow: ListNode? = head + var fast: ListNode? = head + while fast != nil && fast?.next != nil { + slow = slow?.next + fast = fast?.next?.next + if slow == fast { + // 环内相遇 + var list1: ListNode? = slow + var list2: ListNode? = head + while list1 != list2 { + list1 = list1?.next + list2 = list2?.next + } + return list2 + } + } + return nil + } +} +extension ListNode: Equatable { + public func hash(into hasher: inout Hasher) { + hasher.combine(val) + hasher.combine(ObjectIdentifier(self)) + } + public static func == (lhs: ListNode, rhs: ListNode) -> Bool { + return lhs === rhs + } +} +``` + +### C: + +```c +ListNode *detectCycle(ListNode *head) { + ListNode *fast = head, *slow = head; + while (fast && fast->next) { + // 这里判断两个指针是否相等,所以移位操作放在前面 + slow = slow->next; + fast = fast->next->next; + if (slow == fast) { // 相交,开始找环形入口:分别从头部和从交点出发,找到相遇的点就是环形入口 + ListNode *f = fast, *h = head; + while (f != h) f = f->next, h = h->next; + return h; + } + } + return NULL; +} +``` + +### Scala: + +```scala +object Solution { + def detectCycle(head: ListNode): ListNode = { + var fast = head // 快指针 + var slow = head // 慢指针 + while (fast != null && fast.next != null) { + fast = fast.next.next // 快指针一次走两步 + slow = slow.next // 慢指针一次走一步 + // 如果相遇,fast快指针回到头 + if (fast == slow) { + fast = head + // 两个指针一步一步的走,第一次相遇的节点必是入环节点 + while (fast != slow) { + fast = fast.next + slow = slow.next + } + return fast + } + } + // 如果fast指向空值,必然无环返回null + null + } +} +``` + +### C#: +```CSharp +public class Solution +{ + public ListNode DetectCycle(ListNode head) + { + ListNode fast = head; + ListNode slow = head; + while (fast != null && fast.next != null) + { + slow = slow.next; + fast = fast.next.next; + if (fast == slow) + { + fast = head; + while (fast != slow) + { + fast = fast.next; + slow = slow.next; + } + return fast; + } + } + return null; + } +} +``` + diff --git "a/problems/0143.\351\207\215\346\216\222\351\223\276\350\241\250.md" "b/problems/0143.\351\207\215\346\216\222\351\223\276\350\241\250.md" new file mode 100755 index 0000000000..e7056913ee --- /dev/null +++ "b/problems/0143.\351\207\215\346\216\222\351\223\276\350\241\250.md" @@ -0,0 +1,689 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 143.重排链表 + +[力扣题目链接](https://leetcode.cn/problems/reorder-list/submissions/) + +![](https://file1.kamacoder.com/i/algo/20210726160122.png) + +## 思路 + +本篇将给出三种C++实现的方法 + +* 数组模拟 +* 双向队列模拟 +* 直接分割链表 + +### 方法一 + +把链表放进数组中,然后通过双指针法,一前一后,来遍历数组,构造链表。 + +代码如下: + +```CPP +class Solution { +public: + void reorderList(ListNode* head) { + vector vec; + ListNode* cur = head; + if (cur == nullptr) return; + while(cur != nullptr) { + vec.push_back(cur); + cur = cur->next; + } + cur = head; + int i = 1; + int j = vec.size() - 1; // i j为之前前后的双指针 + int count = 0; // 计数,偶数取后面,奇数取前面 + while (i <= j) { + if (count % 2 == 0) { + cur->next = vec[j]; + j--; + } else { + cur->next = vec[i]; + i++; + } + cur = cur->next; + count++; + } + cur->next = nullptr; // 注意结尾 + } +}; +``` + +### 方法二 + +把链表放进双向队列,然后通过双向队列一前一后弹出数据,来构造新的链表。这种方法比操作数组容易一些,不用双指针模拟一前一后了 + +```CPP +class Solution { +public: + void reorderList(ListNode* head) { + deque que; + ListNode* cur = head; + if (cur == nullptr) return; + + while(cur->next != nullptr) { + que.push_back(cur->next); + cur = cur->next; + } + + cur = head; + int count = 0; // 计数,偶数取后面,奇数取前面 + ListNode* node; + while(que.size()) { + if (count % 2 == 0) { + node = que.back(); + que.pop_back(); + } else { + node = que.front(); + que.pop_front(); + } + count++; + cur->next = node; + cur = cur->next; + } + cur->next = nullptr; // 注意结尾 + } +}; +``` + +### 方法三 + +将链表分割成两个链表,然后把第二个链表反转,之后在通过两个链表拼接成新的链表。 + +如图: + + + +这种方法,比较难,平均切割链表,看上去很简单,真正代码写的时候有很多细节,同时两个链表最后拼装整一个新的链表也有一些细节需要注意! + +代码如下: + +```CPP +class Solution { +private: + // 反转链表 + ListNode* reverseList(ListNode* head) { + ListNode* temp; // 保存cur的下一个节点 + ListNode* cur = head; + ListNode* pre = NULL; + while(cur) { + temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next + cur->next = pre; // 翻转操作 + // 更新pre 和 cur指针 + pre = cur; + cur = temp; + } + return pre; + } + +public: + void reorderList(ListNode* head) { + if (head == nullptr) return; + // 使用快慢指针法,将链表分成长度均等的两个链表head1和head2 + // 如果总链表长度为奇数,则head1相对head2多一个节点 + ListNode* fast = head; + ListNode* slow = head; + while (fast && fast->next && fast->next->next) { + fast = fast->next->next; + slow = slow->next; + } + ListNode* head1 = head; + ListNode* head2; + head2 = slow->next; + slow->next = nullptr; + + // 对head2进行翻转 + head2 = reverseList(head2); + + // 将head1和head2交替生成新的链表head + ListNode* cur1 = head1; + ListNode* cur2 = head2; + ListNode* cur = head; + cur1 = cur1->next; + int count = 0; // 偶数取head2的元素,奇数取head1的元素 + while (cur1 && cur2) { + if (count % 2 == 0) { + cur->next = cur2; + cur2 = cur2->next; + } else { + cur->next = cur1; + cur1 = cur1->next; + } + count++; + cur = cur->next; + } + if (cur2 != nullptr) { // 处理结尾 + cur->next = cur2; + } + if (cur1 != nullptr) { + cur->next = cur1; + } + } +}; +``` + +## 其他语言版本 + +### Java + +```java + +// 方法一 Java实现,使用数组存储节点 + class Solution { + public void reorderList(ListNode head) { + // 双指针的做法 + ListNode cur = head; + // ArrayList底层是数组,可以使用下标随机访问 + List list = new ArrayList<>(); + while (cur != null){ + list.add(cur); + cur = cur.next; + } + cur = head; // 重新回到头部 + int l = 1, r = list.size() - 1; // 注意左边是从1开始 + int count = 0; + while (l <= r){ + if (count % 2 == 0){ + // 偶数 + cur.next = list.get(r); + r--; + }else { + // 奇数 + cur.next = list.get(l); + l++; + } + // 每一次指针都需要移动 + cur = cur.next; + count++; + } + // 注意结尾要结束一波 + cur.next = null; + } +} +// 方法二:使用双端队列,简化了数组的操作,代码相对于前者更简洁(避免一些边界条件) +class Solution { + public void reorderList(ListNode head) { + // 使用双端队列的方法来解决 + Deque de = new LinkedList<>(); + // 这里是取head的下一个节点,head不需要再入队了,避免造成重复 + ListNode cur = head.next; + while (cur != null){ + de.offer(cur); + cur = cur.next; + } + cur = head; // 回到头部 + + int count = 0; + while (!de.isEmpty()){ + if (count % 2 == 0){ + // 偶数,取出队列右边尾部的值 + cur.next = de.pollLast(); + }else { + // 奇数,取出队列左边头部的值 + cur.next = de.poll(); + } + cur = cur.next; + count++; + } + cur.next = null; + } +} + +// 方法三 +public class ReorderList { + public void reorderList(ListNode head) { + ListNode fast = head, slow = head; + //求出中点 + while (fast.next != null && fast.next.next != null) { + slow = slow.next; + fast = fast.next.next; + } + //right就是右半部分 12345 就是45 1234 就是34 + ListNode right = slow.next; + //断开左部分和右部分 + slow.next = null; + //反转右部分 right就是反转后右部分的起点 + right = reverseList(right); + //左部分的起点 + ListNode left = head; + //进行左右部分来回连接 + //这里左部分的节点个数一定大于等于右部分的节点个数 因此只判断right即可 + while (right != null) { + ListNode curLeft = left.next; + left.next = right; + left = curLeft; + + ListNode curRight = right.next; + right.next = left; + right = curRight; + } + } + + public ListNode reverseList(ListNode head) { + ListNode headNode = new ListNode(0); + ListNode cur = head; + ListNode next = null; + while (cur != null) { + next = cur.next; + cur.next = headNode.next; + headNode.next = cur; + cur = next; + } + return headNode.next; + } +} + +``` + +### Python +```python +# 方法二 双向队列 +class Solution: + def reorderList(self, head: ListNode) -> None: + """ + Do not return anything, modify head in-place instead. + """ + d = collections.deque() + tmp = head + while tmp.next: # 链表除了首元素全部加入双向队列 + d.append(tmp.next) + tmp = tmp.next + tmp = head + while len(d): # 一后一前加入链表 + tmp.next = d.pop() + tmp = tmp.next + if len(d): + tmp.next = d.popleft() + tmp = tmp.next + tmp.next = None # 尾部置空 + +# 方法三 反转链表 +class Solution: + def reorderList(self, head: ListNode) -> None: + if head == None or head.next == None: + return True + slow, fast = head, head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + right = slow.next # 分割右半边 + slow.next = None # 切断 + right = self.reverseList(right) #反转右半边 + left = head + # 左半边一定比右半边长, 因此判断右半边即可 + while right: + curLeft = left.next + left.next = right + left = curLeft + + curRight = right.next + right.next = left + right = curRight + + + def reverseList(self, head: ListNode) -> ListNode: + cur = head + pre = None + while(cur!=None): + temp = cur.next # 保存一下cur的下一个节点 + cur.next = pre # 反转 + pre = cur + cur = temp + return pre +``` +### Go + +```go +// 方法一 数组模拟 +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func reorderList(head *ListNode) { + vec := make([]*ListNode, 0) + cur := head + if cur == nil { + return + } + for cur != nil { + vec = append(vec, cur) + cur = cur.Next + } + cur = head + i := 1 + j := len(vec) - 1 // i j为前后的双指针 + count := 0 // 计数,偶数取后面,奇数取前面 + for i <= j { + if count % 2 == 0 { + cur.Next = vec[j] + j-- + } else { + cur.Next = vec[i] + i++ + } + cur = cur.Next + count++ + } + cur.Next = nil // 注意结尾 +} +``` + +```go +// 方法二 双向队列模拟 +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func reorderList(head *ListNode) { + que := make([]*ListNode, 0) + cur := head + if cur == nil { + return + } + + for cur.Next != nil { + que = append(que, cur.Next) + cur = cur.Next + } + + cur = head + count := 0 // 计数,偶数取后面,奇数取前面 + for len(que) > 0 { + if count % 2 == 0 { + cur.Next = que[len(que)-1] + que = que[:len(que)-1] + } else { + cur.Next = que[0] + que = que[1:] + } + count++ + cur = cur.Next + } + cur.Next = nil // 注意结尾 +} +``` + +```go +// 方法三 分割链表 +func reorderList(head *ListNode) { + var slow=head + var fast=head + for fast!=nil&&fast.Next!=nil{ + slow=slow.Next + fast=fast.Next.Next + } //双指针将链表分为左右两部分 + var right =new(ListNode) + for slow!=nil{ + temp:=slow.Next + slow.Next=right.Next + right.Next=slow + slow=temp + } //翻转链表右半部分 + right=right.Next //right为反转后得右半部分 + h:=head + for right.Next!=nil{ + temp:=right.Next + right.Next=h.Next + h.Next=right + h=h.Next.Next + right=temp + } //将左右两部分重新组合 +} +``` +### JavaScript + +```javascript +// 方法一 使用数组存储节点 +var reorderList = function(head, s = [], tmp) { + let cur = head; + // list是数组,可以使用下标随机访问 + const list = []; + while(cur != null){ + list.push(cur); + cur = cur.next; + } + cur = head; // 重新回到头部 + let l = 1, r = list.length - 1; // 注意左边是从1开始 + let count = 0; + while(l <= r){ + if(count % 2 == 0){ + // even + cur.next = list[r]; + r--; + } else { + // odd + cur.next = list[l]; + l++; + } + // 每一次指针都需要移动 + cur = cur.next; + count++; + } + // 注意结尾要结束一波 + cur.next = null; +} + +// 方法二 使用双端队列的方法来解决 js中运行很慢 +var reorderList = function(head, s = [], tmp) { + // js数组作为双端队列 + const deque = []; + // 这里是取head的下一个节点,head不需要再入队了,避免造成重复 + let cur = head.next; + while(cur != null){ + deque.push(cur); + cur = cur.next; + } + cur = head; // 回到头部 + let count = 0; + while(deque.length !== 0){ + if(count % 2 == 0){ + // even,取出队列右边尾部的值 + cur.next = deque.pop(); + } else { + // odd, 取出队列左边头部的值 + cur.next = deque.shift(); + } + cur = cur.next; + count++; + } + cur.next = null; +} + +//方法三 将链表分割成两个链表,然后把第二个链表反转,之后在通过两个链表拼接成新的链表 +var reorderList = function(head, s = [], tmp) { + const reverseList = head => { + let headNode = new ListNode(0); + let cur = head; + let next = null; + while(cur != null){ + next = cur.next; + cur.next = headNode.next; + headNode.next = cur; + cur = next; + } + return headNode.next; + } + + let fast = head, slow = head; + //求出中点 + while(fast.next != null && fast.next.next != null){ + slow = slow.next; + fast = fast.next.next; + } + //right就是右半部分 12345 就是45 1234 就是34 + let right = slow.next; + //断开左部分和右部分 + slow.next = null; + //反转右部分 right就是反转后右部分的起点 + right = reverseList(right); + //左部分的起点 + let left = head; + //进行左右部分来回连接 + //这里左部分的节点个数一定大于等于右部分的节点个数 因此只判断right即可 + while (right != null) { + let curLeft = left.next; + left.next = right; + left = curLeft; + + let curRight = right.next; + right.next = left; + right = curRight; + } +} +``` + +### TypeScript + +> 辅助数组法: + +```typescript +function reorderList(head: ListNode | null): void { + if (head === null) return; + const helperArr: ListNode[] = []; + let curNode: ListNode | null = head; + while (curNode !== null) { + helperArr.push(curNode); + curNode = curNode.next; + } + let node: ListNode = head; + let left: number = 1, + right: number = helperArr.length - 1; + let count: number = 0; + while (left <= right) { + if (count % 2 === 0) { + node.next = helperArr[right--]; + } else { + node.next = helperArr[left++]; + } + count++; + node = node.next; + } + node.next = null; +}; +``` + +> 分割链表法: + +```typescript +function reorderList(head: ListNode | null): void { + if (head === null || head.next === null) return; + let fastNode: ListNode = head, + slowNode: ListNode = head; + while (fastNode.next !== null && fastNode.next.next !== null) { + slowNode = slowNode.next!; + fastNode = fastNode.next.next; + } + let head1: ListNode | null = head; + // 反转后半部分链表 + let head2: ListNode | null = reverseList(slowNode.next); + // 分割链表 + slowNode.next = null; + /** + 直接在head1链表上进行插入 + head1 链表长度一定大于或等于head2, + 因此在下面的循环中,只要head2不为null, head1 一定不为null + */ + while (head2 !== null) { + const tempNode1: ListNode | null = head1!.next, + tempNode2: ListNode | null = head2.next; + head1!.next = head2; + head2.next = tempNode1; + head1 = tempNode1; + head2 = tempNode2; + } +}; +function reverseList(head: ListNode | null): ListNode | null { + let curNode: ListNode | null = head, + preNode: ListNode | null = null; + while (curNode !== null) { + const tempNode: ListNode | null = curNode.next; + curNode.next = preNode; + preNode = curNode; + curNode = tempNode; + } + return preNode; +} +``` + +### C + +方法三:反转链表 +```c +//翻转链表 +struct ListNode *reverseList(struct ListNode *head) { + if(!head) + return NULL; + struct ListNode *preNode = NULL, *curNode = head; + while(curNode) { + //创建tempNode记录curNode->next(即将被更新) + struct ListNode* tempNode = curNode->next; + //将curNode->next指向preNode + curNode->next = preNode; + //更新preNode为curNode + preNode = curNode; + //curNode更新为原链表中下一个元素 + curNode = tempNode; + } + return preNode; +} + +void reorderList(struct ListNode* head){ + //slow用来截取到链表的中间节点(第一个链表的最后节点),每次循环跳一个节点。fast用来辅助,每次循环跳两个节点 + struct ListNode *fast = head, *slow = head; + while(fast && fast->next && fast->next->next) { + //fast每次跳两个节点 + fast = fast->next->next; + //slow每次跳一个节点 + slow = slow->next; + } + //将slow->next后的节点翻转 + struct ListNode *sndLst = reverseList(slow->next); + //将第一个链表与第二个链表断开 + slow->next = NULL; + //因为插入从curNode->next开始,curNode刚开始已经head。所以fstList要从head->next开始 + struct ListNode *fstLst = head->next; + struct ListNode *curNode = head; + + int count = 0; + //当第一个链表和第二个链表中都有节点时循环 + while(sndLst && fstLst) { + //count为奇数,插入fstLst中的节点 + if(count % 2) { + curNode->next = fstLst; + fstLst = fstLst->next; + } + //count为偶数,插入sndList的节点 + else { + curNode->next = sndLst; + sndLst = sndLst->next; + } + //设置下一个节点 + curNode = curNode->next; + //更新count + ++count; + } + + //若两个链表fstList和sndLst中还有节点,将其放入链表 + if(fstLst) { + curNode->next = fstLst; + } + if(sndLst) { + curNode->next = sndLst; + } + + //返回链表 + return head; +} +``` + + diff --git "a/problems/0150.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" "b/problems/0150.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" new file mode 100755 index 0000000000..de56c51ff7 --- /dev/null +++ "b/problems/0150.\351\200\206\346\263\242\345\205\260\350\241\250\350\276\276\345\274\217\346\261\202\345\200\274.md" @@ -0,0 +1,550 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 这不仅仅是一道好题,也展现出计算机的思考方式 + +# 150. 逆波兰表达式求值 + +[力扣题目链接](https://leetcode.cn/problems/evaluate-reverse-polish-notation/) + +根据 逆波兰表示法,求表达式的值。 + +有效的运算符包括 + ,  - ,  * ,  / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 + +说明: + +整数除法只保留整数部分。 +给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。 + + +示例 1: +* 输入: ["2", "1", "+", "3", " * "] +* 输出: 9 +* 解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 + +示例 2: +* 输入: ["4", "13", "5", "/", "+"] +* 输出: 6 +* 解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 + +示例 3: +* 输入: ["10", "6", "9", "3", "+", "-11", " * ", "/", " * ", "17", "+", "5", "+"] + +* 输出: 22 + +* 解释:该算式转化为常见的中缀算术表达式为: + + ``` + ((10 * (6 / ((9 + 3) * -11))) + 17) + 5 + = ((10 * (6 / (12 * -11))) + 17) + 5 + = ((10 * (6 / -132)) + 17) + 5 + = ((10 * 0) + 17) + 5 + = (0 + 17) + 5 + = 17 + 5 + = 22 + ``` + + +逆波兰表达式:是一种后缀表达式,所谓后缀就是指运算符写在后面。 + +平常使用的算式则是一种中缀表达式,如 ( 1 + 2 ) * ( 3 + 4 ) 。 + +该算式的逆波兰表达式写法为 ( ( 1 2 + ) ( 3 4 + ) * ) 。 + +逆波兰表达式主要有以下两个优点: + +* 去掉括号后表达式无歧义,上式即便写成 1 2 + 3 4 + * 也可以依据次序计算出正确结果。 + +* 适合用栈操作运算:遇到数字则入栈;遇到运算符则取出栈顶两个数字进行计算,并将结果压入栈中。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[栈的最后表演! | LeetCode:150. 逆波兰表达式求值](https://www.bilibili.com/video/BV1kd4y1o7on),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 正题 + +在上一篇文章中[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)提到了 递归就是用栈来实现的。 + +所以**栈与递归之间在某种程度上是可以转换的!** 这一点我们在后续讲解二叉树的时候,会更详细的讲解到。 + +那么来看一下本题,**其实逆波兰表达式相当于是二叉树中的后序遍历**。 大家可以把运算符作为中间节点,按照后序遍历的规则画出一个二叉树。 + +但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后序遍历的方式把二叉树序列化了,就可以了。 + +在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[1047.删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)中的对对碰游戏是不是就非常像了。** + +如动画所示: +![150.逆波兰表达式求值](https://file1.kamacoder.com/i/algo/150.逆波兰表达式求值.gif) + +相信看完动画大家应该知道,这和[1047. 删除字符串中的所有相邻重复项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)是差不多的,只不过本题不要相邻元素做消除了,而是做运算! + +C++代码如下: + + +```CPP +class Solution { +public: + int evalRPN(vector& tokens) { + // 力扣修改了后台测试数据,需要用longlong + stack st; + for (int i = 0; i < tokens.size(); i++) { + if (tokens[i] == "+" || tokens[i] == "-" || tokens[i] == "*" || tokens[i] == "/") { + long long num1 = st.top(); + st.pop(); + long long num2 = st.top(); + st.pop(); + if (tokens[i] == "+") st.push(num2 + num1); + if (tokens[i] == "-") st.push(num2 - num1); + if (tokens[i] == "*") st.push(num2 * num1); + if (tokens[i] == "/") st.push(num2 / num1); + } else { + st.push(stoll(tokens[i])); + } + } + + long long result = st.top(); + st.pop(); // 把栈里最后一个元素弹出(其实不弹出也没事) + return result; + } +}; + +``` +* 时间复杂度: O(n) +* 空间复杂度: O(n) + + +### 题外话 + +我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。 + +例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算符,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法,你说麻不麻烦! + +那么将中缀表达式,转化为后缀表达式之后:["4", "13", "5", "/", "+"] ,就不一样了,计算机可以利用栈来顺序处理,不需要考虑优先级了。也不用回退了, **所以后缀表达式对计算机来说是非常友好的。** + +可以说本题不仅仅是一道好题,也展现出计算机的思考方式。 + +在1970年代和1980年代,惠普在其所有台式和手持式计算器中都使用了RPN(后缀表达式),直到2020年代仍在某些模型中使用了RPN。 + +参考维基百科如下: + +> During the 1970s and 1980s, Hewlett-Packard used RPN in all of their desktop and hand-held calculators, and continued to use it in some models into the 2020s. + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int evalRPN(String[] tokens) { + Deque stack = new LinkedList(); + for (String s : tokens) { + if ("+".equals(s)) { // leetcode 内置jdk的问题,不能使用==判断字符串是否相等 + stack.push(stack.pop() + stack.pop()); // 注意 - 和/ 需要特殊处理 + } else if ("-".equals(s)) { + stack.push(-stack.pop() + stack.pop()); + } else if ("*".equals(s)) { + stack.push(stack.pop() * stack.pop()); + } else if ("/".equals(s)) { + int temp1 = stack.pop(); + int temp2 = stack.pop(); + stack.push(temp2 / temp1); + } else { + stack.push(Integer.valueOf(s)); + } + } + return stack.pop(); + } +} +``` + +### Python3: + +```python +from operator import add, sub, mul + +def div(x, y): + # 使用整数除法的向零取整方式 + return int(x / y) if x * y > 0 else -(abs(x) // abs(y)) + +class Solution(object): + op_map = {'+': add, '-': sub, '*': mul, '/': div} + + def evalRPN(self, tokens: List[str]) -> int: + stack = [] + for token in tokens: + if token not in {'+', '-', '*', '/'}: + stack.append(int(token)) + else: + op2 = stack.pop() + op1 = stack.pop() + stack.append(self.op_map[token](op1, op2)) # 第一个出来的在运算符后面 + return stack.pop() +``` + +另一种可行,但因为使用eval()相对较慢的方法: +```python +class Solution(object): + def evalRPN(self, tokens: List[str]) -> int: + stack = [] + for token in tokens: + # 判断是否为数字,因为isdigit()不识别负数,故需要排除第一位的符号 + if token.isdigit() or (len(token)>1 and token[1].isdigit()): + stack.append(token) + else: + op2 = stack.pop() + op1 = stack.pop() + # 由题意"The division always truncates toward zero",所以使用int()可以天然取整 + stack.append(str(int(eval(op1 + token + op2)))) + return int(stack.pop()) +``` + +### Go: + +```Go +func evalRPN(tokens []string) int { + stack := []int{} + for _, token := range tokens { + val, err := strconv.Atoi(token) + if err == nil { + stack = append(stack, val) + } else { // 如果err不为nil说明不是数字 + num1, num2 := stack[len(stack)-2], stack[(len(stack))-1] + stack = stack[:len(stack)-2] + switch token { + case "+": + stack = append(stack, num1+num2) + case "-": + stack = append(stack, num1-num2) + case "*": + stack = append(stack, num1*num2) + case "/": + stack = append(stack, num1/num2) + } + } + } + return stack[0] +} +``` + +### JavaScript: + +```js +var evalRPN = function (tokens) { + const stack = []; + for (const token of tokens) { + if (isNaN(Number(token))) { // 非数字 + const n2 = stack.pop(); // 出栈两个数字 + const n1 = stack.pop(); + switch (token) { // 判断运算符类型,算出新数入栈 + case "+": + stack.push(n1 + n2); + break; + case "-": + stack.push(n1 - n2); + break; + case "*": + stack.push(n1 * n2); + break; + case "/": + stack.push(n1 / n2 | 0); + break; + } + } else { // 数字 + stack.push(Number(token)); + } + } + return stack[0]; // 因没有遇到运算符而待在栈中的结果 +}; +``` + +### TypeScript: + +普通版: + +```typescript +function evalRPN(tokens: string[]): number { + let helperStack: number[] = []; + let temp: number; + let i: number = 0; + while (i < tokens.length) { + let t: string = tokens[i]; + switch (t) { + case '+': + temp = helperStack.pop()! + helperStack.pop()!; + helperStack.push(temp); + break; + case '-': + temp = helperStack.pop()!; + temp = helperStack.pop()! - temp; + helperStack.push(temp); + break; + case '*': + temp = helperStack.pop()! * helperStack.pop()!; + helperStack.push(temp); + break; + case '/': + temp = helperStack.pop()!; + temp = Math.trunc(helperStack.pop()! / temp); + helperStack.push(temp); + break; + default: + helperStack.push(Number(t)); + break; + } + i++; + } + return helperStack.pop()!; +}; +``` + +优化版: + +```typescript +function evalRPN(tokens: string[]): number { + const helperStack: number[] = []; + const operatorMap: Map number> = new Map([ + ['+', (a, b) => a + b], + ['-', (a, b) => a - b], + ['/', (a, b) => Math.trunc(a / b)], + ['*', (a, b) => a * b], + ]); + let a: number, b: number; + for (let t of tokens) { + if (operatorMap.has(t)) { + b = helperStack.pop()!; + a = helperStack.pop()!; + helperStack.push(operatorMap.get(t)!(a, b)); + } else { + helperStack.push(Number(t)); + } + } + return helperStack.pop()!; +}; +``` + +### Swift: + +```Swift +func evalRPN(_ tokens: [String]) -> Int { + var stack = [Int]() + for c in tokens { + let v = Int(c) + if let num = v { + // 遇到数字直接入栈 + stack.append(num) + } else { + // 遇到运算符, 取出栈顶两元素计算, 结果压栈 + var res: Int = 0 + let num2 = stack.popLast()! + let num1 = stack.popLast()! + switch c { + case "+": + res = num1 + num2 + case "-": + res = num1 - num2 + case "*": + res = num1 * num2 + case "/": + res = num1 / num2 + default: + break + } + stack.append(res) + } + } + return stack.last! +} +``` + +### C#: + +```csharp +public int EvalRPN(string[] tokens) { + int num; + Stack stack = new Stack(); + foreach(string s in tokens){ + if(int.TryParse(s, out num)){ + stack.Push(num); + }else{ + int num1 = stack.Pop(); + int num2 = stack.Pop(); + switch (s) + { + case "+": + stack.Push(num1 + num2); + break; + case "-": + stack.Push(num2 - num1); + break; + case "*": + stack.Push(num1 * num2); + break; + case "/": + stack.Push(num2 / num1); + break; + default: + break; + } + } + } + return stack.Pop(); + } +``` + +### PHP: + +```php +class Solution { + function evalRPN($tokens) { + $st = new SplStack(); + for($i = 0;$ipush($tokens[$i]); + }else{ + // 是符号进行运算 + $num1 = $st->pop(); + $num2 = $st->pop(); + if ($tokens[$i] == "+") $st->push($num2 + $num1); + if ($tokens[$i] == "-") $st->push($num2 - $num1); + if ($tokens[$i] == "*") $st->push($num2 * $num1); + // 注意处理小数部分 + if ($tokens[$i] == "/") $st->push(intval($num2 / $num1)); + } + } + return $st->pop(); + } +} +``` + +### Scala: + +```scala +object Solution { + import scala.collection.mutable + def evalRPN(tokens: Array[String]): Int = { + val stack = mutable.Stack[Int]() // 定义栈 + // 抽取运算操作,需要传递x,y,和一个函数 + def operator(x: Int, y: Int, f: (Int, Int) => Int): Int = f(x, y) + for (token <- tokens) { + // 模式匹配,匹配不同的操作符做什么样的运算 + token match { + // 最后一个参数 _+_,代表x+y,遵循Scala的函数至简原则,以下运算同理 + case "+" => stack.push(operator(stack.pop(), stack.pop(), _ + _)) + case "-" => stack.push(operator(stack.pop(), stack.pop(), -_ + _)) + case "*" => stack.push(operator(stack.pop(), stack.pop(), _ * _)) + case "/" => { + var pop1 = stack.pop() + var pop2 = stack.pop() + stack.push(operator(pop2, pop1, _ / _)) + } + case _ => stack.push(token.toInt) // 不是运算符就入栈 + } + } + // 最后返回栈顶,不需要加return关键字 + stack.pop() + } + +} +``` + +### Rust: + +```rust +impl Solution { + pub fn eval_rpn(tokens: Vec) -> i32 { + let mut stack = vec![]; + for token in tokens.into_iter() { + match token.as_str() { + "+" => { + let a = stack.pop().unwrap(); + *stack.last_mut().unwrap() += a; + } + "-" => { + let a = stack.pop().unwrap(); + *stack.last_mut().unwrap() -= a; + } + "*" => { + let a = stack.pop().unwrap(); + *stack.last_mut().unwrap() *= a; + } + "/" => { + let a = stack.pop().unwrap(); + *stack.last_mut().unwrap() /= a; + } + _ => { + stack.push(token.parse::().unwrap()); + } + } + } + stack.pop().unwrap() + } +} +``` + +### C: + +```c +int str_to_int(char *str) { + // string转integer + int num = 0, tens = 1; + for (int i = strlen(str) - 1; i >= 0; i--) { + if (str[i] == '-') { + num *= -1; + break; + } + num += (str[i] - '0') * tens; + tens *= 10; + } + return num; +} + +int evalRPN(char** tokens, int tokensSize) { + + int *stack = (int *)malloc(tokensSize * sizeof(int)); + assert(stack); + int stackTop = 0; + + for (int i = 0; i < tokensSize; i++) { + char symbol = (tokens[i])[0]; + if (symbol < '0' && (tokens[i])[1] == '\0') { + + // pop两个数字 + int num1 = stack[--stackTop]; + int num2 = stack[--stackTop]; + + // 计算结果 + int result; + if (symbol == '+') { + result = num1 + num2; + } else if (symbol == '-') { + result = num2 - num1; + } else if (symbol == '/') { + result = num2 / num1; + } else { + result = num1 * num2; + } + + // push回stack + stack[stackTop++] = result; + + } else { + + // push数字进stack + int num = str_to_int(tokens[i]); + stack[stackTop++] = num; + + } + } + + int result = stack[0]; + free(stack); + return result; +} +``` + diff --git "a/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" "b/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" new file mode 100755 index 0000000000..b5246a7dbb --- /dev/null +++ "b/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" @@ -0,0 +1,1087 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 综合考察字符串操作的好题。 + +# 151.翻转字符串里的单词 + +[力扣题目链接](https://leetcode.cn/problems/reverse-words-in-a-string/) + +给定一个字符串,逐个翻转字符串中的每个单词。 + +示例 1: +输入: "the sky is blue" +输出: "blue is sky the" + +示例 2: +输入: "  hello world!  " +输出: "world! hello" +解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 + +示例 3: +输入: "a good   example" +输出: "example good a" +解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[字符串复杂操作拿捏了! | LeetCode:151.翻转字符串里的单词](https://www.bilibili.com/video/BV1uT41177fX),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +**这道题目可以说是综合考察了字符串的多种操作。** + +一些同学会使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。 + +所以这里我还是提高一下本题的难度:**不要使用辅助空间,空间复杂度要求为O(1)。** + +不能使用辅助空间之后,那么只能在原字符串上下功夫了。 + +想一下,我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒序了,那么再把单词反转一下,单词不就正过来了。 + +所以解题思路如下: + +* 移除多余空格 +* 将整个字符串反转 +* 将每个单词反转 + +举个例子,源字符串为:"the sky is blue " + +* 移除多余空格 : "the sky is blue" +* 字符串反转:"eulb si yks eht" +* 单词反转:"blue is sky the" + +这样我们就完成了翻转字符串里的单词。 + + +思路很明确了,我们说一说代码的实现细节,就拿移除多余空格来说,一些同学会上来写如下代码: + +```CPP +void removeExtraSpaces(string& s) { + for (int i = s.size() - 1; i > 0; i--) { + if (s[i] == s[i - 1] && s[i] == ' ') { + s.erase(s.begin() + i); + } + } + // 删除字符串最后面的空格 + if (s.size() > 0 && s[s.size() - 1] == ' ') { + s.erase(s.begin() + s.size() - 1); + } + // 删除字符串最前面的空格 + if (s.size() > 0 && s[0] == ' ') { + s.erase(s.begin()); + } +} +``` + +逻辑很简单,从前向后遍历,遇到空格了就erase。 + +如果不仔细琢磨一下erase的时间复杂度,还以为以上的代码是O(n)的时间复杂度呢。 + +想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作。 + +erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。 + +那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。 + +```CPP +//版本一 +void removeExtraSpaces(string& s) { + int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 + // 去掉字符串前面的空格 + while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') { + fastIndex++; + } + for (; fastIndex < s.size(); fastIndex++) { + // 去掉字符串中间部分的冗余空格 + if (fastIndex - 1 > 0 + && s[fastIndex - 1] == s[fastIndex] + && s[fastIndex] == ' ') { + continue; + } else { + s[slowIndex++] = s[fastIndex]; + } + } + if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格 + s.resize(slowIndex - 1); + } else { + s.resize(slowIndex); // 重新设置字符串大小 + } +} +``` + + +有的同学可能发现用erase来移除空格,在leetcode上性能也还行。主要是以下几点;: + +1. leetcode上的测试集里,字符串的长度不够长,如果足够长,性能差距会非常明显。 +2. leetcode的测程序耗时不是很准确的。 + +版本一的代码是一般的思考过程,就是 先移除字符串前的空格,再移除中间的,再移除后面部分。 + +不过其实还可以优化,这部分和[27.移除元素](https://programmercarl.com/0027.移除元素.html)的逻辑是一样一样的,本题是移除空格,而 27.移除元素 就是移除元素。 + +所以代码可以写的很精简,大家可以看 如下 代码 removeExtraSpaces 函数的实现: + +```CPP +// 版本二 +void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。 + int slow = 0; //整体思想参考https://programmercarl.com/0027.移除元素.html + for (int i = 0; i < s.size(); ++i) { // + if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。 + if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。 + while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。 + s[slow++] = s[i++]; + } + } + } + s.resize(slow); //slow的大小即为去除多余空格后的大小。 +} +``` + +如果以上代码看不懂,建议先把 [27.移除元素](https://programmercarl.com/0027.移除元素.html)这道题目做了,或者看视频讲解:[数组中移除元素并不容易!LeetCode:27. 移除元素](https://www.bilibili.com/video/BV12A4y1Z7LP) 。 + +此时我们已经实现了removeExtraSpaces函数来移除冗余空格。 + +还要实现反转字符串的功能,支持反转字符串子区间,这个实现我们分别在[344.反转字符串](https://programmercarl.com/0344.反转字符串.html)和[541.反转字符串II](https://programmercarl.com/0541.反转字符串II.html)里已经讲过了。 + +代码如下: + +```CPP +// 反转字符串s中左闭右闭的区间[start, end] +void reverse(string& s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + swap(s[i], s[j]); + } +} +``` + +整体代码如下: + +```CPP +class Solution { +public: + void reverse(string& s, int start, int end){ //翻转,区间写法:左闭右闭 [] + for (int i = start, j = end; i < j; i++, j--) { + swap(s[i], s[j]); + } + } + + void removeExtraSpaces(string& s) {//去除所有空格并在相邻单词之间添加空格, 快慢指针。 + int slow = 0; //整体思想参考https://programmercarl.com/0027.移除元素.html + for (int i = 0; i < s.size(); ++i) { // + if (s[i] != ' ') { //遇到非空格就处理,即删除所有空格。 + if (slow != 0) s[slow++] = ' '; //手动控制空格,给单词之间添加空格。slow != 0说明不是第一个单词,需要在单词前添加空格。 + while (i < s.size() && s[i] != ' ') { //补上该单词,遇到空格说明单词结束。 + s[slow++] = s[i++]; + } + } + } + s.resize(slow); //slow的大小即为去除多余空格后的大小。 + } + + string reverseWords(string s) { + removeExtraSpaces(s); //去除多余空格,保证单词之间之只有一个空格,且字符串首尾没空格。 + reverse(s, 0, s.size() - 1); + int start = 0; //removeExtraSpaces后保证第一个单词的开始下标一定是0。 + for (int i = 0; i <= s.size(); ++i) { + if (i == s.size() || s[i] == ' ') { //到达空格或者串尾,说明一个单词结束。进行翻转。 + reverse(s, start, i - 1); //翻转,注意是左闭右闭 []的翻转。 + start = i + 1; //更新下一个单词的开始下标start + } + } + return s; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) 或 O(n),取决于语言中字符串是否可变 + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + /** + * 不使用Java内置方法实现 + *

+ * 1.去除首尾以及中间多余空格 + * 2.反转整个字符串 + * 3.反转各个单词 + */ + public String reverseWords(String s) { + // System.out.println("ReverseWords.reverseWords2() called with: s = [" + s + "]"); + // 1.去除首尾以及中间多余空格 + StringBuilder sb = removeSpace(s); + // 2.反转整个字符串 + reverseString(sb, 0, sb.length() - 1); + // 3.反转各个单词 + reverseEachWord(sb); + return sb.toString(); + } + + private StringBuilder removeSpace(String s) { + // System.out.println("ReverseWords.removeSpace() called with: s = [" + s + "]"); + int start = 0; + int end = s.length() - 1; + while (s.charAt(start) == ' ') start++; + while (s.charAt(end) == ' ') end--; + StringBuilder sb = new StringBuilder(); + while (start <= end) { + char c = s.charAt(start); + if (c != ' ' || sb.charAt(sb.length() - 1) != ' ') { + sb.append(c); + } + start++; + } + // System.out.println("ReverseWords.removeSpace returned: sb = [" + sb + "]"); + return sb; + } + + /** + * 反转字符串指定区间[start, end]的字符 + */ + public void reverseString(StringBuilder sb, int start, int end) { + // System.out.println("ReverseWords.reverseString() called with: sb = [" + sb + "], start = [" + start + "], end = [" + end + "]"); + while (start < end) { + char temp = sb.charAt(start); + sb.setCharAt(start, sb.charAt(end)); + sb.setCharAt(end, temp); + start++; + end--; + } + // System.out.println("ReverseWords.reverseString returned: sb = [" + sb + "]"); + } + + private void reverseEachWord(StringBuilder sb) { + int start = 0; + int end = 1; + int n = sb.length(); + while (start < n) { + while (end < n && sb.charAt(end) != ' ') { + end++; + } + reverseString(sb, start, end - 1); + start = end + 1; + end = start + 1; + } + } +} +``` + +```java +//解法二:创建新字符数组填充。时间复杂度O(n) +class Solution { + public String reverseWords(String s) { + //源字符数组 + char[] initialArr = s.toCharArray(); + //新字符数组 + char[] newArr = new char[initialArr.length+1];//下面循环添加"单词 ",最终末尾的空格不会返回 + int newArrPos = 0; + //i来进行整体对源字符数组从后往前遍历 + int i = initialArr.length-1; + while(i>=0){ + while(i>=0 && initialArr[i] == ' '){i--;} //跳过空格 + //此时i位置是边界或!=空格,先记录当前索引,之后的while用来确定单词的首字母的位置 + int right = i; + while(i>=0 && initialArr[i] != ' '){i--;} + //指定区间单词取出(由于i为首字母的前一位,所以这里+1,),取出的每组末尾都带有一个空格 + for (int j = i+1; j <= right; j++) { + newArr[newArrPos++] = initialArr[j]; + if(j == right){ + newArr[newArrPos++] = ' ';//空格 + } + } + } + //若是原始字符串没有单词,直接返回空字符串;若是有单词,返回0-末尾空格索引前范围的字符数组(转成String返回) + if(newArrPos == 0){ + return ""; + }else{ + return new String(newArr,0,newArrPos-1); + } + } +} +``` + +```java +//解法三:双反转+移位,String 的 toCharArray() 方法底层会 new 一个和原字符串相同大小的 char 数组,空间复杂度:O(n) +class Solution { + /** + * 思路: + * ①反转字符串 "the sky is blue " => " eulb si yks eht" + * ②遍历 " eulb si yks eht",每次先对某个单词进行反转再移位 + * 这里以第一个单词进行为演示:" eulb si yks eht" ==反转=> " blue si yks eht" ==移位=> "blue si yks eht" + */ + public String reverseWords(String s) { + //步骤1:字符串整体反转(此时其中的单词也都反转了) + char[] initialArr = s.toCharArray(); + reverse(initialArr, 0, s.length() - 1); + int k = 0; + for (int i = 0; i < initialArr.length; i++) { + if (initialArr[i] == ' ') { + continue; + } + int tempCur = i; + while (i < initialArr.length && initialArr[i] != ' ') { + i++; + } + for (int j = tempCur; j < i; j++) { + if (j == tempCur) { //步骤二:二次反转 + reverse(initialArr, tempCur, i - 1);//对指定范围字符串进行反转,不反转从后往前遍历一个个填充有问题 + } + //步骤三:移动操作 + initialArr[k++] = initialArr[j]; + if (j == i - 1) { //遍历结束 + //避免越界情况,例如=> "asdasd df f",不加判断最后就会数组越界 + if (k < initialArr.length) { + initialArr[k++] = ' '; + } + } + } + } + if (k == 0) { + return ""; + } else { + //参数三:以防出现如"asdasd df f"=>"f df asdasd"正好凑满不需要省略空格情况 + return new String(initialArr, 0, (k == initialArr.length) && (initialArr[k - 1] != ' ') ? k : k - 1); + } + } + + public void reverse(char[] chars, int begin, int end) { + for (int i = begin, j = end; i < j; i++, j--) { + chars[i] ^= chars[j]; + chars[j] ^= chars[i]; + chars[i] ^= chars[j]; + } + } +} +``` + +```java +/* + * 解法四:时间复杂度 O(n) + * 参考卡哥 c++ 代码的三步骤:先移除多余空格,再将整个字符串反转,最后把单词逐个反转 + * 有别于解法一 :没有用 StringBuilder 实现,而是对 String 的 char[] 数组操作来实现以上三个步骤 + */ +class Solution { + //用 char[] 来实现 String 的 removeExtraSpaces,reverse 操作 + public String reverseWords(String s) { + char[] chars = s.toCharArray(); + //1.去除首尾以及中间多余空格 + chars = removeExtraSpaces(chars); + //2.整个字符串反转 + reverse(chars, 0, chars.length - 1); + //3.单词反转 + reverseEachWord(chars); + return new String(chars); + } + + //1.用 快慢指针 去除首尾以及中间多余空格,可参考数组元素移除的题解 + public char[] removeExtraSpaces(char[] chars) { + int slow = 0; + for (int fast = 0; fast < chars.length; fast++) { + //先用 fast 移除所有空格 + if (chars[fast] != ' ') { + //在用 slow 加空格。 除第一个单词外,单词末尾要加空格 + if (slow != 0) + chars[slow++] = ' '; + //fast 遇到空格或遍历到字符串末尾,就证明遍历完一个单词了 + while (fast < chars.length && chars[fast] != ' ') + chars[slow++] = chars[fast++]; + } + } + //相当于 c++ 里的 resize() + char[] newChars = new char[slow]; + System.arraycopy(chars, 0, newChars, 0, slow); + return newChars; + } + + //双指针实现指定范围内字符串反转,可参考字符串反转题解 + public void reverse(char[] chars, int left, int right) { + if (right >= chars.length) { + System.out.println("set a wrong right"); + return; + } + while (left < right) { + chars[left] ^= chars[right]; + chars[right] ^= chars[left]; + chars[left] ^= chars[right]; + left++; + right--; + } + } + + //3.单词反转 + public void reverseEachWord(char[] chars) { + int start = 0; + //end <= s.length() 这里的 = ,是为了让 end 永远指向单词末尾后一个位置,这样 reverse 的实参更好设置 + for (int end = 0; end <= chars.length; end++) { + // end 每次到单词末尾后的空格或串尾,开始反转单词 + if (end == chars.length || chars[end] == ' ') { + reverse(chars, start, end - 1); + start = end + 1; + } + } + } +} +``` + +### Python: +(版本一)先删除空白,然后整个反转,最后单词反转。 +**因为字符串是不可变类型,所以反转单词的时候,需要将其转换成列表,然后通过join函数再将其转换成列表,所以空间复杂度不是O(1)** + +```Python +class Solution: + def reverseWords(self, s: str) -> str: + # 反转整个字符串 + s = s[::-1] + # 将字符串拆分为单词,并反转每个单词 + # split()函数能够自动忽略多余的空白字符 + s = ' '.join(word[::-1] for word in s.split()) + return s + +``` +(版本二)使用双指针 + +```python +class Solution: + def reverseWords(self, s: str) -> str: + # 将字符串拆分为单词,即转换成列表类型 + words = s.split() + + # 反转单词 + left, right = 0, len(words) - 1 + while left < right: + words[left], words[right] = words[right], words[left] + left += 1 + right -= 1 + + # 将列表转换成字符串 + return " ".join(words) +``` +(版本三) 拆分字符串 + 反转列表 +```python +class Solution: + def reverseWords(self, s): + words = s.split() #type(words) --- list + words = words[::-1] # 反转单词 + return ' '.join(words) #列表转换成字符串 +``` +(版本四) 将字符串转换为列表后,使用双指针去除空格 +```python +class Solution: + def single_reverse(self, s, start: int, end: int): + while start < end: + s[start], s[end] = s[end], s[start] + start += 1 + end -= 1 + + def reverseWords(self, s: str) -> str: + result = "" + fast = 0 + # 1. 首先将原字符串反转并且除掉空格, 并且加入到新的字符串当中 + # 由于Python字符串的不可变性,因此只能转换为列表进行处理 + s = list(s) + s.reverse() + while fast < len(s): + if s[fast] != " ": + if len(result) != 0: + result += " " + while s[fast] != " " and fast < len(s): + result += s[fast] + fast += 1 + else: + fast += 1 + # 2.其次将每个单词进行翻转操作 + slow = 0 + fast = 0 + result = list(result) + while fast <= len(result): + if fast == len(result) or result[fast] == " ": + self.single_reverse(result, slow, fast - 1) + slow = fast + 1 + fast += 1 + else: + fast += 1 + + return "".join(result) +``` + +(版本五) 遇到空格就说明前面的是一个单词,把它加入到一个数组中。 + +```python +class Solution: + def reverseWords(self, s: str) -> str: + words = [] + word = '' + s += ' ' # 帮助处理最后一个字词 + + for char in s: + if char == ' ': # 遇到空格就说明前面的可能是一个单词 + if word != '': # 确认是单词,把它加入到一个数组中 + words.append(word) + word = '' # 清空当前单词 + continue + + word += char # 收集单词的字母 + + words.reverse() + return ' '.join(words) +``` + +### Go: + +版本一: + +```go +func reverseWords(s string) string { + b := []byte(s) + + // 移除前面、中间、后面存在的多余空格 + slow := 0 + for i := 0; i < len(b); i++ { + if b[i] != ' ' { + if slow != 0 { + b[slow] = ' ' + slow++ + } + for i < len(b) && b[i] != ' ' { // 复制逻辑 + b[slow] = b[i] + slow++ + i++ + } + } + } + b = b[0:slow] + + // 翻转整个字符串 + reverse(b) + // 翻转每个单词 + last := 0 + for i := 0; i <= len(b); i++ { + if i == len(b) || b[i] == ' ' { + reverse(b[last:i]) + last = i + 1 + } + } + return string(b) +} + +func reverse(b []byte) { + left := 0 + right := len(b) - 1 + for left < right { + b[left], b[right] = b[right], b[left] + left++ + right-- + } +} +``` + +版本二: + +```go +import ( + "fmt" +) + +func reverseWords(s string) string { + //1.使用双指针删除冗余的空格 + slowIndex, fastIndex := 0, 0 + b := []byte(s) + //删除头部冗余空格 + for len(b) > 0 && fastIndex < len(b) && b[fastIndex] == ' ' { + fastIndex++ + } + //删除单词间冗余空格 + for ; fastIndex < len(b); fastIndex++ { + if fastIndex-1 > 0 && b[fastIndex-1] == b[fastIndex] && b[fastIndex] == ' ' { + continue + } + b[slowIndex] = b[fastIndex] + slowIndex++ + } + //删除尾部冗余空格 + if slowIndex-1 > 0 && b[slowIndex-1] == ' ' { + b = b[:slowIndex-1] + } else { + b = b[:slowIndex] + } + //2.反转整个字符串 + reverse(b) + //3.反转单个单词 i单词开始位置,j单词结束位置 + i := 0 + for i < len(b) { + j := i + for ; j < len(b) && b[j] != ' '; j++ { + } + reverse(b[i:j]) + i = j + i++ + } + return string(b) +} + +func reverse(b []byte) { + left := 0 + right := len(b) - 1 + for left < right { + b[left], b[right] = b[right], b[left] + left++ + right-- + } +} +``` +```go +//双指针解法。指针逆序遍历,将遍历后得到的单词(间隔为空格,用以区分)顺序放置在额外空间 +//时间复杂度O(n),空间复杂度O(n) +func reverseWords(s string) string { + strBytes := []byte(s) + n := len(strBytes) + // 记录有效字符范围的起始和结束位置 + start, end := 0, n-1 + // 去除开头空格 + for start < n && strBytes[start] == 32 { + start++ + } + // 处理全是空格或空字符串情况 + if start == n { + return "" + } + // 去除结尾空格 + for end >= 0 && strBytes[end] == 32 { + end-- + } + // 结果切片,预分配容量 + res := make([]byte, 0, end-start+1)//这里挺重要的,本人之前没有预分配容量,每次循环都添加单词,导致内存超限(也可能就是我之前的思路有问题) + // 从后往前遍历有效字符范围 + for i := end; i >= start; { + // 找单词起始位置,直接通过循环条件判断定位 + for ; i >= start && strBytes[i] == 32; i-- { + } + j := i + for ; j >= start && strBytes[j]!= 32; j-- { + } + res = append(res, strBytes[j+1:i+1]...) + // 只在不是最后一个单词时添加空格 + if j > start { + res = append(res, 32) + } + i = j + } + return string(res) +} +``` + + +### JavaScript: + +```js +/** + * @param {string} s + * @return {string} + */ + var reverseWords = function(s) { + // 字符串转数组 + const strArr = Array.from(s); + // 移除多余空格 + removeExtraSpaces(strArr); + // 翻转 + reverse(strArr, 0, strArr.length - 1); + + let start = 0; + + for(let i = 0; i <= strArr.length; i++) { + if (strArr[i] === ' ' || i === strArr.length) { + // 翻转单词 + reverse(strArr, start, i - 1); + start = i + 1; + } + } + + return strArr.join(''); +}; + +// 删除多余空格 +function removeExtraSpaces(strArr) { + let slowIndex = 0; + let fastIndex = 0; + + while(fastIndex < strArr.length) { + // 移除开始位置和重复的空格 + if (strArr[fastIndex] === ' ' && (fastIndex === 0 || strArr[fastIndex - 1] === ' ')) { + fastIndex++; + } else { + strArr[slowIndex++] = strArr[fastIndex++]; + } + } + + // 移除末尾空格 + strArr.length = strArr[slowIndex - 1] === ' ' ? slowIndex - 1 : slowIndex; +} + +// 翻转从 start 到 end 的字符 +function reverse(strArr, start, end) { + let left = start; + let right = end; + + while(left < right) { + // 交换 + [strArr[left], strArr[right]] = [strArr[right], strArr[left]]; + left++; + right--; + } +} +``` + +### TypeScript: + +```typescript +function reverseWords(s: string): string { + /** Utils **/ + // 删除多余空格, 如' hello world ' => 'hello world' + function delExtraSpace(arr: string[]): void { + let left: number = 0, + right: number = 0, + length: number = arr.length; + while (right < length && arr[right] === ' ') { + right++; + } + while (right < length) { + if (arr[right] === ' ' && arr[right - 1] === ' ') { + right++; + continue; + } + arr[left++] = arr[right++]; + } + if (arr[left - 1] === ' ') { + arr.length = left - 1; + } else { + arr.length = left; + } + } + // 翻转字符串,如:'hello' => 'olleh' + function reverseWords(strArr: string[], start: number, end: number) { + let temp: string; + while (start < end) { + temp = strArr[start]; + strArr[start] = strArr[end]; + strArr[end] = temp; + start++; + end--; + } + } + + /** Main code **/ + let strArr: string[] = s.split(''); + delExtraSpace(strArr); + let length: number = strArr.length; + // 翻转整个字符串 + reverseWords(strArr, 0, length - 1); + let start: number = 0, + end: number = 0; + while (start < length) { + end = start; + while (strArr[end] !== ' ' && end < length) { + end++; + } + // 翻转单个单词 + reverseWords(strArr, start, end - 1); + start = end + 1; + } + return strArr.join(''); +}; +``` + +### Swift: + +```swift +func reverseWords(_ s: String) -> String { + var stringArr = removeSpace(s) + reverseString(&stringArr, startIndex: 0, endIndex: stringArr.count - 1) + reverseWord(&stringArr) + return String(stringArr) +} + +/// 1、移除多余的空格(前后所有的空格,中间只留一个空格) +func removeSpace(_ s: String) -> [Character] { + let ch = Array(s) + var left = 0 + var right = ch.count - 1 + // 忽略字符串前面的所有空格 + while ch[left] == " " { + left += 1 + } + // 忽略字符串后面的所有空格 + while ch[right] == " " { + right -= 1 + } + + // 接下来就是要处理中间的多余空格 + var lastArr = Array() + while left <= right { + // 准备加到新字符串当中的字符 + let char = ch[left] + // 新的字符串的最后一个字符;或者原字符串中,准备加到新字符串的那个字符;这两个字符当中,只要有一个不是空格,就可以加到新的字符串当中 + if char != " " || lastArr[lastArr.count - 1] != " " { + lastArr.append(char) + } + + left += 1 + } + return lastArr +} + +/// 2、反转整个字符串 +func reverseString(_ s: inout [Character], startIndex: Int, endIndex: Int) { + var start = startIndex + var end = endIndex + while start < end { + (s[start], s[end]) = (s[end], s[start]) + start += 1 + end -= 1 + } +} + +/// 3、再次将字符串里面的单词反转 +func reverseWord(_ s: inout [Character]) { + var start = 0 + var end = 0 + var entry = false + + for i in 0..= 0 && s(end) == ' ') end -= 1 + var sb = "" // String + // 当start小于等于end的时候,执行添加操作 + while (start <= end) { + var c = s(start) + // 当前字符不等于空,sb的最后一个字符不等于空的时候添加到sb + if (c != ' ' || sb(sb.length - 1) != ' ') { + sb ++= c.toString + } + start += 1 // 指针向右移动 + } + sb.toArray + } + + // 翻转字符串 + def reverseString(s: Array[Char], start: Int, end: Int): Unit = { + var (left, right) = (start, end) + while (left < right) { + var tmp = s(left) + s(left) = s(right) + s(right) = tmp + left += 1 + right -= 1 + } + } + + // 翻转每个单词 + def reverseEachWord(s: Array[Char]): Unit = { + var i = 0 + while (i < s.length) { + var j = i + 1 + // 向后迭代寻找每个单词的坐标 + while (j < s.length && s(j) != ' ') j += 1 + reverseString(s, i, j - 1) // 翻转每个单词 + i = j + 1 // i往后更新 + } + } +} +``` + +### PHP: + +```php +function reverseWords($s) { + $this->removeExtraSpaces($s); + $this->reverseString($s, 0, strlen($s)-1); + // 将每个单词反转 + $start = 0; + for ($i = 0; $i <= strlen($s); $i++) { + // 到达空格或者串尾,说明一个单词结束。进行翻转。 + if ($i == strlen($s) || $s[$i] == ' ') { + // 翻转,注意是左闭右闭 []的翻转。 + $this->reverseString($s, $start, $i-1); + // +1: 单词与单词直接有个空格 + $start = $i + 1; + } + } + return $s; +} + +// 移除多余空格 +function removeExtraSpaces(&$s){ + $slow = 0; + for ($i = 0; $i < strlen($s); $i++) { + if ($s[$i] != ' ') { + if ($slow != 0){ + $s[$slow++] = ' '; + } + while ($i < strlen($s) && $s[$i] != ' ') { + $s[$slow++] = $s[$i++]; + } + } + } + // 移动覆盖处理,丢弃多余的脏数据。 + $s = substr($s,0,$slow); + return ; +} + +// 翻转字符串 +function reverseString(&$s, $start, $end) { + for ($i = $start, $j = $end; $i < $j; $i++, $j--) { + $tmp = $s[$i]; + $s[$i] = $s[$j]; + $s[$j] = $tmp; + } + return ; +} +``` +### Rust: + +```Rust +// 根据C++版本二思路进行实现 +// 函数名根据Rust编译器建议由驼峰命名法改为蛇形命名法 +impl Solution { + pub fn reverse(s: &mut Vec, mut begin: usize, mut end: usize){ + while begin < end { + let temp = s[begin]; + s[begin] = s[end]; + s[end] = temp; + begin += 1; + end -= 1; + } +} +pub fn remove_extra_spaces(s: &mut Vec) { + let mut slow: usize = 0; + let len = s.len(); + // 注意这里不能用for循环,不然在里面那个while循环中对i的递增会失效 + let mut i: usize = 0; + while i < len { + if !s[i].is_ascii_whitespace() { + if slow != 0 { + s[slow] = ' '; + slow += 1; + } + while i < len && !s[i].is_ascii_whitespace() { + s[slow] = s[i]; + slow += 1; + i += 1; + } + } + i += 1; + } + s.resize(slow, ' '); + } + pub fn reverse_words(s: String) -> String { + let mut s = s.chars().collect::>(); + Self::remove_extra_spaces(&mut s); + let len = s.len(); + Self::reverse(&mut s, 0, len - 1); + let mut start = 0; + for i in 0..=len { + if i == len || s[i].is_ascii_whitespace() { + Self::reverse(&mut s, start, i - 1); + start = i + 1; + } + } + s.iter().collect::() + } +} +``` +### C: + +```C +// 翻转字符串中指定范围的字符 +void reverse(char* s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + int tmp = s[i]; + s[i] = s[j]; + s[j] = tmp; + } +} + +// 删除字符串两端和中间多余的空格 +void removeExtraSpace(char* s) { + int start = 0; // 指向字符串开头的指针 + int end = strlen(s) - 1; // 指向字符串结尾的指针 + while (s[start] == ' ') start++; // 移动指针 start,直到找到第一个非空格字符 + while (s[end] == ' ') end--; // 移动指针 end,直到找到第一个非空格字符 + int slow = 0; // 指向新字符串的下一个写入位置的指针 + for (int i = start; i <= end; i++) { // 遍历整个字符串 + if (s[i] == ' ' && s[i+1] == ' ') { // 如果当前字符是空格,并且下一个字符也是空格,则跳过 + continue; + } + s[slow] = s[i]; // 否则,将当前字符复制到新字符串的 slow 位置 + slow++; // 将 slow 指针向后移动 + } + s[slow] = '\0'; // 在新字符串的末尾添加一个空字符 +} + +// 翻转字符串中的单词 +char * reverseWords(char * s){ + removeExtraSpace(s); // 先删除字符串两端和中间的多余空格 + reverse(s, 0, strlen(s) - 1); // 翻转整个字符串 + int slow = 0; // 指向每个单词的开头位置的指针 + for (int i = 0; i <= strlen(s); i++) { // 遍历整个字符串 + if (s[i] ==' ' || s[i] == '\0') { // 如果当前字符是空格或空字符,说明一个单词结束了 + reverse(s, slow, i-1); // 翻转单词 + slow = i + 1; // 将 slow 指针指向下一个单词的开头位置 + } + } + return s; // 返回处理后的字符串 +} +``` + +### C# +```csharp LINQ高级方法 +public string ReverseWords(string s) { + return string.Join(' ', s.Trim().Split(' ',StringSplitOptions.RemoveEmptyEntries).Reverse()); +} +``` + + diff --git "a/problems/0160.\347\233\270\344\272\244\351\223\276\350\241\250.md" "b/problems/0160.\347\233\270\344\272\244\351\223\276\350\241\250.md" new file mode 100755 index 0000000000..cdc58912fe --- /dev/null +++ "b/problems/0160.\347\233\270\344\272\244\351\223\276\350\241\250.md" @@ -0,0 +1,5 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +同:[链表:链表相交](https://programmercarl.com/面试题02.07.链表相交.html) diff --git "a/problems/0188.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272IV.md" "b/problems/0188.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272IV.md" new file mode 100755 index 0000000000..350533d8c8 --- /dev/null +++ "b/problems/0188.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272IV.md" @@ -0,0 +1,642 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 188.买卖股票的最佳时机IV + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-iv/) + +给定一个整数数组 prices ,它的第 i 个元素 prices[i] 是一支给定的股票在第 i 天的价格。 + +设计一个算法来计算你所能获取的最大利润。你最多可以完成 k 笔交易。 + +注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 + +* 示例 1: +* 输入:k = 2, prices = [2,4,1] +* 输出:2 +解释:在第 1 天 (股票价格 = 2) 的时候买入,在第 2 天 (股票价格 = 4) 的时候卖出,这笔交易所能获得利润 = 4-2 = 2。 + +* 示例 2: +* 输入:k = 2, prices = [3,2,6,5,0,3] +* 输出:7 +解释:在第 2 天 (股票价格 = 2) 的时候买入,在第 3 天 (股票价格 = 6) 的时候卖出, 这笔交易所能获得利润 = 6-2 = 4。随后,在第 5 天 (股票价格 = 0) 的时候买入,在第 6 天 (股票价格 = 3) 的时候卖出, 这笔交易所能获得利润 = 3-0 = 3 。 + + +提示: + +* 0 <= k <= 100 +* 0 <= prices.length <= 1000 +* 0 <= prices[i] <= 1000 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划来决定最佳时机,至多可以买卖K次!| LeetCode:188.买卖股票最佳时机4](https://www.bilibili.com/video/BV16M411U7XJ),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目可以说是[动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html)的进阶版,这里要求至多有k次交易。 + +动规五部曲,分析如下: + +1. 确定dp数组以及下标的含义 + +在[动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html)中,我是定义了一个二维dp数组,本题其实依然可以用一个二维dp数组。 + +使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j] + +j的状态表示为: + +* 0 表示不操作 +* 1 第一次买入 +* 2 第一次卖出 +* 3 第二次买入 +* 4 第二次卖出 +* ..... + +**大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入**。 + +题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。 + +所以二维dp数组的C++定义为: + +```CPP +vector> dp(prices.size(), vector(2 * k + 1, 0)); +``` + +2. 确定递推公式 + +还要强调一下:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。 + +达到dp[i][1]状态,有两个具体操作: + +* 操作一:第i天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i] +* 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1] + +选最大的,所以 dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]); + +同理dp[i][2]也有两个操作: + +* 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i] +* 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2] + +所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]) + +同理可以类比剩下的状态,代码如下: + +```CPP +for (int j = 0; j < 2 * k - 1; j += 2) { + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); +} +``` + +**本题和[动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html)最大的区别就是这里要类比j为奇数是买,偶数是卖的状态**。 + +3. dp数组如何初始化 + +第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0; + +第0天做第一次买入的操作,dp[0][1] = -prices[0]; + +第0天做第一次卖出的操作,这个初始值应该是多少呢? + +此时还没有买入,怎么就卖出呢? 其实大家可以理解当天买入,当天卖出,所以dp[0][2] = 0; + +第0天第二次买入操作,初始值应该是多少呢?应该不少同学疑惑,第一次还没买入呢,怎么初始化第二次买入呢? + +第二次买入依赖于第一次卖出的状态,其实相当于第0天第一次买入了,第一次卖出了,然后在买入一次(第二次买入),那么现在手头上没有现金,只要买入,现金就做相应的减少。 + +所以第二次买入操作,初始化为:dp[0][3] = -prices[0]; + +第二次卖出初始化dp[0][4] = 0; + +**所以同理可以推出dp[0][j]当j为奇数的时候都初始化为 -prices[0]** + +代码如下: + +```CPP +for (int j = 1; j < 2 * k; j += 2) { + dp[0][j] = -prices[0]; +} +``` + +**在初始化的地方同样要类比j为偶数是卖、奇数是买的状态**。 + +4. 确定遍历顺序 + +从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。 + +5. 举例推导dp数组 + +以输入[1,2,3,4,5],k=2为例。 + +![188.买卖股票的最佳时机IV](https://file1.kamacoder.com/i/algo/20201229100358221.png) + +最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int maxProfit(int k, vector& prices) { + + if (prices.size() == 0) return 0; + vector> dp(prices.size(), vector(2 * k + 1, 0)); + for (int j = 1; j < 2 * k; j += 2) { + dp[0][j] = -prices[0]; + } + for (int i = 1;i < prices.size(); i++) { + for (int j = 0; j < 2 * k - 1; j += 2) { + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); + } + } + return dp[prices.size() - 1][2 * k]; + } +}; +``` + +* 时间复杂度: O(n * k),其中 n 为 prices 的长度 +* 空间复杂度: O(n * k) + + + +当然有的解法是定义一个三维数组dp[i][j][k],第i天,第j次买卖,k表示买还是卖的状态,从定义上来讲是比较直观。 + +但感觉三维数组操作起来有些麻烦,我是直接用二维数组来模拟三维数组的情况,代码看起来也清爽一些。 + + +## 其他语言版本 + +### Java: + +```java +// 版本一: 三维 dp数组 +class Solution { + public int maxProfit(int k, int[] prices) { + if (prices.length == 0) return 0; + + // [天数][交易次数][是否持有股票] + int len = prices.length; + int[][][] dp = new int[len][k + 1][2]; + + // dp数组初始化 + // 初始化所有的交易次数是为确保 最后结果是最多 k 次买卖的最大利润 + for (int i = 0; i <= k; i++) { + dp[0][i][1] = -prices[0]; + } + + for (int i = 1; i < len; i++) { + for (int j = 1; j <= k; j++) { + // dp方程, 0表示不持有/卖出, 1表示持有/买入 + dp[i][j][0] = Math.max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]); + dp[i][j][1] = Math.max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]); + } + } + return dp[len - 1][k][0]; + } +} + +// 版本二: 二维 dp数组 +class Solution { + public int maxProfit(int k, int[] prices) { + if (prices.length == 0) return 0; + + // [天数][股票状态] + // 股票状态: 奇数表示第 k 次交易持有/买入, 偶数表示第 k 次交易不持有/卖出, 0 表示没有操作 + int len = prices.length; + int[][] dp = new int[len][k*2 + 1]; + + // dp数组的初始化, 与版本一同理 + for (int i = 1; i < k*2; i += 2) { + dp[0][i] = -prices[0]; + } + + for (int i = 1; i < len; i++) { + for (int j = 0; j < k*2 - 1; j += 2) { + dp[i][j + 1] = Math.max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = Math.max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); + } + } + return dp[len - 1][k*2]; + } +} + +//版本三:一维 dp数组 (下面有和卡哥邏輯一致的一維數組JAVA解法) +class Solution { + public int maxProfit(int k, int[] prices) { + if(prices.length == 0){ + return 0; + } + if(k == 0){ + return 0; + } + // 其实就是123题的扩展,123题只用记录2次交易的状态 + // 这里记录k次交易的状态就行了 + // 每次交易都有买入,卖出两个状态,所以要乘 2 + int[] dp = new int[2 * k]; + // 按123题解题格式那样,做一个初始化 + for(int i = 0; i < dp.length / 2; i++){ + dp[i * 2] = -prices[0]; + } + for(int i = 1; i <= prices.length; i++){ + dp[0] = Math.max(dp[0], -prices[i - 1]); + dp[1] = Math.max(dp[1], dp[0] + prices[i - 1]); + // 还是与123题一样,与123题对照来看 + // 就很容易啦 + for(int j = 2; j < dp.length; j += 2){ + dp[j] = Math.max(dp[j], dp[j - 1] - prices[i-1]); + dp[j + 1] = Math.max(dp[j + 1], dp[j] + prices[i - 1]); + } + } + // 返回最后一次交易卖出状态的结果就行了 + return dp[dp.length - 1]; + } +} +``` +```JAVA +class Solution { + public int maxProfit(int k, int[] prices) { + + //edge cases + if(prices.length == 0 || k == 0) + return 0; + + + int dp[] = new int [k * 2 + 1]; + + //和卡哥邏輯一致,奇數天購入股票,故初始化只初始化奇數天。 + for(int i = 1; i < 2 * k + 1; i += 2){ + dp[i] = -prices[0]; + } + + for(int i = 1; i < prices.length; i++){ //i 從 1 開始,因爲第 i = 0 天已經透過初始化完成了。 + for(int j = 1; j < 2 * k + 1; j++){ //j 從 1 開始,因爲第 j = 0 天已經透過初始化完成了。 + //奇數天購買 + if(j % 2 == 1) + dp[j] = Math.max(dp[j], dp[j - 1] - prices[i]); + //偶數天賣出 + else + dp[j] = Math.max(dp[j], dp[j - 1] + prices[i]); + } + //打印DP數組 + //for(int x : dp) + // System.out.print(x +", "); + //System.out.println(); + } + //return 第2 * k次賣出的獲利。 + return dp[2 * k]; + } +} +``` + +### Python: + +> 版本一 +```python +class Solution: + def maxProfit(self, k: int, prices: List[int]) -> int: + if len(prices) == 0: + return 0 + dp = [[0] * (2*k+1) for _ in range(len(prices))] + for j in range(1, 2*k, 2): + dp[0][j] = -prices[0] + for i in range(1, len(prices)): + for j in range(0, 2*k-1, 2): + dp[i][j+1] = max(dp[i-1][j+1], dp[i-1][j] - prices[i]) + dp[i][j+2] = max(dp[i-1][j+2], dp[i-1][j+1] + prices[i]) + return dp[-1][2*k] +``` + +> 版本二 +```python +class Solution: + def maxProfit(self, k: int, prices: List[int]) -> int: + if len(prices) == 0: return 0 + dp = [0] * (2*k + 1) + for i in range(1,2*k,2): + dp[i] = -prices[0] + for i in range(1,len(prices)): + for j in range(1,2*k + 1): + if j % 2: + dp[j] = max(dp[j],dp[j-1]-prices[i]) + else: + dp[j] = max(dp[j],dp[j-1]+prices[i]) + return dp[2*k] +``` + +> 版本三: 一维 dp 数组(易理解版本) +```python +class Solution: + def maxProfit(self, k: int, prices: List[int]) -> int: + dp = [0] * k * 2 + for i in range(k): + dp[i * 2] = -prices[0] + + for price in prices[1:]: + dc = dp.copy() # 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + + for i in range(2 * k): + if i % 2 == 1: + dp[i] = max(dc[i], dc[i - 1] + price) + else: + pre = 0 if i == 0 else dc[i - 1] + dp[i] = max(dc[i], pre - price) + + return dp[-1] +``` + +### Go: + +> 版本一: + +```go +// 买卖股票的最佳时机IV 动态规划 +// 时间复杂度O(kn) 空间复杂度O(kn) +func maxProfit(k int, prices []int) int { + if k == 0 || len(prices) == 0 { + return 0 + } + + dp := make([][]int, len(prices)) + status := make([]int, (2 * k + 1) * len(prices)) + for i := range dp { + dp[i] = status[:2 * k + 1] + status = status[2 * k + 1:] + } + for j := 1; j < 2 * k; j += 2 { + dp[0][j] = -prices[0] + } + + for i := 1; i < len(prices); i++ { + for j := 0; j < 2 * k; j += 2 { + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]) + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]) + } + } + return dp[len(prices) - 1][2 * k] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +> 版本二: 三维 dp数组 +```go +func maxProfit(k int, prices []int) int { + length := len(prices) + if length == 0 { + return 0 + } + // [天数][交易次数][是否持有股票] + // 1表示不持有/卖出, 0表示持有/买入 + dp := make([][][]int, length) + for i := 0; i < length; i++ { + dp[i] = make([][]int, k+1) + for j := 0; j <= k; j++ { + dp[i][j] = make([]int, 2) + } + } + for j := 0; j <= k; j++ { + dp[0][j][0] = -prices[0] + } + for i := 1; i < length; i++ { + for j := 1; j <= k; j++ { + dp[i][j][0] = max188(dp[i-1][j][0], dp[i-1][j-1][1]-prices[i]) + dp[i][j][1] = max188(dp[i-1][j][1], dp[i-1][j][0]+prices[i]) + } + } + return dp[length-1][k][1] +} + +func max188(a, b int) int { + if a > b { + return a + } + return b +} +``` + +版本三:空间优化版本 + +```go +func maxProfit(k int, prices []int) int { + n := len(prices) + // k次交易,2 * k种状态 + // 状态从1开始计算,避免判断 + // 奇数时持有(保持或买入) + // 偶数时不持有(保持或卖出) + dp := make([][]int, 2) + dp[0] = make([]int, k * 2 + 1) + dp[1] = make([]int, k * 2 + 1) + + // 奇数状态时持有,i += 2 + for i := 1; i <= k * 2; i += 2 { + dp[0][i] = -prices[0] + } + + for i := 1; i < len(prices); i++ { + for j := 1; j <= k * 2; j++ { + if j % 2 == 1 { + dp[i % 2][j] = max(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j - 1] - prices[i]) + } else { + dp[i % 2][j] = max(dp[(i - 1) % 2][j], dp[(i - 1) % 2][j - 1] + prices[i]) + } + } + } + + return dp[(n - 1) % 2][k * 2] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +> 版本四:一维 dp 数组(易理解版本) + +```go +func maxProfit(k int, prices []int) int { + dp := make([]int, 2 * k) + for i := range k { + dp[i * 2] = -prices[0] + } + + for j := 1; j < len(prices); j++ { + dc := slices.Clone(dp) // 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + + for i := range k * 2 { + if i % 2 == 1 { + dp[i] = max(dc[i], dc[i - 1] + prices[j]) + } else { + pre := 0; if i >= 1 { pre = dc[i - 1] } + dp[i] = max(dc[i], pre - prices[j]) + } + } + } + + return dp[2 * k - 1] +} +``` + +### JavaScript: + +```javascript +// 方法一:动态规划 +const maxProfit = (k,prices) => { + if (prices == null || prices.length < 2 || k == 0) { + return 0; + } + + let dp = Array.from(Array(prices.length), () => Array(2*k+1).fill(0)); + + for (let j = 1; j < 2 * k; j += 2) { + dp[0][j] = 0 - prices[0]; + } + + for(let i = 1; i < prices.length; i++) { + for (let j = 0; j < 2 * k; j += 2) { + dp[i][j+1] = Math.max(dp[i-1][j+1], dp[i-1][j] - prices[i]); + dp[i][j+2] = Math.max(dp[i-1][j+2], dp[i-1][j+1] + prices[i]); + } + } + + return dp[prices.length - 1][2 * k]; +}; + +// 方法二:动态规划+空间优化 +var maxProfit = function(k, prices) { + let n = prices.length; + let dp = new Array(2*k+1).fill(0); + // dp 买入状态初始化 + for (let i = 1; i <= 2*k; i += 2) { + dp[i] = - prices[0]; + } + + for (let i = 1; i < n; i++) { + for (let j = 1; j < 2*k+1; j++) { + // j 为奇数:买入状态 + if (j % 2) { + dp[j] = Math.max(dp[j], dp[j-1] - prices[i]); + } else { + // j为偶数:卖出状态 + dp[j] = Math.max(dp[j], dp[j-1] + prices[i]); + } + } + } + + return dp[2*k]; +}; +``` + +### TypeScript: + +```typescript +function maxProfit(k: number, prices: number[]): number { + const length: number = prices.length; + if (length === 0) return 0; + const dp: number[][] = new Array(length).fill(0) + .map(_ => new Array(k * 2 + 1).fill(0)); + for (let i = 1; i <= k; i++) { + dp[0][i * 2 - 1] = -prices[0]; + } + for (let i = 1; i < length; i++) { + for (let j = 1; j < 2 * k + 1; j++) { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - 1] + Math.pow(-1, j) * prices[i]); + } + } + return dp[length - 1][2 * k]; +}; +``` + +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int maxProfit(int k, int* prices, int pricesSize) { + if(pricesSize == 0){ + return 0; + } + + int dp[pricesSize][2 * k + 1]; + memset(dp, 0, sizeof(int) * pricesSize * (2 * k + 1)); + for (int j = 1; j < 2 * k; j += 2) { + dp[0][j] = -prices[0]; + } + + for (int i = 1;i < pricesSize; i++) {//枚举股票 + for (int j = 0; j < 2 * k - 1; j += 2) { //更新每一次买入卖出 + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); + } + } + return dp[pricesSize - 1][2 * k]; +} +``` + + + +### Rust: + +```rust +impl Solution { + pub fn max_profit(k: i32, prices: Vec) -> i32 { + let mut dp = vec![vec![0; 2 * k as usize + 1]; prices.len()]; + + for v in dp[0].iter_mut().skip(1).step_by(2) { + *v = -prices[0]; + } + + for (i, &p) in prices.iter().enumerate().skip(1) { + for j in (0..2 * k as usize - 1).step_by(2) { + dp[i][j + 1] = dp[i - 1][j + 1].max(dp[i - 1][j] - p); + dp[i][j + 2] = dp[i - 1][j + 2].max(dp[i - 1][j + 1] + p); + } + } + + dp[prices.len() - 1][2 * k as usize] + } +} +``` + +空间优化: + +```rust +impl Solution { + pub fn max_profit(k: i32, prices: Vec) -> i32 { + let mut dp = vec![0; 2 * k as usize + 1]; + for v in dp.iter_mut().skip(1).step_by(2) { + *v = -prices[0]; + } + + for p in prices { + for i in 1..=2 * k as usize { + if i % 2 == 1 { + // 买入 + dp[i] = dp[i].max(dp[i - 1] - p); + continue; + } + // 卖出 + dp[i] = dp[i].max(dp[i - 1] + p); + } + } + + dp[2 * k as usize] + } +} +``` + + + + diff --git "a/problems/0189.\346\227\213\350\275\254\346\225\260\347\273\204.md" "b/problems/0189.\346\227\213\350\275\254\346\225\260\347\273\204.md" new file mode 100755 index 0000000000..976cbed4d1 --- /dev/null +++ "b/problems/0189.\346\227\213\350\275\254\346\225\260\347\273\204.md" @@ -0,0 +1,212 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 189. 旋转数组 + +[力扣题目链接](https://leetcode.cn/problems/rotate-array/) + +给定一个数组,将数组中的元素向右移动 k 个位置,其中 k 是非负数。 + +进阶: + +尽可能想出更多的解决方案,至少有三种不同的方法可以解决这个问题。 +你可以使用空间复杂度为 O(1) 的 原地 算法解决这个问题吗? + +示例 1: + +* 输入: nums = [1,2,3,4,5,6,7], k = 3 +* 输出: [5,6,7,1,2,3,4] +* 解释: +向右旋转 1 步: [7,1,2,3,4,5,6]。 +向右旋转 2 步: [6,7,1,2,3,4,5]。 +向右旋转 3 步: [5,6,7,1,2,3,4]。 + +示例 2: +* 输入:nums = [-1,-100,3,99], k = 2 +* 输出:[3,99,-1,-100] +* 解释: +向右旋转 1 步: [99,-1,-100,3]。 +向右旋转 2 步: [3,99,-1,-100]。 + + +## 思路 + +这道题目在字符串里其实很常见,我把字符串反转相关的题目列一下: + +* [字符串:力扣541.反转字符串II](https://programmercarl.com/0541.反转字符串II.html) +* [字符串:力扣151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html) +* [字符串:剑指Offer58-II.左旋转字符串](https://programmercarl.com/剑指Offer58-II.左旋转字符串.html) + +本题其实和[字符串:剑指Offer58-II.左旋转字符串](https://programmercarl.com/剑指Offer58-II.左旋转字符串.html)就非常像了,剑指offer上左旋转,本题是右旋转。 + +注意题目要求是**要求使用空间复杂度为 O(1) 的 原地 算法** + +那么我来提供一种旋转的方式哈。 + +在[字符串:剑指Offer58-II.左旋转字符串](https://programmercarl.com/剑指Offer58-II.左旋转字符串.html)中,我们提到,如下步骤就可以坐旋转字符串: + +1. 反转区间为前n的子串 +2. 反转区间为n到末尾的子串 +3. 反转整个字符串 + +本题是右旋转,其实就是反转的顺序改动一下,优先反转整个字符串,步骤如下: + +1. 反转整个字符串 +2. 反转区间为前k的子串 +3. 反转区间为k到末尾的子串 + +**需要注意的是,本题还有一个小陷阱,题目输入中,如果k大于nums.size了应该怎么办?** + +举个例子,比较容易想, + +例如,1,2,3,4,5,6,7 如果右移动15次的话,是 7 1 2 3 4 5 6 。 + +所以其实就是右移 k % nums.size() 次,即:15 % 7 = 1 + +C++代码如下: + +```CPP +class Solution { +public: + void rotate(vector& nums, int k) { + k = k % nums.size(); + reverse(nums.begin(), nums.end()); + reverse(nums.begin(), nums.begin() + k); + reverse(nums.begin() + k, nums.end()); + + } +}; +``` + + +## 其他语言版本 + +### Java + +```java +class Solution { + private void reverse(int[] nums, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + int temp = nums[j]; + nums[j] = nums[i]; + nums[i] = temp; + } + } + public void rotate(int[] nums, int k) { + int n = nums.length; + k %= n; + reverse(nums, 0, n - 1); + reverse(nums, 0, k - 1); + reverse(nums, k, n - 1); + } +} +``` + +### Python + +方法一:局部翻转 + 整体翻转 +```python +class Solution: + def rotate(self, A: List[int], k: int) -> None: + def reverse(i, j): + while i < j: + A[i], A[j] = A[j], A[i] + i += 1 + j -= 1 + n = len(A) + k %= n + reverse(0, n - 1) + reverse(0, k - 1) + reverse(k, n - 1) +``` + +方法二:利用余数 + +```python +class Solution: + def rotate(self, nums: List[int], k: int) -> None: + copy = nums[:] + + for i in range(len(nums)): + nums[(i + k) % len(nums)] = copy[i] + + return nums + + # 备注:这个方法会导致空间复杂度变成 O(n) 因为我们要创建一个 copy 数组。但是不失为一种思路。 +``` + +### Go + +```go +func rotate(nums []int, k int) { + l:=len(nums) + index:=l-k%l + reverse(nums) + reverse(nums[:l-index]) + reverse(nums[l-index:]) +} +func reverse(nums []int){ + l:=len(nums) + for i:=0;i, k: i32) { + let k = k as usize % nums.len(); + nums.reverse(); + nums[..k].reverse(); + nums[k..].reverse(); + } +} +``` + + diff --git "a/problems/0198.\346\211\223\345\256\266\345\212\253\350\210\215.md" "b/problems/0198.\346\211\223\345\256\266\345\212\253\350\210\215.md" new file mode 100755 index 0000000000..7c0aab8ec0 --- /dev/null +++ "b/problems/0198.\346\211\223\345\256\266\345\212\253\350\210\215.md" @@ -0,0 +1,361 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 198.打家劫舍 + +[力扣题目链接](https://leetcode.cn/problems/house-robber/) + +你是一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,影响你偷窃的唯一制约因素就是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。 + +给定一个代表每个房屋存放金额的非负整数数组,计算你 不触动警报装置的情况下 ,一夜之内能够偷窃到的最高金额。 + +* 示例 1: +* 输入:[1,2,3,1] +* 输出:4 + +解释:偷窃 1 号房屋 (金额 = 1) ,然后偷窃 3 号房屋 (金额 = 3)。 +  偷窃到的最高金额 = 1 + 3 = 4 。 + +* 示例 2: +* 输入:[2,7,9,3,1] +* 输出:12 +解释:偷窃 1 号房屋 (金额 = 2), 偷窃 3 号房屋 (金额 = 9),接着偷窃 5 号房屋 (金额 = 1)。 +  偷窃到的最高金额 = 2 + 9 + 1 = 12 。 + + +提示: + +* 0 <= nums.length <= 100 +* 0 <= nums[i] <= 400 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,偷不偷这个房间呢?| LeetCode:198.打家劫舍](https://www.bilibili.com/video/BV1Te411N7SX),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +大家如果刚接触这样的题目,会有点困惑,当前的状态我是偷还是不偷呢? + +仔细一想,当前房屋偷与不偷取决于 前一个房屋和前两个房屋是否被偷了。 + +所以这里就更感觉到,当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。 + +当然以上是大概思路,打家劫舍是dp解决的经典问题,接下来我们来动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]**。 + +2. 确定递推公式 + +决定dp[i]的因素就是第i房间偷还是不偷。 + +如果偷第i房间,那么dp[i] = dp[i - 2] + nums[i] ,即:第i-1房一定是不考虑的,找出 下标i-2(包括i-2)以内的房屋,最多可以偷窃的金额为dp[i-2] 加上第i房间偷到的钱。 + +如果不偷第i房间,那么dp[i] = dp[i - 1],即考 虑i-1房,(**注意这里是考虑,并不是一定要偷i-1房,这是很多同学容易混淆的点**) + +然后dp[i]取最大值,即dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); + +3. dp数组如何初始化 + +从递推公式dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]);可以看出,递推公式的基础就是dp[0] 和 dp[1] + +从dp[i]的定义上来讲,dp[0] 一定是 nums[0],dp[1]就是nums[0]和nums[1]的最大值即:dp[1] = max(nums[0], nums[1]); + +代码如下: + +```CPP +vector dp(nums.size()); +dp[0] = nums[0]; +dp[1] = max(nums[0], nums[1]); +``` + +4. 确定遍历顺序 + +dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历! + +代码如下: +```CPP +for (int i = 2; i < nums.size(); i++) { + dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); +} +``` + +5. 举例推导dp数组 + +以示例二,输入[2,7,9,3,1]为例。 + +![198.打家劫舍](https://file1.kamacoder.com/i/algo/20210221170954115.jpg) + +红框dp[nums.size() - 1]为结果。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int rob(vector& nums) { + if (nums.size() == 0) return 0; + if (nums.size() == 1) return nums[0]; + vector dp(nums.size()); + dp[0] = nums[0]; + dp[1] = max(nums[0], nums[1]); + for (int i = 2; i < nums.size(); i++) { + dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); + } + return dp[nums.size() - 1]; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(n) + +## 总结 + +打家劫舍是DP解决的经典题目,这道题也是打家劫舍入门级题目,后面我们还会变种方式来打劫的。 + +## 其他语言版本 + +### Java: + +```Java +// 动态规划 +class Solution { + public int rob(int[] nums) { + if (nums == null || nums.length == 0) return 0; + if (nums.length == 1) return nums[0]; + + int[] dp = new int[nums.length]; + dp[0] = nums[0]; + dp[1] = Math.max(dp[0], nums[1]); + for (int i = 2; i < nums.length; i++) { + dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); + } + + return dp[nums.length - 1]; + } +} + +// 使用滚动数组思想,优化空间 +// 分析本题可以发现,所求结果仅依赖于前两种状态,此时可以使用滚动数组思想将空间复杂度降低为3个空间 +class Solution { + public int rob(int[] nums) { + + int len = nums.length; + + if (len == 0) return 0; + else if (len == 1) return nums[0]; + else if (len == 2) return Math.max(nums[0],nums[1]); + + + int[] result = new int[3]; //存放选择的结果 + result[0] = nums[0]; + result[1] = Math.max(nums[0],nums[1]); + + + for(int i=2;i int: + if len(nums) == 0: # 如果没有房屋,返回0 + return 0 + if len(nums) == 1: # 如果只有一个房屋,返回其金额 + return nums[0] + + # 创建一个动态规划数组,用于存储最大金额 + dp = [0] * len(nums) + dp[0] = nums[0] # 将dp的第一个元素设置为第一个房屋的金额 + dp[1] = max(nums[0], nums[1]) # 将dp的第二个元素设置为第一二个房屋中的金额较大者 + + # 遍历剩余的房屋 + for i in range(2, len(nums)): + # 对于每个房屋,选择抢劫当前房屋和抢劫前一个房屋的最大金额 + dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]) + + return dp[-1] # 返回最后一个房屋中可抢劫的最大金额 +``` +2维DP +```python +class Solution: + def rob(self, nums: List[int]) -> int: + if not nums: # 如果没有房屋,返回0 + return 0 + + n = len(nums) + dp = [[0, 0] for _ in range(n)] # 创建二维动态规划数组,dp[i][0]表示不抢劫第i个房屋的最大金额,dp[i][1]表示抢劫第i个房屋的最大金额 + + dp[0][1] = nums[0] # 抢劫第一个房屋的最大金额为第一个房屋的金额 + + for i in range(1, n): + dp[i][0] = max(dp[i-1][0], dp[i-1][1]) # 不抢劫第i个房屋,最大金额为前一个房屋抢劫和不抢劫的最大值 + dp[i][1] = dp[i-1][0] + nums[i] # 抢劫第i个房屋,最大金额为前一个房屋不抢劫的最大金额加上当前房屋的金额 + + return max(dp[n-1][0], dp[n-1][1]) # 返回最后一个房屋中可抢劫的最大金额 + +``` +优化版 +```python +class Solution: + def rob(self, nums: List[int]) -> int: + if not nums: # 如果没有房屋,返回0 + return 0 + + prev_max = 0 # 上一个房屋的最大金额 + curr_max = 0 # 当前房屋的最大金额 + + for num in nums: + temp = curr_max # 临时变量保存当前房屋的最大金额 + curr_max = max(prev_max + num, curr_max) # 更新当前房屋的最大金额 + prev_max = temp # 更新上一个房屋的最大金额 + + return curr_max # 返回最后一个房屋中可抢劫的最大金额 + + +``` +### Go: + +```Go +func rob(nums []int) int { + n := len(nums) + dp := make([]int, n+1) // dp[i]表示偷到第i家能够偷得的最大金额 + dp[1] = nums[0] + for i := 2; i <= n; i++ { + dp[i] = max(dp[i-1], dp[i-2] + nums[i-1]) + } + return dp[n] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +### JavaScript: + +```javascript +const rob = nums => { + // 数组长度 + const len = nums.length; + // dp数组初始化 + const dp = [nums[0], Math.max(nums[0], nums[1])]; + // 从下标2开始遍历 + for (let i = 2; i < len; i++) { + dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]); + } + return dp[len - 1]; +}; +``` + +### TypeScript: + +```typescript +function rob(nums: number[]): number { + /** + dp[i]: 前i个房屋能偷到的最大金额 + dp[0]: nums[0]; + dp[1]: max(nums[0], nums[1]); + ... + dp[i]: max(dp[i-1], dp[i-2]+nums[i]); + */ + const length: number = nums.length; + if (length === 1) return nums[0]; + const dp: number[] = []; + dp[0] = nums[0]; + dp[1] = Math.max(nums[0], nums[1]); + for (let i = 2; i < length; i++) { + dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); + } + return dp[length - 1]; +}; +``` + +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int rob(int* nums, int numsSize) { + if(numsSize == 0){ + return 0; + } + if(numsSize == 1){ + return nums[0]; + } + // dp初始化 + int dp[numsSize]; + dp[0] = nums[0]; + dp[1] = max(nums[0], nums[1]); + for(int i = 2; i < numsSize; i++){ + dp[i] = max(dp[i - 1], dp[i - 2] + nums[i]); + } + return dp[numsSize - 1]; +} +``` + + + +### Rust: + +```rust +impl Solution { + pub fn rob(nums: Vec) -> i32 { + if nums.len() == 1 { + return nums[0]; + } + let mut dp = vec![0; nums.len()]; + dp[0] = nums[0]; + dp[1] = nums[0].max(nums[1]); + for i in 2..nums.len() { + dp[i] = (dp[i - 2] + nums[i]).max(dp[i - 1]); + } + dp[nums.len() - 1] + } +} +``` + + + diff --git "a/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\345\271\277\346\220\234\347\211\210.md" "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\345\271\277\346\220\234\347\211\210.md" new file mode 100755 index 0000000000..7ae44b5222 --- /dev/null +++ "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\345\271\277\346\220\234\347\211\210.md" @@ -0,0 +1,410 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 200. 岛屿数量 + +[题目链接](https://leetcode.cn/problems/number-of-islands/) + +给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 + +岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 + +此外,你可以假设该网格的四条边均被水包围。 + +![](https://file1.kamacoder.com/i/algo/20220726093256.png) + +提示: + +* m == grid.length +* n == grid[i].length +* 1 <= m, n <= 300 +* grid[i][j] 的值为 '0' 或 '1' + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题题目是 DFS,BFS,并查集,基础题目。 + +本题思路,是用遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。 + +在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 + +那么如果把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 + +### 广度优先搜索 + +不少同学用广搜做这道题目的时候,超时了。 这里有一个广搜中很重要的细节: + +根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。 + +很多同学可能感觉这有区别吗? + +如果从队列拿出节点,再去标记这个节点走过,就会发生下图所示的结果,会导致很多节点重复加入队列。 + +![图二](https://file1.kamacoder.com/i/algo/20220727100846.png) + +超时写法 (从队列中取出节点再标记) + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + visited[curx][cury] = true; // 从队列中取出在标记走过 + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { + que.push({nextx, nexty}); + } + } + } + +} +``` + +加入队列 就代表走过,立刻标记,正确写法: + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + visited[x][y] = true; // 只要加入队列,立刻标记 + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { + que.push({nextx, nexty}); + visited[nextx][nexty] = true; // 只要加入队列立刻标记 + } + } + } + +} +``` + +以上两个版本其实,其实只有细微区别,就是 `visited[x][y] = true;` 放在的地方,着去取决于我们对 代码中队列的定义,队列中的节点就表示已经走过的节点。 **所以只要加入队列,立即标记该节点走过**。 + +本题完整广搜代码: + +```CPP +class Solution { +private: +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + visited[x][y] = true; // 只要加入队列,立刻标记 + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { + que.push({nextx, nexty}); + visited[nextx][nexty] = true; // 只要加入队列立刻标记 + } + } + } +} +public: + int numIslands(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == '1') { + result++; // 遇到没访问过的陆地,+1 + bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + return result; + } +}; + +``` + +## 其他语言版本 + +### Java + +```java +class Solution { + + boolean[][] visited; + int[][] move = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + public int numIslands(char[][] grid) { + int res = 0; + visited = new boolean[grid.length][grid[0].length]; + for(int i = 0; i < grid.length; i++) { + for(int j = 0; j < grid[0].length; j++) { + if(!visited[i][j] && grid[i][j] == '1') { + bfs(grid, i, j); + res++; + } + } + } + return res; + } + + //将这片岛屿上的所有陆地都访问到 + public void bfs(char[][] grid, int y, int x) { + Deque queue = new ArrayDeque<>(); + queue.offer(new int[]{y, x}); + visited[y][x] = true; + while(!queue.isEmpty()) { + int[] cur = queue.poll(); + int m = cur[0]; + int n = cur[1]; + for(int i = 0; i < 4; i++) { + int nexty = m + move[i][0]; + int nextx = n + move[i][1]; + if(nextx < 0 || nexty == grid.length || nexty < 0 || nextx == grid[0].length) continue; + if(!visited[nexty][nextx] && grid[nexty][nextx] == '1') { + queue.offer(new int[]{nexty, nextx}); + visited[nexty][nextx] = true; //只要加入队列就标记为访问 + } + } + } + } +} +``` + + +### Python + +BFS solution + +```python +class Solution: + def __init__(self): + self.dirs = [[0, 1], [1, 0], [-1, 0], [0, -1]] + + def numIslands(self, grid: List[List[str]]) -> int: + m = len(grid) + n = len(grid[0]) + visited = [[False]*n for _ in range(m)] + res = 0 + for i in range(m): + for j in range(n): + if visited[i][j] == False and grid[i][j] == '1': + res += 1 + self.bfs(grid, i, j, visited) # Call bfs within this condition + return res + + def bfs(self, grid, i, j, visited): + q = deque() + q.append((i,j)) + visited[i][j] = True + while q: + x, y = q.popleft() + for k in range(4): + next_i = x + self.dirs[k][0] + next_j = y + self.dirs[k][1] + + if next_i < 0 or next_i >= len(grid): + continue + if next_j < 0 or next_j >= len(grid[0]): + continue + if visited[next_i][next_j]: + continue + if grid[next_i][next_j] == '0': + continue + q.append((next_i, next_j)) + visited[next_i][next_j] = True + +``` +### JavaScript + +```javascript +var numIslands = function (grid) { + let dir = [[0, 1], [1, 0], [-1, 0], [0, -1]]; // 四个方向 + let bfs = (grid, visited, x, y) => { + let queue = []; + queue.push([x, y]); + visited[x][y] = true; + while (queue.length) { + let top = queue.shift();//取出队列头部元素 + console.log(top) + for (let i = 0; i < 4; i++) { + let nextX = top[0] + dir[i][0] + let nextY = top[1] + dir[i][1] + if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) + continue; + if (!visited[nextX][nextY] && grid[nextX][nextY] === "1") { + queue.push([nextX, nextY]) + visited[nextX][nextY] = true + } + } + } + } + let visited = new Array(grid.length).fill().map(() => Array(grid[0].length).fill(false)) + let res = 0 + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[i].length; j++) { + if (!visited[i][j] && grid[i][j] === "1") { + ++res; + bfs(grid, visited, i, j); + } + } + } + return res +}; +``` + +### TypeScript + +```TypeScript +function numIslands2(grid: string[][]): number { + // 四个方向 + const dir: number[][] = [[0, 1], [1, 0], [-1, 0], [0, -1]]; + const [m, n]: [number, number] = [grid.length, grid[0].length]; + + function dfs(grid: string[][], visited: boolean[][], x: number, y: number) { + const queue: number[][] = [[x, y]]; + while (queue.length !== 0) { + //取出队列头部元素 + const top: number[] = queue.shift()!; + for (let i = 0; i < 4; i++) { + const nextX: number = top[0] + dir[i][0]; + const nextY: number = top[1] + dir[i][1]; + // 越界了,直接跳过 + if (nextX < 0 || nextX >= m || nextY < 0 || nextY >= n) { + continue; + } + if (!visited[nextX][nextY] && grid[nextX][nextY] === '1') { + queue.push([nextX, nextY]); + // 只要加入队列立刻标记 + visited[nextX][nextY] = true; + } + } + } + } + + const visited: boolean[][] = Array.from({ length: m }, _ => new Array(n).fill(false)); + + let result = 0; + for (let i = 0; i < m; i++) { + for (let k = 0; k < n; k++) { + if (!visited[i][k] && grid[i][k] === '1') { + ++result; // 遇到没访问过的陆地,+1 + visited[i][k] = true; + dfs(grid, visited, i, k); // 将与其链接的陆地都标记上 true + } + } + } + return result; +} +``` + +### Go + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} + +func numIslands(grid [][]byte) int { + res := 0 + + visited := make([][]bool, len(grid)) + for i := 0; i < len(grid); i++ { + visited[i] = make([]bool, len(grid[0])) + } + + for i, rows := range grid { + for j, v := range rows { + if v == '1' && !visited[i][j] { + res++ + bfs(grid, visited, i, j) + } + } + } + return res +} + +func bfs(grid [][]byte, visited [][]bool, i, j int) { + queue := [][2]int{{i, j}} + visited[i][j] = true // 标记已访问,循环中标记会导致重复 + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + for _, d := range DIRECTIONS { + x, y := cur[0]+d[0], cur[1]+d[1] + if x < 0 || x >= len(grid) || y < 0 || y >= len(grid[0]) { + continue + } + if grid[x][y] == '1' && !visited[x][y] { + visited[x][y] = true + queue = append(queue, [2]int{x, y}) + } + } + } +} +``` + +### Rust + +```rust +use std::collections::VecDeque; +impl Solution { + const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + pub fn num_islands(grid: Vec>) -> i32 { + let mut visited = vec![vec![false; grid[0].len()]; grid.len()]; + let mut res = 0; + for (i, chars) in grid.iter().enumerate() { + for (j, &c) in chars.iter().enumerate() { + if !visited[i][j] && c == '1' { + res += 1; + Self::bfs(&grid, &mut visited, (i as i32, j as i32)); + } + } + } + res + } + + pub fn bfs(grid: &Vec>, visited: &mut Vec>, (x, y): (i32, i32)) { + let mut queue = VecDeque::new(); + queue.push_back((x, y)); + visited[x as usize][y as usize] = true; + while let Some((cur_x, cur_y)) = queue.pop_front() { + for (dx, dy) in Self::DIRECTIONS { + let (nx, ny) = (cur_x + dx, cur_y + dy); + if nx < 0 || nx >= grid.len() as i32 || ny < 0 || ny >= grid[0].len() as i32 { + continue; + } + let (nx, ny) = (nx as usize, ny as usize); + if grid[nx][ny] == '1' && !visited[nx][ny] { + visited[nx][ny] = true; + queue.push_back((nx as i32, ny as i32)); + } + } + } + } +} +``` +``` + diff --git "a/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\346\267\261\346\220\234\347\211\210.md" "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\346\267\261\346\220\234\347\211\210.md" new file mode 100755 index 0000000000..128007bb62 --- /dev/null +++ "b/problems/0200.\345\262\233\345\261\277\346\225\260\351\207\217.\346\267\261\346\220\234\347\211\210.md" @@ -0,0 +1,463 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 200. 岛屿数量 + +[题目链接](https://leetcode.cn/problems/number-of-islands/) + +给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。 + +岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。 + +此外,你可以假设该网格的四条边均被水包围。 + +![](https://file1.kamacoder.com/i/algo/20220726093256.png) + +提示: + +* m == grid.length +* n == grid[i].length +* 1 <= m, n <= 300 +* grid[i][j] 的值为 '0' 或 '1' + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题题目是 DFS,BFS,并查集,基础题目。 + +本题思路,是用遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。 + +在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 + +那么如何把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 + +### 深度优先搜索 + +以下代码使用dfs实现,如果对dfs不太了解的话,建议先看这篇题解:[797.所有可能的路径](https://programmercarl.com/0797.%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E8%B7%AF%E5%BE%84.html), + +C++代码如下: + +```CPP +// 版本一 +class Solution { +private: + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void dfs(vector>& grid, vector>& visited, int x, int y) { + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { // 没有访问过的 同时 是陆地的 + + visited[nextx][nexty] = true; + dfs(grid, visited, nextx, nexty); + } + } + } +public: + int numIslands(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == '1') { + visited[i][j] = true; + result++; // 遇到没访问过的陆地,+1 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + return result; + } +}; +``` + +很多录友可能有疑惑,为什么 以上代码中的dfs函数,没有终止条件呢? 感觉递归没有终止很危险。 + +其实终止条件 就写在了 调用dfs的地方,如果遇到不合法的方向,直接不会去调用dfs。 + +当然也可以这么写: + +```CPP +// 版本二 +class Solution { +private: + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y] || grid[x][y] == '0') return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty); + } + } +public: + int numIslands(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == '1') { + result++; // 遇到没访问过的陆地,+1 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + return result; + } +}; +``` + +这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件判断 放在了 版本二 的 终止条件位置上。 + +**版本一的写法**是 :下一个节点是否能合法已经判断完了,只要调用dfs就是可以合法的节点。 + +**版本二的写法**是:不管节点是否合法,上来就dfs,然后在终止条件的地方进行判断,不合法再return。 + +**理论上来讲,版本一的效率更高一些**,因为避免了 没有意义的递归调用,在调用dfs之前,就做合法性判断。 但从写法来说,可能版本二 更利于理解一些。(不过其实都差不太多) + +很多同学看了同一道题目,都是dfs,写法却不一样,有时候有终止条件,有时候连终止条件都没有,其实这就是根本原因,两种写法而已。 + + +## 总结 + +其实本题是 dfs,bfs 模板题,但正是因为是模板题,所以大家或者一些题解把重要的细节都很忽略了,我这里把大家没注意的但以后会踩的坑 都给列出来了。 + +本篇我只给出的dfs的写法,大家发现我写的还是比较细的,那么后面我再单独更本题的bfs写法,虽然是模板题,但依然有很多注意的点,敬请期待! + + + + + + +## 其他语言版本 + +### Java + +下面的代码使用的是深度优先搜索 DFS 的做法。为了统计岛屿数量同时不重复记录,每当我们搜索到一个岛后,就将这个岛 “淹没” —— 将这个岛所占的地方从 “1” 改为 “0”,这样就不用担心后续会重复记录这个岛屿了。而 DFS 的过程就体现在 “淹没” 这一步中。详见代码: + +```java +public int numIslands(char[][] grid) { + int res = 0; //记录找到的岛屿数量 + for(int i = 0;i < grid.length;i++){ + for(int j = 0;j < grid[0].length;j++){ + //找到“1”,res加一,同时淹没这个岛 + if(grid[i][j] == '1'){ + res++; + dfs(grid,i,j); + } + } + } + return res; +} +//使用DFS“淹没”岛屿 +public void dfs(char[][] grid, int i, int j){ + //搜索边界:索引越界或遍历到了"0" + if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0') return; + //将这块土地标记为"0" + grid[i][j] = '0'; + //根据"每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成",对上下左右的相邻顶点进行dfs + dfs(grid,i - 1,j); + dfs(grid,i + 1,j); + dfs(grid,i,j + 1); + dfs(grid,i,j - 1); +} +``` +```java +//graph - dfs (和卡哥的代碼邏輯一致) +class Solution { + boolean[][] visited; + int dir[][] = { + {0, 1}, //right + {1, 0}, //down + {-1, 0}, //up + {0, -1} //left + }; + public int numIslands(char[][] grid) { + int count = 0; + visited = new boolean[grid.length][grid[0].length]; + + for(int i = 0; i < grid.length; i++){ + for(int j = 0; j < grid[0].length; j++){ + if(visited[i][j] == false && grid[i][j] == '1'){ + count++; + dfs(grid, i, j); + } + } + } + return count; + } + + private void dfs(char[][]grid, int x, int y){ + if(visited[x][y] == true || grid[x][y] == '0') + return; + + visited[x][y] = true; + + for(int i = 0; i < 4; i++){ + int nextX = x + dir[i][0]; + int nextY = y + dir[i][1]; + if(nextX < 0 || nextY < 0 || nextX >= grid.length || nextY >= grid[0].length) + continue; + dfs(grid, nextX, nextY); + } + } +} +``` + +### Python: + +```python +# 版本一 +class Solution: + def numIslands(self, grid: List[List[str]]) -> int: + m, n = len(grid), len(grid[0]) + visited = [[False] * n for _ in range(m)] + dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)] # 四个方向 + result = 0 + + def dfs(x, y): + for d in dirs: + nextx = x + d[0] + nexty = y + d[1] + if nextx < 0 or nextx >= m or nexty < 0 or nexty >= n: # 越界了,直接跳过 + continue + if not visited[nextx][nexty] and grid[nextx][nexty] == '1': # 没有访问过的同时是陆地的 + visited[nextx][nexty] = True + dfs(nextx, nexty) + + for i in range(m): + for j in range(n): + if not visited[i][j] and grid[i][j] == '1': + visited[i][j] = True + result += 1 # 遇到没访问过的陆地,+1 + dfs(i, j) # 将与其链接的陆地都标记上 true + + return result +``` + +```python +# 版本二 +class Solution: + def numIslands(self, grid: List[List[str]]) -> int: + m, n = len(grid), len(grid[0]) + visited = [[False] * n for _ in range(m)] + dirs = [(-1, 0), (0, 1), (1, 0), (0, -1)] # 四个方向 + result = 0 + + def dfs(x, y): + if visited[x][y] or grid[x][y] == '0': + return # 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = True + for d in dirs: + nextx = x + d[0] + nexty = y + d[1] + if nextx < 0 or nextx >= m or nexty < 0 or nexty >= n: # 越界了,直接跳过 + continue + dfs(nextx, nexty) + + for i in range(m): + for j in range(n): + if not visited[i][j] and grid[i][j] == '1': + result += 1 # 遇到没访问过的陆地,+1 + dfs(i, j) # 将与其链接的陆地都标记上 true + + return result +``` +```python +# 我们用三个状态去标记每一个格子 +# 0 代表海水 +# 1 代表陆地 +# 2 代表已经访问的陆地 +class Solution: + def traversal(self, grid, i, j): + m = len(grid) + n = len(grid[0]) + + if i < 0 or j < 0 or i >= m or j >= n: + return # 越界了 + elif grid[i][j] == "2" or grid[i][j] == "0": + return + + grid[i][j] = "2" + self.traversal(grid, i - 1, j) # 往上走 + self.traversal(grid, i + 1, j) # 往下走 + self.traversal(grid, i, j - 1) # 往左走 + self.traversal(grid, i, j + 1) # 往右走 + + def numIslands(self, grid: List[List[str]]) -> int: + res = 0 + + + for i in range(len(grid)): + for j in range(len(grid[0])): + if grid[i][j] == "1": + res += 1 + self.traversal(grid, i, j) + + return res +``` + +### JavaScript + +```javascript +var numIslands = function (grid) { + let dir = [[0, 1], [1, 0], [-1, 0], [0, -1]]; // 四个方向 + + let dfs = (grid, visited, x, y) => { + for (let i = 0; i < 4; i++) { + let nextX = x + dir[i][0] + let nextY = y + dir[i][1] + if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) + continue; + if (!visited[nextX][nextY] && grid[nextX][nextY] === "1") { + visited[nextX][nextY] = true + dfs(grid,visited,nextX,nextY) + } + } + } + let visited = new Array(grid.length).fill().map(() => Array(grid[0].length).fill(false)) + + let res = 0 + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[i].length; j++) { + if (!visited[i][j] && grid[i][j] === "1") { + ++res; + visited[i][j] = true; + dfs(grid, visited, i, j); + } + } + } + return res +}; +``` + +### TypeScript + +```TypeScript +function numIslands(grid: string[][]): number { + // 四个方向 + const dir: number[][] = [[0, 1], [1, 0], [-1, 0], [0, -1]]; + const [m, n]: [number, number] = [grid.length, grid[0].length]; + + function dfs(grid: string[][], visited: boolean[][], x: number, y: number) { + for (let i = 0; i < 4; i++) { + let nextX: number = x + dir[i][0]; + let nextY: number = y + dir[i][1]; + // 越界了,直接跳过 + if (nextX < 0 || nextX >= m || nextY < 0 || nextY >= n) { + continue; + } + // 没有访问过同时是陆地 + if (!visited[nextX][nextY] && grid[nextX][nextY] === '1') { + visited[nextX][nextY] = true; + dfs(grid, visited, nextX, nextY); + } + } + } + + const visited: boolean[][] = Array.from({ length: m }, _ => new Array(n).fill(false)); + + let result: number = 0; + for (let i = 0; i < m; i++) { + for (let k = 0; k < n; k++) { + if (!visited[i][k] && grid[i][k] === '1') { + ++result; // 遇到没访问过的陆地,+1 + visited[i][k] = true; + dfs(grid, visited, i, k); // 将与其链接的陆地都标记上 true + } + } + } + return result; +} +``` + +### Go + +```go + +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} + +func numIslands(grid [][]byte) int { + res := 0 + + visited := make([][]bool, len(grid)) + for i := 0; i < len(grid); i++ { + visited[i] = make([]bool, len(grid[0])) + } + + for i, rows := range grid { + for j, v := range rows { + if v == '1' && !visited[i][j] { + res++ + dfs(grid, visited, i, j) + } + } + } + + return res +} + +func dfs(grid [][]byte, visited [][]bool, i, j int) { + visited[x][y] = true + for _, d := range DIRECTIONS { + x, y := i+d[0], j+d[1] + if x < 0 || x >= len(grid) || y < 0 || y >= len(grid[0]) { + continue + } + if grid[x][y] == '1' && !visited[x][y] { + dfs(grid, visited, x, y) + } + } + +} +``` + +### Rust: + + +```rust +impl Solution { + const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + pub fn num_islands(grid: Vec>) -> i32 { + let mut visited = vec![vec![false; grid[0].len()]; grid.len()]; + let mut res = 0; + for (i, chars) in grid.iter().enumerate() { + for (j, &c) in chars.iter().enumerate() { + if !visited[i][j] && c == '1' { + res += 1; + Self::dfs(&grid, &mut visited, (i as i32, j as i32)); + } + } + } + res + } + + pub fn dfs(grid: &Vec>, visited: &mut Vec>, (x, y): (i32, i32)) { + for (dx, dy) in Self::DIRECTIONS { + let (nx, ny) = (x + dx, y + dy); + if nx < 0 || nx >= grid.len() as i32 || ny < 0 || ny >= grid[0].len() as i32 { + continue; + } + let (nx, ny) = (nx as usize, ny as usize); + if grid[nx][ny] == '1' && !visited[nx][ny] { + visited[nx][ny] = true; + Self::dfs(grid, visited, (nx as i32, ny as i32)); + } + } + } +} +``` + diff --git "a/problems/0202.\345\277\253\344\271\220\346\225\260.md" "b/problems/0202.\345\277\253\344\271\220\346\225\260.md" new file mode 100755 index 0000000000..fdcadee97c --- /dev/null +++ "b/problems/0202.\345\277\253\344\271\220\346\225\260.md" @@ -0,0 +1,558 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 该用set的时候,还是得用set + +# 第202题. 快乐数 + +[力扣题目链接](https://leetcode.cn/problems/happy-number/) + +编写一个算法来判断一个数 n 是不是快乐数。 + +「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。 + +如果 n 是快乐数就返回 True ;不是,则返回 False 。 + +**示例:** + +输入:19 +输出:true +解释: +1^2 + 9^2 = 82 +8^2 + 2^2 = 68 +6^2 + 8^2 = 100 +1^2 + 0^2 + 0^2 = 1 + +## 思路 + +这道题目看上去貌似一道数学问题,其实并不是! + +题目中说了会 **无限循环**,那么也就是说**求和的过程中,sum会重复出现,这对解题很重要!** + +正如:[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)中所说,**当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法了。** + +所以这道题目使用哈希法,来判断这个sum是否重复出现,如果重复了就是return false, 否则一直找到sum为1为止。 + +判断sum是否重复出现就可以使用unordered_set。 + +**还有一个难点就是求和的过程,如果对取数值各个位上的单数操作不熟悉的话,做这道题也会比较艰难。** + +C++代码如下: + +```CPP +class Solution { +public: + // 取数值各个位上的单数之和 + int getSum(int n) { + int sum = 0; + while (n) { + sum += (n % 10) * (n % 10); + n /= 10; + } + return sum; + } + bool isHappy(int n) { + unordered_set set; + while(1) { + int sum = getSum(n); + if (sum == 1) { + return true; + } + // 如果这个sum曾经出现过,说明已经陷入了无限循环了,立刻return false + if (set.find(sum) != set.end()) { + return false; + } else { + set.insert(sum); + } + n = sum; + } + } +}; +``` + +* 时间复杂度: O(logn) +* 空间复杂度: O(logn) + + + +## 其他语言版本 + +### Java: + +```java +class Solution { + public boolean isHappy(int n) { + Set record = new HashSet<>(); + while (n != 1 && !record.contains(n)) { + record.add(n); + n = getNextNumber(n); + } + return n == 1; + } + + private int getNextNumber(int n) { + int res = 0; + while (n > 0) { + int temp = n % 10; + res += temp * temp; + n = n / 10; + } + return res; + } +} +``` + +### Python: +(版本一)使用集合 + +```python +class Solution: + def isHappy(self, n: int) -> bool: + record = set() + + while True: + n = self.get_sum(n) + if n == 1: + return True + + # 如果中间结果重复出现,说明陷入死循环了,该数不是快乐数 + if n in record: + return False + else: + record.add(n) + + def get_sum(self,n: int) -> int: + new_num = 0 + while n: + n, r = divmod(n, 10) + new_num += r ** 2 + return new_num +``` + (版本二)使用集合 + ```python +class Solution: + def isHappy(self, n: int) -> bool: + record = set() + while n not in record: + record.add(n) + new_num = 0 + n_str = str(n) + for i in n_str: + new_num+=int(i)**2 + if new_num==1: return True + else: n = new_num + return False + ``` + (版本三)使用数组 + ```python +class Solution: + def isHappy(self, n: int) -> bool: + record = [] + while n not in record: + record.append(n) + new_num = 0 + n_str = str(n) + for i in n_str: + new_num+=int(i)**2 + if new_num==1: return True + else: n = new_num + return False + ``` + (版本四)使用快慢指针 + ```python +class Solution: + def isHappy(self, n: int) -> bool: + slow = n + fast = n + while self.get_sum(fast) != 1 and self.get_sum(self.get_sum(fast)): + slow = self.get_sum(slow) + fast = self.get_sum(self.get_sum(fast)) + if slow == fast: + return False + return True + def get_sum(self,n: int) -> int: + new_num = 0 + while n: + n, r = divmod(n, 10) + new_num += r ** 2 + return new_num + ``` + (版本五)使用集合+精简 + ```python +class Solution: + def isHappy(self, n: int) -> bool: + seen = set() + while n != 1: + n = sum(int(i) ** 2 for i in str(n)) + if n in seen: + return False + seen.add(n) + return True + ``` + (版本六)使用数组+精简 + ```python +class Solution: + def isHappy(self, n: int) -> bool: + seen = [] + while n != 1: + n = sum(int(i) ** 2 for i in str(n)) + if n in seen: + return False + seen.append(n) + return True + ``` +### Go: + +```go +func isHappy(n int) bool { + m := make(map[int]bool) + for n != 1 && !m[n] { + n, m[n] = getSum(n), true + } + return n == 1 +} + +func getSum(n int) int { + sum := 0 + for n > 0 { + sum += (n % 10) * (n % 10) + n = n / 10 + } + return sum +} +``` + +### JavaScript: + +```js +var isHappy = function (n) { + let m = new Map() + + const getSum = (num) => { + let sum = 0 + while (n) { + sum += (n % 10) ** 2 + n = Math.floor(n / 10) + } + return sum + } + + while (true) { + // n出现过,证明已陷入无限循环 + if (m.has(n)) return false + if (n === 1) return true + m.set(n, 1) + n = getSum(n) + } +} + +// 方法二:使用环形链表的思想 说明出现闭环 退出循环 +var isHappy = function(n) { + if (getN(n) == 1) return true; + let a = getN(n), b = getN(getN(n)); + // 如果 a === b + while (b !== 1 && getN(b) !== 1 && a !== b) { + a = getN(a); + b = getN(getN(b)); + } + return b === 1 || getN(b) === 1 ; +}; + +// 方法三:使用Set()更简洁 +/** + * @param {number} n + * @return {boolean} + */ + +var getSum = function (n) { + let sum = 0; + while (n) { + sum += (n % 10) ** 2; + n = Math.floor(n/10); + } + return sum; +} +var isHappy = function(n) { + let set = new Set(); // Set() 里的数是惟一的 + // 如果在循环中某个值重复出现,说明此时陷入死循环,也就说明这个值不是快乐数 + while (n !== 1 && !set.has(n)) { + set.add(n); + n = getSum(n); + } + return n === 1; +}; + +// 方法四:使用Set(),求和用reduce +var isHappy = function(n) { + let set = new Set(); + let totalCount; + while(totalCount !== 1) { + let arr = (''+(totalCount || n)).split(''); + totalCount = arr.reduce((total, num) => { + return total + num * num + }, 0) + if (set.has(totalCount)) { + return false; + } + set.add(totalCount); + } + return true; +}; +``` + +### TypeScript: + +```typescript +function isHappy(n: number): boolean { + // Utils + // 计算val各位的平方和 + function calcSum(val: number): number { + return String(val).split("").reduce((pre, cur) => (pre + Number(cur) * Number(cur)), 0); + } + + let storeSet: Set = new Set(); + while (n !== 1 && !storeSet.has(n)) { + storeSet.add(n); + n = calcSum(n); + } + return n === 1; +}; +``` + +### Swift: + +```swift +// number 每个位置上的数字的平方和 +func getSum(_ number: Int) -> Int { + var sum = 0 + var num = number + while num > 0 { + let temp = num % 10 + sum += (temp * temp) + num /= 10 + } + return sum +} +func isHappy(_ n: Int) -> Bool { + var set = Set() + var num = n + while true { + let sum = self.getSum(num) + if sum == 1 { + return true + } + // 如果这个sum曾经出现过,说明已经陷入了无限循环了 + if set.contains(sum) { + return false + } else { + set.insert(sum) + } + num = sum + } +} +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer $n + * @return Boolean + */ + function isHappy($n) { + // use a set to record sum + // whenever there is a duplicated, stop + // == 1 return true, else false + $table = []; + while ($n != 1 && !isset($table[$n])) { + $table[$n] = 1; + $n = self::getNextN($n); + } + return $n == 1; + } + + function getNextN(int $n) { + $res = 0; + while ($n > 0) { + $temp = $n % 10; + $res += $temp * $temp; + $n = floor($n / 10); + } + return $res; + } +} +``` + +### Rust: + +```Rust +use std::collections::HashSet; +impl Solution { + pub fn get_sum(mut n: i32) -> i32 { + let mut sum = 0; + while n > 0 { + sum += (n % 10) * (n % 10); + n /= 10; + } + sum + } + + pub fn is_happy(n: i32) -> bool { + let mut n = n; + let mut set = HashSet::new(); + loop { + let sum = Self::get_sum(n); + if sum == 1 { + return true; + } + if set.contains(&sum) { + return false; + } else { set.insert(sum); } + n = sum; + } + } +} +``` + +### C: + +```C +int get_sum(int n) { + int sum = 0; + div_t n_div = { .quot = n }; + while (n_div.quot != 0) { + n_div = div(n_div.quot, 10); + sum += n_div.rem * n_div.rem; + } + return sum; +} + +// (版本1)使用数组 +bool isHappy(int n) { + // sum = a1^2 + a2^2 + ... ak^2 + // first round: + // 1 <= k <= 10 + // 1 <= sum <= 1 + 81 * 9 = 730 + // second round: + // 1 <= k <= 3 + // 1 <= sum <= 36 + 81 * 2 = 198 + // third round: + // 1 <= sum <= 81 * 2 = 162 + // fourth round: + // 1 <= sum <= 81 * 2 = 162 + + uint8_t visited[163] = { 0 }; + int sum = get_sum(get_sum(n)); + int next_n = sum; + + while (next_n != 1) { + sum = get_sum(next_n); + + if (visited[sum]) return false; + + visited[sum] = 1; + next_n = sum; + }; + + return true; +} + +// (版本2)使用快慢指针 +bool isHappy(int n) { + int slow = n; + int fast = n; + + do { + slow = get_sum(slow); + fast = get_sum(get_sum(fast)); + } while (slow != fast); + + return (fast == 1); +} +``` + +### Scala: +```scala +object Solution { + // 引入mutable + import scala.collection.mutable + def isHappy(n: Int): Boolean = { + // 存放每次计算后的结果 + val set: mutable.HashSet[Int] = new mutable.HashSet[Int]() + var tmp = n // 因为形参是不可变量,所以需要找到一个临时变量 + // 开始进入循环 + while (true) { + val sum = getSum(tmp) // 获取这个数每个值的平方和 + if (sum == 1) return true // 如果最终等于 1,则返回true + // 如果set里面已经有这个值了,说明进入无限循环,可以返回false,否则添加这个值到set + if (set.contains(sum)) return false + else set.add(sum) + tmp = sum + } + // 最终需要返回值,直接返回个false + false + } + + def getSum(n: Int): Int = { + var sum = 0 + var tmp = n + while (tmp != 0) { + sum += (tmp % 10) * (tmp % 10) + tmp = tmp / 10 + } + sum + } +``` + +### C#: + +```csharp +public class Solution { + private int getSum(int n) { + int sum = 0; + //每位数的换算 + while (n > 0) { + sum += (n % 10) * (n % 10); + n /= 10; + } + return sum; + } + public bool IsHappy(int n) { + HashSet set = new HashSet(); + while(n != 1 && !set.Contains(n)) { //判断避免循环 + set.Add(n); + n = getSum(n); + } + return n == 1; + } +} +``` + +### Ruby: + +```ruby +# @param {Integer} n +# @return {Boolean} +def is_happy(n) + @occurred_nums = Set.new + + while true + n = next_value(n) + + return true if n == 1 + + return false if @occurred_nums.include?(n) + + @occurred_nums << n + end +end + +def next_value(n) + n.to_s.chars.sum { |char| char.to_i ** 2 } +end +``` + diff --git "a/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" "b/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" new file mode 100755 index 0000000000..ffe04a5b07 --- /dev/null +++ "b/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" @@ -0,0 +1,808 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +> 链表操作中,可以使用原链表来直接进行删除操作,也可以设置一个虚拟头结点再进行删除操作,接下来看一看哪种方式更方便。 + +# 203.移除链表元素 + +[力扣题目链接](https://leetcode.cn/problems/remove-linked-list-elements/) + +题意:删除链表中等于给定值 val 的所有节点。 + +示例 1: +输入:head = [1,2,6,3,4,5,6], val = 6 +输出:[1,2,3,4,5] + +示例 2: +输入:head = [], val = 1 +输出:[] + +示例 3: +输入:head = [7,7,7,7], val = 7 +输出:[] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[链表基础操作| LeetCode:203.移除链表元素](https://www.bilibili.com/video/BV18B4y1s7R9),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这里以链表 1 4 2 4 来举例,移除元素4。 + +![203_链表删除元素1](https://file1.kamacoder.com/i/algo/20210316095351161.png) + +如果使用C,C++编程语言的话,不要忘了还要从内存中删除这两个移除的节点, 清理节点内存之后如图: + +![203_链表删除元素2](https://file1.kamacoder.com/i/algo/20210316095418280.png) + +**当然如果使用java ,python的话就不用手动管理内存了。** + +还要说明一下,就算使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养成手动清理内存的习惯。 + +这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了, + +那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢? + +这里就涉及如下链表操作的两种方式: + +* **直接使用原来的链表来进行删除操作。** +* **设置一个虚拟头结点在进行删除操作。** + + +来看第一种操作:直接使用原来的链表来进行移除。 + +![203_链表删除元素3](https://file1.kamacoder.com/i/algo/2021031609544922.png) + +移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。 + +所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。 + +![203_链表删除元素4](https://file1.kamacoder.com/i/algo/20210316095512470.png) + +依然别忘将原头结点从内存中删掉。 +![203_链表删除元素5](https://file1.kamacoder.com/i/algo/20210316095543775.png) + + +这样移除了一个头结点,是不是发现,在单链表中移除头结点 和 移除其他节点的操作方式是不一样,其实在写代码的时候也会发现,需要单独写一段逻辑来处理移除头结点的情况。 + +那么可不可以 以一种统一的逻辑来移除 链表的节点呢。 + +其实**可以设置一个虚拟头结点**,这样原链表的所有节点就都可以按照统一的方式进行移除了。 + +来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。 + +![203_链表删除元素6](https://file1.kamacoder.com/i/algo/20210316095619221.png) + +这里来给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1。 + +这样是不是就可以使用和移除链表其他节点的方式统一了呢? + +来看一下,如何移除元素1 呢,还是熟悉的方式,然后从内存中删除元素1。 + +最后呢在题目中,return 头结点的时候,别忘了 `return dummyNode->next;`, 这才是新的头结点 + +**直接使用原来的链表来进行移除节点操作:** + +```CPP +class Solution { +public: + ListNode* removeElements(ListNode* head, int val) { + // 删除头结点 + while (head != NULL && head->val == val) { // 注意这里不是if + ListNode* tmp = head; + head = head->next; + delete tmp; + } + + // 删除非头结点 + ListNode* cur = head; + while (cur != NULL && cur->next!= NULL) { + if (cur->next->val == val) { + ListNode* tmp = cur->next; + cur->next = cur->next->next; + delete tmp; + } else { + cur = cur->next; + } + } + return head; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + +**设置一个虚拟头结点在进行移除节点操作:** + +```CPP +class Solution { +public: + ListNode* removeElements(ListNode* head, int val) { + ListNode* dummyHead = new ListNode(0); // 设置一个虚拟头结点 + dummyHead->next = head; // 将虚拟头结点指向head,这样方便后面做删除操作 + ListNode* cur = dummyHead; + while (cur->next != NULL) { + if(cur->next->val == val) { + ListNode* tmp = cur->next; + cur->next = cur->next->next; + delete tmp; + } else { + cur = cur->next; + } + } + head = dummyHead->next; + delete dummyHead; + return head; + } +}; + +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + +**也可以通过递归的思路解决本题:** + +基础情况:对于空链表,不需要移除元素。 + +递归情况:首先检查头节点的值是否为 val,如果是则移除头节点,答案即为在头节点的后续节点上递归的结果;如果头节点的值不为 val,则答案为头节点与在头节点的后续节点上递归得到的新链表拼接的结果。 + +```CPP +class Solution { +public: + ListNode* removeElements(ListNode* head, int val) { + // 基础情况:空链表 + if (head == nullptr) { + return nullptr; + } + + // 递归处理 + if (head->val == val) { + ListNode* newHead = removeElements(head->next, val); + delete head; + return newHead; + } else { + head->next = removeElements(head->next, val); + return head; + } + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(n) + + +## 其他语言版本 + +### C: +用原来的链表操作: + +```c +struct ListNode* removeElements(struct ListNode* head, int val){ + struct ListNode* temp; + // 当头结点存在并且头结点的值等于val时 + while(head && head->val == val) { + temp = head; + // 将新的头结点设置为head->next并删除原来的头结点 + head = head->next; + free(temp); + } + + struct ListNode *cur = head; + // 当cur存在并且cur->next存在时 + // 此解法需要判断cur存在因为cur指向head。若head本身为NULL或者原链表中元素都为val的话,cur也会为NULL + while(cur && (temp = cur->next)) { + // 若cur->next的值等于val + if(temp->val == val) { + // 将cur->next设置为cur->next->next并删除cur->next + cur->next = temp->next; + free(temp); + } + // 若cur->next不等于val,则将cur后移一位 + else + cur = cur->next; + } + + // 返回头结点 + return head; +} +``` + +设置一个虚拟头结点: + +```c +/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * struct ListNode *next; + * }; + */ + + +struct ListNode* removeElements(struct ListNode* head, int val){ + typedef struct ListNode ListNode; + ListNode *shead; + shead = (ListNode *)malloc(sizeof(ListNode)); + shead->next = head; + ListNode *cur = shead; + while(cur->next != NULL){ + if (cur->next->val == val){ + ListNode *tmp = cur->next; + cur->next = cur->next->next; + free(tmp); + } + else{ + cur = cur->next; + } + } + head = shead->next; + free(shead); + return head; +} +``` + +### Java: + +用原来的链表操作: +```java +/** + * 方法1 + * 时间复杂度 O(n) + * 空间复杂度 O(1) + * @param head + * @param val + * @return + */ +public ListNode removeElements(ListNode head, int val) { + while(head!=null && head.val==val) { + head = head.next; + } + ListNode curr = head; + while(curr!=null && curr.next !=null) { + if(curr.next.val == val){ + curr.next = curr.next.next; + } else { + curr = curr.next; + } + } + return head; +} + +/** + * 方法1 + * 时间复杂度 O(n) + * 空间复杂度 O(1) + * @param head + * @param val + * @return + */ +public ListNode removeElements(ListNode head, int val) { + while (head != null && head.val == val) { + head = head.next; + } + // 已经为null,提前退出 + if (head == null) { + return head; + } + // 已确定当前head.val != val + ListNode pre = head; + ListNode cur = head.next; + while (cur != null) { + if (cur.val == val) { + pre.next = cur.next; + } else { + pre = cur; + } + cur = cur.next; + } + return head; +} + +``` + +设置一个虚拟头结点: + +```java +/** + * 时间复杂度 O(n) + * 空间复杂度 O(1) + * @param head + * @param val + * @return + */ +public ListNode removeElements(ListNode head, int val) { + // 设置一个虚拟的头结点 + ListNode dummy = new ListNode(); + dummy.next = head; + + ListNode cur = dummy; + while (cur.next != null) { + if (cur.next.val == val) { + cur.next = cur.next.next; + } else { + cur = cur.next; + } + } + return dummy.next; +} + +``` + +递归 + +```java +/** + * 时间复杂度 O(n) + * 空间复杂度 O(n) + * @param head + * @param val + * @return + */ +class Solution { + public ListNode removeElements(ListNode head, int val) { + if (head == null) { + return head; + } + + // 假设 removeElements() 返回后面完整的已经去掉val节点的子链表 + // 在当前递归层用当前节点接住后面的子链表 + // 随后判断当前层的node是否需要被删除,如果是,就返回 + // 也可以先判断是否需要删除当前node,但是这样条件语句会比较不好想 + head.next = removeElements(head.next, val); + if (head.val == val) { + return head.next; + } + return head; + + // 实际上就是还原一个从尾部开始重新构建链表的过程 + } +} +``` + +### Python: + +```python +(版本一)虚拟头节点法 +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next +class Solution: + def removeElements(self, head: Optional[ListNode], val: int) -> Optional[ListNode]: + # 创建虚拟头部节点以简化删除过程 + dummy_head = ListNode(next = head) + + # 遍历列表并删除值为val的节点 + current = dummy_head + while current.next: + if current.next.val == val: + current.next = current.next.next + else: + current = current.next + + return dummy_head.next + +``` + +### Go: +直接使用原链表 +```go +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func removeElements(head *ListNode, val int) *ListNode { + + //依旧是先定义逻辑 + + //如果原链表的头节点为val的话,head=head.next,且为持续过程,防止头节点后面的节点也为Val + //这里前置循环 并且要判定head 是否为nil,防止出错 + for head != nil && head.Val == val {//由于leetcode代码运行方式,for循环条件判断前后顺序不能修改,下面的for循环也同样如此 + head = head.Next + } + cur := head + + for cur != nil && cur.Next != nil { + if cur.Next.Val == val { + cur.Next = cur.Next.Next + } else { + cur = cur.Next + } + } + + return head + +} + +``` +虚拟头节点方式: +```go +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +func removeElements(head *ListNode, val int) *ListNode { + dummyHead := &ListNode{} + dummyHead.Next = head + cur := dummyHead + for cur != nil && cur.Next != nil { + if cur.Next.Val == val { + cur.Next = cur.Next.Next + } else { + cur = cur.Next + } + } + return dummyHead.Next +} +``` + +### JavaScript: + +```js +/** + * @param {ListNode} head + * @param {number} val + * @return {ListNode} + */ +var removeElements = function(head, val) { + const ret = new ListNode(0, head); + let cur = ret; + while(cur.next) { + if(cur.next.val === val) { + cur.next = cur.next.next; + continue; + } + cur = cur.next; + } + return ret.next; +}; +``` + +### TypeScript: + +版本一(在原链表上直接删除): + +```typescript +/** + * Definition for singly-linked list. + * class ListNode { + * val: number + * next: ListNode | null + * constructor(val?: number, next?: ListNode | null) { + * this.val = (val===undefined ? 0 : val) + * this.next = (next===undefined ? null : next) + * } + * } + */ +function removeElements(head: ListNode | null, val: number): ListNode | null { + // 删除头部节点 + while (head !== null && head.val === val) { + head = head.next; + } + if (head === null) return head; + let pre: ListNode = head, cur: ListNode | null = head.next; + // 删除非头部节点 + while (cur) { + if (cur.val === val) { + pre.next = cur.next; + } else { + //此处不加类型断言时:编译器会认为pre类型为ListNode, pre.next类型为ListNode | null + pre = pre.next as ListNode; + } + cur = cur.next; + } + return head; +}; +``` + +版本二(虚拟头节点): + +```typescript +function removeElements(head: ListNode | null, val: number): ListNode | null { + // 添加虚拟节点 + const data = new ListNode(0, head); + let pre = data, cur = data.next; + while (cur) { + if (cur.val === val) { + pre.next = cur.next + } else { + pre = cur; + } + cur = cur.next; + } + return data.next; +}; +``` + +### Swift: + +```swift +/** + * Definition for singly-linked list. + * public class ListNode { + * public var val: Int + * public var next: ListNode? + * public init() { self.val = 0; self.next = nil; } + * public init(_ val: Int) { self.val = val; self.next = nil; } + * public init(_ val: Int, _ next: ListNode?) { self.val = val; self.next = next; } + * } + */ +func removeElements(_ head: ListNode?, _ val: Int) -> ListNode? { + let dummyNode = ListNode() + dummyNode.next = head + var currentNode = dummyNode + while let curNext = currentNode.next { + if curNext.val == val { + currentNode.next = curNext.next + } else { + currentNode = curNext + } + } + return dummyNode.next +} +``` + +### PHP: + +```php +/** + * Definition for a singly-linked list. + * class ListNode { + * public $val = 0; + * public $next = null; + * function __construct($val = 0, $next = null) { + * $this->val = $val; + * $this->next = $next; + * } + * } + */ + +//版本一(在原链表上直接删除): +class Solution { + + /** + * @param ListNode $head + * @param Integer $val + * @return ListNode + */ + function removeElements($head, $val) + { + if ($head == null) { + return null; + } + + $now = $head; + while ($now->next != null) { + if ($now->next->val == $val) { + $now->next = $now->next->next; + } else { + $now = $now->next; + } + } + if ($head->val == $val) { + return $head->next; + } + return $head; + } +} + +//版本二(虚拟头结点方式): +class Solution { + + /** + * @param ListNode $head + * @param Integer $val + * @return ListNode + */ + function removeElements($head, $val) + { + $dummyHead = new ListNode(0, $head); + $now = $dummyHead; + while ($now->next != null){ + if ($now->next->val == $val) { + $now->next = $now->next->next; + } else { + $now = $now->next; + } + } + return $dummyHead->next; + } +} +``` + +### Rust: + +```rust +// Definition for singly-linked list. +// #[derive(PartialEq, Eq, Clone, Debug)] +// pub struct ListNode { +// pub val: i32, +// pub next: Option> +// } +// +// impl ListNode { +// #[inline] +// fn new(val: i32) -> Self { +// ListNode { +// next: None, +// val +// } +// } +// } +impl Solution { + pub fn remove_elements(head: Option>, val: i32) -> Option> { + let mut dummyHead = Box::new(ListNode::new(0)); + dummyHead.next = head; + let mut cur = dummyHead.as_mut(); + // 使用take()替换std::mem::replace(&mut node.next, None)达到相同的效果,并且更普遍易读 + while let Some(nxt) = cur.next.take() { + if nxt.val == val { + cur.next = nxt.next; + } else { + cur.next = Some(nxt); + cur = cur.next.as_mut().unwrap(); + } + } + dummyHead.next + } +} +``` + +### Scala: + +```scala +/** + * Definition for singly-linked list. + * class ListNode(_x: Int = 0, _next: ListNode = null) { + * var next: ListNode = _next + * var x: Int = _x + * } + */ +object Solution { + def removeElements(head: ListNode, `val`: Int): ListNode = { + if (head == null) return head + var dummy = new ListNode(-1, head) // 定义虚拟头节点 + var cur = head // cur 表示当前节点 + var pre = dummy // pre 表示cur前一个节点 + while (cur != null) { + if (cur.x == `val`) { + // 相等,就删除那么cur的前一个节点pre执行cur的下一个 + pre.next = cur.next + } else { + // 不相等,pre就等于当前cur节点 + pre = cur + } + // 向下迭代 + cur = cur.next + } + // 最终返回dummy的下一个,就是链表的头 + dummy.next + } +} +``` + +### Kotlin: + +```kotlin +/** + * Example: + * var li = ListNode(5) + * var v = li.`val` + * Definition for singly-linked list. + * class ListNode(var `val`: Int) { + * var next: ListNode? = null + * } + */ +class Solution { + fun removeElements(head: ListNode?, `val`: Int): ListNode? { + // 使用虚拟节点,令该节点指向head + var dummyNode = ListNode(-1) + dummyNode.next = head + // 使用cur遍历链表各个节点 + var cur = dummyNode + // 判断下个节点是否为空 + while (cur.next != null) { + // 符合条件,移除节点 + if (cur.next.`val` == `val`) { + cur.next = cur.next.next + } + // 不符合条件,遍历下一节点 + else { + cur = cur.next + } + } + // 注意:返回的不是虚拟节点 + return dummyNode.next + } +} +``` + +### C# + +```CSharp +/** + * Definition for singly-linked list. + * public class ListNode { + * public int val; + * public ListNode next; + * public ListNode(int val=0, ListNode next=null) { + * this.val = val; + * this.next = next; + * } + * } + */ +public class Solution +{ + public ListNode RemoveElements(ListNode head, int val) + { + ListNode dummyHead = new ListNode(0,head); + ListNode temp = dummyHead; + while(temp.next != null) + { + if(temp.next.val == val) + { + temp.next = temp.next.next; + } + else + { + temp = temp.next; + } + } + return dummyHead.next; + } +} +``` +### Ruby# + +```ruby +# 定义链表节点 +class ListNode + attr_accessor :val, :next + def initialize(val = 0, _next = nil) + @val = val + @next = _next + end +end + +# 删除链表中值为 val 的节点 +def remove_elements(head, val) + # 创建一个虚拟头节点,这样可以简化删除头节点的处理 + # 虚拟头节点的值为 0,指向当前链表的头节点 + dummy = ListNode.new(0) + dummy.next = head + + # 初始化当前节点为虚拟头节点 + current = dummy + + # 遍历链表,直到当前节点的下一个节点为空 + while current.next + # 如果当前节点的下一个节点的值等于 val + if current.next.val == val + # 跳过该节点,即将当前节点的 next 指向下一个节点的 next + current.next = current.next.next + else + # 否则继续遍历,当前节点向前移动 + current = current.next + end + end + + # 返回删除 val 后的新链表的头节点,虚拟头节点的 next 就是新的头节点 + dummy.next +end + +``` + diff --git "a/problems/0205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" "b/problems/0205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" new file mode 100755 index 0000000000..ba255e0685 --- /dev/null +++ "b/problems/0205.\345\220\214\346\236\204\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,180 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 205. 同构字符串 + +[力扣题目链接](https://leetcode.cn/problems/isomorphic-strings/) + +给定两个字符串 s 和 t,判断它们是否是同构的。 + +如果 s 中的字符可以按某种映射关系替换得到 t ,那么这两个字符串是同构的。 + +每个出现的字符都应当映射到另一个字符,同时不改变字符的顺序。不同字符不能映射到同一个字符上,相同字符只能映射到同一个字符上,字符可以映射到自己本身。 + +示例 1: +* 输入:s = "egg", t = "add" +* 输出:true + +示例 2: +* 输入:s = "foo", t = "bar" +* 输出:false + +示例 3: +* 输入:s = "paper", t = "title" +* 输出:true + +提示:可以假设 s 和 t 长度相同。 + +## 思路 + +字符串没有说都是小写字母之类的,所以用数组不合适了,用map来做映射。 + +使用两个map 保存 s[i] 到 t[j] 和 t[j] 到 s[i] 的映射关系,如果发现对应不上,立刻返回 false + +C++代码 如下: + +```CPP +class Solution { +public: + bool isIsomorphic(string s, string t) { + unordered_map map1; + unordered_map map2; + for (int i = 0, j = 0; i < s.size(); i++, j++) { + if (map1.find(s[i]) == map1.end()) { // map1保存s[i] 到 t[j]的映射 + map1[s[i]] = t[j]; + } + if (map2.find(t[j]) == map2.end()) { // map2保存t[j] 到 s[i]的映射 + map2[t[j]] = s[i]; + } + // 发现映射 对应不上,立刻返回false + if (map1[s[i]] != t[j] || map2[t[j]] != s[i]) { + return false; + } + } + return true; + } +}; +``` + + +## 其他语言版本 + +### Java + +```java +class Solution { + public boolean isIsomorphic(String s, String t) { + Map map1 = new HashMap<>(); + Map map2 = new HashMap<>(); + for (int i = 0, j = 0; i < s.length(); i++, j++) { + if (!map1.containsKey(s.charAt(i))) { + map1.put(s.charAt(i), t.charAt(j)); // map1保存 s[i] 到 t[j]的映射 + } + if (!map2.containsKey(t.charAt(j))) { + map2.put(t.charAt(j), s.charAt(i)); // map2保存 t[j] 到 s[i]的映射 + } + // 无法映射,返回 false + if (map1.get(s.charAt(i)) != t.charAt(j) || map2.get(t.charAt(j)) != s.charAt(i)) { + return false; + } + } + return true; + } +} +``` + +### Python + +```python +class Solution: + def isIsomorphic(self, s: str, t: str) -> bool: + default_dict1 = defaultdict(str) + default_dict2 = defaultdict(str) + + if len(s) != len(t): return false + + for i in range(len(s)): + if not default_dict1[s[i]]: + default_dict1[s[i]] = t[i] + + if not default_dict2[t[i]]: + default_dict2[t[i]] = s[i] + + if default_dict1[s[i]] != t[i] or default_dict2[t[i]] != s[i]: + return False + + return True +``` + +### Go + +```go +func isIsomorphic(s string, t string) bool { + map1 := make(map[byte]byte) + map2 := make(map[byte]byte) + for i := range s { + if _, ok := map1[s[i]]; !ok { + map1[s[i]] = t[i] // map1保存 s[i] 到 t[j]的映射 + } + if _, ok := map2[t[i]]; !ok { + map2[t[i]] = s[i] // map2保存 t[i] 到 s[j]的映射 + } + // 无法映射,返回 false + if (map1[s[i]] != t[i]) || (map2[t[i]] != s[i]) { + return false + } + } + return true +} +``` + +### JavaScript + +```js +var isIsomorphic = function(s, t) { + let len = s.length; + if(len === 0) return true; + let maps = new Map(); + let mapt = new Map(); + for(let i = 0, j = 0; i < len; i++, j++){ + if(!maps.has(s[i])){ + maps.set(s[i],t[j]);// maps保存 s[i] 到 t[j]的映射 + } + if(!mapt.has(t[j])){ + mapt.set(t[j],s[i]);// mapt保存 t[j] 到 s[i]的映射 + } + // 无法映射,返回 false + if(maps.get(s[i]) !== t[j] || mapt.get(t[j]) !== s[i]){ + return false; + } + }; + return true; +}; +``` + +### TypeScript + +```typescript +function isIsomorphic(s: string, t: string): boolean { + const helperMap1: Map = new Map(); + const helperMap2: Map = new Map(); + for (let i = 0, length = s.length; i < length; i++) { + let temp1: string | undefined = helperMap1.get(s[i]); + let temp2: string | undefined = helperMap2.get(t[i]); + if (temp1 === undefined && temp2 === undefined) { + helperMap1.set(s[i], t[i]); + helperMap2.set(t[i], s[i]); + } else if (temp1 !== t[i] || temp2 !== s[i]) { + return false; + } + } + return true; +}; +``` + + + + + diff --git "a/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" "b/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" new file mode 100755 index 0000000000..e49037dd2b --- /dev/null +++ "b/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" @@ -0,0 +1,739 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 反转链表的写法很简单,一些同学甚至可以背下来但过一阵就忘了该咋写,主要是因为没有理解真正的反转过程。 + +# 206.反转链表 + +[力扣题目链接](https://leetcode.cn/problems/reverse-linked-list/) + +题意:反转一个单链表。 + +示例: +输入: 1->2->3->4->5->NULL +输出: 5->4->3->2->1->NULL + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[帮你拿下反转链表 | LeetCode:206.反转链表](https://www.bilibili.com/video/BV1nB4y1i7eL),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。 + +其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示: + + +![206_反转链表](https://file1.kamacoder.com/i/algo/20210218090901207.png) + +之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改变next指针的方向。 + +那么接下来看一看是如何反转的呢? + +我们拿有示例中的链表来举例,如动画所示:(纠正:动画应该是先移动pre,在移动cur) + +![](https://file1.kamacoder.com/i/algo/206.%E7%BF%BB%E8%BD%AC%E9%93%BE%E8%A1%A8.gif) + +首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。 + +然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。 + +为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。 + +接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。 + +最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。 + +### 双指针法 +```CPP +class Solution { +public: + ListNode* reverseList(ListNode* head) { + ListNode* temp; // 保存cur的下一个节点 + ListNode* cur = head; + ListNode* pre = NULL; + while(cur) { + temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next + cur->next = pre; // 翻转操作 + // 更新pre 和 cur指针 + pre = cur; + cur = temp; + } + return pre; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + +### 递归法 + +递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。 + +关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。 + +具体可以看代码(已经详细注释),**双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。** +```CPP +class Solution { +public: + ListNode* reverse(ListNode* pre,ListNode* cur){ + if(cur == NULL) return pre; + ListNode* temp = cur->next; + cur->next = pre; + // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步 + // pre = cur; + // cur = temp; + return reverse(cur,temp); + } + ListNode* reverseList(ListNode* head) { + // 和双指针法初始化是一样的逻辑 + // ListNode* cur = head; + // ListNode* pre = NULL; + return reverse(NULL, head); + } + +}; +``` + +* 时间复杂度: O(n), 要递归处理链表的每个节点 +* 空间复杂度: O(n), 递归调用了 n 层栈空间 + +我们可以发现,上面的递归写法和双指针法实质上都是从前往后翻转指针指向,其实还有另外一种与双指针法不同思路的递归写法:从后往前翻转指针指向。 + +具体代码如下(带详细注释): + +```CPP +class Solution { +public: + ListNode* reverseList(ListNode* head) { + // 边缘条件判断 + if(head == NULL) return NULL; + if (head->next == NULL) return head; + + // 递归调用,翻转第二个节点开始往后的链表 + ListNode *last = reverseList(head->next); + // 翻转头节点与第二个节点的指向 + head->next->next = head; + // 此时的 head 节点为尾节点,next 需要指向 NULL + head->next = NULL; + return last; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(n) + + +## 其他语言版本 + +### Java: + +```java +// 双指针 +class Solution { + public ListNode reverseList(ListNode head) { + ListNode prev = null; + ListNode cur = head; + ListNode temp = null; + while (cur != null) { + temp = cur.next;// 保存下一个节点 + cur.next = prev; + prev = cur; + cur = temp; + } + return prev; + } +} +``` + +```java +// 递归 +class Solution { + public ListNode reverseList(ListNode head) { + return reverse(null, head); + } + + private ListNode reverse(ListNode prev, ListNode cur) { + if (cur == null) { + return prev; + } + ListNode temp = null; + temp = cur.next;// 先保存下一个节点 + cur.next = prev;// 反转 + // 更新prev、cur位置 + // prev = cur; + // cur = temp; + return reverse(cur, temp); + } +} +``` + +```java +// 从后向前递归 +class Solution { + ListNode reverseList(ListNode head) { + // 边缘条件判断 + if(head == null) return null; + if (head.next == null) return head; + + // 递归调用,翻转第二个节点开始往后的链表 + ListNode last = reverseList(head.next); + // 翻转头节点与第二个节点的指向 + head.next.next = head; + // 此时的 head 节点为尾节点,next 需要指向 NULL + head.next = null; + return last; + } +} +``` + +### Python: + +```python +(版本一)双指针法 +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next +class Solution: + def reverseList(self, head: ListNode) -> ListNode: + cur = head + pre = None + while cur: + temp = cur.next # 保存一下 cur的下一个节点,因为接下来要改变cur->next + cur.next = pre #反转 + #更新pre、cur指针 + pre = cur + cur = temp + return pre +``` + +```python +(版本二)递归法 +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, val=0, next=None): +# self.val = val +# self.next = next +class Solution: + def reverseList(self, head: ListNode) -> ListNode: + return self.reverse(head, None) + def reverse(self, cur: ListNode, pre: ListNode) -> ListNode: + if cur == None: + return pre + temp = cur.next + cur.next = pre + return self.reverse(temp, cur) + +``` + + + +### Go: + +```go +//双指针 +func reverseList(head *ListNode) *ListNode { + var pre *ListNode + cur := head + for cur != nil { + next := cur.Next + cur.Next = pre + pre = cur + cur = next + } + return pre +} + +//递归 +func reverseList(head *ListNode) *ListNode { + return help(nil, head) +} + +func help(pre, head *ListNode)*ListNode{ + if head == nil { + return pre + } + next := head.Next + head.Next = pre + return help(head, next) +} + +``` +### JavaScript: + +```js +/** + * @param {ListNode} head + * @return {ListNode} + */ + +// 双指针: +var reverseList = function(head) { + if(!head || !head.next) return head; + let temp = null, pre = null, cur = head; + while(cur) { + temp = cur.next; + cur.next = pre; + pre = cur; + cur = temp; + } + // temp = cur = null; + return pre; +}; + +// 递归: +var reverse = function(pre, head) { + if(!head) return pre; + const temp = head.next; + head.next = pre; + pre = head + return reverse(pre, temp); +} + +var reverseList = function(head) { + return reverse(null, head); +}; + +// 递归2 +var reverse = function(head) { + if(!head || !head.next) return head; + // 从后往前翻 + const pre = reverse(head.next); + head.next = pre.next; + pre.next = head; + return head; +} + +var reverseList = function(head) { + let cur = head; + while(cur && cur.next) { + cur = cur.next; + } + reverse(head); + return cur; +}; +``` + +### TypeScript: + +```typescript +// 双指针法 +function reverseList(head: ListNode | null): ListNode | null { + let preNode: ListNode | null = null, + curNode: ListNode | null = head, + tempNode: ListNode | null; + while (curNode) { + tempNode = curNode.next; + curNode.next = preNode; + preNode = curNode; + curNode = tempNode; + } + return preNode; +}; + +// 递归(从前往后翻转) +function reverseList(head: ListNode | null): ListNode | null { + function recur(preNode: ListNode | null, curNode: ListNode | null): ListNode | null { + if (curNode === null) return preNode; + let tempNode: ListNode | null = curNode.next; + curNode.next = preNode; + preNode = curNode; + curNode = tempNode; + return recur(preNode, curNode); + } + return recur(null, head); +}; + +// 递归(从后往前翻转) +function reverseList(head: ListNode | null): ListNode | null { + if (head === null) return null; + let newHead: ListNode | null; + function recur(node: ListNode | null, preNode: ListNode | null): void { + if (node.next === null) { + newHead = node; + newHead.next = preNode; + } else { + recur(node.next, node); + node.next = preNode; + } + } + recur(head, null); + return newHead; +}; +``` + +### Ruby: + +```ruby +# 双指针 +# Definition for singly-linked list. +# class ListNode +# attr_accessor :val, :next +# def initialize(val = 0, _next = nil) +# @val = val +# @next = _next +# end +# end +def reverse_list(head) + # return nil if head.nil? # 循环判断条件亦能起到相同作用因此不必单独判断 + cur, per = head, nil + until cur.nil? + tem = cur.next + cur.next = per + per = cur + cur = tem + end + per +end + +# 递归 +# Definition for singly-linked list. +# class ListNode +# attr_accessor :val, :next +# def initialize(val = 0, _next = nil) +# @val = val +# @next = _next +# end +# end +def reverse_list(head) + reverse(nil, head) +end + +def reverse(pre, cur) + return pre if cur.nil? + tem = cur.next + cur.next = pre + reverse(cur, tem) # 通过递归实现双指针法中的更新操作 +end +``` + +### Kotlin: + +```Kotlin +fun reverseList(head: ListNode?): ListNode? { + var pre: ListNode? = null + var cur = head + while (cur != null) { + val temp = cur.next + cur.next = pre + pre = cur + cur = temp + } + return pre +} +``` +```kotlin +/** + * Example: + * var li = ListNode(5) + * var v = li.`val` + * Definition for singly-linked list. + * class ListNode(var `val`: Int) { + * var next: ListNode? = null + * } + */ +class Solution { + fun reverseList(head: ListNode?): ListNode? { + // temp用来存储临时的节点 + var temp: ListNode? + // cur用来遍历链表 + var cur: ListNode? = head + // pre用来作为链表反转的工具 + // pre是比pre前一位的节点 + var pre: ListNode? = null + while (cur != null) { + // 临时存储原本cur的下一个节点 + temp = cur.next + // 使cur下一节点地址为它之前的 + cur.next = pre + // 之后随着cur的遍历移动pre + pre = cur; + // 移动cur遍历链表各个节点 + cur = temp; + } + // 由于开头使用pre为null,所以cur等于链表本身长度+1, + // 此时pre在cur前一位,所以此时pre为头节点 + return pre; + } +} +``` + +### Swift: + +```swift +/// 双指针法 (迭代) +/// - Parameter head: 头结点 +/// - Returns: 翻转后的链表头结点 +func reverseList(_ head: ListNode?) -> ListNode? { + if head == nil || head?.next == nil { + return head + } + var pre: ListNode? = nil + var cur: ListNode? = head + var temp: ListNode? = nil + while cur != nil { + temp = cur?.next + cur?.next = pre + pre = cur + cur = temp + } + return pre +} + +/// 递归 +/// - Parameter head: 头结点 +/// - Returns: 翻转后的链表头结点 +func reverseList2(_ head: ListNode?) -> ListNode? { + return reverse(pre: nil, cur: head) +} +func reverse(pre: ListNode?, cur: ListNode?) -> ListNode? { + if cur == nil { + return pre + } + let temp: ListNode? = cur?.next + cur?.next = pre + return reverse(pre: cur, cur: temp) +} +``` + +### C: +双指针法: + +```c +struct ListNode* reverseList(struct ListNode* head){ + //保存cur的下一个结点 + struct ListNode* temp; + //pre指针指向前一个当前结点的前一个结点 + struct ListNode* pre = NULL; + //用head代替cur,也可以再定义一个cur结点指向head。 + while(head) { + //保存下一个结点的位置 + temp = head->next; + //翻转操作 + head->next = pre; + //更新结点 + pre = head; + head = temp; + } + return pre; +} +``` + +递归法: +```c +struct ListNode* reverse(struct ListNode* pre, struct ListNode* cur) { + if(!cur) + return pre; + struct ListNode* temp = cur->next; + cur->next = pre; + //将cur作为pre传入下一层 + //将temp作为cur传入下一层,改变其指针指向当前cur + return reverse(cur, temp); +} + +struct ListNode* reverseList(struct ListNode* head){ + return reverse(NULL, head); +} +``` + + + +### PHP: + +```php +// 双指针法: +function reverseList($head) { + $cur = $head; + $pre = NULL; + while($cur){ + $temp = $cur->next; + $cur->next = $pre; + $pre = $cur; + $cur = $temp; + } + return $pre; + } +``` + +### Scala: +双指针法: + +```scala +object Solution { + def reverseList(head: ListNode): ListNode = { + var pre: ListNode = null + var cur = head + while (cur != null) { + var tmp = cur.next + cur.next = pre + pre = cur + cur = tmp + } + pre + } +} +``` +递归法: +```scala +object Solution { + def reverseList(head: ListNode): ListNode = { + reverse(null, head) + } + + def reverse(pre: ListNode, cur: ListNode): ListNode = { + if (cur == null) { + return pre // 如果当前cur为空,则返回pre + } + val tmp: ListNode = cur.next + cur.next = pre + reverse(cur, tmp) // 此时cur成为前一个节点,tmp是当前节点 + } + +} +``` + +### Rust: +双指针法: + +```rust +impl Solution { + pub fn reverse_list(head: Option>) -> Option> { + let mut cur = head; + let mut pre = None; + while let Some(mut node) = cur.take() { + cur = node.next; + node.next = pre; + pre = Some(node); + } + pre + } +} +``` + +递归法: + +```rust +impl Solution { + pub fn reverse_list(head: Option>) -> Option> { + fn rev( + mut head: Option>, + mut pre: Option>, + ) -> Option> { + if let Some(mut node) = head.take() { + let cur = node.next; + node.next = pre; + pre = Some(node); + return rev(cur, pre); + } + pre + } + rev(head, None) + } +} +``` +### C#: +三指针法, 感觉会更直观: + +```cs +public LinkNumbers Reverse() +{ + ///用三指针,写的过程中能够弥补二指针在翻转过程中的想象 + LinkNumbers pre = null; + var move = root; + var next = root; + + while (next != null) + { + next = next.linknext; + move.linknext = pre; + pre = move; + move = next; + } + root = pre; + return root; +} + +///LinkNumbers的定义 +public class LinkNumbers +{ + ///

+ /// 链表值 + /// + public int value { get; set; } + + /// + /// 链表指针 + /// + public LinkNumbers linknext { get; set; } +} +``` + +## 其他解法 + +### 使用虚拟头结点解决链表反转 + +> 使用虚拟头结点,通过头插法实现链表的反转(不需要栈) + +```java +// 迭代方法:增加虚头结点,使用头插法实现链表翻转 +public static ListNode reverseList1(ListNode head) { + // 创建虚头结点 + ListNode dumpyHead = new ListNode(-1); + dumpyHead.next = null; + // 遍历所有节点 + ListNode cur = head; + while(cur != null){ + ListNode temp = cur.next; + // 头插法 + cur.next = dumpyHead.next; + dumpyHead.next = cur; + cur = temp; + } + return dumpyHead.next; +} +``` + + + +### 使用栈解决反转链表的问题 + +* 首先将所有的结点入栈 +* 然后创建一个虚拟虚拟头结点,让cur指向虚拟头结点。然后开始循环出栈,每出来一个元素,就把它加入到以虚拟头结点为头结点的链表当中,最后返回即可。 + +```java +public ListNode reverseList(ListNode head) { + // 如果链表为空,则返回空 + if (head == null) return null; + // 如果链表中只有只有一个元素,则直接返回 + if (head.next == null) return head; + // 创建栈 每一个结点都入栈 + Stack stack = new Stack<>(); + ListNode cur = head; + while (cur != null) { + stack.push(cur); + cur = cur.next; + } + // 创建一个虚拟头结点 + ListNode pHead = new ListNode(0); + cur = pHead; + while (!stack.isEmpty()) { + ListNode node = stack.pop(); + cur.next = node; + cur = cur.next; + } + // 最后一个元素的next要赋值为空 + cur.next = null; + return pHead.next; +} +``` + +> 采用这种方法需要注意一点。就是当整个出栈循环结束以后,cur正好指向原来链表的第一个结点,而此时结点1中的next指向的是结点2,因此最后还需要`cur.next = null` +![image-20230117195418626](https://raw.githubusercontent.com/liyuxuan7762/MyImageOSS/master/md_images/image-20230117195418626.png) + diff --git "a/problems/0207.\350\257\276\347\250\213\350\241\250.md" "b/problems/0207.\350\257\276\347\250\213\350\241\250.md" new file mode 100644 index 0000000000..f992c72b89 --- /dev/null +++ "b/problems/0207.\350\257\276\347\250\213\350\241\250.md" @@ -0,0 +1,59 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +拓扑排序指的是一种 解决问题的大体思路, 而具体算法,可能是 广搜 可能是深搜。 + +大家可能发现 各式各样的解法,纠结哪个是拓扑排序? + +只要能在把 有向无环图 进行线性排序 的算法 都可以叫做 拓扑排序。 + +引用与任务调度,课程安排等等。 + + +「拓扑排序」是专门应用于有向图的算法; + +把一个 有向无环图 转成 线性的排序 就叫 拓扑排序。 + +拓扑排序(Kahn 算法,其实就是广度优先遍历的思路) + +这道题的做法同样适用于第 210 题。 + + +```CPP +class Solution { +public: + bool canFinish(int numCourses, vector>& prerequisites) { + vector inDegree(numCourses, 0); + unordered_map> umap; + for (int i = 0; i < prerequisites.size(); i++) { + + // prerequisites[i][0] 是 课程入度,prerequisites[i][1] 是课程出度 + // 即: 上课prerequisites[i][0] 之前,必须先上课prerequisites[i][1] + // prerequisites[i][1] -> prerequisites[i][0] + inDegree[prerequisites[i][0]]++;//当前课程入度值+1 + umap[prerequisites[i][1]].push_back(prerequisites[i][0]); // 添加 prerequisites[i][1] 指向的课程 + } + queue que; + for (int i = 0; i < numCourses; i++) { + if (inDegree[i] == 0) que.push(i); // 所有入度为0,即为 开头课程 加入队列 + } + int count = 0; + while (que.size()) { + int cur = que.front(); //当前选的课 + que.pop(); + count++; // 选课数+1 + vector courses = umap[cur]; //获取这门课指向的课程,也就是这么课的后续课 + if (courses.size()) { // 有后续课 + for (int i = 0; i < courses.size(); i++) { + inDegree[courses[i]]--; // 它的后续课的入度-1 + if (inDegree[courses[i]] == 0) que.push(courses[i]); // 如果入度为0,加入队列 + } + } + } + if (count == numCourses) return true; + return false; + + } +}; +``` diff --git "a/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" "b/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" new file mode 100755 index 0000000000..40917f9b8e --- /dev/null +++ "b/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" @@ -0,0 +1,558 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 209.长度最小的子数组 + +[力扣题目链接](https://leetcode.cn/problems/minimum-size-subarray-sum/) + +给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。 + +示例: + +* 输入:s = 7, nums = [2,3,1,2,4,3] +* 输出:2 +* 解释:子数组 [4,3] 是该条件下的长度最小的子数组。 + +提示: + +* 1 <= target <= 10^9 +* 1 <= nums.length <= 10^5 +* 1 <= nums[i] <= 10^5 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[拿下滑动窗口! | LeetCode 209 长度最小的子数组](https://www.bilibili.com/video/BV1tZ4y1q7XE),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +### 暴力解法 + +这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是O(n^2)。 + +代码如下: + +```CPP +class Solution { +public: + int minSubArrayLen(int s, vector& nums) { + int result = INT32_MAX; // 最终的结果 + int sum = 0; // 子序列的数值之和 + int subLength = 0; // 子序列的长度 + for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i + sum = 0; + for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j + sum += nums[j]; + if (sum >= s) { // 一旦发现子序列和超过了s,更新result + subLength = j - i + 1; // 取子序列的长度 + result = result < subLength ? result : subLength; + break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break + } + } + } + // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 + return result == INT32_MAX ? 0 : result; + } +}; +``` +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + +后面力扣更新了数据,暴力解法已经超时了。 + +### 滑动窗口 + +接下来就开始介绍数组操作中另一个重要的方法:**滑动窗口**。 + +所谓滑动窗口,**就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果**。 + +在暴力解法中,是一个for循环滑动窗口的起始位置,一个for循环为滑动窗口的终止位置,用两个for循环 完成了一个不断搜索区间的过程。 + +那么滑动窗口如何用一个for循环来完成这个操作呢。 + +首先要思考 如果用一个for循环,那么应该表示 滑动窗口的起始位置,还是终止位置。 + +如果只用一个for循环来表示 滑动窗口的起始位置,那么如何遍历剩下的终止位置? + +此时难免再次陷入 暴力解法的怪圈。 + +所以 只用一个for循环,那么这个循环的索引,一定是表示 滑动窗口的终止位置。 + +那么问题来了, 滑动窗口的起始位置如何移动呢? + +这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程: + +![209.长度最小的子数组](https://file1.kamacoder.com/i/algo/209.%E9%95%BF%E5%BA%A6%E6%9C%80%E5%B0%8F%E7%9A%84%E5%AD%90%E6%95%B0%E7%BB%84.gif) + +最后找到 4,3 是最短距离。 + +其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。 + +在本题中实现滑动窗口,主要确定如下三点: + +* 窗口内是什么? +* 如何移动窗口的起始位置? +* 如何移动窗口的结束位置? + +窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。 + +窗口的起始位置如何移动:如果当前窗口的值大于等于s了,窗口就要向前移动了(也就是该缩小了)。 + +窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,也就是for循环里的索引。 + +解题的关键在于 窗口的起始位置如何移动,如图所示: + +![leetcode_209](https://file1.kamacoder.com/i/algo/20210312160441942.png) + +可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)暴力解法降为O(n)。** + +C++代码如下: + +```CPP +class Solution { +public: + int minSubArrayLen(int s, vector& nums) { + int result = INT32_MAX; + int sum = 0; // 滑动窗口数值之和 + int i = 0; // 滑动窗口起始位置 + int subLength = 0; // 滑动窗口的长度 + for (int j = 0; j < nums.size(); j++) { + sum += nums[j]; + // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件 + while (sum >= s) { + subLength = (j - i + 1); // 取子序列的长度 + result = result < subLength ? result : subLength; + sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置) + } + } + // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 + return result == INT32_MAX ? 0 : result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +**一些录友会疑惑为什么时间复杂度是O(n)**。 + +不要以为for里放一个while就以为是O(n^2)啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被操作两次,所以时间复杂度是 2 × n 也就是O(n)。 + +## 相关题目推荐 + +* [904.水果成篮](https://leetcode.cn/problems/fruit-into-baskets/) +* [76.最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) + + + +## 其他语言版本 + +### Java: + +```java +class Solution { + + // 滑动窗口 + public int minSubArrayLen(int s, int[] nums) { + int left = 0; + int sum = 0; + int result = Integer.MAX_VALUE; + for (int right = 0; right < nums.length; right++) { + sum += nums[right]; + while (sum >= s) { + result = Math.min(result, right - left + 1); + sum -= nums[left++]; + } + } + return result == Integer.MAX_VALUE ? 0 : result; + } +} +``` + +### Python: + +```python +(版本一)滑动窗口法 +class Solution: + def minSubArrayLen(self, s: int, nums: List[int]) -> int: + l = len(nums) + left = 0 + right = 0 + min_len = float('inf') + cur_sum = 0 #当前的累加值 + + while right < l: + cur_sum += nums[right] + + while cur_sum >= s: # 当前累加值大于目标值 + min_len = min(min_len, right - left + 1) + cur_sum -= nums[left] + left += 1 + + right += 1 + + return min_len if min_len != float('inf') else 0 +``` + +```python +(版本二)暴力法 +class Solution: + def minSubArrayLen(self, s: int, nums: List[int]) -> int: + l = len(nums) + min_len = float('inf') + + for i in range(l): + cur_sum = 0 + for j in range(i, l): + cur_sum += nums[j] + if cur_sum >= s: + min_len = min(min_len, j - i + 1) + break + + return min_len if min_len != float('inf') else 0 +``` + +### Go: + +```go +func minSubArrayLen(target int, nums []int) int { + i := 0 + l := len(nums) // 数组长度 + sum := 0 // 子数组之和 + result := l + 1 // 初始化返回长度为l+1,目的是为了判断“不存在符合条件的子数组,返回0”的情况 + for j := 0; j < l; j++ { + sum += nums[j] + for sum >= target { + subLength := j - i + 1 + if subLength < result { + result = subLength + } + sum -= nums[i] + i++ + } + } + if result == l+1 { + return 0 + } else { + return result + } +} +``` + +### JavaScript: + +```js +var minSubArrayLen = function(target, nums) { + let start, end + start = end = 0 + let sum = 0 + let len = nums.length + let ans = Infinity + + while(end < len){ + sum += nums[end]; + while (sum >= target) { + ans = Math.min(ans, end - start + 1); + sum -= nums[start]; + start++; + } + end++; + } + return ans === Infinity ? 0 : ans +}; +``` + +### TypeScript: + +```typescript +function minSubArrayLen(target: number, nums: number[]): number { + let left: number = 0, + res: number = Infinity, + subLen: number = 0, + sum: number = 0; + for (let right: number = 0; right < nums.length; right++) { + sum += nums[right]; + while (sum >= target) { + subLen = right - left + 1; + res = Math.min(res, subLen); + sum -= nums[left]; + left++; + } + } + return res === Infinity ? 0 : res; +} +``` + +### Swift: + +```swift +func minSubArrayLen(_ target: Int, _ nums: [Int]) -> Int { + var result = Int.max + var sum = 0 + var starIndex = 0 + for endIndex in 0..= target { + result = min(result, endIndex - starIndex + 1) + sum -= nums[starIndex] + starIndex += 1 + } + } + + return result == Int.max ? 0 : result +} +``` + +### Rust: + +```rust +impl Solution { + pub fn min_sub_array_len(target: i32, nums: Vec) -> i32 { + let (mut result, mut subLength): (i32, i32) = (i32::MAX, 0); + let (mut sum, mut i) = (0, 0); + + for (pos, val) in nums.iter().enumerate() { + sum += val; + while sum >= target { + subLength = (pos - i + 1) as i32; + if result > subLength { + result = subLength; + } + sum -= nums[i]; + i += 1; + } + } + if result == i32::MAX { + return 0; + } + result + } +} +``` + +### PHP: + +```php +// 双指针 - 滑动窗口 +class Solution { + /** + * @param Integer $target + * @param Integer[] $nums + * @return Integer + */ + function minSubArrayLen($target, $nums) { + if (count($nums) < 1) { + return 0; + } + $sum = 0; + $res = PHP_INT_MAX; + $left = 0; + for ($right = 0; $right < count($nums); $right++) { + $sum += $nums[$right]; + while ($sum >= $target) { + $res = min($res, $right - $left + 1); + $sum -= $nums[$left]; + $left++; + } + } + return $res == PHP_INT_MAX ? 0 : $res; + } +} +``` + +### Ruby: + +```ruby +def min_sub_array_len(target, nums) + res = Float::INFINITY # 无穷大 + i, sum = 0, 0 + nums.length.times do |j| + sum += nums[j] + while sum >= target + res = [res, j - i + 1].min + sum -= nums[i] + i += 1 + end + end + res == Float::INFINITY ? 0 : res +end +``` + +### C: +暴力解法: + +```c +int minSubArrayLen(int target, int* nums, int numsSize){ + //初始化最小长度为INT_MAX + int minLength = INT_MAX; + int sum; + + int left, right; + for(left = 0; left < numsSize; ++left) { + //每次遍历都清零sum,计算当前位置后和>=target的子数组的长度 + sum = 0; + //从left开始,sum中添加元素 + for(right = left; right < numsSize; ++right) { + sum += nums[right]; + //若加入当前元素后,和大于target,则更新minLength + if(sum >= target) { + int subLength = right - left + 1; + minLength = minLength < subLength ? minLength : subLength; + } + } + } + //若minLength不为INT_MAX,则返回minLnegth + return minLength == INT_MAX ? 0 : minLength; +} +``` + +滑动窗口: +```c +int minSubArrayLen(int target, int* nums, int numsSize){ + //初始化最小长度为INT_MAX + int minLength = INT_MAX; + int sum = 0; + + int left = 0, right = 0; + //右边界向右扩展 + for(; right < numsSize; ++right) { + sum += nums[right]; + //当sum的值大于等于target时,保存长度,并且收缩左边界 + while(sum >= target) { + int subLength = right - left + 1; + minLength = minLength < subLength ? minLength : subLength; + sum -= nums[left++]; + } + } + //若minLength不为INT_MAX,则返回minLnegth + return minLength == INT_MAX ? 0 : minLength; +} +``` + +### Kotlin: + +```kotlin +class Solution { + fun minSubArrayLen(target: Int, nums: IntArray): Int { + var start = 0 + var end = 0 + var ret = Int.MAX_VALUE + var count = 0 + while (end < nums.size) { + count += nums[end] + while (count >= target) { + ret = if (ret > (end - start + 1)) end - start + 1 else ret + count -= nums[start++] + } + end++ + } + return if (ret == Int.MAX_VALUE) 0 else ret + } +} +``` +滑动窗口 +```kotlin +class Solution { + fun minSubArrayLen(target: Int, nums: IntArray): Int { + // 左边界 和 右边界 + var left: Int = 0 + var right: Int = 0 + // sum 用来记录和 + var sum: Int = 0 + // result记录一个固定值,便于判断是否存在的这样的数组 + var result: Int = Int.MAX_VALUE + // subLenth记录长度 + var subLength = Int.MAX_VALUE + + + while (right < nums.size) { + // 从数组首元素开始逐次求和 + sum += nums[right++] + // 判断 + while (sum >= target) { + var temp = right - left + // 每次和上一次比较求出最小数组长度 + subLength = if (subLength > temp) temp else subLength + // sum减少,左边界右移 + sum -= nums[left++] + } + } + // 如果subLength为初始值,则说明长度为0,否则返回subLength + return if(subLength == result) 0 else subLength + } +} +``` +### Scala: + +滑动窗口: +```scala +object Solution { + def minSubArrayLen(target: Int, nums: Array[Int]): Int = { + var result = Int.MaxValue // 返回结果,默认最大值 + var left = 0 // 慢指针,当sum>=target,向右移动 + var sum = 0 // 窗口值的总和 + for (right <- 0 until nums.length) { + sum += nums(right) + while (sum >= target) { + result = math.min(result, right - left + 1) // 产生新结果 + sum -= nums(left) // 左指针移动,窗口总和减去左指针的值 + left += 1 // 左指针向右移动 + } + } + // 相当于三元运算符,return关键字可以省略 + if (result == Int.MaxValue) 0 else result + } +} +``` + +暴力解法: +```scala +object Solution { + def minSubArrayLen(target: Int, nums: Array[Int]): Int = { + import scala.util.control.Breaks + var res = Int.MaxValue + var subLength = 0 + for (i <- 0 until nums.length) { + var sum = 0 + Breaks.breakable( + for (j <- i until nums.length) { + sum += nums(j) + if (sum >= target) { + subLength = j - i + 1 + res = math.min(subLength, res) + Breaks.break() + } + } + ) + } + // 相当于三元运算符 + if (res == Int.MaxValue) 0 else res + } +} +``` +### C#: + +```csharp +public class Solution { + public int MinSubArrayLen(int s, int[] nums) { + int n = nums.Length; + int ans = int.MaxValue; + int start = 0, end = 0; + int sum = 0; + while (end < n) { + sum += nums[end]; + while (sum >= s) + { + ans = Math.Min(ans, end - start + 1); + sum -= nums[start]; + start++; + } + end++; + } + return ans == int.MaxValue ? 0 : ans; + } +} +``` diff --git "a/problems/0210.\350\257\276\347\250\213\350\241\250II.md" "b/problems/0210.\350\257\276\347\250\213\350\241\250II.md" new file mode 100644 index 0000000000..b0d9fe8a9e --- /dev/null +++ "b/problems/0210.\350\257\276\347\250\213\350\241\250II.md" @@ -0,0 +1,42 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +```CPP +class Solution { +public: + vector findOrder(int numCourses, vector>& prerequisites) { + vector inDegree(numCourses, 0); + vector result; + unordered_map> umap; + for (int i = 0; i < prerequisites.size(); i++) { + + // prerequisites[i][0] 是 课程入度,prerequisites[i][1] 是课程出度 + // 即: 上课prerequisites[i][0] 之前,必须先上课prerequisites[i][1] + // prerequisites[i][1] -> prerequisites[i][0] + inDegree[prerequisites[i][0]]++;//当前课程入度值+1 + umap[prerequisites[i][1]].push_back(prerequisites[i][0]); // 添加 prerequisites[i][1] 指向的课程 + } + queue que; + for (int i = 0; i < numCourses; i++) { + if (inDegree[i] == 0) que.push(i); // 所有入度为0,即为 开头课程 加入队列 + } + int count = 0; + while (que.size()) { + int cur = que.front(); //当前选的课 + que.pop(); + count++; // 选课数+1 + result.push_back(cur); + vector courses = umap[cur]; //获取这门课指向的课程,也就是这么课的后续课 + if (courses.size()) { // 有后续课 + for (int i = 0; i < courses.size(); i++) { + inDegree[courses[i]]--; // 它的后续课的入度-1 + if (inDegree[courses[i]] == 0) que.push(courses[i]); // 如果入度为0,加入队列 + } + } + } + if (count == numCourses) return result; + else return vector(); + } +}; +``` diff --git "a/problems/0213.\346\211\223\345\256\266\345\212\253\350\210\215II.md" "b/problems/0213.\346\211\223\345\256\266\345\212\253\350\210\215II.md" new file mode 100755 index 0000000000..6f2fdd0610 --- /dev/null +++ "b/problems/0213.\346\211\223\345\256\266\345\212\253\350\210\215II.md" @@ -0,0 +1,367 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 213.打家劫舍II + +[力扣题目链接](https://leetcode.cn/problems/house-robber-ii/) + +你是一个专业的小偷,计划偷窃沿街的房屋,每间房内都藏有一定的现金。这个地方所有的房屋都 围成一圈 ,这意味着第一个房屋和最后一个房屋是紧挨着的。同时,相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警 。 + +给定一个代表每个房屋存放金额的非负整数数组,计算你 在不触动警报装置的情况下 ,能够偷窃到的最高金额。 + +示例 1: + +* 输入:nums = [2,3,2] +* 输出:3 +* 解释:你不能先偷窃 1 号房屋(金额 = 2),然后偷窃 3 号房屋(金额 = 2), 因为他们是相邻的。 + +* 示例 2: +* 输入:nums = [1,2,3,1] +* 输出:4 +* 解释:你可以先偷窃 1 号房屋(金额 = 1),然后偷窃 3 号房屋(金额 = 3)。偷窃到的最高金额 = 1 + 3 = 4 。 + +* 示例 3: +* 输入:nums = [0] +* 输出:0 + +提示: +* 1 <= nums.length <= 100 +* 0 <= nums[i] <= 1000 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,房间连成环了那还偷不偷呢?| LeetCode:213.打家劫舍II](https://www.bilibili.com/video/BV1oM411B7xq),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目和[198.打家劫舍](https://programmercarl.com/0198.打家劫舍.html)是差不多的,唯一区别就是成环了。 + +对于一个数组,成环的话主要有如下三种情况: + +* 情况一:考虑不包含首尾元素 + +![213.打家劫舍II](https://file1.kamacoder.com/i/algo/20210129160748643-20230310134000692.jpg) + +* 情况二:考虑包含首元素,不包含尾元素 + +![213.打家劫舍II1](https://file1.kamacoder.com/i/algo/20210129160821374-20230310134003961.jpg) + +* 情况三:考虑包含尾元素,不包含首元素 + +![213.打家劫舍II2](https://file1.kamacoder.com/i/algo/20210129160842491-20230310134008133.jpg) + +**注意我这里用的是"考虑"**,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。 + +**而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了**。 + +分析到这里,本题其实比较简单了。 剩下的和[198.打家劫舍](https://programmercarl.com/0198.打家劫舍.html)就是一样的了。 + +代码如下: + +```CPP +// 注意注释中的情况二情况三,以及把198.打家劫舍的代码抽离出来了 +class Solution { +public: + int rob(vector& nums) { + if (nums.size() == 0) return 0; + if (nums.size() == 1) return nums[0]; + int result1 = robRange(nums, 0, nums.size() - 2); // 情况二 + int result2 = robRange(nums, 1, nums.size() - 1); // 情况三 + return max(result1, result2); + } + // 198.打家劫舍的逻辑 + int robRange(vector& nums, int start, int end) { + if (end == start) return nums[start]; + vector dp(nums.size()); + dp[start] = nums[start]; + dp[start + 1] = max(nums[start], nums[start + 1]); + for (int i = start + 2; i <= end; i++) { + dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); + } + return dp[end]; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(n) + + + +## 总结 + +成环之后还是难了一些的, 不少题解没有把“考虑房间”和“偷房间”说清楚。 + +这就导致大家会有这样的困惑:情况三怎么就包含了情况一了呢? 本文图中最后一间房不能偷啊,偷了一定不是最优结果。 + +所以我在本文重点强调了情况一二三是“考虑”的范围,而具体房间偷与不偷交给递推公式去抉择。 + +这样大家就不难理解情况二和情况三包含了情况一了。 + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int rob(int[] nums) { + if (nums == null || nums.length == 0) + return 0; + int len = nums.length; + if (len == 1) + return nums[0]; + return Math.max(robAction(nums, 0, len - 1), robAction(nums, 1, len)); + } + + int robAction(int[] nums, int start, int end) { + int x = 0, y = 0, z = 0; + for (int i = start; i < end; i++) { + y = z; + z = Math.max(y, x + nums[i]); + x = y; + } + return z; + } +} +``` + +### Python: + +```Python +class Solution: + def rob(self, nums: List[int]) -> int: + if len(nums) == 0: + return 0 + if len(nums) == 1: + return nums[0] + + result1 = self.robRange(nums, 0, len(nums) - 2) # 情况二 + result2 = self.robRange(nums, 1, len(nums) - 1) # 情况三 + return max(result1, result2) + # 198.打家劫舍的逻辑 + def robRange(self, nums: List[int], start: int, end: int) -> int: + if end == start: + return nums[start] + + prev_max = nums[start] + curr_max = max(nums[start], nums[start + 1]) + + for i in range(start + 2, end + 1): + temp = curr_max + curr_max = max(prev_max + nums[i], curr_max) + prev_max = temp + + return curr_max + +``` +2维DP +```python +class Solution: + def rob(self, nums: List[int]) -> int: + if len(nums) < 3: + return max(nums) + + # 情况二:不抢劫第一个房屋 + result1 = self.robRange(nums[:-1]) + + # 情况三:不抢劫最后一个房屋 + result2 = self.robRange(nums[1:]) + + return max(result1, result2) + + def robRange(self, nums): + dp = [[0, 0] for _ in range(len(nums))] + dp[0][1] = nums[0] + + for i in range(1, len(nums)): + dp[i][0] = max(dp[i - 1]) + dp[i][1] = dp[i - 1][0] + nums[i] + + return max(dp[-1]) + + + +``` + +优化版 +```python +class Solution: + def rob(self, nums: List[int]) -> int: + if not nums: # 如果没有房屋,返回0 + return 0 + + if len(nums) == 1: # 如果只有一个房屋,返回该房屋的金额 + return nums[0] + + # 情况二:不抢劫第一个房屋 + prev_max = 0 # 上一个房屋的最大金额 + curr_max = 0 # 当前房屋的最大金额 + for num in nums[1:]: + temp = curr_max # 临时变量保存当前房屋的最大金额 + curr_max = max(prev_max + num, curr_max) # 更新当前房屋的最大金额 + prev_max = temp # 更新上一个房屋的最大金额 + result1 = curr_max + + # 情况三:不抢劫最后一个房屋 + prev_max = 0 # 上一个房屋的最大金额 + curr_max = 0 # 当前房屋的最大金额 + for num in nums[:-1]: + temp = curr_max # 临时变量保存当前房屋的最大金额 + curr_max = max(prev_max + num, curr_max) # 更新当前房屋的最大金额 + prev_max = temp # 更新上一个房屋的最大金额 + result2 = curr_max + + return max(result1, result2) + + +``` +### Go: + +```go +// 打家劫舍Ⅱ 动态规划 +// 时间复杂度O(n) 空间复杂度O(n) +func rob(nums []int) int { + // 如果长度为0或1,那么有没有环的限制都一样 + if len(nums) <= 1 { + return robWithoutCircle(nums) + } + + // 否则,去头或去尾,取最大 + res1 := robWithoutCircle(nums[:len(nums)-1]) + res2 := robWithoutCircle(nums[1:]) + + return max(res1, res2) +} + +// 原始的打家劫舍版 +func robWithoutCircle(nums []int) int { + switch len(nums) { + case 0: return 0 + case 1: return nums[0] + } + dp := make([]int, len(nums)) + dp[0]=nums[0] + dp[1] = max(nums[0], nums[1]) + + for i:=2; ib { + return a + } + return b +} +``` + +### JavaScript: + +```javascript +var rob = function(nums) { + const n = nums.length + if (n === 0) return 0 + if (n === 1) return nums[0] + const result1 = robRange(nums, 0, n - 2) + const result2 = robRange(nums, 1, n - 1) + return Math.max(result1, result2) +}; + +const robRange = (nums, start, end) => { + if (end === start) return nums[start] + const dp = Array(nums.length).fill(0) + dp[start] = nums[start] + dp[start + 1] = Math.max(nums[start], nums[start + 1]) + for (let i = start + 2; i <= end; i++) { + dp[i] = Math.max(dp[i - 2] + nums[i], dp[i - 1]) + } + return dp[end] +} +``` +### TypeScript: + +```typescript +function rob(nums: number[]): number { + const length: number = nums.length; + if (length === 0) return 0; + if (length === 1) return nums[0]; + return Math.max(robRange(nums, 0, length - 2), + robRange(nums, 1, length - 1)); +}; +function robRange(nums: number[], start: number, end: number): number { + if (start === end) return nums[start]; + const dp: number[] = []; + dp[start] = nums[start]; + dp[start + 1] = Math.max(nums[start], nums[start + 1]); + for (let i = start + 2; i <= end; i++) { + dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]); + } + return dp[end]; +} +``` + +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +// 198.打家劫舍的逻辑 +int robRange(int* nums, int start, int end, int numsSize) { + if (end == start) return nums[start]; + int dp[numsSize]; + dp[start] = nums[start]; + dp[start + 1] = max(nums[start], nums[start + 1]); + for (int i = start + 2; i <= end; i++) { + dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); + } + return dp[end]; +} + +int rob(int* nums, int numsSize) { + if (numsSize == 0) return 0; + if (numsSize == 1) return nums[0]; + int result1 = robRange(nums, 0, numsSize - 2, numsSize); // 情况二 + int result2 = robRange(nums, 1, numsSize - 1, numsSize); // 情况三 + return max(result1, result2); +} +``` + + + +### Rust: + +```rust +impl Solution { + pub fn rob(nums: Vec) -> i32 { + match nums.len() { + 1 => nums[0], + _ => Self::rob_range(&nums, 0, nums.len() - 2).max(Self::rob_range( + &nums, + 1, + nums.len() - 1, + )), + } + } + + pub fn rob_range(nums: &Vec, start: usize, end: usize) -> i32 { + if start == end { + return nums[start]; + } + let mut dp = vec![0; nums.len()]; + dp[start] = nums[start]; + dp[start + 1] = nums[start].max(nums[start + 1]); + for i in start + 2..=end { + dp[i] = dp[i - 1].max(dp[i - 2] + nums[i]); + } + dp[end] + } +} +``` + + diff --git "a/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" "b/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" new file mode 100755 index 0000000000..5ef5b5e67c --- /dev/null +++ "b/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" @@ -0,0 +1,739 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +> 别看本篇选的是组合总和III,而不是组合总和,本题和上一篇77.组合相比难度刚刚好! + +# 216.组合总和III + +[力扣题目链接](https://leetcode.cn/problems/combination-sum-iii/) + +找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。 + +说明: + +* 所有数字都是正整数。 +* 解集不能包含重复的组合。 + +示例 1: +输入: k = 3, n = 7 +输出: [[1,2,4]] + +示例 2: +输入: k = 3, n = 9 +输出: [[1,2,6], [1,3,5], [2,3,4]] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[和组合问题有啥区别?回溯算法如何剪枝?| LeetCode:216.组合总和III](https://www.bilibili.com/video/BV1wg411873x),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。 + +相对于[77. 组合](https://programmercarl.com/0077.组合.html),无非就是多了一个限制,本题是要找到和为n的k个数的组合,而整个集合已经是固定的了[1,...,9]。 + +想到这一点了,做过[77. 组合](https://programmercarl.com/0077.组合.html)之后,本题是简单一些了。 + +本题k相当于树的深度,9(因为整个集合就是9个数)就是树的宽度。 + +例如 k = 2,n = 4的话,就是在集合[1,2,3,4,5,6,7,8,9]中求 k(个数) = 2, n(和) = 4的组合。 + +选取过程如图: + +![216.组合总和III](https://file1.kamacoder.com/i/algo/20201123195717975.png) + +图中,可以看出,只有最后取到集合(1,3)和为4 符合条件。 + + +### 回溯三部曲 + +* **确定递归函数参数** + +和[77. 组合](https://programmercarl.com/0077.组合.html)一样,依然需要一维数组path来存放符合条件的结果,二维数组result来存放结果集。 + +这里我依然定义path 和 result为全局变量。 + +至于为什么取名为path?从上面树形结构中,可以看出,结果其实就是一条根节点到叶子节点的路径。 + +```cpp +vector> result; // 存放结果集 +vector path; // 符合条件的结果 +``` + +接下来还需要如下参数: + +* targetSum(int)目标和,也就是题目中的n。 +* k(int)就是题目中要求k个数的集合。 +* sum(int)为已经收集的元素的总和,也就是path里元素的总和。 +* startIndex(int)为下一层for循环搜索的起始位置。 + +所以代码如下: + +```cpp +vector> result; +vector path; +void backtracking(int targetSum, int k, int sum, int startIndex) +``` + +其实这里sum这个参数也可以省略,每次targetSum减去选取的元素数值,然后判断如果targetSum为0了,说明收集到符合条件的结果了,我这里为了直观便于理解,还是加一个sum参数。 + +还要强调一下,回溯法中递归函数参数很难一次性确定下来,一般先写逻辑,需要啥参数了,填什么参数。 + +* 确定终止条件 + +什么时候终止呢? + +在上面已经说了,k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。 + +所以如果path.size() 和 k相等了,就终止。 + +如果此时path里收集到的元素和(sum) 和targetSum(就是题目描述的n)相同了,就用result收集当前的结果。 + +所以 终止代码如下: + +```CPP +if (path.size() == k) { + if (sum == targetSum) result.push_back(path); + return; // 如果path.size() == k 但sum != targetSum 直接返回 +} +``` + +* **单层搜索过程** + +本题和[77. 组合](https://programmercarl.com/0077.组合.html)区别之一就是集合固定的就是9个数[1,...,9],所以for循环固定i<=9 + +如图: +![216.组合总和III](https://file1.kamacoder.com/i/algo/20201123195717975-20230310113546003.png) + +处理过程就是 path收集每次选取的元素,相当于树型结构里的边,sum来统计path里元素的总和。 + +代码如下: + +```CPP +for (int i = startIndex; i <= 9; i++) { + sum += i; + path.push_back(i); + backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex + sum -= i; // 回溯 + path.pop_back(); // 回溯 +} +``` + +**别忘了处理过程 和 回溯过程是一一对应的,处理有加,回溯就要有减!** + +参照[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中的模板,不难写出如下C++代码: + +```CPP +class Solution { +private: + vector> result; // 存放结果集 + vector path; // 符合条件的结果 + // targetSum:目标和,也就是题目中的n。 + // k:题目中要求k个数的集合。 + // sum:已经收集的元素的总和,也就是path里元素的总和。 + // startIndex:下一层for循环搜索的起始位置。 + void backtracking(int targetSum, int k, int sum, int startIndex) { + if (path.size() == k) { + if (sum == targetSum) result.push_back(path); + return; // 如果path.size() == k 但sum != targetSum 直接返回 + } + for (int i = startIndex; i <= 9; i++) { + sum += i; // 处理 + path.push_back(i); // 处理 + backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex + sum -= i; // 回溯 + path.pop_back(); // 回溯 + } + } + +public: + vector> combinationSum3(int k, int n) { + result.clear(); // 可以不加 + path.clear(); // 可以不加 + backtracking(n, k, 0, 1); + return result; + } +}; +``` + +### 剪枝 + +这道题目,剪枝操作其实是很容易想到了,想必大家看上面的树形图的时候已经想到了。 + +如图: +![216.组合总和III1](https://file1.kamacoder.com/i/algo/2020112319580476.png) + +已选元素总和如果已经大于n(图中数值为4)了,那么往后遍历就没有意义了,直接剪掉。 + +那么剪枝的地方可以放在递归函数开始的地方,剪枝代码如下: + +```cpp +if (sum > targetSum) { // 剪枝操作 + return; +} +``` + +当然这个剪枝也可以放在 调用递归之前,即放在这里,只不过要记得 要回溯操作给做了。 + +```CPP +for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝 + sum += i; // 处理 + path.push_back(i); // 处理 + if (sum > targetSum) { // 剪枝操作 + sum -= i; // 剪枝之前先把回溯做了 + path.pop_back(); // 剪枝之前先把回溯做了 + return; + } + backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex + sum -= i; // 回溯 + path.pop_back(); // 回溯 +} +``` + + +和[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html) 一样,for循环的范围也可以剪枝,i <= 9 - (k - path.size()) + 1就可以了。 + +最后C++代码如下: + +```CPP +class Solution { +private: + vector> result; // 存放结果集 + vector path; // 符合条件的结果 + void backtracking(int targetSum, int k, int sum, int startIndex) { + if (sum > targetSum) { // 剪枝操作 + return; + } + if (path.size() == k) { + if (sum == targetSum) result.push_back(path); + return; // 如果path.size() == k 但sum != targetSum 直接返回 + } + for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { // 剪枝 + sum += i; // 处理 + path.push_back(i); // 处理 + backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex + sum -= i; // 回溯 + path.pop_back(); // 回溯 + } + } + +public: + vector> combinationSum3(int k, int n) { + result.clear(); // 可以不加 + path.clear(); // 可以不加 + backtracking(n, k, 0, 1); + return result; + } +}; +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n) + +## 总结 + +开篇就介绍了本题与[77.组合](https://programmercarl.com/0077.组合.html)的区别,相对来说加了元素总和的限制,如果做完[77.组合](https://programmercarl.com/0077.组合.html)再做本题在合适不过。 + +分析完区别,依然把问题抽象为树形结构,按照回溯三部曲进行讲解,最后给出剪枝的优化。 + +相信做完本题,大家对组合问题应该有初步了解了。 + + + + +## 其他语言版本 + + +### Java + +模板方法 + +```java +class Solution { + List> result = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + + public List> combinationSum3(int k, int n) { + backTracking(n, k, 1, 0); + return result; + } + + private void backTracking(int targetSum, int k, int startIndex, int sum) { + // 减枝 + if (sum > targetSum) { + return; + } + + if (path.size() == k) { + if (sum == targetSum) result.add(new ArrayList<>(path)); + return; + } + + // 减枝 9 - (k - path.size()) + 1 + for (int i = startIndex; i <= 9 - (k - path.size()) + 1; i++) { + path.add(i); + sum += i; + backTracking(targetSum, k, i + 1, sum); + //回溯 + path.removeLast(); + //回溯 + sum -= i; + } + } +} + +// 上面剪枝 i <= 9 - (k - path.size()) + 1; 如果还是不清楚 +// 也可以改为 if (path.size() > k) return; 执行效率上是一样的 +class Solution { + LinkedList path = new LinkedList<>(); + List> ans = new ArrayList<>(); + public List> combinationSum3(int k, int n) { + build(k, n, 1, 0); + return ans; + } + + private void build(int k, int n, int startIndex, int sum) { + + if (sum > n) return; + + if (path.size() > k) return; + + if (sum == n && path.size() == k) { + ans.add(new ArrayList<>(path)); + return; + } + + for(int i = startIndex; i <= 9; i++) { + path.add(i); + sum += i; + build(k, n, i + 1, sum); + sum -= i; + path.removeLast(); + } + } +} +``` + +其他方法 + +```java +class Solution { + List> res = new ArrayList<>(); + List list = new ArrayList<>(); + + public List> combinationSum3(int k, int n) { + res.clear(); + list.clear(); + backtracking(k, n, 9); + return res; + } + + private void backtracking(int k, int n, int maxNum) { + if (k == 0 && n == 0) { + res.add(new ArrayList<>(list)); + return; + } + + // 因为不能重复,并且单个数字最大值是maxNum,所以sum最大值为 + // (maxNum + (maxNum - 1) + ... + (maxNum - k + 1)) == k * maxNum - k*(k - 1) / 2 + if (maxNum == 0 + || n > k * maxNum - k * (k - 1) / 2 + || n < (1 + k) * k / 2) { + return; + } + list.add(maxNum); + backtracking(k - 1, n - maxNum, maxNum - 1); + list.remove(list.size() - 1); + backtracking(k, n, maxNum - 1); + } + +} +``` + +### Python + +```py +class Solution: + def combinationSum3(self, k: int, n: int) -> List[List[int]]: + result = [] # 存放结果集 + self.backtracking(n, k, 0, 1, [], result) + return result + + def backtracking(self, targetSum, k, currentSum, startIndex, path, result): + if currentSum > targetSum: # 剪枝操作 + return # 如果currentSum已经超过targetSum,则直接返回 + if len(path) == k: + if currentSum == targetSum: + result.append(path[:]) + return + for i in range(startIndex, 9 - (k - len(path)) + 2): # 剪枝 + currentSum += i # 处理 + path.append(i) # 处理 + self.backtracking(targetSum, k, currentSum, i + 1, path, result) # 注意i+1调整startIndex + currentSum -= i # 回溯 + path.pop() # 回溯 + +``` + +### Go + +回溯+减枝 + +```go +var ( + res [][]int + path []int +) +func combinationSum3(k int, n int) [][]int { + res, path = make([][]int, 0), make([]int, 0, k) + dfs(k, n, 1, 0) + return res +} + +func dfs(k, n int, start int, sum int) { + if len(path) == k { + if sum == n { + tmp := make([]int, k) + copy(tmp, path) + res = append(res, tmp) + } + return + } + for i := start; i <= 9; i++ { + if sum + i > n || 9-i+1 < k-len(path) { + break + } + path = append(path, i) + dfs(k, n, i+1, sum+i) + path = path[:len(path)-1] + } +} +``` + +### JavaScript +- 未剪枝: + +```js +/** + * @param {number} k + * @param {number} n + * @return {number[][]} + */ +var combinationSum3 = function (k, n) { + // 回溯法 + let result = [], + path = []; + const backtracking = (_k, targetSum, sum, startIndex) => { + // 终止条件 + if (path.length === _k) { + if (sum === targetSum) { + result.push(path.slice()); + } + // 如果总和不相等,就直接返回 + return; + } + + // 循环当前节点,因为只使用数字1到9,所以最大是9 + for (let i = startIndex; i <= 9; i++) { + path.push(i); + sum += i; + // 回调函数 + backtracking(_k, targetSum, sum, i + 1); + // 回溯 + sum -= i; + path.pop(); + } + }; + backtracking(k, n, 0, 1); + return result; +}; +``` + +- 剪枝: + +```js +/** + * @param {number} k + * @param {number} n + * @return {number[][]} + */ +var combinationSum3 = function (k, n) { + // 回溯法 + let result = [], + path = []; + const backtracking = (_k, targetSum, sum, startIndex) => { + if (sum > targetSum) { + return; + } + // 终止条件 + if (path.length === _k) { + if (sum === targetSum) { + result.push(path.slice()); + } + // 如果总和不相等,就直接返回 + return; + } + + // 循环当前节点,因为只使用数字1到9,所以最大是9 + for (let i = startIndex; i <= 9 - (_k - path.length) + 1; i++) { + path.push(i); + sum += i; + // 回调函数 + backtracking(_k, targetSum, sum, i + 1); + // 回溯 + sum -= i; + path.pop(); + } + }; + backtracking(k, n, 0, 1); + return result; +}; +``` + +### TypeScript + +```typescript +function combinationSum3(k: number, n: number): number[][] { + const resArr: number[][] = []; + function backTracking(k: number, n: number, sum: number, startIndex: number, tempArr: number[]): void { + if (sum > n) return; + if (tempArr.length === k) { + if (sum === n) { + resArr.push(tempArr.slice()); + } + return; + } + for (let i = startIndex; i <= 9 - (k - tempArr.length) + 1; i++) { + tempArr.push(i); + backTracking(k, n, sum + i, i + 1, tempArr); + tempArr.pop(); + } + } + backTracking(k, n, 0, 1, []); + return resArr; +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn combination_sum3(k: i32, n: i32) -> Vec> { + let mut result = vec![]; + let mut path = vec![]; + Self::backtrace(&mut result, &mut path, n, k, 0, 1); + result + } + pub fn backtrace( + result: &mut Vec>, + path: &mut Vec, + target_sum: i32, + k: i32, + sum: i32, + start_index: i32, + ) { + if sum > target_sum { + return; + } + let len = path.len() as i32; + if len == k { + if sum == target_sum { + result.push(path.to_vec()); + } + return; + } + for i in start_index..=9 - (k - len) + 1 { + path.push(i); + Self::backtrace(result, path, target_sum, k, sum + i, i + 1); + path.pop(); + } + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; +int getPathSum() { + int i; + int sum = 0; + for(i = 0; i < pathTop; i++) { + sum += path[i]; + } + return sum; +} + +void backtracking(int targetSum, int k, int sum, int startIndex) { + if(pathTop == k) { + if(sum == targetSum) { + int* tempPath = (int*)malloc(sizeof(int) * k); + int j; + for(j = 0; j < k; j++) + tempPath[j] = path[j]; + ans[ansTop++] = tempPath; + } + // 如果path.size() == k 但sum != targetSum 直接返回 + return; + } + int i; + //从startIndex开始遍历,一直遍历到9 + for (i = startIndex; i <= 9; i++) { + sum += i; // 处理 + path[pathTop++] = i; // 处理 + backtracking(targetSum, k, sum, i + 1); // 注意i+1调整startIndex + sum -= i; // 回溯 + pathTop--;; // 回溯 + } +} + +int** combinationSum3(int k, int n, int* returnSize, int** returnColumnSizes){ + //初始化辅助变量 + path = (int*)malloc(sizeof(int) * k); + ans = (int**)malloc(sizeof(int*) * 20); + pathTop = ansTop = 0; + + backtracking(n, k, 0, 1); + + //设置返回的二维数组中元素个数为ansTop + *returnSize = ansTop; + //设置二维数组中每个元素个数的大小为k + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; i++) { + (*returnColumnSizes)[i] = k; + } + return ans; +} +``` + +### Swift + +```swift +func combinationSum3(_ count: Int, _ targetSum: Int) -> [[Int]] { + var result = [[Int]]() + var path = [Int]() + func backtracking(sum: Int, start: Int) { + // 剪枝 + if sum > targetSum { return } + // 终止条件 + if path.count == count { + if sum == targetSum { + result.append(path) + } + return + } + + // 单层逻辑 + let end = 9 + guard start <= end else { return } + for i in start ... end { + path.append(i) // 处理 + backtracking(sum: sum + i, start: i + 1) + path.removeLast() // 回溯 + } + } + + backtracking(sum: 0, start: 1) + return result +} +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def combinationSum3(k: Int, n: Int): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + + def backtracking(k: Int, n: Int, sum: Int, startIndex: Int): Unit = { + if (sum > n) return // 剪枝,如果sum>目标和,就返回 + if (sum == n && path.size == k) { + result.append(path.toList) + return + } + // 剪枝 + for (i <- startIndex to (9 - (k - path.size) + 1)) { + path.append(i) + backtracking(k, n, sum + i, i + 1) + path = path.take(path.size - 1) + } + } + + backtracking(k, n, 0, 1) // 调用递归方法 + result.toList // 最终返回结果集的List形式 + } +} +``` +### C# +```csharp +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> CombinationSum3(int k, int n) + { + BackTracking(k, n, 0, 1); + return res; + } + public void BackTracking(int k, int n, int sum, int start) + { + if (path.Count == k) + { + if (sum == n) + res.Add(new List(path)); + return; + } + for (int i = start; i <= 9; i++) + { + sum += i; + path.Add(i); + BackTracking(k, n, sum, i + 1); + sum -= i; + path.RemoveAt(path.Count - 1); + } + } +} +// 剪枝 +public class Solution +{ + public IList> res = new List>(); + public IList path = new List(); + public IList> CombinationSum3(int k, int n) + { + BackTracking(k, n, 0, 1); + return res; + } + public void BackTracking(int k, int n, int sum, int start) + { + if (sum > n) + return; + if (path.Count == k) + { + if (sum == n) + res.Add(new List(path)); + return; + } + for (int i = start; i <= 9 - (k - path.Count) + 1; i++) + { + sum += i; + path.Add(i); + BackTracking(k, n, sum, i + 1); + sum -= i; + path.RemoveAt(path.Count - 1); + } + } +} +``` + + + diff --git "a/problems/0222.\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221\347\232\204\350\212\202\347\202\271\344\270\252\346\225\260.md" "b/problems/0222.\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221\347\232\204\350\212\202\347\202\271\344\270\252\346\225\260.md" new file mode 100755 index 0000000000..eaf4eab2c9 --- /dev/null +++ "b/problems/0222.\345\256\214\345\205\250\344\272\214\345\217\211\346\240\221\347\232\204\350\212\202\347\202\271\344\270\252\346\225\260.md" @@ -0,0 +1,894 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 222.完全二叉树的节点个数 + +[力扣题目链接](https://leetcode.cn/problems/count-complete-tree-nodes/) + +给出一个完全二叉树,求出该树的节点个数。 + +示例 1: +* 输入:root = [1,2,3,4,5,6] +* 输出:6 + +示例 2: +* 输入:root = [] +* 输出:0 + +示例 3: +* 输入:root = [1] +* 输出:1 + +提示: + +* 树中节点的数目范围是[0, 5 * 10^4] +* 0 <= Node.val <= 5 * 10^4 +* 题目数据保证输入的树是 完全二叉树 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[要理解普通二叉树和完全二叉树的区别! | LeetCode:222.完全二叉树节点的数量](https://www.bilibili.com/video/BV1eW4y1B7pD),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + + +本篇给出按照普通二叉树的求法以及利用完全二叉树性质的求法。 + +### 普通二叉树 + +首先按照普通二叉树的逻辑来求。 + +这道题目的递归法和求二叉树的深度写法类似, 而迭代法,[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html)遍历模板稍稍修改一下,记录遍历的节点数量就可以了。 + +递归遍历的顺序依然是后序(左右中)。 + +#### 递归 + +如果对求二叉树深度还不熟悉的话,看这篇:[二叉树:看看这些树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)。 + +1. 确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回以该节点为根节点二叉树的节点数量,所以返回值为int类型。 + +代码如下: +```CPP +int getNodesNum(TreeNode* cur) { +``` + +2. 确定终止条件:如果为空节点的话,就返回0,表示节点数为0。 + +代码如下: + +```CPP +if (cur == NULL) return 0; +``` + +3. 确定单层递归的逻辑:先求它的左子树的节点数量,再求右子树的节点数量,最后取总和再加一 (加1是因为算上当前中间节点)就是目前节点为根节点的节点数量。 + +代码如下: + +```CPP +int leftNum = getNodesNum(cur->left); // 左 +int rightNum = getNodesNum(cur->right); // 右 +int treeNum = leftNum + rightNum + 1; // 中 +return treeNum; +``` + +所以整体C++代码如下: + +```CPP +// 版本一 +class Solution { +private: + int getNodesNum(TreeNode* cur) { + if (cur == NULL) return 0; + int leftNum = getNodesNum(cur->left); // 左 + int rightNum = getNodesNum(cur->right); // 右 + int treeNum = leftNum + rightNum + 1; // 中 + return treeNum; + } +public: + int countNodes(TreeNode* root) { + return getNodesNum(root); + } +}; +``` + +代码精简之后C++代码如下: +```CPP +// 版本二 +class Solution { +public: + int countNodes(TreeNode* root) { + if (root == NULL) return 0; + return 1 + countNodes(root->left) + countNodes(root->right); + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(log n),算上了递归系统栈占用的空间 + +**网上基本都是这个精简的代码版本,其实不建议大家照着这个来写,代码确实精简,但隐藏了一些内容,连遍历的顺序都看不出来,所以初学者建议学习版本一的代码,稳稳的打基础**。 + + +#### 迭代 + +如果对求二叉树层序遍历还不熟悉的话,看这篇:[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html)。 + +那么只要模板少做改动,加一个变量result,统计节点数量就可以了 + +```CPP +class Solution { +public: + int countNodes(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + int result = 0; + while (!que.empty()) { + int size = que.size(); + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + result++; // 记录节点数量 + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + } + return result; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +### 完全二叉树 + +以上方法都是按照普通二叉树来做的,对于完全二叉树特性不了解的同学可以看这篇 [关于二叉树,你该了解这些!](https://programmercarl.com/二叉树理论基础.html),这篇详细介绍了各种二叉树的特性。 + +在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1)  个节点。 + +**大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。** + +我来举一个典型的例子如题: + + + +完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。 + +对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。 + +对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。 + +完全二叉树(一)如图: +![222.完全二叉树的节点个数](https://file1.kamacoder.com/i/algo/20201124092543662.png) + +完全二叉树(二)如图: +![222.完全二叉树的节点个数1](https://file1.kamacoder.com/i/algo/20201124092634138.png) + +可以看出如果整个树不是满二叉树,就递归其左右孩子,直到遇到满二叉树为止,用公式计算这个子树(满二叉树)的节点数量。 + +这里关键在于如何去判断一个左子树或者右子树是不是满二叉树呢? + +在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。如图: + +![](https://file1.kamacoder.com/i/algo/20220829163554.png) + +在完全二叉树中,如果递归向左遍历的深度不等于递归向右遍历的深度,则说明不是满二叉树,如图: + +![](https://file1.kamacoder.com/i/algo/20220829163709.png) + +那有录友说了,这种情况,递归向左遍历的深度等于递归向右遍历的深度,但也不是满二叉树,如题: + +![](https://file1.kamacoder.com/i/algo/20220829163811.png) + +如果这么想,大家就是对 完全二叉树理解有误区了,**以上这棵二叉树,它根本就不是一个完全二叉树**! + +判断其子树是不是满二叉树,如果是则利用公式计算这个子树(满二叉树)的节点数量,如果不是则继续递归,那么 在递归三部曲中,第二部:终止条件的写法应该是这样的: + +```CPP +if (root == nullptr) return 0; +// 开始根据左深度和右深度是否相同来判断该子树是不是满二叉树 +TreeNode* left = root->left; +TreeNode* right = root->right; +int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便 +while (left) { // 求左子树深度 + left = left->left; + leftDepth++; +} +while (right) { // 求右子树深度 + right = right->right; + rightDepth++; +} +if (leftDepth == rightDepth) { + return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,返回满足满二叉树的子树节点数量 +} +``` + +递归三部曲,第三部,单层递归的逻辑:(可以看出使用后序遍历) + +```CPP +int leftTreeNum = countNodes(root->left); // 左 +int rightTreeNum = countNodes(root->right); // 右 +int result = leftTreeNum + rightTreeNum + 1; // 中 +return result; +``` + +该部分精简之后代码为: + +```CPP +return countNodes(root->left) + countNodes(root->right) + 1; +``` + +最后整体C++代码如下: + +```CPP +class Solution { +public: + int countNodes(TreeNode* root) { + if (root == nullptr) return 0; + TreeNode* left = root->left; + TreeNode* right = root->right; + int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便 + while (left) { // 求左子树深度 + left = left->left; + leftDepth++; + } + while (right) { // 求右子树深度 + right = right->right; + rightDepth++; + } + if (leftDepth == rightDepth) { + return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0 + } + return countNodes(root->left) + countNodes(root->right) + 1; + } +}; +``` + +* 时间复杂度:O(log n × log n) +* 空间复杂度:O(log n) + +## 其他语言版本 + +### Java: +```java +class Solution { + // 通用递归解法 + public int countNodes(TreeNode root) { + if(root == null) { + return 0; + } + return countNodes(root.left) + countNodes(root.right) + 1; + } +} +``` +```java +class Solution { + // 迭代法 + public int countNodes(TreeNode root) { + if (root == null) return 0; + Queue queue = new LinkedList<>(); + queue.offer(root); + int result = 0; + while (!queue.isEmpty()) { + int size = queue.size(); + while (size -- > 0) { + TreeNode cur = queue.poll(); + result++; + if (cur.left != null) queue.offer(cur.left); + if (cur.right != null) queue.offer(cur.right); + } + } + return result; + } +} +``` +```java +class Solution { + /** + * 针对完全二叉树的解法 + * + * 满二叉树的结点数为:2^depth - 1 + */ + public int countNodes(TreeNode root) { + if (root == null) return 0; + TreeNode left = root.left; + TreeNode right = root.right; + int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便 + while (left != null) { // 求左子树深度 + left = left.left; + leftDepth++; + } + while (right != null) { // 求右子树深度 + right = right.right; + rightDepth++; + } + if (leftDepth == rightDepth) { + return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0 + } + return countNodes(root.left) + countNodes(root.right) + 1; + } +} +``` + +### Python: + +递归法: +```python +class Solution: + def countNodes(self, root: TreeNode) -> int: + return self.getNodesNum(root) + + def getNodesNum(self, cur): + if not cur: + return 0 + leftNum = self.getNodesNum(cur.left) #左 + rightNum = self.getNodesNum(cur.right) #右 + treeNum = leftNum + rightNum + 1 #中 + return treeNum +``` + +递归法:精简版 +```python +class Solution: + def countNodes(self, root: TreeNode) -> int: + if not root: + return 0 + return 1 + self.countNodes(root.left) + self.countNodes(root.right) +``` + +迭代法: +```python +import collections +class Solution: + def countNodes(self, root: TreeNode) -> int: + queue = collections.deque() + if root: + queue.append(root) + result = 0 + while queue: + size = len(queue) + for i in range(size): + node = queue.popleft() + result += 1 #记录节点数量 + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + return result +``` + +完全二叉树 +```python +class Solution: + def countNodes(self, root: TreeNode) -> int: + if not root: + return 0 + left = root.left + right = root.right + leftDepth = 0 #这里初始为0是有目的的,为了下面求指数方便 + rightDepth = 0 + while left: #求左子树深度 + left = left.left + leftDepth += 1 + while right: #求右子树深度 + right = right.right + rightDepth += 1 + if leftDepth == rightDepth: + return (2 << leftDepth) - 1 #注意(2<<1) 相当于2^2,所以leftDepth初始为0 + return self.countNodes(root.left) + self.countNodes(root.right) + 1 +``` +完全二叉树写法2 +```python +class Solution: # 利用完全二叉树特性 + def countNodes(self, root: TreeNode) -> int: + if not root: return 0 + count = 1 + left = root.left; right = root.right + while left and right: + count+=1 + left = left.left; right = right.right + if not left and not right: # 如果同时到底说明是满二叉树,反之则不是 + return 2**count-1 + return 1+self.countNodes(root.left)+self.countNodes(root.right) +``` +完全二叉树写法3 +```python +class Solution: # 利用完全二叉树特性 + def countNodes(self, root: TreeNode) -> int: + if not root: return 0 + count = 0 + left = root.left; right = root.right + while left and right: + count+=1 + left = left.left; right = right.right + if not left and not right: # 如果同时到底说明是满二叉树,反之则不是 + return (2< 0 { + n := q.Len() + for i := 0; i < n; i++ { + node := q.Remove(q.Front()).(*TreeNode) + if node.Left != nil { + q.PushBack(node.Left) + } + if node.Right != nil { + q.PushBack(node.Right) + } + res++ + } + } + return res +} +``` + +### JavaScript: + +递归版本 +```javascript +var countNodes = function(root) { + //递归法计算二叉树节点数 + // 1. 确定递归函数参数 + const getNodeSum = function(node) { + //2. 确定终止条件 + if(node === null) { + return 0; + } + //3. 确定单层递归逻辑 + let leftNum = getNodeSum(node.left); + let rightNum = getNodeSum(node.right); + return leftNum + rightNum + 1; + } + return getNodeSum(root); +}; +``` + +迭代(层序遍历)版本 +```javascript +var countNodes = function(root) { + //层序遍历 + let queue = []; + if(root === null) { + return 0; + } + queue.push(root); + let nodeNums = 0; + while(queue.length) { + let length = queue.length; + while(length--) { + let node = queue.shift(); + nodeNums++; + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + return nodeNums; +}; +``` + +利用完全二叉树性质 +```javascript +var countNodes = function(root) { + //利用完全二叉树的特点 + if(root === null) { + return 0; + } + let left = root.left; + let right = root.right; + let leftDepth = 0, rightDepth = 0; + while(left) { + left = left.left; + leftDepth++; + } + while(right) { + right = right.right; + rightDepth++; + } + if(leftDepth == rightDepth) { + return Math.pow(2, leftDepth+1) - 1; + } + return countNodes(root.left) + countNodes(root.right) + 1; +}; +``` + +### TypeScrpt: + +> 递归法 + +```typescript +function countNodes(root: TreeNode | null): number { + if (root === null) return 0; + return 1 + countNodes(root.left) + countNodes(root.right); +}; +``` + +> 迭代法 + +```typescript +function countNodes(root: TreeNode | null): number { + let helperQueue: TreeNode[] = []; + let resCount: number = 0; + let tempNode: TreeNode; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + resCount++; + if (tempNode.left) helperQueue.push(tempNode.left); + if (tempNode.right) helperQueue.push(tempNode.right); + } + } + return resCount; +}; +``` + +> 利用完全二叉树性质 + +```typescript +function countNodes(root: TreeNode | null): number { + if (root === null) return 0; + let left: number = 0, + right: number = 0; + let curNode: TreeNode | null= root; + while (curNode !== null) { + left++; + curNode = curNode.left; + } + curNode = root; + while (curNode !== null) { + right++; + curNode = curNode.right; + } + if (left === right) { + return 2 ** left - 1; + } + return 1 + countNodes(root.left) + countNodes(root.right); +}; +``` + +### C: + +递归法 +```c +int countNodes(struct TreeNode* root) { + //若传入结点不存在,返回0 + if(!root) + return 0; + //算出左右子树的结点总数 + int leftCount = countNodes(root->left); + int rightCount = countNodes(root->right); + //返回左右子树结点总数+1 + return leftCount + rightCount + 1; +} + +int countNodes(struct TreeNode* root){ + return getNodes(root); +} +``` + +迭代法 +```c +int countNodes(struct TreeNode* root){ + //记录结点总数 + int totalNum = 0; + //开辟栈空间 + struct TreeNode** stack = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 100); + int stackTop = 0; + //如果root结点不为NULL,则将其入栈。若为NULL,则不会进入遍历,返回0 + if(root) + stack[stackTop++] = root; + //若栈中有结点存在,则进行遍历 + while(stackTop) { + //取出栈顶元素 + struct TreeNode* tempNode = stack[--stackTop]; + //结点总数+1 + totalNum++; + //若栈顶结点有左右孩子,将它们入栈 + if(tempNode->left) + stack[stackTop++] = tempNode->left; + if(tempNode->right) + stack[stackTop++] = tempNode->right; + } + return totalNum; +} +``` + +满二叉树 +```c +int countNodes(struct TreeNode* root){ + if(!root) + return 0; + int leftDepth = 0; + int rightDepth = 0; + struct TreeNode* rightNode = root->right; + struct TreeNode* leftNode = root->left; + //求出左子树深度 + while(leftNode) { + leftNode = leftNode->left; + leftDepth++; + } + + //求出右子树深度 + while(rightNode) { + rightNode = rightNode->right; + rightDepth++; + } + //若左右子树深度相同,为满二叉树。结点个数为2^height-1 + if(rightDepth == leftDepth) { + return (2 << leftDepth) - 1; + } + //否则返回左右子树的结点个数+1 + return countNodes(root->right) + countNodes(root->left) + 1; +} +``` + +### Swift: + +> 递归 +```swift +func countNodes(_ root: TreeNode?) -> Int { + return _countNodes(root) +} +func _countNodes(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + let leftCount = _countNodes(root.left) + let rightCount = _countNodes(root.right) + return 1 + leftCount + rightCount +} +``` + +> 层序遍历 +```Swift +func countNodes(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + var res = 0 + var queue = [TreeNode]() + queue.append(root) + while !queue.isEmpty { + let size = queue.count + for _ in 0 ..< size { + let node = queue.removeFirst() + res += 1 + if let left = node.left { + queue.append(left) + } + if let right = node.right { + queue.append(right) + } + } + } + return res +} +``` + +> 利用完全二叉树性质 +```Swift +func countNodes(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + var leftNode = root.left + var rightNode = root.right + var leftDepth = 0 + var rightDepth = 0 + while leftNode != nil { + leftNode = leftNode!.left + leftDepth += 1 + } + while rightNode != nil { + rightNode = rightNode!.right + rightDepth += 1 + } + if leftDepth == rightDepth { + return (2 << leftDepth) - 1 + } + return countNodes(root.left) + countNodes(root.right) + 1 +} +``` + +### Scala: + +递归: +```scala +object Solution { + def countNodes(root: TreeNode): Int = { + if(root == null) return 0 + 1 + countNodes(root.left) + countNodes(root.right) + } +} +``` + +层序遍历: +```scala +object Solution { + import scala.collection.mutable + def countNodes(root: TreeNode): Int = { + if (root == null) return 0 + val queue = mutable.Queue[TreeNode]() + var node = 0 + queue.enqueue(root) + while (!queue.isEmpty) { + val len = queue.size + for (i <- 0 until len) { + node += 1 + val curNode = queue.dequeue() + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + } + node + } +} +``` + +利用完全二叉树性质: +```scala +object Solution { + def countNodes(root: TreeNode): Int = { + if (root == null) return 0 + var leftNode = root.left + var rightNode = root.right + // 向左向右往下探 + var leftDepth = 0 + while (leftNode != null) { + leftDepth += 1 + leftNode = leftNode.left + } + var rightDepth = 0 + while (rightNode != null) { + rightDepth += 1 + rightNode = rightNode.right + } + // 如果相等就是一个满二叉树 + if (leftDepth == rightDepth) { + return (2 << leftDepth) - 1 + } + // 如果不相等就不是一个完全二叉树,继续向下递归 + countNodes(root.left) + countNodes(root.right) + 1 + } +} +``` + +### Rust: + +递归 +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn count_nodes(root: Option>>) -> i32 { + if root.is_none() { + return 0; + } + 1 + Self::count_nodes(Rc::clone(root.as_ref().unwrap()).borrow().left.clone()) + + Self::count_nodes(root.unwrap().borrow().right.clone()) + } +} +``` + +迭代 +```rust +use std::rc::Rc; +use std::cell::RefCell; +use std::collections::VecDeque; +impl Solution { + pub fn count_nodes(root: Option>>) -> i32 { + let mut res = 0; + let mut queue = VecDeque::new(); + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + for _ in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + res += 1; + } + } + res + } +} +``` +### C# +```csharp +// 递归 +public int CountNodes(TreeNode root) +{ + if (root == null) return 0; + var left = root.left; + var right = root.right; + int leftDepth = 0, rightDepth = 0; + while (left != null) + { + left = left.left; + leftDepth++; + } + while (right != null) + { + right = right.right; + rightDepth++; + } + if (leftDepth == rightDepth) + return (int)Math.Pow(2, leftDepth+1) - 1; + return CountNodes(root.left) + CountNodes(root.right) + 1; + +} +``` + + diff --git "a/problems/0225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" "b/problems/0225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" new file mode 100755 index 0000000000..72dfd2aacf --- /dev/null +++ "b/problems/0225.\347\224\250\351\230\237\345\210\227\345\256\236\347\216\260\346\240\210.md" @@ -0,0 +1,1367 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 用队列实现栈还是有点别扭。 + +# 225. 用队列实现栈 + +[力扣题目链接](https://leetcode.cn/problems/implement-stack-using-queues/) + +使用队列实现栈的下列操作: + +* push(x) -- 元素 x 入栈 +* pop() -- 移除栈顶元素 +* top() -- 获取栈顶元素 +* empty() -- 返回栈是否为空 + +注意: + +* 你只能使用队列的基本操作-- 也就是 push to back, peek/pop from front, size, 和 is empty 这些操作是合法的。 +* 你所使用的语言也许不支持队列。 你可以使用 list 或者 deque(双端队列)来模拟一个队列 , 只要是标准的队列操作即可。 +* 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[队列的基本操作! | LeetCode:225. 用队列实现栈](https://www.bilibili.com/video/BV1Fd4y1K7sm),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +(这里要强调是单向队列) + +有的同学可能疑惑这种题目有什么实际工程意义,**其实很多算法题目主要是对知识点的考察和教学意义远大于其工程实践的意义,所以面试题也是这样!** + +刚刚做过[栈与队列:我用栈来实现队列怎么样?](https://programmercarl.com/0232.用栈实现队列.html)的同学可能依然想着用一个输入队列,一个输出队列,就可以模拟栈的功能,仔细想一下还真不行! + +**队列模拟栈,其实一个队列就够了**,那么我们先说一说两个队列来实现栈的思路。 + +**队列是先进先出的规则,把一个队列中的数据导入另一个队列中,数据的顺序并没有变,并没有变成先进后出的顺序。** + +所以用栈实现队列, 和用队列实现栈的思路还是不一样的,这取决于这两个数据结构的性质。 + +但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用来备份的! + +如下面动画所示,**用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用**,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。 + +模拟的队列执行语句如下: + +```cpp +queue.push(1); +queue.push(2); +queue.pop(); // 注意弹出的操作 +queue.push(3); +queue.push(4); +queue.pop(); // 注意弹出的操作 +queue.pop(); +queue.pop(); +queue.empty(); +``` + +![225.用队列实现栈](https://file1.kamacoder.com/i/algo/225.用队列实现栈.gif) + +详细如代码注释所示: + + +```CPP +class MyStack { +public: + queue que1; + queue que2; // 辅助队列,用来备份 + + /** Initialize your data structure here. */ + MyStack() { + + } + + /** Push element x onto stack. */ + void push(int x) { + que1.push(x); + } + + /** Removes the element on top of the stack and returns that element. */ + int pop() { + int size = que1.size(); + size--; + while (size--) { // 将que1 导入que2,但要留下最后一个元素 + que2.push(que1.front()); + que1.pop(); + } + + int result = que1.front(); // 留下的最后一个元素就是要返回的值 + que1.pop(); + que1 = que2; // 再将que2赋值给que1 + while (!que2.empty()) { // 清空que2 + que2.pop(); + } + return result; + } + + /** Get the top element. + ** Can not use back() direactly. + */ + int top(){ + int size = que1.size(); + size--; + while (size--){ + // 将que1 导入que2,但要留下最后一个元素 + que2.push(que1.front()); + que1.pop(); + } + + int result = que1.front(); // 留下的最后一个元素就是要回返的值 + que2.push(que1.front()); // 获取值后将最后一个元素也加入que2中,保持原本的结构不变 + que1.pop(); + + que1 = que2; // 再将que2赋值给que1 + while (!que2.empty()){ + // 清空que2 + que2.pop(); + } + return result; + } + + /** Returns whether the stack is empty. */ + bool empty() { + return que1.empty(); + } +}; +``` +* 时间复杂度: pop为O(n),top为O(n),其他为O(1) +* 空间复杂度: O(n) + +## 优化 + +其实这道题目就是用一个队列就够了。 + +**一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。** + +C++优化代码 + +```CPP +class MyStack { +public: + queue que; + + MyStack() { + + } + + void push(int x) { + que.push(x); + } + + int pop() { + int size = que.size(); + size--; + while (size--) { // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部 + que.push(que.front()); + que.pop(); + } + int result = que.front(); // 此时弹出的元素顺序就是栈的顺序了 + que.pop(); + return result; + } + + int top(){ + int size = que.size(); + size--; + while (size--){ + // 将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部 + que.push(que.front()); + que.pop(); + } + int result = que.front(); // 此时获得的元素就是栈顶的元素了 + que.push(que.front()); // 将获取完的元素也重新添加到队列尾部,保证数据结构没有变化 + que.pop(); + return result; + } + + bool empty() { + return que.empty(); + } +}; +``` +* 时间复杂度: pop为O(n),top为O(n),其他为O(1) +* 空间复杂度: O(n) + + +## 其他语言版本 + +### Java: + +使用两个 Queue 实现方法1 +```java +class MyStack { + + Queue queue1; // 和栈中保持一样元素的队列 + Queue queue2; // 辅助队列 + + /** Initialize your data structure here. */ + public MyStack() { + queue1 = new LinkedList<>(); + queue2 = new LinkedList<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + queue2.offer(x); // 先放在辅助队列中 + while (!queue1.isEmpty()){ + queue2.offer(queue1.poll()); + } + Queue queueTemp; + queueTemp = queue1; + queue1 = queue2; + queue2 = queueTemp; // 最后交换queue1和queue2,将元素都放到queue1中 + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + return queue1.poll(); // 因为queue1中的元素和栈中的保持一致,所以这个和下面两个的操作只看queue1即可 + } + + /** Get the top element. */ + public int top() { + return queue1.peek(); + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return queue1.isEmpty(); + } +} + +``` +使用两个 Queue 实现方法2 +```java +class MyStack { + //q1作为主要的队列,其元素排列顺序和出栈顺序相同 + Queue q1 = new ArrayDeque<>(); + //q2仅作为临时放置 + Queue q2 = new ArrayDeque<>(); + + public MyStack() { + + } + //在加入元素时先将q1中的元素依次出栈压入q2,然后将新加入的元素压入q1,再将q2中的元素依次出栈压入q1 + public void push(int x) { + while (q1.size() > 0) { + q2.add(q1.poll()); + } + q1.add(x); + while (q2.size() > 0) { + q1.add(q2.poll()); + } + } + + public int pop() { + return q1.poll(); + } + + public int top() { + return q1.peek(); + } + + public boolean empty() { + return q1.isEmpty(); + } +} +``` + +使用两个 Deque 实现 +```java +class MyStack { + // Deque 接口继承了 Queue 接口 + // 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst + Deque que1; // 和栈中保持一样元素的队列 + Deque que2; // 辅助队列 + /** Initialize your data structure here. */ + public MyStack() { + que1 = new ArrayDeque<>(); + que2 = new ArrayDeque<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + que1.addLast(x); + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + int size = que1.size(); + size--; + // 将 que1 导入 que2 ,但留下最后一个值 + while (size-- > 0) { + que2.addLast(que1.peekFirst()); + que1.pollFirst(); + } + + int res = que1.pollFirst(); + // 将 que2 对象的引用赋给了 que1 ,此时 que1,que2 指向同一个队列 + que1 = que2; + // 如果直接操作 que2,que1 也会受到影响,所以为 que2 分配一个新的空间 + que2 = new ArrayDeque<>(); + return res; + } + + /** Get the top element. */ + public int top() { + return que1.peekLast(); + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return que1.isEmpty(); + } +} +``` +优化,使用一个 Deque 实现 +```java +class MyStack { + // Deque 接口继承了 Queue 接口 + // 所以 Queue 中的 add、poll、peek等效于 Deque 中的 addLast、pollFirst、peekFirst + Deque que1; + /** Initialize your data structure here. */ + public MyStack() { + que1 = new ArrayDeque<>(); + } + + /** Push element x onto stack. */ + public void push(int x) { + que1.addLast(x); + } + + /** Removes the element on top of the stack and returns that element. */ + public int pop() { + int size = que1.size(); + size--; + // 将 que1 导入 que2 ,但留下最后一个值 + while (size-- > 0) { + que1.addLast(que1.peekFirst()); + que1.pollFirst(); + } + + int res = que1.pollFirst(); + return res; + } + + /** Get the top element. */ + public int top() { + return que1.peekLast(); + } + + /** Returns whether the stack is empty. */ + public boolean empty() { + return que1.isEmpty(); + } +} +``` + +优化,使用一个 Queue 实现 +```java +class MyStack { + + Queue queue; + + public MyStack() { + queue = new LinkedList<>(); + } + + //每 offer 一个数(A)进来,都重新排列,把这个数(A)放到队列的队首 + public void push(int x) { + queue.offer(x); + int size = queue.size(); + //移动除了 A 的其它数 + while (size-- > 1) + queue.offer(queue.poll()); + } + + public int pop() { + return queue.poll(); + } + + public int top() { + return queue.peek(); + } + + public boolean empty() { + return queue.isEmpty(); + } +} + +``` +优化,使用一个 Queue 实现,但用卡哥的逻辑实现 +```Java +class MyStack { + Queue queue; + + public MyStack() { + queue = new LinkedList<>(); + } + + public void push(int x) { + queue.add(x); + } + + public int pop() { + rePosition(); + return queue.poll(); + } + + public int top() { + rePosition(); + int result = queue.poll(); + queue.add(result); + return result; + } + + public boolean empty() { + return queue.isEmpty(); + } + + public void rePosition(){ + int size = queue.size(); + size--; + while(size-->0) + queue.add(queue.poll()); + } +} +``` + +### Python: + +```python +from collections import deque + +class MyStack: + + def __init__(self): + """ + Python普通的Queue或SimpleQueue没有类似于peek的功能 + 也无法用索引访问,在实现top的时候较为困难。 + + 用list可以,但是在使用pop(0)的时候时间复杂度为O(n) + 因此这里使用双向队列,我们保证只执行popleft()和append(),因为deque可以用索引访问,可以实现和peek相似的功能 + + in - 存所有数据 + out - 仅在pop的时候会用到 + """ + self.queue_in = deque() + self.queue_out = deque() + + def push(self, x: int) -> None: + """ + 直接append即可 + """ + self.queue_in.append(x) + + + def pop(self) -> int: + """ + 1. 首先确认不空 + 2. 因为队列的特殊性,FIFO,所以我们只有在pop()的时候才会使用queue_out + 3. 先把queue_in中的所有元素(除了最后一个),依次出列放进queue_out + 4. 交换in和out,此时out里只有一个元素 + 5. 把out中的pop出来,即是原队列的最后一个 + + tip:这不能像栈实现队列一样,因为另一个queue也是FIFO,如果执行pop()它不能像 + stack一样从另一个pop(),所以干脆in只用来存数据,pop()的时候两个进行交换 + """ + if self.empty(): + return None + + for i in range(len(self.queue_in) - 1): + self.queue_out.append(self.queue_in.popleft()) + + self.queue_in, self.queue_out = self.queue_out, self.queue_in # 交换in和out,这也是为啥in只用来存 + return self.queue_out.popleft() + + def top(self) -> int: + """ + 写法一: + 1. 首先确认不空 + 2. 我们仅有in会存放数据,所以返回第一个即可(这里实际上用到了栈) + 写法二: + 1. 首先确认不空 + 2. 因为队列的特殊性,FIFO,所以我们只有在pop()的时候才会使用queue_out + 3. 先把queue_in中的所有元素(除了最后一个),依次出列放进queue_out + 4. 交换in和out,此时out里只有一个元素 + 5. 把out中的pop出来,即是原队列的最后一个,并使用temp变量暂存 + 6. 把temp追加到queue_in的末尾 + """ + # 写法一: + # if self.empty(): + # return None + + # return self.queue_in[-1] # 这里实际上用到了栈,因为直接获取了queue_in的末尾元素 + + # 写法二: + if self.empty(): + return None + + for i in range(len(self.queue_in) - 1): + self.queue_out.append(self.queue_in.popleft()) + + self.queue_in, self.queue_out = self.queue_out, self.queue_in + temp = self.queue_out.popleft() + self.queue_in.append(temp) + return temp + + + def empty(self) -> bool: + """ + 因为只有in存了数据,只要判断in是不是有数即可 + """ + return len(self.queue_in) == 0 + +``` +优化,使用一个队列实现 +```python +class MyStack: + + def __init__(self): + self.que = deque() + + def push(self, x: int) -> None: + self.que.append(x) + + def pop(self) -> int: + if self.empty(): + return None + for i in range(len(self.que)-1): + self.que.append(self.que.popleft()) + return self.que.popleft() + + def top(self) -> int: + # 写法一: + # if self.empty(): + # return None + # return self.que[-1] + + # 写法二: + if self.empty(): + return None + for i in range(len(self.que)-1): + self.que.append(self.que.popleft()) + temp = self.que.popleft() + self.que.append(temp) + return temp + + def empty(self) -> bool: + return not self.que +``` + +### Go: + +使用两个队列实现 +```go +type MyStack struct { + //创建两个队列 + queue1 []int + queue2 []int +} + + +func Constructor() MyStack { + return MyStack{ //初始化 + queue1:make([]int,0), + queue2:make([]int,0), + } +} + + +func (this *MyStack) Push(x int) { + //先将数据存在queue2中 + this.queue2 = append(this.queue2,x) + //将queue1中所有元素移到queue2中,再将两个队列进行交换 + this.Move() +} + + +func (this *MyStack) Move(){ + if len(this.queue1) == 0{ + //交换,queue1置为queue2,queue2置为空 + this.queue1,this.queue2 = this.queue2,this.queue1 + }else{ + //queue1元素从头开始一个一个追加到queue2中 + this.queue2 = append(this.queue2,this.queue1[0]) + this.queue1 = this.queue1[1:] //去除第一个元素 + this.Move() //重复 + } +} + +func (this *MyStack) Pop() int { + val := this.queue1[0] + this.queue1 = this.queue1[1:] //去除第一个元素 + return val + +} + + +func (this *MyStack) Top() int { + return this.queue1[0] //直接返回 +} + + +func (this *MyStack) Empty() bool { +return len(this.queue1) == 0 +} + + +/** + * Your MyStack object will be instantiated and called as such: + * obj := Constructor(); + * obj.Push(x); + * param_2 := obj.Pop(); + * param_3 := obj.Top(); + * param_4 := obj.Empty(); + */ +``` + +使用一个队列实现 +```go +type MyStack struct { + queue []int//创建一个队列 +} + + +/** Initialize your data structure here. */ +func Constructor() MyStack { + return MyStack{ //初始化 + queue:make([]int,0), + } +} + + +/** Push element x onto stack. */ +func (this *MyStack) Push(x int) { + //添加元素 + this.queue=append(this.queue,x) +} + + +/** Removes the element on top of the stack and returns that element. */ +func (this *MyStack) Pop() int { + n:=len(this.queue)-1//判断长度 + for n!=0{ //除了最后一个,其余的都重新添加到队列里 + val:=this.queue[0] + this.queue=this.queue[1:] + this.queue=append(this.queue,val) + n-- + } + //弹出元素 + val:=this.queue[0] + this.queue=this.queue[1:] + return val + +} + + +/** Get the top element. */ +func (this *MyStack) Top() int { + //利用Pop函数,弹出来的元素重新添加 + val:=this.Pop() + this.queue=append(this.queue,val) + return val +} + + +/** Returns whether the stack is empty. */ +func (this *MyStack) Empty() bool { + return len(this.queue)==0 +} + + +/** + * Your MyStack object will be instantiated and called as such: + * obj := Constructor(); + * obj.Push(x); + * param_2 := obj.Pop(); + * param_3 := obj.Top(); + * param_4 := obj.Empty(); + */ +``` + +### JavaScript: + +使用数组(push, shift)模拟队列 + +```js + +// 使用两个队列实现 +/** + * Initialize your data structure here. + */ +var MyStack = function() { + this.queue1 = []; + this.queue2 = []; +}; + +/** + * Push element x onto stack. + * @param {number} x + * @return {void} + */ +MyStack.prototype.push = function(x) { + this.queue1.push(x); +}; + +/** + * Removes the element on top of the stack and returns that element. + * @return {number} + */ +MyStack.prototype.pop = function() { + // 减少两个队列交换的次数, 只有当queue1为空时,交换两个队列 + if(!this.queue1.length) { + [this.queue1, this.queue2] = [this.queue2, this.queue1]; + } + while(this.queue1.length > 1) { + this.queue2.push(this.queue1.shift()); + } + return this.queue1.shift(); +}; + +/** + * Get the top element. + * @return {number} + */ +MyStack.prototype.top = function() { + const x = this.pop(); + this.queue1.push(x); + return x; +}; + +/** + * Returns whether the stack is empty. + * @return {boolean} + */ +MyStack.prototype.empty = function() { + return !this.queue1.length && !this.queue2.length; +}; + +``` + +```js + +// 使用一个队列实现 +/** + * Initialize your data structure here. + */ +var MyStack = function() { + this.queue = []; +}; + +/** + * Push element x onto stack. + * @param {number} x + * @return {void} + */ +MyStack.prototype.push = function(x) { + this.queue.push(x); +}; + +/** + * Removes the element on top of the stack and returns that element. + * @return {number} + */ +MyStack.prototype.pop = function() { + let size = this.queue.length; + while(size-- > 1) { + this.queue.push(this.queue.shift()); + } + return this.queue.shift(); +}; + +/** + * Get the top element. + * @return {number} + */ +MyStack.prototype.top = function() { + const x = this.pop(); + this.queue.push(x); + return x; +}; + +/** + * Returns whether the stack is empty. + * @return {boolean} + */ +MyStack.prototype.empty = function() { + return !this.queue.length; +}; + +``` + +### TypeScript: + +版本一:使用两个队列模拟栈 + +```typescript +class MyStack { + private queue: number[]; + private tempQueue: number[]; + constructor() { + this.queue = []; + this.tempQueue = []; + } + + push(x: number): void { + this.queue.push(x); + } + + pop(): number { + for (let i = 0, length = this.queue.length - 1; i < length; i++) { + this.tempQueue.push(this.queue.shift()!); + } + let res: number = this.queue.pop()!; + let temp: number[] = this.queue; + this.queue = this.tempQueue; + this.tempQueue = temp; + return res; + } + + top(): number { + let res: number = this.pop(); + this.push(res); + return res; + } + + empty(): boolean { + return this.queue.length === 0; + } +} +``` + +版本二:使用一个队列模拟栈 + +```typescript +class MyStack { + private queue: number[]; + constructor() { + this.queue = []; + } + + push(x: number): void { + this.queue.push(x); + } + + pop(): number { + for (let i = 0, length = this.queue.length - 1; i < length; i++) { + this.queue.push(this.queue.shift()!); + } + return this.queue.shift()!; + } + + top(): number { + let res: number = this.pop(); + this.push(res); + return res; + } + + empty(): boolean { + return this.queue.length === 0; + } +} +``` + +### Swift: + +```Swift +// 定义一个队列数据结构 +class Queue { + var array: [Int] + init() { + array = [Int]() + } + + /** Push element x to the back of queue. */ + func push(_ x: Int) { + array.append(x) + } + + /** Removes the element from in front of queue and returns that element. */ + func pop() -> Int { + if array.isEmpty { + return -1 + } + return array.removeFirst() + } + + /** Get the front element. */ + func peek() -> Int { + if array.isEmpty { + return -1 + } + return array.first! + } + + /** Returns whether the queue is empty. */ + func empty() -> Bool { + return array.isEmpty + } + + func count() -> Int { + return array.count + } +} + +// 使用双队列 +class MyStack { + var queue1: Queue + var queue2: Queue + + init() { + queue1 = Queue() + queue2 = Queue() + } + + func push(_ x: Int) { + queue1.push(x) + } + + func pop() -> Int { + if queue1.empty() { + return -1 + } + while queue1.count() > 1 { + queue2.push(queue1.pop()) + } + let res = queue1.pop() + while !queue2.empty() { + queue1.push(queue2.pop()) + } + return res + } + + func top() -> Int { + if queue1.empty() { + return -1 + } + let res = pop() + push(res) + return res + } + + func empty() -> Bool { + return queue1.empty() && queue2.empty() + } +} + +// 使用单队列 +class MyStack { + var queue: Queue + + init() { + queue = Queue() + } + + func push(_ x: Int) { + queue.push(x) + } + + func pop() -> Int { + if queue.empty() { + return -1 + } + for _ in 1 ..< queue.count() { + queue.push(queue.pop()) + } + return queue.pop() + } + + func top() -> Int { + if queue.empty() { + return -1 + } + let res = pop() + push(res) + return res + } + + func empty() -> Bool { + return queue.empty() + } +} +``` +### Scala: +使用两个队列模拟栈: + +```scala +import scala.collection.mutable + +class MyStack() { + + val queue1 = new mutable.Queue[Int]() + val queue2 = new mutable.Queue[Int]() + + def push(x: Int) { + queue1.enqueue(x) + } + + def pop(): Int = { + var size = queue1.size + // 将queue1中的每个元素都移动到queue2 + for (i <- 0 until size - 1) { + queue2.enqueue(queue1.dequeue()) + } + var res = queue1.dequeue() + // 再将queue2中的每个元素都移动到queue1 + while (!queue2.isEmpty) { + queue1.enqueue(queue2.dequeue()) + } + res + } + + def top(): Int = { + var size = queue1.size + for (i <- 0 until size - 1) { + queue2.enqueue(queue1.dequeue()) + } + var res = queue1.dequeue() + while (!queue2.isEmpty) { + queue1.enqueue(queue2.dequeue()) + } + // 最终还需要把res送进queue1 + queue1.enqueue(res) + res + } + + def empty(): Boolean = { + queue1.isEmpty + } +} +``` +使用一个队列模拟: +```scala +import scala.collection.mutable + +class MyStack() { + + val queue = new mutable.Queue[Int]() + + def push(x: Int) { + queue.enqueue(x) + } + + def pop(): Int = { + var size = queue.size + for (i <- 0 until size - 1) { + queue.enqueue(queue.head) // 把头添到队列最后 + queue.dequeue() // 再出队 + } + queue.dequeue() + } + + def top(): Int = { + var size = queue.size + var res = 0 + for (i <- 0 until size) { + queue.enqueue(queue.head) // 把头添到队列最后 + res = queue.dequeue() // 再出队 + } + res + } + + def empty(): Boolean = { + queue.isEmpty + } + } +``` + +### C#: + +> 双队列 + +```csharp +public class MyStack { + Queue queue1; + Queue queue2; + public MyStack() { + queue1 = new Queue(); + queue2 = new Queue(); + } + + public void Push(int x) { + queue2.Enqueue(x); + while(queue1.Count != 0){ + queue2.Enqueue(queue1.Dequeue()); + } + Queue queueTemp; + queueTemp = queue1; + queue1 = queue2; + queue2 = queueTemp; + } + + public int Pop() { + return queue1.Count > 0 ? queue1.Dequeue() : -1; + } + + public int Top() { + return queue1.Count > 0 ? queue1.Peek() : -1; + } + + public bool Empty() { + return queue1.Count == 0; + } +} +``` + +> 单队列 + +```c# +/* + * @lc app=leetcode id=225 lang=csharp + * 版本二:单队列 + * [225] Implement Stack using Queues + */ + +// @lc code=start +public class MyStack { + Queue myQueue; + public MyStack() { + myQueue = new Queue(); + } + + public void Push(int x) { + myQueue.Enqueue(x); + } + + //使用一个队列实现 + public int Pop() { + //一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时再去弹出元素就是栈的顺序了。 + for (var i = 0; i < myQueue.Count-1; i++) + { + myQueue.Enqueue(myQueue.Dequeue()); + } + return myQueue.Dequeue(); + } + + //复用Pop()的代码 + public int Top() { + int res = Pop(); + myQueue.Enqueue(res); + return res; + } + + public bool Empty() { + return (myQueue.Count == 0); + } +} + +// @lc code=end + +``` + + + +### PHP: + +> 双队列 +```php +// SplQueue 类通过使用一个双向链表来提供队列的主要功能。(PHP 5 >= 5.3.0, PHP 7, PHP 8) +// https://www.php.net/manual/zh/class.splqueue.php +class MyStack { + public $queueMain; // 保存数据 + public $queueTmp; // 辅助作用 + + function __construct() { + $this->queueMain=new SplQueue(); + $this->queueTmp=new SplQueue(); + } + + // queueMain: 1,2,3 <= add + function push($x) { + $this->queueMain->enqueue($x); + } + + function pop() { + $qmSize = $this->queueMain->Count(); + $qmSize --; + // queueMain: 3,2,1 => pop =>2,1 => add => 2,1 :queueTmp + while($qmSize --){ + $this->queueTmp->enqueue($this->queueMain->dequeue()); + } + // queueMain: 3 + $val = $this->queueMain->dequeue(); + // queueMain <= queueTmp + $this->queueMain = $this->queueTmp; + // 清空queueTmp,下次使用 + $this->queueTmp = new SplQueue(); + return $val; + } + + function top() { + // 底层是双链表实现:从双链表的末尾查看节点 + return $this->queueMain->top(); + } + + function empty() { + return $this->queueMain->isEmpty(); + } +} +``` +> 单队列 +```php +class MyStack { + public $queue; + + function __construct() { + $this->queue=new SplQueue(); + } + + function push($x) { + $this->queue->enqueue($x); + } + + function pop() { + $qmSize = $this->queue->Count(); + $qmSize --; + //queue: 3,2,1 => pop =>2,1 => add => 2,1,3 :queue + while($qmSize --){ + $this->queue->enqueue($this->queue->dequeue()); + } + $val = $this->queue->dequeue(); + return $val; + } + + function top() { + return $this->queue->top(); + } + + function empty() { + return $this->queue->isEmpty(); + } +} +``` + +### Rust: + +> rust:单队列 + +```rust +struct MyStack { + queue: Vec, +} + +impl MyStack { + fn new() -> Self { + MyStack { queue: vec![] } + } + + fn push(&mut self, x: i32) { + self.queue.push(x); + } + + fn pop(&mut self) -> i32 { + let len = self.queue.len() - 1; + for _ in 0..len { + let tmp = self.queue.remove(0); + self.queue.push(tmp); + } + self.queue.remove(0) + } + + fn top(&mut self) -> i32 { + let res = self.pop(); + self.queue.push(res); + res + } + + fn empty(&self) -> bool { + self.queue.is_empty() + } +} +``` + +### C: + +> C:单队列 + +```c +typedef struct Node { + int val; + struct Node *next; +} Node_t; + +// 用单向链表实现queue +typedef struct { + Node_t *head; + Node_t *foot; + int size; +} MyStack; + +MyStack* myStackCreate() { + MyStack *obj = (MyStack *)malloc(sizeof(MyStack)); + assert(obj); + obj->head = NULL; + obj->foot = NULL; + obj->size = 0; + return obj; +} + +void myStackPush(MyStack* obj, int x) { + + Node_t *temp = (Node_t *)malloc(sizeof(Node_t)); + assert(temp); + temp->val = x; + temp->next = NULL; + + // 添加至queue末尾 + if (obj->foot) { + obj->foot->next = temp; + } else { + obj->head = temp; + } + obj->foot = temp; + obj->size++; +} + +int myStackPop(MyStack* obj) { + + // 获取末尾元素 + int target = obj->foot->val; + + if (obj->head == obj->foot) { + free(obj->foot); + obj->head = NULL; + obj->foot = NULL; + } else { + + Node_t *prev = obj->head; + // 移动至queue尾部节点前一个节点 + while (prev->next != obj->foot) { + prev = prev->next; + } + + free(obj->foot); + obj->foot = prev; + obj->foot->next = NULL; + } + + obj->size--; + return target; +} + +int myStackTop(MyStack* obj) { + return obj->foot->val; +} + +bool myStackEmpty(MyStack* obj) { + return obj->size == 0; +} + +void myStackFree(MyStack* obj) { + Node_t *curr = obj->head; + while (curr != NULL) { + Node_t *temp = curr->next; + free(curr); + curr = temp; + } + free(obj); +} + +``` + + diff --git "a/problems/0226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" "b/problems/0226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" new file mode 100755 index 0000000000..67a1a59338 --- /dev/null +++ "b/problems/0226.\347\277\273\350\275\254\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,1022 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 226.翻转二叉树 + +[力扣题目链接](https://leetcode.cn/problems/invert-binary-tree/) + +翻转一棵二叉树。 + + +![226.翻转二叉树](https://file1.kamacoder.com/i/algo/20210203192644329.png) + +这道题目背后有一个让程序员心酸的故事,听说 Homebrew的作者Max Howell,就是因为没在白板上写出翻转二叉树,最后被Google拒绝了。(真假不做判断,全当一个乐子哈) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[听说一位巨佬面Google被拒了,因为没写出翻转二叉树 | LeetCode:226.翻转二叉树](https://www.bilibili.com/video/BV1sP4y1f7q7),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 题外话 + +这道题目是非常经典的题目,也是比较简单的题目(至少一看就会)。 + +但正是因为这道题太简单,一看就会,一些同学都没有抓住起本质,稀里糊涂的就把这道题目过了。 + +如果做过这道题的同学也建议认真看完,相信一定有所收获! + +## 思路 + +我们之前介绍的都是各种方式遍历二叉树,这次要翻转了,感觉还是有点懵逼。 + +这得怎么翻转呢? + +如果要从整个树来看,翻转还真的挺复杂,整个树以中间分割线进行翻转,如图: + + +![226.翻转二叉树1](https://file1.kamacoder.com/i/algo/20210203192724351.png) + +可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。 + +关键在于遍历顺序,前中后序应该选哪一种遍历顺序? (一些同学这道题都过了,但是不知道自己用的是什么顺序) + +遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。 + +**注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果** + +**这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了** + +那么层序遍历可以不可以呢?**依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!** + +### 递归法 + +对于二叉树的递归法的前中后序遍历,已经在[二叉树:前中后序递归遍历](https://programmercarl.com/二叉树的递归遍历.html)详细讲解了。 + +我们下文以前序遍历为例,通过动画来看一下翻转的过程: + +![翻转二叉树](https://file1.kamacoder.com/i/algo/%E7%BF%BB%E8%BD%AC%E4%BA%8C%E5%8F%89%E6%A0%91.gif) + +我们来看一下递归三部曲: + +1. 确定递归函数的参数和返回值 + +参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归的逻辑中发现还需要其他参数的时候,随时补充。 + +返回值的话其实也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就函数的返回类型为`TreeNode*`。 + +```cpp +TreeNode* invertTree(TreeNode* root) +``` + +2. 确定终止条件 + +当前节点为空的时候,就返回 + +```cpp +if (root == NULL) return root; +``` + +3. 确定单层递归的逻辑 + +因为是前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。 + +```cpp +swap(root->left, root->right); +invertTree(root->left); +invertTree(root->right); +``` + +基于这递归三步法,代码基本写完,C++代码如下: + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + if (root == NULL) return root; + swap(root->left, root->right); // 中 + invertTree(root->left); // 左 + invertTree(root->right); // 右 + return root; + } +}; +``` + +### 迭代法 + +#### 深度优先遍历 + +[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)中给出了前中后序迭代方式的写法,所以本题可以很轻松的写出如下迭代法的代码: + +C++代码迭代法(前序遍历) + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + if (root == NULL) return root; + stack st; + st.push(root); + while(!st.empty()) { + TreeNode* node = st.top(); // 中 + st.pop(); + swap(node->left, node->right); + if(node->right) st.push(node->right); // 右 + if(node->left) st.push(node->left); // 左 + } + return root; + } +}; +``` +如果这个代码看不懂的话可以再回顾一下[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)。 + + +我们在[二叉树:前中后序迭代方式的统一写法](https://programmercarl.com/二叉树的统一迭代法.html)中介绍了统一的写法,所以,本题也只需将文中的代码少做修改便可。 + +C++代码如下迭代法(前序遍历) + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 右 + if (node->left) st.push(node->left); // 左 + st.push(node); // 中 + st.push(NULL); + } else { + st.pop(); + node = st.top(); + st.pop(); + swap(node->left, node->right); // 节点处理逻辑 + } + } + return root; + } +}; +``` + +如果上面这个代码看不懂,回顾一下文章[二叉树:前中后序迭代方式的统一写法](https://programmercarl.com/二叉树的统一迭代法.html)。 + +#### 广度优先遍历 + +也就是层序遍历,层数遍历也是可以翻转这棵树的,因为层序遍历也可以把每个节点的左右孩子都翻转一遍,代码如下: + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + while (!que.empty()) { + int size = que.size(); + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + swap(node->left, node->right); // 节点处理 + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + } + return root; + } +}; +``` +如果对以上代码不理解,或者不清楚二叉树的层序遍历,可以看这篇[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html) + +## 拓展 + +**文中我指的是递归的中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。** + +如果非要使用递归中序的方式写,也可以,如下代码就可以避免节点左右孩子翻转两次的情况: + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + if (root == NULL) return root; + invertTree(root->left); // 左 + swap(root->left, root->right); // 中 + invertTree(root->left); // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了 + return root; + } +}; +``` + +代码虽然可以,但这毕竟不是真正的递归中序遍历了。 + +但使用迭代方式统一写法的中序是可以的。 + +代码如下: + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 右 + st.push(node); // 中 + st.push(NULL); + if (node->left) st.push(node->left); // 左 + + } else { + st.pop(); + node = st.top(); + st.pop(); + swap(node->left, node->right); // 节点处理逻辑 + } + } + return root; + } +}; + +``` + +为什么这个中序就是可以的呢,因为这是用栈来遍历,而不是靠指针来遍历,避免了递归法中翻转了两次的情况,大家可以画图理解一下,这里有点意思的。 + +## 总结 + +针对二叉树的问题,解题之前一定要想清楚究竟是前中后序遍历,还是层序遍历。 + +**二叉树解题的大忌就是自己稀里糊涂的过了(因为这道题相对简单),但是也不知道自己是怎么遍历的。** + +这也是造成了二叉树的题目“一看就会,一写就废”的原因。 + +**针对翻转二叉树,我给出了一种递归,三种迭代(两种模拟深度优先遍历,一种层序遍历)的写法,都是之前我们讲过的写法,融汇贯通一下而已。** + +大家一定也有自己的解法,但一定要成方法论,这样才能通用,才能举一反三! + +## 其他语言版本 + + +### Java: +```Java +//DFS递归 +class Solution { + /** + * 前后序遍历都可以 + * 中序不行,因为先左孩子交换孩子,再根交换孩子(做完后,右孩子已经变成了原来的左孩子),再右孩子交换孩子(此时其实是对原来的左孩子做交换) + */ + public TreeNode invertTree(TreeNode root) { + if (root == null) { + return null; + } + invertTree(root.left); + invertTree(root.right); + swapChildren(root); + return root; + } + + private void swapChildren(TreeNode root) { + TreeNode tmp = root.left; + root.left = root.right; + root.right = tmp; + } +} + +//BFS +class Solution { + public TreeNode invertTree(TreeNode root) { + if (root == null) {return null;} + ArrayDeque deque = new ArrayDeque<>(); + deque.offer(root); + while (!deque.isEmpty()) { + int size = deque.size(); + while (size-- > 0) { + TreeNode node = deque.poll(); + swap(node); + if (node.left != null) deque.offer(node.left); + if (node.right != null) deque.offer(node.right); + } + } + return root; + } + + public void swap(TreeNode root) { + TreeNode temp = root.left; + root.left = root.right; + root.right = temp; + } +} +``` + +### Python: + +递归法:前序遍历: +```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 invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + root.left, root.right = root.right, root.left + self.invertTree(root.left) + self.invertTree(root.right) + return root +``` + +迭代法:前序遍历: +```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 invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + stack = [root] + while stack: + node = stack.pop() + node.left, node.right = node.right, node.left + if node.right: + stack.append(node.right) + if node.left: + stack.append(node.left) + return root +``` + +递归法:中序遍历: +```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 invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + self.invertTree(root.left) + root.left, root.right = root.right, root.left + self.invertTree(root.left) + return root +``` + +迭代法,伪中序遍历(结果是对的,看起来像是中序遍历,实际上它是前序遍历,只不过把中间节点处理逻辑放到了中间。还是要用'统一写法'才是真正的中序遍历): +```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 invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + stack = [root] + while stack: + node = stack.pop() + if node.right: + stack.append(node.right) + node.left, node.right = node.right, node.left # 放到中间,依然是前序遍历 + if node.right: + stack.append(node.right) + return root +``` + +递归法:后序遍历: +```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 invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + self.invertTree(root.left) + self.invertTree(root.right) + root.left, root.right = root.right, root.left + return root +``` + +迭代法,伪后序遍历(结果是对的,看起来像是后序遍历,实际上它是前序遍历,只不过把中间节点处理逻辑放到了最后。还是要用'统一写法'才是真正的后序遍历): +```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 invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + stack = [root] + while stack: + node = stack.pop() + if node.right: + stack.append(node.right) + if node.left: + stack.append(node.left) + node.left, node.right = node.right, node.left + + return root +``` + +迭代法:广度优先遍历(层序遍历): +```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 invertTree(self, root: TreeNode) -> TreeNode: + if not root: + return None + + queue = collections.deque([root]) + while queue: + node = queue.popleft() + node.left, node.right = node.right, node.left + if node.left: queue.append(node.left) + if node.right: queue.append(node.right) + return root + +``` + +### Go: + +递归版本的前序遍历 +```Go +func invertTree(root *TreeNode) *TreeNode { + if root == nil { + return nil + } + root.Left, root.Right = root.Right, root.Left //交换 + + invertTree(root.Left) + invertTree(root.Right) + + return root +} +``` + +递归版本的后序遍历 + +```go +func invertTree(root *TreeNode) *TreeNode { + if root == nil { + return root + } + + invertTree(root.Left) //遍历左节点 + invertTree(root.Right) //遍历右节点 + root.Left, root.Right = root.Right, root.Left //交换 + + return root +} +``` + +迭代版本的前序遍历 + +```go +func invertTree(root *TreeNode) *TreeNode { + stack := []*TreeNode{} + node := root + for node != nil || len(stack) > 0 { + for node != nil { + node.Left, node.Right = node.Right, node.Left //交换 + stack = append(stack,node) + node = node.Left + } + node = stack[len(stack)-1] + stack = stack[:len(stack)-1] + node = node.Right + } + + return root +} +``` + +迭代版本的后序遍历 + +```go +func invertTree(root *TreeNode) *TreeNode { + stack := []*TreeNode{} + node := root + var prev *TreeNode + for node != nil || len(stack) > 0 { + for node != nil { + stack = append(stack, node) + node = node.Left + } + node = stack[len(stack)-1] + stack = stack[:len(stack)-1] + if node.Right == nil || node.Right == prev { + node.Left, node.Right = node.Right, node.Left //交换 + prev = node + node = nil + } else { + stack = append(stack, node) + node = node.Right + } + } + + return root +} +``` + +层序遍历 + +```go +func invertTree(root *TreeNode) *TreeNode { + if root == nil{ + return root + } + queue := list.New() + node := root + queue.PushBack(node) + for queue.Len() > 0 { + length := queue.Len() + for i := 0; i < length; i++ { + e := queue.Remove(queue.Front()).(*TreeNode) + e.Left, e.Right = e.Right, e.Left //交换 + if e.Left != nil { + queue.PushBack(e.Left) + } + if e.Right != nil { + queue.PushBack(e.Right) + } + } + } + return root +} +``` + +### JavaScript: + +使用递归版本的前序遍历 +```javascript +var invertTree = function(root) { + // 终止条件 + if (!root) { + return null; + } + // 交换左右节点 + const rightNode = root.right; + root.right = invertTree(root.left); + root.left = invertTree(rightNode); + return root; +}; +``` +使用迭代版本(统一模板))的前序遍历: +```javascript +var invertTree = function(root) { + //我们先定义节点交换函数 + const invertNode = function(root, left, right) { + let temp = left; + left = right; + right = temp; + root.left = left; + root.right = right; + } + //使用迭代方法的前序遍历 + let stack = []; + if(root === null) { + return root; + } + stack.push(root); + while(stack.length) { + let node = stack.pop(); + if(node !== null) { + //前序遍历顺序中左右 入栈顺序是前序遍历的倒序右左中 + node.right && stack.push(node.right); + node.left && stack.push(node.left); + stack.push(node); + stack.push(null); + } else { + node = stack.pop(); + //节点处理逻辑 + invertNode(node, node.left, node.right); + } + } + return root; +}; +``` +使用层序遍历: +```javascript +var invertTree = function(root) { + //我们先定义节点交换函数 + const invertNode = function(root, left, right) { + let temp = left; + left = right; + right = temp; + root.left = left; + root.right = right; + } + //使用层序遍历 + let queue = []; + if(root === null) { + return root; + } + queue.push(root); + while(queue.length) { + let length = queue.length; + while(length--) { + let node = queue.shift(); + //节点处理逻辑 + invertNode(node, node.left, node.right); + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + return root; +}; +``` + +### TypeScript: + +递归法: + +```typescript +// 递归法(前序遍历) +function invertTree(root: TreeNode | null): TreeNode | null { + if (root === null) return root; + let tempNode: TreeNode | null = root.left; + root.left = root.right; + root.right = tempNode; + invertTree(root.left); + invertTree(root.right); + return root; +}; + +// 递归法(后序遍历) +function invertTree(root: TreeNode | null): TreeNode | null { + if (root === null) return root; + invertTree(root.left); + invertTree(root.right); + let tempNode: TreeNode | null = root.left; + root.left = root.right; + root.right = tempNode; + return root; +}; + +// 递归法(中序遍历) +function invertTree(root: TreeNode | null): TreeNode | null { + if (root === null) return root; + invertTree(root.left); + let tempNode: TreeNode | null = root.left; + root.left = root.right; + root.right = tempNode; + // 因为左右节点已经进行交换,此时的root.left 是原先的root.right + invertTree(root.left); + return root; +}; +``` + +迭代法: + +```typescript +// 迭代法(栈模拟前序遍历) +function invertTree(root: TreeNode | null): TreeNode | null { + let helperStack: TreeNode[] = []; + let curNode: TreeNode, + tempNode: TreeNode | null; + if (root !== null) helperStack.push(root); + while (helperStack.length > 0) { + curNode = helperStack.pop()!; + // 入栈操作最好在交换节点之前进行,便于理解 + if (curNode.right) helperStack.push(curNode.right); + if (curNode.left) helperStack.push(curNode.left); + tempNode = curNode.left; + curNode.left = curNode.right; + curNode.right = tempNode; + } + return root; +}; + +// 迭代法(栈模拟中序遍历-统一写法形式) +function invertTree(root: TreeNode | null): TreeNode | null { + let helperStack: (TreeNode | null)[] = []; + let curNode: TreeNode | null, + tempNode: TreeNode | null; + if (root !== null) helperStack.push(root); + while (helperStack.length > 0) { + curNode = helperStack.pop(); + if (curNode !== null) { + if (curNode.right !== null) helperStack.push(curNode.right); + helperStack.push(curNode); + helperStack.push(null); + if (curNode.left !== null) helperStack.push(curNode.left); + } else { + curNode = helperStack.pop()!; + tempNode = curNode.left; + curNode.left = curNode.right; + curNode.right = tempNode; + } + } + return root; +}; + +// 迭代法(栈模拟后序遍历-统一写法形式) +function invertTree(root: TreeNode | null): TreeNode | null { + let helperStack: (TreeNode | null)[] = []; + let curNode: TreeNode | null, + tempNode: TreeNode | null; + if (root !== null) helperStack.push(root); + while (helperStack.length > 0) { + curNode = helperStack.pop(); + if (curNode !== null) { + helperStack.push(curNode); + helperStack.push(null); + if (curNode.right !== null) helperStack.push(curNode.right); + if (curNode.left !== null) helperStack.push(curNode.left); + } else { + curNode = helperStack.pop()!; + tempNode = curNode.left; + curNode.left = curNode.right; + curNode.right = tempNode; + } + } + return root; +}; + +// 迭代法(队列模拟层序遍历) +function invertTree(root: TreeNode | null): TreeNode | null { + const helperQueue: TreeNode[] = []; + let curNode: TreeNode, + tempNode: TreeNode | null; + if (root !== null) helperQueue.push(root); + while (helperQueue.length > 0) { + for (let i = 0, length = helperQueue.length; i < length; i++) { + curNode = helperQueue.shift()!; + tempNode = curNode.left; + curNode.left = curNode.right; + curNode.right = tempNode; + if (curNode.left !== null) helperQueue.push(curNode.left); + if (curNode.right !== null) helperQueue.push(curNode.right); + } + } + return root; +}; +``` + +### C: + +递归法 +```c +struct TreeNode* invertTree(struct TreeNode* root){ + if(!root) + return NULL; + //交换结点的左右孩子(中) + struct TreeNode* temp = root->right; + root->right = root->left; + root->left = temp; + 左 + invertTree(root->left); + //右 + invertTree(root->right); + return root; +} +``` + +迭代法:深度优先遍历 +```c +struct TreeNode* invertTree(struct TreeNode* root){ + if(!root) + return NULL; + //存储结点的栈 + struct TreeNode** stack = (struct TreeNode**)malloc(sizeof(struct TreeNode*) * 100); + int stackTop = 0; + //将根节点入栈 + stack[stackTop++] = root; + //若栈中还有元素(进行循环) + while(stackTop) { + //取出栈顶元素 + struct TreeNode* temp = stack[--stackTop]; + //交换结点的左右孩子 + struct TreeNode* tempNode = temp->right; + temp->right = temp->left; + temp->left = tempNode; + //若当前结点有左右孩子,将其入栈 + if(temp->right) + stack[stackTop++] = temp->right; + if(temp->left) + stack[stackTop++] = temp->left; + } + return root; +} +``` + +### Swift: +```swift +// 前序遍历-递归 +func invertTree(_ root: TreeNode?) -> TreeNode? { + guard let root = root else { + return root + } + let tmp = root.left + root.left = root.right + root.right = tmp + let _ = invertTree(root.left) + let _ = invertTree(root.right) + return root +} + +// 层序遍历-迭代 +func invertTree1(_ root: TreeNode?) -> TreeNode? { + guard let root = root else { + return nil + } + var queue = [TreeNode]() + queue.append(root) + while !queue.isEmpty { + let node = queue.removeFirst() + let tmp = node.left + node.left = node.right + node.right = tmp + if let left = node.left { + queue.append(left) + } + if let right = node.right { + queue.append(right) + } + } + return root +} +``` + + +深度优先递归。 + +```swift +func invertTree(_ root: TreeNode?) -> TreeNode? { + guard let node = root else { return root } + swap(&node.left, &node.right) + _ = invertTree(node.left) + _ = invertTree(node.right) + return root +} +``` + +深度优先迭代,子结点顺序不重要,从根结点出发深度遍历即可。 + +```swift +func invertTree(_ root: TreeNode?) -> TreeNode? { + guard let node = root else { return root } + var stack = [node] + while !stack.isEmpty { + guard let node = stack.popLast() else { break } + swap(&node.left, &node.right) + if let node = node.left { stack.append(node) } + if let node = node.right { stack.append(node) } + } + return root +} +``` + +广度优先迭代。 + +```swift +func invertTree(_ root: TreeNode?) -> TreeNode? { + guard let node = root else { return root } + var queue = [node] + while !queue.isEmpty { + let count = queue.count + for _ in 0 ..< count { + let node = queue.removeFirst() + swap(&node.left, &node.right) + if let node = node.left { queue.append(node) } + if let node = node.right { queue.append(node) } + } + } + return root +} +``` +深度优先遍历(前序遍历): +```scala +object Solution { + def invertTree(root: TreeNode): TreeNode = { + if (root == null) return root + // 递归 + def process(node: TreeNode): Unit = { + if (node == null) return + // 翻转节点 + val curNode = node.left + node.left = node.right + node.right = curNode + process(node.left) + process(node.right) + } + process(root) + root + } +} +``` + +广度优先遍历(层序遍历): +```scala +object Solution { + import scala.collection.mutable + def invertTree(root: TreeNode): TreeNode = { + if (root == null) return root + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + while (!queue.isEmpty) { + val len = queue.size + for (i <- 0 until len) { + var curNode = queue.dequeue() + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + // 翻转 + var tmpNode = curNode.left + curNode.left = curNode.right + curNode.right = tmpNode + } + } + root + } +} +``` + +### Rust: + +```rust +impl Solution { + //* 递归 */ + pub fn invert_tree(root: Option>>) -> Option>> { + if let Some(node) = root.as_ref() { + let (left, right) = (node.borrow().left.clone(), node.borrow().right.clone()); + node.borrow_mut().left = Self::invert_tree(right); + node.borrow_mut().right = Self::invert_tree(left); + } + root + } + //* 迭代 */ + pub fn invert_tree(root: Option>>) -> Option>> { + let mut stack = vec![root.clone()]; + while !stack.is_empty() { + if let Some(node) = stack.pop().unwrap() { + let (left, right) = (node.borrow().left.clone(), node.borrow().right.clone()); + stack.push(right.clone()); + stack.push(left.clone()); + node.borrow_mut().left = right; + node.borrow_mut().right = left; + } + } + root + } +} +``` + +### C#: + +```csharp +//递归 +public class Solution { + public TreeNode InvertTree(TreeNode root) { + if (root == null) return root; + + swap(root); + InvertTree(root.left); + InvertTree(root.right); + return root; + } + + public void swap(TreeNode node) { + TreeNode temp = node.left; + node.left = node.right; + node.right = temp; + } +} +``` + +```csharp +//迭代 +public class Solution { +public TreeNode InvertTree(TreeNode root) { + if(root == null) return root; + (root.left,root.right) = (root.right, root.left); + InvertTree(root.left); + InvertTree(root.right); + return root; +} +} +``` + + diff --git "a/problems/0232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" "b/problems/0232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" new file mode 100755 index 0000000000..56698e023f --- /dev/null +++ "b/problems/0232.\347\224\250\346\240\210\345\256\236\347\216\260\351\230\237\345\210\227.md" @@ -0,0 +1,691 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 工作上一定没人这么搞,但是考察对栈、队列理解程度的好题 + +# 232.用栈实现队列 + +[力扣题目链接](https://leetcode.cn/problems/implement-queue-using-stacks/) + +使用栈实现队列的下列操作: + +push(x) -- 将一个元素放入队列的尾部。 +pop() -- 从队列首部移除元素。 +peek() -- 返回队列首部的元素。 +empty() -- 返回队列是否为空。 + + +示例: + +```cpp +MyQueue queue = new MyQueue(); +queue.push(1); +queue.push(2); +queue.peek(); // 返回 1 +queue.pop(); // 返回 1 +queue.empty(); // 返回 false +``` + +说明: + +* 你只能使用标准的栈操作 -- 也就是只有 push to top, peek/pop from top, size, 和 is empty 操作是合法的。 +* 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。 +* 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[栈的基本操作! | LeetCode:232.用栈实现队列](https://www.bilibili.com/video/BV1nY4y1w7VC),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。 + +使用栈来模拟队列的行为,如果仅仅用一个栈,是一定不行的,所以需要两个栈**一个输入栈,一个输出栈**,这里要注意输入栈和输出栈的关系。 + +下面动画模拟以下队列的执行过程: + +执行语句: +queue.push(1); +queue.push(2); +queue.pop(); **注意此时的输出栈的操作** +queue.push(3); +queue.push(4); +queue.pop(); +queue.pop();**注意此时的输出栈的操作** +queue.pop(); +queue.empty(); + +![232.用栈实现队列版本2](https://file1.kamacoder.com/i/algo/232.用栈实现队列版本2.gif) + +在push数据的时候,只要数据放进输入栈就好,**但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入)**,再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。 + +最后如何判断队列为空呢?**如果进栈和出栈都为空的话,说明模拟的队列为空了。** + +在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。 + +C++代码如下: + +```CPP +class MyQueue { +public: + stack stIn; + stack stOut; + /** Initialize your data structure here. */ + MyQueue() { + + } + /** Push element x to the back of queue. */ + void push(int x) { + stIn.push(x); + } + + /** Removes the element from in front of queue and returns that element. */ + int pop() { + // 只有当stOut为空的时候,再从stIn里导入数据(导入stIn全部数据) + if (stOut.empty()) { + // 从stIn导入数据直到stIn为空 + while(!stIn.empty()) { + stOut.push(stIn.top()); + stIn.pop(); + } + } + int result = stOut.top(); + stOut.pop(); + return result; + } + + /** Get the front element. */ + int peek() { + int res = this->pop(); // 直接使用已有的pop函数 + stOut.push(res); // 因为pop函数弹出了元素res,所以再添加回去 + return res; + } + + /** Returns whether the queue is empty. */ + bool empty() { + return stIn.empty() && stOut.empty(); + } +}; + +``` + +* 时间复杂度: 都为O(1)。pop和peek看起来像O(n),实际上一个循环n会被使用n次,最后还是O(1)。 +* 空间复杂度: O(n) + + +## 拓展 + +可以看出peek()的实现,直接复用了pop(), 要不然,对stOut判空的逻辑又要重写一遍。 + +再多说一些代码开发上的习惯问题,在工业级别代码开发中,最忌讳的就是 实现一个类似的函数,直接把代码粘过来改一改就完事了。 + +这样的项目代码会越来越乱,**一定要懂得复用,功能相近的函数要抽象出来,不要大量的复制粘贴,很容易出问题!(踩过坑的人自然懂)** + +工作中如果发现某一个功能自己要经常用,同事们可能也会用到,自己就花点时间把这个功能抽象成一个好用的函数或者工具类,不仅自己方便,也方便了同事们。 + +同事们就会逐渐认可你的工作态度和工作能力,自己的口碑都是这么一点一点积累起来的!在同事圈里口碑起来了之后,你就发现自己走上了一个正循环,以后的升职加薪才少不了你! + + + +## 其他语言版本 + +### Java: + +```java +class MyQueue { + + Stack stackIn; + Stack stackOut; + + /** Initialize your data structure here. */ + public MyQueue() { + stackIn = new Stack<>(); // 负责进栈 + stackOut = new Stack<>(); // 负责出栈 + } + + /** Push element x to the back of queue. */ + public void push(int x) { + stackIn.push(x); + } + + /** Removes the element from in front of queue and returns that element. */ + public int pop() { + dumpstackIn(); + return stackOut.pop(); + } + + /** Get the front element. */ + public int peek() { + dumpstackIn(); + return stackOut.peek(); + } + + /** Returns whether the queue is empty. */ + public boolean empty() { + return stackIn.isEmpty() && stackOut.isEmpty(); + } + + // 如果stackOut为空,那么将stackIn中的元素全部放到stackOut中 + private void dumpstackIn(){ + if (!stackOut.isEmpty()) return; + while (!stackIn.isEmpty()){ + stackOut.push(stackIn.pop()); + } + } +} + +``` + +### Python: + +```python +class MyQueue: + + def __init__(self): + """ + in主要负责push,out主要负责pop + """ + self.stack_in = [] + self.stack_out = [] + + + def push(self, x: int) -> None: + """ + 有新元素进来,就往in里面push + """ + self.stack_in.append(x) + + + def pop(self) -> int: + """ + Removes the element from in front of queue and returns that element. + """ + if self.empty(): + return None + + if self.stack_out: + return self.stack_out.pop() + else: + for i in range(len(self.stack_in)): + self.stack_out.append(self.stack_in.pop()) + return self.stack_out.pop() + + + def peek(self) -> int: + """ + Get the front element. + """ + ans = self.pop() + self.stack_out.append(ans) + return ans + + + def empty(self) -> bool: + """ + 只要in或者out有元素,说明队列不为空 + """ + return not (self.stack_in or self.stack_out) + +``` + +### Go: + +```Go +// 通过切片实现一个栈 +// 由于只是辅助实现算法题目,因此不做异常情况处理 +type MyStack []int + +func (s *MyStack) Push(v int) { + *s = append(*s, v) +} + +func (s *MyStack) Pop() int { + val := (*s)[len(*s)-1] + *s = (*s)[:len(*s)-1] + return val +} + +func (s *MyStack) Peek() int { + return (*s)[len(*s)-1] +} + +func (s *MyStack) Size() int { + return len(*s) +} + +func (s *MyStack) Empty() bool { + return s.Size() == 0 +} + +// ---------- 分界线 ---------- + +type MyQueue struct { + stackIn *MyStack + stackOut *MyStack +} + + +func Constructor() MyQueue { + return MyQueue { + stackIn: &MyStack{}, + stackOut: &MyStack{}, + } +} + + +func (this *MyQueue) Push(x int) { + this.stackIn.Push(x) +} + + +func (this *MyQueue) Pop() int { + this.fillStackOut() + return this.stackOut.Pop() +} + + +func (this *MyQueue) Peek() int { + this.fillStackOut() + return this.stackOut.Peek() +} + + +func (this *MyQueue) Empty() bool { + return this.stackIn.Empty() && this.stackOut.Empty() +} + +// fillStackOut 填充输出栈 +func (this *MyQueue) fillStackOut() { + if this.stackOut.Empty() { + for !this.stackIn.Empty() { + val := this.stackIn.Pop() + this.stackOut.Push(val) + } + } +} +``` + +### JavaScript: + + ```js + // 使用两个数组的栈方法(push, pop) 实现队列 + /** + * Initialize your data structure here. + */ +var MyQueue = function() { + this.stackIn = []; + this.stackOut = []; +}; + +/** + * Push element x to the back of queue. + * @param {number} x + * @return {void} + */ +MyQueue.prototype.push = function(x) { + this.stackIn.push(x); +}; + +/** + * Removes the element from in front of queue and returns that element. + * @return {number} + */ +MyQueue.prototype.pop = function() { + const size = this.stackOut.length; + if(size) { + return this.stackOut.pop(); + } + while(this.stackIn.length) { + this.stackOut.push(this.stackIn.pop()); + } + return this.stackOut.pop(); +}; + +/** + * Get the front element. + * @return {number} + */ +MyQueue.prototype.peek = function() { + const x = this.pop(); + this.stackOut.push(x); + return x; +}; + +/** + * Returns whether the queue is empty. + * @return {boolean} + */ +MyQueue.prototype.empty = function() { + return !this.stackIn.length && !this.stackOut.length +}; + ``` + +### TypeScript: + +```typescript +class MyQueue { + private stackIn: number[] + private stackOut: number[] + constructor() { + this.stackIn = []; + this.stackOut = []; + } + + push(x: number): void { + this.stackIn.push(x); + } + + pop(): number { + if (this.stackOut.length === 0) { + while (this.stackIn.length > 0) { + this.stackOut.push(this.stackIn.pop()!); + } + } + return this.stackOut.pop()!; + } + + peek(): number { + let temp: number = this.pop(); + this.stackOut.push(temp); + return temp; + } + + empty(): boolean { + return this.stackIn.length === 0 && this.stackOut.length === 0; + } +} +``` + +### Swift: + +```swift +class MyQueue { + + var stackIn = [Int]() + var stackOut = [Int]() + + init() {} + + /** Push element x to the back of queue. */ + func push(_ x: Int) { + stackIn.append(x) + } + + /** Removes the element from in front of queue and returns that element. */ + func pop() -> Int { + if stackOut.isEmpty { + while !stackIn.isEmpty { + stackOut.append(stackIn.popLast()!) + } + } + return stackOut.popLast() ?? -1 + } + + /** Get the front element. */ + func peek() -> Int { + let res = pop() + stackOut.append(res) + return res + } + + /** Returns whether the queue is empty. */ + func empty() -> Bool { + return stackIn.isEmpty && stackOut.isEmpty + } +} +``` + +### C: + +```C +/* +1.两个type为int的数组(栈),大小为100 + 第一个栈stackIn用来存放数据,第二个栈stackOut作为辅助用来输出数据 +2.两个指针stackInTop和stackOutTop,分别指向栈顶 +*/ +typedef struct { + int stackInTop, stackOutTop; + int stackIn[100], stackOut[100]; +} MyQueue; + +/* +1.开辟一个队列的大小空间 +2.将指针stackInTop和stackOutTop初始化为0 +3.返回开辟的队列 +*/ +MyQueue* myQueueCreate() { + MyQueue* queue = (MyQueue*)malloc(sizeof(MyQueue)); + queue->stackInTop = 0; + queue->stackOutTop = 0; + return queue; +} + +/* +将元素存入第一个栈中,存入后栈顶指针+1 +*/ +void myQueuePush(MyQueue* obj, int x) { + obj->stackIn[(obj->stackInTop)++] = x; +} + +/* +1.若输出栈为空且当第一个栈中有元素(stackInTop>0时),将第一个栈中元素复制到第二个栈中(stackOut[stackTop2++] = stackIn[--stackTop1]) +2.将栈顶元素保存 +3.当stackTop2>0时,将第二个栈中元素复制到第一个栈中(stackIn[stackTop1++] = stackOut[--stackTop2]) +*/ +int myQueuePop(MyQueue* obj) { + //优化:复制栈顶指针,减少对内存的访问次数 + int stackInTop = obj->stackInTop; + int stackOutTop = obj->stackOutTop; + //若输出栈为空 + if(stackOutTop == 0) { + //将第一个栈中元素复制到第二个栈中 + while(stackInTop > 0) { + obj->stackOut[stackOutTop++] = obj->stackIn[--stackInTop]; + } + } + //将第二个栈中栈顶元素(队列的第一个元素)出栈,并保存 + int top = obj->stackOut[--stackOutTop]; + //将输出栈中元素放回输入栈中 + while(stackOutTop > 0) { + obj->stackIn[stackInTop++] = obj->stackOut[--stackOutTop]; + } + //更新栈顶指针 + obj->stackInTop = stackInTop; + obj->stackOutTop = stackOutTop; + //返回队列中第一个元素 + return top; +} + +//返回输入栈中的栈底元素 +int myQueuePeek(MyQueue* obj) { + return obj->stackIn[0]; +} + +//若栈顶指针均为0,则代表队列为空 +bool myQueueEmpty(MyQueue* obj) { + return obj->stackInTop == 0 && obj->stackOutTop == 0; +} + +//将栈顶指针置0 +void myQueueFree(MyQueue* obj) { + obj->stackInTop = 0; + obj->stackOutTop = 0; +} +``` + +### C#: + +```csharp +public class MyQueue { + Stack inStack; + Stack outStack; + + public MyQueue() { + inStack = new Stack();// 负责进栈 + outStack = new Stack();// 负责出栈 + } + + public void Push(int x) { + inStack.Push(x); + } + + public int Pop() { + dumpstackIn(); + return outStack.Pop(); + } + + public int Peek() { + dumpstackIn(); + return outStack.Peek(); + } + + public bool Empty() { + return inStack.Count == 0 && outStack.Count == 0; + } + + // 处理方法: + // 如果outStack为空,那么将inStack中的元素全部放到outStack中 + private void dumpstackIn(){ + if (outStack.Count != 0) return; + while(inStack.Count != 0){ + outStack.Push(inStack.Pop()); + } + } +} + +``` + + + +### PHP: + +```php +// SplStack 类通过使用一个双向链表来提供栈的主要功能。[PHP 5 >= 5.3.0, PHP 7, PHP 8] +// https://www.php.net/manual/zh/class.splstack.php +class MyQueue { + // 双栈模拟队列:In栈存储数据;Out栈辅助处理 + private $stackIn; + private $stackOut; + + function __construct() { + $this->stackIn = new SplStack(); + $this->stackOut = new SplStack(); + } + + // In: 1 2 3 <= push + function push($x) { + $this->stackIn->push($x); + } + + function pop() { + $this->peek(); + return $this->stackOut->pop(); + } + + function peek() { + if($this->stackOut->isEmpty()){ + $this->shift(); + } + return $this->stackOut->top(); + } + + function empty() { + return $this->stackOut->isEmpty() && $this->stackIn->isEmpty(); + } + + // 如果Out栈为空,把In栈数据压入Out栈 + // In: 1 2 3 => pop push => 1 2 3 :Out + private function shift(){ + while(!$this->stackIn->isEmpty()){ + $this->stackOut->push($this->stackIn->pop()); + } + } + } +``` + +### Scala: + +```scala +class MyQueue() { + import scala.collection.mutable + val stackIn = mutable.Stack[Int]() // 负责出栈 + val stackOut = mutable.Stack[Int]() // 负责入栈 + + // 添加元素 + def push(x: Int) { + stackIn.push(x) + } + + // 复用代码,如果stackOut为空就把stackIn的所有元素都压入StackOut + def dumpStackIn(): Unit = { + if (!stackOut.isEmpty) return + while (!stackIn.isEmpty) { + stackOut.push(stackIn.pop()) + } + } + + // 弹出元素 + def pop(): Int = { + dumpStackIn() + stackOut.pop() + } + + // 获取队头 + def peek(): Int = { + dumpStackIn() + val res: Int = stackOut.pop() + stackOut.push(res) + res + } + + // 判断是否为空 + def empty(): Boolean = { + stackIn.isEmpty && stackOut.isEmpty + } + +} +``` + +### Rust: + +```rust +struct MyQueue { + stack_in: Vec, + stack_out: Vec, +} +impl MyQueue { + fn new() -> Self { + MyQueue { + stack_in: Vec::new(), + stack_out: Vec::new(), + } + + } + + fn push(&mut self, x: i32) { + self.stack_in.push(x); + } + + fn pop(&mut self) -> i32 { + if self.stack_out.is_empty(){ + while !self.stack_in.is_empty() { + self.stack_out.push(self.stack_in.pop().unwrap()); + } + } + self.stack_out.pop().unwrap() + } + + fn peek(&mut self) -> i32 { + let res = self.pop(); + self.stack_out.push(res); + res + } + + fn empty(&self) -> bool { + self.stack_in.is_empty() && self.stack_out.is_empty() + } +} +``` + diff --git "a/problems/0234.\345\233\236\346\226\207\351\223\276\350\241\250.md" "b/problems/0234.\345\233\236\346\226\207\351\223\276\350\241\250.md" new file mode 100755 index 0000000000..6248861d94 --- /dev/null +++ "b/problems/0234.\345\233\236\346\226\207\351\223\276\350\241\250.md" @@ -0,0 +1,429 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 234.回文链表 + +[力扣题目链接](https://leetcode.cn/problems/palindrome-linked-list/) + +请判断一个链表是否为回文链表。 + +示例 1: +* 输入: 1->2 +* 输出: false + +示例 2: +* 输入: 1->2->2->1 +* 输出: true + + +## 思路 + +### 数组模拟 + +最直接的想法,就是把链表装成数组,然后再判断是否回文。 + +代码也比较简单。如下: + +```CPP +class Solution { +public: + bool isPalindrome(ListNode* head) { + vector vec; + ListNode* cur = head; + while (cur) { + vec.push_back(cur->val); + cur = cur->next; + } + // 比较数组回文 + for (int i = 0, j = vec.size() - 1; i < j; i++, j--) { + if (vec[i] != vec[j]) return false; + } + return true; + } +}; +``` + +上面代码可以在优化,就是先求出链表长度,然后给定vector的初始长度,这样避免vector每次添加节点重新开辟空间 + +```CPP +class Solution { +public: + bool isPalindrome(ListNode* head) { + + ListNode* cur = head; + int length = 0; + while (cur) { + length++; + cur = cur->next; + } + vector vec(length, 0); // 给定vector的初始长度,这样避免vector每次添加节点重新开辟空间 + cur = head; + int index = 0; + while (cur) { + vec[index++] = cur->val; + cur = cur->next; + } + // 比较数组回文 + for (int i = 0, j = vec.size() - 1; i < j; i++, j--) { + if (vec[i] != vec[j]) return false; + } + return true; + } +}; + +``` + +### 反转后半部分链表 + +分为如下几步: + +* 用快慢指针,快指针有两步,慢指针走一步,快指针遇到终止位置时,慢指针就在链表中间位置 +* 同时用pre记录慢指针指向节点的前一个节点,用来分割链表 +* 将链表分为前后均等两部分,如果链表长度是奇数,那么后半部分多一个节点 +* 将后半部分反转 ,得cur2,前半部分为cur1 +* 按照cur1的长度,一次比较cur1和cur2的节点数值 + +如图所示: + + + +代码如下: + +```CPP +class Solution { +public: + bool isPalindrome(ListNode* head) { + if (head == nullptr || head->next == nullptr) return true; + ListNode* slow = head; // 慢指针,找到链表中间分位置,作为分割 + ListNode* fast = head; + ListNode* pre = head; // 记录慢指针的前一个节点,用来分割链表 + while (fast && fast->next) { + pre = slow; + slow = slow->next; + fast = fast->next->next; + } + pre->next = nullptr; // 分割链表 + + ListNode* cur1 = head; // 前半部分 + ListNode* cur2 = reverseList(slow); // 反转后半部分,总链表长度如果是奇数,cur2比cur1多一个节点 + + // 开始两个链表的比较 + while (cur1) { + if (cur1->val != cur2->val) return false; + cur1 = cur1->next; + cur2 = cur2->next; + } + return true; + } + // 反转链表 + ListNode* reverseList(ListNode* head) { + ListNode* temp; // 保存cur的下一个节点 + ListNode* cur = head; + ListNode* pre = nullptr; + while(cur) { + temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next + cur->next = pre; // 翻转操作 + // 更新pre 和 cur指针 + pre = cur; + cur = temp; + } + return pre; + } +}; +``` + + +## 其他语言版本 + +### Java + +```java +// 方法一,使用数组 +class Solution { + public boolean isPalindrome(ListNode head) { + int len = 0; + // 统计链表长度 + ListNode cur = head; + while (cur != null) { + len++; + cur = cur.next; + } + cur = head; + int[] res = new int[len]; + // 将元素加到数组之中 + for (int i = 0; i < res.length; i++){ + res[i] = cur.val; + cur = cur.next; + } + // 比较回文 + for (int i = 0, j = len - 1; i < j; i++, j--){ + if (res[i] != res[j]){ + return false; + } + } + return true; + } +} + +// 方法二,快慢指针 +class Solution { + public boolean isPalindrome(ListNode head) { + // 如果为空或者仅有一个节点,返回true + if (head == null && head.next == null) return true; + ListNode slow = head; + ListNode fast = head; + ListNode pre = head; + while (fast != null && fast.next != null){ + pre = slow; // 记录slow的前一个结点 + slow = slow.next; + fast = fast.next.next; + } + pre.next = null; // 分割两个链表 + + // 前半部分 + ListNode cur1 = head; + // 后半部分。这里使用了反转链表 + ListNode cur2 = reverseList(slow); + + while (cur1 != null){ + if (cur1.val != cur2.val) return false; + + // 注意要移动两个结点 + cur1 = cur1.next; + cur2 = cur2.next; + } + return true; + } + ListNode reverseList(ListNode head){ + // 反转链表 + ListNode tmp = null; + ListNode pre = null; + while (head != null){ + tmp = head.next; + head.next = pre; + pre = head; + head = tmp; + } + return pre; + } +} +``` + +### Python + +```python +#数组模拟 +class Solution: + def isPalindrome(self, head: Optional[ListNode]) -> bool: + list=[] + while head: + list.append(head.val) + head=head.next + l,r=0, len(list)-1 + while l<=r: + if list[l]!=list[r]: + return False + l+=1 + r-=1 + return True + +#反转后半部分链表 +class Solution: + def isPalindrome(self, head: Optional[ListNode]) -> bool: + fast = slow = head + + # find mid point which including (first) mid point into the first half linked list + while fast and fast.next: + fast = fast.next.next + slow = slow.next + node = None + + # reverse second half linked list + while slow: + slow.next, slow, node = node, slow.next, slow + + # compare reversed and original half; must maintain reversed linked list is shorter than 1st half + while node: + if node.val != head.val: + return False + node = node.next + head = head.next + return True +``` + +### Go + +```go +/** + * Definition for singly-linked list. + * type ListNode struct { + * Val int + * Next *ListNode + * } + */ +//方法一,使用数组 +func isPalindrome(head *ListNode) bool{ + //计算切片长度,避免切片频繁扩容 + cur,ln:=head,0 + for cur!=nil{ + ln++ + cur=cur.Next + } + nums:=make([]int,ln) + index:=0 + for head!=nil{ + nums[index]=head.Val + index++ + head=head.Next + } + //比较回文切片 + for i,j:=0,ln-1;i<=j;i,j=i+1,j-1{ + if nums[i]!=nums[j]{return false} + } + return true +} + +// 方法二,快慢指针 +func isPalindrome(head *ListNode) bool { + if head==nil&&head.Next==nil{return true} + //慢指针,找到链表中间分位置,作为分割 + slow:=head + fast:=head + //记录慢指针的前一个节点,用来分割链表 + pre:=head + for fast!=nil && fast.Next!=nil{ + pre=slow + slow=slow.Next + fast=fast.Next.Next + } + //分割链表 + pre.Next=nil + //前半部分 + cur1:=head + //反转后半部分,总链表长度如果是奇数,cur2比cur1多一个节点 + cur2:=ReverseList(slow) + + //开始两个链表的比较 + for cur1!=nil{ + if cur1.Val!=cur2.Val{return false} + cur1=cur1.Next + cur2=cur2.Next + } + return true +} +//反转链表 +func ReverseList(head *ListNode) *ListNode{ + var pre *ListNode + cur:=head + for cur!=nil{ + tmp:=cur.Next + cur.Next=pre + pre=cur + cur=tmp + } + return pre +} +``` + +### JavaScript + +```js +var isPalindrome = function(head) { + const reverseList = head => {// 反转链表 + let temp = null; + let pre = null; + while(head != null){ + temp = head.next; + head.next = pre; + pre = head; + head = temp; + } + return pre; + } + // 如果为空或者仅有一个节点,返回true + if(!head && !head.next) return true; + let slow = head; + let fast = head; + let pre = head; + while(fast != null && fast.next != null){ + pre = slow; // 记录slow的前一个结点 + slow = slow.next; + fast = fast.next.next; + } + pre.next = null; // 分割两个链表 + // 前半部分 + let cur1 = head; + // 后半部分。这里使用了反转链表 + let cur2 = reverseList(slow); + while(cur1 != null){ + if(cur1.val != cur2.val) return false; + // 注意要移动两个结点 + cur1 = cur1.next; + cur2 = cur2.next; + } + return true; +}; +``` + +### TypeScript + +> 数组模拟 + +```typescript +function isPalindrome(head: ListNode | null): boolean { + const helperArr: number[] = []; + let curNode: ListNode | null = head; + while (curNode !== null) { + helperArr.push(curNode.val); + curNode = curNode.next; + } + let left: number = 0, + right: number = helperArr.length - 1; + while (left < right) { + if (helperArr[left++] !== helperArr[right--]) return false; + } + return true; +}; +``` + +> 反转后半部分链表 + +```typescript +function isPalindrome(head: ListNode | null): boolean { + if (head === null || head.next === null) return true; + let fastNode: ListNode | null = head, + slowNode: ListNode = head, + preNode: ListNode = head; + while (fastNode !== null && fastNode.next !== null) { + preNode = slowNode; + slowNode = slowNode.next!; + fastNode = fastNode.next.next; + } + preNode.next = null; + let cur1: ListNode | null = head; + let cur2: ListNode | null = reverseList(slowNode); + while (cur1 !== null) { + if (cur1.val !== cur2!.val) return false; + cur1 = cur1.next; + cur2 = cur2!.next; + } + return true; +}; +function reverseList(head: ListNode | null): ListNode | null { + let curNode: ListNode | null = head, + preNode: ListNode | null = null; + while (curNode !== null) { + let tempNode: ListNode | null = curNode.next; + curNode.next = preNode; + preNode = curNode; + curNode = tempNode; + } + return preNode; +} +``` + + + + diff --git "a/problems/0235.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" "b/problems/0235.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" new file mode 100755 index 0000000000..a1fe78d169 --- /dev/null +++ "b/problems/0235.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" @@ -0,0 +1,548 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 235. 二叉搜索树的最近公共祖先 + +[力扣题目链接](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/) + +给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 + +百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” + +例如,给定如下二叉搜索树:  root = [6,2,8,0,4,7,9,null,null,3,5] + + +![235. 二叉搜索树的最近公共祖先](https://file1.kamacoder.com/i/algo/20201018172243602.png) + +示例 1: + +* 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 +* 输出: 6 +* 解释: 节点 2 和节点 8 的最近公共祖先是 6。 + +示例 2: + +* 输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 +* 输出: 2 +* 解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。 + + +说明: + +* 所有节点的值都是唯一的。 +* p、q 为不同节点且均存在于给定的二叉搜索树中。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[二叉搜索树找祖先就有点不一样了!| 235. 二叉搜索树的最近公共祖先](https://www.bilibili.com/video/BV1Zt4y1F7ww?share_source=copy_web),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +做过[二叉树:公共祖先问题](https://programmercarl.com/0236.二叉树的最近公共祖先.html)题目的同学应该知道,利用回溯从底向上搜索,遇到一个节点的左子树里有p,右子树里有q,那么当前节点就是最近公共祖先。 + +那么本题是二叉搜索树,二叉搜索树是有序的,那得好好利用一下这个特点。 + +在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢? + +因为是有序树,所以 如果 中间节点是 q 和 p 的公共祖先,那么 中节点的数组 一定是在 [p, q]区间的。即 中节点 > p && 中节点 < q 或者 中节点 > q && 中节点 < p。 + +那么只要从上到下去遍历,遇到 cur节点是数值在[p, q]区间中则一定可以说明该节点cur就是p 和 q的公共祖先。 那问题来了,**一定是最近公共祖先吗**? + +如图,我们从根节点搜索,第一次遇到 cur节点是数值在[q, p]区间中,即 节点5,此时可以说明 q 和 p 一定分别存在于 节点 5的左子树,和右子树中。 + +![235.二叉搜索树的最近公共祖先](https://file1.kamacoder.com/i/algo/20220926164214.png) + +此时节点5是不是最近公共祖先? 如果 从节点5继续向左遍历,那么将错过成为p的祖先, 如果从节点5继续向右遍历则错过成为q的祖先。 + +所以当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[q, p]区间中,那么cur就是 q和p的最近公共祖先。 + +理解这一点,本题就很好解了。 + +而递归遍历顺序,本题就不涉及到 前中后序了(这里没有中节点的处理逻辑,遍历顺序无所谓了)。 + +如图所示:p为节点6,q为节点9 + +![235.二叉搜索树的最近公共祖先2](https://file1.kamacoder.com/i/algo/20220926165141.png) + + +可以看出直接按照指定的方向,就可以找到节点8,为最近公共祖先,而且不需要遍历整棵树,找到结果直接返回! + +### 递归法 + +递归三部曲如下: + +* 确定递归函数返回值以及参数 + +参数就是当前节点,以及两个结点 p、q。 + +返回值是要返回最近公共祖先,所以是TreeNode * 。 + +代码如下: + +``` +TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) +``` + +* 确定终止条件 + +遇到空返回就可以了,代码如下: + +``` +if (cur == NULL) return cur; +``` + +其实都不需要这个终止条件,因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。 + +* 确定单层递归的逻辑 + +在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭右闭) + +那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。 + +**需要注意的是此时不知道p和q谁大,所以两个都要判断** + +代码如下: + +```CPP +if (cur->val > p->val && cur->val > q->val) { + TreeNode* left = traversal(cur->left, p, q); + if (left != NULL) { + return left; + } +} +``` + +**细心的同学会发现,在这里调用递归函数的地方,把递归函数的返回值left,直接return**。 + + +在[二叉树:公共祖先问题](https://programmercarl.com/0236.二叉树的最近公共祖先.html)中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。 + +搜索一条边的写法: + +``` +if (递归函数(root->left)) return ; +if (递归函数(root->right)) return ; +``` + +搜索整个树写法: + +``` +left = 递归函数(root->left); +right = 递归函数(root->right); +left与right的逻辑处理; +``` + +本题就是标准的搜索一条边的写法,遇到递归函数的返回值,如果不为空,立刻返回。 + + +如果 cur->val 小于 p->val,同时 cur->val 小于 q->val,那么就应该向右遍历(目标区间在右子树)。 + +```CPP +if (cur->val < p->val && cur->val < q->val) { + TreeNode* right = traversal(cur->right, p, q); + if (right != NULL) { + return right; + } +} +``` + +剩下的情况,就是cur节点在区间(p->val <= cur->val && cur->val <= q->val)或者 (q->val <= cur->val && cur->val <= p->val)中,那么cur就是最近公共祖先了,直接返回cur。 + +代码如下: + +``` +return cur; +``` + +那么整体递归代码如下: + +```CPP +class Solution { +private: + TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) { + if (cur == NULL) return cur; + // 中 + if (cur->val > p->val && cur->val > q->val) { // 左 + TreeNode* left = traversal(cur->left, p, q); + if (left != NULL) { + return left; + } + } + + if (cur->val < p->val && cur->val < q->val) { // 右 + TreeNode* right = traversal(cur->right, p, q); + if (right != NULL) { + return right; + } + } + return cur; + } +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + return traversal(root, p, q); + } +}; +``` + +精简后代码如下: + +```CPP +class Solution { +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + if (root->val > p->val && root->val > q->val) { + return lowestCommonAncestor(root->left, p, q); + } else if (root->val < p->val && root->val < q->val) { + return lowestCommonAncestor(root->right, p, q); + } else return root; + } +}; +``` + +### 迭代法 + +对于二叉搜索树的迭代法,大家应该在[二叉树:二叉搜索树登场!](https://programmercarl.com/0700.二叉搜索树中的搜索.html)就了解了。 + +利用其有序性,迭代的方式还是比较简单的,解题思路在递归中已经分析了。 + +迭代代码如下: + +```CPP +class Solution { +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + while(root) { + if (root->val > p->val && root->val > q->val) { + root = root->left; + } else if (root->val < p->val && root->val < q->val) { + root = root->right; + } else return root; + } + return NULL; + } +}; +``` + +灵魂拷问:是不是又被简单的迭代法感动到痛哭流涕? + +## 总结 + +对于二叉搜索树的最近祖先问题,其实要比[普通二叉树公共祖先问题](https://programmercarl.com/0236.二叉树的最近公共祖先.html)简单的多。 + +不用使用回溯,二叉搜索树自带方向性,可以方便的从上向下查找目标区间,遇到目标区间内的节点,直接返回。 + +最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。 + + +## 其他语言版本 + + +### Java + +递归法: +```java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root.val > p.val && root.val > q.val) return lowestCommonAncestor(root.left, p, q); + if (root.val < p.val && root.val < q.val) return lowestCommonAncestor(root.right, p, q); + return root; + } +} +``` + +迭代法: +```java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + while (true) { + if (root.val > p.val && root.val > q.val) { + root = root.left; + } else if (root.val < p.val && root.val < q.val) { + root = root.right; + } else { + break; + } + } + return root; + } +} +``` + + +### Python + +递归法(版本一) +```python +class Solution: + def traversal(self, cur, p, q): + if cur is None: + return cur + # 中 + if cur.val > p.val and cur.val > q.val: # 左 + left = self.traversal(cur.left, p, q) + if left is not None: + return left + + if cur.val < p.val and cur.val < q.val: # 右 + right = self.traversal(cur.right, p, q) + if right is not None: + return right + + return cur + + def lowestCommonAncestor(self, root, p, q): + return self.traversal(root, p, q) +``` + +递归法(版本二)精简 +```python +class Solution: + def lowestCommonAncestor(self, root, p, q): + if root.val > p.val and root.val > q.val: + return self.lowestCommonAncestor(root.left, p, q) + elif root.val < p.val and root.val < q.val: + return self.lowestCommonAncestor(root.right, p, q) + else: + return root + +``` + +迭代法 +```python +class Solution: + def lowestCommonAncestor(self, root, p, q): + while root: + if root.val > p.val and root.val > q.val: + root = root.left + elif root.val < p.val and root.val < q.val: + root = root.right + else: + return root + return None + + +``` +### Go + +递归法 +```go +func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { + if root.Val > p.Val && root.Val > q.Val { + return lowestCommonAncestor(root.Left, p, q) + } else if root.Val < p.Val && root.Val < q.Val { + return lowestCommonAncestor(root.Right, p, q) + } else { + return root + } +} +``` + +迭代法 +```go +func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { + for root != nil { + if root.Val > p.Val && root.Val > q.Val { + root = root.Left + } else if root.Val < p.Val && root.Val < q.Val { + root = root.Right + } else { + return root + } + } + return nil +} +``` + +### JavaScript + +递归法: +```javascript +var lowestCommonAncestor = function(root, p, q) { + // 使用递归的方法 + // 1. 使用给定的递归函数lowestCommonAncestor + // 2. 确定递归终止条件 + if(root === null) { + return root; + } + if(root.val > p.val && root.val > q.val) { + // 向左子树查询 + return root.left = lowestCommonAncestor(root.left,p,q); + } + if(root.val < p.val && root.val < q.val) { + // 向右子树查询 + return root.right = lowestCommonAncestor(root.right,p,q); + } + return root; +}; +``` + +迭代法 +```javascript +var lowestCommonAncestor = function(root, p, q) { + // 使用迭代的方法 + while(root) { + if(root.val > p.val && root.val > q.val) { + root = root.left; + }else if(root.val < p.val && root.val < q.val) { + root = root.right; + }else { + return root; + } + + } + return null; +}; +``` + +### TypeScript + +> 递归法: + +```typescript +function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null { + if (root.val > p.val && root.val > q.val) + return lowestCommonAncestor(root.left, p, q); + if (root.val < p.val && root.val < q.val) + return lowestCommonAncestor(root.right, p, q); + return root; +}; +``` + +> 迭代法: + +```typescript +function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null { + while (root !== null) { + if (root.val > p.val && root.val > q.val) { + root = root.left; + } else if (root.val < p.val && root.val < q.val) { + root = root.right; + } else { + return root; + }; + }; + return null; +}; +``` + +### Scala + +递归: + +```scala +object Solution { + def lowestCommonAncestor(root: TreeNode, p: TreeNode, q: TreeNode): TreeNode = { + // scala中每个关键字都有其返回值,于是可以不写return + if (root.value > p.value && root.value > q.value) lowestCommonAncestor(root.left, p, q) + else if (root.value < p.value && root.value < q.value) lowestCommonAncestor(root.right, p, q) + else root + } +} +``` + +迭代: + +```scala +object Solution { + def lowestCommonAncestor(root: TreeNode, p: TreeNode, q: TreeNode): TreeNode = { + var curNode = root // 因为root是不可变量,所以要赋值给curNode一个可变量 + while(curNode != null){ + if(curNode.value > p.value && curNode.value > q.value) curNode = curNode.left + else if(curNode.value < p.value && curNode.value < q.value) curNode = curNode.right + else return curNode + } + null + } +} +``` + +### Rust + +递归: + +```rust +impl Solution { + pub fn lowest_common_ancestor( + root: Option>>, + p: Option>>, + q: Option>>, + ) -> Option>> { + let q_val = q.as_ref().unwrap().borrow().val; + let p_val = p.as_ref().unwrap().borrow().val; + let root_val = root.as_ref().unwrap().borrow().val; + + if root_val > q_val && root_val > p_val { + return Self::lowest_common_ancestor( + root.as_ref().unwrap().borrow().left.clone(), + p, + q, + ); + }; + + if root_val < q_val && root_val < p_val { + return Self::lowest_common_ancestor( + root.as_ref().unwrap().borrow().right.clone(), + p, + q, + ); + } + root + } +} +``` + +迭代: + +```rust +impl Solution { + pub fn lowest_common_ancestor( + mut root: Option>>, + p: Option>>, + q: Option>>, + ) -> Option>> { + let p_val = p.unwrap().borrow().val; + let q_val = q.unwrap().borrow().val; + while let Some(node) = root.clone() { + let root_val = node.borrow().val; + if root_val > q_val && root_val > p_val { + root = node.borrow().left.clone(); + } else if root_val < q_val && root_val < p_val { + root = node.borrow().right.clone(); + } else { + return root; + } + } + None + } +} +``` +### C# +```csharp +// 递归 +public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) +{ + if (root.val > p.val && root.val > q.val) + return LowestCommonAncestor(root.left, p, q); + if (root.val < p.val && root.val < q.val) + return LowestCommonAncestor(root.right, p, q); + return root; +} +// 迭代 +public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) +{ + while (root != null) + { + if (root.val > p.val && root.val > q.val) + root = root.left; + else if (root.val < p.val && root.val < q.val) + root = root.right; + else return root; + } + return null; +} +``` + + + diff --git "a/problems/0236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" "b/problems/0236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" new file mode 100755 index 0000000000..5044e3ba00 --- /dev/null +++ "b/problems/0236.\344\272\214\345\217\211\346\240\221\347\232\204\346\234\200\350\277\221\345\205\254\345\205\261\347\245\226\345\205\210.md" @@ -0,0 +1,491 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 本来是打算将二叉树和二叉搜索树的公共祖先问题一起讲,后来发现篇幅过长了,只能先说一说二叉树的公共祖先问题。 + +# 236. 二叉树的最近公共祖先 + +[力扣题目链接](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-tree/) + +给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 + +百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。” + +例如,给定如下二叉树:  root = [3,5,1,6,2,0,8,null,null,7,4] + + +![236. 二叉树的最近公共祖先](https://file1.kamacoder.com/i/algo/20201016173414722.png) + +示例 1: +输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 +输出: 3 +解释: 节点 5 和节点 1 的最近公共祖先是节点 3。 + +示例 2: +输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 +输出: 5 +解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 + +说明: +* 所有节点的值都是唯一的。 +* p、q 为不同节点且均存在于给定的二叉树中。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[自底向上查找,有点难度! | LeetCode:236. 二叉树的最近公共祖先](https://www.bilibili.com/video/BV1jd4y1B7E2),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。 + +那么二叉树如何可以自底向上查找呢? + +回溯啊,二叉树回溯的过程就是从底到上。 + +后序遍历(左右中)就是天然的回溯过程,可以根据左右子树的返回值,来处理中节点的逻辑。 + +接下来就看如何判断一个节点是节点q和节点p的公共祖先呢。 + +**首先最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。** 即情况一: + +![](https://file1.kamacoder.com/i/algo/20220922173502.png) + +判断逻辑是 如果递归遍历遇到q,就将q返回,遇到p 就将p返回,那么如果 左右子树的返回值都不为空,说明此时的中节点,一定是q 和p 的最近祖先。 + +那么有录友可能疑惑,会不会左子树 遇到q 返回,右子树也遇到q返回,这样并没有找到 q 和p的最近祖先。 + +这么想的录友,要审题了,题目强调:**二叉树节点数值是不重复的,而且一定存在 q 和 p**。 + +**但是很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)。** 情况二: + +![](https://file1.kamacoder.com/i/algo/20220922173530.png) + +其实情况一 和 情况二 代码实现过程都是一样的,也可以说,实现情况一的逻辑,顺便包含了情况二。 + +因为遇到 q 或者 p 就返回,这样也包含了 q 或者 p 本身就是 公共祖先的情况。 + +这一点是很多录友容易忽略的,在下面的代码讲解中,可以再去体会。 + +递归三部曲: + +* 确定递归函数返回值以及参数 + +需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。 + +但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。 + +代码如下: + +```CPP +TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) +``` + +* 确定终止条件 + +遇到空的话,因为树都是空了,所以返回空。 + +那么我们来说一说,如果 root == q,或者 root == p,说明找到 q p ,则将其返回,这个返回值,后面在中节点的处理过程中会用到,那么中节点的处理逻辑,下面讲解。 + +代码如下: + +```CPP +if (root == q || root == p || root == NULL) return root; +``` + + + +* 确定单层递归逻辑 + +值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。 + +我们在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html)中说了 递归函数有返回值就是要遍历某一条边,但有返回值也要看如何处理返回值! + +如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢? + +搜索一条边的写法: + +```CPP +if (递归函数(root->left)) return ; + +if (递归函数(root->right)) return ; +``` + +搜索整个树写法: + +```CPP +left = 递归函数(root->left); // 左 +right = 递归函数(root->right); // 右 +left与right的逻辑处理; // 中 +``` + +看出区别了没? + +**在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)**。 + +那么为什么要遍历整棵树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。 + +如图: + +![236.二叉树的最近公共祖先](https://file1.kamacoder.com/i/algo/2021020415105872.png) + +就像图中一样直接返回7。 + +但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。 + +因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回。 + +```CPP +left = 递归函数(root->left); // 左 +right = 递归函数(root->right); // 右 +left与right的逻辑处理; // 中 +``` + +所以此时大家要知道我们要遍历整棵树。知道这一点,对本题就有一定深度的理解了。 + + +那么先用left和right接住左子树和右子树的返回值,代码如下: + +```CPP +TreeNode* left = lowestCommonAncestor(root->left, p, q); +TreeNode* right = lowestCommonAncestor(root->right, p, q); + +``` + +**如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解** + +**如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然**。 + +这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢? + +如图: + +![236.二叉树的最近公共祖先1](https://file1.kamacoder.com/i/algo/20210204151125844.png) + +图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去! + +这里也很重要,可能刷过这道题目的同学,都不清楚结果究竟是如何从底层一层一层传到头结点的。 + +那么如果left和right都为空,则返回left或者right都是可以的,也就是返回空。 + +代码如下: + +```CPP +if (left == NULL && right != NULL) return right; +else if (left != NULL && right == NULL) return left; +else { // (left == NULL && right == NULL) + return NULL; +} + +``` + +那么寻找最小公共祖先,完整流程图如下: + +![236.二叉树的最近公共祖先2](https://file1.kamacoder.com/i/algo/202102041512582.png) + +**从图中,大家可以看到,我们是如何回溯遍历整棵二叉树,将结果返回给头结点的!** + +整体代码如下: + +```CPP +class Solution { +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + if (root == q || root == p || root == NULL) return root; + TreeNode* left = lowestCommonAncestor(root->left, p, q); + TreeNode* right = lowestCommonAncestor(root->right, p, q); + if (left != NULL && right != NULL) return root; + + if (left == NULL && right != NULL) return right; + else if (left != NULL && right == NULL) return left; + else { // (left == NULL && right == NULL) + return NULL; + } + + } +}; +``` + +稍加精简,代码如下: + +```CPP +class Solution { +public: + TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { + if (root == q || root == p || root == NULL) return root; + TreeNode* left = lowestCommonAncestor(root->left, p, q); + TreeNode* right = lowestCommonAncestor(root->right, p, q); + if (left != NULL && right != NULL) return root; + if (left == NULL) return right; + return left; + } +}; +``` + +## 总结 + +这道题目刷过的同学未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的。 + +**那么我给大家归纳如下三点**: + +1. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从底向上的遍历方式。 + +2. 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。 + +3. 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果。 + +可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解。 + +本题没有给出迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了。 + + +## 其他语言版本 + + +### Java +递归 +```Java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + if (root == null || root == p || root == q) { // 递归结束条件 + return root; + } + + // 后序遍历 + TreeNode left = lowestCommonAncestor(root.left, p, q); + TreeNode right = lowestCommonAncestor(root.right, p, q); + + if(left == null && right == null) { // 若未找到节点 p 或 q + return null; + }else if(left == null && right != null) { // 若找到一个节点 + return right; + }else if(left != null && right == null) { // 若找到一个节点 + return left; + }else { // 若找到两个节点 + return root; + } + } +} + +``` +迭代 +```Java +class Solution { + public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { + int max = Integer.MAX_VALUE; + Stack st = new Stack<>(); + TreeNode cur = root, pre = null; + while (cur != null || !st.isEmpty()) { + while (cur != null) { + st.push(cur); + cur = cur.left; + } + cur = st.pop(); + if (cur.right == null || cur.right == pre) { + // p/q是 中/左 或者 中/右 , 返回中 + if (cur == p || cur == q) { + if ((cur.left != null && cur.left.val == max) || (cur.right != null && cur.right.val == max)) { + return cur; + } + cur.val = max; + } + // p/q是 左/右 , 返回中 + if (cur.left != null && cur.left.val == max && cur.right != null && cur.right.val == max) { + return cur; + } + // MAX_VALUE 往上传递 + if ((cur.left != null && cur.left.val == max) || (cur.right != null && cur.right.val == max)) { + cur.val = max; + } + pre = cur; + cur = null; + } else { + st.push(cur); + cur = cur.right; + } + } + return null; + } +} + +``` + +### Python +递归法(版本一) +```python +class Solution: + def lowestCommonAncestor(self, root, p, q): + if root == q or root == p or root is None: + return root + + left = self.lowestCommonAncestor(root.left, p, q) + right = self.lowestCommonAncestor(root.right, p, q) + + if left is not None and right is not None: + return root + + if left is None and right is not None: + return right + elif left is not None and right is None: + return left + else: + return None +``` +递归法(版本二)精简 +```python +class Solution: + def lowestCommonAncestor(self, root, p, q): + if root == q or root == p or root is None: + return root + + left = self.lowestCommonAncestor(root.left, p, q) + right = self.lowestCommonAncestor(root.right, p, q) + + if left is not None and right is not None: + return root + + if left is None: + return right + return left + +``` +### Go + +```Go +func lowestCommonAncestor(root, p, q *TreeNode) *TreeNode { + // check + if root == nil { + return root + } + // 相等 直接返回root节点即可 + if root == p || root == q { + return root + } + // Divide + left := lowestCommonAncestor(root.Left, p, q) + right := lowestCommonAncestor(root.Right, p, q) + + // Conquer + // 左右两边都不为空,则根节点为祖先 + if left != nil && right != nil { + return root + } + if left != nil { + return left + } + if right != nil { + return right + } + return nil +} +``` + +### JavaScript + +```javascript +var lowestCommonAncestor = function(root, p, q) { + // 使用递归的方法 + // 需要从下到上,所以使用后序遍历 + // 1. 确定递归的函数 + const travelTree = function(root,p,q) { + // 2. 确定递归终止条件 + if(root === null || root === p || root === q) { + return root; + } + // 3. 确定递归单层逻辑 + let left = travelTree(root.left,p,q); + let right = travelTree(root.right,p,q); + if(left !== null && right !== null) { + return root; + } + if(left === null) { + return right; + } + return left; + } + return travelTree(root,p,q); +}; +``` + +### TypeScript + +```typescript +function lowestCommonAncestor(root: TreeNode | null, p: TreeNode | null, q: TreeNode | null): TreeNode | null { + if (root === null || root === p || root === q) return root; + const left = lowestCommonAncestor(root.left, p, q); + const right = lowestCommonAncestor(root.right, p, q); + if (left !== null && right !== null) return root; + if (left !== null) return left; + if (right !== null) return right; + return null; +}; +``` + +### Scala + +```scala +object Solution { + def lowestCommonAncestor(root: TreeNode, p: TreeNode, q: TreeNode): TreeNode = { + // 递归结束条件 + if (root == null || root == p || root == q) { + return root + } + + var left = lowestCommonAncestor(root.left, p, q) + var right = lowestCommonAncestor(root.right, p, q) + + if (left != null && right != null) return root + if (left == null) return right + left + } +} +``` + +### Rust + +```rust +impl Solution { + pub fn lowest_common_ancestor( + root: Option>>, + p: Option>>, + q: Option>>, + ) -> Option>> { + if root.is_none() { + return root; + } + if Rc::ptr_eq(root.as_ref().unwrap(), p.as_ref().unwrap()) + || Rc::ptr_eq(root.as_ref().unwrap(), q.as_ref().unwrap()) { + return root; + } + let left = Self::lowest_common_ancestor( + root.as_ref().unwrap().borrow().left.clone(), + p.clone(), + q.clone(), + ); + let right = + Self::lowest_common_ancestor(root.as_ref().unwrap().borrow().right.clone(), p, q); + match (&left, &right) { + (None, Some(_)) => right, + (Some(_), Some(_)) => root, + _ => left, + } + } +} +``` +### C# +```csharp +public TreeNode LowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) +{ + if (root == null || root == p || root == q) return root; + TreeNode left = LowestCommonAncestor(root.left, p, q); + TreeNode right = LowestCommonAncestor(root.right, p, q); + if (left != null && right != null) return root; + if (left == null && right != null) return right; + if (left != null && right == null) return left; + return null; +} +``` + + diff --git "a/problems/0239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" "b/problems/0239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" new file mode 100755 index 0000000000..5ea810104d --- /dev/null +++ "b/problems/0239.\346\273\221\345\212\250\347\252\227\345\217\243\346\234\200\345\244\247\345\200\274.md" @@ -0,0 +1,923 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 要用啥数据结构呢? + +# 239. 滑动窗口最大值 + +[力扣题目链接](https://leetcode.cn/problems/sliding-window-maximum/) + +给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 + +返回滑动窗口中的最大值。 + +进阶: + +你能在线性时间复杂度内解决此题吗? + + + +提示: + +* 1 <= nums.length <= 10^5 +* -10^4 <= nums[i] <= 10^4 +* 1 <= k <= nums.length + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[单调队列正式登场!| LeetCode:239. 滑动窗口最大值](https://www.bilibili.com/video/BV1XS4y1p7qj),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这是使用单调队列的经典题目。 + +难点是如何求一个区间里的最大值呢? (这好像是废话),暴力一下不就得了。 + +暴力方法,遍历一遍的过程中每次从窗口中再找到最大的数值,这样很明显是O(n × k)的算法。 + +有的同学可能会想用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。** + +此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。 + + +这个队列应该长这个样子: + +```cpp +class MyQueue { +public: + void pop(int value) { + } + void push(int value) { + } + int front() { + return que.front(); + } +}; +``` + +每次窗口移动的时候,调用que.pop(滑动窗口中移除元素的数值),que.push(滑动窗口添加元素的数值),然后que.front()就返回我们要的最大值。 + +这么个队列香不香,要是有现成的这种数据结构是不是更香了! + +其实在C++中,可以使用 multiset 来模拟这个过程,文末提供这个解法仅针对C++,以下讲解我们还是靠自己来实现这个单调队列。 + +然后再分析一下,队列里的元素一定是要排序的,而且要最大值放在出队口,要不然怎么知道最大值呢。 + +但如果把窗口里的元素都放进队列里,窗口移动的时候,队列需要弹出元素。 + +那么问题来了,已经排序之后的队列 怎么能把窗口要移除的元素(这个元素可不一定是最大值)弹出呢。 + +大家此时应该陷入深思..... + +**其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队列里的元素数值是由大到小的。** + +那么这个维护元素单调递减的队列就叫做**单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来实现一个单调队列** + +**不要以为实现的单调队列就是 对窗口里面的数进行排序,如果排序的话,那和优先级队列又有什么区别了呢。** + +来看一下单调队列如何维护队列里的元素。 + +动画如下: + +![239.滑动窗口最大值](https://file1.kamacoder.com/i/algo/239.滑动窗口最大值.gif) + +对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。 + +此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口进行滑动呢? + +设计单调队列的时候,pop,和push操作要保持如下规则: + +1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作 +2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 + +保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。 + +为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下: + + +![239.滑动窗口最大值-2](https://file1.kamacoder.com/i/algo/239.滑动窗口最大值-2.gif) + +那么我们用什么数据结构来实现这个单调队列呢? + +使用deque最为合适,在文章[栈与队列:来看看栈和队列不为人知的一面](https://programmercarl.com/栈与队列理论基础.html)中,我们就提到了常用的queue在没有指定容器的情况下,deque就是默认底层容器。 + +基于刚刚说过的单调队列pop和push的规则,代码不难实现,如下: + +```CPP +class MyQueue { //单调队列(从大到小) +public: + deque que; // 使用deque来实现单调队列 + // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 + // 同时pop之前判断队列当前是否为空。 + void pop(int value) { + if (!que.empty() && value == que.front()) { + que.pop_front(); + } + } + // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 + // 这样就保持了队列里的数值是单调从大到小的了。 + void push(int value) { + while (!que.empty() && value > que.back()) { + que.pop_back(); + } + que.push_back(value); + + } + // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。 + int front() { + return que.front(); + } +}; +``` + + +这样我们就用deque实现了一个单调队列,接下来解决滑动窗口最大值的问题就很简单了,直接看代码吧。 + +C++代码如下: + +```CPP +class Solution { +private: + class MyQueue { //单调队列(从大到小) + public: + deque que; // 使用deque来实现单调队列 + // 每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 + // 同时pop之前判断队列当前是否为空。 + void pop(int value) { + if (!que.empty() && value == que.front()) { + que.pop_front(); + } + } + // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 + // 这样就保持了队列里的数值是单调从大到小的了。 + void push(int value) { + while (!que.empty() && value > que.back()) { + que.pop_back(); + } + que.push_back(value); + + } + // 查询当前队列里的最大值 直接返回队列前端也就是front就可以了。 + int front() { + return que.front(); + } + }; +public: + vector maxSlidingWindow(vector& nums, int k) { + MyQueue que; + vector result; + for (int i = 0; i < k; i++) { // 先将前k的元素放进队列 + que.push(nums[i]); + } + result.push_back(que.front()); // result 记录前k的元素的最大值 + for (int i = k; i < nums.size(); i++) { + que.pop(nums[i - k]); // 滑动窗口移除最前面元素 + que.push(nums[i]); // 滑动窗口前加入最后面的元素 + result.push_back(que.front()); // 记录对应的最大值 + } + return result; + } +}; +``` +* 时间复杂度: O(n) +* 空间复杂度: O(k) + + +再来看一下时间复杂度,使用单调队列的时间复杂度是 O(n)。 + +有的同学可能想了,在队列中 push元素的过程中,还有pop操作呢,感觉不是纯粹的O(n)。 + +其实,大家可以自己观察一下单调队列的实现,nums 中的每个元素最多也就被 push_back 和 pop_back 各一次,没有任何多余操作,所以整体的复杂度还是 O(n)。 + +空间复杂度因为我们定义一个辅助队列,所以是O(k)。 + +## 扩展 + +大家貌似对单调队列 都有一些疑惑,首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题哈。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本题中的单调队列实现就是固定的写法哈。 + +大家貌似对deque也有一些疑惑,C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过啦),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。 + + + +## 其他语言版本 + +### Java: + +```Java +//解法一 +//自定义数组 +class MyQueue { + Deque deque = new LinkedList<>(); + //弹出元素时,比较当前要弹出的数值是否等于队列出口的数值,如果相等则弹出 + //同时判断队列当前是否为空 + void poll(int val) { + if (!deque.isEmpty() && val == deque.peek()) { + deque.poll(); + } + } + //添加元素时,如果要添加的元素大于入口处的元素,就将入口元素弹出 + //保证队列元素单调递减 + //比如此时队列元素3,1,2将要入队,比1大,所以1弹出,此时队列:3,2 + void add(int val) { + while (!deque.isEmpty() && val > deque.getLast()) { + deque.removeLast(); + } + deque.add(val); + } + //队列队顶元素始终为最大值 + int peek() { + return deque.peek(); + } +} + +class Solution { + public int[] maxSlidingWindow(int[] nums, int k) { + if (nums.length == 1) { + return nums; + } + int len = nums.length - k + 1; + //存放结果元素的数组 + int[] res = new int[len]; + int num = 0; + //自定义队列 + MyQueue myQueue = new MyQueue(); + //先将前k的元素放入队列 + for (int i = 0; i < k; i++) { + myQueue.add(nums[i]); + } + res[num++] = myQueue.peek(); + for (int i = k; i < nums.length; i++) { + //滑动窗口移除最前面的元素,移除是判断该元素是否放入队列 + myQueue.poll(nums[i - k]); + //滑动窗口加入最后面的元素 + myQueue.add(nums[i]); + //记录对应的最大值 + res[num++] = myQueue.peek(); + } + return res; + } +} + +//解法二 +//利用双端队列手动实现单调队列 +/** + * 用一个单调队列来存储对应的下标,每当窗口滑动的时候,直接取队列的头部指针对应的值放入结果集即可 + * 单调递减队列类似 (head -->) 3 --> 2 --> 1 --> 0 (--> tail) (左边为头结点,元素存的是下标) + */ +class Solution { + public int[] maxSlidingWindow(int[] nums, int k) { + ArrayDeque deque = new ArrayDeque<>(); + int n = nums.length; + int[] res = new int[n - k + 1]; + int idx = 0; + for(int i = 0; i < n; i++) { + // 根据题意,i为nums下标,是要在[i - k + 1, i] 中选到最大值,只需要保证两点 + // 1.队列头结点需要在[i - k + 1, i]范围内,不符合则要弹出 + while(!deque.isEmpty() && deque.peek() < i - k + 1){ + deque.poll(); + } + // 2.维护单调递减队列:新元素若大于队尾元素,则弹出队尾元素,直到满足单调性 + while(!deque.isEmpty() && nums[deque.peekLast()] < nums[i]) { + deque.pollLast(); + } + + deque.offer(i); + + // 因为单调,当i增长到符合第一个k范围的时候,每滑动一步都将队列头节点放入结果就行了 + if(i >= k - 1){ + res[idx++] = nums[deque.peek()]; + } + } + return res; + } +} +``` + +### Python: +#### 解法一:使用自定义的单调队列类 +```python +from collections import deque + + +class MyQueue: #单调队列(从大到小 + def __init__(self): + self.queue = deque() #这里需要使用deque实现单调队列,直接使用list会超时 + + #每次弹出的时候,比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 + #同时pop之前判断队列当前是否为空。 + def pop(self, value): + if self.queue and value == self.queue[0]: + self.queue.popleft()#list.pop()时间复杂度为O(n),这里需要使用collections.deque() + + #如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 + #这样就保持了队列里的数值是单调从大到小的了。 + def push(self, value): + while self.queue and value > self.queue[-1]: + self.queue.pop() + self.queue.append(value) + + #查询当前队列里的最大值 直接返回队列前端也就是front就可以了。 + def front(self): + return self.queue[0] + +class Solution: + def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: + que = MyQueue() + result = [] + for i in range(k): #先将前k的元素放进队列 + que.push(nums[i]) + result.append(que.front()) #result 记录前k的元素的最大值 + for i in range(k, len(nums)): + que.pop(nums[i - k]) #滑动窗口移除最前面元素 + que.push(nums[i]) #滑动窗口前加入最后面的元素 + result.append(que.front()) #记录对应的最大值 + return result +``` + + +#### 解法二:直接用单调队列 +```python +from collections import deque +class Solution: + def maxSlidingWindow(self, nums: List[int], k: int) -> List[int]: + max_list = [] # 结果集合 + kept_nums = deque() # 单调队列 + + for i in range(len(nums)): + update_kept_nums(kept_nums, nums[i]) # 右侧新元素加入 + + if i >= k and nums[i - k] == kept_nums[0]: # 左侧旧元素如果等于单调队列头元素,需要移除头元素 + kept_nums.popleft() + + if i >= k - 1: + max_list.append(kept_nums[0]) + + return max_list + +def update_kept_nums(kept_nums, num): # num 是新加入的元素 + # 所有小于新元素的队列尾部元素,在新元素出现后,都是没有价值的,都需要被移除 + while kept_nums and num > kept_nums[-1]: + kept_nums.pop() + + kept_nums.append(num) + +``` + +### Go: + +```go +// 封装单调队列的方式解题 +type MyQueue struct { + queue []int +} + +func NewMyQueue() *MyQueue { + return &MyQueue{ + queue: make([]int, 0), + } +} + +func (m *MyQueue) Front() int { + return m.queue[0] +} + +func (m *MyQueue) Back() int { + return m.queue[len(m.queue)-1] +} + +func (m *MyQueue) Empty() bool { + return len(m.queue) == 0 +} + +func (m *MyQueue) Push(val int) { + for !m.Empty() && val > m.Back() { + m.queue = m.queue[:len(m.queue)-1] + } + m.queue = append(m.queue, val) +} + +func (m *MyQueue) Pop(val int) { + if !m.Empty() && val == m.Front() { + m.queue = m.queue[1:] + } +} + +func maxSlidingWindow(nums []int, k int) []int { + queue := NewMyQueue() + length := len(nums) + res := make([]int, 0) + // 先将前k个元素放入队列 + for i := 0; i < k; i++ { + queue.Push(nums[i]) + } + // 记录前k个元素的最大值 + res = append(res, queue.Front()) + + for i := k; i < length; i++ { + // 滑动窗口移除最前面的元素 + queue.Pop(nums[i-k]) + // 滑动窗口添加最后面的元素 + queue.Push(nums[i]) + // 记录最大值 + res = append(res, queue.Front()) + } + return res +} +``` + +### JavaScript: + +```javascript +/** + * @param {number[]} nums + * @param {number} k + * @return {number[]} + */ +var maxSlidingWindow = function (nums, k) { + class MonoQueue { + queue; + constructor() { + this.queue = []; + } + enqueue(value) { + let back = this.queue[this.queue.length - 1]; + while (back !== undefined && back < value) { + this.queue.pop(); + back = this.queue[this.queue.length - 1]; + } + this.queue.push(value); + } + dequeue(value) { + let front = this.front(); + if (front === value) { + this.queue.shift(); + } + } + front() { + return this.queue[0]; + } + } + let helperQueue = new MonoQueue(); + let i = 0, j = 0; + let resArr = []; + while (j < k) { + helperQueue.enqueue(nums[j++]); + } + resArr.push(helperQueue.front()); + while (j < nums.length) { + helperQueue.enqueue(nums[j]); + helperQueue.dequeue(nums[i]); + resArr.push(helperQueue.front()); + i++, j++; + } + return resArr; +}; +``` + +### TypeScript: + +```typescript +function maxSlidingWindow(nums: number[], k: number): number[] { + /** 单调递减队列 */ + class MonoQueue { + private queue: number[]; + constructor() { + this.queue = []; + }; + /** 入队:value如果大于队尾元素,则将队尾元素删除,直至队尾元素大于value,或者队列为空 */ + public enqueue(value: number): void { + let back: number | undefined = this.queue[this.queue.length - 1]; + while (back !== undefined && back < value) { + this.queue.pop(); + back = this.queue[this.queue.length - 1]; + } + this.queue.push(value); + }; + /** 出队:只有当队头元素等于value,才出队 */ + public dequeue(value: number): void { + let top: number | undefined = this.top(); + if (top !== undefined && top === value) { + this.queue.shift(); + } + } + public top(): number | undefined { + return this.queue[0]; + } + } + const helperQueue: MonoQueue = new MonoQueue(); + let i: number = 0, + j: number = 0; + let resArr: number[] = []; + while (j < k) { + helperQueue.enqueue(nums[j++]); + } + resArr.push(helperQueue.top()!); + while (j < nums.length) { + helperQueue.enqueue(nums[j]); + helperQueue.dequeue(nums[i]); + resArr.push(helperQueue.top()!); + j++, i++; + } + return resArr; +}; +``` + +### Swift: + +解法一: + +```Swift +/// 双向链表 +class DoublyListNode { + var head: DoublyListNode? + var tail: DoublyListNode? + var next: DoublyListNode? + var pre: DoublyListNode? + var value: Int = 0 + init(_ value: Int = 0) { + self.value = value + } + func isEmpty() -> Bool { + return self.head == nil + } + func first() -> Int? { + return self.head?.value + } + func last() -> Int? { + return self.tail?.value + } + func removeFirst() { + if isEmpty() { + return + } + let next = self.head!.next + self.head?.next = nil// 移除首节点 + next?.pre = nil + self.head = next + } + func removeLast() { + if let tail = self.tail { + if let pre = tail.pre { + self.tail?.pre = nil + pre.next = nil + self.tail = pre + } else { + self.head = nil + self.tail = nil + } + } + } + func append(_ value: Int) { + let node = DoublyListNode(value) + if self.head != nil { + node.pre = self.tail + self.tail?.next = node + self.tail = node + } else { + self.head = node + self.tail = node + self.pre = nil + self.next = nil + } + } +} +// 单调队列, 从大到小 +class MyQueue { +// var queue: [Int]!// 用数组会超时 + var queue: DoublyListNode! + init() { +// queue = [Int]() + queue = DoublyListNode() + } + // 滑动窗口时弹出第一个元素, 如果相等再弹出 + func pop(x: Int) { + if !queue.isEmpty() && front() == x { + queue.removeFirst() + } + } + // 滑动窗口时添加下一个元素, 移除队尾比 x 小的元素 始终保证队头 > 队尾 + func push(x: Int) { + while !queue.isEmpty() && queue.last()! < x { + queue.removeLast() + } + queue.append(x) + } + // 此时队头就是滑动窗口最大值 + func front() -> Int { + return queue.first() ?? -1 + } +} + +class Solution { + func maxSlidingWindow(_ nums: [Int], _ k: Int) -> [Int] { + // 存放结果 + var res = [Int]() + let queue = MyQueue() + // 先将前K个元素放入队列 + for i in 0 ..< k { + queue.push(x: nums[i]) + } + // 添加当前队列最大值到结果数组 + res.append(queue.front()) + for i in k ..< nums.count { + // 滑动窗口移除最前面元素 + queue.pop(x: nums[i - k]) + // 滑动窗口添加下一个元素 + queue.push(x: nums[i]) + // 保存当前队列最大值 + res.append(queue.front()) + } + return res + } +} +``` + +Swift解法二: + +```swift +func maxSlidingWindow(_ nums: [Int], _ k: Int) -> [Int] { + var result = [Int]() + var window = [Int]() + var right = 0, left = right - k + 1 + + while right < nums.count { + let value = nums[right] + + // 因为窗口移动丢弃的左边数 + if left > 0, left - 1 == window.first { + window.removeFirst() + } + + // 保证末尾的是最大的 + while !window.isEmpty, value > nums[window.last!] { + window.removeLast() + } + window.append(right) + + if left >= 0 { // 窗口形成 + result.append(nums[window.first!]) + } + + right += 1 + left += 1 + } + + return result +} +``` +### Scala: + +```scala +import scala.collection.mutable.ArrayBuffer +object Solution { + def maxSlidingWindow(nums: Array[Int], k: Int): Array[Int] = { + var len = nums.length - k + 1 // 滑动窗口长度 + var res: Array[Int] = new Array[Int](len) // 声明存储结果的数组 + var index = 0 // 结果数组指针 + val queue: MyQueue = new MyQueue // 自定义队列 + // 将前k个添加到queue + for (i <- 0 until k) { + queue.add(nums(i)) + } + res(index) = queue.peek // 第一个滑动窗口的最大值 + index += 1 + for (i <- k until nums.length) { + queue.poll(nums(i - k)) // 首先移除第i-k个元素 + queue.add(nums(i)) // 添加当前数字到队列 + res(index) = queue.peek() // 赋值 + index+=1 + } + // 最终返回res,return关键字可以省略 + res + } + } + +class MyQueue { + var queue = ArrayBuffer[Int]() + + // 移除元素,如果传递进来的跟队头相等,那么移除 + def poll(value: Int): Unit = { + if (!queue.isEmpty && queue.head == value) { + queue.remove(0) + } + } + + // 添加元素,当队尾大于当前元素就删除 + def add(value: Int): Unit = { + while (!queue.isEmpty && value > queue.last) { + queue.remove(queue.length - 1) + } + queue.append(value) + } + + def peek(): Int = queue.head +} +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer[] $nums + * @param Integer $k + * @return Integer[] + */ + function maxSlidingWindow($nums, $k) { + $myQueue = new MyQueue(); + // 先将前k的元素放进队列 + for ($i = 0; $i < $k; $i++) { + $myQueue->push($nums[$i]); + } + + $result = []; + $result[] = $myQueue->max(); // result 记录前k的元素的最大值 + + for ($i = $k; $i < count($nums); $i++) { + $myQueue->pop($nums[$i - $k]); // 滑动窗口移除最前面元素 + $myQueue->push($nums[$i]); // 滑动窗口前加入最后面的元素 + $result[]= $myQueue->max(); // 记录对应的最大值 + } + return $result; + } + +} + +// 单调对列构建 +class MyQueue{ + private $queue; + + public function __construct(){ + $this->queue = new SplQueue(); //底层是双向链表实现。 + } + + public function pop($v){ + // 判断当前对列是否为空 + // 比较当前要弹出的数值是否等于队列出口元素的数值,如果相等则弹出。 + // bottom 从链表前端查看元素, dequeue 从双向链表的开头移动一个节点 + if(!$this->queue->isEmpty() && $v == $this->queue->bottom()){ + $this->queue->dequeue(); //弹出队列 + } + } + + public function push($v){ + // 判断当前对列是否为空 + // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 + // 这样就保持了队列里的数值是单调从大到小的了。 + while (!$this->queue->isEmpty() && $v > $this->queue->top()) { + $this->queue->pop(); // pop从链表末尾弹出一个元素, + } + $this->queue->enqueue($v); + } + + // 查询当前队列里的最大值 直接返回队首 + public function max(){ + // bottom 从链表前端查看元素, top从链表末尾查看元素 + return $this->queue->bottom(); + } + + // 辅助理解: 打印队列元素 + public function println(){ + // "迭代器移动到链表头部": 可理解为从头遍历链表元素做准备。 + // 【PHP中没有指针概念,所以就没说指针。从数据结构上理解,就是把指针指向链表头部】 + $this->queue->rewind(); + + echo "Println: "; + while($this->queue->valid()){ + echo $this->queue->current()," -> "; + $this->queue->next(); + } + echo "\n"; + } +} +``` + +### C#: + +```csharp +class myDequeue{ + private LinkedList linkedList = new LinkedList(); + + public void Enqueue(int n){ + while(linkedList.Count > 0 && linkedList.Last.Value < n){ + linkedList.RemoveLast(); + } + linkedList.AddLast(n); + } + + public int Max(){ + return linkedList.First.Value; + } + + public void Dequeue(int n){ + if(linkedList.First.Value == n){ + linkedList.RemoveFirst(); + } + } + } + + myDequeue window = new myDequeue(); + List res = new List(); + + public int[] MaxSlidingWindow(int[] nums, int k) { + for(int i = 0; i < k; i++){ + window.Enqueue(nums[i]); + } + res.Add(window.Max()); + for(int i = k; i < nums.Length; i++){ + window.Dequeue(nums[i-k]); + window.Enqueue(nums[i]); + res.Add(window.Max()); + } + + return res.ToArray(); + } +``` + +### Rust: + +```rust +impl Solution { + pub fn max_sliding_window(nums: Vec, k: i32) -> Vec { + let mut res = vec![]; + let mut queue = VecDeque::with_capacity(k as usize); + for (i, &v) in nums.iter().enumerate() { + // 如果队列长度超过 k,那么需要移除队首过期元素 + if i - queue.front().unwrap_or(&0) == k as usize { + queue.pop_front(); + } + while let Some(&index) = queue.back() { + if nums[index] >= v { + break; + } + // 如果队列第一个元素比当前元素小,那么就把队列第一个元素弹出 + queue.pop_back(); + } + queue.push_back(i); + if i >= k as usize - 1 { + res.push(nums[queue[0]]); + } + } + res + } +} +``` + +### C++ +使用multiset作为单调队列 + +多重集合(`multiset`) 用以有序地存储元素的容器。允许存在相等的元素。 + +在遍历原数组的时候,只需要把窗口的头元素加入到multiset中,然后把窗口的尾元素删除即可。因为multiset是有序的,并且提供了*rbegin(),可以直接获取窗口最大值。 +```cpp +class Solution { +public: + vector maxSlidingWindow(vector& nums, int k) { + multiset st; + vector ans; + for (int i = 0; i < nums.size(); i++) { + if (i >= k) st.erase(st.find(nums[i - k])); + st.insert(nums[i]); + if (i >= k - 1) ans.push_back(*st.rbegin()); + } + return ans; + } +}; +``` + +### C + +```c +int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize) { + *returnSize = numsSize - k + 1; + int *res = (int*)malloc((*returnSize) * sizeof(int)); + assert(res); + int *deque = (int*)malloc(numsSize * sizeof(int)); + assert(deque); + int front = 0, rear = 0, idx = 0; + + for (int i = 0 ; i < numsSize ; i++) { + while (front < rear && deque[front] <= i - k) { + front++; + } + + while (front < rear && nums[deque[rear - 1]] <= nums[i]) { + rear--; + } + + deque[rear++] = i; + + if (i >= k - 1) { + res[idx++] = nums[deque[front]]; + } + } + + return res; +} + +``` + + diff --git "a/problems/0242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" "b/problems/0242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" new file mode 100755 index 0000000000..0a37ea26cc --- /dev/null +++ "b/problems/0242.\346\234\211\346\225\210\347\232\204\345\255\227\346\257\215\345\274\202\344\275\215\350\257\215.md" @@ -0,0 +1,416 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 数组就是简单的哈希表,但是数组的大小可不是无限开辟的 + +# 242.有效的字母异位词 + +[力扣题目链接](https://leetcode.cn/problems/valid-anagram/) + +给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 + +示例 1: +输入: s = "anagram", t = "nagaram" +输出: true + +示例 2: +输入: s = "rat", t = "car" +输出: false + +**说明:** +你可以假设字符串只包含小写字母。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[学透哈希表,数组使用有技巧!Leetcode:242.有效的字母异位词](https://www.bilibili.com/video/BV1YG411p7BA),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。 + +暴力的方法这里就不做介绍了,直接看一下有没有更优的方式。 + +**数组其实就是一个简单哈希表**,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。 + +如果对哈希表的理论基础关于数组,set,map不了解的话可以看这篇:[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html) + +需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。 + +为了方便举例,判断一下字符串s= "aee", t = "eae"。 + +操作动画如下: + +![242.有效的字母异位词](https://file1.kamacoder.com/i/algo/242.%E6%9C%89%E6%95%88%E7%9A%84%E5%AD%97%E6%AF%8D%E5%BC%82%E4%BD%8D%E8%AF%8D.gif) + +定义一个数组叫做record用来上记录字符串s里字符出现的次数。 + +需要把字符映射到数组也就是哈希表的索引下标上,**因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。** + +再遍历 字符串s的时候,**只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。** 这样就将字符串s中字符出现的次数,统计出来了。 + +那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。 + +那么最后检查一下,**record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。** + +最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。 + +时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。 + + +C++ 代码如下: +```CPP +class Solution { +public: + bool isAnagram(string s, string t) { + int record[26] = {0}; + for (int i = 0; i < s.size(); i++) { + // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了 + record[s[i] - 'a']++; + } + for (int i = 0; i < t.size(); i++) { + record[t[i] - 'a']--; + } + for (int i = 0; i < 26; i++) { + if (record[i] != 0) { + // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。 + return false; + } + } + // record数组所有元素都为零0,说明字符串s和t是字母异位词 + return true; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + +## 其他语言版本 + +### Java: + +```java +/** + * 242. 有效的字母异位词 字典解法 + * 时间复杂度O(m+n) 空间复杂度O(1) + */ +class Solution { + public boolean isAnagram(String s, String t) { + int[] record = new int[26]; + + for (int i = 0; i < s.length(); i++) { + record[s.charAt(i) - 'a']++; // 并不需要记住字符a的ASCII,只要求出一个相对数值就可以了 + } + + for (int i = 0; i < t.length(); i++) { + record[t.charAt(i) - 'a']--; + } + + for (int count: record) { + if (count != 0) { // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。 + return false; + } + } + return true; // record数组所有元素都为零0,说明字符串s和t是字母异位词 + } +} +``` + +### Python: + +```python +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + record = [0] * 26 + for i in s: + #并不需要记住字符a的ASCII,只要求出一个相对数值就可以了 + record[ord(i) - ord("a")] += 1 + for i in t: + record[ord(i) - ord("a")] -= 1 + for i in range(26): + if record[i] != 0: + #record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。 + return False + return True +``` +Python写法二(没有使用数组作为哈希表,只是介绍defaultdict这样一种解题思路): + +```python +class Solution: + def isAnagram(self, s: str, t: str) -> bool: + from collections import defaultdict + + s_dict = defaultdict(int) + t_dict = defaultdict(int) + for x in s: + s_dict[x] += 1 + + for x in t: + t_dict[x] += 1 + return s_dict == t_dict +``` +Python写法三(没有使用数组作为哈希表,只是介绍Counter这种更方便的解题思路): + +```python +class Solution(object): + def isAnagram(self, s: str, t: str) -> bool: + from collections import Counter + a_count = Counter(s) + b_count = Counter(t) + return a_count == b_count +``` + +### Go: + +```go +func isAnagram(s string, t string) bool { + record := [26]int{} + + for _, r := range s { + record[r-rune('a')]++ + } + for _, r := range t { + record[r-rune('a')]-- + } + + return record == [26]int{} // record数组如果有的元素不为零0,说明字符串s和t 一定是谁多了字符或者谁少了字符。 +} +``` + +Go 写法二(只对字符串遍历一次) +```go +func isAnagram(s string, t string) bool { + if len(s) != len(t) { + return false + } + records := [26]int{} + for index := 0; index < len(s); index++ { + if s[index] == t[index] { + continue + } + sCharIndex := s[index] - 'a' + records[sCharIndex]++ + tCharIndex := t[index] - 'a' + records[tCharIndex]-- + } + for _, record := range records { + if record != 0 { + return false + } + } + return true +} +``` + +### JavaScript: + +```js +/** + * @param {string} s + * @param {string} t + * @return {boolean} + */ +var isAnagram = function(s, t) { + if(s.length !== t.length) return false; + const resSet = new Array(26).fill(0); + const base = "a".charCodeAt(); + for(const i of s) { + resSet[i.charCodeAt() - base]++; + } + for(const i of t) { + if(!resSet[i.charCodeAt() - base]) return false; + resSet[i.charCodeAt() - base]--; + } + return true; +}; + +var isAnagram = function(s, t) { + if(s.length !== t.length) return false; + let char_count = new Map(); + for(let item of s) { + char_count.set(item, (char_count.get(item) || 0) + 1) ; + } + for(let item of t) { + if(!char_count.get(item)) return false; + char_count.set(item, char_count.get(item)-1); + } + return true; +}; +``` + +### TypeScript: + +```typescript +function isAnagram(s: string, t: string): boolean { + if (s.length !== t.length) return false; + let helperArr: number[] = new Array(26).fill(0); + let pivot: number = 'a'.charCodeAt(0); + for (let i = 0, length = s.length; i < length; i++) { + helperArr[s.charCodeAt(i) - pivot]++; + helperArr[t.charCodeAt(i) - pivot]--; + } + return helperArr.every(i => i === 0); +}; +``` + +### Swift: + +```Swift +func isAnagram(_ s: String, _ t: String) -> Bool { + if s.count != t.count { + return false + } + var record = Array(repeating: 0, count: 26) + let aUnicodeScalar = "a".unicodeScalars.first!.value + for c in s.unicodeScalars { + record[Int(c.value - aUnicodeScalar)] += 1 + } + for c in t.unicodeScalars { + record[Int(c.value - aUnicodeScalar)] -= 1 + } + for value in record { + if value != 0 { + return false + } + } + return true +} +``` + +### PHP: + +```php +class Solution { + /** + * @param String $s + * @param String $t + * @return Boolean + */ + function isAnagram($s, $t) { + if (strlen($s) != strlen($t)) { + return false; + } + $table = []; + for ($i = 0; $i < strlen($s); $i++) { + if (!isset($table[$s[$i]])) { + $table[$s[$i]] = 1; + } else { + $table[$s[$i]]++; + } + if (!isset($table[$t[$i]])) { + $table[$t[$i]] = -1; + } else { + $table[$t[$i]]--; + } + } + foreach ($table as $record) { + if ($record != 0) { + return false; + } + } + return true; + } +} +``` + +### Rust: + +```rust +impl Solution { + pub fn is_anagram(s: String, t: String) -> bool { + let mut record = vec![0; 26]; + + let baseChar = 'a'; + + for byte in s.bytes() { + record[byte as usize - baseChar as usize] += 1; + } + for byte in t.bytes() { + record[byte as usize - baseChar as usize] -= 1; + } + + record.iter().filter(|x| **x != 0).count() == 0 + } +} +``` + +### Scala: + +```scala +object Solution { + def isAnagram(s: String, t: String): Boolean = { + // 如果两个字符串的长度不等,直接返回false + if (s.length != t.length) return false + val record = new Array[Int](26) // 记录每个单词出现了多少次 + // 遍历字符串,对于s字符串单词对应的记录+=1,t字符串对应的记录-=1 + for (i <- 0 until s.length) { + record(s(i) - 97) += 1 + record(t(i) - 97) -= 1 + } + // 如果不等于则直接返回false + for (i <- 0 until 26) { + if (record(i) != 0) { + return false + } + } + // 如果前面不返回false,说明匹配成功,返回true,return可以省略 + true + } +} +``` + +### C#: + +```csharp + public bool IsAnagram(string s, string t) { + int sl=s.Length,tl=t.Length; + if(sl!=tl) return false; + int[] a = new int[26]; + for(int i = 0; i < sl; i++){ + a[s[i] - 'a']++; + a[t[i] - 'a']--; + } + foreach (int i in a) + { + if (i != 0) + return false; + } + return true; + } +``` + +### C + +```c +bool isAnagram(char* s, char* t) { + int len1 = strlen(s), len2 = strlen(t); + if (len1 != len2) { + return false; + } + + int map1[26] = {0}, map2[26] = {0}; + for (int i = 0; i < len1; i++) { + map1[s[i] - 'a'] += 1; + map2[t[i] - 'a'] += 1; + } + + for (int i = 0; i < 26; i++) { + if (map1[i] != map2[i]) { + return false; + } + } + + return true; +} +``` + +## 相关题目 + +* [383.赎金信](https://programmercarl.com/0383.%E8%B5%8E%E9%87%91%E4%BF%A1.html) +* [49.字母异位词分组](https://leetcode.cn/problems/group-anagrams/) +* [438.找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) + + + diff --git "a/problems/0257.\344\272\214\345\217\211\346\240\221\347\232\204\346\211\200\346\234\211\350\267\257\345\276\204.md" "b/problems/0257.\344\272\214\345\217\211\346\240\221\347\232\204\346\211\200\346\234\211\350\267\257\345\276\204.md" new file mode 100755 index 0000000000..4a66c816bc --- /dev/null +++ "b/problems/0257.\344\272\214\345\217\211\346\240\221\347\232\204\346\211\200\346\234\211\350\267\257\345\276\204.md" @@ -0,0 +1,938 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 以为只用了递归,其实还用了回溯 + +# 257. 二叉树的所有路径 + +[力扣题目链接](https://leetcode.cn/problems/binary-tree-paths/) + +给定一个二叉树,返回所有从根节点到叶子节点的路径。 + +说明: 叶子节点是指没有子节点的节点。 + +示例: +![257.二叉树的所有路径1](https://file1.kamacoder.com/i/algo/2021020415161576.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)::[递归中带着回溯,你感受到了没?| LeetCode:257. 二叉树的所有路径](https://www.bilibili.com/video/BV1ZG411G7Dh),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这道题目要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。 + +在这道题目中将第一次涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一个路径再进入另一个路径。 + +前序遍历以及回溯的过程如图: + +![257.二叉树的所有路径](https://file1.kamacoder.com/i/algo/20210204151702443.png) + +我们先使用递归的方式,来做前序遍历。**要知道递归和回溯就是一家的,本题也需要回溯。** + +### 递归 + +1. 递归函数参数以及返回值 + +要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值,代码如下: + +```CPP +void traversal(TreeNode* cur, vector& path, vector& result) +``` + +2. 确定递归终止条件 + +在写递归的时候都习惯了这么写: + +```CPP +if (cur == NULL) { + 终止处理逻辑 +} +``` + +但是本题的终止条件这样写会很麻烦,因为本题要找到叶子节点,就开始结束的处理逻辑了(把路径放进result里)。 + +**那么什么时候算是找到了叶子节点?** 是当 cur不为空,其左右孩子都为空的时候,就找到叶子节点。 + +所以本题的终止条件是: +```CPP +if (cur->left == NULL && cur->right == NULL) { + 终止处理逻辑 +} +``` + +为什么没有判断cur是否为空呢,因为下面的逻辑可以控制空节点不入循环。 + +再来看一下终止处理的逻辑。 + +这里使用`vector` 结构path来记录路径,所以要把`vector` 结构的path转为string格式,再把这个string 放进 result里。 + +**那么为什么使用了`vector` 结构来记录路径呢?** 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。 + +可能有的同学问了,我看有些人的代码也没有回溯啊。 + +**其实是有回溯的,只不过隐藏在函数调用时的参数赋值里**,下文我还会提到。 + +这里我们先使用`vector`结构的path容器来记录路径,那么终止处理逻辑如下: + +```CPP +if (cur->left == NULL && cur->right == NULL) { // 遇到叶子节点 + string sPath; + for (int i = 0; i < path.size() - 1; i++) { // 将path里记录的路径转为string格式 + sPath += to_string(path[i]); + sPath += "->"; + } + sPath += to_string(path[path.size() - 1]); // 记录最后一个节点(叶子节点) + result.push_back(sPath); // 收集一个路径 + return; +} +``` + +3. 确定单层递归逻辑 + +因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。 + +`path.push_back(cur->val);` + +然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。 + +所以递归前要加上判断语句,下面要递归的节点是否为空,如下 + +```CPP +if (cur->left) { + traversal(cur->left, path, result); +} +if (cur->right) { + traversal(cur->right, path, result); +} +``` + +此时还没完,递归完,要做回溯啊,因为path 不能一直加入节点,它还要删节点,然后才能加入新的节点。 + +那么回溯要怎么回溯呢,一些同学会这么写,如下: + +```CPP +if (cur->left) { + traversal(cur->left, path, result); +} +if (cur->right) { + traversal(cur->right, path, result); +} +path.pop_back(); +``` + +这个回溯就有很大的问题,我们知道,**回溯和递归是一一对应的,有一个递归,就要有一个回溯**,这么写的话相当于把递归和回溯拆开了, 一个在花括号里,一个在花括号外。 + +**所以回溯要和递归永远在一起,世界上最遥远的距离是你在花括号里,而我在花括号外!** + +那么代码应该这么写: + +```CPP +if (cur->left) { + traversal(cur->left, path, result); + path.pop_back(); // 回溯 +} +if (cur->right) { + traversal(cur->right, path, result); + path.pop_back(); // 回溯 +} +``` + +那么本题整体代码如下: + +```CPP +// 版本一 +class Solution { +private: + + void traversal(TreeNode* cur, vector& path, vector& result) { + path.push_back(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中 + // 这才到了叶子节点 + if (cur->left == NULL && cur->right == NULL) { + string sPath; + for (int i = 0; i < path.size() - 1; i++) { + sPath += to_string(path[i]); + sPath += "->"; + } + sPath += to_string(path[path.size() - 1]); + result.push_back(sPath); + return; + } + if (cur->left) { // 左 + traversal(cur->left, path, result); + path.pop_back(); // 回溯 + } + if (cur->right) { // 右 + traversal(cur->right, path, result); + path.pop_back(); // 回溯 + } + } + +public: + vector binaryTreePaths(TreeNode* root) { + vector result; + vector path; + if (root == NULL) return result; + traversal(root, path, result); + return result; + } +}; +``` +如上的C++代码充分体现了回溯。 + +那么如上代码可以精简成如下代码: + +```CPP +class Solution { +private: + + void traversal(TreeNode* cur, string path, vector& result) { + path += to_string(cur->val); // 中 + if (cur->left == NULL && cur->right == NULL) { + result.push_back(path); + return; + } + if (cur->left) traversal(cur->left, path + "->", result); // 左 + if (cur->right) traversal(cur->right, path + "->", result); // 右 + } + +public: + vector binaryTreePaths(TreeNode* root) { + vector result; + string path; + if (root == NULL) return result; + traversal(root, path, result); + return result; + + } +}; +``` + +如上代码精简了不少,也隐藏了不少东西。 + +注意在函数定义的时候`void traversal(TreeNode* cur, string path, vector& result)` ,定义的是`string path`,每次都是复制赋值,不用使用引用,否则就无法做到回溯的效果。(这里涉及到C++语法知识) + +那么在如上代码中,**貌似没有看到回溯的逻辑,其实不然,回溯就隐藏在`traversal(cur->left, path + "->", result);`中的 `path + "->"`。** 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。 + +为了把这份精简代码的回溯过程展现出来,大家可以试一试把: + +```CPP +if (cur->left) traversal(cur->left, path + "->", result); // 左 回溯就隐藏在这里 +``` + +改成如下代码: + +```CPP +path += "->"; +traversal(cur->left, path, result); // 左 +``` + +即: + +```CPP +if (cur->left) { + path += "->"; + traversal(cur->left, path, result); // 左 +} +if (cur->right) { + path += "->"; + traversal(cur->right, path, result); // 右 +} +``` + +此时就没有回溯了,这个代码就是通过不了的了。 + +如果想把回溯加上,就要 在上面代码的基础上,加上回溯,就可以AC了。 + +```CPP +if (cur->left) { + path += "->"; + traversal(cur->left, path, result); // 左 + path.pop_back(); // 回溯 '>' + path.pop_back(); // 回溯 '-' +} +if (cur->right) { + path += "->"; + traversal(cur->right, path, result); // 右 + path.pop_back(); // 回溯 '>' + path.pop_back(); // 回溯 '-' +} +``` + +整体代码如下: + +```CPP +//版本二 +class Solution { +private: + void traversal(TreeNode* cur, string path, vector& result) { + path += to_string(cur->val); // 中,中为什么写在这里,因为最后一个节点也要加入到path中 + if (cur->left == NULL && cur->right == NULL) { + result.push_back(path); + return; + } + if (cur->left) { + path += "->"; + traversal(cur->left, path, result); // 左 + path.pop_back(); // 回溯 '>' + path.pop_back(); // 回溯 '-' + } + if (cur->right) { + path += "->"; + traversal(cur->right, path, result); // 右 + path.pop_back(); // 回溯'>' + path.pop_back(); // 回溯 '-' + } + } + +public: + vector binaryTreePaths(TreeNode* root) { + vector result; + string path; + if (root == NULL) return result; + traversal(root, path, result); + return result; + + } +}; + +``` + +**大家应该可以感受出来,如果把 `path + "->"`作为函数参数就是可以的,因为并没有改变path的数值,执行完递归函数之后,path依然是之前的数值(相当于回溯了)** + + +**综合以上,第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现出来了。** + +### 拓展 + +这里讲解本题解的写法逻辑以及一些更具体的细节,下面的讲解中,涉及到C++语法特性,如果不是C++的录友,就可以不看了,避免越看越晕。 + +如果是C++的录友,建议本题独立刷过两遍,再看下面的讲解,同样避免越看越晕,造成不必要的负担。 + +在第二版本的代码中,其实仅仅是回溯了 `->` 部分(调用两次pop_back,一个pop`>` 一次pop`-`),大家应该疑惑那么 `path += to_string(cur->val);` 这一步为什么没有回溯呢? 一条路径能持续加节点 不做回溯吗? + +其实关键还在于 参数,使用的是 `string path`,这里并没有加上引用`&` ,即本层递归中,path + 该节点数值,但该层递归结束,上一层path的数值并不会受到任何影响。 如图所示: + +![](https://file1.kamacoder.com/i/algo/20220831173322.png) + +节点4 的path,在遍历到节点3,path+3,遍历节点3的递归结束之后,返回节点4(回溯的过程),path并不会把3加上。 + +所以这是参数中,不带引用,不做地址拷贝,只做内容拷贝的效果。(这里涉及到C++引用方面的知识) + +在第一个版本中,函数参数我就使用了引用,即 `vector& path` ,这是会拷贝地址的,所以 本层递归逻辑如果有`path.push_back(cur->val);` 就一定要有对应的 `path.pop_back()` + +那有同学可能想,为什么不去定义一个 `string& path` 这样的函数参数呢,然后也可能在递归函数中展现回溯的过程,但关键在于,`path += to_string(cur->val);` 每次是加上一个数字,这个数字如果是个位数,那好说,就调用一次`path.pop_back()`,但如果是 十位数,百位数,千位数呢? 百位数就要调用三次`path.pop_back()`,才能实现对应的回溯操作,这样代码实现就太冗余了。 + +所以,第一个代码版本中,我才使用 vector 类型的path,这样方便给大家演示代码中回溯的操作。 vector类型的path,不管 每次 路径收集的数字是几位数,总之一定是int,所以就一次 pop_back就可以。 + + +### 迭代法 + + +至于非递归的方式,我们可以依然可以使用前序遍历的迭代方式来模拟遍历路径的过程,对该迭代方式不了解的同学,可以看文章[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)和[二叉树:前中后序迭代方式统一写法](https://programmercarl.com/二叉树的统一迭代法.html)。 + +这里除了模拟递归需要一个栈,同时还需要一个栈来存放对应的遍历路径。 + +C++代码如下: + +```CPP +class Solution { +public: + vector binaryTreePaths(TreeNode* root) { + stack treeSt;// 保存树的遍历节点 + stack pathSt; // 保存遍历路径的节点 + vector result; // 保存最终路径集合 + if (root == NULL) return result; + treeSt.push(root); + pathSt.push(to_string(root->val)); + while (!treeSt.empty()) { + TreeNode* node = treeSt.top(); treeSt.pop(); // 取出节点 中 + string path = pathSt.top();pathSt.pop(); // 取出该节点对应的路径 + if (node->left == NULL && node->right == NULL) { // 遇到叶子节点 + result.push_back(path); + } + if (node->right) { // 右 + treeSt.push(node->right); + pathSt.push(path + "->" + to_string(node->right->val)); + } + if (node->left) { // 左 + treeSt.push(node->left); + pathSt.push(path + "->" + to_string(node->left->val)); + } + } + return result; + } +}; +``` +当然,使用java的同学,可以直接定义一个成员变量为object的栈`Stack stack = new Stack<>();`,这样就不用定义两个栈了,都放到一个栈里就可以了。 + +## 总结 + +**本文我们开始初步涉及到了回溯,很多同学过了这道题目,可能都不知道自己其实使用了回溯,回溯和递归都是相伴相生的。** + +我在第一版递归代码中,把递归与回溯的细节都充分的展现了出来,大家可以自己感受一下。 + +第二版递归代码对于初学者其实非常不友好,代码看上去简单,但是隐藏细节于无形。 + +最后我依然给出了迭代法。 + +对于本题充分了解递归与回溯的过程之后,有精力的同学可以再去实现迭代法。 + +## 其他语言版本 + +### Java: + +```Java +//解法一 + +//方式一 +class Solution { + /** + * 递归法 + */ + public List binaryTreePaths(TreeNode root) { + List res = new ArrayList<>();// 存最终的结果 + if (root == null) { + return res; + } + List paths = new ArrayList<>();// 作为结果中的路径 + traversal(root, paths, res); + return res; + } + + private void traversal(TreeNode root, List paths, List res) { + paths.add(root.val);// 前序遍历,中 + // 遇到叶子结点 + if (root.left == null && root.right == null) { + // 输出 + StringBuilder sb = new StringBuilder();// StringBuilder用来拼接字符串,速度更快 + for (int i = 0; i < paths.size() - 1; i++) { + sb.append(paths.get(i)).append("->"); + } + sb.append(paths.get(paths.size() - 1));// 记录最后一个节点 + res.add(sb.toString());// 收集一个路径 + return; + } + // 递归和回溯是同时进行,所以要放在同一个花括号里 + if (root.left != null) { // 左 + traversal(root.left, paths, res); + paths.remove(paths.size() - 1);// 回溯 + } + if (root.right != null) { // 右 + traversal(root.right, paths, res); + paths.remove(paths.size() - 1);// 回溯 + } + } +} + +//方式二 +class Solution { + + List result = new ArrayList<>(); + + public List binaryTreePaths(TreeNode root) { + deal(root, ""); + return result; + } + + public void deal(TreeNode node, String s) { + if (node == null) + return; + if (node.left == null && node.right == null) { + result.add(new StringBuilder(s).append(node.val).toString()); + return; + } + String tmp = new StringBuilder(s).append(node.val).append("->").toString(); + deal(node.left, tmp); + deal(node.right, tmp); + } +} +``` +```java +// 解法二 +class Solution { + /** + * 迭代法 + */ + public List binaryTreePaths(TreeNode root) { + List result = new ArrayList<>(); + if (root == null) + return result; + Stack stack = new Stack<>(); + // 节点和路径同时入栈 + stack.push(root); + stack.push(root.val + ""); + while (!stack.isEmpty()) { + // 节点和路径同时出栈 + String path = (String) stack.pop(); + TreeNode node = (TreeNode) stack.pop(); + // 若找到叶子节点 + if (node.left == null && node.right == null) { + result.add(path); + } + //右子节点不为空 + if (node.right != null) { + stack.push(node.right); + stack.push(path + "->" + node.right.val); + } + //左子节点不为空 + if (node.left != null) { + stack.push(node.left); + stack.push(path + "->" + node.left.val); + } + } + return result; + } +} +``` +--- +### Python: + + +递归法+回溯 +```Python +# Definition for a binary tree node. +class Solution: + def traversal(self, cur, path, result): + path.append(cur.val) # 中 + if not cur.left and not cur.right: # 到达叶子节点 + sPath = '->'.join(map(str, path)) + result.append(sPath) + return + if cur.left: # 左 + self.traversal(cur.left, path, result) + path.pop() # 回溯 + if cur.right: # 右 + self.traversal(cur.right, path, result) + path.pop() # 回溯 + + def binaryTreePaths(self, root): + result = [] + path = [] + if not root: + return result + self.traversal(root, path, result) + return result + + +``` +递归法+隐形回溯(版本一) +```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 +from typing import List, Optional + +class Solution: + def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]: + if not root: + return [] + result = [] + self.traversal(root, [], result) + return result + + def traversal(self, cur: TreeNode, path: List[int], result: List[str]) -> None: + if not cur: + return + path.append(cur.val) + if not cur.left and not cur.right: + result.append('->'.join(map(str, path))) + if cur.left: + self.traversal(cur.left, path[:], result) + if cur.right: + self.traversal(cur.right, path[:], result) + +``` + +递归法+隐形回溯(版本二) +```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 binaryTreePaths(self, root: TreeNode) -> List[str]: + path = '' + result = [] + if not root: return result + self.traversal(root, path, result) + return result + + def traversal(self, cur: TreeNode, path: str, result: List[str]) -> None: + path += str(cur.val) + # 若当前节点为leave,直接输出 + if not cur.left and not cur.right: + result.append(path) + + if cur.left: + # + '->' 是隐藏回溯 + self.traversal(cur.left, path + '->', result) + + if cur.right: + self.traversal(cur.right, path + '->', result) +``` + +迭代法: + +```Python +class Solution: + + def binaryTreePaths(self, root: TreeNode) -> List[str]: + # 题目中节点数至少为1 + stack, path_st, result = [root], [str(root.val)], [] + + while stack: + cur = stack.pop() + path = path_st.pop() + # 如果当前节点为叶子节点,添加路径到结果中 + if not (cur.left or cur.right): + result.append(path) + if cur.right: + stack.append(cur.right) + path_st.append(path + '->' + str(cur.right.val)) + if cur.left: + stack.append(cur.left) + path_st.append(path + '->' + str(cur.left.val)) + + return result +``` + +--- + +### Go: + +递归法: + +```go +func binaryTreePaths(root *TreeNode) []string { + res := make([]string, 0) + var travel func(node *TreeNode, s string) + travel = func(node *TreeNode, s string) { + if node.Left == nil && node.Right == nil { + v := s + strconv.Itoa(node.Val) + res = append(res, v) + return + } + s = s + strconv.Itoa(node.Val) + "->" + if node.Left != nil { + travel(node.Left, s) + } + if node.Right != nil { + travel(node.Right, s) + } + } + travel(root, "") + return res +} +``` + +迭代法: + +```go +func binaryTreePaths(root *TreeNode) []string { + stack := []*TreeNode{} + paths := make([]string, 0) + res := make([]string, 0) + if root != nil { + stack = append(stack, root) + paths = append(paths, "") + } + for len(stack) > 0 { + l := len(stack) + node := stack[l-1] + path := paths[l-1] + stack = stack[:l-1] + paths = paths[:l-1] + if node.Left == nil && node.Right == nil { + res = append(res, path+strconv.Itoa(node.Val)) + continue + } + if node.Right != nil { + stack = append(stack, node.Right) + paths = append(paths, path+strconv.Itoa(node.Val)+"->") + } + if node.Left != nil { + stack = append(stack, node.Left) + paths = append(paths, path+strconv.Itoa(node.Val)+"->") + } + } + return res +} +``` + +--- +### JavaScript: + +递归法: + +```javascript +var binaryTreePaths = function(root) { + //递归遍历+递归三部曲 + let res = []; + //1. 确定递归函数 函数参数 + const getPath = function(node,curPath) { + //2. 确定终止条件,到叶子节点就终止 + if(node.left === null && node.right === null) { + curPath += node.val; + res.push(curPath); + return; + } + //3. 确定单层递归逻辑 + curPath += node.val + '->'; + node.left && getPath(node.left, curPath); + node.right && getPath(node.right, curPath); + } + getPath(root, ''); + return res; +}; +``` + +迭代法: + +```javascript +var binaryTreePaths = function(root) { + if (!root) return []; + const stack = [root], paths = [''], res = []; + while (stack.length) { + const node = stack.pop(); + let path = paths.pop(); + if (!node.left && !node.right) { // 到叶子节点终止, 添加路径到结果中 + res.push(path + node.val); + continue; + } + path += node.val + '->'; + if (node.right) { // 右节点存在 + stack.push(node.right); + paths.push(path); + } + if (node.left) { // 左节点存在 + stack.push(node.left); + paths.push(path); + } + } + return res; +}; +``` + +### TypeScript: + +> 递归法 + +```typescript +function binaryTreePaths(root: TreeNode | null): string[] { + function recur(node: TreeNode, route: string, resArr: string[]): void { + route += String(node.val); + if (node.left === null && node.right === null) { + resArr.push(route); + return; + } + if (node.left !== null) recur(node.left, route + '->', resArr); + if (node.right !== null) recur(node.right, route + '->', resArr); + } + const resArr: string[] = []; + if (root === null) return resArr; + recur(root, '', resArr); + return resArr; +}; +``` + +> 迭代法 + +```typescript +// 迭代法2 +function binaryTreePaths(root: TreeNode | null): string[] { + let helperStack: TreeNode[] = []; + let tempNode: TreeNode; + let routeArr: string[] = []; + let resArr: string[] = []; + if (root !== null) { + helperStack.push(root); + routeArr.push(String(root.val)); + }; + while (helperStack.length > 0) { + tempNode = helperStack.pop()!; + let route: string = routeArr.pop()!; // tempNode 对应的路径 + if (tempNode.left === null && tempNode.right === null) { + resArr.push(route); + } + if (tempNode.right !== null) { + helperStack.push(tempNode.right); + routeArr.push(route + '->' + tempNode.right.val); // tempNode.right 对应的路径 + } + if (tempNode.left !== null) { + helperStack.push(tempNode.left); + routeArr.push(route + '->' + tempNode.left.val); // tempNode.left 对应的路径 + } + } + return resArr; +}; +``` + +### Swift: + +> 递归/回溯 +```swift +func binaryTreePaths(_ root: TreeNode?) -> [String] { + var res = [String]() + guard let root = root else { + return res + } + var path = [Int]() + _binaryTreePaths(root, path: &path, res: &res) + return res +} +func _binaryTreePaths(_ root: TreeNode, path: inout [Int], res: inout [String]) { + path.append(root.val) + if root.left == nil && root.right == nil { + var str = "" + for i in 0 ..< path.count - 1 { + str.append("\(path[i])->") + } + str.append("\(path.last!)") + res.append(str) + return + } + if let left = root.left { + _binaryTreePaths(left, path: &path, res: &res) + path.removeLast() + } + if let right = root.right { + _binaryTreePaths(right, path: &path, res: &res) + path.removeLast() + } +} +``` + +> 迭代 +```swift +func binaryTreePaths(_ root: TreeNode?) -> [String] { + var res = [String]() + guard let root = root else { + return res + } + var stackNode = [TreeNode]() + stackNode.append(root) + + var stackStr = [String]() + stackStr.append("\(root.val)") + + while !stackNode.isEmpty { + let node = stackNode.popLast()! + let str = stackStr.popLast()! + if node.left == nil && node.right == nil { + res.append(str) + } + if let left = node.left { + stackNode.append(left) + stackStr.append("\(str)->\(left.val)") + } + if let right = node.right { + stackNode.append(right) + stackStr.append("\(str)->\(right.val)") + } + } + return res +} +``` + +### Scala: + +递归: +```scala +object Solution { + import scala.collection.mutable.ListBuffer + def binaryTreePaths(root: TreeNode): List[String] = { + val res = ListBuffer[String]() + def traversal(curNode: TreeNode, path: ListBuffer[Int]): Unit = { + path.append(curNode.value) + if (curNode.left == null && curNode.right == null) { + res.append(path.mkString("->")) // mkString函数: 将数组的所有值按照指定字符串拼接 + return // 处理完可以直接return + } + + if (curNode.left != null) { + traversal(curNode.left, path) + path.remove(path.size - 1) + } + if (curNode.right != null) { + traversal(curNode.right, path) + path.remove(path.size - 1) + } + } + traversal(root, ListBuffer[Int]()) + res.toList + } +} +``` + +### Rust: + +```rust +// 递归 +impl Solution { + pub fn binary_tree_paths(root: Option>>) -> Vec { + let mut res = vec![]; + Self::recur(&root, String::from(""), &mut res); + res + } + pub fn recur(node: &Option>>, mut path: String, res: &mut Vec) { + let r = node.as_ref().unwrap().borrow(); + path += format!("{}", r.val).as_str(); + if r.left.is_none() && r.right.is_none() { + res.push(path.to_string()); + return; + } + if r.left.is_some() { + Self::recur(&r.left, path.clone() + "->", res); + } + if r.right.is_some() { + Self::recur(&r.right, path + "->", res); + } + } +} +``` +### C# +```csharp +public IList BinaryTreePaths(TreeNode root) +{ + List path = new(); + List res = new(); + if (root == null) return res; + Traversal(root, path, res); + return res; +} +public void Traversal(TreeNode node, List path, List res) +{ + path.Add(node.val); + if (node.left == null && node.right == null) + { + string sPath = ""; + for (int i = 0; i < path.Count - 1; i++) + { + sPath += path[i].ToString(); + sPath += "->"; + } + sPath += path[path.Count - 1].ToString(); + res.Add(sPath); + return; + } + if (node.left != null) + { + Traversal(node.left, path, res); + path.RemoveAt(path.Count-1); + } + if (node.right != null) + { + Traversal(node.right, path, res); + path.RemoveAt(path.Count-1); + } +} +``` + diff --git "a/problems/0279.\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" "b/problems/0279.\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" new file mode 100755 index 0000000000..7c5d7c9c9f --- /dev/null +++ "b/problems/0279.\345\256\214\345\205\250\345\271\263\346\226\271\346\225\260.md" @@ -0,0 +1,479 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 279.完全平方数 + +[力扣题目链接](https://leetcode.cn/problems/perfect-squares/) + +给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少。 + +给你一个整数 n ,返回和为 n 的完全平方数的 最少数量 。 + +完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。 + +示例 1: +* 输入:n = 12 +* 输出:3 +* 解释:12 = 4 + 4 + 4 + +示例 2: +* 输入:n = 13 +* 输出:2 +* 解释:13 = 4 + 9 + +提示: +* 1 <= n <= 10^4 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[换汤不换药!| LeetCode:279.完全平方数](https://www.bilibili.com/video/BV12P411T7Br/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +可能刚看这种题感觉没啥思路,又平方和的,又最小数的。 + +**我来把题目翻译一下:完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?** + +感受出来了没,这么浓厚的完全背包氛围,而且和昨天的题目[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)就是一样一样的! + +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[j]:和为j的完全平方数的最少数量为dp[j]** + +2. 确定递推公式 + +dp[j] 可以由dp[j - i * i]推出, dp[j - i * i] + 1 便可以凑成dp[j]。 + +此时我们要选择最小的dp[j],所以递推公式:dp[j] = min(dp[j - i * i] + 1, dp[j]); + +3. dp数组如何初始化 + +dp[0]表示 和为0的完全平方数的最小数量,那么dp[0]一定是0。 + +有同学问题,那0 * 0 也算是一种啊,为啥dp[0] 就是 0呢? + +看题目描述,找到若干个完全平方数(比如 1, 4, 9, 16, ...),题目描述中可没说要从0开始,dp[0]=0完全是为了递推公式。 + +非0下标的dp[j]应该是多少呢? + +从递归公式dp[j] = min(dp[j - i * i] + 1, dp[j]);中可以看出每次dp[j]都要选最小的,**所以非0下标的dp[j]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖**。 + +4. 确定遍历顺序 + +我们知道这是完全背包, + +如果求组合数就是外层for循环遍历物品,内层for遍历背包。 + +如果求排列数就是外层for遍历背包,内层for循环遍历物品。 + +在[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)中我们就深入探讨了这个问题,本题也是一样的,是求最小数! + +**所以本题外层for遍历背包,内层for遍历物品,还是外层for遍历物品,内层for遍历背包,都是可以的!** + +我这里先给出外层遍历背包,内层遍历物品的代码: + +```CPP +vector dp(n + 1, INT_MAX); +dp[0] = 0; +for (int i = 0; i <= n; i++) { // 遍历背包 + for (int j = 1; j * j <= i; j++) { // 遍历物品 + dp[i] = min(dp[i - j * j] + 1, dp[i]); + } +} + +``` + +5. 举例推导dp数组 + +已输入n为5例,dp状态图如下: + + +![279.完全平方数](https://file1.kamacoder.com/i/algo/20210202112617341.jpg) + +dp[0] = 0 +dp[1] = min(dp[0] + 1) = 1 +dp[2] = min(dp[1] + 1) = 2 +dp[3] = min(dp[2] + 1) = 3 +dp[4] = min(dp[3] + 1, dp[0] + 1) = 1 +dp[5] = min(dp[4] + 1, dp[1] + 1) = 2 + +最后的dp[n]为最终结果。 + +以上动规五部曲分析完毕C++代码如下: + +```CPP +// 版本一 +class Solution { +public: + int numSquares(int n) { + vector dp(n + 1, INT_MAX); + dp[0] = 0; + for (int i = 0; i <= n; i++) { // 遍历背包 + for (int j = 1; j * j <= i; j++) { // 遍历物品 + dp[i] = min(dp[i - j * j] + 1, dp[i]); + } + } + return dp[n]; + } +}; +``` + +* 时间复杂度: O(n * √n) +* 空间复杂度: O(n) + + +同样我在给出先遍历物品,在遍历背包的代码,一样的可以AC的。 + +```CPP +// 版本二 +class Solution { +public: + int numSquares(int n) { + vector dp(n + 1, INT_MAX); + dp[0] = 0; + for (int i = 1; i * i <= n; i++) { // 遍历物品 + for (int j = i * i; j <= n; j++) { // 遍历背包 + dp[j] = min(dp[j - i * i] + 1, dp[j]); + } + } + return dp[n]; + } +}; +``` +* 同上 + + +## 总结 + +如果大家认真做了昨天的题目[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html),今天这道就非常简单了,一样的套路一样的味道。 + +但如果没有按照「代码随想录」的题目顺序来做的话,做动态规划或者做背包问题,上来就做这道题,那还是挺难的! + +经过前面的训练这道题已经是简单题了 + + + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + // 版本一,先遍历物品, 再遍历背包 + public int numSquares(int n) { + int max = Integer.MAX_VALUE; + int[] dp = new int[n + 1]; + //初始化 + for (int j = 0; j <= n; j++) { + dp[j] = max; + } + //如果不想要寫for-loop填充數組的話,也可以用JAVA內建的Arrays.fill()函數。 + //Arrays.fill(dp, Integer.MAX_VALUE); + + //当和为0时,组合的个数为0 + dp[0] = 0; + // 遍历物品 + for (int i = 1; i * i <= n; i++) { + // 遍历背包 + for (int j = i * i; j <= n; j++) { + //if (dp[j - i * i] != max) { + dp[j] = Math.min(dp[j], dp[j - i * i] + 1); + //} + //不需要這個if statement,因爲在完全平方數這一題不會有"湊不成"的狀況發生( 一定可以用"1"來組成任何一個n),故comment掉這個if statement。 + } + } + return dp[n]; + } +} + +class Solution { + // 版本二, 先遍历背包, 再遍历物品 + public int numSquares(int n) { + int max = Integer.MAX_VALUE; + int[] dp = new int[n + 1]; + // 初始化 + for (int j = 0; j <= n; j++) { + dp[j] = max; + } + // 当和为0时,组合的个数为0 + dp[0] = 0; + // 遍历背包 + for (int j = 1; j <= n; j++) { + // 遍历物品 + for (int i = 1; i * i <= j; i++) { + dp[j] = Math.min(dp[j], dp[j - i * i] + 1); + } + } + return dp[n]; + } +} +``` + +### Python: + +先遍历背包, 再遍历物品 +```python +class Solution: + def numSquares(self, n: int) -> int: + dp = [float('inf')] * (n + 1) + dp[0] = 0 + + for i in range(1, n + 1): # 遍历背包 + for j in range(1, int(i ** 0.5) + 1): # 遍历物品 + # 更新凑成数字 i 所需的最少完全平方数数量 + dp[i] = min(dp[i], dp[i - j * j] + 1) + + return dp[n] + +``` +先遍历物品, 再遍历背包 +```python +class Solution: + def numSquares(self, n: int) -> int: + dp = [float('inf')] * (n + 1) + dp[0] = 0 + + for i in range(1, int(n ** 0.5) + 1): # 遍历物品 + for j in range(i * i, n + 1): # 遍历背包 + # 更新凑成数字 j 所需的最少完全平方数数量 + dp[j] = min(dp[j - i * i] + 1, dp[j]) + + return dp[n] + + +``` +其他版本 +```python +class Solution: + def numSquares(self, n: int) -> int: + # 创建动态规划数组,初始值为最大值 + dp = [float('inf')] * (n + 1) + # 初始化已知情况 + dp[0] = 0 + + # 遍历背包容量 + for i in range(1, n + 1): + # 遍历完全平方数作为物品 + j = 1 + while j * j <= i: + # 更新最少完全平方数的数量 + dp[i] = min(dp[i], dp[i - j * j] + 1) + j += 1 + + # 返回结果 + return dp[n] + +``` +```python +class Solution(object): + def numSquares(self, n): + # 先把可以选的数准备好,更好理解 + nums, num = [], 1 + while num ** 2 <= n: + nums.append(num ** 2) + num += 1 + # dp数组初始化 + dp = [float('inf')] * (n + 1) + dp[0] = 0 + + # 遍历准备好的完全平方数 + for i in range(len(nums)): + # 遍历背包容量 + for j in range(nums[i], n+1): + dp[j] = min(dp[j], dp[j-nums[i]]+1) + # 返回结果 + return dp[-1] + + +``` +### Go: + +```go +// 版本一,先遍历物品, 再遍历背包 +func numSquares1(n int) int { + //定义 + dp := make([]int, n+1) + // 初始化 + dp[0] = 0 + for i := 1; i <= n; i++ { + dp[i] = math.MaxInt32 + } + // 遍历物品 + for i := 1; i <= n; i++ { + // 遍历背包 + for j := i*i; j <= n; j++ { + dp[j] = min(dp[j], dp[j-i*i]+1) + } + } + + return dp[n] +} + +// 版本二,先遍历背包, 再遍历物品 +func numSquares2(n int) int { + //定义 + dp := make([]int, n+1) + // 初始化 + dp[0] = 0 + // 遍历背包 + for j := 1; j <= n; j++ { + //初始化 + dp[j] = math.MaxInt32 + // 遍历物品 + for i := 1; i <= n; i++ { + if j >= i*i { + dp[j] = min(dp[j], dp[j-i*i]+1) + } + } + } + + return dp[n] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +### JavaScript: + +```Javascript +// 先遍历物品,再遍历背包 +var numSquares1 = function(n) { + let dp = new Array(n + 1).fill(Infinity) + dp[0] = 0 + + for(let i = 1; i**2 <= n; i++) { + let val = i**2 + for(let j = val; j <= n; j++) { + dp[j] = Math.min(dp[j], dp[j - val] + 1) + } + } + return dp[n] +}; +// 先遍历背包,再遍历物品 +var numSquares2 = function(n) { + let dp = new Array(n + 1).fill(Infinity) + dp[0] = 0 + + for(let i = 1; i <= n; i++) { + for(let j = 1; j * j <= i; j++) { + dp[i] = Math.min(dp[i - j * j] + 1, dp[i]) + } + } + + return dp[n] +}; +``` + +### TypeScript: + +```typescript +// 先遍历物品 +function numSquares(n: number): number { + const goodsNum: number = Math.floor(Math.sqrt(n)); + const dp: number[] = new Array(n + 1).fill(Infinity); + dp[0] = 0; + for (let i = 1; i <= goodsNum; i++) { + const tempVal: number = i * i; + for (let j = tempVal; j <= n; j++) { + dp[j] = Math.min(dp[j], dp[j - tempVal] + 1); + } + } + return dp[n]; +}; +``` + +```rust +// 先遍历背包 +function numSquares(n: number): number { + const dp = Array(n + 1).fill(Infinity) + dp[0] = 0; + for(let i = 1; i <= n; i++){ + for(let j = 1; j * j <= i; j++){ + dp[i] = Math.min(dp[i], dp[i -j * j] + 1) + } + } + return dp[n] +}; +``` + +### C + +```c +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int numSquares(int n) { + int* dp = (int*)malloc(sizeof(int) * (n + 1)); + for (int j = 0; j < n + 1; j++) { + dp[j] = INT_MAX; + } + dp[0] = 0; + // 遍历背包 + for (int i = 0; i <= n; ++i) { + // 遍历物品 + for (int j = 1; j * j <= i; ++j) { + dp[i] = min(dp[i - j * j] + 1, dp[i]); + } + } + return dp[n]; +} +``` + + + +### Rust: + +```rust +// 先遍历背包 +impl Solution { + pub fn num_squares(n: i32) -> i32 { + let n = n as usize; + let mut dp = vec![i32::MAX; n + 1]; + dp[0] = 0; + for i in 0..=n { + let mut j = 1; + loop { + match j * j > i { + true => break, + false => dp[i] = dp[i].min(dp[i - j * j] + 1), + } + j += 1; + } + } + dp[n] + } +} +``` + +```rust +// 先遍历物品 +impl Solution { + pub fn num_squares(n: i32) -> i32 { + let (n, mut goods) = (n as usize, 1); + let mut dp = vec![i32::MAX; n + 1]; + dp[0] = 0; + loop { + if goods * goods > n { + break; + } + for j in goods * goods..=n { + dp[j] = dp[j].min(dp[j - goods * goods] + 1); + } + goods += 1; + } + dp[n] + } +} +``` + + diff --git "a/problems/0283.\347\247\273\345\212\250\351\233\266.md" "b/problems/0283.\347\247\273\345\212\250\351\233\266.md" new file mode 100755 index 0000000000..e25525684e --- /dev/null +++ "b/problems/0283.\347\247\273\345\212\250\351\233\266.md" @@ -0,0 +1,186 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 283. 移动零:动态规划:一样的套路,再求一次完全平方数 + +[力扣题目链接](https://leetcode.cn/problems/move-zeroes/) + +给定一个数组 nums,编写一个函数将所有 0 移动到数组的末尾,同时保持非零元素的相对顺序。 + +示例: + +输入: [0,1,0,3,12] +输出: [1,3,12,0,0] +说明: + +必须在原数组上操作,不能拷贝额外的数组。 +尽量减少操作次数。 + + +## 思路 + +做这道题目之前,大家可以做一做[27.移除元素](https://programmercarl.com/0027.移除元素.html) + +这道题目,使用暴力的解法,可以两层for循环,模拟数组删除元素(也就是向前覆盖)的过程。 + +好了,我们说一说双指针法,大家如果对双指针还不熟悉,可以看我的这篇总结[双指针法:总结篇!](https://programmercarl.com/双指针总结.html)。 + +双指针法在数组移除元素中,可以达到O(n)的时间复杂度,在[27.移除元素](https://programmercarl.com/0027.移除元素.html)里已经详细讲解了,那么本题和移除元素其实是一个套路。 + +**相当于对整个数组移除元素0,然后slowIndex之后都是移除元素0的冗余元素,把这些元素都赋值为0就可以了**。 + +如动画所示: + +![移动零](https://file1.kamacoder.com/i/algo/283.%E7%A7%BB%E5%8A%A8%E9%9B%B6.gif) + +C++代码如下: + +```CPP +class Solution { +public: + void moveZeroes(vector& nums) { + int slowIndex = 0; + for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { + if (nums[fastIndex] != 0) { + nums[slowIndex++] = nums[fastIndex]; + } + } + // 将slowIndex之后的冗余元素赋值为0 + for (int i = slowIndex; i < nums.size(); i++) { + nums[i] = 0; + } + } +}; +``` + +## 其他语言版本 + +### Java: + +```java +public void moveZeroes(int[] nums) { + int slow = 0; + for (int fast = 0; fast < nums.length; fast++) { + if (nums[fast] != 0) { + nums[slow++] = nums[fast]; + } + } + // 后面的元素全变成 0 + for (int j = slow; j < nums.length; j++) { + nums[j] = 0; + } + } +``` + +### Python: + +```python + def moveZeroes(self, nums: List[int]) -> None: + slow = 0 + for fast in range(len(nums)): + if nums[fast] != 0: + nums[slow] = nums[fast] + slow += 1 + for i in range(slow, len(nums)): + nums[i] = 0 +``` +交换前后变量,避免补零 +```python + def moveZeroes(self, nums: List[int]) -> None: + slow, fast = 0, 0 + while fast < len(nums): + if nums[fast] != 0: + nums[slow], nums[fast] = nums[fast], nums[slow] + slow += 1 # 保持[0, slow)区间是没有0的 + fast += 1 +``` + +### Go: + +```go +func moveZeroes(nums []int) { + slow := 0 + for fast := 0; fast < len(nums); fast ++ { + if nums[fast] != 0 { + temp := nums[slow] + nums[slow] = nums[fast] + nums[fast] = temp + slow++ + } + } +} +``` + +### JavaScript: + +```javascript +var moveZeroes = function(nums) { + let slow = 0; + for(let fast = 0; fast < nums.length; fast++){ + if(nums[fast] != 0){//找到非0的元素 + nums[slow] = nums[fast];//把非0的元素赋值给数组慢指针指向的索引处的值 + slow++;//慢指针向右移动 + } + } + // 后面的元素全变成 0 + for(let j = slow; j < nums.length; j++){ + nums[j] = 0; + } +}; +``` + +### TypeScript: + +```typescript +function moveZeroes(nums: number[]): void { + const length: number = nums.length; + let slowIndex: number = 0, + fastIndex: number = 0; + while (fastIndex < length) { + if (nums[fastIndex] !== 0) { + nums[slowIndex++] = nums[fastIndex]; + }; + fastIndex++; + } + while (slowIndex < length) { + nums[slowIndex++] = 0; + } +}; +``` + +### C + +```c +void moveZeroes(int* nums, int numsSize){ + int fastIndex = 0, slowIndex = 0; + for (; fastIndex < numsSize; fastIndex++) { + if (nums[fastIndex] != 0) { + nums[slowIndex++] = nums[fastIndex]; + } + } + + // 将slowIndex之后的元素变为0 + for (; slowIndex < numsSize; slowIndex++) { + nums[slowIndex] = 0; + } +} +``` + +### Rust +```rust +impl Solution { + pub fn move_zeroes(nums: &mut Vec) { + let mut slow = 0; + for fast in 0..nums.len() { + if nums[fast] != 0 { + nums.swap(slow, fast); + slow += 1; + } + } + } +} +``` + + + diff --git "a/problems/0300.\346\234\200\351\225\277\344\270\212\345\215\207\345\255\220\345\272\217\345\210\227.md" "b/problems/0300.\346\234\200\351\225\277\344\270\212\345\215\207\345\255\220\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..06adfd950d --- /dev/null +++ "b/problems/0300.\346\234\200\351\225\277\344\270\212\345\215\207\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,361 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 300.最长递增子序列 + +[力扣题目链接](https://leetcode.cn/problems/longest-increasing-subsequence/) + +给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 + +子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 + + +示例 1: +* 输入:nums = [10,9,2,5,3,7,101,18] +* 输出:4 +* 解释:最长递增子序列是 [2,3,7,101],因此长度为 4 。 + +示例 2: +* 输入:nums = [0,1,0,3,2,3] +* 输出:4 + +示例 3: +* 输入:nums = [7,7,7,7,7,7,7] +* 输出:1 + +提示: + +* 1 <= nums.length <= 2500 +* -10^4 <= nums[i] <= 104 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)::[动态规划之子序列问题,元素不连续!| LeetCode:300.最长递增子序列](https://www.bilibili.com/video/BV1ng411J7xP),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +首先通过本题大家要明确什么是子序列,“子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序”。 + +本题也是代码随想录中子序列问题的第一题,如果没接触过这种题目的话,本题还是很难的,甚至想暴力去搜索也不知道怎么搜。 +子序列问题是动态规划解决的经典问题,当前下标i的递增子序列长度,其实和i之前的下标j的子序列长度有关系,那又是什么样的关系呢。 + +接下来,我们依然用动规五部曲来详细分析一波: + +1. dp[i]的定义 + +本题中,正确定义dp数组的含义十分重要。 + +**dp[i]表示i之前包括i的以nums[i]结尾的最长递增子序列的长度** + +为什么一定表示 “以nums[i]结尾的最长递增子序” ,因为我们在 做 递增比较的时候,如果比较 nums[j] 和 nums[i] 的大小,那么两个递增子序列一定分别以nums[j]为结尾 和 nums[i]为结尾, 要不然这个比较就没有意义了,不是尾部元素的比较那么 如何算递增呢。 + + +2. 状态转移方程 + +位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。 + +所以:if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); + +**注意这里不是要dp[i] 与 dp[j] + 1进行比较,而是我们要取dp[j] + 1的最大值**。 + +3. dp[i]的初始化 + +每一个i,对应的dp[i](即最长递增子序列)起始大小至少都是1. + +4. 确定遍历顺序 + +dp[i] 是有0到i-1各个位置的最长递增子序列 推导而来,那么遍历i一定是从前向后遍历。 + +j其实就是遍历0到i-1,那么是从前到后,还是从后到前遍历都无所谓,只要吧 0 到 i-1 的元素都遍历了就行了。 所以默认习惯 从前向后遍历。 + +遍历i的循环在外层,遍历j则在内层,代码如下: + +```CPP +for (int i = 1; i < nums.size(); i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); + } + if (dp[i] > result) result = dp[i]; // 取长的子序列 +} +``` + +5. 举例推导dp数组 + +输入:[0,1,0,3,2],dp数组的变化如下: + +![300.最长上升子序列](https://file1.kamacoder.com/i/algo/20210110170945618.jpg) + + +如果代码写出来,但一直AC不了,那么就把dp数组打印出来,看看对不对! + +以上五部分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int lengthOfLIS(vector& nums) { + if (nums.size() <= 1) return nums.size(); + vector dp(nums.size(), 1); + int result = 0; + for (int i = 1; i < nums.size(); i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); + } + if (dp[i] > result) result = dp[i]; // 取长的子序列 + } + return result; + } +}; +``` +* 时间复杂度: O(n^2) +* 空间复杂度: O(n) + + + +## 总结 + +本题最关键的是要想到dp[i]由哪些状态可以推出来,并取最大值,那么很自然就能想到递推公式:dp[i] = max(dp[i], dp[j] + 1); + +子序列问题是动态规划的一个重要系列,本题算是入门题目,好戏刚刚开始! + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int lengthOfLIS(int[] nums) { + if (nums.length <= 1) return nums.length; + int[] dp = new int[nums.length]; + int res = 1; + Arrays.fill(dp, 1); + for (int i = 1; i < dp.length; i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + res = Math.max(res, dp[i]); + } + return res; + } +} +``` + +### Python: + +DP +```python +class Solution: + def lengthOfLIS(self, nums: List[int]) -> int: + if len(nums) <= 1: + return len(nums) + dp = [1] * len(nums) + result = 1 + for i in range(1, len(nums)): + for j in range(0, i): + if nums[i] > nums[j]: + dp[i] = max(dp[i], dp[j] + 1) + result = max(result, dp[i]) #取长的子序列 + return result +``` +贪心 +```python +class Solution: + def lengthOfLIS(self, nums: List[int]) -> int: + if len(nums) <= 1: + return len(nums) + + tails = [nums[0]] # 存储递增子序列的尾部元素 + for num in nums[1:]: + if num > tails[-1]: + tails.append(num) # 如果当前元素大于递增子序列的最后一个元素,直接加入到子序列末尾 + else: + # 使用二分查找找到当前元素在递增子序列中的位置,并替换对应位置的元素 + left, right = 0, len(tails) - 1 + while left < right: + mid = (left + right) // 2 + if tails[mid] < num: + left = mid + 1 + else: + right = mid + tails[left] = num + + return len(tails) # 返回递增子序列的长度 + +``` +### Go: + +```go +// 动态规划求解 +func lengthOfLIS(nums []int) int { + // dp数组的定义 dp[i]表示取第i个元素的时候,表示子序列的长度,其中包括 nums[i] 这个元素 + dp := make([]int, len(nums)) + + // 初始化,所有的元素都应该初始化为1 + for i := range dp { + dp[i] = 1 + } + + ans := dp[0] + for i := 1; i < len(nums); i++ { + for j := 0; j < i; j++ { + if nums[i] > nums[j] { + dp[i] = max(dp[i], dp[j] + 1) + } + } + if dp[i] > ans { + ans = dp[i] + } + } + return ans +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` +贪心+二分 优化 +```go +func lengthOfLIS(nums []int ) int { + dp := []int{} + for _, num := range nums { + if len(dp) == 0 || dp[len(dp) - 1] < num { + dp = append(dp, num) + } else { + l, r := 0, len(dp) - 1 + pos := r + for l <= r { + mid := (l + r) >> 1 + if dp[mid] >= num { + pos = mid; + r = mid - 1 + } else { + l = mid + 1 + } + } + dp[pos] = num + }//二分查找 + } + return len(dp) +} +``` + +### JavaScript: + +```javascript +const lengthOfLIS = (nums) => { + let dp = Array(nums.length).fill(1); + let result = 1; + + for(let i = 1; 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); + } + } + result = Math.max(result, dp[i]); + } + + return result; +}; +``` + +### TypeScript: + +```typescript +function lengthOfLIS(nums: number[]): number { + /** + dp[i]: 前i个元素中,以nums[i]结尾,最长子序列的长度 + */ + const dp: number[] = new Array(nums.length).fill(1); + let resMax: number = 0; + for (let i = 0, length = nums.length; i < length; i++) { + for (let j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + dp[i] = Math.max(dp[i], dp[j] + 1); + } + } + resMax = Math.max(resMax, dp[i]); + } + return resMax; +}; +``` + +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int lengthOfLIS(int* nums, int numsSize) { + if(numsSize <= 1){ + return numsSize; + } + int dp[numsSize]; + for(int i = 0; i < numsSize; i++){ + dp[i]=1; + } + int result = 1; + for (int i = 1; i < numsSize; ++i) { + for (int j = 0; j < i; ++j) { + if(nums[i] > nums[j]){ + dp[i] = max(dp[i], dp[j] + 1); + } + if(dp[i] > result){ + result = dp[i]; + } + } + } + return result; +} +``` + + + +### Rust: + +```rust +pub fn length_of_lis(nums: Vec) -> i32 { + let mut dp = vec![1; nums.len()]; + let mut result = 1; + for i in 1..nums.len() { + for j in 0..i { + if nums[j] < nums[i] { + dp[i] = dp[i].max(dp[j] + 1); + } + result = result.max(dp[i]); + } + } + result +} +``` + +### Cangjie: + +```cangjie +func lengthOfLIS(nums: Array): Int64 { + let n = nums.size + if (n <= 1) { + return n + } + + let dp = Array(n, item: 1) + var res = 0 + for (i in 1..n) { + for (j in 0..i) { + if (nums[i] > nums[j]) { + dp[i] = max(dp[i], dp[j] + 1) + } + } + res = max(dp[i], res) + } + return res +} +``` + + diff --git "a/problems/0309.\346\234\200\344\275\263\344\271\260\345\215\226\350\202\241\347\245\250\346\227\266\346\234\272\345\220\253\345\206\267\345\206\273\346\234\237.md" "b/problems/0309.\346\234\200\344\275\263\344\271\260\345\215\226\350\202\241\347\245\250\346\227\266\346\234\272\345\220\253\345\206\267\345\206\273\346\234\237.md" new file mode 100755 index 0000000000..d396e521b3 --- /dev/null +++ "b/problems/0309.\346\234\200\344\275\263\344\271\260\345\215\226\350\202\241\347\245\250\346\227\266\346\234\272\345\220\253\345\206\267\345\206\273\346\234\237.md" @@ -0,0 +1,604 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 309.最佳买卖股票时机含冷冻期 + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-cooldown/) + +给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。 + +设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): + +* 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 +* 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 + +示例: +* 输入: [1,2,3,0,2] +* 输出: 3 +* 解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划来决定最佳时机,这次有冷冻期!| LeetCode:309.买卖股票的最佳时机含冷冻期](https://www.bilibili.com/video/BV1rP4y1D7ku),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + + +相对于[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html),本题加上了一个冷冻期 + + +在[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html) 中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。 + +动规五部曲,分析如下: + +1. 确定dp数组以及下标的含义 + +dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。 + +**其实本题很多同学搞的比较懵,是因为出现冷冻期之后,状态其实是比较复杂度**,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。 + +具体可以区分出如下四个状态: + +* 状态一:持有股票状态(今天买入股票,或者是之前就买入了股票然后没有操作,一直持有) +* 不持有股票状态,这里就有两种卖出股票状态 + * 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。或者是前一天就是卖出股票状态,一直没操作) + * 状态三:今天卖出股票 +* 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天! + +![](https://file1.kamacoder.com/i/algo/518d5baaf33f4b2698064f8efb42edbf.png) + +j的状态为: + +* 0:状态一 +* 1:状态二 +* 2:状态三 +* 3:状态四 + +很多题解为什么讲的比较模糊,是因为把这四个状态合并成三个状态了,其实就是把状态二和状态四合并在一起了。 + +从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。 + +如果大家按照代码随想录顺序来刷的话,会发现 买卖股票最佳时机 1,2,3,4 的题目讲解中 + +* [动态规划:121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) +* [动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html) +* [动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html) +* [动态规划:188.买卖股票的最佳时机IV](https://programmercarl.com/0188.买卖股票的最佳时机IV.html) + +「今天卖出股票」我是没有单独列出一个状态的归类为「不持有股票的状态」,而本题为什么要单独列出「今天卖出股票」 一个状态呢? + +因为本题我们有冷冻期,而冷冻期的前一天,只能是 「今天卖出股票」状态,如果是 「不持有股票状态」那么就很模糊,因为不一定是 卖出股票的操作。 + +如果没有按照 代码随想录 顺序去刷的录友,可能看这里的讲解 会有点困惑,建议把代码随想录本篇之前股票内容的讲解都看一下,领会一下每天 状态的设置。 + +**注意这里的每一个状态,例如状态一,是持有股票股票状态并不是说今天一定就买入股票,而是说保持买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态**。 + +1. 确定递推公式 + +**达到买入股票状态**(状态一)即:dp[i][0],有两个具体操作: + +* 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0] +* 操作二:今天买入了,有两种情况 + * 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i] + * 前一天是保持卖出股票的状态(状态二),dp[i - 1][1] - prices[i] + +那么dp[i][0] = max(dp[i - 1][0], dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i]); + +**达到保持卖出股票状态**(状态二)即:dp[i][1],有两个具体操作: + +* 操作一:前一天就是状态二 +* 操作二:前一天是冷冻期(状态四) + +dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); + +**达到今天就卖出股票状态**(状态三),即:dp[i][2] ,只有一个操作: + +昨天一定是持有股票状态(状态一),今天卖出 + +即:dp[i][2] = dp[i - 1][0] + prices[i]; + +**达到冷冻期状态**(状态四),即:dp[i][3],只有一个操作: + +昨天卖出了股票(状态三) + +dp[i][3] = dp[i - 1][2]; + +综上分析,递推代码如下: + +```CPP +dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); +dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); +dp[i][2] = dp[i - 1][0] + prices[i]; +dp[i][3] = dp[i - 1][2]; +``` + +3. dp数组如何初始化 + +这里主要讨论一下第0天如何初始化。 + +如果是持有股票状态(状态一)那么:dp[0][0] = -prices[0],一定是当天买入股票。 + +保持卖出股票状态(状态二),这里其实从 「状态二」的定义来说 ,很难明确应该初始多少,这种情况我们就看递推公式需要我们给他初始成什么数值。 + +如果i为1,第1天买入股票,那么递归公式中需要计算 dp[i - 1][1] - prices[i] ,即 dp[0][1] - prices[1],那么大家感受一下 dp[0][1] (即第0天的状态二)应该初始成多少,只能初始为0。想一想如果初始为其他数值,是我们第1天买入股票后 手里还剩的现金数量是不是就不对了。 + +今天卖出了股票(状态三),同上分析,dp[0][2]初始化为0,dp[0][3]也初始为0。 + + +4. 确定遍历顺序 + +从递归公式上可以看出,dp[i] 依赖于 dp[i-1],所以是从前向后遍历。 + +5. 举例推导dp数组 + +以 [1,2,3,0,2] 为例,dp数组如下: + + +![309.最佳买卖股票时机含冷冻期](https://file1.kamacoder.com/i/algo/2021032317451040.png) + +最后结果是取 状态二,状态三,和状态四的最大值,不少同学会把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。 + +代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int n = prices.size(); + if (n == 0) return 0; + vector> dp(n, vector(4, 0)); + dp[0][0] -= prices[0]; // 持股票 + for (int i = 1; i < n; i++) { + dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3] - prices[i], dp[i - 1][1] - prices[i])); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); + dp[i][2] = dp[i - 1][0] + prices[i]; + dp[i][3] = dp[i - 1][2]; + } + return max(dp[n - 1][3], max(dp[n - 1][1], dp[n - 1][2])); + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +当然,空间复杂度可以优化,定义一个dp[2][4]大小的数组就可以了,就保存前一天的当前的状态,感兴趣的同学可以自己去写一写,思路是一样的。 + +## 总结 + +这次把冷冻期这道题目,讲的很透彻了,细分为四个状态,其状态转移也十分清晰,建议大家都按照四个状态来分析,如果只划分三个状态确实很容易给自己绕进去。 + +## 其他语言版本 + +### Java: + +```java +class Solution { + public int maxProfit(int[] prices) { + if (prices == null || prices.length < 2) { + return 0; + } + int[][] dp = new int[prices.length][2]; + + // bad case + dp[0][0] = 0; + dp[0][1] = -prices[0]; + dp[1][0] = Math.max(dp[0][0], dp[0][1] + prices[1]); + dp[1][1] = Math.max(dp[0][1], -prices[1]); + + for (int i = 2; i < prices.length; i++) { + // dp公式 + 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[prices.length - 1][0]; + } +} +``` +```java +//using 2*4 array for space optimization +//這裡稍微說一下,我在LeetCode提交的時候,2*4 2-D array的performance基本上和下面的1-D array performance差不多 +//都是time: 1ms, space: 40.X MB (其實 length*4 的 2-D array也僅僅是space:41.X MB,看起來不多) +//股票累的DP題目大致上都是這樣,就當作是一個延伸就好了。真的有人問如何優化,最起碼有東西可以講。 +class Solution { + /** + 1. [i][0] holding the stock + 2. [i][1] after cooldown but stil not buing the stock + 3. [i][2] selling the stock + 4. [i][3] cooldown + */ + public int maxProfit(int[] prices) { + int len = prices.length; + int dp[][] = new int [2][4]; + dp[0][0] = -prices[0]; + + for(int i = 1; i < len; i++){ + dp[i % 2][0] = Math.max(Math.max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]), dp[(i - 1) % 2][3] - prices[i]); + dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][3]); + dp[i % 2][2] = dp[(i - 1) % 2][0] + prices[i]; + dp[i % 2][3] = dp[(i - 1) % 2][2]; + } + return Math.max(Math.max(dp[(len - 1) % 2][1], dp[(len - 1) % 2][2]), dp[(len - 1) % 2][3]); + } +} +``` +```java +// 一维数组优化 +class Solution { + public int maxProfit(int[] prices) { + int[] dp=new int[4]; + + dp[0] = -prices[0]; + dp[1] = 0; + for(int i = 1; i <= prices.length; i++){ + // 使用临时变量来保存dp[0], dp[2] + // 因为马上dp[0]和dp[2]的数据都会变 + int temp = dp[0]; + int temp1 = dp[2]; + dp[0] = Math.max(dp[0], Math.max(dp[3], dp[1]) - prices[i]); + dp[1] = Math.max(dp[1], dp[3]); + dp[2] = temp + prices[i]; + dp[3] = temp1; + } + return Math.max(dp[3],Math.max(dp[1],dp[2])); + } +} +``` +```java +//另一种解题思路 +class Solution { + public int maxProfit(int[] prices) { + int[][] dp = new int[prices.length + 1][2]; + dp[1][0] = -prices[0]; + + for (int i = 2; i <= prices.length; i++) { + /* + dp[i][0] 第i天持有股票收益; + dp[i][1] 第i天不持有股票收益; + 情况一:第i天是冷静期,不能以dp[i-1][1]购买股票,所以以dp[i - 2][1]买股票,没问题 + 情况二:第i天不是冷静期,理论上应该以dp[i-1][1]购买股票,但是第i天不是冷静期说明,第i-1天没有卖出股票, + 则dp[i-1][1]=dp[i-2][1],所以可以用dp[i-2][1]买股票,没问题 + */ + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 2][1] - prices[i - 1]); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i - 1]); + } + + return dp[prices.length][1]; + } +} +``` + +### Python: +> 版本一 + +```python +from typing import List + +class Solution: + def maxProfit(self, prices: List[int]) -> int: + n = len(prices) + if n == 0: + return 0 + dp = [[0] * 4 for _ in range(n)] # 创建动态规划数组,4个状态分别表示持有股票、不持有股票且处于冷冻期、不持有股票且不处于冷冻期、不持有股票且当天卖出后处于冷冻期 + dp[0][0] = -prices[0] # 初始状态:第一天持有股票的最大利润为买入股票的价格 + for i in range(1, n): + dp[i][0] = max(dp[i-1][0], max(dp[i-1][3], dp[i-1][1]) - prices[i]) # 当前持有股票的最大利润等于前一天持有股票的最大利润或者前一天不持有股票且不处于冷冻期的最大利润减去当前股票的价格 + dp[i][1] = max(dp[i-1][1], dp[i-1][3]) # 当前不持有股票且处于冷冻期的最大利润等于前一天持有股票的最大利润加上当前股票的价格 + dp[i][2] = dp[i-1][0] + prices[i] # 当前不持有股票且不处于冷冻期的最大利润等于前一天不持有股票的最大利润或者前一天处于冷冻期的最大利润 + dp[i][3] = dp[i-1][2] # 当前不持有股票且当天卖出后处于冷冻期的最大利润等于前一天不持有股票且不处于冷冻期的最大利润 + return max(dp[n-1][3], dp[n-1][1], dp[n-1][2]) # 返回最后一天不持有股票的最大利润 + +``` + +> 版本二 +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + n = len(prices) + if n < 2: + return 0 + + # 定义三种状态的动态规划数组 + dp = [[0] * 3 for _ in range(n)] + dp[0][0] = -prices[0] # 持有股票的最大利润 + dp[0][1] = 0 # 不持有股票,且处于冷冻期的最大利润 + dp[0][2] = 0 # 不持有股票,不处于冷冻期的最大利润 + + for i in range(1, n): + # 当前持有股票的最大利润等于前一天持有股票的最大利润或者前一天不持有股票且不处于冷冻期的最大利润减去当前股票的价格 + dp[i][0] = max(dp[i-1][0], dp[i-1][2] - prices[i]) + # 当前不持有股票且处于冷冻期的最大利润等于前一天持有股票的最大利润加上当前股票的价格 + dp[i][1] = dp[i-1][0] + prices[i] + # 当前不持有股票且不处于冷冻期的最大利润等于前一天不持有股票的最大利润或者前一天处于冷冻期的最大利润 + dp[i][2] = max(dp[i-1][2], dp[i-1][1]) + + # 返回最后一天不持有股票的最大利润 + return max(dp[-1][1], dp[-1][2]) + +``` + +> 版本三 +```python +class Solution: + def maxProfit(self, prices: List[int]) -> int: + # 0: holding stocks + # (1) keep holding stocks: dp[i][0] = dp[i - 1][0] + # (2) buy stocks: dp[i][0] = dp[i - 1][1] - price, or dp[i - 1][3] - price + # 1: keep no stocks: dp[i][1] = dp[i - 1][1] + # 2: sell stocks: dp[i][2] = dp[i - 1][0] + price + # 3: cooldown day: dp[i][3] = dp[i - 1][2] + dp = [-prices[0], 0, 0, 0] + + for price in prices[1:]: + dc = dp.copy() # 这句话是关键,把前一天的 dp 状态保存下来,防止被覆盖掉,后面只用它,不用 dp,逻辑简单易懂 + dp[0] = max( + dc[0], + dc[1] - price, + dc[3] - price + ) + dp[1] = max( + dc[1], + dc[3] + ) + dp[2] = dc[0] + price + dp[3] = dc[2] + + return max(dp) +``` + +### Go: + +```go +// 最佳买卖股票时机含冷冻期 动态规划 +// 时间复杂度O(n) 空间复杂度O(n) +func maxProfit(prices []int) int { + n := len(prices) + if n < 2 { + return 0 + } + + dp := make([][]int, n) + status := make([]int, n * 4) + for i := range dp { + dp[i] = status[:4] + status = status[4:] + } + dp[0][0] = -prices[0] + + for i := 1; i < n; i++ { + dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][1] - prices[i], dp[i - 1][3] - prices[i])) + dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]) + dp[i][2] = dp[i - 1][0] + prices[i] + dp[i][3] = dp[i - 1][2] + } + + return max(dp[n - 1][1], max(dp[n - 1][2], dp[n - 1][3])) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +```go +// 一维优化版本 +// 时间复杂度O(n), 空间复杂度O(1) +func maxProfit(prices []int) int { + + // 0: 持有,一直持有和买入 + // 1: 不持有,一直不持有(不包含前一天卖出,因为这样的一天是冷静期,状态有区别) + // 2:不持有,今天卖出 + // 3:冷静期,前一天卖出(一直不持有) + dp0, dp1, dp2, dp3 := -prices[0], 0, 0, 0 + + n := len(prices) + + for i := 1; i < n; i++ { + t0 := max(dp0, max(dp1, dp3)-prices[i]) + t1 := max(dp1, dp3) + t2 := dp0 + prices[i] + t3 := dp2 + + // 更新 + dp0, dp1, dp2, dp3 = t0, t1, t2, t3 + } + + return max(dp1, max(dp2, dp3)) +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + + + +### JavaScript: + +> 不同的状态定义 感觉更容易理解些 +```javascript +function maxProfit(prices) { + // 第i天状态 持股 卖出 非冷冻期(不持股) 处于冷冻期 + const dp = new Array(prices.length).fill(0).map(() => [0, 0, 0, 0]); + dp[0][0] = -prices[0]; + for (let i = 1; i < prices.length; i++) { + // 持股 + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][2] - prices[i]); + // 卖出 + dp[i][1] = dp[i - 1][0] + prices[i]; + // 非冷冻期(不持股) + dp[i][2] = Math.max(dp[i - 1][2], dp[i - 1][1]); + // 冷冻期(上一天卖出) + dp[i][3] = dp[i - 1][1]; + } + return Math.max(...dp.pop()); +}; +``` + +```javascript +const maxProfit = (prices) => { + if(prices.length < 2) { + return 0 + } else if(prices.length < 3) { + return Math.max(0, prices[1] - prices[0]); + } + + let dp = Array.from(Array(prices.length), () => Array(4).fill(0)); + dp[0][0] = 0 - prices[0]; + + for(i = 1; i < prices.length; i++) { + dp[i][0] = Math.max(dp[i - 1][0], Math.max(dp[i-1][1], dp[i-1][3]) - prices[i]); + dp[i][1] = Math.max(dp[i -1][1], dp[i - 1][3]); + dp[i][2] = dp[i-1][0] + prices[i]; + dp[i][3] = dp[i-1][2]; + } + + return Math.max(dp[prices.length - 1][1], dp[prices.length - 1][2], dp[prices.length - 1][3]); +}; +``` + +```javascript +// 一维数组空间优化 +const maxProfit = (prices) => { + const n = prices.length + const dp = new Array(4).fill(0) + dp[0] = -prices[0] + for (let i = 1; i < n; i ++) { + const temp = dp[0] // 缓存上一次的状态 + const temp1 = dp[2] + dp[0] = Math.max(dp[0], Math.max(dp[3] - prices[i], dp[1] - prices[i])) // 持有状态 + dp[1] = Math.max(dp[1], dp[3]) // 今天不操作且不持有股票 + dp[2] = temp + prices[i] // 今天卖出股票 + dp[3] = temp1 // 冷冻期 + } + return Math.max(...dp) +}; +``` + +### TypeScript: + +> 版本一,与本文思路一致 + +```typescript +function maxProfit(prices: number[]): number { + /** + dp[i][0]: 持股状态; + dp[i][1]: 无股状态,当天为非冷冻期; + dp[i][2]: 无股状态,当天卖出; + dp[i][3]: 无股状态,当天为冷冻期; + */ + const length: number = prices.length; + const dp: number[][] = new Array(length).fill(0).map(_ => []); + dp[0][0] = -prices[0]; + dp[0][1] = dp[0][2] = dp[0][3] = 0; + for (let i = 1; i < length; i++) { + dp[i][0] = Math.max( + dp[i - 1][0], + Math.max(dp[i - 1][1], dp[i - 1][3]) - prices[i] + ); + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][3]); + dp[i][2] = dp[i - 1][0] + prices[i]; + dp[i][3] = dp[i - 1][2]; + } + const lastEl: number[] = dp[length - 1]; + return Math.max(lastEl[1], lastEl[2], lastEl[3]); +}; +``` + +> 版本二,状态定义略有不同,可以帮助理解 + +```typescript +function maxProfit(prices: number[]): number { + /** + dp[i][0]: 持股状态,当天买入; + dp[i][1]: 持股状态,当天未买入; + dp[i][2]: 无股状态,当天卖出; + dp[i][3]: 无股状态,当天未卖出; + + 买入有冷冻期限制,其实就是状态[0]只能由前一天的状态[3]得到; + 如果卖出有冷冻期限制,其实就是[2]由[1]得到。 + */ + const length: number = prices.length; + const dp: number[][] = new Array(length).fill(0).map(_ => []); + dp[0][0] = -prices[0]; + dp[0][1] = -Infinity; + dp[0][2] = dp[0][3] = 0; + for (let i = 1; i < length; i++) { + dp[i][0] = dp[i - 1][3] - prices[i]; + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0]); + dp[i][2] = Math.max(dp[i - 1][0], dp[i - 1][1]) + prices[i]; + dp[i][3] = Math.max(dp[i - 1][3], dp[i - 1][2]); + } + return Math.max(dp[length - 1][2], dp[length - 1][3]); +}; +``` + +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +/** + * 状态一:持有股票状态(今天买入股票, + * 或者是之前就买入了股票然后没有操作,一直持有) + * 不持有股票状态,这里就有两种卖出股票状态 + * 状态二:保持卖出股票的状态(两天前就卖出了股票,度过一天冷冻期。 + * 或者是前一天就是卖出股票状态,一直没操作) + * 状态三:今天卖出股票 + * 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天! + + */ +int maxProfit(int* prices, int pricesSize) { + if(pricesSize == 0){ + return 0; + } + int dp[pricesSize][4]; + memset(dp, 0, sizeof (int ) * pricesSize * 4); + dp[0][0] = -prices[0]; + for (int i = 1; i < pricesSize; ++i) { + dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][1] - prices[i], dp[i - 1][3] - prices[i])); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); + dp[i][2] = dp[i - 1][0] + prices[i]; + dp[i][3] = dp[i - 1][2]; + } + return max(dp[pricesSize - 1][1], max(dp[pricesSize - 1][2], dp[pricesSize - 1][3])); +} +``` + + + +### Rust: + +```rust +impl Solution { + pub fn max_profit(prices: Vec) -> i32 { + /* + * dp[i][0]: 持股状态; + * dp[i][1]: 无股状态,当天为非冷冻期; + * dp[i][2]: 无股状态,当天卖出; + * dp[i][3]: 无股状态,当天为冷冻期; + */ + let mut dp = vec![vec![0; 4]; prices.len()]; + dp[0][0] = -prices[0]; + for (i, &p) in prices.iter().enumerate().skip(1) { + dp[i][0] = dp[i - 1][0].max((dp[i - 1][3] - p).max(dp[i - 1][1] - p)); + dp[i][1] = dp[i - 1][1].max(dp[i - 1][3]); + dp[i][2] = dp[i - 1][0] + p; + dp[i][3] = dp[i - 1][2]; + } + *dp[prices.len() - 1].iter().skip(1).max().unwrap() + } +} +``` + + + diff --git "a/problems/0322.\351\233\266\351\222\261\345\205\221\346\215\242.md" "b/problems/0322.\351\233\266\351\222\261\345\205\221\346\215\242.md" new file mode 100755 index 0000000000..f3a0a07dd9 --- /dev/null +++ "b/problems/0322.\351\233\266\351\222\261\345\205\221\346\215\242.md" @@ -0,0 +1,499 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 322. 零钱兑换 + +[力扣题目链接](https://leetcode.cn/problems/coin-change/) + +给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。 + +你可以认为每种硬币的数量是无限的。 + +示例 1: +* 输入:coins = [1, 2, 5], amount = 11 +* 输出:3 +* 解释:11 = 5 + 5 + 1 + +示例 2: +* 输入:coins = [2], amount = 3 +* 输出:-1 + +示例 3: +* 输入:coins = [1], amount = 0 +* 输出:0 + +示例 4: +* 输入:coins = [1], amount = 1 +* 输出:1 + +示例 5: +* 输入:coins = [1], amount = 2 +* 输出:2 + +提示: + +* 1 <= coins.length <= 12 +* 1 <= coins[i] <= 2^31 - 1 +* 0 <= amount <= 10^4 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[装满背包最少的物品件数是多少?| LeetCode:322.零钱兑换](https://www.bilibili.com/video/BV14K411R7yv/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + + +## 思路 + +在[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)中我们已经兑换一次零钱了,这次又要兑换,套路不一样! + +题目中说每种硬币的数量是无限的,可以看出是典型的完全背包问题。 + +动规五部曲分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[j]:凑足总额为j所需钱币的最少个数为dp[j]** + +2. 确定递推公式 + +凑足总额为j - coins[i]的最少个数为dp[j - coins[i]],那么只需要加上一个钱币coins[i]即dp[j - coins[i]] + 1就是dp[j](考虑coins[i]) + +所以dp[j] 要取所有 dp[j - coins[i]] + 1 中最小的。 + +递推公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); + +3. dp数组如何初始化 + +首先凑足总金额为0所需钱币的个数一定是0,那么dp[0] = 0; + +其他下标对应的数值呢? + +考虑到递推公式的特性,dp[j]必须初始化为一个最大的数,否则就会在min(dp[j - coins[i]] + 1, dp[j])比较的过程中被初始值覆盖。 + +所以下标非0的元素都是应该是最大值。 + +代码如下: + +``` +vector dp(amount + 1, INT_MAX); +dp[0] = 0; +``` + +4. 确定遍历顺序 + +本题求钱币最小个数,**那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数**。 + +所以本题并不强调集合是组合还是排列。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +在动态规划专题我们讲过了求组合数是[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html),求排列数是[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)。 + +**所以本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!** + +那么我采用coins放在外循环,target在内循环的方式。 + +本题钱币数量可以无限使用,那么是完全背包。所以遍历的内循环是正序 + +综上所述,遍历顺序为:coins(物品)放在外循环,target(背包)在内循环。且内循环正序。 + +5. 举例推导dp数组 + +以输入:coins = [1, 2, 5], amount = 5为例 + +![322.零钱兑换](https://file1.kamacoder.com/i/algo/20210201111833906.jpg) + +dp[amount]为最终结果。 + +以上分析完毕,C++ 代码如下: + +```CPP +// 版本一 +class Solution { +public: + int coinChange(vector& coins, int amount) { + vector dp(amount + 1, INT_MAX); + dp[0] = 0; + for (int i = 0; i < coins.size(); i++) { // 遍历物品 + for (int j = coins[i]; j <= amount; j++) { // 遍历背包 + if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过 + dp[j] = min(dp[j - coins[i]] + 1, dp[j]); + } + } + } + if (dp[amount] == INT_MAX) return -1; + return dp[amount]; + } +}; +``` + +* 时间复杂度: O(n * amount),其中 n 为 coins 的长度 +* 空间复杂度: O(amount) + + + +对于遍历方式遍历背包放在外循环,遍历物品放在内循环也是可以的,我就直接给出代码了 + +```CPP +// 版本二 +class Solution { +public: + int coinChange(vector& coins, int amount) { + vector dp(amount + 1, INT_MAX); + dp[0] = 0; + for (int i = 1; i <= amount; i++) { // 遍历背包 + for (int j = 0; j < coins.size(); j++) { // 遍历物品 + if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) { + dp[i] = min(dp[i - coins[j]] + 1, dp[i]); + } + } + } + if (dp[amount] == INT_MAX) return -1; + return dp[amount]; + } +}; +``` +* 同上 + + +## 总结 + +细心的同学看网上的题解,**可能看一篇是遍历背包的for循环放外面,看一篇又是遍历背包的for循环放里面,看多了都看晕了**,到底两个for循环应该是什么先后关系。 + +能把遍历顺序讲明白的文章几乎找不到! + +这也是大多数同学学习动态规划的苦恼所在,有的时候递推公式很简单,难在遍历顺序上! + +但最终又可以稀里糊涂的把题目过了,也不知道为什么这样可以过,反正就是过了。 + +那么这篇文章就把遍历顺序分析的清清楚楚。 + +[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)中求的是组合数,[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)中求的是排列数。 + +**而本题是要求最少硬币数量,硬币是组合数还是排列数都无所谓!所以两个for循环先后顺序怎样都可以!** + +这也是我为什么要先讲518.零钱兑换II 然后再讲本题即:322.零钱兑换,这是Carl的良苦用心那。 + +相信大家看完之后,对背包问题中的遍历顺序有更深的理解了。 + + + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int coinChange(int[] coins, int amount) { + int max = Integer.MAX_VALUE; + int[] dp = new int[amount + 1]; + //初始化dp数组为最大值 + for (int j = 0; j < dp.length; j++) { + dp[j] = max; + } + //当金额为0时需要的硬币数目为0 + dp[0] = 0; + for (int i = 0; i < coins.length; i++) { + //正序遍历:完全背包每个硬币可以选择多次 + for (int j = coins[i]; j <= amount; j++) { + //只有dp[j-coins[i]]不是初始最大值时,该位才有选择的必要 + if (dp[j - coins[i]] != max) { + //选择硬币数目最小的情况 + dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1); + } + } + } + return dp[amount] == max ? -1 : dp[amount]; + } +} +``` + +### Python: + + +先遍历物品 后遍历背包 +```python +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + dp = [float('inf')] * (amount + 1) # 创建动态规划数组,初始值为正无穷大 + dp[0] = 0 # 初始化背包容量为0时的最小硬币数量为0 + + for coin in coins: # 遍历硬币列表,相当于遍历物品 + for i in range(coin, amount + 1): # 遍历背包容量 + if dp[i - coin] != float('inf'): # 如果dp[i - coin]不是初始值,则进行状态转移 + dp[i] = min(dp[i - coin] + 1, dp[i]) # 更新最小硬币数量 + + if dp[amount] == float('inf'): # 如果最终背包容量的最小硬币数量仍为正无穷大,表示无解 + return -1 + return dp[amount] # 返回背包容量为amount时的最小硬币数量 + +``` + +先遍历背包 后遍历物品 +```python +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + dp = [float('inf')] * (amount + 1) # 创建动态规划数组,初始值为正无穷大 + dp[0] = 0 # 初始化背包容量为0时的最小硬币数量为0 + + for i in range(1, amount + 1): # 遍历背包容量 + for j in range(len(coins)): # 遍历硬币列表,相当于遍历物品 + if i - coins[j] >= 0 and dp[i - coins[j]] != float('inf'): # 如果dp[i - coins[j]]不是初始值,则进行状态转移 + dp[i] = min(dp[i - coins[j]] + 1, dp[i]) # 更新最小硬币数量 + + if dp[amount] == float('inf'): # 如果最终背包容量的最小硬币数量仍为正无穷大,表示无解 + return -1 + return dp[amount] # 返回背包容量为amount时的最小硬币数量 + +``` +先遍历物品 后遍历背包(优化版) +```python +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + dp = [float('inf')] * (amount + 1) + dp[0] = 0 + + for coin in coins: + for i in range(coin, amount + 1): # 进行优化,从能装得下的背包开始计算,则不需要进行比较 + # 更新凑成金额 i 所需的最少硬币数量 + dp[i] = min(dp[i], dp[i - coin] + 1) + + return dp[amount] if dp[amount] != float('inf') else -1 + + +``` +先遍历背包 后遍历物品(优化版) +```python +class Solution: + def coinChange(self, coins: List[int], amount: int) -> int: + dp = [float('inf')] * (amount + 1) + dp[0] = 0 + + for i in range(1, amount + 1): # 遍历背包容量 + for coin in coins: # 遍历物品 + if i - coin >= 0: + # 更新凑成金额 i 所需的最少硬币数量 + dp[i] = min(dp[i], dp[i - coin] + 1) + + return dp[amount] if dp[amount] != float('inf') else -1 + + + +``` + +### Go: + +```go +// 版本一, 先遍历物品,再遍历背包 +func coinChange1(coins []int, amount int) int { + dp := make([]int, amount+1) + // 初始化dp[0] + dp[0] = 0 + // 初始化为math.MaxInt32 + for j := 1; j <= amount; j++ { + dp[j] = math.MaxInt32 + } + + // 遍历物品 + for i := 0; i < len(coins); i++ { + // 遍历背包 + for j := coins[i]; j <= amount; j++ { + if dp[j-coins[i]] != math.MaxInt32 { + // 推导公式 + dp[j] = min(dp[j], dp[j-coins[i]]+1) + //fmt.Println(dp,j,i) + } + } + } + // 没找到能装满背包的, 就返回-1 + if dp[amount] == math.MaxInt32 { + return -1 + } + return dp[amount] +} + +// 版本二,先遍历背包,再遍历物品 +func coinChange2(coins []int, amount int) int { + dp := make([]int, amount+1) + // 初始化dp[0] + dp[0] = 0 + // 遍历背包,从1开始 + for j := 1; j <= amount; j++ { + // 初始化为math.MaxInt32 + dp[j] = math.MaxInt32 + // 遍历物品 + for i := 0; i < len(coins); i++ { + if j >= coins[i] && dp[j-coins[i]] != math.MaxInt32 { + // 推导公式 + dp[j] = min(dp[j], dp[j-coins[i]]+1) + //fmt.Println(dp) + } + } + } + // 没找到能装满背包的, 就返回-1 + if dp[amount] == math.MaxInt32 { + return -1 + } + return dp[amount] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} + +``` + +## C + +```c +#define min(a, b) ((a) > (b) ? (b) : (a)) + +int coinChange(int* coins, int coinsSize, int amount) { + int* dp = (int*)malloc(sizeof(int) * (amount + 1)); + for (int j = 0; j < amount + 1; j++) { + dp[j] = INT_MAX; + } + dp[0] = 0; + // 遍历背包 + for(int i = 0; i <= amount; i++){ + // 遍历物品 + for(int j = 0; j < coinsSize; j++){ + if(i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX){ + dp[i] = min(dp[i], dp[i - coins[j]] + 1); + } + } + } + if(dp[amount] == INT_MAX){ + return -1; + } + return dp[amount]; +} +``` + + + +### Rust: + +```rust +// 遍历物品 +impl Solution { + pub fn coin_change(coins: Vec, amount: i32) -> i32 { + let amount = amount as usize; + let mut dp = vec![i32::MAX; amount + 1]; + dp[0] = 0; + for coin in coins { + for i in coin as usize..=amount { + if dp[i - coin as usize] != i32::MAX { + dp[i] = dp[i].min(dp[i - coin as usize] + 1); + } + } + } + if dp[amount] == i32::MAX { + return -1; + } + dp[amount] + } +} +``` + +```rust +// 遍历背包 +impl Solution { + pub fn coin_change(coins: Vec, amount: i32) -> i32 { + let amount = amount as usize; + let mut dp = vec![i32::MAX; amount + 1]; + dp[0] = 0; + for i in 1..=amount { + for &coin in &coins { + if i >= coin as usize && dp[i - coin as usize] != i32::MAX { + dp[i] = dp[i].min(dp[i - coin as usize] + 1) + } + } + } + if dp[amount] == i32::MAX { + return -1; + } + dp[amount] + } +} +``` + +### JavaScript: + +```javascript +// 遍历物品 +const coinChange = (coins, amount) => { + if(!amount) { + return 0; + } + + let dp = Array(amount + 1).fill(Infinity); + dp[0] = 0; + + for(let i = 0; i < coins.length; i++) { + for(let j = coins[i]; j <= amount; j++) { + dp[j] = Math.min(dp[j - coins[i]] + 1, dp[j]); + } + } + + return dp[amount] === Infinity ? -1 : dp[amount]; +} +``` + +```javascript +// 遍历背包 +var coinChange = function(coins, amount) { + const dp = Array(amount + 1).fill(Infinity) + dp[0] = 0 + for (let i = 1; i <= amount; i++) { + for (let j = 0; j < coins.length; j++) { + if (i >= coins[j] && dp[i - coins[j]] !== Infinity) { + dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1) + } + } + } + return dp[amount] === Infinity ? -1 : dp[amount] +} +``` + +### TypeScript: + +```typescript +// 遍历物品 +function coinChange(coins: number[], amount: number): number { + const dp: number[] = new Array(amount + 1).fill(Infinity); + dp[0] = 0; + for (let i = 0; i < coins.length; i++) { + for (let j = coins[i]; j <= amount; j++) { + if (dp[j - coins[i]] === Infinity) continue; + dp[j] = Math.min(dp[j], dp[j - coins[i]] + 1); + } + } + return dp[amount] === Infinity ? -1 : dp[amount]; +}; +``` + +```typescript +// 遍历背包 +function coinChange(coins: number[], amount: number): number { + const dp: number[] = Array(amount + 1).fill(Infinity) + dp[0] = 0 + for (let i = 1; i <= amount; i++) { + for (let j = 0; j < coins.length; j++) { + if (i >= coins[j] && dp[i - coins[j]] !== Infinity) { + dp[i] = Math.min(dp[i], dp[i - coins[j]] + 1) + } + } + } + return dp[amount] === Infinity ? -1 : dp[amount] +} +``` + + diff --git "a/problems/0332.\351\207\215\346\226\260\345\256\211\346\216\222\350\241\214\347\250\213.md" "b/problems/0332.\351\207\215\346\226\260\345\256\211\346\216\222\350\241\214\347\250\213.md" new file mode 100755 index 0000000000..1168277a8d --- /dev/null +++ "b/problems/0332.\351\207\215\346\226\260\345\256\211\346\216\222\350\241\214\347\250\213.md" @@ -0,0 +1,941 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 这也可以用回溯法? 其实深搜和回溯也是相辅相成的,毕竟都用递归。 + +# 332.重新安排行程 + +[力扣题目链接](https://leetcode.cn/problems/reconstruct-itinerary/) + +给定一个机票的字符串二维数组 [from, to],子数组中的两个成员分别表示飞机出发和降落的机场地点,对该行程进行重新规划排序。所有这些机票都属于一个从 JFK(肯尼迪国际机场)出发的先生,所以该行程必须从 JFK 开始。 + +提示: +* 如果存在多种有效的行程,请你按字符自然排序返回最小的行程组合。例如,行程 ["JFK", "LGA"] 与 ["JFK", "LGB"] 相比就更小,排序更靠前 +* 所有的机场都用三个大写字母表示(机场代码)。 +* 假定所有机票至少存在一种合理的行程。 +* 所有的机票必须都用一次 且 只能用一次。 + + +示例 1: +* 输入:[["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] +* 输出:["JFK", "MUC", "LHR", "SFO", "SJC"] + +示例 2: +* 输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] +* 输出:["JFK","ATL","JFK","SFO","ATL","SFO"] +* 解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透回溯算法(理论篇)](https://www.bilibili.com/video/BV1cy4y167mM/) ,相信结合视频再看本篇题解,更有助于大家对本题的理解。** + +## 思路 + + +这道题目还是很难的,之前我们用回溯法解决了如下问题:[组合问题](https://programmercarl.com/0077.组合.html),[分割问题](https://programmercarl.com/0093.复原IP地址.html),[子集问题](https://programmercarl.com/0078.子集.html),[排列问题](https://programmercarl.com/0046.全排列.html)。 + +直觉上来看 这道题和回溯法没有什么关系,更像是图论中的深度优先搜索。 + +实际上确实是深搜,但这是深搜中使用了回溯的例子,在查找路径的时候,如果不回溯,怎么能查到目标路径呢。 + +所以我倾向于说本题应该使用回溯法,那么我也用回溯法的思路来讲解本题,其实深搜一般都使用了回溯法的思路,在图论系列中我会再详细讲解深搜。 + +**这里就是先给大家拓展一下,原来回溯法还可以这么玩!** + +**这道题目有几个难点:** + +1. 一个行程中,如果航班处理不好容易变成一个圈,成为死循环 +2. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ? +3. 使用回溯法(也可以说深搜) 的话,那么终止条件是什么呢? +4. 搜索的过程中,如何遍历一个机场所对应的所有机场。 + +针对以上问题我来逐一解答! + +### 如何理解死循环 + +对于死循环,我来举一个有重复机场的例子: + +![332.重新安排行程](https://file1.kamacoder.com/i/algo/20201115180537865.png) + +为什么要举这个例子呢,就是告诉大家,出发机场和到达机场也会重复的,**如果在解题的过程中没有对集合元素处理好,就会死循环。** + +### 该记录映射关系 + +有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ? + +一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。 + +如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)。 + +这样存放映射关系可以定义为 `unordered_map> targets` 或者 `unordered_map> targets`。 + +含义如下: + +unordered_map> targets:unordered_map<出发机场, 到达机场的集合> targets + +unordered_map> targets:unordered_map<出发机场, map<到达机场, 航班次数>> targets + +这两个结构,我选择了后者,因为如果使用`unordered_map> targets` 遍历multiset的时候,不能删除元素,一旦删除元素,迭代器就失效了。 + +**再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。** + +所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用`unordered_map> targets`。 + +在遍历 `unordered_map<出发机场, map<到达机场, 航班次数>> targets`的过程中,**可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。** + + +如果“航班次数”大于零,说明目的地还可以飞,如果“航班次数”等于零说明目的地不能飞了,而不用对集合做删除元素或者增加元素的操作。 + +**相当于说我不删,我就做一个标记!** + +### 回溯法 + +这道题目我使用回溯法,那么下面按照我总结的回溯模板来: + +``` +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} +``` + +本题以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下: + +![332.重新安排行程1](https://file1.kamacoder.com/i/algo/2020111518065555-20230310121223600.png) + +开始回溯三部曲讲解: + +* 递归函数参数 + +在讲解映射关系的时候,已经讲过了,使用`unordered_map> targets;` 来记录航班的映射关系,我定义为全局变量。 + +当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。 + +参数里还需要ticketNum,表示有多少个航班(终止条件会用上)。 + +代码如下: + +```cpp +// unordered_map<出发机场, map<到达机场, 航班次数>> targets +unordered_map> targets; +bool backtracking(int ticketNum, vector& result) { +``` + +**注意函数返回值我用的是bool!** + +我们之前讲解回溯算法的时候,一般函数返回值都是void,这次为什么是bool呢? + +因为我们只需要找到一个行程,就是在树形结构中唯一的一条通向叶子节点的路线,如图: + +![332.重新安排行程1](https://file1.kamacoder.com/i/algo/2020111518065555-20230310121240991.png) + +所以找到了这个叶子节点了直接返回,这个递归函数的返回值问题我们在讲解二叉树的系列的时候,在这篇[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html)详细介绍过。 + +当然本题的targets和result都需要初始化,代码如下: + +```cpp +for (const vector& vec : tickets) { + targets[vec[0]][vec[1]]++; // 记录映射关系 +} +result.push_back("JFK"); // 起始机场 +``` + +* 递归终止条件 + +拿题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。 + +所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。 + +代码如下: + +```cpp +if (result.size() == ticketNum + 1) { + return true; +} +``` + +已经看习惯回溯法代码的同学,到叶子节点了习惯性的想要收集结果,但发现并不需要,本题的result相当于 [回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中的path,也就是本题的result就是记录路径的(就一条),在如下单层搜索的逻辑中result就添加元素了。 + +* 单层搜索的逻辑 + +回溯的过程中,如何遍历一个机场所对应的所有机场呢? + +这里刚刚说过,在选择映射函数的时候,不能选择`unordered_map> targets`, 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不再讨论了。 + +**可以说本题既要找到一个对数据进行排序的容器,而且还要容易增删元素,迭代器还不能失效**。 + +所以我选择了`unordered_map> targets` 来做机场之间的映射。 + +遍历过程如下: + +```CPP +for (pair& target : targets[result[result.size() - 1]]) { + if (target.second > 0 ) { // 记录到达机场是否飞过了 + result.push_back(target.first); + target.second--; + if (backtracking(ticketNum, result)) return true; + result.pop_back(); + target.second++; + } +} +``` + +可以看出 通过`unordered_map> targets`里的int字段来判断 这个集合里的机场是否使用过,这样避免了直接去删元素。 + +分析完毕,此时完整C++代码如下: + +```CPP +class Solution { +private: +// unordered_map<出发机场, map<到达机场, 航班次数>> targets +unordered_map> targets; +bool backtracking(int ticketNum, vector& result) { + if (result.size() == ticketNum + 1) { + return true; + } + for (pair& target : targets[result[result.size() - 1]]) { + if (target.second > 0 ) { // 记录到达机场是否飞过了 + result.push_back(target.first); + target.second--; + if (backtracking(ticketNum, result)) return true; + result.pop_back(); + target.second++; + } + } + return false; +} +public: + vector findItinerary(vector>& tickets) { + targets.clear(); + vector result; + for (const vector& vec : tickets) { + targets[vec[0]][vec[1]]++; // 记录映射关系 + } + result.push_back("JFK"); // 起始机场 + backtracking(tickets.size(), result); + return result; + } +}; +``` + +一波分析之后,可以看出我就是按照回溯算法的模板来的。 + +代码中 + +```cpp +for (pair& target : targets[result[result.size() - 1]]) +``` + +一定要加上引用即 `& target`,因为后面有对 target.second 做减减操作,如果没有引用,单纯复制,这个结果就没记录下来,那最后的结果就不对了。 + +加上引用之后,就必须在 string 前面加上 const,因为map中的key 是不可修改了,这就是语法规定了。 + + +## 总结 + +本题其实可以算是一道hard的题目了,关于本题的难点我在文中已经列出了。 + +**如果单纯的回溯搜索(深搜)并不难,难还难在容器的选择和使用上**。 + +本题其实是一道深度优先搜索的题目,但是我完全使用回溯法的思路来讲解这道题题目,**算是给大家拓展一下思维方式,其实深搜和回溯也是分不开的,毕竟最终都是用递归**。 + +如果最终代码,发现照着回溯法模板画的话好像也能画出来,但难就难如何知道可以使用回溯,以及如果套进去,所以我再写了这么长的一篇来详细讲解。 + + +## 其他语言版本 + +### Java + +```java +class Solution { + private LinkedList res; + private LinkedList path = new LinkedList<>(); + + public List findItinerary(List> tickets) { + Collections.sort(tickets, (a, b) -> a.get(1).compareTo(b.get(1))); + path.add("JFK"); + boolean[] used = new boolean[tickets.size()]; + backTracking((ArrayList) tickets, used); + return res; + } + + public boolean backTracking(ArrayList> tickets, boolean[] used) { + if (path.size() == tickets.size() + 1) { + res = new LinkedList(path); + return true; + } + + for (int i = 0; i < tickets.size(); i++) { + if (!used[i] && tickets.get(i).get(0).equals(path.getLast())) { + path.add(tickets.get(i).get(1)); + used[i] = true; + + if (backTracking(tickets, used)) { + return true; + } + + used[i] = false; + path.removeLast(); + } + } + return false; + } +} +``` + +```java +class Solution { + private Deque res; + private Map> map; + + private boolean backTracking(int ticketNum){ + if(res.size() == ticketNum + 1){ + return true; + } + String last = res.getLast(); + if(map.containsKey(last)){//防止出现null + for(Map.Entry target : map.get(last).entrySet()){ + int count = target.getValue(); + if(count > 0){ + res.add(target.getKey()); + target.setValue(count - 1); + if(backTracking(ticketNum)) return true; + res.removeLast(); + target.setValue(count); + } + } + } + return false; + } + + public List findItinerary(List> tickets) { + map = new HashMap>(); + res = new LinkedList<>(); + for(List t : tickets){ + Map temp; + if(map.containsKey(t.get(0))){ + temp = map.get(t.get(0)); + temp.put(t.get(1), temp.getOrDefault(t.get(1), 0) + 1); + }else{ + temp = new TreeMap<>();//升序Map + temp.put(t.get(1), 1); + } + map.put(t.get(0), temp); + + } + res.add("JFK"); + backTracking(tickets.size()); + return new ArrayList<>(res); + } +} +``` + +```java +/* 该方法是对第二个方法的改进,主要变化在于将某点的所有终点变更为链表的形式,优点在于 + 1.添加终点时直接在对应位置添加节点,避免了TreeMap增元素时的频繁调整 + 2.同时每次对终点进行增加删除查找时直接通过下标操作,避免hashMap反复计算hash*/ +class Solution { + //key为起点,value是有序的终点的列表 + Map> ticketMap = new HashMap<>(); + LinkedList result = new LinkedList<>(); + int total; + + public List findItinerary(List> tickets) { + total = tickets.size() + 1; + //遍历tickets,存入ticketMap中 + for (List ticket : tickets) { + addNew(ticket.get(0), ticket.get(1)); + } + deal("JFK"); + return result; + } + + boolean deal(String currentLocation) { + result.add(currentLocation); + //机票全部用完,找到最小字符路径 + if (result.size() == total) { + return true; + } + //当前位置的终点列表 + LinkedList targetLocations = ticketMap.get(currentLocation); + //没有从当前位置出发的机票了,说明这条路走不通 + if (targetLocations != null && !targetLocations.isEmpty()) { + //终点列表中遍历到的终点 + String targetLocation; + //遍历从当前位置出发的机票 + for (int i = 0; i < targetLocations.size(); i++) { + //去重,否则在最后一个测试用例中遇到循环时会无限递归 + if(i > 0 && targetLocations.get(i).equals(targetLocations.get(i - 1))) continue; + targetLocation = targetLocations.get(i); + //删除终点列表中当前的终点 + targetLocations.remove(i); + //递归 + if (deal(targetLocation)) { + return true; + } + //路线走不通,将机票重新加回去 + targetLocations.add(i, targetLocation); + result.removeLast(); + } + } + return false; + } + + /** + * 在map中按照字典顺序添加新元素 + * + * @param start 起点 + * @param end 终点 + */ + void addNew(String start, String end) { + LinkedList startAllEnd = ticketMap.getOrDefault(start, new LinkedList<>()); + if (!startAllEnd.isEmpty()) { + for (int i = 0; i < startAllEnd.size(); i++) { + if (end.compareTo(startAllEnd.get(i)) < 0) { + startAllEnd.add(i, end); + return; + } + } + startAllEnd.add(startAllEnd.size(), end); + } else { + startAllEnd.add(end); + ticketMap.put(start, startAllEnd); + } + } +} +``` + +### Python +回溯 使用字典 +```python +class Solution: + def findItinerary(self, tickets: List[List[str]]) -> List[str]: + self.adj = {} + + # sort by the destination alphabetically + # 根据航班每一站的重点字母顺序排序 + tickets.sort(key=lambda x:x[1]) + + # get all possible connection for each destination + # 罗列每一站的下一个可选项 + for u,v in tickets: + if u in self.adj: self.adj[u].append(v) + else: self.adj[u] = [v] + + # 从JFK出发 + self.result = [] + self.dfs("JFK") # start with JFK + + return self.result[::-1] # reverse to get the result + + def dfs(self, s): + # if depart city has flight and the flight can go to another city + while s in self.adj and len(self.adj[s]) > 0: + # 找到s能到哪里,选能到的第一个机场 + v = self.adj[s][0] # we go to the 1 choice of the city + # 在之后的可选项机场中去掉这个机场 + self.adj[s].pop(0) # get rid of this choice since we used it + # 从当前的新出发点开始 + self.dfs(v) # we start from the new airport + + self.result.append(s) # after append, it will back track to last node, thus the result list is in reversed order + +``` +回溯 使用字典 逆序 +```python +from collections import defaultdict + +class Solution: + def findItinerary(self, tickets): + targets = defaultdict(list) # 创建默认字典,用于存储机场映射关系 + for ticket in tickets: + targets[ticket[0]].append(ticket[1]) # 将机票输入到字典中 + + for key in targets: + targets[key].sort(reverse=True) # 对到达机场列表进行字母逆序排序 + + result = [] + self.backtracking("JFK", targets, result) # 调用回溯函数开始搜索路径 + return result[::-1] # 返回逆序的行程路径 + + def backtracking(self, airport, targets, result): + while targets[airport]: # 当机场还有可到达的机场时 + next_airport = targets[airport].pop() # 弹出下一个机场 + self.backtracking(next_airport, targets, result) # 递归调用回溯函数进行深度优先搜索 + result.append(airport) # 将当前机场添加到行程路径中 +``` + +### Go +```go +type pair struct { + target string + visited bool +} +type pairs []*pair + +func (p pairs) Len() int { + return len(p) +} +func (p pairs) Swap(i, j int) { + p[i], p[j] = p[j], p[i] +} +func (p pairs) Less(i, j int) bool { + return p[i].target < p[j].target +} + +func findItinerary(tickets [][]string) []string { + result := []string{} + // map[出发机场] pair{目的地,是否被访问过} + targets := make(map[string]pairs) + for _, ticket := range tickets { + if targets[ticket[0]] == nil { + targets[ticket[0]] = make(pairs, 0) + } + targets[ticket[0]] = append(targets[ticket[0]], &pair{target: ticket[1], visited: false}) + } + for k, _ := range targets { + sort.Sort(targets[k]) + } + result = append(result, "JFK") + var backtracking func() bool + backtracking = func() bool { + if len(tickets)+1 == len(result) { + return true + } + // 取出起飞航班对应的目的地 + for _, pair := range targets[result[len(result)-1]] { + if pair.visited == false { + result = append(result, pair.target) + pair.visited = true + if backtracking() { + return true + } + result = result[:len(result)-1] + pair.visited = false + } + } + return false + } + + backtracking() + + return result +} +``` + +### JavaScript + +```Javascript + +var findItinerary = function(tickets) { + let result = ['JFK'] + let map = {} + + for (const tickt of tickets) { + const [from, to] = tickt + if (!map[from]) { + map[from] = [] + } + map[from].push(to) + } + + for (const city in map) { + // 对到达城市列表排序 + map[city].sort() + } + function backtracing() { + if (result.length === tickets.length + 1) { + return true + } + if (!map[result[result.length - 1]] || !map[result[result.length - 1]].length) { + return false + } + for(let i = 0 ; i < map[result[result.length - 1]].length; i++) { + let city = map[result[result.length - 1]][i] + // 删除已走过航线,防止死循环 + map[result[result.length - 1]].splice(i, 1) + result.push(city) + if (backtracing()) { + return true + } + result.pop() + map[result[result.length - 1]].splice(i, 0, city) + } + } + backtracing() + return result +}; + +``` + +**javascript版本二 处理对象key无序问题** + +```javascript +/** + * @param {string[][]} tickets + * @return {string[]} + */ +var findItinerary = function (tickets) { + const ans = ["JFK"]; + let map = {}; + // 整理每个站点的终点站信息 + tickets.forEach((t) => { + let targets = map[t[0]]; + if (!targets) { + targets = { [t[1]]: 0 }; + map[t[0]] = targets; + } + targets[t[1]] = (targets[t[1]] || 0) + 1; + }); + // 按照key字典序排序对象 + const sortObject = (obj) => { + const newObj = {}; + const keys = Object.keys(obj); + keys.sort((k1, k2) => (k1 < k2 ? -1 : 1)); + keys.forEach((key) => { + if (obj[key] !== null && typeof obj[key] === "object") { + newObj[key] = sortObject(obj[key]); + } else { + newObj[key] = obj[key]; + } + }); + return newObj; + }; + const backtrack = (tickets, targets) => { + if (ans.length === tickets.length + 1) { + return true; + } + const target = targets[ans[ans.length - 1]]; + // 没有下一站 + if (!target) { + return false; + } + // 或者在这里排序 + // const keyList = Object.keys(target).sort((k1, k2) => (k1 < k2 ? -1 : 1)); + const keyList = Object.keys(target); + for (const key of keyList) { + // 判断当前站是否还能飞 + if (target[key] > 0) { + target[key]--; + ans.push(key); + // 对象key有序 此时的行程就是字典序最小的 直接跳出 + if (backtrack(tickets, targets)) { + return true; + } + target[key]++; + ans.pop(); + } + } + return false; + }; + map = sortObject(map); + backtrack(tickets, map); + return ans; +}; +``` + + + +### TypeScript + +```typescript +function findItinerary(tickets: string[][]): string[] { + /** + TicketsMap 实例: + { NRT: Map(1) { 'JFK' => 1 }, JFK: Map(2) { 'KUL' => 1, 'NRT' => 1 } } + 这里选择Map数据结构的原因是:与Object类型的一个主要差异是,Map实例会维护键值对的插入顺序。 + */ + type TicketsMap = { + [index: string]: Map + }; + tickets.sort((a, b) => { + return a[1] < b[1] ? -1 : 1; + }); + const ticketMap: TicketsMap = {}; + for (const [from, to] of tickets) { + if (ticketMap[from] === undefined) { + ticketMap[from] = new Map(); + } + ticketMap[from].set(to, (ticketMap[from].get(to) || 0) + 1); + } + const resRoute = ['JFK']; + backTracking(tickets.length, ticketMap, resRoute); + return resRoute; + function backTracking(ticketNum: number, ticketMap: TicketsMap, route: string[]): boolean { + if (route.length === ticketNum + 1) return true; + const targetMap = ticketMap[route[route.length - 1]]; + if (targetMap !== undefined) { + for (const [to, count] of targetMap.entries()) { + if (count > 0) { + route.push(to); + targetMap.set(to, count - 1); + if (backTracking(ticketNum, ticketMap, route) === true) return true; + targetMap.set(to, count); + route.pop(); + } + } + } + return false; + } +}; +``` + +### C + +```C +typedef struct { + char *name; /* key */ + int cnt; /* 记录到达机场是否飞过了 */ + UT_hash_handle hh; /* makes this structure hashable */ +} to_airport_t; + +typedef struct { + char *name; /* key */ + to_airport_t *to_airports; + UT_hash_handle hh; /* makes this structure hashable */ +} from_airport_t; + +void to_airport_destroy(to_airport_t *airports) { + to_airport_t *airport, *tmp; + HASH_ITER(hh, airports, airport, tmp) { + HASH_DEL(airports, airport); + free(airport); + } +} + +void from_airport_destroy(from_airport_t *airports) { + from_airport_t *airport, *tmp; + HASH_ITER(hh, airports, airport, tmp) { + to_airport_destroy(airport->to_airports); + HASH_DEL(airports, airport); + free(airport); + } +} + +int name_sort(to_airport_t *a, to_airport_t *b) { + return strcmp(a->name, b->name); +} + +bool backtracking(from_airport_t *airports, int target_path_len, char **path, + int path_len) { + if (path_len == target_path_len) return true; + + from_airport_t *from_airport = NULL; + HASH_FIND_STR(airports, path[path_len - 1], from_airport); + if (!from_airport) return false; + + for (to_airport_t *to_airport = from_airport->to_airports; + to_airport != NULL; to_airport = to_airport->hh.next) { + if (to_airport->cnt == 0) continue; + to_airport->cnt--; + path[path_len] = to_airport->name; + if (backtracking(airports, target_path_len, path, path_len + 1)) + return true; + to_airport->cnt++; + } + return false; +} + +char **findItinerary(char ***tickets, int ticketsSize, int *ticketsColSize, + int *returnSize) { + from_airport_t *airports = NULL; + + // 记录映射关系 + for (int i = 0; i < ticketsSize; i++) { + from_airport_t *from_airport = NULL; + to_airport_t *to_airport = NULL; + HASH_FIND_STR(airports, tickets[i][0], from_airport); + if (!from_airport) { + from_airport = malloc(sizeof(from_airport_t)); + from_airport->name = tickets[i][0]; + from_airport->to_airports = NULL; + HASH_ADD_KEYPTR(hh, airports, from_airport->name, + strlen(from_airport->name), from_airport); + } + HASH_FIND_STR(from_airport->to_airports, tickets[i][1], to_airport); + if (!to_airport) { + to_airport = malloc(sizeof(to_airport_t)); + to_airport->name = tickets[i][1]; + to_airport->cnt = 0; + HASH_ADD_KEYPTR(hh, from_airport->to_airports, to_airport->name, + strlen(to_airport->name), to_airport); + } + to_airport->cnt++; + } + + // 机场排序 + for (from_airport *from_airport = airports; from_airport != NULL; + from_airport = from_airport->hh.next) { + HASH_SRT(hh, from_airport->to_airports, name_sort); + } + + char **path = malloc(sizeof(char *) * (ticketsSize + 1)); + path[0] = "JFK"; // 起始机场 + backtracking(airports, ticketsSize + 1, path, 1); + + from_airport_destroy(airports); + + *returnSize = ticketsSize + 1; + return path; +} +``` + +### Swift + +直接迭代tickets数组: + +```swift +func findItinerary(_ tickets: [[String]]) -> [String] { + // 先对路线进行排序 + let tickets = tickets.sorted { (arr1, arr2) -> Bool in + if arr1[0] < arr2[0] { + return true + } else if arr1[0] > arr2[0] { + return false + } + if arr1[1] < arr2[1] { + return true + } else if arr1[1] > arr2[1] { + return false + } + return true + } + var path = ["JFK"] + var used = [Bool](repeating: false, count: tickets.count) + + @discardableResult + func backtracking() -> Bool { + // 结束条件:满足一条路径的数量 + if path.count == tickets.count + 1 { return true } + + for i in 0 ..< tickets.count { + // 巧妙之处!跳过处理过或出发站不是path末尾站的线路,即筛选出未处理的又可以衔接path的线路 + guard !used[i], tickets[i][0] == path.last! else { continue } + // 处理 + used[i] = true + path.append(tickets[i][1]) + // 递归 + if backtracking() { return true } + // 回溯 + path.removeLast() + used[i] = false + } + return false + } + backtracking() + return path +} +``` + +使用字典优化迭代遍历: + +```swift +func findItinerary(_ tickets: [[String]]) -> [String] { + // 建立出发站和目的站的一对多关系,要对目的地进行排序 + typealias Destination = (name: String, used: Bool) + var targets = [String: [Destination]]() + for line in tickets { + let src = line[0], des = line[1] + var value = targets[src] ?? [] + value.append((des, false)) + targets[src] = value + } + for (k, v) in targets { + targets[k] = v.sorted { $0.name < $1.name } + } + + var path = ["JFK"] + let pathCount = tickets.count + 1 + @discardableResult + func backtracking() -> Bool { + if path.count == pathCount { return true } + + let startPoint = path.last! + guard let end = targets[startPoint]?.count, end > 0 else { return false } + for i in 0 ..< end { + // 排除处理过的线路 + guard !targets[startPoint]![i].used else { continue } + // 处理 + targets[startPoint]![i].used = true + path.append(targets[startPoint]![i].name) + // 递归 + if backtracking() { return true } + // 回溯 + path.removeLast() + targets[startPoint]![i].used = false + } + return false + } + backtracking() + return path +} +``` + +使用插入时排序优化targets字典的构造: + +```swift +// 建立出发站和目的站的一对多关系,在构建的时候进行插入排序 +typealias Destination = (name: String, used: Bool) +var targets = [String: [Destination]]() +func sortedInsert(_ element: Destination, to array: inout [Destination]) { + var left = 0, right = array.count - 1 + while left <= right { + let mid = left + (right - left) / 2 + if array[mid].name < element.name { + left = mid + 1 + } else if array[mid].name > element.name { + right = mid - 1 + } else { + left = mid + break + } + } + array.insert(element, at: left) +} +for line in tickets { + let src = line[0], des = line[1] + var value = targets[src] ?? [] + sortedInsert((des, false), to: &value) + targets[src] = value +} +``` + +### Rust +** 文中的Hashmap嵌套Hashmap的方法因为Rust的所有权问题暂时无法实现,此方法为删除哈希表中元素法 ** +```Rust +use std::collections::HashMap; +impl Solution { + fn backtracking(airport: String, targets: &mut HashMap<&String, Vec<&String>>, result: &mut Vec) { + while let Some(next_airport) = targets.get_mut(&airport).unwrap_or(&mut vec![]).pop() { + Self::backtracking(next_airport.clone(), targets, result); + } + result.push(airport.clone()); + } + + pub fn find_itinerary(tickets: Vec>) -> Vec { + let mut targets: HashMap<&String, Vec<&String>> = HashMap::new(); + let mut result = Vec::new(); + for t in 0..tickets.len() { + targets.entry(&tickets[t][0]).or_default().push(&tickets[t][1]); + } + for (_, target) in targets.iter_mut() { + target.sort_by(|a, b| b.cmp(a)); + } + Self::backtracking("JFK".to_string(), &mut targets, &mut result); + result.reverse(); + result + } +} +``` + + diff --git "a/problems/0337.\346\211\223\345\256\266\345\212\253\350\210\215III.md" "b/problems/0337.\346\211\223\345\256\266\345\212\253\350\210\215III.md" new file mode 100755 index 0000000000..44af86bb4c --- /dev/null +++ "b/problems/0337.\346\211\223\345\256\266\345\212\253\350\210\215III.md" @@ -0,0 +1,623 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 337.打家劫舍 III + +[力扣题目链接](https://leetcode.cn/problems/house-robber-iii/) + +在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。 + +计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。 + + +![337.打家劫舍III](https://file1.kamacoder.com/i/algo/20210223173849619.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,房间连成树了,偷不偷呢?| LeetCode:337.打家劫舍3](https://www.bilibili.com/video/BV1H24y1Q7sY),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目和 [198.打家劫舍](https://programmercarl.com/0198.打家劫舍.html),[213.打家劫舍II](https://programmercarl.com/0213.打家劫舍II.html)也是如出一辙,只不过这个换成了树。 + +如果对树的遍历不够熟悉的话,那本题就有难度了。 + +对于树的话,首先就要想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。 + +**本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算**。 + +与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。 + +如果抢了当前节点,两个孩子就不能动,如果没抢当前节点,就可以考虑抢左右孩子(**注意这里说的是“考虑”**) + +### 暴力递归 + +代码如下: + +```CPP +class Solution { +public: + int rob(TreeNode* root) { + if (root == NULL) return 0; + if (root->left == NULL && root->right == NULL) return root->val; + // 偷父节点 + int val1 = root->val; + if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了 + if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了 + // 不偷父节点 + int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子 + return max(val1, val2); + } +}; +``` + +* 时间复杂度:O(n^2),这个时间复杂度不太标准,也不容易准确化,例如越往下的节点重复计算次数就越多 +* 空间复杂度:O(log n),算上递推系统栈的空间 + +当然以上代码超时了,这个递归的过程中其实是有重复计算了。 + +我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。 + +### 记忆化递推 + +所以可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。 + +代码如下: + +```CPP +class Solution { +public: + unordered_map umap; // 记录计算过的结果 + int rob(TreeNode* root) { + if (root == NULL) return 0; + if (root->left == NULL && root->right == NULL) return root->val; + if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回 + // 偷父节点 + int val1 = root->val; + if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left + if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right + // 不偷父节点 + int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子 + umap[root] = max(val1, val2); // umap记录一下结果 + return max(val1, val2); + } +}; + +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(log n),算上递推系统栈的空间 + + +### 动态规划 + +在上面两种方法,其实对一个节点 偷与不偷得到的最大金钱都没有做记录,而是需要实时计算。 + +而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。 + +**这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解**。 + +1. 确定递归函数的参数和返回值 + +这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。 + +参数为当前节点,代码如下: + +```CPP +vector robTree(TreeNode* cur) { +``` + +其实这里的返回数组就是dp数组。 + +所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。 + +**所以本题dp数组就是一个长度为2的数组!** + +那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢? + +**别忘了在递归的过程中,系统栈会保存每一层递归的参数**。 + +如果还不理解的话,就接着往下看,看到代码就理解了哈。 + +2. 确定终止条件 + +在遍历的过程中,如果遇到空节点的话,很明显,无论偷还是不偷都是0,所以就返回 +``` +if (cur == NULL) return vector{0, 0}; +``` +这也相当于dp数组的初始化 + + +3. 确定遍历顺序 + +首先明确的是使用后序遍历。 因为要通过递归函数的返回值来做下一步计算。 + +通过递归左节点,得到左节点偷与不偷的金钱。 + +通过递归右节点,得到右节点偷与不偷的金钱。 + +代码如下: + +```CPP +// 下标0:不偷,下标1:偷 +vector left = robTree(cur->left); // 左 +vector right = robTree(cur->right); // 右 +// 中 + +``` + +4. 确定单层递归的逻辑 + +如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; (**如果对下标含义不理解就再回顾一下dp数组的含义**) + +如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]); + +最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱} + +代码如下: + +```CPP +vector left = robTree(cur->left); // 左 +vector right = robTree(cur->right); // 右 + +// 偷cur +int val1 = cur->val + left[0] + right[0]; +// 不偷cur +int val2 = max(left[0], left[1]) + max(right[0], right[1]); +return {val2, val1}; +``` + + + +5. 举例推导dp数组 + +以示例1为例,dp数组状态如下:(**注意用后序遍历的方式推导**) + + +![](https://file1.kamacoder.com/i/algo/20230203110031.png) + +**最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱**。 + +递归三部曲与动规五部曲分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int rob(TreeNode* root) { + vector result = robTree(root); + return max(result[0], result[1]); + } + // 长度为2的数组,0:不偷,1:偷 + vector robTree(TreeNode* cur) { + if (cur == NULL) return vector{0, 0}; + vector left = robTree(cur->left); + vector right = robTree(cur->right); + // 偷cur,那么就不能偷左右节点。 + int val1 = cur->val + left[0] + right[0]; + // 不偷cur,那么可以偷也可以不偷左右节点,则取较大的情况 + int val2 = max(left[0], left[1]) + max(right[0], right[1]); + return {val2, val1}; + } +}; +``` +* 时间复杂度:O(n),每个节点只遍历了一次 +* 空间复杂度:O(log n),算上递推系统栈的空间 + +## 总结 + +这道题是树形DP的入门题目,通过这道题目大家应该也了解了,所谓树形DP就是在树上进行递归公式的推导。 + +**所以树形DP也没有那么神秘!** + +只不过平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解! + +大家还记不记得我在讲解贪心专题的时候,讲到这道题目:[贪心算法:我要监控二叉树!](https://programmercarl.com/0968.监控二叉树.html),这也是贪心算法在树上的应用。**那我也可以把这个算法起一个名字,叫做树形贪心**。 + +“树形贪心”词汇从此诞生,来自「代码随想录」 + +## 其他语言版本 + + +### Java +```Java +class Solution { + // 1.递归去偷,超时 + public int rob(TreeNode root) { + if (root == null) + return 0; + int money = root.val; + if (root.left != null) { + money += rob(root.left.left) + rob(root.left.right); + } + if (root.right != null) { + money += rob(root.right.left) + rob(root.right.right); + } + return Math.max(money, rob(root.left) + rob(root.right)); + } + + // 2.递归去偷,记录状态 + // 执行用时:3 ms , 在所有 Java 提交中击败了 56.24% 的用户 + public int rob1(TreeNode root) { + Map memo = new HashMap<>(); + return robAction(root, memo); + } + + int robAction(TreeNode root, Map memo) { + if (root == null) + return 0; + if (memo.containsKey(root)) + return memo.get(root); + int money = root.val; + if (root.left != null) { + money += robAction(root.left.left, memo) + robAction(root.left.right, memo); + } + if (root.right != null) { + money += robAction(root.right.left, memo) + robAction(root.right.right, memo); + } + int res = Math.max(money, robAction(root.left, memo) + robAction(root.right, memo)); + memo.put(root, res); + return res; + } + + // 3.状态标记递归 + // 执行用时:0 ms , 在所有 Java 提交中击败了 100% 的用户 + // 不偷:Max(左孩子不偷,左孩子偷) + Max(右孩子不偷,右孩子偷) + // root[0] = Math.max(rob(root.left)[0], rob(root.left)[1]) + + // Math.max(rob(root.right)[0], rob(root.right)[1]) + // 偷:左孩子不偷+ 右孩子不偷 + 当前节点偷 + // root[1] = rob(root.left)[0] + rob(root.right)[0] + root.val; + public int rob3(TreeNode root) { + int[] res = robAction1(root); + return Math.max(res[0], res[1]); + } + + int[] robAction1(TreeNode root) { + int res[] = new int[2]; + if (root == null) + return res; + + int[] left = robAction1(root.left); + int[] right = robAction1(root.right); + + res[0] = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); + res[1] = root.val + left[0] + right[0]; + return res; + } +} +``` + +### Python + +> 暴力递归 + +```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 rob(self, root: TreeNode) -> int: + if root is None: + return 0 + if root.left is None and root.right is None: + return root.val + # 偷父节点 + val1 = root.val + if root.left: + val1 += self.rob(root.left.left) + self.rob(root.left.right) + if root.right: + val1 += self.rob(root.right.left) + self.rob(root.right.right) + # 不偷父节点 + val2 = self.rob(root.left) + self.rob(root.right) + return max(val1, val2) +``` + +> 记忆化递归 + +```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: + memory = {} + def rob(self, root: TreeNode) -> int: + if root is None: + return 0 + if root.left is None and root.right is None: + return root.val + if self.memory.get(root) is not None: + return self.memory[root] + # 偷父节点 + val1 = root.val + if root.left: + val1 += self.rob(root.left.left) + self.rob(root.left.right) + if root.right: + val1 += self.rob(root.right.left) + self.rob(root.right.right) + # 不偷父节点 + val2 = self.rob(root.left) + self.rob(root.right) + self.memory[root] = max(val1, val2) + return max(val1, val2) +``` + +> 动态规划 +```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 rob(self, root: Optional[TreeNode]) -> int: + # dp数组(dp table)以及下标的含义: + # 1. 下标为 0 记录 **不偷该节点** 所得到的的最大金钱 + # 2. 下标为 1 记录 **偷该节点** 所得到的的最大金钱 + dp = self.traversal(root) + return max(dp) + + # 要用后序遍历, 因为要通过递归函数的返回值来做下一步计算 + def traversal(self, node): + + # 递归终止条件,就是遇到了空节点,那肯定是不偷的 + if not node: + return (0, 0) + + left = self.traversal(node.left) + right = self.traversal(node.right) + + # 不偷当前节点, 偷子节点 + val_0 = max(left[0], left[1]) + max(right[0], right[1]) + + # 偷当前节点, 不偷子节点 + val_1 = node.val + left[0] + right[0] + + return (val_0, val_1) +``` + +### Go + +暴力递归 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func rob(root *TreeNode) int { + if root == nil { + return 0 + } + if root.Left == nil && root.Right == nil { + return root.Val + } + // 偷父节点 + val1 := root.Val + if root.Left != nil { + val1 += rob(root.Left.Left) + rob(root.Left.Right) // 跳过root->left,相当于不考虑左孩子了 + } + if root.Right != nil { + val1 += rob(root.Right.Left) + rob(root.Right.Right) // 跳过root->right,相当于不考虑右孩子了 + } + // 不偷父节点 + val2 := rob(root.Left) + rob(root.Right) // 考虑root的左右孩子 + return max(val1, val2) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +记忆化递推 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +var umap = make(map[*TreeNode]int) + +func rob(root *TreeNode) int { + if root == nil { + return 0 + } + if root.Left == nil && root.Right == nil { + return root.Val + } + if val, ok := umap[root]; ok { + return val // 如果umap里已经有记录则直接返回 + } + // 偷父节点 + val1 := root.Val + if root.Left != nil { + val1 += rob(root.Left.Left) + rob(root.Left.Right) // 跳过root->left,相当于不考虑左孩子了 + } + if root.Right != nil { + val1 += rob(root.Right.Left) + rob(root.Right.Right) // 跳过root->right,相当于不考虑右孩子了 + } + // 不偷父节点 + val2 := rob(root.Left) + rob(root.Right) // 考虑root的左右孩子 + umap[root] = max(val1, val2) // umap记录一下结果 + return max(val1, val2) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +动态规划 + +```go +func rob(root *TreeNode) int { + res := robTree(root) + return slices.Max(res) +} + +func robTree(cur *TreeNode) []int { + if cur == nil { + return []int{0, 0} + } + // 后序遍历 + left := robTree(cur.Left) + right := robTree(cur.Right) + + // 考虑去偷当前的屋子 + robCur := cur.Val + left[0] + right[0] + // 考虑不去偷当前的屋子 + notRobCur := slices.Max(left) + slices.Max(right) + + // 注意顺序:0:不偷,1:去偷 + return []int{notRobCur, robCur} +} +``` + +### JavaScript + +> 动态规划 + +```javascript +const rob = root => { + // 后序遍历函数 + const postOrder = node => { + // 递归出口 + if (!node) return [0, 0]; + // 遍历左子树 + const left = postOrder(node.left); + // 遍历右子树 + const right = postOrder(node.right); + // 不偷当前节点,左右子节点都可以偷或不偷,取最大值 + const DoNot = Math.max(left[0], left[1]) + Math.max(right[0], right[1]); + // 偷当前节点,左右子节点只能不偷 + const Do = node.val + left[0] + right[0]; + // [不偷,偷] + return [DoNot, Do]; + }; + const res = postOrder(root); + // 返回最大值 + return Math.max(...res); +}; +``` + +### TypeScript + +> 记忆化后序遍历 + +```typescript +const memory: Map = new Map(); +function rob(root: TreeNode | null): number { + if (root === null) return 0; + if (memory.has(root)) return memory.get(root); + // 不取当前节点 + const res1: number = rob(root.left) + rob(root.right); + // 取当前节点 + let res2: number = root.val; + if (root.left !== null) res2 += rob(root.left.left) + rob(root.left.right); + if (root.right !== null) res2 += rob(root.right.left) + rob(root.right.right); + const res: number = Math.max(res1, res2); + memory.set(root, res); + return res; +}; +``` + +> 状态标记化后序遍历 + +```typescript +function rob(root: TreeNode | null): number { + return Math.max(...robNode(root)); +}; +// [0]-不偷当前节点能获得的最大金额; [1]-偷~~ +type MaxValueArr = [number, number]; +function robNode(node: TreeNode | null): MaxValueArr { + if (node === null) return [0, 0]; + const leftArr: MaxValueArr = robNode(node.left); + const rightArr: MaxValueArr = robNode(node.right); + // 不偷 + const val1: number = Math.max(leftArr[0], leftArr[1]) + + Math.max(rightArr[0], rightArr[1]); + // 偷 + const val2: number = leftArr[0] + rightArr[0] + node.val; + return [val1, val2]; +} +``` + +### C + +```c +int *robTree(struct TreeNode *node) { + int* amounts = (int*) malloc(sizeof(int) * 2); + memset(amounts, 0, sizeof(int) * 2); + if(node == NULL){ + return amounts; + } + int * left = robTree(node->left); + int * right = robTree(node->right); + // 偷当前节点 + amounts[1] = node->val + left[0] + right[0]; + // 不偷当前节点 + amounts[0] = max(left[0], left[1]) + max(right[0], right[1]); + return amounts; +} + +int rob(struct TreeNode* root) { + int * dp = robTree(root); + // 0代表不偷当前节点可以获得的最大值,1表示偷当前节点可以获取的最大值 + return max(dp[0], dp[1]); +} +``` + + + +### Rust + +动态规划: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn rob(root: Option>>) -> i32 { + let (v1, v2) = Self::rob_tree(&root); + v1.max(v2) + } + pub fn rob_tree(cur: &Option>>) -> (i32, i32) { + match cur { + None => (0, 0), + Some(node) => { + let left = Self::rob_tree(&node.borrow_mut().left); + let right = Self::rob_tree(&node.borrow_mut().right); + ( + left.0.max(left.1) + right.0.max(right.1), // 偷左右节点 + node.borrow().val + left.0 + right.0, // 偷父节点 + ) + } + } + } +} +``` + + diff --git "a/problems/0343.\346\225\264\346\225\260\346\213\206\345\210\206.md" "b/problems/0343.\346\225\264\346\225\260\346\213\206\345\210\206.md" new file mode 100755 index 0000000000..c9467e361f --- /dev/null +++ "b/problems/0343.\346\225\264\346\225\260\346\213\206\345\210\206.md" @@ -0,0 +1,564 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 343. 整数拆分 + +[力扣题目链接](https://leetcode.cn/problems/integer-break/) + +给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 + +示例 1: +* 输入: 2 +* 输出: 1 +* 解释: 2 = 1 + 1, 1 × 1 = 1。 + +示例 2: +* 输入: 10 +* 输出: 36 +* 解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 +* 说明: 你可以假设 n 不小于 2 且不大于 58。 + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,本题关键在于理解递推公式!| LeetCode:343. 整数拆分](https://www.bilibili.com/video/BV1Mg411q7YJ/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +看到这道题目,都会想拆成两个呢,还是三个呢,还是四个.... + +我们来看一下如何使用动规来解决。 + +### 动态规划 + +动规五部曲,分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i]:分拆数字i,可以得到的最大乘积为dp[i]。 + +dp[i]的定义将贯彻整个解题过程,下面哪一步想不懂了,就想想dp[i]究竟表示的是啥! + +2. 确定递推公式 + +可以想 dp[i]最大乘积是怎么得到的呢? + +其实可以从1遍历j,然后有两种渠道得到dp[i]. + +一个是j * (i - j) 直接相乘。 + +一个是j * dp[i - j],相当于是拆分(i - j),对这个拆分不理解的话,可以回想dp数组的定义。 + +**那有同学问了,j怎么就不拆分呢?** + +j是从1开始遍历,拆分j的情况,在遍历j的过程中其实都计算过了。那么从1遍历j,比较(i - j) * j和dp[i - j] * j 取最大的。递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); + +也可以这么理解,j * (i - j) 是单纯的把整数拆分为两个数相乘,而j * dp[i - j]是拆分成两个以及两个以上的个数相乘。 + +如果定义dp[i - j] * dp[j] 也是默认将一个数强制拆成4份以及4份以上了。 + +所以递推公式:dp[i] = max({dp[i], (i - j) * j, dp[i - j] * j}); + +那么在取最大值的时候,为什么还要比较dp[i]呢? + +因为在递推公式推导的过程中,每次计算dp[i],取最大的而已。 + + +3. dp的初始化 + +不少同学应该疑惑,dp[0] dp[1]应该初始化多少呢? + +有的题解里会给出dp[0] = 1,dp[1] = 1的初始化,但解释比较牵强,主要还是因为这么初始化可以把题目过了。 + +严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。 + +拆分0和拆分1的最大乘积是多少? + +这是无解的。 + +这里我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1,这个没有任何异议! + + +4. 确定遍历顺序 + +确定遍历顺序,先来看看递归公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); + + +dp[i] 是依靠 dp[i - j]的状态,所以遍历i一定是从前向后遍历,先有dp[i - j]再有dp[i]。 + + +所以遍历顺序为: +```CPP +for (int i = 3; i <= n ; i++) { + for (int j = 1; j < i - 1; j++) { + dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); + } +} +``` +注意 枚举j的时候,是从1开始的。从0开始的话,那么让拆分一个数拆个0,求最大乘积就没有意义了。 + +j的结束条件是 j < i - 1 ,其实 j < i 也是可以的,不过可以节省一步,例如让j = i - 1,的话,其实在 j = 1的时候,这一步就已经拆出来了,重复计算,所以 j < i - 1 + +至于 i是从3开始,这样dp[i - j]就是dp[2]正好可以通过我们初始化的数值求出来。 + +更优化一步,可以这样: + +```CPP +for (int i = 3; i <= n ; i++) { + for (int j = 1; j <= i / 2; j++) { + dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); + } +} +``` + +因为拆分一个数n 使之乘积最大,那么一定是拆分成m个近似相同的子数相乘才是最大的。 + +例如 6 拆成 3 * 3, 10 拆成 3 * 3 * 4。 100的话 也是拆成m个近似数组的子数 相乘才是最大的。 + +只不过我们不知道m究竟是多少而已,但可以明确的是m一定大于等于2,既然m大于等于2,也就是 最差也应该是拆成两个相同的 可能是最大值。 + +那么 j 遍历,只需要遍历到 n/2 就可以,后面就没有必要遍历了,一定不是最大值。 + +至于 “拆分一个数n 使之乘积最大,那么一定是拆分成m个近似相同的子数相乘才是最大的” 这个我就不去做数学证明了,感兴趣的同学,可以自己证明。 + +5. 举例推导dp数组 + +举例当n为10 的时候,dp数组里的数值,如下: + +![343.整数拆分](https://file1.kamacoder.com/i/algo/20210104173021581.png) + +以上动规五部曲分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int integerBreak(int n) { + vector dp(n + 1); + dp[2] = 1; + for (int i = 3; i <= n ; i++) { + for (int j = 1; j <= i / 2; j++) { + dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); + } + } + return dp[n]; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n) + +### 贪心 + +本题也可以用贪心,每次拆成n个3,如果剩下是4,则保留4,然后相乘,**但是这个结论需要数学证明其合理性!** + +我没有证明,而是直接用了结论。感兴趣的同学可以自己再去研究研究数学证明哈。 + +给出我的C++代码如下: + +```CPP +class Solution { +public: + int integerBreak(int n) { + if (n == 2) return 1; + if (n == 3) return 2; + if (n == 4) return 4; + int result = 1; + while (n > 4) { + result *= 3; + n -= 3; + } + result *= n; + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +## 总结 + +本题掌握其动规的方法,就可以了,贪心的解法确实简单,但需要有数学证明,如果能自圆其说也是可以的。 + +其实这道题目的递推公式并不好想,而且初始化的地方也很有讲究,我在写本题的时候一开始写的代码是这样的: + +```CPP +class Solution { +public: + int integerBreak(int n) { + if (n <= 3) return 1 * (n - 1); + vector dp(n + 1, 0); + dp[1] = 1; + dp[2] = 2; + dp[3] = 3; + for (int i = 4; i <= n ; i++) { + for (int j = 1; j <= i / 2; j++) { + dp[i] = max(dp[i], dp[i - j] * dp[j]); + } + } + return dp[n]; + } +}; +``` +**这个代码也是可以过的!** + +在解释递推公式的时候,也可以解释通,dp[i] 就等于 拆解i - j的最大乘积 * 拆解j的最大乘积。 看起来没毛病! + +但是在解释初始化的时候,就发现自相矛盾了,dp[1]为什么一定是1呢?根据dp[i]的定义,dp[2]也不应该是2啊。 + +但如果递归公式是 dp[i] = max(dp[i], dp[i - j] * dp[j]);,就一定要这么初始化。递推公式没毛病,但初始化解释不通! + +虽然代码在初始位置有一个判断if (n <= 3) return 1 * (n - 1);,保证n<=3 结果是正确的,但代码后面又要给dp[1]赋值1 和 dp[2] 赋值 2,**这其实就是自相矛盾的代码,违背了dp[i]的定义!** + +我举这个例子,其实就说做题的严谨性,上面这个代码也可以AC,大体上一看好像也没有毛病,递推公式也说得过去,但是仅仅是恰巧过了而已。 + + + +## 其他语言版本 + + +### Java +```Java +class Solution { + public int integerBreak(int n) { + //dp[i] 为正整数 i 拆分后的结果的最大乘积 + int[] dp = new int[n+1]; + dp[2] = 1; + for(int i = 3; i <= n; i++) { + for(int j = 1; j <= i-j; j++) { + // 这里的 j 其实最大值为 i-j,再大只不过是重复而已, + //并且,在本题中,我们分析 dp[0], dp[1]都是无意义的, + //j 最大到 i-j,就不会用到 dp[0]与dp[1] + dp[i] = Math.max(dp[i], Math.max(j*(i-j), j*dp[i-j])); + // j * (i - j) 是单纯的把整数 i 拆分为两个数 也就是 i,i-j ,再相乘 + //而j * dp[i - j]是将 i 拆分成两个以及两个以上的个数,再相乘。 + } + } + return dp[n]; + } +} +``` +贪心 +```Java +class Solution { + public int integerBreak(int n) { + // with 贪心 + // 通过数学原理拆出更多的3乘积越大,则 + /** + @Param: an int, the integer we need to break. + @Return: an int, the maximum integer after breaking + @Method: Using math principle to solve this problem + @Time complexity: O(1) + **/ + if(n == 2) return 1; + if(n == 3) return 2; + int result = 1; + while(n > 4) { + n-=3; + result *=3; + } + return result*n; + } +} +``` + +### Python +动态规划(版本一) +```python +class Solution: + # 假设对正整数 i 拆分出的第一个正整数是 j(1 <= j < i),则有以下两种方案: + # 1) 将 i 拆分成 j 和 i−j 的和,且 i−j 不再拆分成多个正整数,此时的乘积是 j * (i-j) + # 2) 将 i 拆分成 j 和 i−j 的和,且 i−j 继续拆分成多个正整数,此时的乘积是 j * dp[i-j] + def integerBreak(self, n): + dp = [0] * (n + 1) # 创建一个大小为n+1的数组来存储计算结果 + dp[2] = 1 # 初始化dp[2]为1,因为当n=2时,只有一个切割方式1+1=2,乘积为1 + + # 从3开始计算,直到n + for i in range(3, n + 1): + # 遍历所有可能的切割点 + for j in range(1, i // 2 + 1): + + # 计算切割点j和剩余部分(i-j)的乘积,并与之前的结果进行比较取较大值 + + dp[i] = max(dp[i], (i - j) * j, dp[i - j] * j) + + return dp[n] # 返回最终的计算结果 + + +``` +动态规划(版本二) +```python +class Solution: + def integerBreak(self, n): + if n <= 3: + return 1 * (n - 1) # 对于n小于等于3的情况,返回1 * (n - 1) + + dp = [0] * (n + 1) # 创建一个大小为n+1的数组来存储最大乘积结果 + dp[1] = 1 # 当n等于1时,最大乘积为1 + dp[2] = 2 # 当n等于2时,最大乘积为2 + dp[3] = 3 # 当n等于3时,最大乘积为3 + + # 从4开始计算,直到n + for i in range(4, n + 1): + # 遍历所有可能的切割点 + for j in range(1, i // 2 + 1): + # 计算切割点j和剩余部分(i - j)的乘积,并与之前的结果进行比较取较大值 + dp[i] = max(dp[i], dp[i - j] * dp[j]) + + return dp[n] # 返回整数拆分的最大乘积结果 + +``` +贪心 +```python +class Solution: + def integerBreak(self, n): + if n == 2: # 当n等于2时,只有一种拆分方式:1+1=2,乘积为1 + return 1 + if n == 3: # 当n等于3时,只有一种拆分方式:2+1=3,乘积为2 + return 2 + if n == 4: # 当n等于4时,有两种拆分方式:2+2=4和1+1+1+1=4,乘积都为4 + return 4 + result = 1 + while n > 4: + result *= 3 # 每次乘以3,因为3的乘积比其他数字更大 + n -= 3 # 每次减去3 + result *= n # 将剩余的n乘以最后的结果 + return result + +``` +### Go + +动态规划 +```go +func integerBreak(n int) int { + /** + 动态五部曲 + 1.确定dp下标及其含义 + 2.确定递推公式 + 3.确定dp初始化 + 4.确定遍历顺序 + 5.打印dp + **/ + dp := make([]int, n+1) + dp[1] = 1 + dp[2] = 1 + for i := 3; i < n+1; i++ { + for j := 1; j < i-1; j++ { +// i可以差分为i-j和j。由于需要最大值,故需要通过j遍历所有存在的值,取其中最大的值作为当前i的最大值,在求最大值的时候,一个是j与i-j相乘,一个是j与dp[i-j]. + dp[i] = max(dp[i], max(j*(i-j), j*dp[i-j])) + } + } + return dp[n] +} +func max(a, b int) int{ + if a > b { + return a + } + return b +} +``` + +贪心 +```go +func integerBreak(n int) int { + if n == 2 { + return 1 + } + if n == 3 { + return 2 + } + if n == 4 { + return 4 + } + result := 1 + for n > 4 { + result *= 3 + n -= 3 + } + result *= n + return result +} +``` + +### JavaScript +```Javascript +var integerBreak = function(n) { + let dp = new Array(n + 1).fill(0) + dp[2] = 1 + + for(let i = 3; i <= n; i++) { + for(let j = 1; j <= i / 2; j++) { + dp[i] = Math.max(dp[i], dp[i - j] * j, (i - j) * j) + } + } + return dp[n] +}; +``` + +### Rust +```rust +pub fn integer_break(n: i32) -> i32 { + let n = n as usize; + let mut dp = vec![0; n + 1]; + dp[2] = 1; + for i in 3..=n { + for j in 1..i-1 { + dp[i] = dp[i].max((i - j) * j).max(dp[i - j] * j); + } + } + dp[n] as i32 +} +``` + +贪心: + +```rust +impl Solution { + pub fn integer_break(mut n: i32) -> i32 { + match n { + 2 => 1, + 3 => 2, + 4 => 4, + 5.. => { + let mut res = 1; + while n > 4 { + res *= 3; + n -= 3; + } + res * n + } + _ => panic!("Error"), + } + } +} +``` + +### TypeScript + +```typescript +function integerBreak(n: number): number { + /** + dp[i]: i对应的最大乘积 + dp[2]: 1; + ... + dp[i]: max( + 1 * dp[i - 1], 1 * (i - 1), + 2 * dp[i - 2], 2 * (i - 2), + ..., (i - 2) * dp[2], (i - 2) * 2 + ); + */ + const dp: number[] = new Array(n + 1).fill(0); + dp[2] = 1; + for (let i = 3; i <= n; i++) { + for (let j = 1; j <= i / 2; j++) { + dp[i] = Math.max(dp[i], j * dp[i - j], j * (i - j)); + } + } + return dp[n]; +}; +``` + +### C + +```c +//初始化DP数组 +int *initDP(int num) { + int* dp = (int*)malloc(sizeof(int) * (num + 1)); + int i; + for(i = 0; i < num + 1; ++i) { + dp[i] = 0; + } + return dp; +} + +//取三数最大值 +int max(int num1, int num2, int num3) { + int tempMax = num1 > num2 ? num1 : num2; + return tempMax > num3 ? tempMax : num3; +} + +int integerBreak(int n){ + int *dp = initDP(n); + //初始化dp[2]为1 + dp[2] = 1; + + int i; + for(i = 3; i <= n; ++i) { + int j; + for(j = 1; j < i - 1; ++j) { + //取得上次循环:dp[i],原数相乘,或j*dp[]i-j] 三数中的最大值 + dp[i] = max(dp[i], j * (i - j), j * dp[i - j]); + } + } + return dp[n]; +} +``` + +### Scala + +```scala +object Solution { + def integerBreak(n: Int): Int = { + var dp = new Array[Int](n + 1) + dp(2) = 1 + for (i <- 3 to n) { + for (j <- 1 until i - 1) { + dp(i) = math.max(dp(i), math.max(j * (i - j), j * dp(i - j))) + } + } + dp(n) + } +} +``` + + +### PHP +```php +class Solution { + + /** + * @param Integer $n + * @return Integer + */ + function integerBreak($n) { + if($n == 0 || $n == 1) return 0; + if($n == 2) return 1; + + $dp = []; + $dp[0] = 0; + $dp[1] = 0; + $dp[2] = 1; + for($i=3;$i<=$n;$i++){ + for($j = 1;$j <= $i/2; $j++){ + $dp[$i] = max(($i-$j)*$j, $dp[$i-$j]*$j, $dp[$i]); + } + } + + return $dp[$n]; + } +} +``` +### C# +```csharp +public class Solution +{ + public int IntegerBreak(int n) + { + int[] dp = new int[n + 1]; + dp[2] = 1; + for (int i = 3; i <= n; i++) + { + for (int j = 1; j <= i / 2; j++) + { + dp[i] = Math.Max(dp[i],Math.Max(j*(i-j),j*dp[i-j])); + } + } + return dp[n]; + } +} +``` + + diff --git "a/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" "b/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" new file mode 100755 index 0000000000..cadb31c97b --- /dev/null +++ "b/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,429 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 打基础的时候,不要太迷恋于库函数。 + +# 344.反转字符串 + +[力扣题目链接](https://leetcode.cn/problems/reverse-string/) + +编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 + +不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 + +你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 + +示例 1: +输入:["h","e","l","l","o"] +输出:["o","l","l","e","h"] + +示例 2: +输入:["H","a","n","n","a","h"] +输出:["h","a","n","n","a","H"] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[字符串基础操作! | LeetCode:344.反转字符串](https://www.bilibili.com/video/BV1fV4y17748),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +先说一说题外话: + +对于这道题目一些同学直接用C++里的一个库函数 reverse,调一下直接完事了, 相信每一门编程语言都有这样的库函数。 + +如果这么做题的话,这样大家不会清楚反转字符串的实现原理了。 + +但是也不是说库函数就不能用,是要分场景的。 + +如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢? + +**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。** + +毕竟面试官一定不是考察你对库函数的熟悉程度, 如果使用python和java 的同学更需要注意这一点,因为python、java提供的库函数十分丰富。 + +**如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。** + +建议大家平时在leetcode上练习算法的时候本着这样的原则去练习,这样才有助于我们对算法的理解。 + +不要沉迷于使用库函数一行代码解决题目之类的技巧,不是说这些技巧不好,而是说这些技巧可以用来娱乐一下。 + +真正自己写的时候,要保证理解可以实现是相应的功能。 + +接下来再来讲一下如何解决反转字符串的问题。 + +大家应该还记得,我们已经讲过了[206.反转链表](https://programmercarl.com/0206.翻转链表.html)。 + +在反转链表中,使用了双指针的方法。 + +那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。 + +因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。 + +如果对数组和链表原理不清楚的同学,可以看这两篇,[关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html),[必须掌握的数组理论知识](https://programmercarl.com/数组理论基础.html)。 + +对于字符串,我们定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。 + +以字符串`hello`为例,过程如下: + +![344.反转字符串](https://file1.kamacoder.com/i/algo/344.%E5%8F%8D%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2.gif) + + +不难写出如下C++代码: + +```CPP +void reverseString(vector& s) { + for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) { + swap(s[i],s[j]); + } +} +``` + +循环里只要做交换s[i] 和s[j]操作就可以了,那么我这里使用了swap 这个库函数。大家可以使用。 + +因为相信大家都知道交换函数如何实现,而且这个库函数仅仅是解题中的一部分, 所以这里使用库函数也是可以的。 + +swap可以有两种实现。 + +一种就是常见的交换数值: + +```CPP +int tmp = s[i]; +s[i] = s[j]; +s[j] = tmp; + +``` + +一种就是通过位运算: + +```CPP +s[i] ^= s[j]; +s[j] ^= s[i]; +s[i] ^= s[j]; +``` + +这道题目还是比较简单的,但是我正好可以通过这道题目说一说在刷题的时候,使用库函数的原则。 + +如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。 + +如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。 + +本着这样的原则,我没有使用reverse库函数,而使用swap库函数。 + +**在字符串相关的题目中,库函数对大家的诱惑力是非常大的,因为会有各种反转,切割取词之类的操作**,这也是为什么字符串的库函数这么丰富的原因。 + +相信大家本着我所讲述的原则来做字符串相关的题目,在选择库函数的角度上会有所原则,也会有所收获。 + +C++代码如下: + +```CPP +class Solution { +public: + void reverseString(vector& s) { + for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) { + swap(s[i],s[j]); + } + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public void reverseString(char[] s) { + int l = 0; + int r = s.length - 1; + while (l < r) { + s[l] ^= s[r]; //构造 a ^ b 的结果,并放在 a 中 + s[r] ^= s[l]; //将 a ^ b 这一结果再 ^ b ,存入b中,此时 b = a, a = a ^ b + s[l] ^= s[r]; //a ^ b 的结果再 ^ a ,存入 a 中,此时 b = a, a = b 完成交换 + l++; + r--; + } + } +} + +// 第二种方法用temp来交换数值更多人容易理解些 +class Solution { + public void reverseString(char[] s) { + int l = 0; + int r = s.length - 1; + while(l < r){ + char temp = s[l]; + s[l] = s[r]; + s[r] = temp; + l++; + r--; + } + } +} + + +``` + +### Python: +(版本一) 双指针 + +```python +class Solution: + def reverseString(self, s: List[str]) -> None: + """ + Do not return anything, modify s in-place instead. + """ + left, right = 0, len(s) - 1 + + # 该方法已经不需要判断奇偶数,经测试后时间空间复杂度比用 for i in range(len(s)//2)更低 + # 因为while每次循环需要进行条件判断,而range函数不需要,直接生成数字,因此时间复杂度更低。推荐使用range + while left < right: + s[left], s[right] = s[right], s[left] + left += 1 + right -= 1 + +``` +(版本二) 使用栈 +```python +class Solution: + def reverseString(self, s: List[str]) -> None: + """ + Do not return anything, modify s in-place instead. + """ + stack = [] + for char in s: + stack.append(char) + for i in range(len(s)): + s[i] = stack.pop() + +``` +(版本三) 使用range +```python +class Solution: + def reverseString(self, s: List[str]) -> None: + """ + Do not return anything, modify s in-place instead. + """ + n = len(s) + for i in range(n // 2): + s[i], s[n - i - 1] = s[n - i - 1], s[i] + +``` +(版本四) 使用reversed +```python +class Solution: + def reverseString(self, s: List[str]) -> None: + """ + Do not return anything, modify s in-place instead. + """ + s[:] = reversed(s) + +``` +(版本五) 使用切片 +```python +class Solution: + def reverseString(self, s: List[str]) -> None: + """ + Do not return anything, modify s in-place instead. + """ + s[:] = s[::-1] + +``` +(版本六) 使用列表推导 +```python +class Solution: + def reverseString(self, s: List[str]) -> None: + """ + Do not return anything, modify s in-place instead. + """ + s[:] = [s[i] for i in range(len(s) - 1, -1, -1)] + +``` + +(版本七) 使用reverse() + +```python +class Solution: + def reverseString(self, s: List[str]) -> None: + """ + Do not return anything, modify s in-place instead. + """ + # 原地反转,无返回值 + s.reverse() + +``` + +### Go: + +```Go +func reverseString(s []byte) { + left := 0 + right := len(s)-1 + for left < right { + s[left], s[right] = s[right], s[left] + left++ + right-- + } +} +``` + +### JavaScript: + +```js +/** + * @param {character[]} s + * @return {void} Do not return anything, modify s in-place instead. + */ +var reverseString = function(s) { + //Do not return anything, modify s in-place instead. + reverse(s) +}; + +var reverse = function(s) { + let l = -1, r = s.length; + while(++l < --r) [s[l], s[r]] = [s[r], s[l]]; +}; +``` + +### TypeScript: + +```typescript +/** + Do not return anything, modify s in-place instead. + */ +function reverseString(s: string[]): void { + let length: number = s.length; + let left: number = 0, + right: number = length - 1; + let tempStr: string; + while (left < right) { + tempStr = s[left]; + s[left] = s[right]; + s[right] = tempStr; + left++; + right--; + } +}; +``` + +### Swift: + +```swift +// 双指针 - 元组 +func reverseString(_ s: inout [Character]) { + var l = 0 + var r = s.count - 1 + while l < r { + // 使用元祖 + (s[l], s[r]) = (s[r], s[l]) + l += 1 + r -= 1 + } +} + +``` + +### Rust: + +```Rust +impl Solution { + pub fn reverse_string(s: &mut Vec) { + let (mut left, mut right) = (0, s.len()-1); + while left < right { + let temp = s[left]; + s[left] = s[right]; + s[right] = temp; + left += 1; + right -= 1; + } + } +} +``` + +### C: + +```c +void reverseString(char* s, int sSize){ + int left = 0; + int right = sSize - 1; + + while(left < right) { + char temp = s[left]; + s[left++] = s[right]; + s[right--] = temp; + + } +} +``` + +### C#: + +```csharp +public class Solution +{ + public void ReverseString(char[] s) + { + for (int i = 0, j = s.Length - 1; i < j; i++, j--) + { + (s[i], s[j]) = (s[j], s[i]); + } + } +} +``` + +### PHP: + +```php +// 双指针 +// 一: +function reverseString(&$s) { + $left = 0; + $right = count($s)-1; + while($left<$right){ + $temp = $s[$left]; + $s[$left] = $s[$right]; + $s[$right] = $temp; + $left++; + $right--; + } +} + +// 二: +function reverseString(&$s) { + $this->reverse($s,0,count($s)-1); +} +// 按指定位置交换元素 +function reverse(&$s, $start, $end) { + for ($i = $start, $j = $end; $i < $j; $i++, $j--) { + $tmp = $s[$i]; + $s[$i] = $s[$j]; + $s[$j] = $tmp; + } + } +``` + +### Scala: + +```scala +object Solution { + def reverseString(s: Array[Char]): Unit = { + var (left, right) = (0, s.length - 1) + while (left < right) { + var tmp = s(left) + s(left) = s(right) + s(right) = tmp + left += 1 + right -= 1 + } + } +} +``` diff --git "a/problems/0347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.md" "b/problems/0347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.md" new file mode 100755 index 0000000000..fa7d6155a5 --- /dev/null +++ "b/problems/0347.\345\211\215K\344\270\252\351\253\230\351\242\221\345\205\203\347\264\240.md" @@ -0,0 +1,610 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 前K个大数问题,老生常谈,不得不谈 + +# 347.前 K 个高频元素 + +[力扣题目链接](https://leetcode.cn/problems/top-k-frequent-elements/) + +给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 + +示例 1: +* 输入: nums = [1,1,1,2,2,3], k = 2 +* 输出: [1,2] + +示例 2: +* 输入: nums = [1], k = 1 +* 输出: [1] + +提示: +* 你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。 +* 你的算法的时间复杂度必须优于 $O(n \log n)$ , n 是数组的大小。 +* 题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。 +* 你可以按任意顺序返回答案。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[优先级队列正式登场!大顶堆、小顶堆该怎么用?| LeetCode:347.前 K 个高频元素](https://www.bilibili.com/video/BV1Xg41167Lz),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这道题目主要涉及到如下三块内容: +1. 要统计元素出现频率 +2. 对频率排序 +3. 找出前K个高频元素 + +首先统计元素出现的频率,这一类的问题可以使用map来进行统计。 + +然后是对频率进行排序,这里我们可以使用一种 容器适配器就是**优先级队列**。 + +什么是优先级队列呢? + +其实**就是一个披着队列外衣的堆**,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。 + +而且优先级队列内部元素是自动依照元素的权值排列。那么它是如何有序排列的呢? + +缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。 + +什么是堆呢? + +**堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。** 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 + +所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。 + +本题我们就要使用优先级队列来对部分频率进行排序。 + +为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。 + +此时要思考一下,是使用小顶堆呢,还是大顶堆? + +有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。 + +那么问题来了,定义一个大小为k的大顶堆,在每次移动更新大顶堆的时候,每次弹出都把最大的元素弹出去了,那么怎么保留下来前K个高频元素呢。 + +而且使用大顶堆就要把所有元素都进行排序,那能不能只排序k个元素呢? + +**所以我们要用小顶堆,因为要统计最大前k个元素,只有小顶堆每次将最小的元素弹出,最后小顶堆里积累的才是前k个最大元素。** + +寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描) + +![347.前K个高频元素](https://file1.kamacoder.com/i/algo/347.前K个高频元素.jpg) + + +我们来看一下C++代码: + + +```CPP +class Solution { +public: + // 小顶堆 + class mycomparison { + public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } + }; + vector topKFrequent(vector& nums, int k) { + // 要统计元素出现频率 + unordered_map map; // map + for (int i = 0; i < nums.size(); i++) { + map[nums[i]]++; + } + + // 对频率排序 + // 定义一个小顶堆,大小为k + priority_queue, vector>, mycomparison> pri_que; + + // 用固定大小为k的小顶堆,扫面所有频率的数值 + for (unordered_map::iterator it = map.begin(); it != map.end(); it++) { + pri_que.push(*it); + if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k + pri_que.pop(); + } + } + + // 找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组 + vector result(k); + for (int i = k - 1; i >= 0; i--) { + result[i] = pri_que.top().first; + pri_que.pop(); + } + return result; + + } +}; +``` + +* 时间复杂度: O(nlogk) +* 空间复杂度: O(n) + +## 拓展 +大家对这个比较运算在建堆时是如何应用的,为什么左大于右就会建立小顶堆,反而建立大顶堆比较困惑。 + +确实 例如我们在写快排的cmp函数的时候,`return left>right` 就是从大到小,`return left队头元素相当于堆的根节点 + * */ +class Solution { + //解法1:基于大顶堆实现 + public int[] topKFrequent1(int[] nums, int k) { + Map map = new HashMap<>(); //key为数组元素值,val为对应出现次数 + for (int num : nums) { + map.put(num, map.getOrDefault(num,0) + 1); + } + //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数 + //出现次数按从队头到队尾的顺序是从大到小排,出现次数最多的在队头(相当于大顶堆) + PriorityQueue pq = new PriorityQueue<>((pair1, pair2) -> pair2[1] - pair1[1]); + for (Map.Entry entry : map.entrySet()) {//大顶堆需要对所有元素进行排序 + pq.add(new int[]{entry.getKey(), entry.getValue()}); + } + int[] ans = new int[k]; + for (int i = 0; i < k; i++) { //依次从队头弹出k个,就是出现频率前k高的元素 + ans[i] = pq.poll()[0]; + } + return ans; + } + //解法2:基于小顶堆实现 + public int[] topKFrequent2(int[] nums, int k) { + Map map = new HashMap<>(); //key为数组元素值,val为对应出现次数 + for (int num : nums) { + map.put(num, map.getOrDefault(num, 0) + 1); + } + //在优先队列中存储二元组(num, cnt),cnt表示元素值num在数组中的出现次数 + //出现次数按从队头到队尾的顺序是从小到大排,出现次数最低的在队头(相当于小顶堆) + PriorityQueue pq = new PriorityQueue<>((pair1, pair2) -> pair1[1] - pair2[1]); + for (Map.Entry entry : map.entrySet()) { //小顶堆只需要维持k个元素有序 + if (pq.size() < k) { //小顶堆元素个数小于k个时直接加 + pq.add(new int[]{entry.getKey(), entry.getValue()}); + } else { + if (entry.getValue() > pq.peek()[1]) { //当前元素出现次数大于小顶堆的根结点(这k个元素中出现次数最少的那个) + pq.poll(); //弹出队头(小顶堆的根结点),即把堆里出现次数最少的那个删除,留下的就是出现次数多的了 + pq.add(new int[]{entry.getKey(), entry.getValue()}); + } + } + } + int[] ans = new int[k]; + for (int i = k - 1; i >= 0; i--) { //依次弹出小顶堆,先弹出的是堆的根,出现次数少,后面弹出的出现次数多 + ans[i] = pq.poll()[0]; + } + return ans; + } +} +``` +简化版代码: +```java +class Solution { + public int[] topKFrequent(int[] nums, int k) { + // 优先级队列,为了避免复杂 api 操作,pq 存储数组 + // lambda 表达式设置优先级队列从大到小存储 o1 - o2 为从小到大,o2 - o1 反之 + PriorityQueue pq = new PriorityQueue<>((o1, o2) -> o1[1] - o2[1]); + int[] res = new int[k]; // 答案数组为 k 个元素 + Map map = new HashMap<>(); // 记录元素出现次数 + for (int num : nums) map.put(num, map.getOrDefault(num, 0) + 1); + for (var x : map.entrySet()) { // entrySet 获取 k-v Set 集合 + // 将 kv 转化成数组 + int[] tmp = new int[2]; + tmp[0] = x.getKey(); + tmp[1] = x.getValue(); + pq.offer(tmp); + // 下面的代码是根据小根堆实现的,我只保留优先队列的最后的k个,只要超出了k我就将最小的弹出,剩余的k个就是答案 + if(pq.size() > k) { + pq.poll(); + } + } + for (int i = 0; i < k; i++) { + res[i] = pq.poll()[0]; // 获取优先队列里的元素 + } + return res; + } +} +``` + +### Python: +解法一: +```python +#时间复杂度:O(nlogk) +#空间复杂度:O(n) +import heapq +class Solution: + def topKFrequent(self, nums: List[int], k: int) -> List[int]: + #要统计元素出现频率 + map_ = {} #nums[i]:对应出现的次数 + for i in range(len(nums)): + map_[nums[i]] = map_.get(nums[i], 0) + 1 + + #对频率排序 + #定义一个小顶堆,大小为k + pri_que = [] #小顶堆 + + #用固定大小为k的小顶堆,扫描所有频率的数值 + for key, freq in map_.items(): + heapq.heappush(pri_que, (freq, key)) + if len(pri_que) > k: #如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k + heapq.heappop(pri_que) + + #找出前K个高频元素,因为小顶堆先弹出的是最小的,所以倒序来输出到数组 + result = [0] * k + for i in range(k-1, -1, -1): + result[i] = heapq.heappop(pri_que)[1] + return result +``` +解法二: +```python +class Solution: + def topKFrequent(self, nums: List[int], k: int) -> List[int]: + # 使用字典统计数字出现次数 + time_dict = defaultdict(int) + for num in nums: + time_dict[num] += 1 + # 更改字典,key为出现次数,value为相应的数字的集合 + index_dict = defaultdict(list) + for key in time_dict: + index_dict[time_dict[key]].append(key) + # 排序 + key = list(index_dict.keys()) + key.sort() + result = [] + cnt = 0 + # 获取前k项 + while key and cnt != k: + result += index_dict[key[-1]] + cnt += len(index_dict[key[-1]]) + key.pop() + + return result[0: k] +``` + +### Go: + +```go +//方法一:小顶堆 +func topKFrequent(nums []int, k int) []int { + map_num:=map[int]int{} + //记录每个元素出现的次数 + for _,item:=range nums{ + map_num[item]++ + } + h:=&IHeap{} + heap.Init(h) + //所有元素入堆,堆的长度为k + for key,value:=range map_num{ + heap.Push(h,[2]int{key,value}) + if h.Len()>k{ + heap.Pop(h) + } + } + res:=make([]int,k) + //按顺序返回堆中的元素 + for i:=0;imap_num[ans[b]] + }) + return ans[:k] +} +``` + + + +### JavaScript: + +解法一: +Leetcode 提供了优先队列的库,具体文档可以参见 [@datastructures-js/priority-queue](https://github.com/datastructures-js/priority-queue/blob/v5/README.md)。 + +```js +var topKFrequent = function (nums, k) { + const map = new Map(); + const res = []; + //使用 map 统计元素出现频率 + for (const num of nums) { + map.set(num, (map.get(num) || 0) + 1); + } + //创建小顶堆 + const heap = new PriorityQueue({ + compare: (a, b) => a.value - b.value + }) + for (const [key, value] of map) { + heap.enqueue({ key, value }); + if (heap.size() > k) heap.dequeue(); + } + //处理输出 + while (heap.size()) res.push(heap.dequeue().key); + return res; +}; +``` + +解法二: +手写实现优先队列 + +```js +// js 没有堆 需要自己构造 +class Heap { + constructor(compareFn) { + this.compareFn = compareFn; + this.queue = []; + } + + // 添加 + push(item) { + // 推入元素 + this.queue.push(item); + + // 上浮 + let index = this.size() - 1; // 记录推入元素下标 + let parent = Math.floor((index - 1) / 2); // 记录父节点下标 + + while (parent >= 0 && this.compare(parent, index) > 0) { // 注意compare参数顺序 + [this.queue[index], this.queue[parent]] = [this.queue[parent], this.queue[index]]; + + // 更新下标 + index = parent; + parent = Math.floor((index - 1) / 2); + } + } + + // 获取堆顶元素并移除 + pop() { + // 边界情况,只有一个元素或没有元素应直接弹出 + if (this.size() <= 1) { + return this.queue.pop() + } + + // 堆顶元素 + const out = this.queue[0]; + + // 移除堆顶元素 填入最后一个元素 + this.queue[0] = this.queue.pop(); + + // 下沉 + let index = 0; // 记录下沉元素下标 + let left = 1; // left 是左子节点下标 left + 1 则是右子节点下标 + let searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left; + + while (this.compare(index, searchChild) > 0) { // 注意compare参数顺序 + [this.queue[index], this.queue[searchChild]] = [this.queue[searchChild], this.queue[index]]; + + // 更新下标 + index = searchChild; + left = 2 * index + 1; + searchChild = this.compare(left, left + 1) > 0 ? left + 1 : left; + } + + return out; + } + + size() { + return this.queue.length; + } + + // 使用传入的 compareFn 比较两个位置的元素 + compare(index1, index2) { + // 处理下标越界问题 + if (this.queue[index1] === undefined) return 1; + if (this.queue[index2] === undefined) return -1; + + return this.compareFn(this.queue[index1], this.queue[index2]); + } + +} + +const topKFrequent = function (nums, k) { + const map = new Map(); + + for (const num of nums) { + map.set(num, (map.get(num) || 0) + 1); + } + + // 创建小顶堆 + const heap= new Heap((a, b) => a[1] - b[1]); + + // entry 是一个长度为2的数组,0位置存储key,1位置存储value + for (const entry of map.entries()) { + heap.push(entry); + + if (heap.size() > k) { + heap.pop(); + } + } + + // return heap.queue.map(e => e[0]); + + const res = []; + + for (let i = heap.size() - 1; i >= 0; i--) { + res[i] = heap.pop()[0]; + } + + return res; +}; +``` + +### TypeScript: + +```typescript +function topKFrequent(nums: number[], k: number): number[] { + const countMap: Map = new Map(); + for (let num of nums) { + countMap.set(num, (countMap.get(num) || 0) + 1); + } + // tS没有最小堆的数据结构,所以直接对整个数组进行排序,取前k个元素 + return [...countMap.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, k) + .map(i => i[0]); +}; +``` + +### C#: + +```csharp + public int[] TopKFrequent(int[] nums, int k) { + //哈希表-标权重 + Dictionary dic = new(); + for(int i = 0; i < nums.Length; i++){ + if(dic.ContainsKey(nums[i])){ + dic[nums[i]]++; + }else{ + dic.Add(nums[i], 1); + } + } + //优先队列-从小到大排列 + PriorityQueue pq = new(); + foreach(var num in dic){ + pq.Enqueue(num.Key, num.Value); + if(pq.Count > k){ + pq.Dequeue(); + } + } + + // //Stack-倒置 + // Stack res = new(); + // while(pq.Count > 0){ + // res.Push(pq.Dequeue()); + // } + // return res.ToArray(); + + //数组倒装 + int[] res = new int[k]; + for(int i = k - 1; i >= 0; i--){ + res[i] = pq.Dequeue(); + } + return res; + } + +``` + +### Scala: + +解法一: 优先级队列 +```scala +object Solution { + import scala.collection.mutable + def topKFrequent(nums: Array[Int], k: Int): Array[Int] = { + val map = mutable.HashMap[Int, Int]() + // 将所有元素都放入Map + for (num <- nums) { + map.put(num, map.getOrElse(num, 0) + 1) + } + // 声明一个优先级队列,在函数柯里化那块需要指明排序方式 + var queue = mutable.PriorityQueue[(Int, Int)]()(Ordering.fromLessThan((x, y) => x._2 > y._2)) + // 将map里面的元素送入优先级队列 + for (elem <- map) { + queue.enqueue(elem) + if(queue.size > k){ + queue.dequeue // 如果队列元素大于k个,出队 + } + } + // 最终只需要key的Array形式就可以了,return关键字可以省略 + queue.map(_._1).toArray + } +} +``` +解法二: 相当于一个wordCount程序 +```scala +object Solution { + def topKFrequent(nums: Array[Int], k: Int): Array[Int] = { + // 首先将数据变为(x,1),然后按照x分组,再使用map进行转换(x,sum),变换为Array + // 再使用sort针对sum进行排序,最后take前k个,再把数据变为x,y,z这种格式 + nums.map((_, 1)).groupBy(_._1) + .map { + case (x, arr) => (x, arr.map(_._2).sum) + } + .toArray + .sortWith(_._2 > _._2) + .take(k) + .map(_._1) + } +} +``` + +### Rust + +小根堆 + +```rust +use std::cmp::Reverse; +use std::collections::{BinaryHeap, HashMap}; +impl Solution { + pub fn top_k_frequent(nums: Vec, k: i32) -> Vec { + let mut hash = HashMap::new(); + let mut heap = BinaryHeap::with_capacity(k as usize); + nums.into_iter().for_each(|k| { + *hash.entry(k).or_insert(0) += 1; + }); + + for (k, v) in hash { + if heap.len() == heap.capacity() { + if *heap.peek().unwrap() < (Reverse(v), k) { + continue; + } else { + heap.pop(); + } + } + heap.push((Reverse(v), k)); + } + heap.into_iter().map(|(_, k)| k).collect() + } +} +``` + + + diff --git "a/problems/0349.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\206.md" "b/problems/0349.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\206.md" new file mode 100755 index 0000000000..77e895da61 --- /dev/null +++ "b/problems/0349.\344\270\244\344\270\252\346\225\260\347\273\204\347\232\204\344\272\244\351\233\206.md" @@ -0,0 +1,545 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +> 如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费! + + +# 349. 两个数组的交集 + +[力扣题目链接](https://leetcode.cn/problems/intersection-of-two-arrays/) + +题意:给定两个数组,编写一个函数来计算它们的交集。 + +![349. 两个数组的交集](https://file1.kamacoder.com/i/algo/20200818193523911.png) + +**说明:** +输出结果中的每个元素一定是唯一的。 +我们可以不考虑输出结果的顺序。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)::[学透哈希表,set使用有技巧!Leetcode:349. 两个数组的交集](https://www.bilibili.com/video/BV1ba411S7wu),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这道题目,主要要学会使用一种哈希数据结构:unordered_set,这个数据结构可以解决很多类似的问题。 + +注意题目特意说明:**输出结果中的每个元素一定是唯一的,也就是说输出的结果的去重的, 同时可以不考虑输出结果的顺序** + +这道题用暴力的解法时间复杂度是O(n^2),那来看看使用哈希法进一步优化。 + +那么用数组来做哈希表也是不错的选择,例如[242. 有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html) + +但是要注意,**使用数组来做哈希的题目,是因为题目都限制了数值的大小。** + +而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。 + +**而且如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费。** + +此时就要使用另一种结构体了,set ,关于set,C++ 给提供了如下三种可用的数据结构: + +* std::set +* std::multiset +* std::unordered_set + +std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希表, 使用unordered_set 读写效率是最高的,并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。 + +思路如图所示: + + +![set哈希法](https://file1.kamacoder.com/i/algo/20220707173513.png) + +C++代码如下: + +```CPP +class Solution { +public: + vector intersection(vector& nums1, vector& nums2) { + unordered_set result_set; // 存放结果,之所以用set是为了给结果集去重 + unordered_set nums_set(nums1.begin(), nums1.end()); + for (int num : nums2) { + // 发现nums2的元素 在nums_set里又出现过 + if (nums_set.find(num) != nums_set.end()) { + result_set.insert(num); + } + } + return vector(result_set.begin(), result_set.end()); + } +}; +``` + +* 时间复杂度: O(n + m) m 是最后要把 set转成vector +* 空间复杂度: O(n) + +## 拓展 + +那有同学可能问了,遇到哈希问题我直接都用set不就得了,用什么数组啊。 + +直接使用set 不仅占用空间比数组大,而且速度要比数组慢,set把数值映射到key上都要做hash计算的。 + +不要小瞧 这个耗时,在数据量大的情况,差距是很明显的。 + +## 后记 + +本题后面 力扣改了 题目描述 和 后台测试数据,增添了 数值范围: + +* 1 <= nums1.length, nums2.length <= 1000 +* 0 <= nums1[i], nums2[i] <= 1000 + +所以就可以 使用数组来做哈希表了, 因为数组都是 1000以内的。 + +对应C++代码如下: + +```CPP +class Solution { +public: + vector intersection(vector& nums1, vector& nums2) { + unordered_set result_set; // 存放结果,之所以用set是为了给结果集去重 + int hash[1005] = {0}; // 默认数值为0 + for (int num : nums1) { // nums1中出现的字母在hash数组中做记录 + hash[num] = 1; + } + for (int num : nums2) { // nums2中出现话,result记录 + if (hash[num] == 1) { + result_set.insert(num); + } + } + return vector(result_set.begin(), result_set.end()); + } +}; +``` + +* 时间复杂度: O(m + n) +* 空间复杂度: O(n) + +## 其他语言版本 + +### Java: +版本一:使用HashSet +```Java +// 时间复杂度O(n+m+k) 空间复杂度O(n+k) +// 其中n是数组nums1的长度,m是数组nums2的长度,k是交集元素的个数 + +import java.util.HashSet; +import java.util.Set; + +class Solution { + public int[] intersection(int[] nums1, int[] nums2) { + if (nums1 == null || nums1.length == 0 || nums2 == null || nums2.length == 0) { + return new int[0]; + } + Set set1 = new HashSet<>(); + Set resSet = new HashSet<>(); + //遍历数组1 + for (int i : nums1) { + set1.add(i); + } + //遍历数组2的过程中判断哈希表中是否存在该元素 + for (int i : nums2) { + if (set1.contains(i)) { + resSet.add(i); + } + } + + //方法1:将结果集合转为数组 + return res.stream().mapToInt(Integer::intValue).toArray(); + /** + * 将 Set 转换为 int[] 数组: + * 1. stream() : Collection 接口的方法,将集合转换为 Stream + * 2. mapToInt(Integer::intValue) : + * - 中间操作,将 Stream 转换为 IntStream + * - 使用方法引用 Integer::intValue,将 Integer 对象拆箱为 int 基本类型 + * 3. toArray() : 终端操作,将 IntStream 转换为 int[] 数组。 + */ + + //方法2:另外申请一个数组存放setRes中的元素,最后返回数组 + int[] arr = new int[resSet.size()]; + int j = 0; + for(int i : resSet){ + arr[j++] = i; + } + + return arr; + } +} +``` +版本二:使用Hash數組 +```java +class Solution { + public int[] intersection(int[] nums1, int[] nums2) { + int[] hash1 = new int[1002]; + int[] hash2 = new int[1002]; + for(int i : nums1) + hash1[i]++; + for(int i : nums2) + hash2[i]++; + List resList = new ArrayList<>(); + for(int i = 0; i < 1002; i++) + if(hash1[i] > 0 && hash2[i] > 0) + resList.add(i); + int index = 0; + int res[] = new int[resList.size()]; + for(int i : resList) + res[index++] = i; + return res; + } +} +``` +### Python3: +(版本一) 使用字典和集合 + +```python +class Solution: + def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: + # 使用哈希表存储一个数组中的所有元素 + table = {} + for num in nums1: + table[num] = table.get(num, 0) + 1 + + # 使用集合存储结果 + res = set() + for num in nums2: + if num in table: + res.add(num) + del table[num] + + return list(res) +``` +(版本二) 使用数组 + +```python + +class Solution: + def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: + count1 = [0]*1001 + count2 = [0]*1001 + result = [] + for i in range(len(nums1)): + count1[nums1[i]]+=1 + for j in range(len(nums2)): + count2[nums2[j]]+=1 + for k in range(1001): + if count1[k]*count2[k]>0: + result.append(k) + return result + +``` +(版本三) 使用集合 + +```python +class Solution: + def intersection(self, nums1: List[int], nums2: List[int]) -> List[int]: + return list(set(nums1) & set(nums2)) + +``` + +### Go: + +(版本一)使用字典和集合 + +```go +func intersection(nums1 []int, nums2 []int) []int { + set:=make(map[int]struct{},0) // 用map模拟set + res:=make([]int,0) + for _,v:=range nums1{ + if _,ok:=set[v];!ok{ + set[v]=struct{}{} + } + } + for _,v:=range nums2{ + //如果存在于上一个数组中,则加入结果集,并清空该set值 + if _,ok:=set[v];ok{ + res=append(res,v) + delete(set, v) + } + } + return res +} +``` + +(版本二)使用数组 + +```go +func intersection(nums1 []int, nums2 []int) []int { + count1 := make([]int, 1001, 1001) + count2 := make([]int, 1001, 1001) + res := make([]int, 0) + for _, v := range nums1 { + count1[v] = 1 + } + for _, v := range nums2 { + count2[v] = 1 + } + for i := 0; i <= 1000; i++ { + if count1[i] + count2[i] == 2 { + res = append(res, i) + } + } + return res +} +``` + +### JavaScript: + +```js +/** + * @param {number[]} nums1 + * @param {number[]} nums2 + * @return {number[]} + */ +var intersection = function(nums1, nums2) { + // 根据数组大小交换操作的数组 + if(nums1.length < nums2.length) { + const _ = nums1; + nums1 = nums2; + nums2 = _; + } + const nums1Set = new Set(nums1); + const resSet = new Set(); + // for(const n of nums2) { + // nums1Set.has(n) && resSet.add(n); + // } + // 循环 比 迭代器快 + for(let i = nums2.length - 1; i >= 0; i--) { + nums1Set.has(nums2[i]) && resSet.add(nums2[i]); + } + return Array.from(resSet); +}; +``` + +### TypeScript: + +版本一(正常解法): + +```typescript +function intersection(nums1: number[], nums2: number[]): number[] { + let resSet: Set = new Set(), + nums1Set: Set = new Set(nums1); + for (let i of nums2) { + if (nums1Set.has(i)) { + resSet.add(i); + } + } + return Array.from(resSet); +}; +``` + +版本二(秀操作): + +```typescript +function intersection(nums1: number[], nums2: number[]): number[] { + return Array.from(new Set(nums1.filter(i => nums2.includes(i)))) +}; +``` + +### Swift: + +```swift +func intersection(_ nums1: [Int], _ nums2: [Int]) -> [Int] { + var set1 = Set() + var set2 = Set() + for num in nums1 { + set1.insert(num) + } + for num in nums2 { + if set1.contains(num) { + set2.insert(num) + } + } + return Array(set2) +} +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer[] $nums1 + * @param Integer[] $nums2 + * @return Integer[] + */ + function intersection($nums1, $nums2) { + if (count($nums1) == 0 || count($nums2) == 0) { + return []; + } + $counts = []; + $res = []; + foreach ($nums1 as $num) { + $counts[$num] = 1; + } + foreach ($nums2 as $num) { + if (isset($counts[$num])) { + $res[] = $num; + } + unset($counts[$num]); + } + + return $res; + } +} +``` + +### Rust: + +```rust +use std::collections::HashSet; +impl Solution { + pub fn intersection(nums1: Vec, nums2: Vec) -> Vec { + let mut resultSet: HashSet = HashSet::with_capacity(1000); + let nums1Set: HashSet = nums1.into_iter().collect(); + + for num in nums2.iter() { + if nums1Set.contains(num) { + resultSet.insert(*num); + } + } + + let ret: Vec = resultSet.into_iter().collect(); + ret + } +} +``` + +解法2: + +```rust +use std::collections::HashSet; +impl Solution { + pub fn intersection(nums1: Vec, nums2: Vec) -> Vec { + nums1 + .into_iter() + .collect::>() + .intersection(&nums2.into_iter().collect::>()) + .copied() + .collect() + } +} +``` + +### C: + +```C +int* intersection1(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize){ + + int nums1Cnt[1000] = {0}; + int lessSize = nums1Size < nums2Size ? nums1Size : nums2Size; + int * result = (int *) calloc(lessSize, sizeof(int)); + int resultIndex = 0; + int* tempNums; + + int i; + + /* Calculate the number's counts for nums1 array */ + for(i = 0; i < nums1Size; i ++) { + nums1Cnt[nums1[i]]++; + } + + /* Check if the value in nums2 is existing in nums1 count array */ + for(i = 0; i < nums2Size; i ++) { + if(nums1Cnt[nums2[i]] > 0) { + result[resultIndex] = nums2[i]; + resultIndex ++; + /* Clear this count to avoid duplicated value */ + nums1Cnt[nums2[i]] = 0; + } + } + * returnSize = resultIndex; + return result; +} +``` + +### Scala: + +正常解法: +```scala +object Solution { + def intersection(nums1: Array[Int], nums2: Array[Int]): Array[Int] = { + // 导入mutable + import scala.collection.mutable + // 临时Set,用于记录数组1出现的每个元素 + val tmpSet: mutable.HashSet[Int] = new mutable.HashSet[Int]() + // 结果Set,存储最终结果 + val resSet: mutable.HashSet[Int] = new mutable.HashSet[Int]() + // 遍历nums1,把每个元素添加到tmpSet + nums1.foreach(tmpSet.add(_)) + // 遍历nums2,如果在tmpSet存在就添加到resSet + nums2.foreach(elem => { + if (tmpSet.contains(elem)) { + resSet.add(elem) + } + }) + // 将结果转换为Array返回,return可以省略 + resSet.toArray + } +} +``` +骚操作1: +```scala +object Solution { + def intersection(nums1: Array[Int], nums2: Array[Int]): Array[Int] = { + // 先转为Set,然后取交集,最后转换为Array + (nums1.toSet).intersect(nums2.toSet).toArray + } +} +``` +骚操作2: +```scala +object Solution { + def intersection(nums1: Array[Int], nums2: Array[Int]): Array[Int] = { + // distinct去重,然后取交集 + (nums1.distinct).intersect(nums2.distinct) + } +} + +``` + +### C#: + +```csharp + public int[] Intersection(int[] nums1, int[] nums2) { + if(nums1==null||nums1.Length==0||nums2==null||nums1.Length==0) + return new int[0]; //注意数组条件 + HashSet one = Insert(nums1); + HashSet two = Insert(nums2); + one.IntersectWith(two); + return one.ToArray(); + } + public HashSet Insert(int[] nums){ + HashSet one = new HashSet(); + foreach(int num in nums){ + one.Add(num); + } + return one; + } + +``` + +### Ruby: +```ruby +def intersection(nums1, nums2) + hash = {} + result = {} + + nums1.each do |num| + hash[num] = 1 if hash[num].nil? + end + + nums2.each do |num| + #取nums1和nums2交集 + result[num] = 1 if hash[num] != nil + end + + return result.keys +end +``` +## 相关题目 + +* [350.两个数组的交集 II](https://leetcode.cn/problems/intersection-of-two-arrays-ii/) + + + diff --git "a/problems/0376.\346\221\206\345\212\250\345\272\217\345\210\227.md" "b/problems/0376.\346\221\206\345\212\250\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..1be9cb4178 --- /dev/null +++ "b/problems/0376.\346\221\206\345\212\250\345\272\217\345\210\227.md" @@ -0,0 +1,714 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 376. 摆动序列 + +[力扣题目链接](https://leetcode.cn/problems/wiggle-subsequence/) + +如果连续数字之间的差严格地在正数和负数之间交替,则数字序列称为摆动序列。第一个差(如果存在的话)可能是正数或负数。少于两个元素的序列也是摆动序列。 + +例如, [1,7,4,9,2,5] 是一个摆动序列,因为差值 (6,-3,5,-7,3)  是正负交替出现的。相反, [1,4,7,2,5]  和  [1,7,4,5,5] 不是摆动序列,第一个序列是因为它的前两个差值都是正数,第二个序列是因为它的最后一个差值为零。 + +给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 + +示例 1: + +- 输入: [1,7,4,9,2,5] +- 输出: 6 +- 解释: 整个序列均为摆动序列。 + +示例 2: + +- 输入: [1,17,5,10,13,15,10,5,16,8] +- 输出: 7 +- 解释: 这个序列包含几个长度为 7 摆动序列,其中一个可为[1,17,10,13,10,16,8]。 + +示例 3: + +- 输入: [1,2,3,4,5,6,7,8,9] +- 输出: 2 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,寻找摆动有细节!| LeetCode:376.摆动序列](https://www.bilibili.com/video/BV17M411b7NS),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 思路 1(贪心解法) + +本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 + +相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢? + +来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢? + +用示例二来举例,如图所示: + +![376.摆动序列](https://file1.kamacoder.com/i/algo/20201124174327597.png) + +**局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值**。 + +**整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列**。 + +局部最优推出全局最优,并举不出反例,那么试试贪心! + +(为方便表述,以下说的峰值都是指局部峰值) + +**实际操作上,其实连删除的操作都不用做,因为题目要求的是最长摆动子序列的长度,所以只需要统计数组的峰值数量就可以了(相当于是删除单一坡度上的节点,然后统计长度)** + +**这就是贪心所贪的地方,让峰值尽可能的保持峰值,然后删除单一坡度上的节点** + +在计算是否有峰值的时候,大家知道遍历的下标 i ,计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i]),如果`prediff < 0 && curdiff > 0` 或者 `prediff > 0 && curdiff < 0` 此时就有波动就需要统计。 + +这是我们思考本题的一个大体思路,但本题要考虑三种情况: + +1. 情况一:上下坡中有平坡 +2. 情况二:数组首尾两端 +3. 情况三:单调坡中有平坡 + +#### 情况一:上下坡中有平坡 + +例如 [1,2,2,2,2,1]这样的数组,如图: + +![](https://file1.kamacoder.com/i/algo/20230106170449.png) + +它的摇摆序列长度是多少呢? **其实是长度是 3**,也就是我们在删除的时候 要不删除左面的三个 2,要不就删除右边的三个 2。 + +如图,可以统一规则,删除左边的三个 2: + +![](https://file1.kamacoder.com/i/algo/20230106172613.png) + +在图中,当 i 指向第一个 2 的时候,`prediff > 0 && curdiff = 0` ,当 i 指向最后一个 2 的时候 `prediff = 0 && curdiff < 0`。 + +如果我们采用,删左面三个 2 的规则,那么 当 `prediff = 0 && curdiff < 0` 也要记录一个峰值,因为他是把之前相同的元素都删掉留下的峰值。 + +所以我们记录峰值的条件应该是: `(preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)`,为什么这里允许 prediff == 0 ,就是为了 上面我说的这种情况。 + +#### 情况二:数组首尾两端 + +所以本题统计峰值的时候,数组最左面和最右面如何统计呢? + +题目中说了,如果只有两个不同的元素,那摆动序列也是 2。 + +例如序列[2,5],如果靠统计差值来计算峰值个数就需要考虑数组最左面和最右面的特殊情况。 + +因为我们在计算 prediff(nums[i] - nums[i-1]) 和 curdiff(nums[i+1] - nums[i])的时候,至少需要三个数字才能计算,而数组只有两个数字。 + +这里我们可以写死,就是 如果只有两个元素,且元素不同,那么结果为 2。 + +不写死的话,如何和我们的判断规则结合在一起呢? + +可以假设,数组最前面还有一个数字,那这个数字应该是什么呢? + +之前我们在 讨论 情况一:相同数字连续 的时候, prediff = 0 ,curdiff < 0 或者 >0 也记为波谷。 + +那么为了规则统一,针对序列[2,5],可以假设为[2,2,5],这样它就有坡度了即 preDiff = 0,如图: + +![376.摆动序列1](https://file1.kamacoder.com/i/algo/20201124174357612.png) + +针对以上情形,result 初始为 1(默认最右面有一个峰值),此时 curDiff > 0 && preDiff <= 0,那么 result++(计算了左面的峰值),最后得到的 result 就是 2(峰值个数为 2 即摆动序列长度为 2) + +经过以上分析后,我们可以写出如下代码: + +```CPP +// 版本一 +class Solution { +public: + int wiggleMaxLength(vector& nums) { + if (nums.size() <= 1) return nums.size(); + int curDiff = 0; // 当前一对差值 + int preDiff = 0; // 前一对差值 + int result = 1; // 记录峰值个数,序列默认序列最右边有一个峰值 + for (int i = 0; i < nums.size() - 1; i++) { + curDiff = nums[i + 1] - nums[i]; + // 出现峰值 + if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) { + result++; + } + preDiff = curDiff; + } + return result; + } +}; +``` + +- 时间复杂度:O(n) +- 空间复杂度:O(1) + +此时大家是不是发现 以上代码提交也不能通过本题? + +所以此时我们要讨论情况三! + +#### 情况三:单调坡度有平坡 + +在版本一中,我们忽略了一种情况,即 如果在一个单调坡度上有平坡,例如[1,2,2,2,3,4],如图: + +![](https://file1.kamacoder.com/i/algo/20230108171505.png) + +图中,我们可以看出,版本一的代码在三个地方记录峰值,但其实结果因为是 2,因为 单调中的平坡 不能算峰值(即摆动)。 + +之所以版本一会出问题,是因为我们实时更新了 prediff。 + +那么我们应该什么时候更新 prediff 呢? + +我们只需要在 这个坡度 摆动变化的时候,更新 prediff 就行,这样 prediff 在 单调区间有平坡的时候 就不会发生变化,造成我们的误判。 + +所以本题的最终代码为: + +```CPP + +// 版本二 +class Solution { +public: + int wiggleMaxLength(vector& nums) { + if (nums.size() <= 1) return nums.size(); + int curDiff = 0; // 当前一对差值 + int preDiff = 0; // 前一对差值 + int result = 1; // 记录峰值个数,序列默认序列最右边有一个峰值 + for (int i = 0; i < nums.size() - 1; i++) { + curDiff = nums[i + 1] - nums[i]; + // 出现峰值 + if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) { + result++; + preDiff = curDiff; // 注意这里,只在摆动变化的时候更新prediff + } + } + return result; + } +}; +``` + +其实本题看起来好像简单,但需要考虑的情况还是很复杂的,而且很难一次性想到位。 + +**本题异常情况的本质,就是要考虑平坡**, 平坡分两种,一个是 上下中间有平坡,一个是单调有平坡,如图: + +![](https://file1.kamacoder.com/i/algo/20230108174452.png) + +### 思路 2(动态规划) + +考虑用动态规划的思想来解决这个问题。 + +很容易可以发现,对于我们当前考虑的这个数,要么是作为山峰(即 nums[i] > nums[i-1]),要么是作为山谷(即 nums[i] < nums[i - 1])。 + +- 设 dp 状态`dp[i][0]`,表示考虑前 i 个数,第 i 个数作为山峰的摆动子序列的最长长度 +- 设 dp 状态`dp[i][1]`,表示考虑前 i 个数,第 i 个数作为山谷的摆动子序列的最长长度 + +则转移方程为: + +- `dp[i][0] = max(dp[i][0], dp[j][1] + 1)`,其中`0 < j < i`且`nums[j] < nums[i]`,表示将 nums[i]接到前面某个山谷后面,作为山峰。 +- `dp[i][1] = max(dp[i][1], dp[j][0] + 1)`,其中`0 < j < i`且`nums[j] > nums[i]`,表示将 nums[i]接到前面某个山峰后面,作为山谷。 + +初始状态: + +由于一个数可以接到前面的某个数后面,也可以以自身为子序列的起点,所以初始状态为:`dp[0][0] = dp[0][1] = 1`。 + +C++代码如下: + +```CPP +class Solution { +public: + int dp[1005][2]; + int wiggleMaxLength(vector& nums) { + memset(dp, 0, sizeof dp); + dp[0][0] = dp[0][1] = 1; + for (int i = 1; i < nums.size(); ++i) { + dp[i][0] = dp[i][1] = 1; + for (int j = 0; j < i; ++j) { + if (nums[j] > nums[i]) dp[i][1] = max(dp[i][1], dp[j][0] + 1); + } + for (int j = 0; j < i; ++j) { + if (nums[j] < nums[i]) dp[i][0] = max(dp[i][0], dp[j][1] + 1); + } + } + return max(dp[nums.size() - 1][0], dp[nums.size() - 1][1]); + } +}; +``` + +- 时间复杂度:O(n^2) +- 空间复杂度:O(n) + +**进阶** + +可以用两棵线段树来维护区间的最大值 + +- 每次更新`dp[i][0]`,则在`tree1`的`nums[i]`位置值更新为`dp[i][0]` +- 每次更新`dp[i][1]`,则在`tree2`的`nums[i]`位置值更新为`dp[i][1]` +- 则 dp 转移方程中就没有必要 j 从 0 遍历到 i-1,可以直接在线段树中查询指定区间的值即可。 + +时间复杂度:O(nlog n) + +空间复杂度:O(n) + +## 其他语言版本 + +### Java + +```Java +class Solution { + public int wiggleMaxLength(int[] nums) { + if (nums.length <= 1) { + return nums.length; + } + //当前差值 + int curDiff = 0; + //上一个差值 + int preDiff = 0; + int count = 1; + for (int i = 1; i < nums.length; i++) { + //得到当前差值 + curDiff = nums[i] - nums[i - 1]; + //如果当前差值和上一个差值为一正一负 + //等于0的情况表示初始时的preDiff + if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) { + count++; + preDiff = curDiff; + } + } + return count; + } +} +``` + +```java +// DP +class Solution { + public int wiggleMaxLength(int[] nums) { + // 0 i 作为波峰的最大长度 + // 1 i 作为波谷的最大长度 + int dp[][] = new int[nums.length][2]; + + dp[0][0] = dp[0][1] = 1; + for (int i = 1; i < nums.length; i++){ + //i 自己可以成为波峰或者波谷 + dp[i][0] = dp[i][1] = 1; + + for (int j = 0; j < i; j++){ + if (nums[j] > nums[i]){ + // i 是波谷 + dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1); + } + if (nums[j] < nums[i]){ + // i 是波峰 + dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1); + } + } + } + + return Math.max(dp[nums.length - 1][0], dp[nums.length - 1][1]); + } +} +``` + +### Python + +贪心(版本一) + +```python +class Solution: + def wiggleMaxLength(self, nums): + if len(nums) <= 1: + return len(nums) # 如果数组长度为0或1,则返回数组长度 + curDiff = 0 # 当前一对元素的差值 + preDiff = 0 # 前一对元素的差值 + result = 1 # 记录峰值的个数,初始为1(默认最右边的元素被视为峰值) + for i in range(len(nums) - 1): + curDiff = nums[i + 1] - nums[i] # 计算下一个元素与当前元素的差值 + # 如果遇到一个峰值 + if (preDiff <= 0 and curDiff > 0) or (preDiff >= 0 and curDiff < 0): + result += 1 # 峰值个数加1 + preDiff = curDiff # 注意这里,只在摆动变化的时候更新preDiff + return result # 返回最长摆动子序列的长度 + +``` +贪心(版本二) + +```python +class Solution: + def wiggleMaxLength(self, nums: List[int]) -> int: + if len(nums) <= 1: + return len(nums) # 如果数组长度为0或1,则返回数组长度 + preDiff,curDiff ,result = 0,0,1 #题目里nums长度大于等于1,当长度为1时,其实到不了for循环里去,所以不用考虑nums长度 + for i in range(len(nums) - 1): + curDiff = nums[i + 1] - nums[i] + if curDiff * preDiff <= 0 and curDiff !=0: #差值为0时,不算摆动 + result += 1 + preDiff = curDiff #如果当前差值和上一个差值为一正一负时,才需要用当前差值替代上一个差值 + return result + +``` + +动态规划(版本一) + +```python +class Solution: + def wiggleMaxLength(self, nums: List[int]) -> int: + # 0 i 作为波峰的最大长度 + # 1 i 作为波谷的最大长度 + # dp是一个列表,列表中每个元素是长度为 2 的列表 + dp = [] + for i in range(len(nums)): + # 初始为[1, 1] + dp.append([1, 1]) + for j in range(i): + # nums[i] 为波谷 + if nums[j] > nums[i]: + dp[i][1] = max(dp[i][1], dp[j][0] + 1) + # nums[i] 为波峰 + if nums[j] < nums[i]: + dp[i][0] = max(dp[i][0], dp[j][1] + 1) + return max(dp[-1][0], dp[-1][1]) +``` + +动态规划(版本二) + +```python +class Solution: + def wiggleMaxLength(self, nums): + dp = [[0, 0] for _ in range(len(nums))] # 创建二维dp数组,用于记录摆动序列的最大长度 + dp[0][0] = dp[0][1] = 1 # 初始条件,序列中的第一个元素默认为峰值,最小长度为1 + for i in range(1, len(nums)): + dp[i][0] = dp[i][1] = 1 # 初始化当前位置的dp值为1 + for j in range(i): + if nums[j] > nums[i]: + dp[i][1] = max(dp[i][1], dp[j][0] + 1) # 如果前一个数比当前数大,可以形成一个上升峰值,更新dp[i][1] + for j in range(i): + if nums[j] < nums[i]: + dp[i][0] = max(dp[i][0], dp[j][1] + 1) # 如果前一个数比当前数小,可以形成一个下降峰值,更新dp[i][0] + return max(dp[-1][0], dp[-1][1]) # 返回最大的摆动序列长度 + +``` + +动态规划(版本三)优化 + +```python +class Solution: + def wiggleMaxLength(self, nums): + if len(nums) <= 1: + return len(nums) # 如果数组长度为0或1,则返回数组长度 + + up = down = 1 # 记录上升和下降摆动序列的最大长度 + for i in range(1, len(nums)): + if nums[i] > nums[i-1]: + up = down + 1 # 如果当前数比前一个数大,则可以形成一个上升峰值 + elif nums[i] < nums[i-1]: + down = up + 1 # 如果当前数比前一个数小,则可以形成一个下降峰值 + + return max(up, down) # 返回上升和下降摆动序列的最大长度 + + +``` +### Go + +**贪心** + +```go +func wiggleMaxLength(nums []int) int { + n := len(nums) + if n < 2 { + return n + } + ans := 1 + prevDiff := nums[1] - nums[0] + if prevDiff != 0 { + ans = 2 + } + for i := 2; i < n; i++ { + diff := nums[i] - nums[i-1] + if diff > 0 && prevDiff <= 0 || diff < 0 && prevDiff >= 0 { + ans++ + prevDiff = diff + } + } + return ans +} +``` + +**动态规划** + +```go +func wiggleMaxLength(nums []int) int { + n := len(nums) + if n <= 1 { + return n + } + dp := make([][2]int, n) + // i 0 作为波峰的最大长度 + // i 1 作为波谷的最大长度 + dp[0][0] = 1 + dp[0][1] = 1 + for i := 0; i < n; i++ { + for j := 0; j < i; j++ { + if nums[j] > nums[i] { //nums[i]为波谷 + dp[i][1] = max(dp[i][1], dp[j][0]+1) + } + if nums[j] < nums[i] { //nums[i]为波峰 或者相等 + dp[i][0] = max(dp[i][0], dp[j][1]+1) + } + if nums[j] == nums[i] { //添加一种情况,nums[i]为相等 + dp[i][0] = max(dp[i][0], dp[j][0]) //波峰 + dp[i][1] = max(dp[i][1], dp[j][1]) //波谷 + } + } + } + return max(dp[n-1][0], dp[n-1][1]) +} +func max(a, b int) int { + if a > b { + return a + } else { + return b + } +} +``` + +### JavaScript + +**贪心** + +```Javascript +var wiggleMaxLength = function(nums) { + if(nums.length <= 1) return nums.length + let result = 1 + let preDiff = 0 + let curDiff = 0 + for(let i = 0; i < nums.length - 1; i++) { + curDiff = nums[i + 1] - nums[i] + if((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) { + result++ + preDiff = curDiff + } + } + return result +}; +``` + +**动态规划** + +```Javascript +var wiggleMaxLength = function(nums) { + if (nums.length === 1) return 1; + // 考虑前i个数,当第i个值作为峰谷时的情况(则第i-1是峰顶) + let down = 1; + // 考虑前i个数,当第i个值作为峰顶时的情况(则第i-1是峰谷) + let up = 1; + for (let i = 1; i < nums.length; i++) { + if (nums[i] < nums[i - 1]) { + down = Math.max(up + 1, down); + } + if (nums[i] > nums[i - 1]) { + up = Math.max(down + 1, up) + } + } + return Math.max(down, up); +}; +``` + +### Rust + +**贪心** + +```Rust +impl Solution { + pub fn wiggle_max_length(nums: Vec) -> i32 { + if nums.len() == 1 { + return 1; + } + let mut res = 1; + let mut pre_diff = 0; + for i in 0..nums.len() - 1 { + let cur_diff = nums[i + 1] - nums[i]; + if (pre_diff <= 0 && cur_diff > 0) || (pre_diff >= 0 && cur_diff < 0) { + res += 1; + pre_diff = cur_diff; + } + } + res + } +} +``` + +**动态规划** + +```rust +impl Solution { + pub fn wiggle_max_length(nums: Vec) -> i32 { + if nums.len() == 1 { + return 1; + } + let (mut down, mut up) = (1, 1); + for i in 1..nums.len() { + // i - 1 为峰顶 + if nums[i] < nums[i - 1] { + down = down.max(up + 1); + } + // i - 1 为峰谷 + if nums[i] > nums[i - 1] { + up = up.max(down + 1); + } + } + down.max(up) + } +} +``` + +### C + +**贪心** + +```c +int wiggleMaxLength(int* nums, int numsSize){ + if(numsSize <= 1) + return numsSize; + + int length = 1; + int preDiff , curDiff; + preDiff = curDiff = 0; + for(int i = 0; i < numsSize - 1; ++i) { + // 计算当前i元素与i+1元素差值 + curDiff = nums[i+1] - nums[i]; + + // 若preDiff与curDiff符号不符,则子序列长度+1。更新preDiff的符号 + // 若preDiff与curDiff符号一致,当前i元素为连续升序/连续降序子序列的中间元素。不被记录入长度 + // 注:当preDiff为0时,curDiff为正或为负都属于符号不同 + if((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) { + preDiff = curDiff; + length++; + } + } + + return length; +} +``` + +**动态规划** + +```c +int max(int left, int right) +{ + return left > right ? left : right; +} +int wiggleMaxLength(int* nums, int numsSize){ + if(numsSize <= 1) + { + return numsSize; + } + // 0 i 作为波峰的最大长度 + // 1 i 作为波谷的最大长度 + int dp[numsSize][2]; + for(int i = 0; i < numsSize; i++) + { + dp[i][0] = 1; + dp[i][1] = 1; + for(int j = 0; j < i; j++) + { + // nums[i] 为山谷 + if(nums[j] > nums[i]) + { + dp[i][1] = max(dp[i][1], dp[j][0] + 1); + } + // nums[i] 为山峰 + if(nums[j] < nums[i]) + { + dp[i][0] = max(dp[i][0], dp[j][1] + 1); + } + } + } + return max(dp[numsSize - 1][0], dp[numsSize - 1][1]); +} +``` + +### TypeScript + +**贪心** + +```typescript +function wiggleMaxLength(nums: number[]): number { + let length: number = nums.length; + if (length <= 1) return length; + let preDiff: number = 0; + let curDiff: number = 0; + let count: number = 1; + for (let i = 1; i < length; i++) { + curDiff = nums[i] - nums[i - 1]; + if ((preDiff <= 0 && curDiff > 0) || (preDiff >= 0 && curDiff < 0)) { + preDiff = curDiff; + count++; + } + } + return count; +} +``` + +**动态规划** + +```typescript +function wiggleMaxLength(nums: number[]): number { + const length: number = nums.length; + if (length <= 1) return length; + const dp: number[][] = new Array(length).fill(0).map((_) => []); + dp[0][0] = 1; // 第一个数作为波峰 + dp[0][1] = 1; // 第一个数作为波谷 + for (let i = 1; i < length; i++) { + dp[i][0] = 1; + dp[i][1] = 1; + for (let j = 0; j < i; j++) { + if (nums[j] < nums[i]) dp[i][0] = Math.max(dp[i][0], dp[j][1] + 1); + } + for (let j = 0; j < i; j++) { + if (nums[j] > nums[i]) dp[i][1] = Math.max(dp[i][1], dp[j][0] + 1); + } + } + return Math.max(dp[length - 1][0], dp[length - 1][1]); +} +``` + +### Scala + +```scala +object Solution { + def wiggleMaxLength(nums: Array[Int]): Int = { + if (nums.length <= 1) return nums.length + var result = 1 + var curDiff = 0 // 当前一对的差值 + var preDiff = 0 // 前一对的差值 + + for (i <- 1 until nums.length) { + curDiff = nums(i) - nums(i - 1) // 计算当前这一对的差值 + // 当 curDiff > 0 的情况,preDiff <= 0 + // 当 curDiff < 0 的情况,preDiff >= 0 + // 这两种情况算是两个峰值 + if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) { + result += 1 // 结果集加 1 + preDiff = curDiff // 当前差值赋值给上一轮 + } + } + + result + } +} +``` +### C# +```csharp +public class Solution +{ + public int WiggleMaxLength(int[] nums) + { + if (nums.Length < 2) return nums.Length; + int curDiff = 0, preDiff = 0, res = 1; + for (int i = 0; i < nums.Length - 1; i++) + { + curDiff = nums[i + 1] - nums[i]; + if ((curDiff > 0 && preDiff <= 0) || (curDiff < 0 && preDiff >= 0)) + { + res++; + preDiff = curDiff; + } + } + return res; + } +} +``` + diff --git "a/problems/0377.\347\273\204\345\220\210\346\200\273\345\222\214\342\205\243.md" "b/problems/0377.\347\273\204\345\220\210\346\200\273\345\222\214\342\205\243.md" new file mode 100755 index 0000000000..ab92f24aef --- /dev/null +++ "b/problems/0377.\347\273\204\345\220\210\346\200\273\345\222\214\342\205\243.md" @@ -0,0 +1,357 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 377. 组合总和 Ⅳ + +[力扣题目链接](https://leetcode.cn/problems/combination-sum-iv/) + +难度:中等 + +给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。 + +示例: + +* nums = [1, 2, 3] +* target = 4 + +所有可能的组合为: +(1, 1, 1, 1) +(1, 1, 2) +(1, 2, 1) +(1, 3) +(2, 1, 1) +(2, 2) +(3, 1) + +请注意,顺序不同的序列被视作不同的组合。 + +因此输出为 7。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[装满背包有几种方法?求排列数?| LeetCode:377.组合总和IV](https://www.bilibili.com/video/BV1V14y1n7B6/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +对完全背包还不了解的同学,可以看这篇:[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html) + +本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,**其实就是求排列!** + +弄清什么是组合,什么是排列很重要。 + +组合不强调顺序,(1,5)和(5,1)是同一个组合。 + +排列强调顺序,(1,5)和(5,1)是两个不同的排列。 + +大家在公众号里学习回溯算法专题的时候,一定做过这两道题目[回溯算法:39.组合总和](https://programmercarl.com/0039.组合总和.html)和[回溯算法:40.组合总和II](https://programmercarl.com/0040.组合总和II.html)会感觉这两题和本题很像! + +但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。 + +**如果本题要把排列都列出来的话,只能使用回溯算法爆搜**。 + +动规五部曲分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[i]: 凑成目标正整数为i的排列个数为dp[i]** + +2. 确定递推公式 + +dp[i](考虑nums[j])可以由 dp[i - nums[j]](不考虑nums[j]) 推导出来。 + +因为只要得到nums[j],排列个数dp[i - nums[j]],就是dp[i]的一部分。 + +在[动态规划:494.目标和](https://programmercarl.com/0494.目标和.html) 和 [动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)中我们已经讲过了,求装满背包有几种方法,递推公式一般都是dp[i] += dp[i - nums[j]]; + +本题也一样。 + +3. dp数组如何初始化 + +因为递推公式dp[i] += dp[i - nums[j]]的缘故,dp[0]要初始化为1,这样递归其他dp[i]的时候才会有数值基础。 + +至于dp[0] = 1 有没有意义呢? + +其实没有意义,所以我也不去强行解释它的意义了,因为题目中也说了:给定目标值是正整数! 所以dp[0] = 1是没有意义的,仅仅是为了推导递推公式。 + +至于非0下标的dp[i]应该初始为多少呢? + +初始化为0,这样才不会影响dp[i]累加所有的dp[i - nums[j]]。 + + +4. 确定遍历顺序 + +个数可以不限使用,说明这是一个完全背包。 + +得到的集合是排列,说明需要考虑元素之间的顺序。 + + +本题要求的是排列,那么这个for循环嵌套的顺序可以有说法了。 + +在[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) 中就已经讲过了。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面! + +所以本题遍历顺序最终遍历顺序:**target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历**。 + +5. 举例来推导dp数组 + +我们再来用示例中的例子推导一下: + +![377.组合总和Ⅳ](https://file1.kamacoder.com/i/algo/20230310000625.png) + +如果代码运行处的结果不是想要的结果,就把dp[i]都打出来,看看和我们推导的一不一样。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int combinationSum4(vector& nums, int target) { + vector dp(target + 1, 0); + dp[0] = 1; + for (int i = 0; i <= target; i++) { // 遍历背包 + for (int j = 0; j < nums.size(); j++) { // 遍历物品 + if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) { + dp[i] += dp[i - nums[j]]; + } + } + } + return dp[target]; + } +}; + +``` + +* 时间复杂度: O(target * n),其中 n 为 nums 的长度 +* 空间复杂度: O(target) + + + +C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。 + +但java就不用考虑这个限制,java里的int也是四个字节吧,也有可能leetcode后台对不同语言的测试数据不一样。 + +## 总结 + +**求装满背包有几种方法,递归公式都是一样的,没有什么差别,但关键在于遍历顺序!** + +本题与[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html)就是一个鲜明的对比,一个是求排列,一个是求组合,遍历顺序完全不同。 + +如果对遍历顺序没有深度理解的话,做这种完全背包的题目会很懵逼,即使题目刷过了可能也不太清楚具体是怎么过的。 + +此时大家应该对动态规划中的遍历顺序又有更深的理解了。 + + + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int combinationSum4(int[] nums, int target) { + int[] dp = new int[target + 1]; + dp[0] = 1; + for (int i = 0; i <= target; i++) { + for (int j = 0; j < nums.length; j++) { + if (i >= nums[j]) { + dp[i] += dp[i - nums[j]]; + } + } + } + return dp[target]; + } +} +``` + +### Python: + + +卡哥版 +```python +class Solution: + def combinationSum4(self, nums: List[int], target: int) -> int: + dp = [0] * (target + 1) + dp[0] = 1 + for i in range(1, target + 1): # 遍历背包 + for j in range(len(nums)): # 遍历物品 + if i - nums[j] >= 0: + dp[i] += dp[i - nums[j]] + return dp[target] + +``` +优化版 + +```python +class Solution: + def combinationSum4(self, nums: List[int], target: int) -> int: + dp = [0] * (target + 1) # 创建动态规划数组,用于存储组合总数 + dp[0] = 1 # 初始化背包容量为0时的组合总数为1 + + for i in range(1, target + 1): # 遍历背包容量 + for j in nums: # 遍历物品列表 + if i >= j: # 当背包容量大于等于当前物品重量时 + dp[i] += dp[i - j] # 更新组合总数 + + return dp[-1] # 返回背包容量为target时的组合总数 + + +``` +二维DP版 +```python +class Solution: + def combinationSum4(self, nums: List[int], target: int) -> int: + # dp[][j]和为j的组合的总数 + dp = [[0] * (target+1) for _ in nums] + + for i in range(len(nums)): + dp[i][0] = 1 + + # 这里不能初始化dp[0][j]。dp[0][j]的值依赖于dp[-1][j-nums[0]] + + for j in range(1, target+1): + for i in range(len(nums)): + + if j - nums[i] >= 0: + dp[i][j] = ( + # 不放nums[i] + # i = 0 时,dp[-1][j]恰好为0,所以没有特殊处理 + dp[i-1][j] + + # 放nums[i]。对于和为j的组合,只有试过全部物品,才能知道有几种组合方式。所以取最后一个物品dp[-1][j-nums[i]] + dp[-1][j-nums[i]] + ) + else: + dp[i][j] = dp[i-1][j] + return dp[-1][-1] +``` + +### Go: + +```go +func combinationSum4(nums []int, target int) int { + //定义dp数组 + dp := make([]int, target+1) + // 初始化 + dp[0] = 1 + // 遍历顺序, 先遍历背包,再循环遍历物品 + for j:=0;j<=target;j++ { + for i:=0 ;i < len(nums);i++ { + if j >= nums[i] { + dp[j] += dp[j-nums[i]] + } + } + } + return dp[target] +} +``` + +### JavaScript: + +```javascript +const combinationSum4 = (nums, target) => { + + let dp = Array(target + 1).fill(0); + dp[0] = 1; + + for(let i = 0; i <= target; i++) { + for(let j = 0; j < nums.length; j++) { + if (i >= nums[j]) { + dp[i] += dp[i - nums[j]]; + } + } + } + + return dp[target]; +}; +``` + +### TypeScript: + +```typescript +function combinationSum4(nums: number[], target: number): number { + const dp: number[] = new Array(target + 1).fill(0); + dp[0] = 1; + // 遍历背包 + for (let i = 1; i <= target; i++) { + // 遍历物品 + for (let j = 0, length = nums.length; j < length; j++) { + if (i >= nums[j]) { + dp[i] += dp[i - nums[j]]; + } + } + } + return dp[target]; +}; +``` + +### Rust: + +```Rust +impl Solution { + pub fn combination_sum4(nums: Vec, target: i32) -> i32 { + let target = target as usize; + let mut dp = vec![0; target + 1]; + dp[0] = 1; + for i in 1..=target { + for &n in &nums { + if i >= n as usize { + dp[i] += dp[i - n as usize]; + } + } + } + dp[target] + } +} +``` +### C + +```c +int combinationSum4(int* nums, int numsSize, int target) { + int dp[target + 1]; + memset(dp, 0, sizeof (dp )); + dp[0] = 1; + for(int i = 0; i <= target; i++){ + for(int j = 0; j < numsSize; j++){ + if(i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]){ + dp[i] += dp[i - nums[j]]; + } + } + } + return dp[target]; +} +``` + + + +### C# + +```csharp +public class Solution +{ + public int CombinationSum4(int[] nums, int target) + { + int[] dp = new int[target + 1]; + dp[0] = 1; + for (int i = 0; i <= target; i++) + { + for (int j = 0; j < nums.Length; j++) + { + if (i >= nums[j] && dp[i] < int.MaxValue - dp[i - nums[j]]) + { + dp[i] += dp[i - nums[j]]; + } + } + } + return dp[target]; + } +} +``` + + diff --git "a/problems/0383.\350\265\216\351\207\221\344\277\241.md" "b/problems/0383.\350\265\216\351\207\221\344\277\241.md" new file mode 100755 index 0000000000..8a2f52ae42 --- /dev/null +++ "b/problems/0383.\350\265\216\351\207\221\344\277\241.md" @@ -0,0 +1,467 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 在哈希法中有一些场景就是为数组量身定做的。 + +# 383. 赎金信 + +[力扣题目链接](https://leetcode.cn/problems/ransom-note/) + +给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。 + +(题目说明:为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思。杂志字符串中的每个字符只能在赎金信字符串中使用一次。) + +**注意:** + +你可以假设两个字符串均只含有小写字母。 + +canConstruct("a", "b") -> false +canConstruct("aa", "ab") -> false +canConstruct("aa", "aab") -> true + +## 思路 + +这道题目和[242.有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html)很像,[242.有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html)相当于求 字符串a 和 字符串b 是否可以相互组成 ,而这道题目是求 字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。 + +本题判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点。 + +*  第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思”  这里*说明杂志里面的字母不可重复使用。* + +* 第二点 “你可以假设两个字符串均只含有小写字母。” *说明只有小写字母*,这一点很重要 + +### 暴力解法 + +那么第一个思路其实就是暴力枚举了,两层for循环,不断去寻找,代码如下: + +```CPP +class Solution { +public: + bool canConstruct(string ransomNote, string magazine) { + for (int i = 0; i < magazine.length(); i++) { + for (int j = 0; j < ransomNote.length(); j++) { + // 在ransomNote中找到和magazine相同的字符 + if (magazine[i] == ransomNote[j]) { + ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符 + break; + } + } + } + // 如果ransomNote为空,则说明magazine的字符可以组成ransomNote + if (ransomNote.length() == 0) { + return true; + } + return false; + } +}; +``` + +* 时间复杂度: O(n^2) +* 空间复杂度: O(1) + +这里时间复杂度是比较高的,而且里面还有一个字符串删除也就是erase的操作,也是费时的,当然这段代码也可以过这道题。 + + +### 哈希解法 + +因为题目说只有小写字母,那可以采用空间换取时间的哈希策略,用一个长度为26的数组来记录magazine里字母出现的次数。 + +然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。 + +依然是数组在哈希法中的应用。 + +一些同学可能想,用数组干啥,都用map完事了,**其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数,是费时的!数据量大的话就能体现出来差别了。 所以数组更加简单直接有效!** + +代码如下: + +```CPP +class Solution { +public: + bool canConstruct(string ransomNote, string magazine) { + int record[26] = {0}; + //add + if (ransomNote.size() > magazine.size()) { + return false; + } + for (int i = 0; i < magazine.length(); i++) { + // 通过record数据记录 magazine里各个字符出现次数 + record[magazine[i]-'a'] ++; + } + for (int j = 0; j < ransomNote.length(); j++) { + // 遍历ransomNote,在record里对应的字符个数做--操作 + record[ransomNote[j]-'a']--; + // 如果小于零说明ransomNote里出现的字符,magazine没有 + if(record[ransomNote[j]-'a'] < 0) { + return false; + } + } + return true; + } +}; +``` + +* 时间复杂度: O(m+n),其中m表示ransomNote的长度,n表示magazine的长度 +* 空间复杂度: O(1) + + + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public boolean canConstruct(String ransomNote, String magazine) { + // shortcut + if (ransomNote.length() > magazine.length()) { + return false; + } + // 定义一个哈希映射数组 + int[] record = new int[26]; + + // 遍历 + for(char c : magazine.toCharArray()){ + record[c - 'a'] += 1; + } + + for(char c : ransomNote.toCharArray()){ + record[c - 'a'] -= 1; + } + + // 如果数组中存在负数,说明ransomNote字符串中存在magazine中没有的字符 + for(int i : record){ + if(i < 0){ + return false; + } + } + + return true; + } +} + +``` + +### Python: +(版本一)使用数组 + +```python +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + ransom_count = [0] * 26 + magazine_count = [0] * 26 + for c in ransomNote: + ransom_count[ord(c) - ord('a')] += 1 + for c in magazine: + magazine_count[ord(c) - ord('a')] += 1 + return all(ransom_count[i] <= magazine_count[i] for i in range(26)) +``` + +(版本二)使用defaultdict + +```python +from collections import defaultdict + +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + + hashmap = defaultdict(int) + + for x in magazine: + hashmap[x] += 1 + + for x in ransomNote: + value = hashmap.get(x) + if not value: + return False + else: + hashmap[x] -= 1 + + return True +``` +(版本三)使用字典 + +```python +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + counts = {} + for c in magazine: + counts[c] = counts.get(c, 0) + 1 + for c in ransomNote: + if c not in counts or counts[c] == 0: + return False + counts[c] -= 1 + return True +``` +(版本四)使用Counter + +```python +from collections import Counter + +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + return not Counter(ransomNote) - Counter(magazine) +``` +(版本五)使用count + +```python +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + return all(ransomNote.count(c) <= magazine.count(c) for c in set(ransomNote)) +``` + +(版本六)使用count(简单易懂) + +```python +class Solution: + def canConstruct(self, ransomNote: str, magazine: str) -> bool: + for char in ransomNote: + if char in magazine and ransomNote.count(char) <= magazine.count(char): + continue + else: + return False + return True +``` + +### Go: + +```go +func canConstruct(ransomNote string, magazine string) bool { + record := make([]int, 26) + for _, v := range magazine { // 通过record数据记录 magazine里各个字符出现次数 + record[v-'a']++ + } + for _, v := range ransomNote { // 遍历ransomNote,在record里对应的字符个数做--操作 + record[v-'a']-- + if record[v-'a'] < 0 { // 如果小于零说明ransomNote里出现的字符,magazine没有 + return false + } + } + return true +} +``` + +### JavaScript: + +```js +/** + * @param {string} ransomNote + * @param {string} magazine + * @return {boolean} + */ +var canConstruct = function(ransomNote, magazine) { + const strArr = new Array(26).fill(0), + base = "a".charCodeAt(); + for(const s of magazine) { // 记录 magazine里各个字符出现次数 + strArr[s.charCodeAt() - base]++; + } + for(const s of ransomNote) { // 对应的字符个数做--操作 + const index = s.charCodeAt() - base; + if(!strArr[index]) return false; // 如果没记录过直接返回false + strArr[index]--; + } + return true; +}; +``` + +### TypeScript: + +```typescript +function canConstruct(ransomNote: string, magazine: string): boolean { + let helperArr: number[] = new Array(26).fill(0); + let base: number = 'a'.charCodeAt(0); + let index: number; + for (let i = 0, length = magazine.length; i < length; i++) { + helperArr[magazine[i].charCodeAt(0) - base]++; + } + for (let i = 0, length = ransomNote.length; i < length; i++) { + index = ransomNote[i].charCodeAt(0) - base; + helperArr[index]--; + if (helperArr[index] < 0) { + return false; + } + } + return true; +}; +``` + +### PHP: + +```php +class Solution { + /** + * @param String $ransomNote + * @param String $magazine + * @return Boolean + */ + function canConstruct($ransomNote, $magazine) { + if (count($ransomNote) > count($magazine)) { + return false; + } + $map = []; + for ($i = 0; $i < strlen($magazine); $i++) { + $map[$magazine[$i]] = ($map[$magazine[$i]] ?? 0) + 1; + } + for ($i = 0; $i < strlen($ransomNote); $i++) { + if (!isset($map[$ransomNote[$i]]) || --$map[$ransomNote[$i]] < 0) { + return false; + } + } + return true; + } +``` + +### Swift: + +```swift +func canConstruct(_ ransomNote: String, _ magazine: String) -> Bool { + var record = Array(repeating: 0, count: 26); + let aUnicodeScalarValue = "a".unicodeScalars.first!.value + for unicodeScalar in magazine.unicodeScalars { + // 通过record 记录 magazine 里各个字符出现的次数 + let idx: Int = Int(unicodeScalar.value - aUnicodeScalarValue) + record[idx] += 1 + } + for unicodeScalar in ransomNote.unicodeScalars { + // 遍历 ransomNote,在record里对应的字符个数做 -- 操作 + let idx: Int = Int(unicodeScalar.value - aUnicodeScalarValue) + record[idx] -= 1 + // 如果小于零说明在magazine没有 + if record[idx] < 0 { + return false + } + } + return true +} +``` + +### Rust: + +```rust +impl Solution { + pub fn can_construct(ransom_note: String, magazine: String) -> bool { + let baseChar = 'a'; + let mut record = vec![0; 26]; + + for byte in magazine.bytes() { + record[byte as usize - baseChar as usize] += 1; + } + + for byte in ransom_note.bytes() { + record[byte as usize - baseChar as usize] -= 1; + if record[byte as usize - baseChar as usize] < 0 { + return false; + } + } + + return true; + } +} +``` + +### Scala: + +版本一: 使用数组作为哈希表 +```scala +object Solution { + def canConstruct(ransomNote: String, magazine: String): Boolean = { + // 如果magazine的长度小于ransomNote的长度,必然是false + if (magazine.length < ransomNote.length) { + return false + } + // 定义一个数组,存储magazine字符出现的次数 + val map: Array[Int] = new Array[Int](26) + // 遍历magazine字符串,对应的字符+=1 + for (i <- magazine.indices) { + map(magazine(i) - 'a') += 1 + } + // 遍历ransomNote + for (i <- ransomNote.indices) { + if (map(ransomNote(i) - 'a') > 0) + map(ransomNote(i) - 'a') -= 1 + else return false + } + // 如果上面没有返回false,直接返回true,关键字return可以省略 + true + } +} +``` + +```scala +object Solution { + import scala.collection.mutable + def canConstruct(ransomNote: String, magazine: String): Boolean = { + // 如果magazine的长度小于ransomNote的长度,必然是false + if (magazine.length < ransomNote.length) { + return false + } + // 定义map,key是字符,value是字符出现的次数 + val map = new mutable.HashMap[Char, Int]() + // 遍历magazine,把所有的字符都记录到map里面 + for (i <- magazine.indices) { + val tmpChar = magazine(i) + // 如果map包含该字符,那么对应的value++,否则添加该字符 + if (map.contains(tmpChar)) { + map.put(tmpChar, map.get(tmpChar).get + 1) + } else { + map.put(tmpChar, 1) + } + } + // 遍历ransomNote + for (i <- ransomNote.indices) { + val tmpChar = ransomNote(i) + // 如果map包含并且该字符的value大于0,则匹配成功,map对应的--,否则直接返回false + if (map.contains(tmpChar) && map.get(tmpChar).get > 0) { + map.put(tmpChar, map.get(tmpChar).get - 1) + } else { + return false + } + } + // 如果上面没有返回false,直接返回true,关键字return可以省略 + true + } +} +``` + +### C#: + +```csharp +public bool CanConstruct(string ransomNote, string magazine) { + if(ransomNote.Length > magazine.Length) return false; + int[] letters = new int[26]; + foreach(char c in magazine){ + letters[c-'a']++; + } + foreach(char c in ransomNote){ + letters[c-'a']--; + if(letters[c-'a']<0){ + return false; + } + } + return true; + } + +``` + +### C: + +```c +bool canConstruct(char* ransomNote, char* magazine) { + // 定义哈希映射数组 + int hashmap[26] = {0}; + // 对magazine中字符计数 + while (*magazine != '\0') hashmap[*magazine++ % 26]++; + // 遍历ransomNote,对应的字符自减,小于0说明该字符magazine没有或不足够表示 + while (*ransomNote != '\0') hashmap[*ransomNote++ % 26]--; + // 如果数组中存在负数,说明ransomNote不能由magazine里面的字符构成 + for (int i = 0; i < 26; i++) { + if (hashmap[i] < 0) return false; + } + return true; +} + +``` + + diff --git "a/problems/0392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" "b/problems/0392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..bf2d959682 --- /dev/null +++ "b/problems/0392.\345\210\244\346\226\255\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,405 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 392.判断子序列 + +[力扣题目链接](https://leetcode.cn/problems/is-subsequence/) + +给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 + +字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde"的一个子序列,而"aec"不是)。 + +示例 1: +* 输入:s = "abc", t = "ahbgdc" +* 输出:true + +示例 2: +* 输入:s = "axc", t = "ahbgdc" +* 输出:false + +提示: + +* 0 <= s.length <= 100 +* 0 <= t.length <= 10^4 + +两个字符串都只由小写字符组成。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,用相似思路解决复杂问题 | LeetCode:392.判断子序列](https://www.bilibili.com/video/BV1tv4y1B7ym/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +(这道题也可以用双指针的思路来实现,时间复杂度也是O(n)) + +这道题应该算是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。 + +**所以掌握本题的动态规划解法是对后面要讲解的编辑距离的题目打下基础**。 + +动态规划五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i][j] 表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]**。 + +注意这里是判断s是否为t的子序列。即t的长度是大于等于s的。 + +有同学问了,为啥要表示下标i-1为结尾的字符串呢,为啥不表示下标i为结尾的字符串呢? + +为什么这么定义我在 [718. 最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html) 中做了详细的讲解。 + +其实用i来表示也可以! + +但我统一以下标i-1为结尾的字符串来计算,这样在下面的递归公式中会容易理解一些,如果还有疑惑,可以继续往下看。 + +2. 确定递推公式 + +在确定递推公式的时候,首先要考虑如下两种操作,整理如下: + +* if (s[i - 1] == t[j - 1]) + * t中找到了一个字符在s中也出现了 +* if (s[i - 1] != t[j - 1]) + * 相当于t要删除元素,继续匹配 + +if (s[i - 1] == t[j - 1]),那么dp[i][j] = dp[i - 1][j - 1] + 1;,因为找到了一个相同的字符,相同子序列长度自然要在dp[i-1][j-1]的基础上加1(**如果不理解,在回看一下dp[i][j]的定义**) + +if (s[i - 1] != t[j - 1]),此时相当于t要删除元素,t如果把当前元素t[j - 1]删除,那么dp[i][j] 的数值就是 看s[i - 1]与 t[j - 2]的比较结果了,即:dp[i][j] = dp[i][j - 1]; + +其实这里 大家可以发现和 [1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html) 的递推公式基本那就是一样的,区别就是 本题 如果删元素一定是字符串t,而 1143.最长公共子序列 是两个字符串都可以删元素。 + + +3. dp数组如何初始化 + +从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],所以dp[0][0]和dp[i][0]是一定要初始化的。 + +这里大家已经可以发现,在定义dp[i][j]含义的时候为什么要**表示以下标i-1为结尾的字符串s,和以下标j-1为结尾的字符串t,相同子序列的长度为dp[i][j]**。 + +因为这样的定义在dp二维矩阵中可以留出初始化的区间,如图: + + +![392.判断子序列](https://file1.kamacoder.com/i/algo/20210303173115966.png) + +如果要是定义的dp[i][j]是以下标i为结尾的字符串s和以下标j为结尾的字符串t,初始化就比较麻烦了。 + +dp[i][0] 表示以下标i-1为结尾的字符串,与空字符串的相同子序列长度,所以为0. dp[0][j]同理。 + + +```CPP +vector> dp(s.size() + 1, vector(t.size() + 1, 0)); +``` + +4. 确定遍历顺序 + +同理从递推公式可以看出dp[i][j]都是依赖于dp[i - 1][j - 1] 和 dp[i][j - 1],那么遍历顺序也应该是从上到下,从左到右 + +如图所示: + + +![392.判断子序列1](https://file1.kamacoder.com/i/algo/20210303172354155.jpg) + +5. 举例推导dp数组 + +以示例一为例,输入:s = "abc", t = "ahbgdc",dp状态转移图如下: + + +![392.判断子序列2](https://file1.kamacoder.com/i/algo/2021030317364166.jpg) + +dp[i][j]表示以下标i-1为结尾的字符串s和以下标j-1为结尾的字符串t 相同子序列的长度,所以如果dp[s.size()][t.size()] 与 字符串s的长度相同说明:s与t的最长相同子序列就是s,那么s 就是 t 的子序列。 + +图中dp[s.size()][t.size()] = 3, 而s.size() 也为3。所以s是t 的子序列,返回true。 + +动规五部曲分析完毕,C++代码如下: + +```CPP +class Solution { +public: + bool isSubsequence(string s, string t) { + vector> dp(s.size() + 1, vector(t.size() + 1, 0)); + for (int i = 1; i <= s.size(); i++) { + for (int j = 1; j <= t.size(); j++) { + if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; + else dp[i][j] = dp[i][j - 1]; + } + } + if (dp[s.size()][t.size()] == s.size()) return true; + return false; + } +}; +``` + +* 时间复杂度:O(n × m) +* 空间复杂度:O(n × m) + +## 总结 + +这道题目算是编辑距离的入门题目(毕竟这里只是涉及到减法),也是动态规划解决的经典题型。 + +这一类题都是题目读上去感觉很复杂,模拟一下也发现很复杂,用动规分析完了也感觉很复杂,但是最终代码却很简短。 + +在之前的题目讲解中,我们讲了 [1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html),大家会发现 本题和 1143.最长公共子序列 的相似之处。 + +编辑距离的题目最能体现出动规精髓和巧妙之处,大家可以好好体会一下。 + + + +## 其他语言版本 + +### Java: + +```java +class Solution { + public boolean isSubsequence(String s, String t) { + int length1 = s.length(); int length2 = t.length(); + int[][] dp = new int[length1+1][length2+1]; + for(int i = 1; i <= length1; i++){ + for(int j = 1; j <= length2; j++){ + if(s.charAt(i-1) == t.charAt(j-1)){ + dp[i][j] = dp[i-1][j-1] + 1; + }else{ + dp[i][j] = dp[i][j-1]; + } + } + } + if(dp[length1][length2] == length1){ + return true; + }else{ + return false; + } + } +} +``` +> 修改遍历顺序后,可以利用滚动数组,对dp数组进行压缩 +```java +class Solution { + public boolean isSubsequence(String s, String t) { + // 修改遍历顺序,外圈遍历t,内圈遍历s。使得dp的推算只依赖正上方和左上方,方便压缩。 + int[][] dp = new int[t.length() + 1][s.length() + 1]; + for (int i = 1; i < dp.length; i++) { // 遍历t字符串 + for (int j = 1; j < dp[i].length; j++) { // 遍历s字符串 + if (t.charAt(i - 1) == s.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = dp[i - 1][j]; + } + } + System.out.println(Arrays.toString(dp[i])); + } + return dp[t.length()][s.length()] == s.length(); + } +} +``` +> 状态压缩 +```java +class Solution { + public boolean isSubsequence(String s, String t) { + int[] dp = new int[s.length() + 1]; + for (int i = 0; i < t.length(); i ++) { + // 需要使用上一轮的dp[j - 1],所以使用倒序遍历 + for (int j = dp.length - 1; j > 0; j --) { + // i遍历的是t字符串,j遍历的是dp数组,dp数组的长度比s的大1,因此需要减1。 + if (t.charAt(i) == s.charAt(j - 1)) { + dp[j] = dp[j - 1] + 1; + } + } + } + return dp[s.length()] == s.length(); + } +} +``` +> 将dp定义为boolean类型,dp[i]直接表示s.substring(0, i)是否为t的子序列 + +```java +class Solution { + public boolean isSubsequence(String s, String t) { + boolean[] dp = new boolean[s.length() + 1]; + // 表示 “” 是t的子序列 + dp[0] = true; + for (int i = 0; i < t.length(); i ++) { + for (int j = dp.length - 1; j > 0; j --) { + if (t.charAt(i) == s.charAt(j - 1)) { + dp[j] = dp[j - 1]; + } + } + } + return dp[dp.length - 1]; + } +} +``` + +### Python: + +```python +class Solution: + def isSubsequence(self, s: str, t: str) -> bool: + dp = [[0] * (len(t)+1) for _ in range(len(s)+1)] + for i in range(1, len(s)+1): + for j in range(1, len(t)+1): + if s[i-1] == t[j-1]: + dp[i][j] = dp[i-1][j-1] + 1 + else: + dp[i][j] = dp[i][j-1] + if dp[-1][-1] == len(s): + return True + return False +``` + +### JavaScript: + +```javascript +const isSubsequence = (s, t) => { + // s、t的长度 + const [m, n] = [s.length, t.length]; + // dp全初始化为0 + const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0)); + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + // 更新dp[i][j],两种情况 + if (s[i - 1] === t[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = dp[i][j - 1]; + } + } + } + // 遍历结束,判断dp右下角的数是否等于s的长度 + return dp[m][n] === m ? true : false; +}; +``` + +### TypeScript: + +> 二维数组 + +```typescript +function isSubsequence(s: string, t: string): boolean { + /** + dp[i][j]: s的前i-1个,t的前j-1个,最长公共子序列的长度 + */ + const sLen = s.length + const tLen = t.length + const dp: number[][] = new Array(sLen + 1).fill(0).map(_ => new Array(tLen + 1).fill(0)) + + for (let i = 1; i <= sLen; i++) { + for (let j = 1; j <= tLen; j++) { + if (s[i - 1] === t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1 + // 只需要取 j-2 的 dp 值即可,不用考虑 i-2 + else dp[i][j] = dp[i][j - 1] + } + } + return dp[sLen][tLen] === s.length +} +``` + +> 滚动数组 +```typescript +function isSubsequence(s: string, t: string): boolean { + const sLen = s.length + const tLen = t.length + const dp: number[] = new Array(tLen + 1).fill(0) + + for (let i = 1; i <= sLen; i++) { + let prev: number = 0; + let temp: number = 0; + for (let j = 1; j <= tLen; j++) { + // 备份一下当前状态(经过上层迭代后的) + temp = dp[j] + // prev 相当于 dp[j-1](累加了上层的状态) + // 如果单纯 dp[j-1] 则不会包含上层状态 + if (s[i - 1] === t[j - 1]) dp[j] = prev + 1 + else dp[j] = dp[j - 1] + // 继续使用上一层状态更新参数用于当前层下一个状态 + prev = temp + } + } + return dp[tLen] === sLen +} +``` + +### Go: + +二维DP: + +```go +func isSubsequence(s string, t string) bool { + dp := make([][]int, len(s) + 1) + for i := 0; i < len(dp); i ++{ + dp[i] = make([]int, len(t) + 1) + } + for i := 1; i < len(dp); i ++{ + for j := 1; j < len(dp[i]); j ++{ + if s[i - 1] == t[j - 1] { + dp[i][j] = dp[i - 1][j - 1] +1 + }else{ + dp[i][j] = dp[i][j - 1] + } + } + } + return dp[len(s)][len(t)] == len(s) +} +``` + +一维DP: + +```go +func isSubsequence(s string, t string) bool { + dp := make([]int, len(s) + 1) + for i := 1; i <= len(t); i ++ { + for j := len(s); j >= 1; j -- { + if t[i - 1] == s[j - 1] { + dp[j] = dp[j - 1] + 1 + } + } + } + return dp[len(s)] == len(s) +} +``` + + +### Rust: + +```rust +impl Solution { + pub fn is_subsequence(s: String, t: String) -> bool { + let mut dp = vec![vec![0; t.len() + 1]; s.len() + 1]; + for (i, char_s) in s.chars().enumerate() { + for (j, char_t) in t.chars().enumerate() { + if char_s == char_t { + dp[i + 1][j + 1] = dp[i][j] + 1; + continue; + } + dp[i + 1][j + 1] = dp[i + 1][j] + } + } + dp[s.len()][t.len()] == s.len() + } +} +``` + +> 滚动数组 + +```rust +impl Solution { + pub fn is_subsequence(s: String, t: String) -> bool { + let mut dp = vec![0; t.len() + 1]; + let (s, t) = (s.as_bytes(), t.as_bytes()); + for &byte_s in s { + let mut pre = 0; + for j in 0..t.len() { + let temp = dp[j + 1]; + if byte_s == t[j] { + dp[j + 1] = pre + 1; + } else { + dp[j + 1] = dp[j]; + } + pre = temp; + } + } + dp[t.len()] == s.len() + } +} +``` + + diff --git "a/problems/0404.\345\267\246\345\217\266\345\255\220\344\271\213\345\222\214.md" "b/problems/0404.\345\267\246\345\217\266\345\255\220\344\271\213\345\222\214.md" new file mode 100755 index 0000000000..10b159b181 --- /dev/null +++ "b/problems/0404.\345\267\246\345\217\266\345\255\220\344\271\213\345\222\214.md" @@ -0,0 +1,685 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 404.左叶子之和 + +[力扣题目链接](https://leetcode.cn/problems/sum-of-left-leaves/) + +计算给定二叉树的所有左叶子之和。 + +示例: + + +![404.左叶子之和1](https://file1.kamacoder.com/i/algo/20210204151927654.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)::[二叉树的题目中,总有一些规则让你找不到北 | LeetCode:404.左叶子之和](https://www.bilibili.com/video/BV1GY4y1K7z8),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +**首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。** + +因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:**节点A的左孩子不为空,且左孩子的左右孩子都为空(说明是叶子节点),那么A节点的左孩子为左叶子节点** + +大家思考一下如下图中二叉树,左叶子之和究竟是多少? + +![404.左叶子之和](https://file1.kamacoder.com/i/algo/20210204151949672.png) +**其实是0,因为这棵树根本没有左叶子!** + +但看这个图的左叶子之和是多少? + +![图二](https://file1.kamacoder.com/i/algo/20220902165805.png) + +相信通过这两个图,大家对最左叶子的定义有明确理解了。 + +那么**判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。** + + +如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子,判断代码如下: + +```CPP +if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) { + 左叶子节点处理逻辑 +} +``` + +### 递归法 + +递归的遍历顺序为后序遍历(左右中),是因为要通过递归函数的返回值来累加求取左叶子数值之和。 + +递归三部曲: + +1. 确定递归函数的参数和返回值 + +判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int + +使用题目中给出的函数就可以了。 + +2. 确定终止条件 + +如果遍历到空节点,那么左叶子值一定是0 + +```CPP +if (root == NULL) return 0; +``` + +注意,只有当前遍历的节点是父节点,才能判断其子节点是不是左叶子。 所以如果当前遍历的节点是叶子节点,那其左叶子也必定是0,那么终止条件为: + +```CPP +if (root == NULL) return 0; +if (root->left == NULL && root->right== NULL) return 0; //其实这个也可以不写,如果不写不影响结果,但就会让递归多进行了一层。 +``` + + +3. 确定单层递归的逻辑 + +当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。 + +代码如下: + +```CPP +int leftValue = sumOfLeftLeaves(root->left); // 左 +if (root->left && !root->left->left && !root->left->right) { + leftValue = root->left->val; +} +int rightValue = sumOfLeftLeaves(root->right); // 右 + +int sum = leftValue + rightValue; // 中 +return sum; + +``` + + +整体递归代码如下: + +```CPP +class Solution { +public: + int sumOfLeftLeaves(TreeNode* root) { + if (root == NULL) return 0; + if (root->left == NULL && root->right== NULL) return 0; + + int leftValue = sumOfLeftLeaves(root->left); // 左 + if (root->left && !root->left->left && !root->left->right) { // 左子树就是一个左叶子的情况 + leftValue = root->left->val; + } + int rightValue = sumOfLeftLeaves(root->right); // 右 + + int sum = leftValue + rightValue; // 中 + return sum; + } +}; + +``` + +以上代码精简之后如下: + +```CPP +class Solution { +public: + int sumOfLeftLeaves(TreeNode* root) { + if (root == NULL) return 0; + int leftValue = 0; + if (root->left != NULL && root->left->left == NULL && root->left->right == NULL) { + leftValue = root->left->val; + } + return leftValue + sumOfLeftLeaves(root->left) + sumOfLeftLeaves(root->right); + } +}; +``` + +精简之后的代码其实看不出来用的是什么遍历方式了,对于算法初学者以上根据第一个版本来学习。 + +### 迭代法 + +本题迭代法使用前中后序都是可以的,只要把左叶子节点统计出来,就可以了,那么参考文章 [二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)和[二叉树:迭代法统一写法](https://programmercarl.com/二叉树的统一迭代法.html)中的写法,可以写出一个前序遍历的迭代法。 + +判断条件都是一样的,代码如下: + +```CPP + +class Solution { +public: + int sumOfLeftLeaves(TreeNode* root) { + stack st; + if (root == NULL) return 0; + st.push(root); + int result = 0; + while (!st.empty()) { + TreeNode* node = st.top(); + st.pop(); + if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) { + result += node->left->val; + } + if (node->right) st.push(node->right); + if (node->left) st.push(node->left); + } + return result; + } +}; +``` + +## 总结 + +这道题目要求左叶子之和,其实是比较绕的,因为不能判断本节点是不是左叶子节点。 + +此时就要通过节点的父节点来判断其左孩子是不是左叶子了。 + +**平时我们解二叉树的题目时,已经习惯了通过节点的左右孩子判断本节点的属性,而本题我们要通过节点的父节点判断本节点的属性。** + +希望通过这道题目,可以扩展大家对二叉树的解题思路。 + + +## 其他语言版本 + +### Java: + +**递归** + +```java +class Solution { + public int sumOfLeftLeaves(TreeNode root) { + if (root == null) return 0; + int leftValue = sumOfLeftLeaves(root.left); // 左 + int rightValue = sumOfLeftLeaves(root.right); // 右 + + int midValue = 0; + if (root.left != null && root.left.left == null && root.left.right == null) { + midValue = root.left.val; + } + int sum = midValue + leftValue + rightValue; // 中 + return sum; + } +} +``` + +**迭代** + +```java +class Solution { + public int sumOfLeftLeaves(TreeNode root) { + if (root == null) return 0; + Stack stack = new Stack<> (); + stack.add(root); + int result = 0; + while (!stack.isEmpty()) { + TreeNode node = stack.pop(); + if (node.left != null && node.left.left == null && node.left.right == null) { + result += node.left.val; + } + if (node.right != null) stack.add(node.right); + if (node.left != null) stack.add(node.left); + } + return result; + } +} +``` +```java +// 层序遍历迭代法 +class Solution { + public int sumOfLeftLeaves(TreeNode root) { + int sum = 0; + if (root == null) return 0; + Queue queue = new LinkedList<>(); + queue.offer(root); + while (!queue.isEmpty()) { + int size = queue.size(); + while (size -- > 0) { + TreeNode node = queue.poll(); + if (node.left != null) { // 左节点不为空 + queue.offer(node.left); + if (node.left.left == null && node.left.right == null){ // 左叶子节点 + sum += node.left.val; + } + } + if (node.right != null) queue.offer(node.right); + } + } + return sum; + } +} +``` + + +### Python: +递归 +```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 sumOfLeftLeaves(self, root): + if root is None: + return 0 + if root.left is None and root.right is None: + return 0 + + leftValue = self.sumOfLeftLeaves(root.left) # 左 + if root.left and not root.left.left and not root.left.right: # 左子树是左叶子的情况 + leftValue = root.left.val + + rightValue = self.sumOfLeftLeaves(root.right) # 右 + + sum_val = leftValue + rightValue # 中 + return sum_val +``` +递归精简版 + +```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 sumOfLeftLeaves(self, root): + if root is None: + return 0 + leftValue = 0 + if root.left is not None and root.left.left is None and root.left.right is None: + leftValue = root.left.val + return leftValue + self.sumOfLeftLeaves(root.left) + self.sumOfLeftLeaves(root.right) +``` + +迭代法 +```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 sumOfLeftLeaves(self, root): + if root is None: + return 0 + st = [root] + result = 0 + while st: + node = st.pop() + if node.left and node.left.left is None and node.left.right is None: + result += node.left.val + if node.right: + st.append(node.right) + if node.left: + st.append(node.left) + return result + +``` + +### Go: + +**递归法** + +```go +func sumOfLeftLeaves(root *TreeNode) int { + if root == nil { + return 0 + } + leftValue := sumOfLeftLeaves(root.Left) // 左 + + if root.Left != nil && root.Left.Left == nil && root.Left.Right == nil { + leftValue = root.Left.Val // 中 + } + + rightValue := sumOfLeftLeaves(root.Right) // 右 + + return leftValue + rightValue +} +``` + +**递归精简版** + +```go +func sumOfLeftLeaves(root *TreeNode) int { + if root == nil { + return 0 + } + leftValue := 0 + if root.Left != nil && root.Left.Left == nil && root.Left.Right == nil { + leftValue = root.Left.Val + } + return leftValue + sumOfLeftLeaves(root.Left) + sumOfLeftLeaves(root.Right) +} +``` + +**迭代法(前序遍历)** + +```go +func sumOfLeftLeaves(root *TreeNode) int { + st := make([]*TreeNode, 0) + if root == nil { + return 0 + } + st = append(st, root) + result := 0 + + for len(st) != 0 { + node := st[len(st)-1] + st = st[:len(st)-1] + if node.Left != nil && node.Left.Left == nil && node.Left.Right == nil { + result += node.Left.Val + } + if node.Right != nil { + st = append(st, node.Right) + } + if node.Left != nil { + st = append(st, node.Left) + } + } + + return result +} + +``` + + +### JavaScript: + +**递归法** + +```javascript +var sumOfLeftLeaves = function(root) { + //采用后序遍历 递归遍历 + // 1. 确定递归函数参数 + const nodesSum = function(node) { + // 2. 确定终止条件 + if(node === null) { + return 0; + } + let leftValue = nodesSum(node.left); + let rightValue = nodesSum(node.right); + // 3. 单层递归逻辑 + let midValue = 0; + if(node.left && node.left.left === null && node.left.right === null) { + midValue = node.left.val; + } + let sum = midValue + leftValue + rightValue; + return sum; + } + return nodesSum(root); +}; +``` + +**迭代法** +```javascript +var sumOfLeftLeaves = function(root) { + //采用层序遍历 + if(root === null) { + return null; + } + let queue = []; + let sum = 0; + queue.push(root); + while(queue.length) { + let node = queue.shift(); + if(node.left !== null && node.left.left === null && node.left.right === null) { + sum+=node.left.val; + } + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + return sum; +}; +``` + +### TypeScript: + +> 递归法 + +```typescript +function sumOfLeftLeaves(root: TreeNode | null): number { + if (root === null) return 0; + let midVal: number = 0; + if ( + root.left !== null && + root.left.left === null && + root.left.right === null + ) { + midVal = root.left.val; + } + let leftVal: number = sumOfLeftLeaves(root.left); + let rightVal: number = sumOfLeftLeaves(root.right); + return midVal + leftVal + rightVal; +}; +``` + +> 迭代法 + +```typescript +function sumOfLeftLeaves(root: TreeNode | null): number { + let helperStack: TreeNode[] = []; + let tempNode: TreeNode; + let sum: number = 0; + if (root !== null) helperStack.push(root); + while (helperStack.length > 0) { + tempNode = helperStack.pop()!; + if ( + tempNode.left !== null && + tempNode.left.left === null && + tempNode.left.right === null + ) { + sum += tempNode.left.val; + } + if (tempNode.right !== null) helperStack.push(tempNode.right); + if (tempNode.left !== null) helperStack.push(tempNode.left); + } + return sum; +}; +``` + +### Swift: + +**递归法** +```swift +func sumOfLeftLeaves(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + + let leftValue = sumOfLeftLeaves(root.left) + let rightValue = sumOfLeftLeaves(root.right) + + var midValue: Int = 0 + if root.left != nil && root.left?.left == nil && root.left?.right == nil { + midValue = root.left!.val + } + + let sum = midValue + leftValue + rightValue + return sum +} +``` +**迭代法** +```swift +func sumOfLeftLeaves(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + + var stack = Array() + stack.append(root) + var sum = 0 + + while !stack.isEmpty { + let lastNode = stack.removeLast() + + if lastNode.left != nil && lastNode.left?.left == nil && lastNode.left?.right == nil { + sum += lastNode.left!.val + } + if let right = lastNode.right { + stack.append(right) + } + if let left = lastNode.left { + stack.append(left) + } + } + return sum +} +``` + +### C: +递归法: +```c +int sumOfLeftLeaves(struct TreeNode* root){ + // 递归结束条件:若当前结点为空,返回0 + if(!root) + return 0; + + // 递归取左子树的左结点和和右子树的左结点和 + int leftValue = sumOfLeftLeaves(root->left); + int rightValue = sumOfLeftLeaves(root->right); + + // 若当前结点的左结点存在,且其为叶子结点。取它的值 + int midValue = 0; + if(root->left && (!root->left->left && !root->left->right)) + midValue = root->left->val; + + return leftValue + rightValue + midValue; +} +``` + +迭代法: +```c +int sumOfLeftLeaves(struct TreeNode* root){ + struct TreeNode* stack[1000]; + int stackTop = 0; + + // 若传入root结点不为空,将其入栈 + if(root) + stack[stackTop++] = root; + + int sum = 0; + //若栈不为空,进行循环 + while(stackTop) { + // 出栈栈顶元素 + struct TreeNode *topNode = stack[--stackTop]; + // 若栈顶元素的左孩子为左叶子结点,将其值加入sum中 + if(topNode->left && (!topNode->left->left && !topNode->left->right)) + sum += topNode->left->val; + + // 若当前栈顶结点有左右孩子。将他们加入栈中进行遍历 + if(topNode->right) + stack[stackTop++] = topNode->right; + if(topNode->left) + stack[stackTop++] = topNode->left; + } + return sum; +} +``` + +### Scala: + +**递归:** +```scala +object Solution { + def sumOfLeftLeaves(root: TreeNode): Int = { + if(root == null) return 0 + var midValue = 0 + if(root.left != null && root.left.left == null && root.left.right == null){ + midValue = root.left.value + } + // return关键字可以省略 + midValue + sumOfLeftLeaves(root.left) + sumOfLeftLeaves(root.right) + } +} +``` + +**迭代:** +```scala +object Solution { + import scala.collection.mutable + def sumOfLeftLeaves(root: TreeNode): Int = { + val stack = mutable.Stack[TreeNode]() + if (root == null) return 0 + stack.push(root) + var sum = 0 + while (!stack.isEmpty) { + val curNode = stack.pop() + if (curNode.left != null && curNode.left.left == null && curNode.left.right == null) { + sum += curNode.left.value // 如果满足条件就累加 + } + if (curNode.right != null) stack.push(curNode.right) + if (curNode.left != null) stack.push(curNode.left) + } + sum + } +} +``` + +### Rust: + +**递归** + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn sum_of_left_leaves(root: Option>>) -> i32 { + let mut res = 0; + if let Some(node) = root { + if let Some(left) = &node.borrow().left { + if left.borrow().right.is_none() && left.borrow().right.is_none() { + res += left.borrow().val; + } + } + res + Self::sum_of_left_leaves(node.borrow().left.clone()) + + Self::sum_of_left_leaves(node.borrow().right.clone()) + } else { + 0 + } + } +} +``` + +**迭代:** + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn sum_of_left_leaves(root: Option>>) -> i32 { + let mut res = 0; + let mut stack = vec![root]; + while !stack.is_empty() { + if let Some(node) = stack.pop().unwrap() { + if let Some(left) = &node.borrow().left { + if left.borrow().left.is_none() && left.borrow().right.is_none() { + res += left.borrow().val; + } + stack.push(Some(left.to_owned())); + } + if let Some(right) = &node.borrow().right { + stack.push(Some(right.to_owned())); + } + } + } + res + } +} +``` +### C# +```csharp +// 递归 +public int SumOfLeftLeaves(TreeNode root) +{ + if (root == null) return 0; + + int leftValue = SumOfLeftLeaves(root.left); + if (root.left != null && root.left.left == null && root.left.right == null) + { + leftValue += root.left.val; + } + int rightValue = SumOfLeftLeaves(root.right); + return leftValue + rightValue; + +} +``` + + diff --git "a/problems/0406.\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227.md" "b/problems/0406.\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227.md" new file mode 100755 index 0000000000..ce9d3bfb4e --- /dev/null +++ "b/problems/0406.\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227.md" @@ -0,0 +1,422 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 406.根据身高重建队列 + +[力扣题目链接](https://leetcode.cn/problems/queue-reconstruction-by-height/) + +假设有打乱顺序的一群人站成一个队列,数组 people 表示队列中一些人的属性(不一定按顺序)。每个 people[i] = [hi, ki] 表示第 i 个人的身高为 hi ,前面 正好 有 ki 个身高大于或等于 hi 的人。 + +请你重新构造并返回输入数组 people 所表示的队列。返回的队列应该格式化为数组 queue ,其中 queue[j] = [hj, kj] 是队列中第 j 个人的属性(queue[0] 是排在队列前面的人)。 + +示例 1: +* 输入:people = [[7,0],[4,4],[7,1],[5,0],[6,1],[5,2]] +* 输出:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] +* 解释: + * 编号为 0 的人身高为 5 ,没有身高更高或者相同的人排在他前面。 + * 编号为 1 的人身高为 7 ,没有身高更高或者相同的人排在他前面。 + * 编号为 2 的人身高为 5 ,有 2 个身高更高或者相同的人排在他前面,即编号为 0 和 1 的人。 + * 编号为 3 的人身高为 6 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 + * 编号为 4 的人身高为 4 ,有 4 个身高更高或者相同的人排在他前面,即编号为 0、1、2、3 的人。 + * 编号为 5 的人身高为 7 ,有 1 个身高更高或者相同的人排在他前面,即编号为 1 的人。 + * 因此 [[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] 是重新构造后的队列。 + +示例 2: +* 输入:people = [[6,0],[5,0],[4,0],[3,2],[2,2],[1,4]] +* 输出:[[4,0],[5,0],[2,2],[3,2],[1,4],[6,0]] + +提示: + +* 1 <= people.length <= 2000 +* 0 <= hi <= 10^6 +* 0 <= ki < people.length + +题目数据确保队列可以被重建 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,不要两边一起贪,会顾此失彼 | LeetCode:406.根据身高重建队列](https://www.bilibili.com/video/BV1EA411675Y),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后再按照另一个维度重新排列。 + +其实如果大家认真做了[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html),就会发现和此题有点点的像。 + +在[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html)我就强调过一次,遇到两个维度权衡的时候,一定要先确定一个维度,再确定另一个维度。 + +**如果两个维度一起考虑一定会顾此失彼**。 + +对于本题相信大家困惑的点是先确定k还是先确定h呢,也就是究竟先按h排序呢,还是先按照k排序呢? + +如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。 + +那么按照身高h来排序呢,身高一定是从大到小排(身高相同的话则k小的站前面),让高个子在前面。 + +**此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!** + +那么只需要按照k为下标重新插入队列就可以了,为什么呢? + +以图中{5,2} 为例: + +![406.根据身高重建队列](https://file1.kamacoder.com/i/algo/20201216201851982.png) + + +按照身高排序之后,优先按身高高的people的k来插入,后序插入节点也不会影响前面已经插入的节点,最终按照k的规则完成了队列。 + +所以在按照身高从大到小排序后: + +**局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性** + +**全局最优:最后都做完插入操作,整个队列满足题目队列属性** + +局部最优可推出全局最优,找不出反例,那就试试贪心。 + +一些同学可能也会疑惑,你怎么知道局部最优就可以推出全局最优呢? 有数学证明么? + +在贪心系列开篇词[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html)中,我已经讲过了这个问题了。 + +刷题或者面试的时候,手动模拟一下感觉可以局部最优推出整体最优,而且想不到反例,那么就试一试贪心,至于严格的数学证明,就不在讨论范围内了。 + +如果没有读过[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html)的同学建议读一下,相信对贪心就有初步的了解了。 + +回归本题,整个插入过程如下: + +排序完的people: +[[7,0], [7,1], [6,1], [5,0], [5,2], [4,4]] + +插入的过程: +* 插入[7,0]:[[7,0]] +* 插入[7,1]:[[7,0],[7,1]] +* 插入[6,1]:[[7,0],[6,1],[7,1]] +* 插入[5,0]:[[5,0],[7,0],[6,1],[7,1]] +* 插入[5,2]:[[5,0],[7,0],[5,2],[6,1],[7,1]] +* 插入[4,4]:[[5,0],[7,0],[5,2],[6,1],[4,4],[7,1]] + +此时就按照题目的要求完成了重新排列。 + +C++代码如下: + +```CPP +// 版本一 +class Solution { +public: + static bool cmp(const vector& a, const vector& b) { + if (a[0] == b[0]) return a[1] < b[1]; + return a[0] > b[0]; + } + vector> reconstructQueue(vector>& people) { + sort (people.begin(), people.end(), cmp); + vector> que; + for (int i = 0; i < people.size(); i++) { + int position = people[i][1]; + que.insert(que.begin() + position, people[i]); + } + return que; + } +}; +``` +* 时间复杂度:O(nlog n + n^2) +* 空间复杂度:O(n) + +但使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。 + +所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。 + +改成链表之后,C++代码如下: + +```CPP +// 版本二 +class Solution { +public: + // 身高从大到小排(身高相同k小的站前面) + static bool cmp(const vector& a, const vector& b) { + if (a[0] == b[0]) return a[1] < b[1]; + return a[0] > b[0]; + } + vector> reconstructQueue(vector>& people) { + sort (people.begin(), people.end(), cmp); + list> que; // list底层是链表实现,插入效率比vector高的多 + for (int i = 0; i < people.size(); i++) { + int position = people[i][1]; // 插入到下标为position的位置 + std::list>::iterator it = que.begin(); + while (position--) { // 寻找在插入位置 + it++; + } + que.insert(it, people[i]); + } + return vector>(que.begin(), que.end()); + } +}; +``` + +* 时间复杂度:O(nlog n + n^2) +* 空间复杂度:O(n) + +大家可以把两个版本的代码提交一下试试,就可以发现其差别了! + +关于本题使用数组还是使用链表的性能差异,我在[贪心算法:根据身高重建队列(续集)](https://programmercarl.com/根据身高重建队列(vector原理讲解).html)中详细讲解了一波 + +## 总结 + +关于出现两个维度一起考虑的情况,我们已经做过两道题目了,另一道就是[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html)。 + +**其技巧都是确定一边然后贪心另一边,两边一起考虑,就会顾此失彼**。 + +这道题目可以说比[135. 分发糖果](https://programmercarl.com/0135.分发糖果.html)难不少,其贪心的策略也是比较巧妙。 + +最后我给出了两个版本的代码,可以明显看是使用C++中的list(底层链表实现)比vector(数组)效率高得多。 + +**对使用某一种语言容器的使用,特性的选择都会不同程度上影响效率**。 + +所以很多人都说写算法题用什么语言都可以,主要体现在算法思维上,其实我是同意的但也不同意。 + +对于看别人题解的同学,题解用什么语言其实影响不大,只要题解把所使用语言特性优化的点讲出来,大家都可以看懂,并使用自己语言的时候注意一下。 + +对于写题解的同学,刷题用什么语言影响就非常大,如果自己语言没有学好而强调算法和编程语言没关系,其实是会误伤别人的。 + +**这也是我为什么统一使用C++写题解的原因** + + +## 其他语言版本 + + +### Java +```java +class Solution { + public int[][] reconstructQueue(int[][] people) { + // 身高从大到小排(身高相同k小的站前面) + Arrays.sort(people, (a, b) -> { + if (a[0] == b[0]) return a[1] - b[1]; // a - b 是升序排列,故在a[0] == b[0]的狀況下,會根據k值升序排列 + return b[0] - a[0]; //b - a 是降序排列,在a[0] != b[0],的狀況會根據h值降序排列 + }); + + LinkedList que = new LinkedList<>(); + + for (int[] p : people) { + que.add(p[1],p); //Linkedlist.add(index, value),會將value插入到指定index裡。 + } + + return que.toArray(new int[people.length][]); + } +} +``` + +### Python +```python +class Solution: + def reconstructQueue(self, people: List[List[int]]) -> List[List[int]]: + # 先按照h维度的身高顺序从高到低排序。确定第一个维度 + # lambda返回的是一个元组:当-x[0](维度h)相同时,再根据x[1](维度k)从小到大排序 + people.sort(key=lambda x: (-x[0], x[1])) + que = [] + + # 根据每个元素的第二个维度k,贪心算法,进行插入 + # people已经排序过了:同一高度时k值小的排前面。 + for p in people: + que.insert(p[1], p) + return que +``` + +### Go +```go +func reconstructQueue(people [][]int) [][]int { + // 先将身高从大到小排序,确定最大个子的相对位置 + sort.Slice(people, func(i, j int) bool { + if people[i][0] == people[j][0] { + return people[i][1] < people[j][1] // 当身高相同时,将K按照从小到大排序 + } + return people[i][0] > people[j][0] // 身高按照由大到小的顺序来排 + }) + + // 再按照K进行插入排序,优先插入K小的 + for i, p := range people { + copy(people[p[1]+1 : i+1], people[p[1] : i+1]) // 空出一个位置 + people[p[1]] = p + } + return people +} +``` +```go +// 链表实现 +func reconstructQueue(people [][]int) [][]int { + sort.Slice(people,func (i,j int) bool { + if people[i][0] == people[j][0] { + return people[i][1] < people[j][1] //当身高相同时,将K按照从小到大排序 + } + return people[i][0] > people[j][0] + }) + l := list.New() //创建链表 + for i:=0; i < len(people); i++ { + position := people[i][1] + mark := l.PushBack(people[i]) //插入元素 + e := l.Front() + for position != 0 { //获取相对位置 + position-- + e = e.Next() + } + l.MoveBefore(mark, e) //移动位置 + + } + res := [][]int{} + for e := l.Front(); e != nil; e = e.Next() { + res = append(res, e.Value.([]int)) + } + return res +} +``` + +### JavaScript + +```Javascript +var reconstructQueue = function(people) { + let queue = [] + people.sort((a, b ) => { + if(b[0] !== a[0]) { + return b[0] - a[0] + } else { + return a[1] - b[1] + } + + }) + + for(let i = 0; i < people.length; i++) { + queue.splice(people[i][1], 0, people[i]) + } + return queue +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn reconstruct_queue(mut people: Vec>) -> Vec> { + let mut queue = vec![]; + people.sort_by(|a, b| { + if a[0] == b[0] { + return a[1].cmp(&b[1]); + } + b[0].cmp(&a[0]) + }); + queue.push(people[0].clone()); + for v in people.iter().skip(1) { + queue.insert(v[1] as usize, v.clone()); + } + queue + } +} +``` + +### C +```c +int cmp(const void *p1, const void *p2) { + int *pp1 = *(int**)p1; + int *pp2 = *(int**)p2; + // 若身高相同,则按照k从小到大排列 + // 若身高不同,按身高从大到小排列 + return pp1[0] == pp2[0] ? pp1[1] - pp2[1] : pp2[0] - pp1[0]; +} + +// 将start与end中间的元素都后移一位 +// start为将要新插入元素的位置 +void moveBack(int **people, int peopleSize, int start, int end) { + int i; + for(i = end; i > start; i--) { + people[i] = people[i-1]; + } +} + +int** reconstructQueue(int** people, int peopleSize, int* peopleColSize, int* returnSize, int** returnColumnSizes){ + int i; + // 将people按身高从大到小排列(若身高相同,按k从小到大排列) + qsort(people, peopleSize, sizeof(int*), cmp); + + for(i = 0; i < peopleSize; ++i) { + // people[i]要插入的位置 + int position = people[i][1]; + int *temp = people[i]; + // 将position到i中间的元素后移一位 + // 注:因为已经排好序,position不会比i大。(举例:排序后people最后一位元素最小,其可能的k最大值为peopleSize-2,小于此时的i) + moveBack(people, peopleSize, position, i); + // 将temp放置到position处 + people[position] = temp; + + } + + + // 设置返回二维数组的大小以及里面每个一维数组的长度 + *returnSize = peopleSize; + *returnColumnSizes = (int*)malloc(sizeof(int) * peopleSize); + for(i = 0; i < peopleSize; ++i) { + (*returnColumnSizes)[i] = 2; + } + return people; +} +``` + +### TypeScript + +```typescript +function reconstructQueue(people: number[][]): number[][] { + people.sort((a, b) => { + if (a[0] === b[0]) return a[1] - b[1]; + return b[0] - a[0]; + }); + const resArr: number[][] = []; + for (let i = 0, length = people.length; i < length; i++) { + resArr.splice(people[i][1], 0, people[i]); + } + return resArr; +}; +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def reconstructQueue(people: Array[Array[Int]]): Array[Array[Int]] = { + val person = people.sortWith((a, b) => { + if (a(0) == b(0)) a(1) < b(1) + else a(0) > b(0) + }) + + var que = mutable.ArrayBuffer[Array[Int]]() + + for (per <- person) { + que.insert(per(1), per) + } + + que.toArray + } +} +``` +### C# +```csharp +public class Solution +{ + public int[][] ReconstructQueue(int[][] people) + { + Array.Sort(people, (a, b) => + { + if (a[0] == b[0]) + { + return a[1] - b[1]; + } + return b[0] - a[0]; + }); + var res = new List(); + for (int i = 0; i < people.Length; i++) + { + res.Insert(people[i][1], people[i]); + } + return res.ToArray(); + } +} +``` + + + diff --git "a/problems/0416.\345\210\206\345\211\262\347\255\211\345\222\214\345\255\220\351\233\206.md" "b/problems/0416.\345\210\206\345\211\262\347\255\211\345\222\214\345\255\220\351\233\206.md" new file mode 100755 index 0000000000..75bc5d0e10 --- /dev/null +++ "b/problems/0416.\345\210\206\345\211\262\347\255\211\345\222\214\345\255\220\351\233\206.md" @@ -0,0 +1,801 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 416. 分割等和子集 + +[力扣题目链接](https://leetcode.cn/problems/partition-equal-subset-sum/) + +题目难易:中等 + +给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +注意: +每个数组中的元素不会超过 100 +数组的大小不会超过 200 + +示例 1: +* 输入: [1, 5, 11, 5] +* 输出: true +* 解释: 数组可以分割成 [1, 5, 5] 和 [11]. + +示例 2: +* 输入: [1, 2, 3, 5] +* 输出: false +* 解释: 数组不能分割成两个元素和相等的子集. + +提示: +* 1 <= nums.length <= 200 +* 1 <= nums[i] <= 100 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划之背包问题,这个包能装满吗?| LeetCode:416.分割等和子集](https://www.bilibili.com/video/BV1rt4y1N7jE/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目初步看,和如下两题几乎是一样的,大家可以用回溯法,解决如下两题 + +* 698.划分为k个相等的子集 +* 473.火柴拼正方形 + +这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。 + +本题是可以用回溯暴力搜索出所有答案的,但最后超时了,也不想再优化了,放弃回溯。 + +是否有其他解法可以解决此题。 + +本题的本质是,能否把容量为 sum / 2的背包装满。 + +**这是 背包算法可以解决的经典类型题目**。 + +如果对01背包不够了解,建议仔细看完如下两篇: + +* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) + +## 01背包问题 + +01背包问题,大家都知道,有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。 + +**背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。** + +要注意题目描述中商品是不是可以重复放入。 + +**即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。** + +**元素我们只能用一次,如果使用背包,那么也是01背包** + +回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。 + +既有一个 只能装重量为 sum / 2 的背包,商品为数字,这些数字能不能把 这个背包装满。 + +那每一件商品是数字的话,对应的重量 和 价值是多少呢? + +一个数字只有一个维度,即 重量等于价值。 + +当数字 可以装满 承载重量为 sum / 2 的背包的背包时,这个背包的价值也是 sum / 2。 + +那么这道题就是 装满 承载重量为 sum / 2 的背包,价值最大是多少? + +如果最大价值是 sum / 2,说明正好被商品装满了。 + +因为商品是数字,重量和对应的价值是相同的。 + +以上分析完,我们就可以直接用01背包 来解决这个问题了。 + +动规五部曲分析如下: + +### 1. 确定dp数组以及下标的含义 + +01背包中,dp[j] 表示: 容量(所能装的重量)为j的背包,所背的物品价值最大可以为dp[j]。 + +如果背包所载重量为target, dp[target]就是装满 背包之后的总价值,因为 本题中每一个元素的数值既是重量,也是价值,所以,当 dp[target] == target 的时候,背包就装满了。 + +有录友可能想,那还有装不满的时候? + +拿输入数组 [1, 5, 11, 5],举例, dp[7] 只能等于 6,因为 只能放进 1 和 5。 + +而dp[6] 就可以等于6了,放进1 和 5,那么dp[6] == 6,说明背包装满了。 + +### 2. 确定递推公式 + +01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + +本题,相当于背包里放入数值,那么物品i的重量是nums[i],其价值也是nums[i]。 + +所以递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); + + +### 3. dp数组如何初始化 + +在01背包,一维dp如何初始化,已经讲过, + +从dp[j]的定义来看,首先dp[0]一定是0。 + +如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。 + +**这样才能让dp数组在递推的过程中取得最大的价值,而不是被初始值覆盖了**。 + +本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。 + +代码如下: + +```CPP +// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 +// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了 +vector dp(10001, 0); +``` + +### 4. 确定遍历顺序 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历! + +代码如下: + +```CPP +// 开始 01背包 +for(int i = 0; i < nums.size(); i++) { + for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历 + dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); + } +} +``` + +### 5. 举例推导dp数组 + +dp[j]的数值一定是小于等于j的。 + +**如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。** + +用例1,输入[1,5,11,5] 为例,如图: + + +![416.分割等和子集2](https://file1.kamacoder.com/i/algo/20210110104240545.png) + +最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +综上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + bool canPartition(vector& nums) { + int sum = 0; + + // dp[i]中的i表示背包内总和 + // 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 + // 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了 + vector dp(10001, 0); + for (int i = 0; i < nums.size(); i++) { + sum += nums[i]; + } + // 也可以使用库函数一步求和 + // int sum = accumulate(nums.begin(), nums.end(), 0); + if (sum % 2 == 1) return false; + int target = sum / 2; + + // 开始 01背包 + for(int i = 0; i < nums.size(); i++) { + for(int j = target; j >= nums[i]; j--) { // 每一个元素一定是不可重复放入,所以从大到小遍历 + dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); + } + } + // 集合中的元素正好可以凑成总和target + if (dp[target] == target) return true; + return false; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n),虽然dp数组大小为一个常数,但是大常数 + +## 总结 + +这道题目就是一道01背包经典应用类的题目,需要我们拆解题目,然后才能发现可以使用01背包。 + +01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。 + +做完本题后,需要大家清晰:背包问题,不仅可以求 背包能被的最大价值,还可以求这个背包是否可以装满。 + +## 其他语言版本 + + +### Java: +```Java +class Solution { + public boolean canPartition(int[] nums) { + if(nums == null || nums.length == 0) return false; + int n = nums.length; + int sum = 0; + for(int num : nums) { + sum += num; + } + //总和为奇数,不能平分 + if(sum % 2 != 0) return false; + int target = sum / 2; + int[] dp = new int[target + 1]; + for(int i = 0; i < n; i++) { + for(int j = target; j >= nums[i]; j--) { + //物品 i 的重量是 nums[i],其价值也是 nums[i] + dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]); + } + + //剪枝一下,每一次完成內層的for-loop,立即檢查是否dp[target] == target,優化時間複雜度(26ms -> 20ms) + if(dp[target] == target) + return true; + } + return dp[target] == target; + } +} +``` + +二维数组版本(易于理解): +```java +public class Solution { + public static void main(String[] args) { + int num[] = {1,5,11,5}; + canPartition(num); + + } + public static boolean canPartition(int[] nums) { + int len = nums.length; + // 题目已经说非空数组,可以不做非空判断 + int sum = 0; + for (int num : nums) { + sum += num; + } + // 特判:如果是奇数,就不符合要求 + if ((sum %2 ) != 0) { + return false; + } + + int target = sum / 2; //目标背包容量 + // 创建二维状态数组,行:物品索引,列:容量(包括 0) + /* + dp[i][j]表示从数组的 [0, i] 这个子区间内挑选一些正整数 + 每个数只能用一次,使得这些数的和恰好等于 j。 + */ + boolean[][] dp = new boolean[len][target + 1]; + + // 先填表格第 0 行,第 1 个数只能让容积为它自己的背包恰好装满 (这里的dp[][]数组的含义就是“恰好”,所以就算容积比它大的也不要) + if (nums[0] <= target) { + dp[0][nums[0]] = true; + } + // 再填表格后面几行 + //外层遍历物品 + for (int i = 1; i < len; i++) { + //内层遍历背包 + for (int j = 0; j <= target; j++) { + // 直接从上一行先把结果抄下来,然后再修正 + dp[i][j] = dp[i - 1][j]; + + //如果某个物品单独的重量恰好就等于背包的重量,那么也是满足dp数组的定义的 + if (nums[i] == j) { + dp[i][j] = true; + continue; + } + //如果某个物品的重量小于j,那就可以看该物品是否放入背包 + //dp[i - 1][j]表示该物品不放入背包,如果在 [0, i - 1] 这个子区间内已经有一部分元素,使得它们的和为 j ,那么 dp[i][j] = true; + //dp[i - 1][j - nums[i]]表示该物品放入背包。如果在 [0, i - 1] 这个子区间内就得找到一部分元素,使得它们的和为 j - nums[i]。 + if (nums[i] < j) { + dp[i][j] = dp[i - 1][j] || dp[i - 1][j - nums[i]]; + } + } + } + for (int i = 0; i < len; i++) { + for (int j = 0; j <= target; j++) { + System.out.print(dp[i][j]+" "); + } + System.out.println(); + } + return dp[len - 1][target]; + } +} +//dp数组的打印结果 +false true false false false false false false false false false false +false true false false false true true false false false false false +false true false false false true true false false false false true +false true false false false true true false false false true true +``` +二維數組整數版本 +```Java +class Solution { + public boolean canPartition(int[] nums) { + //using 2-D DP array. + int len = nums.length; + //check edge cases; + if(len == 0) + return false; + + int sum = 0; + for (int num : nums) + sum += num; + //we only deal with even numbers. If sum is odd, return false; + if(sum % 2 == 1) + return false; + + int target = sum / 2; + int[][] dp = new int[nums.length][target + 1]; + + // for(int j = 0; j <= target; j++){ + // if(j < nums[0]) + // dp[0][j] = 0; + // else + // dp[0][j] = nums[0]; + // } + + //initialize dp array + for(int j = nums[0]; j <= target; j++){ + dp[0][j] = nums[0]; + } + + for(int i = 1; i < len; i++){ + for(int j = 0; j <= target; j++){ + if (j < nums[i]) + dp[i][j] = dp[i - 1][j]; + else + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]); + } + } + + //print out DP array + // for(int x : dp){ + // System.out.print(x + ","); + // } + // System.out.print(" "+i+" row"+"\n"); + return dp[len - 1][target] == target; + } +} +//dp数组的打印结果 for test case 1. +0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, +0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 6, +0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 11, +0, 1, 1, 1, 1, 5, 6, 6, 6, 6, 10, 11, +``` + +### Python: +卡哥版 +```python +class Solution: + def canPartition(self, nums: List[int]) -> bool: + _sum = 0 + + # dp[i]中的i表示背包内总和 + # 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 + # 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了 + dp = [0] * 10001 + for num in nums: + _sum += num + # 也可以使用内置函数一步求和 + # _sum = sum(nums) + if _sum % 2 == 1: + return False + target = _sum // 2 + + # 开始 0-1背包 + for num in nums: + for j in range(target, num - 1, -1): # 每一个元素一定是不可重复放入,所以从大到小遍历 + dp[j] = max(dp[j], dp[j - num] + num) + + # 集合中的元素正好可以凑成总和target + if dp[target] == target: + return True + return False + +``` + +卡哥版(简化版) +```python +class Solution: + def canPartition(self, nums: List[int]) -> bool: + if sum(nums) % 2 != 0: + return False + target = sum(nums) // 2 + dp = [0] * (target + 1) + for num in nums: + for j in range(target, num-1, -1): + dp[j] = max(dp[j], dp[j-num] + num) + return dp[-1] == target + +``` +二维DP版 +```python +class Solution: + def canPartition(self, nums: List[int]) -> bool: + + total_sum = sum(nums) + + if total_sum % 2 != 0: + return False + + target_sum = total_sum // 2 + dp = [[False] * (target_sum + 1) for _ in range(len(nums) + 1)] + + # 初始化第一行(空子集可以得到和为0) + for i in range(len(nums) + 1): + dp[i][0] = True + + for i in range(1, len(nums) + 1): + for j in range(1, target_sum + 1): + if j < nums[i - 1]: + # 当前数字大于目标和时,无法使用该数字 + 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[len(nums)][target_sum] + +``` +一维DP版 +```python +class Solution: + def canPartition(self, nums: List[int]) -> bool: + + total_sum = sum(nums) + + if total_sum % 2 != 0: + return False + + target_sum = total_sum // 2 + dp = [False] * (target_sum + 1) + dp[0] = True + + for num in nums: + # 从target_sum逆序迭代到num,步长为-1 + for i in range(target_sum, num - 1, -1): + dp[i] = dp[i] or dp[i - num] + return dp[target_sum] + + +``` + +### Go: +一维dp +```go +// 分割等和子集 动态规划 +// 时间复杂度O(n^2) 空间复杂度O(n) +func canPartition(nums []int) bool { + sum := 0 + for _, num := range nums { + sum += num + } + // 如果 nums 的总和为奇数则不可能平分成两个子集 + if sum % 2 == 1 { + return false + } + + target := sum / 2 + dp := make([]int, target + 1) + + for _, num := range nums { + for j := target; j >= num; j-- { + if dp[j] < dp[j - num] + num { + dp[j] = dp[j - num] + num + } + } + } + return dp[target] == target +} +``` + +二维dp +```go +func canPartition(nums []int) bool { + sum := 0 + for _, val := range nums { + sum += val + } + if sum % 2 == 1 { + return false + } + target := sum / 2 + dp := make([][]int, len(nums)) + for i := range dp { + dp[i] = make([]int, target + 1) + } + for j := nums[0]; j <= target; j++ { + dp[0][j] = nums[0] + } + for i := 1; i < len(nums); i++ { + for j := 0; j <= target; j++ { + if j < nums[i] { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = max(dp[i-1][j], dp[i-1][j-nums[i]] + nums[i]) + } + } + } + return dp[len(nums)-1][target] == target +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +### JavaScript: + +```js +var canPartition = function(nums) { + const sum = (nums.reduce((p, v) => p + v)); + if (sum & 1) return false; + const dp = Array(sum / 2 + 1).fill(0); + for(let i = 0; i < nums.length; i++) { + for(let j = sum / 2; j >= nums[i]; j--) { + dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]); + if (dp[j] === sum / 2) { + return true; + } + } + } + return dp[sum / 2] === sum / 2; +}; +``` + + +### Rust: + +```Rust +impl Solution { + pub fn can_partition(nums: Vec) -> bool { + let sum = nums.iter().sum::() as usize; + if sum % 2 == 1 { + return false; + } + let target = sum / 2; + let mut dp = vec![0; target + 1]; + for n in nums { + for j in (n as usize..=target).rev() { + dp[j] = dp[j].max(dp[j - n as usize] + n) + } + } + if dp[target] == target as i32 { + return true; + } + false + } +} +``` + + +### C: + +二维dp: +```c +/** +1. dp数组含义:dp[i][j]为背包重量为j时,从[0-i]元素和最大值 +2. 递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]) +3. 初始化:dp[i][0]初始化为0。因为背包重量为0时,不可能放入元素。dp[0][j] = nums[0],当j >= nums[0] && j < target时 +4. 遍历顺序:先遍历物品,再遍历背包 +*/ +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + +int getSum(int* nums, int numsSize) { + int sum = 0; + + int i; + for(i = 0; i < numsSize; ++i) { + sum += nums[i]; + } + return sum; +} + +bool canPartition(int* nums, int numsSize){ + // 求出元素总和 + int sum = getSum(nums, numsSize); + // 若元素总和为奇数,则不可能得到两个和相等的子数组 + if(sum % 2) + return false; + + // 若子数组的和等于target,则nums可以被分割 + int target = sum / 2; + // 初始化dp数组 + int dp[numsSize][target + 1]; + // dp[j][0]都应被设置为0。因为当背包重量为0时,不可放入元素 + memset(dp, 0, sizeof(int) * numsSize * (target + 1)); + + int i, j; + // 当背包重量j大于nums[0]时,可以在dp[0][j]中放入元素nums[0] + for(j = nums[0]; j <= target; ++j) { + dp[0][j] = nums[0]; + } + + for(i = 1; i < numsSize; ++i) { + for(j = 1; j <= target; ++j) { + // 若当前背包重量j小于nums[i],则其值等于只考虑0到i-1物品时的值 + if(j < nums[i]) + dp[i][j] = dp[i - 1][j]; + // 否则,背包重量等于在背包中放入num[i]/不放入nums[i]的较大值 + else + dp[i][j] = MAX(dp[i - 1][j], dp[i - 1][j - nums[i]] + nums[i]); + } + } + // 判断背包重量为target,且考虑到所有物品时,放入的元素和是否等于target + return dp[numsSize - 1][target] == target; +} +``` +滚动数组: +```c +/** +1. dp数组含义:dp[j]为背包重量为j时,其中可放入元素的最大值 +2. 递推公式:dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]) +3. 初始化:均初始化为0即可 +4. 遍历顺序:先遍历物品,再后序遍历背包 +*/ +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + +int getSum(int* nums, int numsSize) { + int sum = 0; + + int i; + for(i = 0; i < numsSize; ++i) { + sum += nums[i]; + } + return sum; +} + +bool canPartition(int* nums, int numsSize){ + // 求出元素总和 + int sum = getSum(nums, numsSize); + // 若元素总和为奇数,则不可能得到两个和相等的子数组 + if(sum % 2) + return false; + // 背包容量 + int target = sum / 2; + + // 初始化dp数组,元素均为0 + int dp[target + 1]; + memset(dp, 0, sizeof(int) * (target + 1)); + + int i, j; + // 先遍历物品,后遍历背包 + for(i = 0; i < numsSize; ++i) { + for(j = target; j >= nums[i]; --j) { + dp[j] = MAX(dp[j], dp[j - nums[i]] + nums[i]); + } + } + + // 查看背包容量为target时,元素总和是否等于target + return dp[target] == target; +} +``` + +### TypeScript: + +> 一维数组,简洁 + +```typescript +function canPartition(nums: number[]): boolean { + const sum: number = nums.reduce((pre, cur) => pre + cur); + if (sum % 2 === 1) return false; + const bagSize: number = sum / 2; + const goodsNum: number = nums.length; + const dp: number[] = new Array(bagSize + 1).fill(0); + for (let i = 0; i < goodsNum; i++) { + for (let j = bagSize; j >= nums[i]; j--) { + dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]); + } + } + return dp[bagSize] === bagSize; +}; +``` + +> 二维数组,易懂 + +```typescript +function canPartition(nums: number[]): boolean { + /** + weightArr = nums; + valueArr = nums; + bagSize = sum / 2; (sum为nums各元素总和); + 按照0-1背包处理 + */ + const sum: number = nums.reduce((pre, cur) => pre + cur); + if (sum % 2 === 1) return false; + const bagSize: number = sum / 2; + const weightArr: number[] = nums; + const valueArr: number[] = nums; + const goodsNum: number = weightArr.length; + const dp: number[][] = new Array(goodsNum) + .fill(0) + .map(_ => new Array(bagSize + 1).fill(0)); + for (let i = weightArr[0]; i <= bagSize; i++) { + dp[0][i] = valueArr[0]; + } + for (let i = 1; i < goodsNum; i++) { + for (let j = 0; j <= bagSize; j++) { + if (j < weightArr[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weightArr[i]] + valueArr[i]); + } + } + } + return dp[goodsNum - 1][bagSize] === bagSize; +}; +``` + +### Scala: + +滚动数组: +```scala +object Solution { + def canPartition(nums: Array[Int]): Boolean = { + var sum = nums.sum + if (sum % 2 != 0) return false + var half = sum / 2 + var dp = new Array[Int](half + 1) + + // 遍历 + for (i <- 0 until nums.length; j <- half to nums(i) by -1) { + dp(j) = math.max(dp(j), dp(j - nums(i)) + nums(i)) + } + + if (dp(half) == half) true else false + } +} +``` + +二维数组: +```scala +object Solution { + def canPartition(nums: Array[Int]): Boolean = { + var sum = nums.sum + if (sum % 2 != 0) return false + var half = sum / 2 + var dp = Array.ofDim[Int](nums.length, half + 1) + + // 初始化 + for (j <- nums(0) to half) dp(0)(j) = nums(0) + + // 遍历 + for (i <- 1 until nums.length; j <- 1 to half) { + if (j - nums(i) >= 0) dp(i)(j) = nums(i) + dp(i - 1)(j - nums(i)) + dp(i)(j) = math.max(dp(i)(j), dp(i - 1)(j)) + } + + // 如果等于half就返回ture,否则返回false + if (dp(nums.length - 1)(half) == half) true else false + } +} +``` +### C# +```csharp +public class Solution +{ + public bool CanPartition(int[] nums) + { + int sum = 0; + int[] dp = new int[10001]; + foreach (int num in nums) + { + sum += num; + } + if (sum % 2 == 1) return false; + int tartget = sum / 2; + for (int i = 0; i < nums.Length; i++) + { + for (int j = tartget; j >= nums[i]; j--) + { + dp[j] = Math.Max(dp[j], dp[j - nums[i]] + nums[i]); + } + } + if (dp[tartget] == tartget) + return true; + + return false; + + } +} +``` + diff --git "a/problems/0417.\345\244\252\345\271\263\346\264\213\345\244\247\350\245\277\346\264\213\346\260\264\346\265\201\351\227\256\351\242\230.md" "b/problems/0417.\345\244\252\345\271\263\346\264\213\345\244\247\350\245\277\346\264\213\346\260\264\346\265\201\351\227\256\351\242\230.md" new file mode 100755 index 0000000000..c9494313a1 --- /dev/null +++ "b/problems/0417.\345\244\252\345\271\263\346\264\213\345\244\247\350\245\277\346\264\213\346\260\264\346\265\201\351\227\256\351\242\230.md" @@ -0,0 +1,837 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 417. 太平洋大西洋水流问题 + +[题目链接](https://leetcode.cn/problems/pacific-atlantic-water-flow/) + +有一个 m × n 的矩形岛屿,与 太平洋 和 大西洋 相邻。 “太平洋” 处于大陆的左边界和上边界,而 “大西洋” 处于大陆的右边界和下边界。 + +这个岛被分割成一个由若干方形单元格组成的网格。给定一个 m x n 的整数矩阵 heights , heights[r][c] 表示坐标 (r, c) 上单元格 高于海平面的高度 。 + +岛上雨水较多,如果相邻单元格的高度 小于或等于 当前单元格的高度,雨水可以直接向北、南、东、西流向相邻单元格。水可以从海洋附近的任何单元格流入海洋。 + +返回网格坐标 result 的 2D 列表 ,其中 result[i] = [ri, ci] 表示雨水从单元格 (ri, ci) 流动 既可流向太平洋也可流向大西洋 。 + +示例 1: + +![](https://file1.kamacoder.com/i/algo/20230129103212.png) + +* 输入: heights = [[1,2,2,3,5],[3,2,3,4,4],[2,4,5,3,1],[6,7,1,4,5],[5,1,1,2,4]] +* 输出: [[0,4],[1,3],[1,4],[2,2],[3,0],[3,1],[4,0]] + +示例 2: + +* 输入: heights = [[2,1],[1,2]] +* 输出: [[0,0],[0,1],[1,0],[1,1]] + +提示: + +* m == heights.length +* n == heights[r].length +* 1 <= m, n <= 200 +* 0 <= heights[r][c] <= 10^5 + + + +## 思路 + +不少同学可能被这道题的题目描述迷惑了,其实就是找到哪些点 可以同时到达太平洋和大西洋。 流动的方式只能从高往低流。 + +那么一个比较直白的想法,其实就是 遍历每个点,然后看这个点 能不能同时到达太平洋和大西洋。 + +至于遍历方式,可以用dfs,也可以用bfs,以下用dfs来举例。 + +那么这种思路的实现代码如下: + +```CPP +class Solution { +private: + int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; + void dfs(vector>& heights, vector>& visited, int x, int y) { + if (visited[x][y]) return; + + visited[x][y] = true; + + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= heights.size() || nexty < 0 || nexty >= heights[0].size()) continue; + if (heights[x][y] < heights[nextx][nexty]) continue; // 高度不合适 + + dfs (heights, visited, nextx, nexty); + } + return; + } + bool isResult(vector>& heights, int x, int y) { + vector> visited = vector>(heights.size(), vector(heights[0].size(), false)); + + // 深搜,将x,y出发 能到的节点都标记上。 + dfs(heights, visited, x, y); + bool isPacific = false; + bool isAtlantic = false; + + // 以下就是判断x,y出发,是否到达太平洋和大西洋 + for (int j = 0; j < heights[0].size(); j++) { + if (visited[0][j]) { + isPacific = true; + break; + } + } + for (int i = 0; i < heights.size(); i++) { + if (visited[i][0]) { + isPacific = true; + break; + } + } + for (int j = 0; j < heights[0].size(); j++) { + if (visited[heights.size() - 1][j]) { + isAtlantic = true; + break; + } + } + for (int i = 0; i < heights.size(); i++) { + if (visited[i][heights[0].size() - 1]) { + isAtlantic = true; + break; + } + } + if (isAtlantic && isPacific) return true; + return false; + } +public: + + vector> pacificAtlantic(vector>& heights) { + vector> result; + // 遍历每一个点,看是否能同时到达太平洋和大西洋 + for (int i = 0; i < heights.size(); i++) { + for (int j = 0; j < heights[0].size(); j++) { + if (isResult(heights, i, j)) result.push_back({i, j}); + } + } + return result; + } +}; + +``` + +这种思路很直白,但很明显,以上代码超时了。 来看看时间复杂度。 + +遍历每一个节点,是 m * n,遍历每一个节点的时候,都要做深搜,深搜的时间复杂度是: m * n + +那么整体时间复杂度 就是 O(m^2 * n^2) ,这是一个四次方的时间复杂度。 + +## 优化 + +那么我们可以 反过来想,从太平洋边上的节点 逆流而上,将遍历过的节点都标记上。 从大西洋的边上节点 逆流而长,将遍历过的节点也标记上。 然后两方都标记过的节点就是既可以流太平洋也可以流大西洋的节点。 + +从太平洋边上节点出发,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220722103029.png) + +从大西洋边上节点出发,如图: + +![图二](https://file1.kamacoder.com/i/algo/20220722103330.png) + +按照这样的逻辑,就可以写出如下遍历代码:(详细注释) + +(如果对dfs基础内容就不懂,建议看 [「代码随想录」DFS算法精讲!](https://programmercarl.com/图论深搜理论基础.html),还可以顺便解决 [797. 所有可能的路径](https://programmercarl.com/0797.%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E8%B7%AF%E5%BE%84.html)) + +```CPP +class Solution { +private: + int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 + + // 从低向高遍历,注意这里visited是引用,即可以改变传入的pacific和atlantic的值 + void dfs(vector>& heights, vector>& visited, int x, int y) { + if (visited[x][y]) return; + visited[x][y] = true; + for (int i = 0; i < 4; i++) { // 向四个方向遍历 + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + // 超过边界 + if (nextx < 0 || nextx >= heights.size() || nexty < 0 || nexty >= heights[0].size()) continue; + // 高度不合适,注意这里是从低向高判断 + if (heights[x][y] > heights[nextx][nexty]) continue; + + dfs (heights, visited, nextx, nexty); + } + return; + + } + +public: + + vector> pacificAtlantic(vector>& heights) { + vector> result; + int n = heights.size(); + int m = heights[0].size(); // 这里不用担心空指针,题目要求说了长宽都大于1 + + // 记录从太平洋边出发,可以遍历的节点 + vector> pacific = vector>(n, vector(m, false)); + + // 记录从大西洋出发,可以遍历的节点 + vector> atlantic = vector>(n, vector(m, false)); + + // 从最左最右列的节点出发,向高处遍历 + for (int i = 0; i < n; i++) { + dfs (heights, pacific, i, 0); // 遍历最左列,接触太平洋 + dfs (heights, atlantic, i, m - 1); // 遍历最右列,接触大西 + } + + // 从最上最下行的节点出发,向高处遍历 + for (int j = 0; j < m; j++) { + dfs (heights, pacific, 0, j); // 遍历最上行,接触太平洋 + dfs (heights, atlantic, n - 1, j); // 遍历最下行,接触大西洋 + } + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + // 如果这个节点,从太平洋和大西洋出发都遍历过,就是结果 + if (pacific[i][j] && atlantic[i][j]) result.push_back({i, j}); + } + } + return result; + } +}; + +``` + +时间复杂度分析, 关于dfs函数搜索的过程 时间复杂度是 O(n * m),这个大家比较容易想。 + +关键看主函数,那么每次dfs的时候,上面还是有for循环的。 + +第一个for循环,时间复杂度是:n * (n * m) 。 + +第二个for循环,时间复杂度是:m * (n * m)。 + +所以本题看起来 时间复杂度好像是 : n * (n * m) + m * (n * m) = (m * n) * (m + n) 。 + +其实这是一个误区,大家再自己看 dfs函数的实现,其实 有visited函数记录 走过的节点,而走过的节点是不会再走第二次的。 + +所以 调用dfs函数,**只要参数传入的是 数组pacific,那么地图中 每一个节点其实就遍历一次,无论你调用多少次**。 + +同理,调用 dfs函数,只要 参数传入的是 数组atlantic,地图中每个节点也只会遍历一次。 + +所以,以下这段代码的时间复杂度是 2 * n * m。 地图用每个节点就遍历了两次,参数传入pacific的时候遍历一次,参数传入atlantic的时候遍历一次。 +```CPP +// 从最上最下行的节点出发,向高处遍历 +for (int i = 0; i < n; i++) { + dfs (heights, pacific, i, 0); // 遍历最上行,接触太平洋 + dfs (heights, atlantic, i, m - 1); // 遍历最下行,接触大西洋 +} + +// 从最左最右列的节点出发,向高处遍历 +for (int j = 0; j < m; j++) { + dfs (heights, pacific, 0, j); // 遍历最左列,接触太平洋 + dfs (heights, atlantic, n - 1, j); // 遍历最右列,接触大西洋 +} +``` + +那么本题整体的时间复杂度其实是: 2 * n * m + n * m ,所以最终时间复杂度为 O(n * m) 。 + +空间复杂度为:O(n * m) 这个就不难理解了。开了几个 n * m 的数组。 + +## 其他语言版本 + +### Java + +深度优先遍历: + +```Java +class Solution { + // 四个位置 + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; + + /** + * @param heights 题目给定的二维数组 + * @param row 当前位置的行号 + * @param col 当前位置的列号 + * @param sign 记录是哪一条河,两条河中可以一个为 0,一个为 1 + * @param visited 记录这个位置可以到哪条河 + */ + public void dfs(int[][] heights, int row, int col, int sign, boolean[][][] visited) { + for (int[] current: position) { + int curRow = row + current[0], curCol = col + current[1]; + // 越界 + if (curRow < 0 || curRow >= heights.length || curCol < 0 || curCol >= heights[0].length) + continue; + // 高度不合适或者已经被访问过了 + if (heights[curRow][curCol] < heights[row][col] || visited[curRow][curCol][sign]) continue; + visited[curRow][curCol][sign] = true; + dfs(heights, curRow, curCol, sign, visited); + } + } + + public List> pacificAtlantic(int[][] heights) { + int rowSize = heights.length, colSize = heights[0].length; + List> ans = new ArrayList<>(); + // 记录 [row, col] 位置是否可以到某条河,可以为 true,反之为 false; + // 假设太平洋的标记为 1,大西洋为 0 + boolean[][][] visited = new boolean[rowSize][colSize][2]; + for (int row = 0; row < rowSize; row++) { + visited[row][colSize - 1][0] = true; + visited[row][0][1] = true; + dfs(heights, row, colSize - 1, 0, visited); + dfs(heights, row, 0, 1, visited); + } + for (int col = 0; col < colSize; col++) { + visited[rowSize - 1][col][0] = true; + visited[0][col][1] = true; + dfs(heights, rowSize - 1, col, 0, visited); + dfs(heights, 0, col, 1, visited); + } + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + // 如果该位置即可以到太平洋又可以到大西洋,就放入答案数组 + if (visited[row][col][0] && visited[row][col][1]) + ans.add(List.of(row, col)); + } + } + return ans; + } +} +``` + +```Java +class Solution { + + // 和Carl题解更加符合的Java DFS + private int[][] directions = {{-1, 0}, {1, 0}, {0, 1}, {0, -1}}; + + /** + * @param heights 题目给定的二维数组 + * @param m 当前位置的行号 + * @param n 当前位置的列号 + * @param visited 记录这个位置可以到哪条河 + */ + + public void dfs(int[][] heights, boolean[][] visited, int m, int n){ + if(visited[m][n]) return; + visited[m][n] = true; + + for(int[] dir: directions){ + int nextm = m + dir[0]; + int nextn = n + dir[1]; + //出了2D array的边界,continue + if(nextm < 0||nextm == heights.length||nextn <0||nextn== heights[0].length) continue; + //下一个位置比当下位置还要低,跳过,继续找下一个更高的位置 + if(heights[m][n] > heights[nextm][nextn]) continue; + dfs(heights, visited, nextm, nextn); + } + } + + + public List> pacificAtlantic(int[][] heights) { + int m = heights.length; + int n = heights[0].length; + + // 记录从太平洋边出发,可以遍历的节点 + boolean[][] pacific = new boolean[m][n]; + // 记录从大西洋出发,可以遍历的节点 + boolean[][] atlantic = new boolean[m][n]; + + // 从最左最右列的节点出发,向高处遍历 + for(int i = 0; i> result = new ArrayList<>(); + for(int a = 0; a pair = new ArrayList<>(); + pair.add(a); + pair.add(b); + result.add(pair); + } + } + } + return result; + } +} +``` + +广度优先遍历: + +```Java +class Solution { + // 四个位置 + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; + + /** + * @param heights 题目给定的二维数组 + * @param queue 记录可以到达边界的节点 + * @param visited 记录这个位置可以到哪条河 + */ + public void bfs(int[][] heights, Queue queue, boolean[][][] visited) { + while (!queue.isEmpty()) { + int[] curPos = queue.poll(); + for (int[] current: position) { + int row = curPos[0] + current[0], col = curPos[1] + current[1], sign = curPos[2]; + // 越界 + if (row < 0 || row >= heights.length || col < 0 || col >= heights[0].length) continue; + // 高度不合适或者已经被访问过了 + if (heights[row][col] < heights[curPos[0]][curPos[1]] || visited[row][col][sign]) continue; + visited[row][col][sign] = true; + queue.add(new int[]{row, col, sign}); + } + } + } + + public List> pacificAtlantic(int[][] heights) { + int rowSize = heights.length, colSize = heights[0].length; + List> ans = new ArrayList<>(); + boolean[][][] visited = new boolean[rowSize][colSize][2]; + // 队列,保存的数据为 [行号, 列号, 标记] + // 假设太平洋的标记为 1,大西洋为 0 + Queue queue = new ArrayDeque<>(); + for (int row = 0; row < rowSize; row++) { + visited[row][colSize - 1][0] = true; + visited[row][0][1] = true; + queue.add(new int[]{row, colSize - 1, 0}); + queue.add(new int[]{row, 0, 1}); + } + for (int col = 0; col < colSize; col++) { + visited[rowSize - 1][col][0] = true; + visited[0][col][1] = true; + queue.add(new int[]{rowSize - 1, col, 0}); + queue.add(new int[]{0, col, 1}); + } + bfs(heights, queue, visited); + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + // 如果该位置即可以到太平洋又可以到大西洋,就放入答案数组 + if (visited[row][col][0] && visited[row][col][1]) + ans.add(List.of(row, col)); + } + } + return ans; + } +} +``` + +### Python + +深度优先遍历 + +```Python3 +class Solution: + def __init__(self): + self.position = [[-1, 0], [0, 1], [1, 0], [0, -1]] # 四个方向 + + # heights:题目给定的二维数组, row:当前位置的行号, col:当前位置的列号 + # sign:记录是哪一条河,两条河中可以一个为 0,一个为 1 + # visited:记录这个位置可以到哪条河 + def dfs(self, heights: List[List[int]], row: int, col: int, sign: int, visited: List[List[List[int]]]): + for current in self.position: + curRow, curCol = row + current[0], col + current[1] + # 索引下标越界 + if curRow < 0 or curRow >= len(heights) or curCol < 0 or curCol >= len(heights[0]): continue + # 不满足条件或者已经被访问过 + if heights[curRow][curCol] < heights[row][col] or visited[curRow][curCol][sign]: continue + visited[curRow][curCol][sign] = True + self.dfs(heights, curRow, curCol, sign, visited) + + def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]: + rowSize, colSize = len(heights), len(heights[0]) + # visited 记录 [row, col] 位置是否可以到某条河,可以为 true,反之为 false; + # 假设太平洋的标记为 1,大西洋为 0 + # ans 用来保存满足条件的答案 + ans, visited = [], [[[False for _ in range(2)] for _ in range(colSize)] for _ in range(rowSize)] + for row in range(rowSize): + visited[row][0][1] = True + visited[row][colSize - 1][0] = True + self.dfs(heights, row, 0, 1, visited) + self.dfs(heights, row, colSize - 1, 0, visited) + for col in range(0, colSize): + visited[0][col][1] = True + visited[rowSize - 1][col][0] = True + self.dfs(heights, 0, col, 1, visited) + self.dfs(heights, rowSize - 1, col, 0, visited) + for row in range(rowSize): + for col in range(colSize): + # 如果该位置即可以到太平洋又可以到大西洋,就放入答案数组 + if visited[row][col][0] and visited[row][col][1]: + ans.append([row, col]) + return ans +``` + +广度优先遍历 + +```Python3 +class Solution: + def __init__(self): + self.position = [[-1, 0], [0, 1], [1, 0], [0, -1]] + + # heights:题目给定的二维数组,visited:记录这个位置可以到哪条河 + def bfs(self, heights: List[List[int]], queue: deque, visited: List[List[List[int]]]): + while queue: + curPos = queue.popleft() + for current in self.position: + row, col, sign = curPos[0] + current[0], curPos[1] + current[1], curPos[2] + # 越界 + if row < 0 or row >= len(heights) or col < 0 or col >= len(heights[0]): continue + # 不满足条件或已经访问过 + if heights[row][col] < heights[curPos[0]][curPos[1]] or visited[row][col][sign]: continue + visited[row][col][sign] = True + queue.append([row, col, sign]) + + def pacificAtlantic(self, heights: List[List[int]]) -> List[List[int]]: + rowSize, colSize = len(heights), len(heights[0]) + # visited 记录 [row, col] 位置是否可以到某条河,可以为 true,反之为 false; + # 假设太平洋的标记为 1,大西洋为 0 + # ans 用来保存满足条件的答案 + ans, visited = [], [[[False for _ in range(2)] for _ in range(colSize)] for _ in range(rowSize)] + # 队列,保存的数据为 [行号, 列号, 标记] + # 假设太平洋的标记为 1,大西洋为 0 + queue = deque() + for row in range(rowSize): + visited[row][0][1] = True + visited[row][colSize - 1][0] = True + queue.append([row, 0, 1]) + queue.append([row, colSize - 1, 0]) + for col in range(0, colSize): + visited[0][col][1] = True + visited[rowSize - 1][col][0] = True + queue.append([0, col, 1]) + queue.append([rowSize - 1, col, 0]) + self.bfs(heights, queue, visited) # 广度优先遍历 + for row in range(rowSize): + for col in range(colSize): + # 如果该位置即可以到太平洋又可以到大西洋,就放入答案数组 + if visited[row][col][0] and visited[row][col][1]: + ans.append([row, col]) + return ans +``` + +### JavaScript +```JavaScript +/** + * @description 深度搜索优先 + * @param {number[][]} heights + * @return {number[][]} + */ +function pacificAtlantic(heights) { + const dir = [[-1, 0], [0, -1], [1, 0], [0, 1]]; + const [rowSize, colSize] = [heights.length, heights[0].length]; + const visited = Array.from({ length: rowSize }, _ => + Array.from({ length: colSize }, _ => new Array(2).fill(false)) + ); + const result = []; + + function dfs(heights, visited, x, y, sign) { + if (visited[x][y][sign]) { + return; + } + visited[x][y][sign] = true; + for (let i = 0; i < 4; i++) { + const nextX = x + dir[i][0]; + const nextY = y + dir[i][1]; + if (nextX < 0 || nextX >= rowSize || nextY < 0 || nextY >= colSize) { + continue; + } + if (heights[x][y] > heights[nextX][nextY]) { + continue; + } + dfs(heights, visited, nextX, nextY, sign); + } + } + + for (let i = 0; i < rowSize; i++) { + dfs(heights, visited, i, 0, 0); + dfs(heights, visited, i, colSize - 1, 1); + } + + for (let i = 0; i < colSize; i++) { + dfs(heights, visited, 0, i, 0); + dfs(heights, visited, rowSize - 1, i, 1); + } + + for (let i = 0; i < rowSize; i++) { + for (let k = 0; k < colSize; k++) { + if (visited[i][k][0] && visited[i][k][1]) { + result.push([i, k]); + } + } + } + + return result; +} + +/** + * @description 广度搜索优先 + * @param {number[][]} heights + * @return {number[][]} + */ +function pacificAtlantic(heights) { + const dir = [[-1, 0], [0, -1], [1, 0], [0, 1]]; + const [rowSize, colSize] = [heights.length, heights[0].length]; + const visited = Array.from({ length: rowSize }, _ => + Array.from({ length: colSize }, _ => new Array(2).fill(false)) + ); + const result = []; + + function bfs(heights, visited, x, y, sign) { + if (visited[x][y][sign]) { + return; + } + visited[x][y][sign] = true; + const stack = [y, x]; + while (stack.length !== 0) { + [x, y] = [stack.pop(), stack.pop()]; + for (let i = 0; i < 4; i++) { + const nextX = x + dir[i][0]; + const nextY = y + dir[i][1]; + if (nextX < 0 || nextX >= rowSize || nextY < 0 || nextY >= colSize) { + continue; + } + if (heights[x][y] > heights[nextX][nextY] || visited[nextX][nextY][sign]) { + continue; + } + visited[nextX][nextY][sign] = true; + stack.push(nextY, nextX); + } + } + } + + for (let i = 0; i < rowSize; i++) { + bfs(heights, visited, i, 0, 0); + bfs(heights, visited, i, colSize - 1, 1); + } + + for (let i = 0; i < colSize; i++) { + bfs(heights, visited, 0, i, 0); + bfs(heights, visited, rowSize - 1, i, 1); + } + + for (let i = 0; i < rowSize; i++) { + for (let k = 0; k < colSize; k++) { + if (visited[i][k][0] && visited[i][k][1]) { + result.push([i, k]); + } + } + } + + return result; +} +``` + +### go + +dfs: + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} + +func pacificAtlantic(heights [][]int) [][]int { + res := make([][]int, 0) + pacific := make([][]bool, len(heights)) + atlantic := make([][]bool, len(heights)) + for i := 0; i < len(heights); i++ { + pacific[i] = make([]bool, len(heights[0])) + atlantic[i] = make([]bool, len(heights[0])) + } + // 列 + for i := 0; i < len(heights); i++ { + dfs(heights, pacific, i, 0) + dfs(heights, atlantic, i, len(heights[0])-1) + } + // 行 + for j := 0; j < len(heights[0]); j++ { + dfs(heights, pacific, 0, j) + dfs(heights, atlantic, len(heights)-1, j) + } + + for i := 0; i < len(heights); i++ { + for j := 0; j < len(heights[0]); j++ { + if pacific[i][j] && atlantic[i][j] { + res = append(res, []int{i, j}) + } + } + } + + return res +} + +func dfs(heights [][]int, visited [][]bool, i, j int) { + visited[i][j] = true + for _, d := range DIRECTIONS { + x, y := i+d[0], j+d[1] + if x < 0 || x >= len(heights) || y < 0 || y >= len(heights[0]) || heights[i][j] > heights[x][y] || visited[x][y] { + continue + } + + dfs(heights, visited, x, y) + } +} +``` + +bfs: + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {1, 0}, {0, -1}, {0, 1}} + +func pacificAtlantic(heights [][]int) [][]int { + res := make([][]int, 0) + pacific := make([][]bool, len(heights)) + atlantic := make([][]bool, len(heights)) + for i := 0; i < len(heights); i++ { + pacific[i] = make([]bool, len(heights[0])) + atlantic[i] = make([]bool, len(heights[0])) + } + // 列 + for i := 0; i < len(heights); i++ { + bfs(heights, pacific, i, 0) + bfs(heights, atlantic, i, len(heights[0])-1) + } + // 行 + for j := 0; j < len(heights[0]); j++ { + bfs(heights, pacific, 0, j) + bfs(heights, atlantic, len(heights)-1, j) + } + + for i := 0; i < len(heights); i++ { + for j := 0; j < len(heights[0]); j++ { + if pacific[i][j] && atlantic[i][j] { + res = append(res, []int{i, j}) + } + } + } + + return res +} + +func bfs(heights [][]int, visited [][]bool, i, j int) { + queue := make([][]int, 0) + queue = append(queue, []int{i, j}) + visited[i][j] = true + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + for _, d := range DIRECTIONS { + x, y := cur[0]+d[0], cur[1]+d[1] + if x < 0 || x >= len(heights) || y < 0 || y >= len(heights[0]) || heights[cur[0]][cur[1]] > heights[x][y] || visited[x][y] { + continue + } + queue = append(queue, []int{x, y}) + visited[x][y] = true + } + } +} +``` + +### Rust + +dfs: + +```rust +impl Solution { + const DIRECTIONS: [(isize, isize); 4] = [(0, 1), (0, -1), (1, 0), (-1, 0)]; + pub fn pacific_atlantic(heights: Vec>) -> Vec> { + let (m, n) = (heights.len(), heights[0].len()); + let mut res = vec![]; + let (mut pacific, mut atlantic) = (vec![vec![false; n]; m], vec![vec![false; n]; m]); + + // 列 + for i in 0..m { + Self::dfs(&heights, &mut pacific, i, 0); + Self::dfs(&heights, &mut atlantic, i, n - 1); + } + + for j in 0..n { + Self::dfs(&heights, &mut pacific, 0, j); + Self::dfs(&heights, &mut atlantic, m - 1, j); + } + + for i in 0..m { + for j in 0..n { + if pacific[i][j] && atlantic[i][j] { + res.push(vec![i as i32, j as i32]); + } + } + } + + res + } + + pub fn dfs(heights: &[Vec], visited: &mut [Vec], i: usize, j: usize) { + visited[i][j] = true; + for (dx, dy) in Self::DIRECTIONS { + let (x, y) = (i as isize + dx, j as isize + dy); + if x < 0 || x >= heights.len() as isize || y < 0 || y >= heights[0].len() as isize { + continue; + } + let (x, y) = (x as usize, y as usize); + if !visited[x][y] && heights[x][y] >= heights[i][j] { + Self::dfs(heights, visited, x, y); + } + } + } +} +``` + +bfs: + +```rust +use std::collections::VecDeque; + +impl Solution { + const DIRECTIONS: [(isize, isize); 4] = [(0, 1), (0, -1), (1, 0), (-1, 0)]; + pub fn pacific_atlantic(heights: Vec>) -> Vec> { + let (m, n) = (heights.len(), heights[0].len()); + let mut res = vec![]; + let (mut pacific, mut atlantic) = (vec![vec![false; n]; m], vec![vec![false; n]; m]); + + // 列 + for i in 0..m { + Self::bfs(&heights, &mut pacific, i, 0); + Self::bfs(&heights, &mut atlantic, i, n - 1); + } + + for j in 0..n { + Self::bfs(&heights, &mut pacific, 0, j); + Self::bfs(&heights, &mut atlantic, m - 1, j); + } + + for i in 0..m { + for j in 0..n { + if pacific[i][j] && atlantic[i][j] { + res.push(vec![i as i32, j as i32]); + } + } + } + + res + } + + pub fn bfs(heights: &[Vec], visited: &mut [Vec], i: usize, j: usize) { + let mut queue = VecDeque::from([(i, j)]); + visited[i][j] = true; + while let Some((i, j)) = queue.pop_front() { + for (dx, dy) in Self::DIRECTIONS { + let (x, y) = (i as isize + dx, j as isize + dy); + if x < 0 || x >= heights.len() as isize || y < 0 || y >= heights[0].len() as isize { + continue; + } + let (x, y) = (x as usize, y as usize); + if !visited[x][y] && heights[x][y] >= heights[i][j] { + queue.push_back((x, y)); + visited[x][y] = true; + } + } + } + } +} +``` + diff --git "a/problems/0435.\346\227\240\351\207\215\345\217\240\345\214\272\351\227\264.md" "b/problems/0435.\346\227\240\351\207\215\345\217\240\345\214\272\351\227\264.md" new file mode 100755 index 0000000000..4231a8ee90 --- /dev/null +++ "b/problems/0435.\346\227\240\351\207\215\345\217\240\345\214\272\351\227\264.md" @@ -0,0 +1,495 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 435. 无重叠区间 + +[力扣题目链接](https://leetcode.cn/problems/non-overlapping-intervals/) + +给定一个区间的集合,找到需要移除区间的最小数量,使剩余区间互不重叠。 + +注意: +可以认为区间的终点总是大于它的起点。 +区间 [1,2] 和 [2,3] 的边界相互“接触”,但没有相互重叠。 + +示例 1: +* 输入: [ [1,2], [2,3], [3,4], [1,3] ] +* 输出: 1 +* 解释: 移除 [1,3] 后,剩下的区间没有重叠。 + +示例 2: +* 输入: [ [1,2], [1,2], [1,2] ] +* 输出: 2 +* 解释: 你需要移除两个 [1,2] 来使剩下的区间没有重叠。 + +示例 3: +* 输入: [ [1,2], [2,3] ] +* 输出: 0 +* 解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,依然是判断重叠区间 | LeetCode:435.无重叠区间](https://www.bilibili.com/video/BV1A14y1c7E1),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +**相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?** + +其实都可以。主要就是为了让区间尽可能的重叠。 + +**我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了**。 + +此时问题就是要求非交叉区间的最大个数。 + +这里记录非交叉区间的个数还是有技巧的,如图: + +![](https://file1.kamacoder.com/i/algo/20230201164134.png) + +区间,1,2,3,4,5,6都按照右边界排好序。 + +当确定区间 1 和 区间2 重叠后,如何确定是否与 区间3 也重贴呢? + +就是取 区间1 和 区间2 右边界的最小值,因为这个最小值之前的部分一定是 区间1 和区间2 的重合部分,如果这个最小值也触达到区间3,那么说明 区间 1,2,3都是重合的。 + +接下来就是找大于区间1结束位置的区间,是从区间4开始。**那有同学问了为什么不从区间5开始?别忘了已经是按照右边界排序的了**。 + +区间4结束之后,再找到区间6,所以一共记录非交叉区间的个数是三个。 + +总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。 + +C++代码如下: + +```CPP +class Solution { +public: + // 按照区间右边界排序 + static bool cmp (const vector& a, const vector& b) { + return a[1] < b[1]; + } + int eraseOverlapIntervals(vector>& intervals) { + if (intervals.size() == 0) return 0; + sort(intervals.begin(), intervals.end(), cmp); + int count = 1; // 记录非交叉区间的个数 + int end = intervals[0][1]; // 记录区间分割点 + for (int i = 1; i < intervals.size(); i++) { + if (end <= intervals[i][0]) { + end = intervals[i][1]; + count++; + } + } + return intervals.size() - count; + } +}; +``` +* 时间复杂度:O(nlog n) ,有一个快排 +* 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间 + +大家此时会发现如此复杂的一个问题,代码实现却这么简单! + +## 补充 + +### 补充(1) + +左边界排序可不可以呢? + +也是可以的,只不过 左边界排序我们就是直接求 重叠的区间,count为记录重叠区间数。 + +```CPP +class Solution { +public: + static bool cmp (const vector& a, const vector& b) { + return a[0] < b[0]; // 改为左边界排序 + } + int eraseOverlapIntervals(vector>& intervals) { + if (intervals.size() == 0) return 0; + sort(intervals.begin(), intervals.end(), cmp); + int count = 0; // 注意这里从0开始,因为是记录重叠区间 + int end = intervals[0][1]; // 记录区间分割点 + for (int i = 1; i < intervals.size(); i++) { + if (intervals[i][0] >= end) end = intervals[i][1]; // 无重叠的情况 + else { // 重叠情况 + end = min(end, intervals[i][1]); + count++; + } + } + return count; + } +}; +``` + +其实代码还可以精简一下, 用 intervals[i][1] 替代 end变量,只判断 重叠情况就好 + +```CPP +class Solution { +public: + static bool cmp (const vector& a, const vector& b) { + return a[0] < b[0]; // 改为左边界排序 + } + int eraseOverlapIntervals(vector>& intervals) { + if (intervals.size() == 0) return 0; + sort(intervals.begin(), intervals.end(), cmp); + int count = 0; // 注意这里从0开始,因为是记录重叠区间 + for (int i = 1; i < intervals.size(); i++) { + if (intervals[i][0] < intervals[i - 1][1]) { //重叠情况 + intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); + count++; + } + } + return count; + } +}; + +``` + +### 补充(2) + +本题其实和[452.用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。 + +把[452.用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)代码稍做修改,就可以AC本题。 + +```CPP +class Solution { +public: + // 按照区间右边界排序 + static bool cmp (const vector& a, const vector& b) { + return a[1] < b[1]; // 右边界排序 + } + int eraseOverlapIntervals(vector>& intervals) { + if (intervals.size() == 0) return 0; + sort(intervals.begin(), intervals.end(), cmp); + + int result = 1; // points 不为空至少需要一支箭 + for (int i = 1; i < intervals.size(); i++) { + if (intervals[i][0] >= intervals[i - 1][1]) { + result++; // 需要一支箭 + } + else { // 气球i和气球i-1挨着 + intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界 + } + } + return intervals.size() - result; + } +}; +``` + +这里按照 左边界排序,或者按照右边界排序,都可以AC,原理是一样的。 +```CPP +class Solution { +public: + // 按照区间左边界排序 + static bool cmp (const vector& a, const vector& b) { + return a[0] < b[0]; // 左边界排序 + } + int eraseOverlapIntervals(vector>& intervals) { + if (intervals.size() == 0) return 0; + sort(intervals.begin(), intervals.end(), cmp); + + int result = 1; // points 不为空至少需要一支箭 + for (int i = 1; i < intervals.size(); i++) { + if (intervals[i][0] >= intervals[i - 1][1]) { + result++; // 需要一支箭 + } + else { // 气球i和气球i-1挨着 + intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界 + } + } + return intervals.size() - result; + } +}; + +``` + +## 其他语言版本 + + +### Java +```java +class Solution { + public int eraseOverlapIntervals(int[][] intervals) { + Arrays.sort(intervals, (a,b)-> { + return Integer.compare(a[0],b[0]); + }); + int count = 1; + for(int i = 1;i < intervals.length;i++){ + if(intervals[i][0] < intervals[i-1][1]){ + intervals[i][1] = Math.min(intervals[i - 1][1], intervals[i][1]); + continue; + }else{ + count++; + } + } + return intervals.length - count; + } +} +``` + +按左边排序,不管右边顺序。相交的时候取最小的右边。 +```java +class Solution { + public int eraseOverlapIntervals(int[][] intervals) { + Arrays.sort(intervals, (a,b)-> { + return Integer.compare(a[0],b[0]); + }); + int remove = 0; + int pre = intervals[0][1]; + for(int i = 1; i < intervals.length; i++) { + if(pre > intervals[i][0]) { + remove++; + pre = Math.min(pre, intervals[i][1]); + } + else pre = intervals[i][1]; + } + return remove; + } +} +``` + +### Python +贪心 基于左边界 +```python +class Solution: + def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: + if not intervals: + return 0 + + intervals.sort(key=lambda x: x[0]) # 按照左边界升序排序 + count = 0 # 记录重叠区间数量 + + for i in range(1, len(intervals)): + if intervals[i][0] < intervals[i - 1][1]: # 存在重叠区间 + intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]) # 更新重叠区间的右边界 + count += 1 + + return count + +``` +贪心 基于左边界 把452.用最少数量的箭引爆气球代码稍做修改 +```python +class Solution: + def eraseOverlapIntervals(self, intervals: List[List[int]]) -> int: + if not intervals: + return 0 + + intervals.sort(key=lambda x: x[0]) # 按照左边界升序排序 + + result = 1 # 不重叠区间数量,初始化为1,因为至少有一个不重叠的区间 + + for i in range(1, len(intervals)): + if intervals[i][0] >= intervals[i - 1][1]: # 没有重叠 + result += 1 + else: # 重叠情况 + intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]) # 更新重叠区间的右边界 + + return len(intervals) - result + + +``` +### Go +```go +func eraseOverlapIntervals(intervals [][]int) int { + sort.Slice(intervals, func(i, j int) bool { + return intervals[i][1] < intervals[j][1] + }) + res := 1 + for i := 1; i < len(intervals); i++ { + if intervals[i][0] >= intervals[i-1][1] { + res++ + } else { + intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]) + } + } + return len(intervals) - res +} +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +### JavaScript +- 按右边界排序 +```Javascript +var eraseOverlapIntervals = function(intervals) { + intervals.sort((a, b) => { + return a[1] - b[1] + }) + + let count = 1 + let end = intervals[0][1] + + for(let i = 1; i < intervals.length; i++) { + let interval = intervals[i] + if(interval[0] >= end) { + end = interval[1] + count += 1 + } + } + + return intervals.length - count +}; +``` +- 按左边界排序 +```js +var eraseOverlapIntervals = function(intervals) { + // 按照左边界升序排列 + intervals.sort((a, b) => a[0] - b[0]) + let count = 1 + let end = intervals[intervals.length - 1][0] + // 倒序遍历,对单个区间来说,左边界越大越好,因为给前面区间的空间越大 + for(let i = intervals.length - 2; i >= 0; i--) { + if(intervals[i][1] <= end) { + count++ + end = intervals[i][0] + } + } + // count 记录的是最大非重复区间的个数 + return intervals.length - count +} +``` + +### TypeScript + +> 按右边界排序,从左往右遍历 + +```typescript +function eraseOverlapIntervals(intervals: number[][]): number { + const length = intervals.length; + if (length === 0) return 0; + intervals.sort((a, b) => a[1] - b[1]); + let right: number = intervals[0][1]; + let count: number = 1; + for (let i = 1; i < length; i++) { + if (intervals[i][0] >= right) { + count++; + right = intervals[i][1]; + } + } + return length - count; +}; +``` + +> 按左边界排序,从左往右遍历 + +```typescript +function eraseOverlapIntervals(intervals: number[][]): number { + if (intervals.length === 0) return 0; + intervals.sort((a, b) => a[0] - b[0]); + let right: number = intervals[0][1]; + let tempInterval: number[]; + let resCount: number = 0; + for (let i = 1, length = intervals.length; i < length; i++) { + tempInterval = intervals[i]; + if (tempInterval[0] >= right) { + // 未重叠 + right = tempInterval[1]; + } else { + // 有重叠,移除当前interval和前一个interval中右边界更大的那个 + right = Math.min(right, tempInterval[1]); + resCount++; + } + } + return resCount; +}; +``` + +### Scala + +```scala +object Solution { + def eraseOverlapIntervals(intervals: Array[Array[Int]]): Int = { + var result = 0 + var interval = intervals.sortWith((a, b) => { + a(1) < b(1) + }) + var edge = Int.MinValue + for (i <- 0 until interval.length) { + if (edge <= interval(i)(0)) { + edge = interval(i)(1) + } else { + result += 1 + } + } + result + } +} +``` + +### Rust + +```Rust +impl Solution { + pub fn erase_overlap_intervals(mut intervals: Vec>) -> i32 { + if intervals.is_empty() { + return 0; + } + intervals.sort_by_key(|interval| interval[1]); + let mut count = 1; + let mut end = intervals[0][1]; + for v in intervals.iter().skip(1) { + if end <= v[0] { + end = v[1]; + count += 1; + } + } + + (intervals.len() - count) as i32 + } +} +``` +### C + +```c +// 按照区间右边界排序 +int cmp(const void * var1, const void * var2){ + return (*(int **) var1)[1] - (*(int **) var2)[1]; +} + +int eraseOverlapIntervals(int** intervals, int intervalsSize, int* intervalsColSize) { + if(intervalsSize == 0){ + return 0; + } + qsort(intervals, intervalsSize, sizeof (int *), cmp); + // 记录非重叠的区间数量 + int count = 1; + // 记录区间分割点 + int end = intervals[0][1]; + for(int i = 1; i < intervalsSize; i++){ + if(end <= intervals[i][0]){ + end = intervals[i][1]; + count++; + } + } + return intervalsSize - count; +} +``` + + + +### C# + +```csharp +public class Solution +{ + public int EraseOverlapIntervals(int[][] intervals) + { + if (intervals.Length == 0) return 0; + Array.Sort(intervals, (a, b) => a[1].CompareTo(b[1])); + int res = 1, end = intervals[0][1]; + for (int i = 1; i < intervals.Length; i++) + { + if (end <= intervals[i][0]) + { + end = intervals[i][1]; + res++; + } + } + return intervals.Length - res; + } +} +``` + + + diff --git "a/problems/0450.\345\210\240\351\231\244\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\350\212\202\347\202\271.md" "b/problems/0450.\345\210\240\351\231\244\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\350\212\202\347\202\271.md" new file mode 100755 index 0000000000..44575b8aff --- /dev/null +++ "b/problems/0450.\345\210\240\351\231\244\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\350\212\202\347\202\271.md" @@ -0,0 +1,836 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 二叉搜索树删除节点就涉及到结构调整了 + +# 450.删除二叉搜索树中的节点 + +[力扣题目链接]( https://leetcode.cn/problems/delete-node-in-a-bst/) + +给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。 + +一般来说,删除节点可分为两个步骤: + +首先找到需要删除的节点; +如果找到了,删除它。 +说明: 要求算法时间复杂度为 $O(h)$,h 为树的高度。 + +示例: + + +![450.删除二叉搜索树中的节点](https://file1.kamacoder.com/i/algo/20201020171048265.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[调整二叉树的结构最难!| LeetCode:450.删除二叉搜索树中的节点](https://www.bilibili.com/video/BV1tP41177us?share_source=copy_web),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑,做好心理准备。 + +### 递归 + +递归三部曲: + +* 确定递归函数参数以及返回值 + +说到递归函数的返回值,在[二叉树:搜索树中的插入操作](https://programmercarl.com/0701.二叉搜索树中的插入操作.html)中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。 + +代码如下: + +```cpp +TreeNode* deleteNode(TreeNode* root, int key) +``` + +* 确定终止条件 + +遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了 + +```cpp +if (root == nullptr) return root; +``` + +* 确定单层递归的逻辑 + +这里就把二叉搜索树中删除节点遇到的情况都搞清楚。 + +有以下五种情况: + +* 第一种情况:没找到删除的节点,遍历到空节点直接返回了 +* 找到删除的节点 + * 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 + * 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点 + * 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 + * 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。 + +第五种情况有点难以理解,看下面动画: + +![450.删除二叉搜索树中的节点](https://file1.kamacoder.com/i/algo/450.%E5%88%A0%E9%99%A4%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E8%8A%82%E7%82%B9.gif) + +动画中的二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。 + +将删除节点(元素7)的左孩子放到删除节点(元素7)的右子树的最左面节点(元素8)的左孩子上,就是把5为根节点的子树移到了8的左孩子的位置。 + +要删除的节点(元素7)的右孩子(元素9)为新的根节点。. + +这样就完成删除元素7的逻辑,最好动手画一个图,尝试删除一个节点试试。 + +代码如下: + +```CPP +if (root->val == key) { + // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 + // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点 + if (root->left == nullptr) return root->right; + // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 + else if (root->right == nullptr) return root->left; + // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置 + // 并返回删除节点右孩子为新的根节点。 + else { + TreeNode* cur = root->right; // 找右子树最左面的节点 + while(cur->left != nullptr) { + cur = cur->left; + } + cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置 + TreeNode* tmp = root; // 把root节点保存一下,下面来删除 + root = root->right; // 返回旧root的右孩子作为新root + delete tmp; // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧) + return root; + } +} +``` + +这里相当于把新的节点返回给上一层,上一层就要用 root->left 或者 root->right接住,代码如下: + +```cpp +if (root->val > key) root->left = deleteNode(root->left, key); +if (root->val < key) root->right = deleteNode(root->right, key); +return root; +``` + +**整体代码如下:(注释中:情况1,2,3,4,5和上面分析严格对应)** + +```CPP +class Solution { +public: + TreeNode* deleteNode(TreeNode* root, int key) { + if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了 + if (root->val == key) { + // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 + if (root->left == nullptr && root->right == nullptr) { + ///! 内存释放 + delete root; + return nullptr; + } + // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点 + else if (root->left == nullptr) { + auto retNode = root->right; + ///! 内存释放 + delete root; + return retNode; + } + // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 + else if (root->right == nullptr) { + auto retNode = root->left; + ///! 内存释放 + delete root; + return retNode; + } + // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置 + // 并返回删除节点右孩子为新的根节点。 + else { + TreeNode* cur = root->right; // 找右子树最左面的节点 + while(cur->left != nullptr) { + cur = cur->left; + } + cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置 + TreeNode* tmp = root; // 把root节点保存一下,下面来删除 + root = root->right; // 返回旧root的右孩子作为新root + delete tmp; // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧) + return root; + } + } + if (root->val > key) root->left = deleteNode(root->left, key); + if (root->val < key) root->right = deleteNode(root->right, key); + return root; + } +}; +``` + +### 普通二叉树的删除方式 + +这里我在介绍一种通用的删除,普通二叉树的删除方式(没有使用搜索树的特性,遍历整棵树),用交换值的操作来删除目标节点。 + +代码中目标节点(要删除的节点)被操作了两次: + +* 第一次是和目标节点的右子树最左面节点交换。 +* 第二次直接被NULL覆盖了。 + +思路有点绕,感兴趣的同学可以画图自己理解一下。 + +代码如下:(关键部分已经注释) + +```CPP +class Solution { +public: + TreeNode* deleteNode(TreeNode* root, int key) { + if (root == nullptr) return root; + if (root->val == key) { + if (root->right == nullptr) { // 这里第二次操作目标值:最终删除的作用 + return root->left; + } + TreeNode *cur = root->right; + while (cur->left) { + cur = cur->left; + } + swap(root->val, cur->val); // 这里第一次操作目标值:交换目标值其右子树最左面节点。 + } + root->left = deleteNode(root->left, key); + root->right = deleteNode(root->right, key); + return root; + } +}; +``` + +这个代码是简短一些,思路也巧妙,但是不太好想,实操性不强,推荐第一种写法! + +### 迭代法 + +删除节点的迭代法还是复杂一些的,但其本质我在递归法里都介绍了,最关键就是删除节点的操作(动画模拟的过程) + +代码如下: + +```CPP +class Solution { +private: + // 将目标节点(删除节点)的左子树放到 目标节点的右子树的最左面节点的左孩子位置上 + // 并返回目标节点右孩子为新的根节点 + // 是动画里模拟的过程 + TreeNode* deleteOneNode(TreeNode* target) { + if (target == nullptr) return target; + if (target->right == nullptr) return target->left; + TreeNode* cur = target->right; + while (cur->left) { + cur = cur->left; + } + cur->left = target->left; + return target->right; + } +public: + TreeNode* deleteNode(TreeNode* root, int key) { + if (root == nullptr) return root; + TreeNode* cur = root; + TreeNode* pre = nullptr; // 记录cur的父节点,用来删除cur + while (cur) { + if (cur->val == key) break; + pre = cur; + if (cur->val > key) cur = cur->left; + else cur = cur->right; + } + if (pre == nullptr) { // 如果搜索树只有头结点 + return deleteOneNode(cur); + } + // pre 要知道是删左孩子还是右孩子 + if (pre->left && pre->left->val == key) { + pre->left = deleteOneNode(cur); + } + if (pre->right && pre->right->val == key) { + pre->right = deleteOneNode(cur); + } + return root; + } +}; +``` + +## 总结 + +读完本篇,大家会发现二叉搜索树删除节点比增加节点复杂的多。 + +**因为二叉搜索树添加节点只需要在叶子上添加就可以的,不涉及到结构的调整,而删除节点操作涉及到结构的调整**。 + +这里我们依然使用递归函数的返回值来完成把节点从二叉树中移除的操作。 + +**这里最关键的逻辑就是第五种情况(删除一个左右孩子都不为空的节点),这种情况一定要想清楚**。 + +而且就算想清楚了,对应的代码也未必可以写出来,所以**这道题目既考察思维逻辑,也考察代码能力**。 + +递归中我给出了两种写法,推荐大家学会第一种(利用搜索树的特性)就可以了,第二种递归写法其实是比较绕的。 + +最后我也给出了相应的迭代法,就是模拟递归法中的逻辑来删除节点,但需要一个pre记录cur的父节点,方便做删除操作。 + +迭代法其实不太容易写出来,所以如果是初学者的话,彻底掌握第一种递归写法就够了。 + +## 其他语言版本 + + +### Java +```java +// 解法1(最好理解的版本) +class Solution { + public TreeNode deleteNode(TreeNode root, int key) { + if (root == null) return root; + if (root.val == key) { + if (root.left == null) { + return root.right; + } else if (root.right == null) { + return root.left; + } else { + TreeNode cur = root.right; + while (cur.left != null) { + cur = cur.left; + } + cur.left = root.left; + root = root.right; + return root; + } + } + if (root.val > key) root.left = deleteNode(root.left, key); + if (root.val < key) root.right = deleteNode(root.right, key); + return root; + } +} +``` + + +```java +class Solution { + public TreeNode deleteNode(TreeNode root, int key) { + root = delete(root,key); + return root; + } + + private TreeNode delete(TreeNode root, int key) { + if (root == null) return null; + + if (root.val > key) { + root.left = delete(root.left,key); + } else if (root.val < key) { + root.right = delete(root.right,key); + } else { + if (root.left == null) return root.right; + if (root.right == null) return root.left; + TreeNode tmp = root.right; + while (tmp.left != null) { + tmp = tmp.left; + } + root.val = tmp.val; + root.right = delete(root.right,tmp.val); + } + return root; + } +} +``` +递归法 +```java +class Solution { + public TreeNode deleteNode(TreeNode root, int key) { + if (root == null){ + return null; + } + //寻找对应的对应的前面的节点,以及他的前一个节点 + TreeNode cur = root; + TreeNode pre = null; + while (cur != null){ + if (cur.val < key){ + pre = cur; + cur = cur.right; + } else if (cur.val > key) { + pre = cur; + cur = cur.left; + }else { + break; + } + } + if (pre == null){ + return deleteOneNode(cur); + } + if (pre.left !=null && pre.left.val == key){ + pre.left = deleteOneNode(cur); + } + if (pre.right !=null && pre.right.val == key){ + pre.right = deleteOneNode(cur); + } + return root; + } + + public TreeNode deleteOneNode(TreeNode node){ + if (node == null){ + return null; + } + if (node.right == null){ + return node.left; + } + TreeNode cur = node.right; + while (cur.left !=null){ + cur = cur.left; + } + cur.left = node.left; + return node.right; + } +} +``` + + +### Python + +递归法(版本一) +```python +class Solution: + def deleteNode(self, root, key): + if root is None: + return root + if root.val == key: + if root.left is None and root.right is None: + return None + elif root.left is None: + return root.right + elif root.right is None: + return root.left + else: + cur = root.right + while cur.left is not None: + cur = cur.left + cur.left = root.left + return root.right + if root.val > key: + root.left = self.deleteNode(root.left, key) + if root.val < key: + root.right = self.deleteNode(root.right, key) + return root +``` + +递归法(版本二) +```python +class Solution: + def deleteNode(self, root, key): + if root is None: # 如果根节点为空,直接返回 + return root + if root.val == key: # 找到要删除的节点 + if root.right is None: # 如果右子树为空,直接返回左子树作为新的根节点 + return root.left + cur = root.right + while cur.left: # 找到右子树中的最左节点 + cur = cur.left + root.val, cur.val = cur.val, root.val # 将要删除的节点值与最左节点值交换 + root.left = self.deleteNode(root.left, key) # 在左子树中递归删除目标节点 + root.right = self.deleteNode(root.right, key) # 在右子树中递归删除目标节点 + return root + +``` + +**迭代法** +```python +class Solution: + def deleteOneNode(self, target: TreeNode) -> TreeNode: + """ + 将目标节点(删除节点)的左子树放到目标节点的右子树的最左面节点的左孩子位置上 + 并返回目标节点右孩子为新的根节点 + 是动画里模拟的过程 + """ + if target is None: + return target + if target.right is None: + return target.left + cur = target.right + while cur.left: + cur = cur.left + cur.left = target.left + return target.right + + def deleteNode(self, root: TreeNode, key: int) -> TreeNode: + if root is None: + return root + cur = root + pre = None # 记录cur的父节点,用来删除cur + while cur: + if cur.val == key: + break + pre = cur + if cur.val > key: + cur = cur.left + else: + cur = cur.right + if pre is None: # 如果搜索树只有头结点 + return self.deleteOneNode(cur) + # pre 要知道是删左孩子还是右孩子 + if pre.left and pre.left.val == key: + pre.left = self.deleteOneNode(cur) + if pre.right and pre.right.val == key: + pre.right = self.deleteOneNode(cur) + return root +``` + +### Go +```Go +// 递归版本 +func deleteNode(root *TreeNode, key int) *TreeNode { + if root == nil { + return nil + } + if key < root.Val { + root.Left = deleteNode(root.Left, key) + return root + } + if key > root.Val { + root.Right = deleteNode(root.Right, key) + return root + } + if root.Right == nil { + return root.Left + } + if root.Left == nil{ + return root.Right + } + minnode := root.Right + for minnode.Left != nil { + minnode = minnode.Left + } + root.Val = minnode.Val + root.Right = deleteNode1(root.Right) + return root +} + +func deleteNode1(root *TreeNode)*TreeNode { + if root.Left == nil { + pRight := root.Right + root.Right = nil + return pRight + } + root.Left = deleteNode1(root.Left) + return root +} +// 迭代版本 +func deleteOneNode(target *TreeNode) *TreeNode { + if target == nil { + return target + } + if target.Right == nil { + return target.Left + } + cur := target.Right + for cur.Left != nil { + cur = cur.Left + } + cur.Left = target.Left + return target.Right +} +func deleteNode(root *TreeNode, key int) *TreeNode { + // 特殊情况处理 + if root == nil { + return root + } + cur := root + var pre *TreeNode + for cur != nil { + if cur.Val == key { + break + } + pre = cur + if cur.Val > key { + cur = cur.Left + } else { + cur = cur.Right + } + } + if pre == nil { + return deleteOneNode(cur) + } + // pre 要知道是删除左孩子还有右孩子 + if pre.Left != nil && pre.Left.Val == key { + pre.Left = deleteOneNode(cur) + } + if pre.Right != nil && pre.Right.Val == key { + pre.Right = deleteOneNode(cur) + } + return root +} +``` + +### JavaScript + +递归 + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @param {number} key + * @return {TreeNode} + */ +var deleteNode = function(root, key) { + if (!root) return null; + if (key > root.val) { + root.right = deleteNode(root.right, key); + return root; + } else if (key < root.val) { + root.left = deleteNode(root.left, key); + return root; + } else { + // 场景1: 该节点是叶节点 + if (!root.left && !root.right) { + return null + } + // 场景2: 有一个孩子节点不存在 + if (root.left && !root.right) { + return root.left; + } else if (root.right && !root.left) { + return root.right; + } + // 场景3: 左右节点都存在 + const rightNode = root.right; + // 获取最小值节点 + const minNode = getMinNode(rightNode); + // 将待删除节点的值替换为最小值节点值 + root.val = minNode.val; + // 删除最小值节点 + root.right = deleteNode(root.right, minNode.val); + return root; + } +}; +function getMinNode(root) { + while (root.left) { + root = root.left; + } + return root; +} +``` + +迭代 +``` javascript +var deleteNode = function (root, key) { + const deleteOneNode = target => { + if (!target) return target + if (!target.right) return target.left + let cur = target.right + while (cur.left) { + cur = cur.left + } + cur.left = target.left + return target.right + } + + if (!root) return root + let cur = root + let pre = null + while (cur) { + if (cur.val === key) break + pre = cur + cur.val > key ? cur = cur.left : cur = cur.right + } + if (!pre) { + return deleteOneNode(cur) + } + if (pre.left && pre.left.val === key) { + pre.left = deleteOneNode(cur) + } + if (pre.right && pre.right.val === key) { + pre.right = deleteOneNode(cur) + } + return root +} +``` + +### TypeScript + +> 递归法: + +```typescript +function deleteNode(root: TreeNode | null, key: number): TreeNode | null { + if (root === null) return null; + if (root.val === key) { + if (root.left === null && root.right === null) return null; + if (root.left === null) return root.right; + if (root.right === null) return root.left; + let curNode: TreeNode = root.right; + while (curNode.left !== null) { + curNode = curNode.left; + } + curNode.left = root.left; + return root.right; + } + if (root.val > key) root.left = deleteNode(root.left, key); + if (root.val < key) root.right = deleteNode(root.right, key); + return root; +}; +``` + +> 迭代法: + +```typescript +function deleteNode(root: TreeNode | null, key: number): TreeNode | null { + function removeTargetNode(root: TreeNode): TreeNode | null { + if (root.left === null && root.right === null) return null; + if (root.right === null) return root.left; + if (root.left === null) return root.right; + let curNode: TreeNode | null = root.right; + while (curNode.left !== null) { + curNode = curNode.left; + } + curNode.left = root.left; + return root.right; + } + let preNode: TreeNode | null = null, + curNode: TreeNode | null = root; + while (curNode !== null) { + if (curNode.val === key) break; + preNode = curNode; + if (curNode.val > key) { + curNode = curNode.left; + } else { + curNode = curNode.right; + } + } + if (curNode === null) return root; + if (preNode === null) { + // 删除头节点 + return removeTargetNode(curNode); + } + if (preNode.val > key) { + preNode.left = removeTargetNode(curNode); + } else { + preNode.right = removeTargetNode(curNode); + } + return root; +}; +``` + +### Scala + +```scala +object Solution { + def deleteNode(root: TreeNode, key: Int): TreeNode = { + if (root == null) return root // 第一种情况,没找到删除的节点,遍历到空节点直接返回 + if (root.value == key) { + // 第二种情况: 左右孩子都为空,直接删除节点,返回null + if (root.left == null && root.right == null) return null + // 第三种情况: 左孩子为空,右孩子不为空,右孩子补位 + else if (root.left == null && root.right != null) return root.right + // 第四种情况: 左孩子不为空,右孩子为空,左孩子补位 + else if (root.left != null && root.right == null) return root.left + // 第五种情况: 左右孩子都不为空,将删除节点的左子树头节点(左孩子)放到 + // 右子树的最左边节点的左孩子上,返回删除节点的右孩子 + else { + var tmp = root.right + while (tmp.left != null) tmp = tmp.left + tmp.left = root.left + return root.right + } + } + if (root.value > key) root.left = deleteNode(root.left, key) + if (root.value < key) root.right = deleteNode(root.right, key) + + root // 返回根节点,return关键字可以省略 + } +} +``` + +### Rust + +```rust +impl Solution { + pub fn delete_node( + root: Option>>, + key: i32, + ) -> Option>> { + root.as_ref()?; + + let mut node = root.as_ref().unwrap().borrow_mut(); + match node.val.cmp(&key) { + std::cmp::Ordering::Less => node.right = Self::delete_node(node.right.clone(), key), + std::cmp::Ordering::Equal => match (node.left.clone(), node.right.clone()) { + (None, None) => return None, + (None, Some(r)) => return Some(r), + (Some(l), None) => return Some(l), + (Some(l), Some(r)) => { + let mut cur = Some(r.clone()); + while let Some(n) = cur.clone().unwrap().borrow().left.clone() { + cur = Some(n); + } + cur.unwrap().borrow_mut().left = Some(l); + return Some(r); + } + }, + std::cmp::Ordering::Greater => node.left = Self::delete_node(node.left.clone(), key), + } + drop(node); + root + } +} +``` + +### C# + +> 递归法: +```csharp + public TreeNode DeleteNode(TreeNode root, int key) { + // 第一种情况:没找到删除的节点,遍历到空节点直接返回了 + if (root == null) return null; + if(key == root.val) { + //第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 + if(root.left == null && root.right == null) return null; + //第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点 + if (root.left == null && root.right != null) return root.right; + //第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 + if (root.left != null && root.right == null) return root.left; + //第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置 + // 并返回删除节点右孩子为新的根节点。 + if(root.left != null && root.right != null) { + TreeNode leftNode = root.right; // 找右子树最左面的节点 + while(leftNode.left != null) + leftNode = leftNode.left; + leftNode.left = root.left; // 把要删除的节点(root)左子树放在leftNode的左孩子的位置 + return root.right; // 返回旧root的右孩子作为新root + } + } + + if(root.val > key) root.left = DeleteNode(root.left, key); + if(root.val < key) root.right = DeleteNode(root.right, key); + + return root; + } +``` + +### Ruby +> 递归法: +```ruby +# @param {TreeNode} root +# @param {Integer} key +# @return {TreeNode} +def delete_node(root, key) + return nil if root.nil? + + right = root.right + left = root.left + + if root.val == key + return right if left.nil? + return left if right.nil? + + node = right + while node.left + node = node.left + end + node.left = left + + return right + end + + if root.val > key + root.left = delete_node(left, key) + else + root.right = delete_node(right, key) + end + + return root +end +``` + diff --git "a/problems/0452.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" "b/problems/0452.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" new file mode 100755 index 0000000000..76de3f93a2 --- /dev/null +++ "b/problems/0452.\347\224\250\346\234\200\345\260\221\346\225\260\351\207\217\347\232\204\347\256\255\345\274\225\347\210\206\346\260\224\347\220\203.md" @@ -0,0 +1,357 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 452. 用最少数量的箭引爆气球 + +[力扣题目链接](https://leetcode.cn/problems/minimum-number-of-arrows-to-burst-balloons/) + +在二维空间中有许多球形的气球。对于每个气球,提供的输入是水平方向上,气球直径的开始和结束坐标。由于它是水平的,所以纵坐标并不重要,因此只要知道开始和结束的横坐标就足够了。开始坐标总是小于结束坐标。 + +一支弓箭可以沿着 x 轴从不同点完全垂直地射出。在坐标 x 处射出一支箭,若有一个气球的直径的开始和结束坐标为 xstart,xend, 且满足  xstart ≤ x ≤ xend,则该气球会被引爆。可以射出的弓箭的数量没有限制。 弓箭一旦被射出之后,可以无限地前进。我们想找到使得所有气球全部被引爆,所需的弓箭的最小数量。 + +给你一个数组 points ,其中 points [i] = [xstart,xend] ,返回引爆所有气球所必须射出的最小弓箭数。 + + +示例 1: +* 输入:points = [[10,16],[2,8],[1,6],[7,12]] +* 输出:2 +* 解释:对于该样例,x = 6 可以射爆 [2,8],[1,6] 两个气球,以及 x = 11 射爆另外两个气球 + +示例 2: +* 输入:points = [[1,2],[3,4],[5,6],[7,8]] +* 输出:4 + +示例 3: +* 输入:points = [[1,2],[2,3],[3,4],[4,5]] +* 输出:2 + +示例 4: +* 输入:points = [[1,2]] +* 输出:1 + +示例 5: +* 输入:points = [[2,3],[2,3]] +* 输出:1 + +提示: +* 0 <= points.length <= 10^4 +* points[i].length == 2 +* -2^31 <= xstart < xend <= 2^31 - 1 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,判断重叠区间问题 | LeetCode:452.用最少数量的箭引爆气球](https://www.bilibili.com/video/BV1SA41167xe),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +如何使用最少的弓箭呢? + +直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个气球,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢? + +尝试一下举反例,发现没有这种情况。 + +那么就试一试贪心吧!局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。 + +**算法确定下来了,那么如何模拟气球射爆的过程呢?是在数组中移除元素还是做标记呢?** + +如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。 + +但仔细思考一下就发现:如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remove气球,只要记录一下箭的数量就可以了。 + +以上为思考过程,已经确定下来使用贪心了,那么开始解题。 + +**为了让气球尽可能的重叠,需要对数组进行排序**。 + +那么按照气球起始位置排序,还是按照气球终止位置排序呢? + +其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。 + +既然按照起始位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。 + +从前向后遍历遇到重叠的气球了怎么办? + +**如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭**。 + +以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序) + +![452.用最少数量的箭引爆气球](https://file1.kamacoder.com/i/algo/20201123101929791.png) + +可以看出首先第一组重叠气球,一定是需要一个箭,气球3,的左边界大于了 第一组重叠气球的最小右边界,所以再需要一支箭来射气球3了。 + +C++代码如下: + +```CPP +class Solution { +private: + static bool cmp(const vector& a, const vector& b) { + return a[0] < b[0]; + } +public: + int findMinArrowShots(vector>& points) { + if (points.size() == 0) return 0; + sort(points.begin(), points.end(), cmp); + + int result = 1; // points 不为空至少需要一支箭 + for (int i = 1; i < points.size(); i++) { + if (points[i][0] > points[i - 1][1]) { // 气球i和气球i-1不挨着,注意这里不是>= + result++; // 需要一支箭 + } + else { // 气球i和气球i-1挨着 + points[i][1] = min(points[i - 1][1], points[i][1]); // 更新重叠气球最小右边界 + } + } + return result; + } +}; +``` + +* 时间复杂度:O(nlog n),因为有一个快排 +* 空间复杂度:O(n),有一个快排,最差情况(倒序)时,需要n次递归调用。因此确实需要O(n)的栈空间 + +可以看出代码并不复杂。 + +## 注意事项 + +注意题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆, + +所以代码中 `if (points[i][0] > points[i - 1][1])` 不能是>= + +## 总结 + +这道题目贪心的思路很简单也很直接,就是重复的一起射了,但本题我认为是有难度的。 + +就算思路都想好了,模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了。 + +而且寻找重复的气球,寻找重叠气球最小右边界,其实都有代码技巧。 + +贪心题目有时候就是这样,看起来很简单,思路很直接,但是一写代码就感觉贼复杂无从下手。 + +这里其实是需要代码功底的,那代码功底怎么练? + +**多看多写多总结!** + + +## 其他语言版本 + + +### Java +```java +/** + * 时间复杂度 : O(NlogN) 排序需要 O(NlogN) 的复杂度 + * 空间复杂度 : O(logN) java所使用的内置函数用的是快速排序需要 logN 的空间 + */ +class Solution { + public int findMinArrowShots(int[][] points) { + // 根据气球直径的开始坐标从小到大排序 + // 使用Integer内置比较方法,不会溢出 + Arrays.sort(points, (a, b) -> Integer.compare(a[0], b[0])); + + int count = 1; // points 不为空至少需要一支箭 + for (int i = 1; i < points.length; i++) { + if (points[i][0] > points[i - 1][1]) { // 气球i和气球i-1不挨着,注意这里不是>= + count++; // 需要一支箭 + } else { // 气球i和气球i-1挨着 + points[i][1] = Math.min(points[i][1], points[i - 1][1]); // 更新重叠气球最小右边界 + } + } + return count; + } +} +``` + +### Python +```python +class Solution: + def findMinArrowShots(self, points: List[List[int]]) -> int: + if len(points) == 0: return 0 + points.sort(key=lambda x: x[0]) + result = 1 + for i in range(1, len(points)): + if points[i][0] > points[i - 1][1]: # 气球i和气球i-1不挨着,注意这里不是>= + result += 1 + else: + points[i][1] = min(points[i - 1][1], points[i][1]) # 更新重叠气球最小右边界 + return result +``` +```python +class Solution: # 不改变原数组 + def findMinArrowShots(self, points: List[List[int]]) -> int: + if len(points) == 0: + return 0 + + points.sort(key = lambda x: x[0]) + + # points已经按照第一个坐标正序排列,因此只需要设置一个变量,记录右侧坐标(阈值) + # 考虑一个气球范围包含两个不相交气球的情况:气球1: [1, 10], 气球2: [2, 5], 气球3: [6, 10] + curr_min_right = points[0][1] + count = 1 + + for i in points: + if i[0] > curr_min_right: + # 当气球左侧大于这个阈值,那么一定就需要在发射一只箭,并且将阈值更新为当前气球的右侧 + count += 1 + curr_min_right = i[1] + else: + # 否则的话,我们只需要求阈值和当前气球的右侧的较小值来更新阈值 + curr_min_right = min(curr_min_right, i[1]) + return count +``` +### Go +```go +func findMinArrowShots(points [][]int) int { + var res int = 1 //弓箭数 + //先按照第一位排序 + sort.Slice(points, func (i,j int) bool { + return points[i][0] < points[j][0] + }) + + for i := 1; i < len(points); i++ { + if points[i-1][1] < points[i][0] { //如果前一位的右边界小于后一位的左边界,则一定不重合 + res++ + } else { + points[i][1] = min(points[i - 1][1], points[i][1]); // 更新重叠气球最小右边界,覆盖该位置的值,留到下一步使用 + } + } + return res +} +func min(a, b int) int { + if a > b { + return b + } + return a +} +``` + +### JavaScript +```Javascript +var findMinArrowShots = function(points) { + points.sort((a, b) => { + return a[0] - b[0] + }) + let result = 1 + for(let i = 1; i < points.length; i++) { + if(points[i][0] > points[i - 1][1]) { + result++ + } else { + points[i][1] = Math.min(points[i - 1][1], points[i][1]) + } + } + + return result +}; +``` + +### TypeScript + +```typescript +function findMinArrowShots(points: number[][]): number { + const length: number = points.length; + if (length === 0) return 0; + points.sort((a, b) => a[0] - b[0]); + let resCount: number = 1; + let right: number = points[0][1]; // 右边界 + let tempPoint: number[]; + for (let i = 1; i < length; i++) { + tempPoint = points[i]; + if (tempPoint[0] > right) { + resCount++; + right = tempPoint[1]; + } else { + right = Math.min(right, tempPoint[1]); + } + } + return resCount; +}; +``` + +### C + +```c +int cmp(const void *a,const void *b) +{ + return ((*((int**)a))[0] > (*((int**)b))[0]); +} + +int findMinArrowShots(int** points, int pointsSize, int* pointsColSize){ + //将points数组作升序排序 + qsort(points, pointsSize, sizeof(points[0]),cmp); + + int arrowNum = 1; + int i = 1; + for(i = 1; i < pointsSize; i++) { + //若前一个气球与当前气球不重叠,证明需要增加箭的数量 + if(points[i][0] > points[i-1][1]) + arrowNum++; + else + //若前一个气球与当前气球重叠,判断并更新最小的x_end + points[i][1] = points[i][1] > points[i-1][1] ? points[i-1][1] : points[i][1]; + } + return arrowNum; +} +``` + +### Rust +```Rust +impl Solution { + pub fn find_min_arrow_shots(mut points: Vec>) -> i32 { + if points.is_empty() { + return 0; + } + points.sort_by_key(|point| point[0]); + let mut result = 1; + for i in 1..points.len() { + if points[i][0] > points[i - 1][1] { + result += 1; + } else { + points[i][1] = points[i][1].min(points[i - 1][1]) + } + } + result + } +} +``` + +### Scala + +```scala +object Solution { + def findMinArrowShots(points: Array[Array[Int]]): Int = { + if (points.length == 0) return 0 + // 排序 + var point = points.sortWith((a, b) => { + a(0) < b(0) + }) + + var result = 1 // points不为空就至少需要一只箭 + for (i <- 1 until point.length) { + if (point(i)(0) > point(i - 1)(1)) { + result += 1 + } else { + point(i)(1) = math.min(point(i - 1)(1), point(i)(1)) + } + } + result // 返回结果 + } +} +``` +### C# +```csharp +public class Solution +{ + public int FindMinArrowShots(int[][] points) + { + if (points.Length == 0) return 0; + Array.Sort(points, (a, b) => a[0].CompareTo(b[0])); + int count = 1; + for (int i = 1; i < points.Length; i++) + { + if (points[i][0] > points[i - 1][1]) count++; + else points[i][1] = Math.Min(points[i][1], points[i - 1][1]); + } + return count; + } +} +``` + diff --git "a/problems/0454.\345\233\233\346\225\260\347\233\270\345\212\240II.md" "b/problems/0454.\345\233\233\346\225\260\347\233\270\345\212\240II.md" new file mode 100755 index 0000000000..a26071a1fa --- /dev/null +++ "b/problems/0454.\345\233\233\346\225\260\347\233\270\345\212\240II.md" @@ -0,0 +1,526 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 需要哈希的地方都能找到map的身影 + +# 第454题.四数相加II + +[力扣题目链接](https://leetcode.cn/problems/4sum-ii/) + +给定四个包含整数的数组列表 A , B , C , D ,计算有多少个元组 (i, j, k, l) ,使得 A[i] + B[j] + C[k] + D[l] = 0。 + +为了使问题简单化,所有的 A, B, C, D 具有相同的长度 N,且 0 ≤ N ≤ 500 。所有整数的范围在 -2^28 到 2^28 - 1 之间,最终结果不会超过 2^31 - 1 。 + +**例如:** + +输入: +* A = [ 1, 2] +* B = [-2,-1] +* C = [-1, 2] +* D = [ 0, 2] + +输出: + +2 + +**解释:** + +两个元组如下: + +1. (0, 0, 0, 1) -> A[0] + B[0] + C[0] + D[1] = 1 + (-2) + (-1) + 2 = 0 +2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[学透哈希表,map使用有技巧!LeetCode:454.四数相加II](https://www.bilibili.com/video/BV1Md4y1Q7Yh),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题乍眼一看好像和[0015.三数之和](https://programmercarl.com/0015.三数之和.html),[0018.四数之和](https://programmercarl.com/0018.四数之和.html)差不多,其实差很多。 + +**本题是使用哈希法的经典题目,而[0015.三数之和](https://programmercarl.com/0015.三数之和.html),[0018.四数之和](https://programmercarl.com/0018.四数之和.html)并不合适使用哈希法**,因为三数之和和四数之和这两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理。 + +**而这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!** + +如果本题想难度升级:就是给出一个数组(而不是四个数组),在这里找出四个元素相加等于0,答案中不可以包含重复的四元组,大家可以思考一下,后续的文章我也会讲到的。 + +本题解题步骤: + +1. 首先定义 一个unordered_map,key放a和b两数之和,value 放a和b两数之和出现的次数。 +2. 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中。 +3. 定义int变量count,用来统计 a+b+c+d = 0 出现的次数。 +4. 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。 +5. 最后返回统计值 count 就可以了 + +C++代码: + +```CPP +class Solution { +public: + int fourSumCount(vector& A, vector& B, vector& C, vector& D) { + unordered_map umap; //key:a+b的数值,value:a+b数值出现的次数 + // 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中 + for (int a : A) { + for (int b : B) { + umap[a + b]++; + } + } + int count = 0; // 统计a+b+c+d = 0 出现的次数 + // 再遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来。 + for (int c : C) { + for (int d : D) { + if (umap.find(0 - (c + d)) != umap.end()) { + count += umap[0 - (c + d)]; + } + } + } + return count; + } +}; + +``` + +* 时间复杂度: O(n^2) +* 空间复杂度: O(n^2),最坏情况下A和B的值各不相同,相加产生的数字个数为 n^2 + + + + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int fourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) { + int res = 0; + Map map = new HashMap(); + //统计两个数组中的元素之和,同时统计出现的次数,放入map + for (int i : nums1) { + for (int j : nums2) { + int sum = i + j; + map.put(sum, map.getOrDefault(sum, 0) + 1); + } + } + //统计剩余的两个元素的和,在map中找是否存在相加为0的情况,同时记录次数 + for (int i : nums3) { + for (int j : nums4) { + res += map.getOrDefault(0 - i - j, 0); + } + } + return res; + } +} +``` + +### Python: +(版本一) 使用字典 + +```python +class Solution(object): + def fourSumCount(self, nums1, nums2, nums3, nums4): + # 使用字典存储nums1和nums2中的元素及其和 + hashmap = dict() + for n1 in nums1: + for n2 in nums2: + if n1 + n2 in hashmap: + hashmap[n1+n2] += 1 + else: + hashmap[n1+n2] = 1 + + # 如果 -(n1+n2) 存在于nums3和nums4, 存入结果 + count = 0 + for n3 in nums3: + for n4 in nums4: + key = - n3 - n4 + if key in hashmap: + count += hashmap[key] + return count + + +``` +(版本二) 使用字典 +```python +class Solution(object): + def fourSumCount(self, nums1, nums2, nums3, nums4): + # 使用字典存储nums1和nums2中的元素及其和 + hashmap = dict() + for n1 in nums1: + for n2 in nums2: + hashmap[n1+n2] = hashmap.get(n1+n2, 0) + 1 + + # 如果 -(n1+n2) 存在于nums3和nums4, 存入结果 + count = 0 + for n3 in nums3: + for n4 in nums4: + key = - n3 - n4 + if key in hashmap: + count += hashmap[key] + return count + + + +``` +(版本三)使用 defaultdict +```python +from collections import defaultdict +class Solution: + def fourSumCount(self, nums1: list, nums2: list, nums3: list, nums4: list) -> int: + rec, cnt = defaultdict(lambda : 0), 0 + for i in nums1: + for j in nums2: + rec[i+j] += 1 + for i in nums3: + for j in nums4: + cnt += rec.get(-(i+j), 0) + return cnt +``` + +### Go: + +```go +func fourSumCount(nums1 []int, nums2 []int, nums3 []int, nums4 []int) int { + m := make(map[int]int) + count := 0 + + // 构建nums1和nums2的和的map + for _, v1 := range nums1 { + for _, v2 := range nums2 { + m[v1+v2]++ + } + } + + // 遍历nums3和nums4,检查-c-d是否在map中 + for _, v3 := range nums3 { + for _, v4 := range nums4 { + sum := -v3 - v4 + if countVal, ok := m[sum]; ok { + count += countVal + } + } + } + + return count +} +``` + +### JavaScript: + +```js +/** + * @param {number[]} nums1 + * @param {number[]} nums2 + * @param {number[]} nums3 + * @param {number[]} nums4 + * @return {number} + */ +var fourSumCount = function(nums1, nums2, nums3, nums4) { + const twoSumMap = new Map(); + let count = 0; + // 统计nums1和nums2数组元素之和,和出现的次数,放到map中 + for(const n1 of nums1) { + for(const n2 of nums2) { + const sum = n1 + n2; + twoSumMap.set(sum, (twoSumMap.get(sum) || 0) + 1) + } + } + // 找到如果 0-(c+d) 在map中出现过的话,就把map中key对应的value也就是出现次数统计出来 + for(const n3 of nums3) { + for(const n4 of nums4) { + const sum = n3 + n4; + count += (twoSumMap.get(0 - sum) || 0) + } + } + + return count; +}; +``` + +### TypeScript: + +```typescript +function fourSumCount(nums1: number[], nums2: number[], nums3: number[], nums4: number[]): number { + let helperMap: Map = new Map(); + let resNum: number = 0; + let tempVal: number | undefined; + for (let i of nums1) { + for (let j of nums2) { + tempVal = helperMap.get(i + j); + helperMap.set(i + j, tempVal ? tempVal + 1 : 1); + } + } + for (let k of nums3) { + for (let l of nums4) { + tempVal = helperMap.get(0 - (k + l)); + if (tempVal) { + resNum += tempVal; + } + } + } + return resNum; +}; +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer[] $nums1 + * @param Integer[] $nums2 + * @param Integer[] $nums3 + * @param Integer[] $nums4 + * @return Integer + */ + function fourSumCount($nums1, $nums2, $nums3, $nums4) { + $map = []; + foreach ($nums1 as $n1) { + foreach ($nums2 as $n2) { + $temp = $n1 + $n2; + $map[$temp] = isset($map[$temp]) ? $map[$temp]+1 : 1; + } + } + $count = 0; + foreach ($nums3 as $n3) { + foreach ($nums4 as $n4) { + $temp = 0 - $n3 - $n4; + if (isset($map[$temp])) { + $count += $map[$temp]; + } + } + } + return $count; + } +} +``` + +### Swift: + +```swift +func fourSumCount(_ nums1: [Int], _ nums2: [Int], _ nums3: [Int], _ nums4: [Int]) -> Int { + // ab和: ab和出现次数 + var countDic = [Int: Int]() + for a in nums1 { + for b in nums2 { + let key = a + b + countDic[key] = countDic[key, default: 0] + 1 + } + } + + // 通过-(c + d)作为key,去累加ab和出现的次数 + var result = 0 + for c in nums3 { + for d in nums4 { + let key = -(c + d) + result += countDic[key, default: 0] + } + } + return result +} +``` + +### Rust: + +```rust +use std::collections::HashMap; +impl Solution { + pub fn four_sum_count(nums1: Vec, nums2: Vec, nums3: Vec, nums4: Vec) -> i32 { + let mut umap:HashMap = HashMap::new(); + for num1 in &nums1 { + for num2 in &nums2 { + *umap.entry(num1 + num2).or_insert(0) += 1; + } + } + + let mut count = 0; + + for num3 in &nums3 { + for num4 in &nums4 { + let target:i32 = - (num3 + num4); + count += umap.get(&target).unwrap_or(&0); + } + } + + count + } +} +``` + +### Scala: + +```scala +object Solution { + // 导包 + import scala.collection.mutable + def fourSumCount(nums1: Array[Int], nums2: Array[Int], nums3: Array[Int], nums4: Array[Int]): Int = { + // 定义一个HashMap,key存储值,value存储该值出现的次数 + val map = new mutable.HashMap[Int, Int]() + // 遍历前两个数组,把他们所有可能的情况都记录到map + for (i <- nums1.indices) { + for (j <- nums2.indices) { + val tmp = nums1(i) + nums2(j) + // 如果包含该值,则对他的key加1,不包含则添加进去 + if (map.contains(tmp)) { + map.put(tmp, map.get(tmp).get + 1) + } else { + map.put(tmp, 1) + } + } + } + var res = 0 // 结果变量 + // 遍历后两个数组 + for (i <- nums3.indices) { + for (j <- nums4.indices) { + val tmp = -(nums3(i) + nums4(j)) + // 如果map中存在该值,结果就+=value + if (map.contains(tmp)) { + res += map.get(tmp).get + } + } + } + // 返回最终结果,可以省略关键字return + res + } +} +``` + +### C#: + +```csharp +public int FourSumCount(int[] nums1, int[] nums2, int[] nums3, int[] nums4) { + Dictionary dic = new Dictionary(); + foreach(var i in nums1){ + foreach(var j in nums2){ + int sum = i + j; + if(dic.ContainsKey(sum)){ + dic[sum]++; + }else{ + dic.Add(sum, 1); + } + + } + } + int res = 0; + foreach(var a in nums3){ + foreach(var b in nums4){ + int sum = a+b; + if(dic.TryGetValue(-sum, out var result)){ + res += result; + } + } + } + return res; + } +``` +### C: + +```c +// 哈希表大小 +const int HASH_SIZE = 101; + +typedef struct node { + int val; + int count; + struct node *next; +} node, *HashMap; + +// 哈希表插入 +void hash_insert(HashMap hashmap[], int val) { + int idx = val < 0 ? (-val) % HASH_SIZE : val % HASH_SIZE, count = 0; + node *p = hashmap[idx]; + while (p->next != NULL) { + p = p->next; + if (p->val == val) { + (p->count)++; + return; + } + } + node *new = malloc(sizeof(node)); + new->val = val; + new->count = 1; + new->next = NULL; + p->next = new; + return; +} + +// 哈希表查找 +int hash_search(HashMap hashmap[], int val) { + int idx = val < 0 ? (-val) % HASH_SIZE : val % HASH_SIZE; + node *p = hashmap[idx]; + while (p->next != NULL) { + p = p->next; + if (p->val == val) return p->count; + } + return 0; +} + +int fourSumCount(int* nums1, int nums1Size, int* nums2, int nums2Size, int* nums3, int nums3Size, int* nums4, int nums4Size){ + // 初始化哈希表 + HashMap hashmap[HASH_SIZE]; + for (int i = 0; i < HASH_SIZE; i++) { + hashmap[i] = malloc(sizeof(node)); + hashmap[i]->next = NULL; + } + + // 统计两个数组元素之和的负值和出现的次数,放到哈希表中 + int count = 0, num; + for (int i = 0; i < nums1Size; i++) { + for(int j = 0; j < nums2Size; j++) { + num = - nums1[i] - nums2[j]; + hash_insert(hashmap, num); + } + } + + // 统计另外两个数组元素之和,查找哈希表中对应元素的出现次数,加入总次数 + for (int i = 0; i < nums3Size; i++) { + for(int j = 0; j < nums4Size; j++) { + num = nums3[i] + nums4[j]; + count += hash_search(hashmap, num); + } + } + return count; +} +``` + +### Ruby: + +```ruby +# @param {Integer[]} nums1 +# @param {Integer[]} nums2 +# @param {Integer[]} nums3 +# @param {Integer[]} nums4 +# @return {Integer} +# 新思路:和版主的思路基本相同,只是对后面两个数组的二重循环,用一个方法调用外加一重循环替代,简化了一点。 +# 简单的说,就是把四数和变成了两个两数和的统计(结果放到两个 hash 中),然后再来一次两数和为0. +# 把四个数分成两组两个数,然后分别计算每组可能的和情况,分别存入 hash 中,key 是 和,value 是 数量; +# 最后,得到的两个 hash 只需要遍历一次,符合和为零的 value 相乘并加总。 +def four_sum_count(nums1, nums2, nums3, nums4) + num_to_count_1 = two_sum_mapping(nums1, nums2) + num_to_count_2 = two_sum_mapping(nums3, nums4) + + count_sum = 0 + + num_to_count_1.each do |num, count| + count_sum += num_to_count_2[-num] * count # 反查另一个 hash,看有没有匹配的,没有的话,hash 默认值为 0,不影响加总;有匹配的,乘积就是可能的情况 + end + + count_sum +end + +def two_sum_mapping(nums1, nums2) + num_to_count = Hash.new(0) + + nums1.each do |num1| + nums2.each do |nums2| + num_to_count[num1 + nums2] += 1 # 统计和为 num1 + nums2 的有几个 + end + end + + num_to_count +end +``` + diff --git "a/problems/0455.\345\210\206\345\217\221\351\245\274\345\271\262.md" "b/problems/0455.\345\210\206\345\217\221\351\245\274\345\271\262.md" new file mode 100755 index 0000000000..2c38ab9ec1 --- /dev/null +++ "b/problems/0455.\345\210\206\345\217\221\351\245\274\345\271\262.md" @@ -0,0 +1,434 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 455.分发饼干 + +[力扣题目链接](https://leetcode.cn/problems/assign-cookies/) + +假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。 + +对每个孩子 i,都有一个胃口值  g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。 + +示例  1: + +- 输入: g = [1,2,3], s = [1,1] +- 输出: 1 + 解释:你有三个孩子和两块小饼干,3 个孩子的胃口值分别是:1,2,3。虽然你有两块小饼干,由于他们的尺寸都是 1,你只能让胃口值是 1 的孩子满足。所以你应该输出 1。 + +示例  2: + +- 输入: g = [1,2], s = [1,2,3] +- 输出: 2 +- 解释:你有两个孩子和三块小饼干,2 个孩子的胃口值分别是 1,2。你拥有的饼干数量和尺寸都足以让所有孩子满足。所以你应该输出 2. + +提示: + +- 1 <= g.length <= 3 \* 10^4 +- 0 <= s.length <= 3 \* 10^4 +- 1 <= g[i], s[j] <= 2^31 - 1 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,你想先喂哪个小孩?| LeetCode:455.分发饼干](https://www.bilibili.com/video/BV1MM411b7cq),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +为了满足更多的小孩,就不要造成饼干尺寸的浪费。 + +大尺寸的饼干既可以满足胃口大的孩子也可以满足胃口小的孩子,那么就应该优先满足胃口大的。 + +**这里的局部最优就是大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优就是喂饱尽可能多的小孩**。 + +可以尝试使用贪心策略,先将饼干数组和小孩数组排序。 + +然后从后向前遍历小孩数组,用大饼干优先满足胃口大的,并统计满足小孩数量。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20230405225628.png) + +这个例子可以看出饼干 9 只有喂给胃口为 7 的小孩,这样才是整体最优解,并想不出反例,那么就可以撸代码了。 + +C++代码整体如下: + +```CPP +// 版本一 +class Solution { +public: + int findContentChildren(vector& g, vector& s) { + sort(g.begin(), g.end()); + sort(s.begin(), s.end()); + int index = s.size() - 1; // 饼干数组的下标 + int result = 0; + for (int i = g.size() - 1; i >= 0; i--) { // 遍历胃口 + if (index >= 0 && s[index] >= g[i]) { // 遍历饼干 + result++; + index--; + } + } + return result; + } +}; +``` +* 时间复杂度:O(nlogn) +* 空间复杂度:O(1) + + +从代码中可以看出我用了一个 index 来控制饼干数组的遍历,遍历饼干并没有再起一个 for 循环,而是采用自减的方式,这也是常用的技巧。 + +有的同学看到要遍历两个数组,就想到用两个 for 循环,那样逻辑其实就复杂了。 + +### 注意事项 + +注意版本一的代码中,可以看出来,是先遍历的胃口,在遍历的饼干,那么可不可以 先遍历 饼干,在遍历胃口呢? + +其实是不可以的。 + +外面的 for 是里的下标 i 是固定移动的,而 if 里面的下标 index 是符合条件才移动的。 + +如果 for 控制的是饼干, if 控制胃口,就是出现如下情况 : + +![](https://file1.kamacoder.com/i/algo/20230112102848.png) + +if 里的 index 指向 胃口 10, for 里的 i 指向饼干 9,因为 饼干 9 满足不了 胃口 10,所以 i 持续向前移动,而 index 走不到` s[index] >= g[i]` 的逻辑,所以 index 不会移动,那么当 i 持续向前移动,最后所有的饼干都匹配不上。 + +所以 一定要 for 控制 胃口,里面的 if 控制饼干。 + +### 其他思路 + +**也可以换一个思路,小饼干先喂饱小胃口** + +代码如下: + +```CPP +class Solution { +public: + int findContentChildren(vector& g, vector& s) { + sort(g.begin(),g.end()); + sort(s.begin(),s.end()); + int index = 0; + for(int i = 0; i < s.size(); i++) { // 饼干 + if(index < g.size() && g[index] <= s[i]){ // 胃口 + index++; + } + } + return index; + } +}; +``` +* 时间复杂度:O(nlogn) +* 空间复杂度:O(1) + + +细心的录友可以发现,这种写法,两个循环的顺序改变了,先遍历的饼干,在遍历的胃口,这是因为遍历顺序变了,我们是从小到大遍历。 + +理由在上面 “注意事项”中 已经讲过。 + +## 总结 + +这道题是贪心很好的一道入门题目,思路还是比较容易想到的。 + +文中详细介绍了思考的过程,**想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心**。 + +## 其他语言版本 + +### Java + +```java +class Solution { + // 思路1:优先考虑饼干,小饼干先喂饱小胃口 + public int findContentChildren(int[] g, int[] s) { + Arrays.sort(g); + Arrays.sort(s); + int start = 0; + int count = 0; + for (int i = 0; i < s.length && start < g.length; i++) { + if (s[i] >= g[start]) { + start++; + count++; + } + } + return count; + } +} +``` + +```java +class Solution { + // 思路2:优先考虑胃口,先喂饱大胃口 + public int findContentChildren(int[] g, int[] s) { + Arrays.sort(g); + Arrays.sort(s); + int count = 0; + int start = s.length - 1; + // 遍历胃口 + for (int index = g.length - 1; index >= 0; index--) { + if(start >= 0 && g[index] <= s[start]) { + start--; + count++; + } + } + return count; + } +} +``` + +### Python +贪心 大饼干优先 +```python +class Solution: + def findContentChildren(self, g, s): + g.sort() # 将孩子的贪心因子排序 + s.sort() # 将饼干的尺寸排序 + index = len(s) - 1 # 饼干数组的下标,从最后一个饼干开始 + result = 0 # 满足孩子的数量 + for i in range(len(g)-1, -1, -1): # 遍历胃口,从最后一个孩子开始 + if index >= 0 and s[index] >= g[i]: # 遍历饼干 + result += 1 + index -= 1 + return result + +``` +贪心 小饼干优先 +```python +class Solution: + def findContentChildren(self, g, s): + g.sort() # 将孩子的贪心因子排序 + s.sort() # 将饼干的尺寸排序 + index = 0 + for i in range(len(s)): # 遍历饼干 + if index < len(g) and g[index] <= s[i]: # 如果当前孩子的贪心因子小于等于当前饼干尺寸 + index += 1 # 满足一个孩子,指向下一个孩子 + return index # 返回满足的孩子数目 + +``` + +栈 大饼干优先 +```python +from collecion import deque +class Solution: + def findContentChildren(self, g: List[int], s: List[int]) -> int: + #思路,饼干和孩子按从大到小排序,依次从栈中取出,若满足条件result += 1 否则将饼干栈顶元素重新返回 + result = 0 + queue_g = deque(sorted(g, reverse = True)) + queue_s = deque(sorted(s, reverse = True)) + while queue_g and queue_s: + child = queue_g.popleft() + cookies = queue_s.popleft() + if child <= cookies: + result += 1 + else: + queue_s.appendleft(cookies) + return result +``` + +### Go + +版本一 大饼干优先 +```Go +func findContentChildren(g []int, s []int) int { + sort.Ints(g) + sort.Ints(s) + index := len(s) - 1 + result := 0 + for i := len(g) - 1; i >= 0; i-- { + if index >= 0 && s[index] >= g[i] { + result++ + index-- + } + } + return result +} +``` + +版本二 小饼干优先 +```Go +func findContentChildren(g []int, s []int) int { + sort.Ints(g) + sort.Ints(s) + index := 0 + for i := 0; i < len(s); i++ { + if index < len(g) && g[index] <= s[i] { + index++ + } + } + return index +} +``` + +### Rust + +```rust +pub fn find_content_children(mut children: Vec, mut cookies: Vec) -> i32 { + children.sort(); + cookies.sort(); + + let (mut child, mut cookie) = (0, 0); + while child < children.len() && cookie < cookies.len() { + // 优先选择最小饼干喂饱孩子 + if children[child] <= cookies[cookie] { + child += 1; + } + cookie += 1; + } + child as i32 +} +``` + +### JavaScript + +```js +var findContentChildren = function (g, s) { + g = g.sort((a, b) => a - b); + s = s.sort((a, b) => a - b); + let result = 0; + let index = s.length - 1; + for (let i = g.length - 1; i >= 0; i--) { + if (index >= 0 && s[index] >= g[i]) { + result++; + index--; + } + } + return result; +}; +``` + +### TypeScript + +```typescript +// 大饼干尽量喂胃口大的 +function findContentChildren(g: number[], s: number[]): number { + g.sort((a, b) => a - b); + s.sort((a, b) => a - b); + const childLength: number = g.length, + cookieLength: number = s.length; + let curChild: number = childLength - 1, + curCookie: number = cookieLength - 1; + let resCount: number = 0; + while (curChild >= 0 && curCookie >= 0) { + if (g[curChild] <= s[curCookie]) { + curCookie--; + resCount++; + } + curChild--; + } + return resCount; +} +``` + +```typescript +// 小饼干先喂饱小胃口的 +function findContentChildren(g: number[], s: number[]): number { + g.sort((a, b) => a - b); + s.sort((a, b) => a - b); + const childLength: number = g.length, + cookieLength: number = s.length; + let curChild: number = 0, + curCookie: number = 0; + while (curChild < childLength && curCookie < cookieLength) { + if (g[curChild] <= s[curCookie]) { + curChild++; + } + curCookie++; + } + return curChild; +} +``` + +### C + +```c +///小餅乾先餵飽小胃口的 +int cmp(int* a, int* b) { + return *a - *b; +} + +int findContentChildren(int* g, int gSize, int* s, int sSize){ + if(sSize == 0) + return 0; + + //将两个数组排序为升序 + qsort(g, gSize, sizeof(int), cmp); + qsort(s, sSize, sizeof(int), cmp); + + int numFedChildren = 0; + int i = 0; + for(i = 0; i < sSize; ++i) { + if(numFedChildren < gSize && g[numFedChildren] <= s[i]) + numFedChildren++; + } + return numFedChildren; +} +``` + +```c +///大餅乾先餵飽大胃口的 +int cmp(int* a, int* b) { + return *a - *b; +} + +int findContentChildren(int* g, int gSize, int* s, int sSize){ + if(sSize == 0) + return 0; + + //将两个数组排序为升序 + qsort(g, gSize, sizeof(int), cmp); + qsort(s, sSize, sizeof(int), cmp); + + int count = 0; + int start = sSize - 1; + + for(int i = gSize - 1; i >= 0; i--) { + if(start >= 0 && s[start] >= g[i] ) { + start--; + count++; + } + } + return count; +} +``` + +### Scala + +```scala +object Solution { + def findContentChildren(g: Array[Int], s: Array[Int]): Int = { + var result = 0 + var children = g.sorted + var cookie = s.sorted + // 遍历饼干 + var j = 0 + for (i <- cookie.indices) { + if (j < children.size && cookie(i) >= children(j)) { + j += 1 + result += 1 + } + } + result + } +} +``` +### C# +```csharp +public class Solution +{ + public int FindContentChildren(int[] g, int[] s) + { + Array.Sort(g); + Array.Sort(s); + int index = s.Length - 1; + int res = 0; + for (int i = g.Length - 1; i >=0; i--) + { + if(index >=0 && s[index]>=g[i]) + { + res++; + index--; + } + } + return res; + } +} +``` + diff --git "a/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" new file mode 100755 index 0000000000..d164277731 --- /dev/null +++ "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,999 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> KMP算法还能干这个 + +# 459.重复的子字符串 + +[力扣题目链接](https://leetcode.cn/problems/repeated-substring-pattern/) + +给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。 + +示例 1: +* 输入: "abab" +* 输出: True +* 解释: 可由子字符串 "ab" 重复两次构成。 + +示例 2: +* 输入: "aba" +* 输出: False + +示例 3: +* 输入: "abcabcabcabc" +* 输出: True +* 解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[字符串这么玩,可有点难度! | LeetCode:459.重复的子字符串](https://www.bilibili.com/video/BV1cg41127fw),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +暴力的解法, 就是一个for循环获取 子串的终止位置, 然后判断子串是否能重复构成字符串,又嵌套一个for循环,所以是O(n^2)的时间复杂度。 + +有的同学可以想,怎么一个for循环就可以获取子串吗? 至少得一个for获取子串起始位置,一个for获取子串结束位置吧。 + +其实我们只需要判断,以第一个字母为开始的子串就可以,所以一个for循环获取子串的终止位置就行了。 而且遍历的时候 都不用遍历结束,只需要遍历到中间位置,因为子串结束位置大于中间位置的话,一定不能重复组成字符串。 + +暴力的解法,这里就不详细讲解了。 + +主要讲一讲移动匹配 和 KMP两种方法。 + +### 移动匹配 + +当一个字符串s:abcabc,内部由重复的子串组成,那么这个字符串的结构一定是这样的: + +![图一](https://file1.kamacoder.com/i/algo/20220728104518.png) + +也就是由前后相同的子串组成。 + +那么既然前面有相同的子串,后面有相同的子串,用 s + s,这样组成的字符串中,后面的子串做前串,前面的子串做后串,就一定还能组成一个s,如图: + +![图二](https://file1.kamacoder.com/i/algo/20220728104931.png) + + +当然,我们在判断 s + s 拼接的字符串里是否出现一个s的的时候,**要刨除 s + s 的首字符和尾字符**,这样避免在s+s中搜索出原来的s,我们要搜索的是中间拼接出来的s。 + + +以上证明的充分性,接下来证明必要性: + +如果有一个字符串s,在 s + s 拼接后, 不算首尾字符,如果能凑成s字符串,说明s 一定是重复子串组成。 + +如图,字符串s,图中数字为数组下标,在 s + s 拼接后, 不算首尾字符,中间凑成s字符串。 (图中数字为数组下标) + +![](https://file1.kamacoder.com/i/algo/20240910115555.png) + +图中,因为中间拼接成了s,根据红色框 可以知道 s[4] = s[0], s[5] = s[1], s[0] = s[2], s[1] = s[3] s[2] = s[4] ,s[3] = s[5] + +![](https://file1.kamacoder.com/i/algo/20240910115819.png) + +以上相等关系我们串联一下: + +s[4] = s[0] = s[2] + +s[5] = s[1] = s[3] + + +即:s[4],s[5] = s[0],s[1] = s[2],s[3] + +**说明这个字符串,是由 两个字符 s[0] 和 s[1] 重复组成的**! + +这里可以有录友想,凭什么就是这样组成的s呢,我换一个方式组成s 行不行,如图: + +![](https://file1.kamacoder.com/i/algo/20240910120751.png) + +s[3] = s[0],s[4] = s[1] ,s[5] = s[2],s[0] = s[3],s[1] = s[4],s[2] = s[5] + +以上相等关系串联: + +s[3] = s[0] + +s[1] = s[4] + +s[2] = s[5] + +s[0] s[1] s[2] = s[3] s[4] s[5] + +和以上推导过程一样,最后可以推导出,这个字符串是由 s[0] ,s[1] ,s[2] 重复组成。 + +如果是这样的呢,如图: + +![](https://file1.kamacoder.com/i/algo/20240910121236.png) + +s[1] = s[0],s[2] = s[1] ,s[3] = s[2],s[4] = s[3],s[5] = s[4],s[0] = s[5] + +以上相等关系串联 + +s[0] = s[1] = s[2] = s[3] = s[4] = s[5] + +最后可以推导出,这个字符串是由 s[0] 重复组成。 + +以上 充分和必要性都证明了,所以判断字符串s是否由重复子串组成,只要两个s拼接在一起,里面还出现一个s的话,就说明是由重复子串组成。 + + +代码如下: + +```CPP +class Solution { +public: + bool repeatedSubstringPattern(string s) { + string t = s + s; + t.erase(t.begin()); t.erase(t.end() - 1); // 掐头去尾 + if (t.find(s) != std::string::npos) return true; // r + return false; + } +}; +``` +* 时间复杂度: O(n) +* 空间复杂度: O(1) + +不过这种解法还有一个问题,就是 我们最终还是要判断 一个字符串(s + s)是否出现过 s 的过程,大家可能直接用contains,find 之类的库函数, 却忽略了实现这些函数的时间复杂度(暴力解法是m * n,一般库函数实现为 O(m + n))。 + +如果我们做过 [28.实现strStr](https://programmercarl.com/0028.实现strStr.html) 题目的话,其实就知道,**实现一个 高效的算法来判断 一个字符串中是否出现另一个字符串是很复杂的**,这里就涉及到了KMP算法。 + +### KMP + +#### 为什么会使用KMP + +以下使用KMP方式讲解,强烈建议大家先把以下两个视频看了,理解KMP算法,再来看下面讲解,否则会很懵。 + +* [视频讲解版:帮你把KMP算法学个通透!(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) +* [视频讲解版:帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) +* [文字讲解版:KMP算法](https://programmercarl.com/0028.实现strStr.html) + +在一个串中查找是否出现过另一个串,这是KMP的看家本领。那么寻找重复子串怎么也涉及到KMP算法了呢? + +KMP算法中next数组为什么遇到字符不匹配的时候可以找到上一个匹配过的位置继续匹配,靠的是有计算好的前缀表。 + +前缀表里,统计了各个位置为终点字符串的最长相同前后缀的长度。 + +那么 最长相同前后缀和重复子串的关系又有什么关系呢。 + +可能很多录友又忘了 前缀和后缀的定义,再回顾一下: + +* 前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串; +* 后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串 + +#### 充分性证明 + +如果一个字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串一定是字符串s的最小重复子串。 + +如果s 是由最小重复子串p组成,即 s = n * p + +那么相同前后缀可以是这样: + +![](https://file1.kamacoder.com/i/algo/20240913110257.png) + +也可以是这样: + +![](https://file1.kamacoder.com/i/algo/20240913110316.png) + +最长的相等前后缀,也就是这样: + +![](https://file1.kamacoder.com/i/algo/20240913110841.png) + +这里有录友就想:如果字符串s 是由最小重复子串p组成,最长相等前后缀就不能更长一些? 例如这样: + +![](https://file1.kamacoder.com/i/algo/20240913114348.png) + +如果这样的话,因为前后缀要相同,所以 p2 = p1,p3 = p2,如图: + +![](https://file1.kamacoder.com/i/algo/20240913114818.png) + +p2 = p1,p3 = p2 即: p1 = p2 = p3 + +说明 p = p1 * 3。 + +这样p 就不是最小重复子串了,不符合我们定义的条件。 + +所以,**如果这个字符串s是由重复子串组成,那么最长相等前后缀不包含的子串是字符串s的最小重复子串**。 + +#### 必要性证明 + +以上是充分性证明,以下是必要性证明: + +**如果 最长相等前后缀不包含的子串是字符串s的最小重复子串, 那么字符串s一定由重复子串组成吗**? + +最长相等前后缀不包含的子串已经是字符串s的最小重复子串,那么字符串s一定由重复子串组成,这个不需要证明了。 + +关键是要证明:最长相等前后缀不包含的子串什么时候才是字符串s的最小重复子串呢。 + +情况一, 最长相等前后缀不包含的子串的长度 比 字符串s的一半的长度还大,那一定不是字符串s的重复子串,如图: + +![](https://file1.kamacoder.com/i/algo/20240911110236.png) + +图中:前后缀不包含的子串的长度 大于 字符串s的长度的 二分之一 + +-------------- + +情况二,最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,如图: + +![](https://file1.kamacoder.com/i/algo/20240910174249.png) + +步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,所以 s[0] 一定和 s[2]相同,s[1] 一定和 s[3]相同,即:,s[0]s[1]与s[2]s[3]相同 。 + +步骤二: 因为在同一个字符串位置,所以 t[2] 与 k[0]相同,t[3] 与 k[1]相同。 + +步骤三: 因为 这是相等的前缀和后缀,t[2] 与 k[2]相同 ,t[3]与k[3] 相同,所以,s[2]一定和s[4]相同,s[3]一定和s[5]相同,即:s[2]s[3] 与 s[4]s[5]相同。 + +步骤四:循环往复。 + +所以字符串s,s[0]s[1]与s[2]s[3]相同, s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同。 + +可以推出,在由重复子串组成的字符串中,最长相等前后缀不包含的子串就是最小重复子串。 + +即 s[0]s[1] 是最小重复子串 + + +以上推导中,录友可能想,你怎么知道 s[0] 和 s[1] 就不相同呢? s[0] 为什么就不能是最小重复子串。 + +如果 s[0] 和 s[1] 也相同,同时 s[0]s[1]与s[2]s[3]相同,s[2]s[3] 与 s[4]s[5]相同,s[4]s[5] 与 s[6]s[7] 相同,那么这个字符串就是有一个字符构成的字符串。 + +那么它的最长相同前后缀,就不是上图中的前后缀,而是这样的的前后缀: + +![](https://file1.kamacoder.com/i/algo/20240910175053.png) + +录友可能再问,由一个字符组成的字符串,最长相等前后缀凭什么就是这样的。 + +有这种疑惑的录友,就是还不知道 最长相等前后缀 是怎么算的。 + +可以看这里:[KMP讲解](https://programmercarl.com/0028.%E5%AE%9E%E7%8E%B0strStr.html),再去回顾一下。 + +或者说,自己举个例子,`aaaaaa`,这个字符串,他的最长相等前后缀是什么? + +同上以上推导,最长相等前后缀不包含的子串的长度只要被 字符串s的长度整除,最长相等前后缀不包含的子串一定是最小重复子串。 + +---------------- + +**情况三,最长相等前后缀不包含的子串的长度 不被 字符串s的长度整除得情况**,如图: + +![](https://file1.kamacoder.com/i/algo/20240913115854.png) + + +步骤一:因为 这是相等的前缀和后缀,t[0] 与 k[0]相同, t[1] 与 k[1]相同,t[2] 与 k[2]相同。 + +所以 s[0] 与 s[3]相同,s[1] 与 s[4]相同,s[2] 与s[5],即:,s[0]s[1]与s[2]s[3]相同 。 + +步骤二: 因为在同一个字符串位置,所以 t[3] 与 k[0]相同,t[4] 与 k[1]相同。 + + +步骤三: 因为 这是相等的前缀和后缀,t[3] 与 k[3]相同 ,t[4]与k[5] 相同,所以,s[3]一定和s[6]相同,s[4]一定和s[7]相同,即:s[3]s[4] 与 s[6]s[7]相同。 + + +以上推导,可以得出 s[0],s[1],s[2] 与 s[3],s[4],s[5] 相同,s[3]s[4] 与 s[6]s[7]相同。 + +那么 最长相等前后缀不包含的子串的长度 不被 字符串s的长度整除 ,最长相等前后缀不包含的子串就不是s的重复子串 + +----------- + +充分条件:如果字符串s是由重复子串组成,那么 最长相等前后缀不包含的子串 一定是 s的最小重复子串。 + +必要条件:如果字符串s的最长相等前后缀不包含的子串 是 s最小重复子串,那么 s是由重复子串组成。 + +在必要条件,这个是 显而易见的,都已经假设 最长相等前后缀不包含的子串 是 s的最小重复子串了,那s必然是重复子串。 + +**关键是需要证明, 字符串s的最长相等前后缀不包含的子串 什么时候才是 s最小重复子串**。 + +同上我们证明了,当 最长相等前后缀不包含的子串的长度 可以被 字符串s的长度整除,那么不包含的子串 就是s的最小重复子串。 + + +------------- + + +### 代码分析 + +next 数组记录的就是最长相同前后缀( [字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html)), 如果 `next[len - 1] != -1`,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。 + +最长相等前后缀的长度为:`next[len - 1] + 1`。(这里的next数组是以统一减一的方式计算的,因此需要+1,两种计算next数组的具体区别看这里:[字符串:KMP算法精讲](https://programmercarl.com/0028.实现strStr.html)) + +数组长度为:len。 + +`len - (next[len - 1] + 1)` 是最长相等前后缀不包含的子串的长度。 + +如果`len % (len - (next[len - 1] + 1)) == 0` ,则说明数组的长度正好可以被 最长相等前后缀不包含的子串的长度 整除 ,说明该字符串有重复的子字符串。 + +### 打印数组 + +**强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法** + +如图: + +![459.重复的子字符串_1](https://file1.kamacoder.com/i/algo/459.重复的子字符串_1.png) + +`next[len - 1] = 7`,`next[len - 1] + 1 = 8`,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。 + +`(len - (next[len - 1] + 1))` 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 为最长相同前后缀不包含的子串长度 + + +4可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。 + +### 代码实现 + +C++代码如下:(这里使用了前缀表统一减一的实现方式) + +```CPP +class Solution { +public: + void getNext (int* next, const string& s){ + next[0] = -1; + int j = -1; + for(int i = 1;i < s.size(); i++){ + while(j >= 0 && s[i] != s[j + 1]) { + j = next[j]; + } + if(s[i] == s[j + 1]) { + j++; + } + next[i] = j; + } + } + bool repeatedSubstringPattern (string s) { + if (s.size() == 0) { + return false; + } + int next[s.size()]; + getNext(next, s); + int len = s.size(); + if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) { + return true; + } + return false; + } +}; +``` +* 时间复杂度: O(n) +* 空间复杂度: O(n) + + +前缀表(不减一)的C++代码实现: + +```CPP +class Solution { +public: + void getNext (int* next, const string& s){ + next[0] = 0; + int j = 0; + for(int i = 1;i < s.size(); i++){ + while(j > 0 && s[i] != s[j]) { + j = next[j - 1]; + } + if(s[i] == s[j]) { + j++; + } + next[i] = j; + } + } + bool repeatedSubstringPattern (string s) { + if (s.size() == 0) { + return false; + } + int next[s.size()]; + getNext(next, s); + int len = s.size(); + if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) { + return true; + } + return false; + } +}; +``` +* 时间复杂度: O(n) +* 空间复杂度: O(n) + + +## 其他语言版本 + +### Java: + +(版本一) 前缀表 减一 + +```java +class Solution { + public boolean repeatedSubstringPattern(String s) { + if (s.equals("")) return false; + + int len = s.length(); + // 原串加个空格(哨兵),使下标从1开始,这样j从0开始,也不用初始化了 + s = " " + s; + char[] chars = s.toCharArray(); + int[] next = new int[len + 1]; + + // 构造 next 数组过程,j从0开始(空格),i从2开始 + for (int i = 2, j = 0; i <= len; i++) { + // 匹配不成功,j回到前一位置 next 数组所对应的值 + while (j > 0 && chars[i] != chars[j + 1]) j = next[j]; + // 匹配成功,j往后移 + if (chars[i] == chars[j + 1]) j++; + // 更新 next 数组的值 + next[i] = j; + } + + // 最后判断是否是重复的子字符串,这里 next[len] 即代表next数组末尾的值 + if (next[len] > 0 && len % (len - next[len]) == 0) { + return true; + } + return false; + } +} +``` + +(版本二) 前缀表 不减一 + +```java +/* + * 充分条件:如果字符串s是由重复子串组成的,那么它的最长相等前后缀不包含的子串一定是s的最小重复子串。 + * 必要条件:如果字符串s的最长相等前后缀不包含的子串是s的最小重复子串,那么s必然是由重复子串组成的。 + * 推得:当字符串s的长度可以被其最长相等前后缀不包含的子串的长度整除时,不包含的子串就是s的最小重复子串。 + * + * 时间复杂度:O(n) + * 空间复杂度:O(n) + */ +class Solution { + public boolean repeatedSubstringPattern(String s) { + // if (s.equals("")) return false; + // 边界判断(可以去掉,因为题目给定范围是1 <= s.length <= 10^4) + int n = s.length(); + + // Step 1.构建KMP算法的前缀表 + int[] next = new int[n]; // 前缀表的值表示 以该位置结尾的字符串的最长相等前后缀的长度 + int j = 0; + next[0] = 0; + for (int i = 1; i < n; i++) { + while (j > 0 && s.charAt(i) != s.charAt(j)) // 只要前缀后缀还不一致,就根据前缀表回退j直到起点为止 + j = next[j - 1]; + if (s.charAt(i) == s.charAt(j)) + j++; + next[i] = j; + } + + // Step 2.判断重复子字符串 + if (next[n - 1] > 0 && n % (n - next[n - 1]) == 0) { // 当字符串s的长度可以被其最长相等前后缀不包含的子串的长度整除时 + return true; // 不包含的子串就是s的最小重复子串 + } else { + return false; + } + } +} +``` + +### Python: + +(版本一) 前缀表 减一 +```python +class Solution: + def repeatedSubstringPattern(self, s: str) -> bool: + if len(s) == 0: + return False + nxt = [0] * len(s) + self.getNext(nxt, s) + if nxt[-1] != -1 and len(s) % (len(s) - (nxt[-1] + 1)) == 0: + return True + return False + + def getNext(self, nxt, s): + nxt[0] = -1 + j = -1 + for i in range(1, len(s)): + while j >= 0 and s[i] != s[j+1]: + j = nxt[j] + if s[i] == s[j+1]: + j += 1 + nxt[i] = j + return nxt +``` + +(版本二) 前缀表 不减一 + +```python +class Solution: + def repeatedSubstringPattern(self, s: str) -> bool: + if len(s) == 0: + return False + nxt = [0] * len(s) + self.getNext(nxt, s) + if nxt[-1] != 0 and len(s) % (len(s) - nxt[-1]) == 0: + return True + return False + + def getNext(self, nxt, s): + nxt[0] = 0 + j = 0 + for i in range(1, len(s)): + while j > 0 and s[i] != s[j]: + j = nxt[j - 1] + if s[i] == s[j]: + j += 1 + nxt[i] = j + return nxt +``` + + +(版本三) 使用 find + +```python +class Solution: + def repeatedSubstringPattern(self, s: str) -> bool: + n = len(s) + if n <= 1: + return False + ss = s[1:] + s[:-1] + print(ss.find(s)) + return ss.find(s) != -1 +``` + +(版本四) 暴力法 + +```python +class Solution: + def repeatedSubstringPattern(self, s: str) -> bool: + n = len(s) + if n <= 1: + return False + + substr = "" + for i in range(1, n//2 + 1): + if n % i == 0: + substr = s[:i] + if substr * (n//i) == s: + return True + + return False +``` + +### Go: + +这里使用了前缀表统一减一的实现方式 + +```go +func repeatedSubstringPattern(s string) bool { + n := len(s) + if n == 0 { + return false + } + next := make([]int, n) + j := -1 + next[0] = j + for i := 1; i < n; i++ { + for j >= 0 && s[i] != s[j+1] { + j = next[j] + } + if s[i] == s[j+1] { + j++ + } + next[i] = j + } + // next[n-1]+1 最长相同前后缀的长度 + if next[n-1] != -1 && n%(n-(next[n-1]+1)) == 0 { + return true + } + return false +} +``` + +前缀表(不减一)的代码实现 + +```go +func repeatedSubstringPattern(s string) bool { + n := len(s) + if n == 0 { + return false + } + j := 0 + next := make([]int, n) + next[0] = j + for i := 1; i < n; i++ { + for j > 0 && s[i] != s[j] { + j = next[j-1] + } + if s[i] == s[j] { + j++ + } + next[i] = j + } + // next[n-1] 最长相同前后缀的长度 + if next[n-1] != 0 && n%(n-next[n-1]) == 0 { + return true + } + return false +} +``` + +移动匹配 + +```go +func repeatedSubstringPattern(s string) bool { + if len(s) == 0 { + return false + } + t := s + s + return strings.Contains(t[1:len(t)-1], s) +} +``` + +### JavaScript: + +> 前缀表统一减一 + +```javascript +/** + * @param {string} s + * @return {boolean} + */ +var repeatedSubstringPattern = function (s) { + if (s.length === 0) + return false; + + const getNext = (s) => { + let next = []; + let j = -1; + + next.push(j); + + for (let i = 1; i < s.length; ++i) { + while (j >= 0 && s[i] !== s[j + 1]) + j = next[j]; + if (s[i] === s[j + 1]) + j++; + next.push(j); + } + + return next; + } + + let next = getNext(s); + + if (next[next.length - 1] !== -1 && s.length % (s.length - (next[next.length - 1] + 1)) === 0) + return true; + return false; +}; +``` + +> 前缀表统一不减一 + +```javascript +/** + * @param {string} s + * @return {boolean} + */ +var repeatedSubstringPattern = function (s) { + if (s.length === 0) + return false; + + const getNext = (s) => { + let next = []; + let j = 0; + + next.push(j); + + for (let i = 1; i < s.length; ++i) { + while (j > 0 && s[i] !== s[j]) + j = next[j - 1]; + if (s[i] === s[j]) + j++; + next.push(j); + } + + return next; + } + + let next = getNext(s); + + if (next[next.length - 1] !== 0 && s.length % (s.length - next[next.length - 1]) === 0) + return true; + return false; +}; +``` + +> 正则匹配 +```javascript +/** + * @param {string} s + * @return {boolean} + */ +var repeatedSubstringPattern = function(s) { + let reg = /^(\w+)\1+$/ + return reg.test(s) +}; +``` +> 移动匹配 +```javascript +/** + * @param {string} s + * @return {boolean} + */ +var repeatedSubstringPattern = function (s) { + let ss = s + s; + return ss.substring(1, ss.length - 1).includes(s); +}; +``` + +### TypeScript: + +> 前缀表统一减一 + +```typescript +function repeatedSubstringPattern(s: string): boolean { + function getNext(str: string): number[] { + let next: number[] = []; + let j: number = -1; + next[0] = j; + for (let i = 1, length = str.length; i < length; i++) { + while (j >= 0 && str[i] !== str[j + 1]) { + j = next[j]; + } + if (str[i] === str[j + 1]) { + j++; + } + next[i] = j; + } + return next; + } + + let next: number[] = getNext(s); + let sLength: number = s.length; + let nextLength: number = next.length; + let suffixLength: number = next[nextLength - 1] + 1; + if (suffixLength > 0 && sLength % (sLength - suffixLength) === 0) return true; + return false; +}; +``` + +> 前缀表不减一 + +```typescript +function repeatedSubstringPattern(s: string): boolean { + function getNext(str: string): number[] { + let next: number[] = []; + let j: number = 0; + next[0] = j; + for (let i = 1, length = str.length; i < length; i++) { + while (j > 0 && str[i] !== str[j]) { + j = next[j - 1]; + } + if (str[i] === str[j]) { + j++; + } + next[i] = j; + } + return next; + } + + let next: number[] = getNext(s); + let sLength: number = s.length; + let nextLength: number = next.length; + let suffixLength: number = next[nextLength - 1]; + if (suffixLength > 0 && sLength % (sLength - suffixLength) === 0) return true; + return false; +}; +``` + +### Swift: + +> 前缀表统一减一 +```swift + func repeatedSubstringPattern(_ s: String) -> Bool { + + let sArr = Array(s) + let len = s.count + if len == 0 { + return false + } + var next = Array.init(repeating: -1, count: len) + + getNext(&next,sArr) + + if next.last != -1 && len % (len - (next[len-1] + 1)) == 0{ + return true + } + + return false + } + + func getNext(_ next: inout [Int], _ str:[Character]) { + + var j = -1 + next[0] = j + + for i in 1 ..< str.count { + + while j >= 0 && str[j+1] != str[i] { + j = next[j] + } + + if str[i] == str[j+1] { + j += 1 + } + + next[i] = j + } + } +``` + +> 前缀表统一不减一 +```swift + func repeatedSubstringPattern(_ s: String) -> Bool { + + let sArr = Array(s) + let len = sArr.count + if len == 0 { + return false + } + + var next = Array.init(repeating: 0, count: len) + getNext(&next, sArr) + + if next[len-1] != 0 && len % (len - next[len-1]) == 0 { + return true + } + + return false + } + + // 前缀表不减一 + func getNext(_ next: inout [Int], _ sArr:[Character]) { + + var j = 0 + next[0] = 0 + + for i in 1 ..< sArr.count { + + while j > 0 && sArr[i] != sArr[j] { + j = next[j-1] + } + + if sArr[i] == sArr[j] { + j += 1 + } + + next[i] = j + } + } +``` + +### Rust: + +>前缀表统一不减一 +```Rust +impl Solution { + pub fn get_next(next: &mut Vec, s: &Vec) { + let len = s.len(); + let mut j = 0; + for i in 1..len { + while j > 0 && s[i] != s[j] { + j = next[j - 1]; + } + if s[i] == s[j] { + j += 1; + } + next[i] = j; + } + } + + pub fn repeated_substring_pattern(s: String) -> bool { + let s = s.chars().collect::>(); + let len = s.len(); + if len == 0 { return false; }; + let mut next = vec![0; len]; + Self::get_next(&mut next, &s); + if next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0 { return true; } + return false; + } +} +``` + +>前缀表统一减一 + +```rust +impl Solution { + pub fn get_next(next_len: usize, s: &Vec) -> Vec { + let mut next = vec![-1; next_len]; + let mut j = -1; + for i in 1..s.len() { + while j >= 0 && s[i] != s[(j + 1) as usize] { + j = next[j as usize]; + } + if s[i] == s[(j + 1) as usize] { + j += 1; + } + next[i] = j; + } + next + } + pub fn repeated_substring_pattern(s: String) -> bool { + let s_chars = s.chars().collect::>(); + let next = Self::get_next(s_chars.len(), &s_chars); + if next[s_chars.len() - 1] >= 0 + && s_chars.len() % (s_chars.len() - (next[s_chars.len() - 1] + 1) as usize) == 0 + { + return true; + } + false + } +} +``` +### C# + +> 前缀表不减一 + +```csharp +public bool RepeatedSubstringPattern(string s) +{ + if (s.Length == 0) + return false; + int[] next = GetNext(s); + int len = s.Length; + if (next[len - 1] != 0 && len % (len - next[len - 1]) == 0) return true; + return false; +} +public int[] GetNext(string s) +{ + int[] next = Enumerable.Repeat(0, s.Length).ToArray(); + for (int i = 1, j = 0; i < s.Length; i++) + { + while (j > 0 && s[i] != s[j]) + j = next[j - 1]; + if (s[i] == s[j]) + j++; + next[i] = j; + } + return next; +} +``` + +> 移动匹配 +```csharp +public bool RepeatedSubstringPattern(string s) { + string ss = (s + s).Substring(1, (s + s).Length - 2); + return ss.Contains(s); +} +``` +### C + +```c +// 前缀表不减一 +int *build_next(char* s, int len) { + + int *next = (int *)malloc(len * sizeof(int)); + assert(next); + + // 初始化前缀表 + next[0] = 0; + + // 构建前缀表表 + int i = 1, j = 0; + while (i < len) { + if (s[i] == s[j]) { + j++; + next[i] = j; + i++; + } else if (j > 0) { + j = next[j - 1]; + } else { + next[i] = 0; + i++; + } + } + return next; +} + +bool repeatedSubstringPattern(char* s) { + + int len = strlen(s); + int *next = build_next(s, len); + bool result = false; + + // 检查最小重复片段能否被长度整除 + if (next[len - 1]) { + result = len % (len - next[len - 1]) == 0; + } + + free(next); + return result; +} + +``` + + diff --git "a/problems/0463.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" "b/problems/0463.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" new file mode 100755 index 0000000000..ba60bc4564 --- /dev/null +++ "b/problems/0463.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" @@ -0,0 +1,433 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 463. 岛屿的周长 + +[力扣题目链接](https://leetcode.cn/problems/island-perimeter/) + +给定一个 row x col 的二维网格地图 grid ,其中:grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域。 + +网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。 + +岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。 + + +![](https://file1.kamacoder.com/i/algo/20230829180848.png) + +* 输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]] +* 输出:16 +* 解释:它的周长是上面图片中的 16 个黄色的边 + + +示例 2: + +* 输入:grid = [[1]] +* 输出:4 + +示例 3: + +* 输入:grid = [[1,0]] +* 输出:4 + +提示: + +* row == grid.length +* col == grid[i].length +* 1 <= row, col <= 100 +* grid[i][j] 为 0 或 1 + +## 思路 + +岛屿问题最容易让人想到BFS或者DFS,但是这道题还真的没有必要,别把简单问题搞复杂了。 + +### 解法一: + +遍历每一个空格,遇到岛屿,计算其上下左右的情况,遇到水域或者出界的情况,就可以计算边了。 + +如图: + + + +C++代码如下:(详细注释) + +```CPP +class Solution { +public: + int direction[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; + int islandPerimeter(vector>& grid) { + int result = 0; + for (int i = 0; i < grid.size(); i++) { + for (int j = 0; j < grid[0].size(); j++) { + if (grid[i][j] == 1) { + for (int k = 0; k < 4; k++) { // 上下左右四个方向 + int x = i + direction[k][0]; + int y = j + direction[k][1]; // 计算周边坐标x,y + if (x < 0 // i在边界上 + || x >= grid.size() // i在边界上 + || y < 0 // j在边界上 + || y >= grid[0].size() // j在边界上 + || grid[x][y] == 0) { // x,y位置是水域 + result++; + } + } + } + } + } + return result; + } +}; +``` + +### 解法二: + +计算出总的岛屿数量,因为有一对相邻两个陆地,边的总数就减2,那么在计算出相邻岛屿的数量就可以了。 + +result = 岛屿数量 * 4 - cover * 2; + +如图: + + + +C++代码如下:(详细注释) + +```CPP +class Solution { +public: + int islandPerimeter(vector>& grid) { + int sum = 0; // 陆地数量 + int cover = 0; // 相邻数量 + for (int i = 0; i < grid.size(); i++) { + for (int j = 0; j < grid[0].size(); j++) { + if (grid[i][j] == 1) { + sum++; + // 统计上边相邻陆地 + if(i - 1 >= 0 && grid[i - 1][j] == 1) cover++; + // 统计左边相邻陆地 + if(j - 1 >= 0 && grid[i][j - 1] == 1) cover++; + // 为什么没统计下边和右边? 因为避免重复计算 + } + } + } + return sum * 4 - cover * 2; + } +}; +``` + + +## 其他语言版本 + +### Java: + +```java +// 解法一 +class Solution { + // 上下左右 4 个方向 + int[] dirx = {-1, 1, 0, 0}; + int[] diry = {0, 0, -1, 1}; + + public int islandPerimeter(int[][] grid) { + int m = grid.length; + int n = grid[0].length; + int res = 0; // 岛屿周长 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 1) { + for (int k = 0; k < 4; k++) { + int x = i + dirx[k]; + int y = j + diry[k]; + // 当前位置是陆地,并且从当前位置4个方向扩展的“新位置”是“水域”或“新位置“越界,则会为周长贡献一条边 + if (x < 0 || x >= m || y < 0 || y >= n || grid[x][y] == 0) { + res++; + continue; + } + } + } + } + } + return res; + } +} + +// 解法二 +class Solution { + public int islandPerimeter(int[][] grid) { + // 计算岛屿的周长 + // 方法二 : 遇到相邻的陆地总周长就-2 + int landSum = 0; // 陆地数量 + int cover = 0; // 相邻陆地数量 + for (int i = 0; i < grid.length; i++) { + for (int j = 0; j < grid[0].length; j++) { + if (grid[i][j] == 1) { + landSum++; + // 统计上面和左边的相邻陆地 + if(i - 1 >= 0 && grid[i-1][j] == 1) cover++; + if(j - 1 >= 0 && grid[i][j-1] == 1) cover++; + } + } + } + return landSum * 4 - cover * 2; + } +} +// 延伸 - 傳統DFS解法(使用visited數組)(遇到邊界 或是 海水 就edge ++) +class Solution { + int dir[][] ={ + {0, 1}, + {0, -1}, + {1, 0}, + {-1, 0} + }; + + boolean visited[][]; + int res = 0; + + public int islandPerimeter(int[][] grid) { + int row = grid.length; + int col = grid[0].length; + visited = new boolean[row][col]; + + int result = 0; + + for(int i = 0; i < row; i++){ + for(int j = 0; j < col; j++){ + if(visited[i][j] == false && grid[i][j] == 1) + result += dfs(grid, i, j); + } + } + return result; + } + + private int dfs(int[][] grid, int x, int y){ + //如果遇到 邊界(x < 0 || y < 0 || x >= grid.length || y >= grid[0].length)或是 遇到海水(grid[x][y] == 0)就return 1(edge + 1) + if(x < 0 || y < 0 || x >= grid.length || y >= grid[0].length || grid[x][y] == 0) + return 1; + //如果該地已經拜訪過,就return 0 避免重複計算 + if(visited[x][y]) + return 0; + int temp = 0; + visited[x][y] = true; + for(int i = 0; i < 4; i++){ + int nextX = x + dir[i][0]; + int nextY = y + dir[i][1]; + //用temp 把edge存起來 + temp +=dfs(grid, nextX, nextY); + } + return temp; + } +} + +``` + +### Python: + +扫描每个cell,如果当前位置为岛屿 grid[i][j] == 1, 从当前位置判断四边方向,如果边界或者是水域,证明有边界存在,res矩阵的对应cell加一。 + +```python +class Solution: + def islandPerimeter(self, grid: List[List[int]]) -> int: + + m = len(grid) + n = len(grid[0]) + + # 创建res二维素组记录答案 + res = [[0] * n for j in range(m)] + + for i in range(m): + for j in range(len(grid[i])): + # 如果当前位置为水域,不做修改或reset res[i][j] = 0 + if grid[i][j] == 0: + res[i][j] = 0 + # 如果当前位置为陆地,往四个方向判断,update res[i][j] + elif grid[i][j] == 1: + if i == 0 or (i > 0 and grid[i-1][j] == 0): + res[i][j] += 1 + if j == 0 or (j >0 and grid[i][j-1] == 0): + res[i][j] += 1 + if i == m-1 or (i < m-1 and grid[i+1][j] == 0): + res[i][j] += 1 + if j == n-1 or (j < n-1 and grid[i][j+1] == 0): + res[i][j] += 1 + + # 最后求和res矩阵,这里其实不一定需要矩阵记录,可以设置一个variable res 记录边长,舍矩阵无非是更加形象而已 + ans = sum([sum(row) for row in res]) + + return ans + +``` + +### Go: + +```go +func islandPerimeter(grid [][]int) int { + m, n := len(grid), len(grid[0]) + res := 0 + for i := 0; i < m; i++ { + for j := 0; j < n; j++ { + if grid[i][j] == 1 { + res += 4 + // 上下左右四个方向 + if i > 0 && grid[i-1][j] == 1 {res--} // 上边有岛屿 + if i < m-1 && grid[i+1][j] == 1 {res--} // 下边有岛屿 + if j > 0 && grid[i][j-1] == 1 {res--} // 左边有岛屿 + if j < n-1 && grid[i][j+1] == 1 {res--} // 右边有岛屿 + } + } + } + return res +} +``` + +### JavaScript: + +```javascript +//解法一 +var islandPerimeter = function(grid) { + // 上下左右 4 个方向 + const dirx = [-1, 1, 0, 0], diry = [0, 0, -1, 1]; + const m = grid.length, n = grid[0].length; + let res = 0; //岛屿周长 + for(let i = 0; i < m; i++){ + for(let j = 0; j < n; j++){ + if(grid[i][j] === 1){ + for(let k = 0; k < 4; k++){ //上下左右四个方向 + // 计算周边坐标的x,y + let x = i + dirx[k]; + let y = j + diry[k]; + // 四个方向扩展的新位置是水域或者越界就会为周长贡献1 + if(x < 0 // i在边界上 + || x >= m // i在边界上 + || y < 0 // j在边界上 + || y >= n // j在边界上 + || grid[x][y] === 0){ // (x,y)位置是水域 + res++; + continue; + } + } + } + } + } + return res; +}; + +//解法二 +var islandPerimeter = function(grid) { + let sum = 0; // 陆地数量 + let cover = 0; // 相邻数量 + for(let i = 0; i < grid.length; i++){ + for(let j = 0; j = 0 && grid[i-1][j] === 1) cover++; + // 统计左边相邻陆地 + if(j - 1 >= 0 && grid[i][j-1] === 1) cover++; + // 为什么没统计下边和右边? 因为避免重复计算 + } + } + } + return sum * 4 - cover * 2; +}; +``` + +TypeScript: + +```typescript +/** + * 方法一:深度优先搜索(DFS) + * @param grid 二维网格地图,其中 grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域 + * @returns 岛屿的周长 + */ +function islandPerimeter(grid: number[][]): number { + // 处理特殊情况:网格为空或行列数为 0,直接返回 0 + if (!grid || grid.length === 0 || grid[0].length === 0) { + return 0; + } + + // 获取网格的行数和列数 + const rows = grid.length; + const cols = grid[0].length; + let perimeter = 0; // 岛屿的周长 + + /** + * 深度优先搜索函数 + * @param i 当前格子的行索引 + * @param j 当前格子的列索引 + */ + const dfs = (i: number, j: number) => { + // 如果当前位置超出网格范围,或者当前位置是水域(grid[i][j] === 0),则周长增加1 + if (i < 0 || i >= rows || j < 0 || j >= cols || grid[i][j] === 0) { + perimeter++; + return; + } + + // 如果当前位置已经访问过(grid[i][j] === -1),则直接返回 + if (grid[i][j] === -1) { + return; + } + + // 标记当前位置为已访问(-1),避免重复计算 + grid[i][j] = -1; + + // 继续搜索上、下、左、右四个方向 + dfs(i + 1, j); + dfs(i - 1, j); + dfs(i, j + 1); + dfs(i, j - 1); + }; + + // 遍历整个网格,找到第一个陆地格子(grid[i][j] === 1),并以此为起点进行深度优先搜索 + for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + if (grid[i][j] === 1) { + dfs(i, j); + break; + } + } + } + + return perimeter; +} + +/** + * 方法二:遍历每个陆地格子,统计周长 + * @param grid 二维网格地图,其中 grid[i][j] = 1 表示陆地, grid[i][j] = 0 表示水域 + * @returns 岛屿的周长 + */ +function islandPerimeter(grid: number[][]): number { + // 处理特殊情况:网格为空或行列数为 0,直接返回 0 + if (!grid || grid.length === 0 || grid[0].length === 0) { + return 0; + } + + // 获取网格的行数和列数 + const rows = grid.length; + const cols = grid[0].length; + let perimeter = 0; // 岛屿的周长 + + // 遍历整个网格 + for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + // 如果当前格子是陆地(grid[i][j] === 1) + if (grid[i][j] === 1) { + perimeter += 4; // 周长先加上4个边 + + // 判断当前格子的上方是否也是陆地,如果是,则周长减去2个边 + if (i > 0 && grid[i - 1][j] === 1) { + perimeter -= 2; + } + + // 判断当前格子的左方是否也是陆地,如果是,则周长减去2个边 + if (j > 0 && grid[i][j - 1] === 1) { + perimeter -= 2; + } + } + } + } + + return perimeter; +} +``` + + diff --git "a/problems/0474.\344\270\200\345\222\214\351\233\266.md" "b/problems/0474.\344\270\200\345\222\214\351\233\266.md" new file mode 100755 index 0000000000..750917de3e --- /dev/null +++ "b/problems/0474.\344\270\200\345\222\214\351\233\266.md" @@ -0,0 +1,741 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 474.一和零 + +[力扣题目链接](https://leetcode.cn/problems/ones-and-zeroes/) + +给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 + +请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。 + +如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。 + +示例 1: + +* 输入:strs = ["10", "0001", "111001", "1", "0"], m = 5, n = 3 +* 输出:4 + +* 解释:最多有 5 个 0 和 3 个 1 的最大子集是 {"10","0001","1","0"} ,因此答案是 4 。 +其他满足题意但较小的子集包括 {"0001","1"} 和 {"10","1","0"} 。{"111001"} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。 + +示例 2: +* 输入:strs = ["10", "0", "1"], m = 1, n = 1 +* 输出:2 +* 解释:最大的子集是 {"0", "1"} ,所以答案是 2 。 + +提示: + +* 1 <= strs.length <= 600 +* 1 <= strs[i].length <= 100 +* strs[i] 仅由 '0' 和 '1' 组成 +* 1 <= m, n <= 100 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[装满这个背包最多用多少个物品?| LeetCode:474.一和零](https://www.bilibili.com/video/BV1rW4y1x7ZQ/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +如果对背包问题不都熟悉先看这两篇: + +* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) + +这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢。 + +来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。 + +其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系 + + +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20210117171307407-20230310132423205.png) + +多重背包是每个物品,数量不同的情况。 + +**本题中strs 数组里的元素就是物品,每个物品都是一个!** + +**而m 和 n相当于是一个背包,两个维度的背包**。 + +理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。 + +但本题其实是01背包问题! + +只不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。 + +开始动规五部曲: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]**。 + +2. 确定递推公式 + +dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。 + +dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1。 + +然后我们在遍历的过程中,取dp[i][j]的最大值。 + +所以递推公式:dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); + +此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + +对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i])。 + +**这就是一个典型的01背包!** 只不过物品的重量有了两个维度而已。 + + +3. dp数组如何初始化 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中已经讲解了,01背包的dp数组初始化为0就可以。 + +因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。 + +4. 确定遍历顺序 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中,我们讲到了01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历! + +那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。 + +代码如下: +```CPP +for (string str : strs) { // 遍历物品 + int oneNum = 0, zeroNum = 0; + for (char c : str) { + if (c == '0') zeroNum++; + else oneNum++; + } + for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历! + for (int j = n; j >= oneNum; j--) { + dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); + } + } +} +``` + +有同学可能想,那个遍历背包容量的两层for循环先后循序有没有什么讲究? + +没讲究,都是物品重量的一个维度,先遍历哪个都行! + +5. 举例推导dp数组 + +以输入:["10","0001","111001","1","0"],m = 3,n = 3为例 + +最后dp数组的状态如下所示: + + +![474.一和零](https://file1.kamacoder.com/i/algo/20210120111201512.jpg) + + +以上动规五部曲分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int findMaxForm(vector& strs, int m, int n) { + vector> dp(m + 1, vector (n + 1, 0)); // 默认初始化0 + for (string str : strs) { // 遍历物品 + int oneNum = 0, zeroNum = 0; + for (char c : str) { + if (c == '0') zeroNum++; + else oneNum++; + } + for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历! + for (int j = n; j >= oneNum; j--) { + dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); + } + } + } + return dp[m][n]; + } +}; +``` + +* 时间复杂度: O(kmn),k 为strs的长度 +* 空间复杂度: O(mn) + +C++: +使用三维数组的版本 + +```CPP +class Solution { +public: + int findMaxForm(vector& strs, int m, int n) { + int num_of_str = strs.size(); + + vector>> dp(num_of_str, vector>(m + 1,vector(n + 1, 0))); + + /* dp[i][j][k] represents, if choosing items among strs[0] to strs[i] to form a subset, + what is the maximum size of this subset such that there are no more than m 0's and n 1's in this subset. + Each entry of dp[i][j][k] is initialized with 0 + + transition formula: + using x[i] to indicates the number of 0's in strs[i] + using y[i] to indicates the number of 1's in strs[i] + + dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j - x[i]][k - y[i]] + 1) + + */ + + + // num_of_zeros records the number of 0's for each str + // num_of_ones records the number of 1's for each str + // find the number of 0's and the number of 1's for each str in strs + vector num_of_zeros; + vector num_of_ones; + for (auto& str : strs){ + int count_of_zero = 0; + int count_of_one = 0; + for (char &c : str){ + if(c == '0') count_of_zero ++; + else count_of_one ++; + } + num_of_zeros.push_back(count_of_zero); + num_of_ones.push_back(count_of_one); + + } + + + // num_of_zeros[0] indicates the number of 0's for str[0] + // num_of_ones[0] indiates the number of 1's for str[1] + + // initialize the 1st plane of dp[i][j][k], i.e., dp[0][j][k] + // if num_of_zeros[0] > m or num_of_ones[0] > n, no need to further initialize dp[0][j][k], + // because they have been intialized to 0 previously + if(num_of_zeros[0] <= m && num_of_ones[0] <= n){ + // for j < num_of_zeros[0] or k < num_of_ones[0], dp[0][j][k] = 0 + for(int j = num_of_zeros[0]; j <= m; j++){ + for(int k = num_of_ones[0]; k <= n; k++){ + dp[0][j][k] = 1; + } + } + } + + /* if j - num_of_zeros[i] >= 0 and k - num_of_ones[i] >= 0: + dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j - num_of_zeros[i]][k - num_of_ones[i]] + 1) + else: + dp[i][j][k] = dp[i-1][j][k] + */ + + for (int i = 1; i < num_of_str; i++){ + int count_of_zeros = num_of_zeros[i]; + int count_of_ones = num_of_ones[i]; + for (int j = 0; j <= m; j++){ + for (int k = 0; k <= n; k++){ + if( j < count_of_zeros || k < count_of_ones){ + dp[i][j][k] = dp[i-1][j][k]; + }else{ + dp[i][j][k] = max(dp[i-1][j][k], dp[i-1][j - count_of_zeros][k - count_of_ones] + 1); + } + } + } + + } + + return dp[num_of_str-1][m][n]; + + } +}; +``` + +## 总结 + +不少同学刷过这道题,可能没有总结这究竟是什么背包。 + +此时我们讲解了0-1背包的多种应用, + +* [纯 0 - 1 背包](https://programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-2.html) 是求 给定背包容量 装满背包 的最大价值是多少。 +* [416. 分割等和子集](https://programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html) 是求 给定背包容量,能不能装满这个背包。 +* [1049. 最后一块石头的重量 II](https://programmercarl.com/1049.%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8FII.html) 是求 给定背包容量,尽可能装,最多能装多少 +* [494. 目标和](https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html) 是求 给定背包容量,装满背包有多少种方法。 +* 本题是求 给定背包容量,装满背包最多有多少个物品。 + +所以在代码随想录中所列举的题目,都是 0-1背包不同维度上的应用,大家可以细心体会! + + + +## 其他语言版本 + +### Java + +三维DP数组实现 + +```java +class Solution { + public int findMaxForm(String[] strs, int m, int n) { + /// 数组有三个维度 + // 第一个维度:取前面的几个字符串 + // 第二个维度:0的数量限制(背包维度 1 容量) + // 第三个维度:1的数量限制(背包维度 2 容量) + int[][][] dpArr = new int[strs.length][m + 1][n + 1]; + + /// 初始化dpArr数组 + // 计算第一个字符串的零数量和1数量 + int zeroNum = 0; + int oneNum = 0; + for (char c : strs[0].toCharArray()) { + if (c == '0') { + zeroNum++; + } else { + oneNum++; + } + } + // 当0数量、1数量都容得下第一个字符串时,将DP数组的相应位置初始化为1,因为当前的子集数量为1 + for (int j = zeroNum; j <= m; j++) { + for (int k = oneNum; k <= n; k++) { + dpArr[0][j][k] = 1; + } + } + /// 依次填充加入第i个字符串之后的DP数组 + for (int i = 1; i < strs.length; i++) { + zeroNum = 0; + oneNum = 0; + for (char c : strs[i].toCharArray()) { + if (c == '0') { + zeroNum++; + } else { + oneNum++; + } + } + for (int j = 0; j <= m; j++) { + for (int k = 0; k <= n; k++) { + if (j >= zeroNum && k >= oneNum) { + // --if-- 当0数量维度和1数量维度的容量都大于等于当前字符串的0数量和1数量时,才考虑是否将当前字符串放入背包 + // 不放入第i个字符串,子集数量仍为 dpArr[i - 1][j][k] + // 放入第i个字符串,需要在0维度腾出 zeroNum 个容量,1维度腾出 oneNum 个容量,然后放入当前字符串,即 dpArr[i - 1][j - zeroNum][k - oneNum] + 1) + dpArr[i][j][k] = Math.max(dpArr[i - 1][j][k], dpArr[i - 1][j - zeroNum][k - oneNum] + 1); + } else { + // --if-- 无法放入第i个字符串,子集数量仍为 dpArr[i - 1][j][k] + dpArr[i][j][k] = dpArr[i - 1][j][k]; + } + } + } + } + return dpArr[dpArr.length - 1][m][n]; + } +} +``` + + + +二维DP数组实现 + +```Java +class Solution { + public int findMaxForm(String[] strs, int m, int n) { + //dp[i][j]表示i个0和j个1时的最大子集 + int[][] dp = new int[m + 1][n + 1]; + int oneNum, zeroNum; + for (String str : strs) { + oneNum = 0; + zeroNum = 0; + for (char ch : str.toCharArray()) { + if (ch == '0') { + zeroNum++; + } else { + oneNum++; + } + } + //倒序遍历 + for (int i = m; i >= zeroNum; i--) { + for (int j = n; j >= oneNum; j--) { + dp[i][j] = Math.max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); + } + } + } + return dp[m][n]; + } +} +``` + +### Python +DP(版本一) +```python +class Solution: + def findMaxForm(self, strs: List[str], m: int, n: int) -> int: + dp = [[0] * (n + 1) for _ in range(m + 1)] # 创建二维动态规划数组,初始化为0 + for s in strs: # 遍历物品 + zeroNum = s.count('0') # 统计0的个数 + oneNum = len(s) - zeroNum # 统计1的个数 + for i in range(m, zeroNum - 1, -1): # 遍历背包容量且从后向前遍历 + for j in range(n, oneNum - 1, -1): + dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1) # 状态转移方程 + return dp[m][n] + +``` +DP(版本二) +```python +class Solution: + def findMaxForm(self, strs: List[str], m: int, n: int) -> int: + dp = [[0] * (n + 1) for _ in range(m + 1)] # 创建二维动态规划数组,初始化为0 + # 遍历物品 + for s in strs: + ones = s.count('1') # 统计字符串中1的个数 + zeros = s.count('0') # 统计字符串中0的个数 + # 遍历背包容量且从后向前遍历 + for i in range(m, zeros - 1, -1): + for j in range(n, ones - 1, -1): + dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1) # 状态转移方程 + return dp[m][n] + +``` + +### Go +```go +func findMaxForm(strs []string, m int, n int) int { + // 定义数组 + dp := make([][]int, m+1) + for i,_ := range dp { + dp[i] = make([]int, n+1 ) + } + // 遍历 + for i:=0;i= zeroNum;j-- { + for k:=n ; k >= oneNum;k-- { + // 推导公式 + dp[j][k] = max(dp[j][k],dp[j-zeroNum][k-oneNum]+1) + } + } + //fmt.Println(dp) + } + return dp[m][n] +} + +func max(a,b int) int { + if a > b { + return a + } + return b +} +``` + +### JavaScript +```javascript +const findMaxForm = (strs, m, n) => { + const dp = Array.from(Array(m+1), () => Array(n+1).fill(0)); + let numOfZeros, numOfOnes; + + for(let str of strs) { + numOfZeros = 0; + numOfOnes = 0; + + for(let c of str) { + if (c === '0') { + numOfZeros++; + } else { + numOfOnes++; + } + } + + for(let i = m; i >= numOfZeros; i--) { + for(let j = n; j >= numOfOnes; j--) { + dp[i][j] = Math.max(dp[i][j], dp[i - numOfZeros][j - numOfOnes] + 1); + } + } + } + + return dp[m][n]; +}; +``` + +### TypeScript + +> 滚动数组,二维数组法 + +```typescript +type BinaryInfo = { numOfZero: number, numOfOne: number }; +function findMaxForm(strs: string[], m: number, n: number): number { + const goodsNum: number = strs.length; + const dp: number[][] = new Array(m + 1).fill(0) + .map(_ => new Array(n + 1).fill(0)); + for (let i = 0; i < goodsNum; i++) { + const { numOfZero, numOfOne } = countBinary(strs[i]); + for (let j = m; j >= numOfZero; j--) { + for (let k = n; k >= numOfOne; k--) { + dp[j][k] = Math.max(dp[j][k], dp[j - numOfZero][k - numOfOne] + 1); + } + } + } + return dp[m][n]; +}; +function countBinary(str: string): BinaryInfo { + let numOfZero: number = 0, + numOfOne: number = 0; + for (let s of str) { + if (s === '0') { + numOfZero++; + } else { + numOfOne++; + } + } + return { numOfZero, numOfOne }; +} +``` + +> 传统背包,三维数组法 + +```typescript +type BinaryInfo = { numOfZero: number, numOfOne: number }; +function findMaxForm(strs: string[], m: number, n: number): number { + /** + dp[i][j][k]: 前i个物品中, 背包的0容量为j, 1容量为k, 最多能放的物品数量 + */ + const goodsNum: number = strs.length; + const dp: number[][][] = new Array(goodsNum).fill(0) + .map(_ => new Array(m + 1) + .fill(0) + .map(_ => new Array(n + 1).fill(0)) + ); + const { numOfZero, numOfOne } = countBinary(strs[0]); + for (let i = numOfZero; i <= m; i++) { + for (let j = numOfOne; j <= n; j++) { + dp[0][i][j] = 1; + } + } + for (let i = 1; i < goodsNum; i++) { + const { numOfZero, numOfOne } = countBinary(strs[i]); + for (let j = 0; j <= m; j++) { + for (let k = 0; k <= n; k++) { + if (j < numOfZero || k < numOfOne) { + dp[i][j][k] = dp[i - 1][j][k]; + } else { + dp[i][j][k] = Math.max(dp[i - 1][j][k], dp[i - 1][j - numOfZero][k - numOfOne] + 1); + } + } + } + } + return dp[dp.length - 1][m][n]; +}; +function countBinary(str: string): BinaryInfo { + let numOfZero: number = 0, + numOfOne: number = 0; + for (let s of str) { + if (s === '0') { + numOfZero++; + } else { + numOfOne++; + } + } + return { numOfZero, numOfOne }; +} +``` + +> 回溯法(会超时) + +```typescript +function findMaxForm(strs: string[], m: number, n: number): number { + /** + 思路:暴力枚举strs的所有子集,记录符合条件子集的最大长度 + */ + let resMax: number = 0; + backTrack(strs, m, n, 0, []); + return resMax; + function backTrack( + strs: string[], m: number, n: number, + startIndex: number, route: string[] + ): void { + if (startIndex === strs.length) return; + for (let i = startIndex, length = strs.length; i < length; i++) { + route.push(strs[i]); + if (isValidSubSet(route, m, n)) { + resMax = Math.max(resMax, route.length); + backTrack(strs, m, n, i + 1, route); + } + route.pop(); + } + } +}; +function isValidSubSet(strs: string[], m: number, n: number): boolean { + let zeroNum: number = 0, + oneNum: number = 0; + strs.forEach(str => { + for (let s of str) { + if (s === '0') { + zeroNum++; + } else { + oneNum++; + } + } + }); + return zeroNum <= m && oneNum <= n; +} +``` + +### Scala + +背包: +```scala +object Solution { + def findMaxForm(strs: Array[String], m: Int, n: Int): Int = { + var dp = Array.ofDim[Int](m + 1, n + 1) + + var (oneNum, zeroNum) = (0, 0) + + for (str <- strs) { + oneNum = 0 + zeroNum = 0 + for (i <- str.indices) { + if (str(i) == '0') zeroNum += 1 + else oneNum += 1 + } + + for (i <- m to zeroNum by -1) { + for (j <- n to oneNum by -1) { + dp(i)(j) = math.max(dp(i)(j), dp(i - zeroNum)(j - oneNum) + 1) + } + } + } + + dp(m)(n) + } +} +``` + +回溯法(超时): +```scala +object Solution { + import scala.collection.mutable + + var res = Int.MinValue + + def test(str: String): (Int, Int) = { + var (zero, one) = (0, 0) + for (i <- str.indices) { + if (str(i) == '1') one += 1 + else zero += 1 + } + (zero, one) + } + + def travsel(strs: Array[String], path: mutable.ArrayBuffer[String], m: Int, n: Int, startIndex: Int): Unit = { + if (startIndex > strs.length) { + return + } + + res = math.max(res, path.length) + + for (i <- startIndex until strs.length) { + + var (zero, one) = test(strs(i)) + + // 如果0的个数小于m,1的个数小于n,则可以回溯 + if (zero <= m && one <= n) { + path.append(strs(i)) + travsel(strs, path, m - zero, n - one, i + 1) + path.remove(path.length - 1) + } + } + } + + def findMaxForm(strs: Array[String], m: Int, n: Int): Int = { + res = Int.MinValue + var path = mutable.ArrayBuffer[String]() + travsel(strs, path, m, n, 0) + res + } +} +``` + +### Rust + +```rust +impl Solution { + pub fn find_max_form(strs: Vec, m: i32, n: i32) -> i32 { + let (m, n) = (m as usize, n as usize); + let mut dp = vec![vec![0; n + 1]; m + 1]; + for s in strs { + let (mut one_num, mut zero_num) = (0, 0); + for c in s.chars() { + match c { + '0' => zero_num += 1, + '1' => one_num += 1, + _ => (), + } + } + for i in (zero_num..=m).rev() { + for j in (one_num..=n).rev() { + dp[i][j] = dp[i][j].max(dp[i - zero_num][j - one_num] + 1); + } + } + } + dp[m][n] + } +} +``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int findMaxForm(char** strs, int strsSize, int m, int n) { + int dp[m + 1][n + 1]; + memset(dp, 0, sizeof (int ) * (m + 1) * (n + 1)); + for(int i = 0; i < strsSize; i++){ + // 统计0和1的数量 + int count0 = 0; + int count1 = 0; + char *str = strs[i]; + while (*str != '\0'){ + if(*str == '0'){ + count0++; + } else{ + count1++; + } + str++; + } + for(int j = m; j >= count0; j--){ + for(int k = n; k >= count1; k--){ + dp[j][k] = max(dp[j][k], dp[j - count0][k - count1] + 1); + } + } + } + return dp[m][n]; +} +``` + + + +### C# + +```csharp +public class Solution +{ + public int FindMaxForm(string[] strs, int m, int n) + { + int[,] dp = new int[m + 1, n + 1]; + foreach (string str in strs) + { + int zero = 0, one = 0; + foreach (char c in str) + { + if (c == '0') zero++; + else one++; + } + for (int i = m; i >= zero; i--) + { + for (int j = n; j >= one; j--) + { + dp[i, j] = Math.Max(dp[i, j], dp[i - zero, j - one] + 1); + } + } + } + return dp[m, n]; + } +} +``` + + + diff --git "a/problems/0491.\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" "b/problems/0491.\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..5d37737169 --- /dev/null +++ "b/problems/0491.\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,640 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 和子集问题有点像,但又处处是陷阱 + +# 491.递增子序列 + +[力扣题目链接](https://leetcode.cn/problems/non-decreasing-subsequences/) + +给定一个整型数组, 你的任务是找到所有该数组的递增子序列,递增子序列的长度至少是2。 + +示例: + +* 输入: [4, 6, 7, 7] +* 输出: [[4, 6], [4, 7], [4, 6, 7], [4, 6, 7, 7], [6, 7], [6, 7, 7], [7,7], [4,7,7]] + +说明: +* 给定数组的长度不会超过15。 +* 数组中的整数范围是 [-100,100]。 +* 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[回溯算法精讲,树层去重与树枝去重 | LeetCode:491.递增子序列](https://www.bilibili.com/video/BV1EG4y1h78v/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。 + +这又是子集,又是去重,是不是不由自主的想起了刚刚讲过的[90.子集II](https://programmercarl.com/0090.子集II.html)。 + +就是因为太像了,更要注意差别所在,要不就掉坑里了! + +在[90.子集II](https://programmercarl.com/0090.子集II.html)中我们是通过排序,再加一个标记数组来达到去重的目的。 + +而本题求自增子序列,是不能对原数组进行排序的,排完序的数组都是自增子序列了。 + +**所以不能使用之前的去重逻辑!** + +本题给出的示例,还是一个有序数组 [4, 6, 7, 7],这更容易误导大家按照排序的思路去做了。 + +为了有鲜明的对比,我用[4, 7, 6, 7]这个数组来举例,抽象为树形结构如图: + + +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201124200229824.png) + + + + +### 回溯三部曲 + +* 递归函数参数 + +本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。 + +代码如下: + +```cpp +vector> result; +vector path; +void backtracking(vector& nums, int startIndex) +``` + +* 终止条件 + +本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和[回溯算法:求子集问题!](https://programmercarl.com/0078.子集.html)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。 + +但本题收集结果有所不同,题目要求递增子序列大小至少为2,所以代码如下: + +```cpp +if (path.size() > 1) { + result.push_back(path); + // 注意这里不要加return,因为要取树上的所有节点 +} +``` + +* 单层搜索逻辑 + +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201124200229824-20230310131640070.png) +在图中可以看出,**同一父节点下的同层上使用过的元素就不能再使用了** + +那么单层搜索代码如下: + +```cpp +unordered_set uset; // 使用set来对本层元素进行去重 +for (int i = startIndex; i < nums.size(); i++) { + if ((!path.empty() && nums[i] < path.back()) + || uset.find(nums[i]) != uset.end()) { + continue; + } + uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 + path.push_back(nums[i]); + backtracking(nums, i + 1); + path.pop_back(); +} +``` + +**对于已经习惯写回溯的同学,看到递归函数上面的`uset.insert(nums[i]);`,下面却没有对应的pop之类的操作,应该很不习惯吧** + +**这也是需要注意的点,`unordered_set uset;` 是记录本层元素是否重复使用,新的一层uset都会重新定义(清空),所以要知道uset只负责本层!** + + +最后整体C++代码如下: + +```CPP +// 版本一 +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& nums, int startIndex) { + if (path.size() > 1) { + result.push_back(path); + // 注意这里不要加return,要取树上的节点 + } + unordered_set uset; // 使用set对本层元素进行去重 + for (int i = startIndex; i < nums.size(); i++) { + if ((!path.empty() && nums[i] < path.back()) + || uset.find(nums[i]) != uset.end()) { + continue; + } + uset.insert(nums[i]); // 记录这个元素在本层用过了,本层后面不能再用了 + path.push_back(nums[i]); + backtracking(nums, i + 1); + path.pop_back(); + } + } +public: + vector> findSubsequences(vector& nums) { + result.clear(); + path.clear(); + backtracking(nums, 0); + return result; + } +}; +``` +* 时间复杂度: O(n * 2^n) +* 空间复杂度: O(n) + +## 优化 + +以上代码用我用了`unordered_set`来记录本层元素是否重复使用。 + +**其实用数组来做哈希,效率就高了很多**。 + +注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希。 + +程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。 + +那么优化后的代码如下: + +```CPP +// 版本二 +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& nums, int startIndex) { + if (path.size() > 1) { + result.push_back(path); + } + int used[201] = {0}; // 这里使用数组来进行去重操作,题目说数值范围[-100, 100] + for (int i = startIndex; i < nums.size(); i++) { + if ((!path.empty() && nums[i] < path.back()) + || used[nums[i] + 100] == 1) { + continue; + } + used[nums[i] + 100] = 1; // 记录这个元素在本层用过了,本层后面不能再用了 + path.push_back(nums[i]); + backtracking(nums, i + 1); + path.pop_back(); + } + } +public: + vector> findSubsequences(vector& nums) { + result.clear(); + path.clear(); + backtracking(nums, 0); + return result; + } +}; +``` + +这份代码在leetcode上提交,要比版本一耗时要好的多。 + +**所以正如在[哈希表:总结篇!(每逢总结必经典)](https://programmercarl.com/哈希表总结.html)中说的那样,数组,set,map都可以做哈希表,而且数组干的活,map和set都能干,但如果数值范围小的话能用数组尽量用数组**。 + + +## 总结 + +本题题解清一色都说是深度优先搜索,但我更倾向于说它用回溯法,而且本题我也是完全使用回溯法的逻辑来分析的。 + +相信大家在本题中处处都能看到是[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)的身影,但处处又都是陷阱。 + +**对于养成思维定式或者套模板套嗨了的同学,这道题起到了很好的警醒作用。更重要的是拓展了大家的思路!** + + + +## 其他语言版本 + + +### Java +```Java +class Solution { + List> result = new ArrayList<>(); + List path = new ArrayList<>(); + public List> findSubsequences(int[] nums) { + backTracking(nums, 0); + return result; + } + private void backTracking(int[] nums, int startIndex){ + if(path.size() >= 2) + result.add(new ArrayList<>(path)); + HashSet hs = new HashSet<>(); + for(int i = startIndex; i < nums.length; i++){ + if(!path.isEmpty() && path.get(path.size() -1 ) > nums[i] || hs.contains(nums[i])) + continue; + hs.add(nums[i]); + path.add(nums[i]); + backTracking(nums, i + 1); + path.remove(path.size() - 1); + } + } +} +``` + +```java +class Solution { + private List path = new ArrayList<>(); + private List> res = new ArrayList<>(); + public List> findSubsequences(int[] nums) { + backtracking(nums,0); + return res; + } + + private void backtracking (int[] nums, int start) { + if (path.size() > 1) { + res.add(new ArrayList<>(path)); + } + + int[] used = new int[201]; + for (int i = start; i < nums.length; i++) { + if (!path.isEmpty() && nums[i] < path.get(path.size() - 1) || + (used[nums[i] + 100] == 1)) continue; + used[nums[i] + 100] = 1; + path.add(nums[i]); + backtracking(nums, i + 1); + path.remove(path.size() - 1); + } + } +} +``` +```java +//法二:使用map +class Solution { + //结果集合 + List> res = new ArrayList<>(); + //路径集合 + LinkedList path = new LinkedList<>(); + public List> findSubsequences(int[] nums) { + getSubsequences(nums,0); + return res; + } + private void getSubsequences( int[] nums, int start ) { + if(path.size()>1 ){ + res.add( new ArrayList<>(path) ); + // 注意这里不要加return,要取树上的节点 + } + HashMap map = new HashMap<>(); + for(int i=start ;i < nums.length ;i++){ + if(!path.isEmpty() && nums[i]< path.getLast()){ + continue; + } + // 使用过了当前数字 + if ( map.getOrDefault( nums[i],0 ) >=1 ){ + continue; + } + map.put(nums[i],map.getOrDefault( nums[i],0 )+1); + path.add( nums[i] ); + getSubsequences( nums,i+1 ); + path.removeLast(); + } + } +} +``` + +### Python + + +回溯 利用set去重 +```python +class Solution: + def findSubsequences(self, nums): + result = [] + path = [] + self.backtracking(nums, 0, path, result) + return result + + def backtracking(self, nums, startIndex, path, result): + if len(path) > 1: + result.append(path[:]) # 注意要使用切片将当前路径的副本加入结果集 + # 注意这里不要加return,要取树上的节点 + + uset = set() # 使用集合对本层元素进行去重 + for i in range(startIndex, len(nums)): + if (path and nums[i] < path[-1]) or nums[i] in uset: + continue + + uset.add(nums[i]) # 记录这个元素在本层用过了,本层后面不能再用了 + path.append(nums[i]) + self.backtracking(nums, i + 1, path, result) + path.pop() + +``` +回溯 利用哈希表去重 +```python +class Solution: + def findSubsequences(self, nums): + result = [] + path = [] + self.backtracking(nums, 0, path, result) + return result + + def backtracking(self, nums, startIndex, path, result): + if len(path) > 1: + result.append(path[:]) # 注意要使用切片将当前路径的副本加入结果集 + + used = [0] * 201 # 使用数组来进行去重操作,题目说数值范围[-100, 100] + for i in range(startIndex, len(nums)): + if (path and nums[i] < path[-1]) or used[nums[i] + 100] == 1: + continue # 如果当前元素小于上一个元素,或者已经使用过当前元素,则跳过当前元素 + + used[nums[i] + 100] = 1 # 标记当前元素已经使用过 + path.append(nums[i]) # 将当前元素加入当前递增子序列 + self.backtracking(nums, i + 1, path, result) + path.pop() + + +``` +### Go + +```go +var ( + res [][]int + path []int +) +func findSubsequences(nums []int) [][]int { + res, path = make([][]int, 0), make([]int, 0, len(nums)) + dfs(nums, 0) + return res +} +func dfs(nums []int, start int) { + if len(path) >= 2 { + tmp := make([]int, len(path)) + copy(tmp, path) + res = append(res, tmp) + } + used := make(map[int]bool, len(nums)) // 初始化used字典,用以对同层元素去重 + for i := start; i < len(nums); i++ { + if used[nums[i]] { // 去重 + continue + } + if len(path) == 0 || nums[i] >= path[len(path)-1] { + path = append(path, nums[i]) + used[nums[i]] = true + dfs(nums, i+1) + path = path[:len(path)-1] + } + } +} +``` + +### JavaScript + +```Javascript + +var findSubsequences = function(nums) { + let result = [] + let path = [] + function backtracing(startIndex) { + if(path.length > 1) { + result.push(path.slice()) + } + let uset = [] + for(let i = startIndex; i < nums.length; i++) { + if((path.length > 0 && nums[i] < path[path.length - 1]) || uset[nums[i] + 100]) { + continue + } + uset[nums[i] + 100] = true + path.push(nums[i]) + backtracing(i + 1) + path.pop() + } + } + backtracing(0) + return result +}; + +``` + +### TypeScript + +```typescript +function findSubsequences(nums: number[]): number[][] { + const resArr: number[][] = []; + backTracking(nums, 0, []); + return resArr; + function backTracking(nums: number[], startIndex: number, route: number[]): void { + let length: number = nums.length; + if (route.length >= 2) { + resArr.push(route.slice()); + } + const usedSet: Set = new Set(); + for (let i = startIndex; i < length; i++) { + if ( + nums[i] < route[route.length - 1] || + usedSet.has(nums[i]) + ) continue; + usedSet.add(nums[i]); + route.push(nums[i]); + backTracking(nums, i + 1, route); + route.pop(); + } + } +}; +``` + +### Rust +**回溯+哈希** +```Rust +use std::collections::HashSet; +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, nums: &Vec, start_index: usize) { + if path.len() > 1 { result.push(path.clone()); } + let len = nums.len(); + let mut uset: HashSet = HashSet::new(); + for i in start_index..len { + if (!path.is_empty() && nums[i] < *path.last().unwrap()) || uset.contains(&nums[i]) { continue; } + uset.insert(nums[i]); + path.push(nums[i]); + Self::backtracking(result, path, nums, i + 1); + path.pop(); + } + } + + pub fn find_subsequences(nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + Self::backtracking(&mut result, &mut path, &nums, 0); + result + } +} +``` +**回溯+数组** +```Rust +impl Solution { + fn backtracking(result: &mut Vec>, path: &mut Vec, nums: &Vec, start_index: usize) { + if path.len() > 1 { result.push(path.clone()); } + let len = nums.len(); + let mut used = [0; 201]; + for i in start_index..len { + if (!path.is_empty() && nums[i] < *path.last().unwrap()) || used[(nums[i] + 100) as usize] == 1 { continue; } + used[(nums[i] + 100) as usize] = 1; + path.push(nums[i]); + Self::backtracking(result, path, nums, i + 1); + path.pop(); + } + } + + pub fn find_subsequences(nums: Vec) -> Vec> { + let mut result: Vec> = Vec::new(); + let mut path: Vec = Vec::new(); + Self::backtracking(&mut result, &mut path, &nums, 0); + result + } +} +``` + +### C + +```c +int* path; +int pathTop; +int** ans; +int ansTop; +int* length; +//将当前path中的内容复制到ans中 +void copy() { + int* tempPath = (int*)malloc(sizeof(int) * pathTop); + memcpy(tempPath, path, pathTop * sizeof(int)); + length[ansTop] = pathTop; + ans[ansTop++] = tempPath; +} + +//查找uset中是否存在值为key的元素 +int find(int* uset, int usetSize, int key) { + int i; + for(i = 0; i < usetSize; i++) { + if(uset[i] == key) + return 1; + } + return 0; +} + +void backTracking(int* nums, int numsSize, int startIndex) { + //当path中元素大于1个时,将path拷贝到ans中 + if(pathTop > 1) { + copy(); + } + int* uset = (int*)malloc(sizeof(int) * numsSize); + int usetTop = 0; + int i; + for(i = startIndex; i < numsSize; i++) { + //若当前元素小于path中最后一位元素 || 在树的同一层找到了相同的元素,则continue + if((pathTop > 0 && nums[i] < path[pathTop - 1]) || find(uset, usetTop, nums[i])) + continue; + //将当前元素放入uset + uset[usetTop++] = nums[i]; + //将当前元素放入path + path[pathTop++] = nums[i]; + backTracking(nums, numsSize, i + 1); + //回溯 + pathTop--; + } +} + +int** findSubsequences(int* nums, int numsSize, int* returnSize, int** returnColumnSizes){ + //辅助数组初始化 + path = (int*)malloc(sizeof(int) * numsSize); + ans = (int**)malloc(sizeof(int*) * 33000); + length = (int*)malloc(sizeof(int*) * 33000); + pathTop = ansTop = 0; + + backTracking(nums, numsSize, 0); + + //设置数组中返回元素个数,以及每个一维数组的长度 + *returnSize = ansTop; + *returnColumnSizes = (int*)malloc(sizeof(int) * ansTop); + int i; + for(i = 0; i < ansTop; i++) { + (*returnColumnSizes)[i] = length[i]; + } + return ans; +} +``` + +### Swift + +```swift +func findSubsequences(_ nums: [Int]) -> [[Int]] { + var result = [[Int]]() + var path = [Int]() + func backtracking(startIndex: Int) { + // 收集结果,但不返回,因为后续还要以此基础拼接 + if path.count > 1 { + result.append(path) + } + + var uset = Set() + let end = nums.count + guard startIndex < end else { return } // 终止条件 + for i in startIndex ..< end { + let num = nums[i] + if uset.contains(num) { continue } // 跳过重复元素 + if !path.isEmpty, num < path.last! { continue } // 确保递增 + uset.insert(num) // 通过set记录 + path.append(num) // 处理:收集元素 + backtracking(startIndex: i + 1) // 元素不重复访问 + path.removeLast() // 回溯 + } + } + backtracking(startIndex: 0) + return result +} +``` + + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def findSubsequences(nums: Array[Int]): List[List[Int]] = { + var result = mutable.ListBuffer[List[Int]]() + var path = mutable.ListBuffer[Int]() + + def backtracking(startIndex: Int): Unit = { + // 集合元素大于1,添加到结果集 + if (path.size > 1) { + result.append(path.toList) + } + + var used = new Array[Boolean](201) + // 使用循环守卫,当前层没有用过的元素才有资格进入回溯 + for (i <- startIndex until nums.size if !used(nums(i) + 100)) { + // 如果path没元素或 当前循环的元素比path的最后一个元素大,则可以进入回溯 + if (path.size == 0 || (!path.isEmpty && nums(i) >= path(path.size - 1))) { + used(nums(i) + 100) = true + path.append(nums(i)) + backtracking(i + 1) + path.remove(path.size - 1) + } + } + } + + backtracking(0) + result.toList + } +} +``` +### C# +```csharp +public class Solution { + public IList> res = new List>(); + public IList path = new List(); + public IList> FindSubsequences(int[] nums) { + BackTracking(nums, 0); + return res; + } + public void BackTracking(int[] nums, int start){ + if(path.Count >= 2){ + res.Add(new List(path)); + } + HashSet hs = new HashSet(); + for(int i = start; i < nums.Length; i++){ + if(path.Count > 0 && path[path.Count - 1] > nums[i] || hs.Contains(nums[i])){ + continue; + } + hs.Add(nums[i]); + path.Add(nums[i]); + BackTracking(nums, i + 1); + path.RemoveAt(path.Count - 1); + } + } +} +``` + diff --git "a/problems/0494.\347\233\256\346\240\207\345\222\214.md" "b/problems/0494.\347\233\256\346\240\207\345\222\214.md" new file mode 100755 index 0000000000..b161bc57a8 --- /dev/null +++ "b/problems/0494.\347\233\256\346\240\207\345\222\214.md" @@ -0,0 +1,1025 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 494.目标和 + +[力扣题目链接](https://leetcode.cn/problems/target-sum/) + +难度:中等 + +给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。 + +返回可以使最终数组和为目标数 S 的所有添加符号的方法数。 + +示例: + +* 输入:nums: [1, 1, 1, 1, 1], S: 3 +* 输出:5 + +解释: +* -1+1+1+1+1 = 3 +* +1-1+1+1+1 = 3 +* +1+1-1+1+1 = 3 +* +1+1+1-1+1 = 3 +* +1+1+1+1-1 = 3 + +一共有5种方法让最终目标和为3。 + +提示: + +* 数组非空,且长度不会超过 20 。 +* 初始的数组的和不会超过 1000 。 +* 保证返回的最终结果能被 32 位整数存下。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[装满背包有多少种方法?| LeetCode:494.目标和](https://www.bilibili.com/video/BV1o8411j73x/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +如果对背包问题不都熟悉先看这两篇: + +* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) + +如果跟着「代码随想录」一起学过[回溯算法系列](https://programmercarl.com/回溯总结.html)的录友,看到这道题,应该有一种直觉,就是感觉好像回溯法可以暴搜出来。 + +事实确实如此,下面我也会给出相应的代码,只不过会超时。 + +这道题目咋眼一看和动态规划背包啥的也没啥关系。 + +本题要如何使表达式结果为target, + +既然为target,那么就一定有 left组合 - right组合 = target。 + +left + right = sum,而sum是固定的。right = sum - left + +left - (sum - left) = target 推导出 left = (target + sum)/2 。 + +target是固定的,sum是固定的,left就可以求出来。 + +此时问题就是在集合nums中找出和为left的组合。 + +### 回溯算法 + +在回溯算法系列中,一起学过这道题目[回溯算法:39. 组合总和](https://programmercarl.com/0039.组合总和.html)的录友应该感觉很熟悉,这不就是组合总和问题么? + +此时可以套组合总和的回溯法代码,几乎不用改动。 + +当然,也可以转变成序列区间选+ 或者 -,使用回溯法,那就是另一个解法。 + +我也把代码给出来吧,大家可以了解一下,回溯的解法,以下是本题转变为组合总和问题的回溯法代码: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& candidates, int target, int sum, int startIndex) { + if (sum == target) { + result.push_back(path); + } + // 如果 sum + candidates[i] > target 就终止遍历 + for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + sum += candidates[i]; + path.push_back(candidates[i]); + backtracking(candidates, target, sum, i + 1); + sum -= candidates[i]; + path.pop_back(); + + } + } +public: + int findTargetSumWays(vector& nums, int S) { + int sum = 0; + for (int i = 0; i < nums.size(); i++) sum += nums[i]; + if (S > sum) return 0; // 此时没有方案 + if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要格外小心数值溢出的问题 + int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和 + + // 以下为回溯法代码 + result.clear(); + path.clear(); + sort(nums.begin(), nums.end()); // 需要排序 + backtracking(nums, bagSize, 0, 0); + return result.size(); + } +}; +``` + +当然以上代码超时了。 + +也可以使用记忆化回溯,但这里我就不在回溯上下功夫了,直接看动规吧 + +### 动态规划 (二维dp数组) + +假设加法的总和为x,那么减法对应的总和就是sum - x。 + +所以我们要求的是 x - (sum - x) = target + +x = (target + sum) / 2 + +**此时问题就转化为,用nums装满容量为x的背包,有几种方法**。 + +这里的x,就是bagSize,也就是我们后面要求的背包容量。 + +大家看到`(target + sum) / 2` 应该担心计算的过程中向下取整有没有影响。 + +这么担心就对了,例如sum是5,target是2 的话其实就是无解的,所以: + +```CPP +(C++代码中,输入的S 就是题目描述的 target) +if ((target + sum) % 2 == 1) return 0; // 此时没有方案 +``` + +同时如果target 的绝对值已经大于sum,那么也是没有方案的。 + +```CPP +if (abs(target) > sum) return 0; // 此时没有方案 +``` + +因为每个物品(题目中的1)只用一次! + +这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。 + +本题则是装满有几种方法。其实这就是一个组合问题了。 + +#### 1. 确定dp数组以及下标的含义 + +先用 二维 dp数组求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。 + +01背包为什么这么定义dp数组,我在[0-1背包理论基础](https://www.programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html)中 确定dp数组的含义里讲解过。 + +#### 2. 确定递推公式 + +我们先手动推导一下,这个二维数组里面的数值。 + +------------ + +先只考虑物品0,如图: + +![](https://file1.kamacoder.com/i/algo/20240808161747.png) + +(这里的所有物品,都是题目中的数字1)。 + +装满背包容量为0 的方法个数是1,即 放0件物品。 + +装满背包容量为1 的方法个数是1,即 放物品0。 + +装满背包容量为2 的方法个数是0,目前没有办法能装满容量为2的背包。 + +-------------- + +接下来 考虑 物品0 和 物品1,如图: + +![](https://file1.kamacoder.com/i/algo/20240808162052.png) + +装满背包容量为0 的方法个数是1,即 放0件物品。 + +装满背包容量为1 的方法个数是2,即 放物品0 或者 放物品1。 + +装满背包容量为2 的方法个数是1,即 放物品0 和 放物品1。 + +其他容量都不能装满,所以方法是0。 + +----------------- + +接下来 考虑 物品0 、物品1 和 物品2 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240808162533.png) + +装满背包容量为0 的方法个数是1,即 放0件物品。 + +装满背包容量为1 的方法个数是3,即 放物品0 或者 放物品1 或者 放物品2。 + +装满背包容量为2 的方法个数是3,即 放物品0 和 放物品1、放物品0 和 物品2、放物品1 和 物品2。 + +装满背包容量为3的方法个数是1,即 放物品0 和 物品1 和 物品2。 + +--------------- + +通过以上举例,我们来看 dp[2][2] 可以有哪些方向推出来。 + +如图红色部分: + +![](https://file1.kamacoder.com/i/algo/20240808163312.png) + +dp[2][2] = 3,即 放物品0 和 放物品1、放物品0 和 物品 2、放物品1 和 物品2, 如图所示,三种方法: + +![](https://file1.kamacoder.com/i/algo/20240826111946.png) + +**容量为2 的背包,如果不放 物品2 有几种方法呢**? + +有 dp[1][2] 种方法,即 背包容量为2,只考虑物品0 和 物品1 ,有 dp[1][2] 种方法,如图: + +![](https://file1.kamacoder.com/i/algo/20240826112805.png) + +**容量为2 的背包, 如果放 物品2 有几种方法呢**? + +首先 要在背包里 先把物品2的容量空出来, 装满 刨除物品2容量 的背包 有几种方法呢? + +刨除物品2容量后的背包容量为 1。 + +此时装满背包容量为1 有 dp[1][1] 种方法,即: 不放物品2,背包容量为1,只考虑物品 0 和 物品 1,有 dp[1][1] 种方法。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240826113043.png) + +有录友可能疑惑,这里计算的是放满 容量为2的背包 有几种方法,那物品2去哪了? + +在上面图中,你把物品2补上就好,同样是两种方法。 + +dp[2][2] = 容量为2的背包不放物品2有几种方法 + 容量为2的背包放物品2有几种方法 + +所以 dp[2][2] = dp[1][2] + dp[1][1] ,如图: + +![](https://file1.kamacoder.com/i/algo/20240826113258.png) + +以上过程,抽象化如下: + +* **不放物品i**:即背包容量为j,里面不放物品i,装满有dp[i - 1][j]中方法。 + +* **放物品i**: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dp[i - 1][j - 物品i容量] 种方法。 + +本题中,物品i的容量是nums[i],价值也是nums[i]。 + +递推公式:dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; + +考到这个递推公式,我们应该注意到,`j - nums[i]` 作为数组下标,如果 `j - nums[i]` 小于零呢? + +说明背包容量装不下 物品i,所以此时装满背包的方法值 等于 不放物品i的装满背包的方法,即:dp[i][j] = dp[i - 1][j]; + +所以递推公式: + +```CPP +if (nums[i] > j) dp[i][j] = dp[i - 1][j]; +else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; +``` + +#### 3. dp数组如何初始化 + +先明确递推的方向,如图,求解 dp[2][2] 是由 上方和左上方推出。 + +![](https://file1.kamacoder.com/i/algo/20240826115800.png) + +那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础,如图红色部分: + +![](https://file1.kamacoder.com/i/algo/20240827103507.png) + +关于dp[0][0]的值,在上面的递推公式讲解中已经讲过,装满背包容量为0 的方法数量是1,即 放0件物品。 + +那么最上行dp[0][j] 如何初始化呢? + +dp[0][j]:只放物品0, 把容量为j的背包填满有几种方法。 + +只有背包容量为 物品0 的容量的时候,方法为1,正好装满。 + +其他情况下,要不是装不满,要不是装不下。 + +所以初始化:dp[0][nums[0]] = 1 ,其他均为0 。 + +表格最左列也要初始化,dp[i][0] : 背包容量为0, 放物品0 到 物品i,装满有几种方法。 + +都是有一种方法,就是放0件物品。 + +即 dp[i][0] = 1 + +但这里有例外,就是如果 物品数值就是0呢? + +如果有两个物品,物品0为0, 物品1为0,装满背包容量为0的方法有几种。 + +* 放0件物品 +* 放物品0 +* 放物品1 +* 放物品0 和 物品1 + +此时是有4种方法。 + +其实就是算数组里有t个0,然后按照组合数量求,即 2^t 。 + +初始化如下: + +```CPP +int numZero = 0; +for (int i = 0; i < nums.size(); i++) { + if (nums[i] == 0) numZero++; + dp[i][0] = (int) pow(2.0, numZero); +} +``` + +#### 4. 确定遍历顺序 + +在明确递推方向时,我们知道 当前值 是由上方和左上方推出。 + +那么我们的遍历顺序一定是 从上到下,从左到右。 + +因为只有这样,我们才能基于之前的数值做推导。 + +例如下图,如果上方没数值,左上方没数值,就无法推出 dp[2][2]。 + +![](https://file1.kamacoder.com/i/algo/20240827105427.png) + +那么是先 从上到下 ,再从左到右遍历,例如这样: + +```CPP +for (int i = 1; i < nums.size(); i++) { // 行,遍历物品 + for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + } +} +``` + +还是先 从左到右,再从上到下呢,例如这样: + +```CPP +for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + for (int i = 1; i < nums.size(); i++) { // 行,遍历物品 + } +} +``` + +**其实以上两种遍历都可以**! (但仅针对二维DP数组是这样的) + +这一点我在 [01背包理论基础](https://www.programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html)中的 遍历顺序部分讲过。 + +这里我再画图讲一下,以求dp[2][2]为例,当先从上到下,再从左到右遍历,矩阵是这样: + +![](https://file1.kamacoder.com/i/algo/20240827110933.png) + +当先从左到右,再从上到下遍历,矩阵是这样: + +![](https://file1.kamacoder.com/i/algo/20240827111013.png) + +这里大家可以看出,无论是以上哪种遍历,都不影响 dp[2][2]的求值,用来 推导 dp[2][2] 的数值都在。 + + +#### 5. 举例推导dp数组 + +输入:nums: [1, 1, 1, 1, 1], target: 3 + +bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4 + +dp数组状态变化如下: + +![](https://file1.kamacoder.com/i/algo/20240827111612.png) + +这么大的矩阵,我们是可以自己手动模拟出来的。 + +在模拟的过程中,既可以帮我们寻找规律,也可以帮我们验证 递推公式加遍历顺序是不是按照我们想象的结果推进的。 + + +最后二维dp数组的C++代码如下: + +```CPP +class Solution { +public: + int findTargetSumWays(vector& nums, int target) { + int sum = 0; + for (int i = 0; i < nums.size(); i++) sum += nums[i]; + if (abs(target) > sum) return 0; // 此时没有方案 + if ((target + sum) % 2 == 1) return 0; // 此时没有方案 + int bagSize = (target + sum) / 2; + + vector> dp(nums.size(), vector(bagSize + 1, 0)); + + // 初始化最上行 + if (nums[0] <= bagSize) dp[0][nums[0]] = 1; + + // 初始化最左列,最左列其他数值在递推公式中就完成了赋值 + dp[0][0] = 1; + + int numZero = 0; + for (int i = 0; i < nums.size(); i++) { + if (nums[i] == 0) numZero++; + dp[i][0] = (int) pow(2.0, numZero); + } + + // 以下遍历顺序行列可以颠倒 + for (int i = 1; i < nums.size(); i++) { // 行,遍历物品 + for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + if (nums[i] > j) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; + } + } + return dp[nums.size() - 1][bagSize]; + } +}; +``` + +### 动态规划 (一维dp数组) + +将二维dp数组压缩成一维dp数组,我们在 [01背包理论基础(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) 讲过滚动数组,原理是一样的,即重复利用每一行的数值。 + +既然是重复利用每一行,就是将二维数组压缩成一行。 + +dp[i][j] 去掉 行的维度,即 dp[j],表示:填满j(包括j)这么大容积的包,有dp[j]种方法。 + +#### 2. 确定递推公式 + +二维DP数组递推公式: `dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];` + +去掉维度i 之后,递推公式:`dp[j] = dp[j] + dp[j - nums[i]]` ,即:`dp[j] += dp[j - nums[i]]` + +**这个公式在后面在讲解背包解决排列组合问题的时候还会用到!** + +#### 3. dp数组如何初始化 + +在上面 二维dp数组中,我们讲解过 dp[0][0] 初始为1,这里dp[0] 同样初始为1 ,即装满背包为0的方法有一种,放0件物品。 + +#### 4. 确定遍历顺序 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中,我们系统讲过对于01背包问题一维dp的遍历。 + +遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。 + +#### 5. 举例推导dp数组 + +输入:nums: [1, 1, 1, 1, 1], target: 3 + +bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4 + +dp数组状态变化如下: + +![](https://file1.kamacoder.com/i/algo/20210125120743274.jpg) + +大家可以和 二维dp数组的打印结果做一下对比。 + +一维DP的C++代码如下: + +```CPP +class Solution { +public: + int findTargetSumWays(vector& nums, int target) { + int sum = 0; + for (int i = 0; i < nums.size(); i++) sum += nums[i]; + if (abs(target) > sum) return 0; // 此时没有方案 + if ((target + sum) % 2 == 1) return 0; // 此时没有方案 + int bagSize = (target + sum) / 2; + vector dp(bagSize + 1, 0); + dp[0] = 1; + for (int i = 0; i < nums.size(); i++) { + for (int j = bagSize; j >= nums[i]; j--) { + dp[j] += dp[j - nums[i]]; + } + } + return dp[bagSize]; + } +}; + +``` +* 时间复杂度:O(n × m),n为正数个数,m为背包容量 +* 空间复杂度:O(m),m为背包容量 + + + +### 拓展 + +关于一维dp数组的递推公式解释,也可以从以下维度来理解。 (**但还是从二维DP数组到一维DP数组这样更容易理解一些**) + +2. 确定递推公式 + +有哪些来源可以推出dp[j]呢? + +只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。 + +例如:dp[j],j 为5, + +* 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。 +* 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。 +* 已经有一个3(nums[i]) 的话,有 dp[2]种方法 凑成 容量为5的背包 +* 已经有一个4(nums[i]) 的话,有 dp[1]种方法 凑成 容量为5的背包 +* 已经有一个5 (nums[i])的话,有 dp[0]种方法 凑成 容量为5的背包 + +那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。 + +所以求组合类问题的公式,都是类似这种: + +``` +dp[j] += dp[j - nums[i]] +``` + + +## 总结 + +此时 大家应该不禁想起,我们之前讲过的[回溯算法:39. 组合总和](https://programmercarl.com/0039.组合总和.html)是不是应该也可以用dp来做啊? + +是可以求的,如果仅仅是求个数的话,就可以用dp,但[回溯算法:39. 组合总和](https://programmercarl.com/0039.组合总和.html)要求的是把所有组合列出来,还是要使用回溯法暴搜的。 + +本题还是有点难度,理解上从二维DP数组更容易理解,做题上直接用一维DP更简洁一些。 + +大家可以选择哪种方式自己更容易理解。 + +在后面得题目中,在求装满背包有几种方法的情况下,递推公式一般为: + +```CPP +dp[j] += dp[j - nums[i]]; +``` + +我们在讲解完全背包的时候,还会用到这个递推公式! + +## 其他语言版本 + +### Java +```java +class Solution { + public int findTargetSumWays(int[] nums, int target) { + int sum = 0; + for (int i = 0; i < nums.length; i++) sum += nums[i]; + + //如果target的绝对值大于sum,那么是没有方案的 + if (Math.abs(target) > sum) return 0; + //如果(target+sum)除以2的余数不为0,也是没有方案的 + if ((target + sum) % 2 == 1) return 0; + + int bagSize = (target + sum) / 2; + int[] dp = new int[bagSize + 1]; + dp[0] = 1; + + for (int i = 0; i < nums.length; i++) { + for (int j = bagSize; j >= nums[i]; j--) { + dp[j] += dp[j - nums[i]]; + } + } + + return dp[bagSize]; + } +} +``` + +易于理解的二维数组版本: +```java +class Solution { + public int findTargetSumWays(int[] nums, int target) { + + // 01背包应用之“有多少种不同的填满背包最大容量的方法“ + // 易于理解的二维数组解法及详细注释 + + int sum = 0; + for(int i = 0; i < nums.length; i++) { + sum += nums[i]; + } + + // 注意nums[i] >= 0的题目条件,意味着sum也是所有nums[i]的绝对值之和 + // 这里保证了sum + target一定是大于等于零的,也就是left大于等于零(毕竟我们定义left大于right) + if(sum < Math.abs(target)){ + return 0; + } + + // 利用二元一次方程组将left用target和sum表示出来(替换掉right组合),详见代码随想录对此题的分析 + // 如果所求的left数组和为小数,则作为整数数组的nums里的任何元素自然是没有办法凑出这个小数的 + if((sum + target) % 2 != 0) { + return 0; + } + + int left = (sum + target) / 2; + + // dp[i][j]:遍历到数组第i个数时, left为j时的能装满背包的方法总数 + int[][] dp = new int[nums.length][left + 1]; + + // 初始化最上行(dp[0][j]),当nums[0] == j时(注意nums[0]和j都一定是大于等于零的,因此不需要判断等于-j时的情况),有唯一一种取法可取到j,dp[0][j]此时等于1 + // 其他情况dp[0][j] = 0 + // java整数数组默认初始值为0 + if (nums[0] <= left) { + dp[0][nums[0]] = 1; + } + + // 初始化最左列(dp[i][0]) + // 当从nums数组的索引0到i的部分有n个0时(n > 0),每个0可以取+/-,因此有2的n次方中可以取到j = 0的方案 + // n = 0说明当前遍历到的数组部分没有0全为正数,因此只有一种方案可以取到j = 0(就是所有数都不取) + int numZeros = 0; + for(int i = 0; i < nums.length; i++) { + if(nums[i] == 0) { + numZeros++; + } + dp[i][0] = (int) Math.pow(2, numZeros); + + } + + // 递推公式分析: + // 当nums[i] > j时,这时候nums[i]一定不能取,所以是dp[i - 1][j]种方案数 + // nums[i] <= j时,num[i]可取可不取,因此方案数是dp[i - 1][j] + dp[i - 1][j - nums[i]] + // 由递推公式可知,先遍历i或j都可 + for(int i = 1; i < nums.length; i++) { + for(int j = 1; j <= left; j++) { + if(nums[i] > j) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]; + } + } + } + + + return dp[nums.length - 1][left]; + + } +} +``` + +### Python +回溯版 +```python +class Solution: + + + def backtracking(self, candidates, target, total, startIndex, path, result): + if total == target: + result.append(path[:]) # 将当前路径的副本添加到结果中 + # 如果 sum + candidates[i] > target,则停止遍历 + for i in range(startIndex, len(candidates)): + if total + candidates[i] > target: + break + total += candidates[i] + path.append(candidates[i]) + self.backtracking(candidates, target, total, i + 1, path, result) + total -= candidates[i] + path.pop() + + def findTargetSumWays(self, nums: List[int], target: int) -> int: + total = sum(nums) + if target > total: + return 0 # 此时没有方案 + if (target + total) % 2 != 0: + return 0 # 此时没有方案,两个整数相加时要注意数值溢出的问题 + bagSize = (target + total) // 2 # 转化为组合总和问题,bagSize就是目标和 + + # 以下是回溯法代码 + result = [] + nums.sort() # 需要对nums进行排序 + self.backtracking(nums, bagSize, 0, 0, [], result) + return len(result) + +``` +二维DP +```python +class Solution: + def findTargetSumWays(self, nums: List[int], target: int) -> int: + total_sum = sum(nums) # 计算nums的总和 + if abs(target) > total_sum: + return 0 # 此时没有方案 + if (target + total_sum) % 2 == 1: + return 0 # 此时没有方案 + target_sum = (target + total_sum) // 2 # 目标和 + + # 创建二维动态规划数组,行表示选取的元素数量,列表示累加和 + dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)] + dp = [[0] * (target_sum + 1) for _ in range(len(nums))] + + # 初始化状态 + dp[0][0] = 1 + if nums[0] <= target_sum: + dp[0][nums[0]] = 1 + numZero = 0 + for i in range(len(nums)): + if nums[i] == 0: + numZero += 1 + dp[i][0] = int(math.pow(2, numZero)) + + # 动态规划过程 + for i in range(1, len(nums)): + for j in range(target_sum + 1): + dp[i][j] = dp[i - 1][j] # 不选取当前元素 + if j >= nums[i - 1]: + dp[i][j] += dp[i - 1][j - nums[i]] # 选取当前元素 + + return dp[len(nums)-1][target_sum] # 返回达到目标和的方案数 + + +``` +一维DP +```python +class Solution: + def findTargetSumWays(self, nums: List[int], target: int) -> int: + total_sum = sum(nums) # 计算nums的总和 + if abs(target) > total_sum: + return 0 # 此时没有方案 + if (target + total_sum) % 2 == 1: + return 0 # 此时没有方案 + target_sum = (target + total_sum) // 2 # 目标和 + dp = [0] * (target_sum + 1) # 创建动态规划数组,初始化为0 + dp[0] = 1 # 当目标和为0时,只有一种方案,即什么都不选 + for num in nums: + for j in range(target_sum, num - 1, -1): + dp[j] += dp[j - num] # 状态转移方程,累加不同选择方式的数量 + return dp[target_sum] # 返回达到目标和的方案数 + +``` + +### Go +回溯法思路 +```go +func findTargetSumWays(nums []int, target int) int { + var result int + var backtracking func(nums []int, target int, index int, currentSum int) + + backtracking = func(nums []int, target int, index int, currentSum int) { + if index == len(nums) { + if currentSum == target { + result++ + } + return + } + + // 选择加上当前数字 + backtracking(nums, target, index+1, currentSum+nums[index]) + + // 选择减去当前数字 + backtracking(nums, target, index+1, currentSum-nums[index]) + } + + backtracking(nums, target, 0, 0) + return result +} +``` +二维dp +```go +func findTargetSumWays(nums []int, target int) int { + sum := 0 + for _, v := range nums { + sum += v + } + if math.Abs(float64(target)) > float64(sum) { + return 0 // 此时没有方案 + } + if (target + sum) % 2 == 1 { + return 0 // 此时没有方案 + } + bagSize := (target + sum) / 2 + + dp := make([][]int, len(nums)) + for i := range dp { + dp[i] = make([]int, bagSize + 1) + } + + // 初始化最上行 + if nums[0] <= bagSize { + dp[0][nums[0]] = 1 + } + + // 初始化最左列,最左列其他数值在递推公式中就完成了赋值 + dp[0][0] = 1 + + var numZero float64 + for i := range nums { + if nums[i] == 0 { + numZero++ + } + dp[i][0] = int(math.Pow(2, numZero)) + } + + // 以下遍历顺序行列可以颠倒 + for i := 1; i < len(nums); i++ { // 行,遍历物品 + for j := 0; j <= bagSize; j++ { // 列,遍历背包 + if nums[i] > j { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]] + } + } + } + return dp[len(nums)-1][bagSize] +} +``` + +一维dp +```go +func findTargetSumWays(nums []int, target int) int { + sum := 0 + for _, v := range nums { + sum += v + } + if abs(target) > sum { + return 0 + } + if (sum+target)%2 == 1 { + return 0 + } + // 计算背包大小 + bag := (sum + target) / 2 + // 定义dp数组 + dp := make([]int, bag+1) + // 初始化 + dp[0] = 1 + // 遍历顺序 + for i := 0; i < len(nums); i++ { + for j := bag; j >= nums[i]; j-- { + //推导公式 + dp[j] += dp[j-nums[i]] + //fmt.Println(dp) + } + } + return dp[bag] +} + +func abs(x int) int { + return int(math.Abs(float64(x))) +} +``` + +### JavaScript +```javascript +/** + * 题目来源: {@link https://leetcode.cn/problems/target-sum/} + * + * 题解来源: {@link https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE} + * + * 时间复杂度: O(n * C), C 为数组元素总和与目标值之和的一半 + * + * 空间复杂度: O(C) + * + * @param { number[] } nums + * @param { number } target + * @return { number } + */ +const findTargetSumWays = (nums, target) => { + // 原题目可转化为: + // + // 将所有元素划分为 2 个集合, + // 一个集合中包含所有要添加 "+" 号的元素, 一个集合中包含所有要添加 "-" 号的元素 + // + // 设两个集合的元素和分别为 positive 和 negative, 所有元素总和为 sum, 那么有如下等式: + // positive + negative = sum (1) + // positive - negative = target (2) + // (1) 与 (2) 联立可得: positive = (sum + target) / 2, + // 所以如果能从原数组中取出若干个元素形成 1 个元素总和为 (sum + target) / 2 的集合, + // 就算得到了 1 种满足题意的组合方法 + // + // 因此, 所求变为: 有多少种取法, 可使得容量为 (sum + target) / 2 的背包被装满? + + const sum = nums.reduce((a, b) => a + b); + + if (Math.abs(target) > sum) { + return 0; + } + + if ((target + sum) % 2) { + return 0; + } + + const bagWeight = (target + sum) / 2; + + // 1. dp 数组的含义 + // dp[j]: 装满容量为 j 的背包, 有 dp[j] 种方法 + let dp = new Array(bagWeight + 1).fill(0); + + // 2. 递推公式 + // dp[j] = Σ(dp[j - nums[j]]), (j ∈ [0, j] 且 j >= nums[j]) + // 因为 dp[j - nums[j]] 表示: 装满容量为 j - nums[j] 背包有 dp[j - nums[j]] 种方法 + // 而容量为 j - nums[j] 的背包只需要再将 nums[j] 放入背包就能使得背包容量达到 j + // 因此, 让背包容量达到 j 有 Σ(dp[j - nums[j]]) 种方法 + + // 3. dp 数组如何初始化 + // dp[0] = 1, dp[1 ~ bagWeight] = 0 + dp[0] = 1; + + // 4. 遍历顺序 + // 先物品后背包, 物品从前往后遍历, 背包容量从后往前遍历 + for (let i = 0; i < nums.length; i++) { + for (let j = bagWeight; j >= nums[i]; j--) { + dp[j] += dp[j - nums[i]]; + } + } + + return dp[bagWeight]; +}; +``` + + +### TypeScript + +```ts +function findTargetSumWays(nums: number[], target: number): number { + // 把数组分成两个组合left, right.left + right = sum, left - right = target. + const sum: number = nums.reduce((a: number, b: number): number => a + b); + if ((sum + target) % 2 || Math.abs(target) > sum) return 0; + const left: number = (sum + target) / 2; + + // 将问题转化为装满容量为left的背包有多少种方法 + // dp[i]表示装满容量为i的背包有多少种方法 + const dp: number[] = new Array(left + 1).fill(0); + dp[0] = 1; // 装满容量为0的背包有1种方法(什么也不装) + for (let i: number = 0; i < nums.length; i++) { + for (let j: number = left; j >= nums[i]; j--) { + dp[j] += dp[j - nums[i]]; + } + } + return dp[left]; +}; +``` + +### Scala + +```scala +object Solution { + def findTargetSumWays(nums: Array[Int], target: Int): Int = { + var sum = nums.sum + if (math.abs(target) > sum) return 0 // 此时没有方案 + if ((sum + target) % 2 == 1) return 0 // 此时没有方案 + var bagSize = (sum + target) / 2 + var dp = new Array[Int](bagSize + 1) + dp(0) = 1 + for (i <- 0 until nums.length; j <- bagSize to nums(i) by -1) { + dp(j) += dp(j - nums(i)) + } + + dp(bagSize) + } +} +``` + +### Rust + +```rust +impl Solution { + pub fn find_target_sum_ways(nums: Vec, target: i32) -> i32 { + let sum = nums.iter().sum::(); + if target.abs() > sum { + return 0; + } + if (target + sum) % 2 == 1 { + return 0; + } + let size = (sum + target) as usize / 2; + let mut dp = vec![0; size + 1]; + dp[0] = 1; + for n in nums { + for s in (n as usize..=size).rev() { + dp[s] += dp[s - n as usize]; + } + } + dp[size] + } +} +``` +### C + +```c +int getSum(int * nums, int numsSize){ + int sum = 0; + for(int i = 0; i < numsSize; i++){ + sum += nums[i]; + } + return sum; +} + +int findTargetSumWays(int* nums, int numsSize, int target) { + int sum = getSum(nums, numsSize); + int diff = sum - target; + // 两种情况不满足 + if(diff < 0 || diff % 2 != 0){ + return 0; + } + int bagSize = diff / 2; + int dp[numsSize + 1][bagSize + 1]; + dp[0][0] = 1; + for(int i = 1; i <= numsSize; i++){ + int num = nums[i - 1]; + for(int j = 0; j <= bagSize; j++){ + dp[i][j] = dp[i - 1][j]; + if(j >= num){ + dp[i][j] += dp[i - 1][j - num]; + } + } + } + return dp[numsSize][bagSize]; +} +``` + + + +### C# + +```csharp +public class Solution +{ + public int FindTargetSumWays(int[] nums, int target) + { + int sum = 0; + foreach (int num in nums) + { + sum += num; + } + if (Math.Abs(target) > sum) return 0; + if ((sum + target) % 2 == 1) return 0; + int bagSize = (sum + target) / 2; + int[] dp = new int[bagSize + 1]; + dp[0] = 1; + for (int i = 0; i < nums.Length; i++) + { + for (int j = bagSize; j >= nums[i]; j--) + { + dp[j] += dp[j - nums[i]]; + } + } + return dp[bagSize]; + } +} +``` + + + diff --git "a/problems/0496.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240I.md" "b/problems/0496.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240I.md" new file mode 100755 index 0000000000..628149b75d --- /dev/null +++ "b/problems/0496.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240I.md" @@ -0,0 +1,507 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 496.下一个更大元素 I + +[力扣题目链接](https://leetcode.cn/problems/next-greater-element-i/) + +给你两个 没有重复元素 的数组 nums1 和 nums2 ,其中nums1 是 nums2 的子集。 + +请你找出 nums1 中每个元素在 nums2 中的下一个比其大的值。 + +nums1 中数字 x 的下一个更大元素是指 x 在 nums2 中对应位置的右边的第一个比 x 大的元素。如果不存在,对应位置输出 -1 。 + +示例 1: + +输入: nums1 = [4,1,2], nums2 = [1,3,4,2]. +输出: [-1,3,-1] +解释: +对于 num1 中的数字 4 ,你无法在第二个数组中找到下一个更大的数字,因此输出 -1 。 +对于 num1 中的数字 1 ,第二个数组中数字1右边的下一个较大数字是 3 。 +对于 num1 中的数字 2 ,第二个数组中没有下一个更大的数字,因此输出 -1 。 + +示例 2: +输入: nums1 = [2,4], nums2 = [1,2,3,4]. +输出: [3,-1] +解释: +对于 num1 中的数字 2 ,第二个数组中的下一个较大数字是 3 。 +对于 num1 中的数字 4 ,第二个数组中没有下一个更大的数字,因此输出-1 。 + +提示: + +* 1 <= nums1.length <= nums2.length <= 1000 +* 0 <= nums1[i], nums2[i] <= 10^4 +* nums1和nums2中所有整数 互不相同 +* nums1 中的所有整数同样出现在 nums2 中 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[单调栈,套上一个壳子就有点绕了| LeetCode:496.下一个更大元素](https://www.bilibili.com/video/BV1jA411m7dX/),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +做本题之前,建议先做一下[739. 每日温度](https://programmercarl.com/0739.每日温度.html) + +在[739. 每日温度](https://programmercarl.com/0739.每日温度.html)中是求每个元素下一个比当前元素大的元素的位置。 + +本题则是说nums1 是 nums2的子集,找nums1中的元素在nums2中下一个比当前元素大的元素。 + +看上去和[739. 每日温度](https://programmercarl.com/0739.每日温度.html) 就如出一辙了。 + +几乎是一样的,但是这么绕了一下,其实还上升了一点难度。 + +需要对单调栈使用的更熟练一些,才能顺利的把本题写出来。 + +从题目示例中我们可以看出最后是要求nums1的每个元素在nums2中下一个比当前元素大的元素,那么就要定义一个和nums1一样大小的数组result来存放结果。 + +一些同学可能看到两个数组都已经懵了,不知道要定一个一个多大的result数组来存放结果了。 + +**这么定义这个result数组初始化应该为多少呢?** + +题目说如果不存在对应位置就输出 -1 ,所以result数组如果某位置没有被赋值,那么就应该是是-1,所以就初始化为-1。 + +在遍历nums2的过程中,我们要判断nums2[i]是否在nums1中出现过,因为最后是要根据nums1元素的下标来更新result数组。 + +**注意题目中说是两个没有重复元素 的数组 nums1 和 nums2**。 + +没有重复元素,我们就可以用map来做映射了。根据数值快速找到下标,还可以判断nums2[i]是否在nums1中出现过。 + +C++中,当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的。我在[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)中也做了详细的解释。 + +那么预处理代码如下: + +```CPP +unordered_map umap; // key:下标元素,value:下标 +for (int i = 0; i < nums1.size(); i++) { + umap[nums1[i]] = i; +} +``` + +使用单调栈,首先要想单调栈是从大到小还是从小到大。 + +本题和739. 每日温度是一样的。 + +栈头到栈底的顺序,要从小到大,也就是保持栈里的元素为递增顺序。只要保持递增,才能找到右边第一个比自己大的元素。 + +可能这里有一些同学不理解,那么可以自己尝试一下用递减栈,能不能求出来。**其实递减栈就是求右边第一个比自己小的元素了**。 + + +接下来就要分析如下三种情况,一定要分析清楚。 + +1. 情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况 + +此时满足递增栈(栈头到栈底的顺序),所以直接入栈。 + +2. 情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况 + +如果相等的话,依然直接入栈,因为我们要求的是右边第一个比自己大的元素,而不是大于等于! + +3. 情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况 + +此时如果入栈就不满足递增栈了,这也是找到右边第一个比自己大的元素的时候。 + +判断栈顶元素是否在nums1里出现过,(注意栈里的元素是nums2的元素),如果出现过,开始记录结果。 + +记录结果这块逻辑有一点小绕,要清楚,此时栈顶元素在nums2数组中右面第一个大的元素是nums2[i](即当前遍历元素)。 + +代码如下: + +```CPP +while (!st.empty() && nums2[i] > nums2[st.top()]) { + if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素 + int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标 + result[index] = nums2[i]; + } + st.pop(); +} +st.push(i); +``` + +以上分析完毕,C++代码如下:(其实本题代码和 [739. 每日温度](https://programmercarl.com/0739.每日温度.html) 是基本差不多的) + + +```CPP +// 版本一 +class Solution { +public: + vector nextGreaterElement(vector& nums1, vector& nums2) { + stack st; + vector result(nums1.size(), -1); + if (nums1.size() == 0) return result; + + unordered_map umap; // key:下标元素,value:下标 + for (int i = 0; i < nums1.size(); i++) { + umap[nums1[i]] = i; + } + st.push(0); + for (int i = 1; i < nums2.size(); i++) { + if (nums2[i] < nums2[st.top()]) { // 情况一 + st.push(i); + } else if (nums2[i] == nums2[st.top()]) { // 情况二 + st.push(i); + } else { // 情况三 + while (!st.empty() && nums2[i] > nums2[st.top()]) { + if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素 + int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标 + result[index] = nums2[i]; + } + st.pop(); + } + st.push(i); + } + } + return result; + } +}; +``` + +针对版本一,进行代码精简后,代码如下: + + +```CPP +// 版本二 +class Solution { +public: + vector nextGreaterElement(vector& nums1, vector& nums2) { + stack st; + vector result(nums1.size(), -1); + if (nums1.size() == 0) return result; + + unordered_map umap; // key:下标元素,value:下标 + for (int i = 0; i < nums1.size(); i++) { + umap[nums1[i]] = i; + } + st.push(0); + for (int i = 1; i < nums2.size(); i++) { + while (!st.empty() && nums2[i] > nums2[st.top()]) { + if (umap.count(nums2[st.top()]) > 0) { // 看map里是否存在这个元素 + int index = umap[nums2[st.top()]]; // 根据map找到nums2[st.top()] 在 nums1中的下标 + result[index] = nums2[i]; + } + st.pop(); + } + st.push(i); + } + return result; + } +}; +``` + +精简的代码是直接把情况一二三都合并到了一起,其实这种代码精简是精简,但思路不是很清晰。 + +建议大家把情况一二三想清楚了,先写出版本一的代码,然后在其基础上在做精简! + +## 其他语言版本 + +### C + +``` C +/* 先用单调栈的方法计算出结果,再根据nums1中的元素去查找对应的结果 */ +/** + * Note: The returned array must be malloced, assume caller calls free(). + */ +int* nextGreaterElement(int* nums1, int nums1Size, int* nums2, int nums2Size, int* returnSize) { + + /* stcak */ + int top = -1; + int stack_len = nums2Size; + int stack[stack_len]; + //memset(stack, 0x00, sizeof(stack)); + + /* nums2 result */ + int* result_nums2 = (int *)malloc(sizeof(int) * nums2Size); + //memset(result_nums2, 0x00, sizeof(int) * nums2Size); + + /* result */ + int* result = (int *)malloc(sizeof(int) * nums1Size); + //memset(result, 0x00, sizeof(int) * nums1Size); + *returnSize = nums1Size; + + /* init */ + stack[++top] = 0; /* stack loaded with array subscripts */ + + for (int i = 0; i < nums2Size; i++) { + result_nums2[i] = -1; + } + + /* get the result_nums2 */ + for (int i = 1; i < nums2Size; i++) { + if (nums2[i] <= nums2[stack[top]]) { + stack[++top] = i; /* push */ + } else { + while ((top >= 0) && (nums2[i] > nums2[stack[top]])) { + result_nums2[stack[top]] = nums2[i]; + top--; /* pop */ + } + stack[++top] = i; + } + } + + /* get the result */ + for (int i = 0; i < nums1Size; i++) { + for (int j = 0; j < nums2Size; j++) { + if (nums1[i] == nums2[j]) { + result[i] = result_nums2[j]; + } + } + } + return result; +} +``` +### Java + +```java +class Solution { + public int[] nextGreaterElement(int[] nums1, int[] nums2) { + Stack temp = new Stack<>(); + int[] res = new int[nums1.length]; + Arrays.fill(res,-1); + HashMap hashMap = new HashMap<>(); + for (int i = 0 ; i< nums1.length ; i++){ + hashMap.put(nums1[i],i); + } + temp.add(0); + for (int i = 1; i < nums2.length; i++) { + if (nums2[i] <= nums2[temp.peek()]) { + temp.add(i); + } else { + while (!temp.isEmpty() && nums2[temp.peek()] < nums2[i]) { + if (hashMap.containsKey(nums2[temp.peek()])){ + Integer index = hashMap.get(nums2[temp.peek()]); + res[index] = nums2[i]; + } + temp.pop(); + } + temp.add(i); + } + } + + return res; + } +} + +// 版本2 +class Solution { + public int[] nextGreaterElement(int[] nums1, int[] nums2) { + HashMap map = new HashMap<>(); + for (int i = 0; i < nums1.length; i++) { + map.put(nums1[i], i); + } + + int[] res = new int[nums1.length]; + Stack stack = new Stack<>(); + Arrays.fill(res, -1); + + for (int i = 0; i < nums2.length; i++) { + while (!stack.isEmpty() && nums2[stack.peek()] < nums2[i]) { + int pre = nums2[stack.pop()]; + if (map.containsKey(pre)) { + res[map.get(pre)] = nums2[i]; + } + } + stack.push(i); + } + + return res; + } +} +``` +### Python3 + +```python +# 版本一 +class Solution: + def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: + result = [-1]*len(nums1) + stack = [0] + for i in range(1,len(nums2)): + # 情况一情况二 + if nums2[i]<=nums2[stack[-1]]: + stack.append(i) + # 情况三 + else: + while len(stack)!=0 and nums2[i]>nums2[stack[-1]]: + if nums2[stack[-1]] in nums1: + index = nums1.index(nums2[stack[-1]]) + result[index]=nums2[i] + stack.pop() + stack.append(i) + return result + +# 版本二 +class Solution: + def nextGreaterElement(self, nums1: List[int], nums2: List[int]) -> List[int]: + stack = [] + # 创建答案数组 + ans = [-1] * len(nums1) + for i in range(len(nums2)): + while len(stack) > 0 and nums2[i] > nums2[stack[-1]]: + # 判断 num1 是否有 nums2[stack[-1]]。如果没有这个判断会出现指针异常 + if nums2[stack[-1]] in nums1: + # 锁定 num1 检索的 index + index = nums1.index(nums2[stack[-1]]) + # 更新答案数组 + ans[index] = nums2[i] + # 弹出小元素 + # 这个代码一定要放在 if 外面。否则单调栈的逻辑就不成立了 + stack.pop() + stack.append(i) + return ans +``` + +### Go + +> 未精简版本 +```go +func nextGreaterElement(nums1 []int, nums2 []int) []int { + res := make([]int, len(nums1)) + for i := range res { res[i] = -1 } + m := make(map[int]int, len(nums1)) + for k, v := range nums1 { m[v] = k } + + stack := []int{0} + for i := 1; i < len(nums2); i++ { + top := stack[len(stack)-1] + if nums2[i] < nums2[top] { + stack = append(stack, i) + } else if nums2[i] == nums2[top] { + stack = append(stack, i) + } else { + for len(stack) != 0 && nums2[i] > nums2[top] { + if v, ok := m[nums2[top]]; ok { + res[v] = nums2[i] + } + stack = stack[:len(stack)-1] + if len(stack) != 0 { + top = stack[len(stack)-1] + } + } + stack = append(stack, i) + } + } + return res +} +``` +> 精简版本 +```go +func nextGreaterElement(nums1 []int, nums2 []int) []int { + res := make([]int, len(nums1)) + for i:= range res { + res[i] = -1 + } + mp := map[int]int{} + for i,v := range nums1 { + mp[v] = i + } + // 单调栈 + stack := []int{} + stack = append(stack,0) + + for i:=1; i0 && nums2[i] > nums2[stack[len(stack)-1]] { + + top := stack[len(stack)-1] + + if _, ok := mp[nums2[top]]; ok { // 看map里是否存在这个元素 + index := mp[nums2[top]]; // 根据map找到nums2[top] 在 nums1中的下标 + res[index] = nums2[i] + } + + stack = stack[:len(stack)-1] // 出栈 + } + stack = append(stack, i) + } + return res +} +``` + +### JavaScript + +```JS +var nextGreaterElement = function (nums1, nums2) { + let stack = []; + let map = new Map(); + for (let i = 0; i < nums2.length; i++) { + while (stack.length && nums2[i] > nums2[stack[stack.length - 1]]) { + let index = stack.pop(); + map.set(nums2[index], nums2[i]); + } + stack.push(i); + } + + let res = []; + for (let j = 0; j < nums1.length; j++) { + res[j] = map.get(nums1[j]) || -1; + } + + return res; +}; +``` + +### TypeScript + +```typescript +function nextGreaterElement(nums1: number[], nums2: number[]): number[] { + const resArr: number[] = new Array(nums1.length).fill(-1); + const stack: number[] = []; + const helperMap: Map = new Map(); + nums1.forEach((num, index) => { + helperMap.set(num, index); + }) + stack.push(0); + for (let i = 1, length = nums2.length; i < length; i++) { + let top = stack[stack.length - 1]; + while (stack.length > 0 && nums2[top] < nums2[i]) { + let index = helperMap.get(nums2[top]); + if (index !== undefined) { + resArr[index] = nums2[i]; + } + stack.pop(); + top = stack[stack.length - 1]; + } + if (helperMap.get(nums2[i]) !== undefined) { + stack.push(i); + } + } + return resArr; +}; +``` + +### Rust + +```rust +use std::collections::HashMap; +impl Solution { + pub fn next_greater_element(nums1: Vec, nums2: Vec) -> Vec { + let (mut res, mut map) = (vec![-1; nums1.len()], HashMap::new()); + if nums1.is_empty() { + return res; + } + + nums1.into_iter().enumerate().for_each(|(v, k)| { + map.insert(k, v); + }); + + let mut stack = vec![]; + for (i, &value) in nums2.iter().enumerate() { + while let Some(&top) = stack.last() { + if value <= nums2[top] { + break; + } + let stacked_index = stack.pop().unwrap(); + if let Some(&mapped_index) = map.get(&nums2[stacked_index]) { + res[mapped_index] = value; + } + } + stack.push(i); + } + + res + } +} +``` + + + + diff --git "a/problems/0501.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\344\274\227\346\225\260.md" "b/problems/0501.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\344\274\227\346\225\260.md" new file mode 100755 index 0000000000..457fd61d24 --- /dev/null +++ "b/problems/0501.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\344\274\227\346\225\260.md" @@ -0,0 +1,1052 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 二叉树上应该怎么求,二叉搜索树上又应该怎么求? + +# 501.二叉搜索树中的众数 + + +[力扣题目链接](https://leetcode.cn/problems/find-mode-in-binary-search-tree/) + + +给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。 + +假定 BST 有如下定义: + +* 结点左子树中所含结点的值小于等于当前结点的值 +* 结点右子树中所含结点的值大于等于当前结点的值 +* 左子树和右子树都是二叉搜索树 + +例如: + +给定 BST [1,null,2,2], + +![501. 二叉搜索树中的众数](https://file1.kamacoder.com/i/algo/20201014221532206.png) + +返回[2]. + +提示:如果众数超过1个,不需考虑输出顺序 + +进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[不仅双指针,还有代码技巧可以惊艳到你! | LeetCode:501.二叉搜索树中的众数](https://www.bilibili.com/video/BV1fD4y117gp),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目呢,递归法我从两个维度来讲。 + +首先如果不是二叉搜索树的话,应该怎么解题,是二叉搜索树,又应该如何解题,两种方式做一个比较,可以加深大家对二叉树的理解。 + +### 递归法 + +#### 如果不是二叉搜索树 + +如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。 + +具体步骤如下: + +1. 这个树都遍历了,用map统计频率 + +至于用前中后序哪种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病! + +这里采用前序遍历,代码如下: + +```CPP +// map key:元素,value:出现频率 +void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 + if (cur == NULL) return ; + map[cur->val]++; // 统计元素频率 + searchBST(cur->left, map); + searchBST(cur->right, map); + return ; +} +``` + +2. 把统计的出来的出现频率(即map中的value)排个序 + +有的同学可能可以想直接对map中的value排序,还真做不到,C++中如果使用std::map或者std::multimap可以对key排序,但不能对value排序。 + +所以要把map转化数组即vector,再进行排序,当然vector里面放的也是`pair`类型的数据,第一个int为元素,第二个int为出现频率。 + +代码如下: + +```CPP +bool static cmp (const pair& a, const pair& b) { + return a.second > b.second; // 按照频率从大到小排序 +} + +vector> vec(map.begin(), map.end()); +sort(vec.begin(), vec.end(), cmp); // 给频率排个序 +``` + +3. 取前面高频的元素 + +此时数组vector中已经是存放着按照频率排好序的pair,那么把前面高频的元素取出来就可以了。 + +代码如下: + +```CPP +result.push_back(vec[0].first); +for (int i = 1; i < vec.size(); i++) { + // 取最高的放到result数组中 + if (vec[i].second == vec[0].second) result.push_back(vec[i].first); + else break; +} +return result; +``` + + +整体C++代码如下: + +```CPP +class Solution { +private: + +void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 + if (cur == NULL) return ; + map[cur->val]++; // 统计元素频率 + searchBST(cur->left, map); + searchBST(cur->right, map); + return ; +} +bool static cmp (const pair& a, const pair& b) { + return a.second > b.second; +} +public: + vector findMode(TreeNode* root) { + unordered_map map; // key:元素,value:出现频率 + vector result; + if (root == NULL) return result; + searchBST(root, map); + vector> vec(map.begin(), map.end()); + sort(vec.begin(), vec.end(), cmp); // 给频率排个序 + result.push_back(vec[0].first); + for (int i = 1; i < vec.size(); i++) { + // 取最高的放到result数组中 + if (vec[i].second == vec[0].second) result.push_back(vec[i].first); + else break; + } + return result; + } +}; +``` + +**所以如果本题没有说是二叉搜索树的话,那么就按照上面的思路写!** + +#### 是二叉搜索树 + +**既然是搜索树,它中序遍历就是有序的**。 + +如图: + +![501.二叉搜索树中的众数1](https://file1.kamacoder.com/i/algo/20210204152758889.png) + +中序遍历代码如下: + +```CPP +void searchBST(TreeNode* cur) { + if (cur == NULL) return ; + searchBST(cur->left); // 左 + (处理节点) // 中 + searchBST(cur->right); // 右 + return ; +} +``` + +遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。 + +关键是在有序数组上的话,好搞,在树上怎么搞呢? + +这就考察对树的操作了。 + +在[二叉树:搜索树的最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html)中我们就使用了pre指针和cur指针的技巧,这次又用上了。 + +弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。 + +而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素。 + +代码如下: + +```CPP +if (pre == NULL) { // 第一个节点 + count = 1; // 频率为1 +} else if (pre->val == cur->val) { // 与前一个节点数值相同 + count++; +} else { // 与前一个节点数值不同 + count = 1; +} +pre = cur; // 更新上一个节点 +``` + +此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办? + +应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个) + +这种方式遍历了两遍数组。 + +那么我们遍历两遍二叉搜索树,把众数集合算出来也是可以的。 + +但这里其实只需要遍历一次就可以找到所有的众数。 + +那么如何只遍历一遍呢? + +如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),代码如下: + +```CPP +if (count == maxCount) { // 如果和最大值相同,放进result中 + result.push_back(cur->val); +} +``` + +是不是感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。 + +所以下面要做如下操作: + +频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。 + +```CPP +if (count > maxCount) { // 如果计数大于最大值 + maxCount = count; // 更新最大频率 + result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + result.push_back(cur->val); +} +``` + +关键代码都讲完了,完整代码如下:(**只需要遍历一遍二叉搜索树,就求出了众数的集合**) + + +```CPP +class Solution { +private: + int maxCount = 0; // 最大频率 + int count = 0; // 统计频率 + TreeNode* pre = NULL; + vector result; + void searchBST(TreeNode* cur) { + if (cur == NULL) return ; + + searchBST(cur->left); // 左 + // 中 + if (pre == NULL) { // 第一个节点 + count = 1; + } else if (pre->val == cur->val) { // 与前一个节点数值相同 + count++; + } else { // 与前一个节点数值不同 + count = 1; + } + pre = cur; // 更新上一个节点 + + if (count == maxCount) { // 如果和最大值相同,放进result中 + result.push_back(cur->val); + } + + if (count > maxCount) { // 如果计数大于最大值频率 + maxCount = count; // 更新最大频率 + result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + result.push_back(cur->val); + } + + searchBST(cur->right); // 右 + return ; + } + +public: + vector findMode(TreeNode* root) { + count = 0; + maxCount = 0; + pre = NULL; // 记录前一个节点 + result.clear(); + + searchBST(root); + return result; + } +}; +``` + + +### 迭代法 + +只要把中序遍历转成迭代,中间节点的处理逻辑完全一样。 + +二叉树前中后序转迭代,传送门: + +* [二叉树:前中后序迭代法](https://programmercarl.com/二叉树的迭代遍历.html) +* [二叉树:前中后序统一风格的迭代方式](https://programmercarl.com/二叉树的统一迭代法.html) + +下面我给出其中的一种中序遍历的迭代法,其中间处理逻辑一点都没有变(我从递归法直接粘过来的代码,连注释都没改) + +代码如下: + +```CPP +class Solution { +public: + vector findMode(TreeNode* root) { + stack st; + TreeNode* cur = root; + TreeNode* pre = NULL; + int maxCount = 0; // 最大频率 + int count = 0; // 统计频率 + vector result; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { // 指针来访问节点,访问到最底层 + st.push(cur); // 将访问的节点放进栈 + cur = cur->left; // 左 + } else { + cur = st.top(); + st.pop(); // 中 + if (pre == NULL) { // 第一个节点 + count = 1; + } else if (pre->val == cur->val) { // 与前一个节点数值相同 + count++; + } else { // 与前一个节点数值不同 + count = 1; + } + if (count == maxCount) { // 如果和最大值相同,放进result中 + result.push_back(cur->val); + } + + if (count > maxCount) { // 如果计数大于最大值频率 + maxCount = count; // 更新最大频率 + result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + result.push_back(cur->val); + } + pre = cur; + cur = cur->right; // 右 + } + } + return result; + } +}; +``` + +## 总结 + +本题在递归法中,我给出了如果是普通二叉树,应该怎么求众数。 + +知道了普通二叉树的做法时候,我再进一步给出二叉搜索树又应该怎么求众数,这样鲜明的对比,相信会对二叉树又有更深层次的理解了。 + +在递归遍历二叉搜索树的过程中,我还介绍了一个统计最高出现频率元素集合的技巧, 要不然就要遍历两次二叉搜索树才能把这个最高出现频率元素的集合求出来。 + + +**为什么没有这个技巧一定要遍历两次呢? 因为要求的是集合,会有多个众数,如果规定只有一个众数,那么就遍历一次稳稳的了。** + +最后我依然给出对应的迭代法,其实就是迭代法中序遍历的模板加上递归法中中间节点的处理逻辑,分分钟就可以写出来,中间逻辑的代码我都是从递归法中直接粘过来的。 + +**求二叉搜索树中的众数其实是一道简单题,但大家可以发现我写了这么一大篇幅的文章来讲解,主要是为了尽量从各个角度对本题进剖析,帮助大家更快更深入理解二叉树**。 + + +> **需要强调的是 leetcode上的耗时统计是非常不准确的,看个大概就行,一样的代码耗时可以差百分之50以上**,所以leetcode的耗时统计别太当回事,知道理论上的效率优劣就行了。 + + +## 其他语言版本 + + +### Java + +暴力法 + +```java +class Solution { + public int[] findMode(TreeNode root) { + Map map = new HashMap<>(); + List list = new ArrayList<>(); + if (root == null) return list.stream().mapToInt(Integer::intValue).toArray(); + // 获得频率 Map + searchBST(root, map); + List> mapList = map.entrySet().stream() + .sorted((c1, c2) -> c2.getValue().compareTo(c1.getValue())) + .collect(Collectors.toList()); + list.add(mapList.get(0).getKey()); + // 把频率最高的加入 list + for (int i = 1; i < mapList.size(); i++) { + if (mapList.get(i).getValue() == mapList.get(i - 1).getValue()) { + list.add(mapList.get(i).getKey()); + } else { + break; + } + } + return list.stream().mapToInt(Integer::intValue).toArray(); + } + + void searchBST(TreeNode curr, Map map) { + if (curr == null) return; + map.put(curr.val, map.getOrDefault(curr.val, 0) + 1); + searchBST(curr.left, map); + searchBST(curr.right, map); + } + +} +``` + +中序遍历-不使用额外空间,利用二叉搜索树特性 + +```Java +class Solution { + ArrayList resList; + int maxCount; + int count; + TreeNode pre; + + public int[] findMode(TreeNode root) { + resList = new ArrayList<>(); + maxCount = 0; + count = 0; + pre = null; + findMode1(root); + int[] res = new int[resList.size()]; + for (int i = 0; i < resList.size(); i++) { + res[i] = resList.get(i); + } + return res; + } + + public void findMode1(TreeNode root) { + if (root == null) { + return; + } + findMode1(root.left); + + int rootValue = root.val; + // 计数 + if (pre == null || rootValue != pre.val) { + count = 1; + } else { + count++; + } + // 更新结果以及maxCount + if (count > maxCount) { + resList.clear(); + resList.add(rootValue); + maxCount = count; + } else if (count == maxCount) { + resList.add(rootValue); + } + pre = root; + + findMode1(root.right); + } +} +``` +迭代法 +```java +class Solution { + public int[] findMode(TreeNode root) { + TreeNode pre = null; + Stack stack = new Stack<>(); + List result = new ArrayList<>(); + int maxCount = 0; + int count = 0; + TreeNode cur = root; + while (cur != null || !stack.isEmpty()) { + if (cur != null) { + stack.push(cur); + cur =cur.left; + }else { + cur = stack.pop(); + // 计数 + if (pre == null || cur.val != pre.val) { + count = 1; + }else { + count++; + } + // 更新结果 + if (count > maxCount) { + maxCount = count; + result.clear(); + result.add(cur.val); + }else if (count == maxCount) { + result.add(cur.val); + } + pre = cur; + cur = cur.right; + } + } + return result.stream().mapToInt(Integer::intValue).toArray(); + } +} +``` +统一迭代法 +```Java +class Solution { + public int[] findMode(TreeNode root) { + int count = 0; + int maxCount = 0; + TreeNode pre = null; + LinkedList res = new LinkedList<>(); + Stack stack = new Stack<>(); + + if(root != null) + stack.add(root); + + while(!stack.isEmpty()){ + TreeNode curr = stack.peek(); + if(curr != null){ + stack.pop(); + if(curr.right != null) + stack.add(curr.right); + stack.add(curr); + stack.add(null); + if(curr.left != null) + stack.add(curr.left); + }else{ + stack.pop(); + TreeNode temp = stack.pop(); + if(pre == null) + count = 1; + else if(pre != null && pre.val == temp.val) + count++; + else + count = 1; + pre = temp; + if(count == maxCount) + res.add(temp.val); + if(count > maxCount){ + maxCount = count; + res.clear(); + res.add(temp.val); + } + } + } + int[] result = new int[res.size()]; + int i = 0; + for (int x : res){ + result[i] = x; + i++; + } + return result; + } +} +``` + + +### Python + +递归法(版本一)利用字典 + +```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 +from collections import defaultdict + +class Solution: + def searchBST(self, cur, freq_map): + if cur is None: + return + freq_map[cur.val] += 1 # 统计元素频率 + self.searchBST(cur.left, freq_map) + self.searchBST(cur.right, freq_map) + + def findMode(self, root): + freq_map = defaultdict(int) # key:元素,value:出现频率 + result = [] + if root is None: + return result + self.searchBST(root, freq_map) + max_freq = max(freq_map.values()) + for key, freq in freq_map.items(): + if freq == max_freq: + result.append(key) + return result + +``` + +递归法(版本二)利用二叉搜索树性质 + +```python +class Solution: + def __init__(self): + self.maxCount = 0 # 最大频率 + self.count = 0 # 统计频率 + self.pre = None + self.result = [] + + def searchBST(self, cur): + if cur is None: + return + + self.searchBST(cur.left) # 左 + # 中 + if self.pre is None: # 第一个节点 + self.count = 1 + elif self.pre.val == cur.val: # 与前一个节点数值相同 + self.count += 1 + else: # 与前一个节点数值不同 + self.count = 1 + self.pre = cur # 更新上一个节点 + + if self.count == self.maxCount: # 如果与最大值频率相同,放进result中 + self.result.append(cur.val) + + if self.count > self.maxCount: # 如果计数大于最大值频率 + self.maxCount = self.count # 更新最大频率 + self.result = [cur.val] # 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + + self.searchBST(cur.right) # 右 + return + + def findMode(self, root): + self.count = 0 + self.maxCount = 0 + self.pre = None # 记录前一个节点 + self.result = [] + + self.searchBST(root) + return self.result +``` +迭代法 +```python +class Solution: + def findMode(self, root): + st = [] + cur = root + pre = None + maxCount = 0 # 最大频率 + count = 0 # 统计频率 + result = [] + + while cur is not None or st: + if cur is not None: # 指针来访问节点,访问到最底层 + st.append(cur) # 将访问的节点放进栈 + cur = cur.left # 左 + else: + cur = st.pop() + if pre is None: # 第一个节点 + count = 1 + elif pre.val == cur.val: # 与前一个节点数值相同 + count += 1 + else: # 与前一个节点数值不同 + count = 1 + + if count == maxCount: # 如果和最大值相同,放进result中 + result.append(cur.val) + + if count > maxCount: # 如果计数大于最大值频率 + maxCount = count # 更新最大频率 + result = [cur.val] # 很关键的一步,不要忘记清空result,之前result里的元素都失效了 + + pre = cur + cur = cur.right # 右 + + return result +``` +### Go + +计数法,不使用额外空间,利用二叉树性质,中序遍历 +```go +func findMode(root *TreeNode) []int { + res := make([]int, 0) + count := 1 + max := 1 + var prev *TreeNode + var travel func(node *TreeNode) + travel = func(node *TreeNode) { + if node == nil { + return + } + travel(node.Left) + if prev != nil && prev.Val == node.Val { + count++ + } else { + count = 1 + } + if count >= max { + if count > max && len(res) > 0 { + res = []int{node.Val} + } else { + res = append(res, node.Val) + } + max = count + } + prev = node + travel(node.Right) + } + travel(root) + return res +} +``` + +### JavaScript + +使用额外空间map的方法 +```javascript +var findMode = function(root) { + // 使用递归中序遍历 + let map = new Map(); + // 1. 确定递归函数以及函数参数 + const traverTree = function(root) { + // 2. 确定递归终止条件 + if(root === null) { + return ; + } + traverTree(root.left); + // 3. 单层递归逻辑 + map.set(root.val,map.has(root.val)?map.get(root.val)+1:1); + traverTree(root.right); + } + traverTree(root); + //上面把数据都存储到map + //下面开始寻找map里面的 + // 定义一个最大出现次数的初始值为root.val的出现次数 + let maxCount = map.get(root.val); + // 定义一个存放结果的数组res + let res = []; + for(let [key,value] of map) { + // 如果当前值等于最大出现次数就直接在res增加该值 + if(value === maxCount) { + res.push(key); + } + // 如果value的值大于原本的maxCount就清空res的所有值,因为找到了更大的 + if(value>maxCount) { + res = []; + maxCount = value; + res.push(key); + } + } + return res; +}; +``` + +不使用额外空间,利用二叉树性质,中序遍历(有序): + +```javascript +var findMode = function(root) { + // 不使用额外空间,使用中序遍历,设置出现最大次数初始值为1 + let count = 0,maxCount = 1; + let pre = root,res = []; + // 1.确定递归函数及函数参数 + const travelTree = function(cur) { + // 2. 确定递归终止条件 + if(cur === null) { + return ; + } + travelTree(cur.left); + // 3. 单层递归逻辑 + if(pre.val === cur.val) { + count++; + }else { + count = 1; + } + pre = cur; + if(count === maxCount) { + res.push(cur.val); + } + if(count > maxCount) { + res = []; + maxCount = count; + res.push(cur.val); + } + travelTree(cur.right); + } + travelTree(root); + return res; +}; +``` + +### TypeScript + +> 辅助Map法 + +```typescript +function findMode(root: TreeNode | null): number[] { + if (root === null) return []; + const countMap: Map = new Map(); + function traverse(root: TreeNode | null): void { + if (root === null) return; + countMap.set(root.val, (countMap.get(root.val) || 0) + 1); + traverse(root.left); + traverse(root.right); + } + traverse(root); + const countArr: number[][] = Array.from(countMap); + countArr.sort((a, b) => { + return b[1] - a[1]; + }) + const resArr: number[] = []; + const maxCount: number = countArr[0][1]; + for (let i of countArr) { + if (i[1] === maxCount) resArr.push(i[0]); + } + return resArr; +}; +``` + +> 递归中直接解决 + +```typescript +function findMode(root: TreeNode | null): number[] { + let preNode: TreeNode | null = null; + let maxCount: number = 0; + let count: number = 0; + let resArr: number[] = []; + function traverse(root: TreeNode | null): void { + if (root === null) return; + traverse(root.left); + if (preNode === null) { // 第一个节点 + count = 1; + } else if (preNode.val === root.val) { + count++; + } else { + count = 1; + } + if (count === maxCount) { + resArr.push(root.val); + } else if (count > maxCount) { + maxCount = count; + resArr.length = 0; + resArr.push(root.val); + } + preNode = root; + traverse(root.right); + } + traverse(root); + return resArr; +}; +``` + +> 迭代法 + +```typescript +function findMode(root: TreeNode | null): number[] { + const helperStack: TreeNode[] = []; + const resArr: number[] = []; + let maxCount: number = 0; + let count: number = 0; + let preNode: TreeNode | null = null; + let curNode: TreeNode | null = root; + while (curNode !== null || helperStack.length > 0) { + if (curNode !== null) { + helperStack.push(curNode); + curNode = curNode.left; + } else { + curNode = helperStack.pop()!; + if (preNode === null) { // 第一个节点 + count = 1; + } else if (preNode.val === curNode.val) { + count++; + } else { + count = 1; + } + if (count === maxCount) { + resArr.push(curNode.val); + } else if (count > maxCount) { + maxCount = count; + resArr.length = 0; + resArr.push(curNode.val); + } + preNode = curNode; + curNode = curNode.right; + } + } + return resArr; +}; +``` + +### Scala + +暴力: +```scala +object Solution { + // 导包 + import scala.collection.mutable // 集合包 + import scala.util.control.Breaks.{break, breakable} // 流程控制包 + def findMode(root: TreeNode): Array[Int] = { + var map = mutable.HashMap[Int, Int]() // 存储节点的值,和该值出现的次数 + def searchBST(curNode: TreeNode): Unit = { + if (curNode == null) return + var value = map.getOrElse(curNode.value, 0) + map.put(curNode.value, value + 1) + searchBST(curNode.left) + searchBST(curNode.right) + } + searchBST(root) // 前序遍历把每个节点的值加入到里面 + // 将map转换为list,随后根据元组的第二个值进行排序 + val list = map.toList.sortWith((map1, map2) => { + if (map1._2 > map2._2) true else false + }) + var res = mutable.ArrayBuffer[Int]() + res.append(list(0)._1) // 将第一个加入结果集 + breakable { + for (i <- 1 until list.size) { + // 如果值相同就加入结果集合,反之break + if (list(i)._2 == list(0)._2) res.append(list(i)._1) + else break + } + } + res.toArray // 最终返回res的Array格式,return关键字可以省略 + } +} +``` + +递归(利用二叉搜索树的性质): +```scala +object Solution { + import scala.collection.mutable + def findMode(root: TreeNode): Array[Int] = { + var maxCount = 0 // 最大频率 + var count = 0 // 统计频率 + var pre: TreeNode = null + var result = mutable.ArrayBuffer[Int]() + + def searchBST(cur: TreeNode): Unit = { + if (cur == null) return + searchBST(cur.left) + if (pre == null) count = 1 // 等于空置为1 + else if (pre.value == cur.value) count += 1 // 与上一个节点的值相同加1 + else count = 1 // 与上一个节点的值不同 + pre = cur + + // 如果和最大值相同,则放入结果集 + if (count == maxCount) result.append(cur.value) + + // 如果当前计数大于最大值频率,更新最大值,清空结果集 + if (count > maxCount) { + maxCount = count + result.clear() + result.append(cur.value) + } + searchBST(cur.right) + } + searchBST(root) + result.toArray // return关键字可以省略 + } +} +``` + +### Rust + +递归: + +```rust +impl Solution { + pub fn find_mode(root: Option>>) -> Vec { + let mut count = 0; + let mut max_count = 0; + let mut res = vec![]; + let mut pre = i32::MIN; + Self::search_bst(&root, &mut pre, &mut res, &mut count, &mut max_count); + res + } + pub fn search_bst( + cur: &Option>>, + mut pre: &mut i32, + res: &mut Vec, + count: &mut i32, + max_count: &mut i32, + ) { + if cur.is_none() { + return; + } + + let cur_node = cur.as_ref().unwrap().borrow(); + Self::search_bst(&cur_node.left, pre, res, count, max_count); + if *pre == i32::MIN { + *count = 1; + } else if *pre == cur_node.val { + *count += 1; + } else { + *count = 1; + }; + match count.cmp(&max_count) { + std::cmp::Ordering::Equal => res.push(cur_node.val), + std::cmp::Ordering::Greater => { + *max_count = *count; + res.clear(); + res.push(cur_node.val); + } + _ => {} + }; + *pre = cur_node.val; + Self::search_bst(&cur_node.right, pre, res, count, max_count); + } +} +``` + +迭代: + +```rust +pub fn find_mode(root: Option>>) -> Vec { + let (mut cur, mut pre) = (root, i32::MIN); + let mut res = vec![]; + let mut stack = vec![]; + let (mut count, mut max_count) = (0, 0); + while !stack.is_empty() || cur.is_some() { + while let Some(node) = cur { + cur = node.borrow().left.clone(); + stack.push(node); + } + if let Some(node) = stack.pop() { + if pre == i32::MIN { + count = 1; + } else if pre == node.borrow().val { + count += 1; + } else { + count = 1; + } + match count.cmp(&max_count) { + std::cmp::Ordering::Equal => res.push(node.borrow().val), + std::cmp::Ordering::Greater => { + max_count = count; + res.clear(); + res.push(node.borrow().val); + } + _ => {} + } + pre = node.borrow().val; + cur = node.borrow().right.clone(); + } + } + res + } +``` +### C# +```csharp +// 递归 +public class Solution +{ + public List res = new List(); + public int count = 0; + public int maxCount = 0; + public TreeNode pre = null; + public int[] FindMode(TreeNode root) + { + SearchBST(root); + return res.ToArray(); + } + public void SearchBST(TreeNode root) + { + if (root == null) return; + SearchBST(root.left); + if (pre == null) + count = 1; + else if (pre.val == root.val) + count++; + else + count = 1; + + pre = root; + if (count == maxCount) + { + res.Add(root.val); + } + else if (count > maxCount) + { + res.Clear(); + res.Add(root.val); + maxCount = count; + } + SearchBST(root.right); + } +} +``` + + + diff --git "a/problems/0503.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240II.md" "b/problems/0503.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240II.md" new file mode 100755 index 0000000000..93924483f3 --- /dev/null +++ "b/problems/0503.\344\270\213\344\270\200\344\270\252\346\233\264\345\244\247\345\205\203\347\264\240II.md" @@ -0,0 +1,358 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 503.下一个更大元素II + +[力扣题目链接](https://leetcode.cn/problems/next-greater-element-ii/) + +给定一个循环数组(最后一个元素的下一个元素是数组的第一个元素),输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序,这个数字之后的第一个比它更大的数,这意味着你应该循环地搜索它的下一个更大的数。如果不存在,则输出 -1。 + +示例 1: + +* 输入: [1,2,1] +* 输出: [2,-1,2] +* 解释: 第一个 1 的下一个更大的数是 2;数字 2 找不到下一个更大的数;第二个 1 的下一个最大的数需要循环搜索,结果也是 2。 + +提示: + +* 1 <= nums.length <= 10^4 +* -10^9 <= nums[i] <= 10^9 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[单调栈,成环了可怎么办?LeetCode:503.下一个更大元素II](https://www.bilibili.com/video/BV15y4y1o7Dw/),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +做本题之前建议先做[739. 每日温度](https://programmercarl.com/0739.每日温度.html) 和 [496.下一个更大元素 I](https://programmercarl.com/0496.下一个更大元素I.html)。 + +这道题和[739. 每日温度](https://programmercarl.com/0739.每日温度.html)也几乎如出一辙。 + +不过,本题要循环数组了。 + +关于单调栈的讲解我在题解[739. 每日温度](https://programmercarl.com/0739.每日温度.html)中已经详细讲解了。 + +本篇我侧重与说一说,如何处理循环数组。 + +相信不少同学看到这道题,就想那我直接把两个数组拼接在一起,然后使用单调栈求下一个最大值不就行了! + +确实可以! + +将两个nums数组拼接在一起,使用单调栈计算出每一个元素的下一个最大值,最后再把结果集即result数组resize到原数组大小就可以了。 + +代码如下: + +```CPP +// 版本一 +class Solution { +public: + vector nextGreaterElements(vector& nums) { + // 拼接一个新的nums + vector nums1(nums.begin(), nums.end()); + nums.insert(nums.end(), nums1.begin(), nums1.end()); + // 用新的nums大小来初始化result + vector result(nums.size(), -1); + if (nums.size() == 0) return result; + + // 开始单调栈 + stack st; + st.push(0); + for (int i = 1; i < nums.size(); i++) { + if (nums[i] < nums[st.top()]) st.push(i); + else if (nums[i] == nums[st.top()]) st.push(i); + else { + while (!st.empty() && nums[i] > nums[st.top()]) { + result[st.top()] = nums[i]; + st.pop(); + } + st.push(i); + } + } + // 最后再把结果集即result数组resize到原数组大小 + result.resize(nums.size() / 2); + return result; + } +}; + +``` + +这种写法确实比较直观,但做了很多无用操作,例如修改了nums数组,而且最后还要把result数组resize回去。 + +resize倒是不费时间,是O(1)的操作,但扩充nums数组相当于多了一个O(n)的操作。 + +其实也可以不扩充nums,而是在遍历的过程中模拟走了两边nums。 + +代码如下: + +```CPP +// 版本二 +class Solution { +public: + vector nextGreaterElements(vector& nums) { + vector result(nums.size(), -1); + if (nums.size() == 0) return result; + stack st; + st.push(0); + for (int i = 1; i < nums.size() * 2; i++) { + // 模拟遍历两边nums,注意一下都是用i % nums.size()来操作 + if (nums[i % nums.size()] < nums[st.top()]) st.push(i % nums.size()); + else if (nums[i % nums.size()] == nums[st.top()]) st.push(i % nums.size()); + else { + while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) { + result[st.top()] = nums[i % nums.size()]; + st.pop(); + } + st.push(i % nums.size()); + } + } + return result; + } +}; +``` + +可以版本二不仅代码精简了,也比版本一少做了无用功! + +最后在给出 单调栈的精简版本,即三种情况都做了合并的操作。 + +```CPP +// 版本二 +class Solution { +public: + vector nextGreaterElements(vector& nums) { + vector result(nums.size(), -1); + if (nums.size() == 0) return result; + stack st; + for (int i = 0; i < nums.size() * 2; i++) { + // 模拟遍历两边nums,注意一下都是用i % nums.size()来操作 + while (!st.empty() && nums[i % nums.size()] > nums[st.top()]) { + result[st.top()] = nums[i % nums.size()]; + st.pop(); + } + st.push(i % nums.size()); + } + return result; + } +}; +``` + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int[] nextGreaterElements(int[] nums) { + //边界判断 + if(nums == null || nums.length <= 1) { + return new int[]{-1}; + } + int size = nums.length; + int[] result = new int[size];//存放结果 + Arrays.fill(result,-1);//默认全部初始化为-1 + Stack st= new Stack<>();//栈中存放的是nums中的元素下标 + for(int i = 0; i < 2*size; i++) { + while(!st.empty() && nums[i % size] > nums[st.peek()]) { + result[st.peek()] = nums[i % size];//更新result + st.pop();//弹出栈顶 + } + st.push(i % size); + } + return result; + } +} +``` + +### Python: +> 版本一: + +```python +class Solution: + def nextGreaterElements(self, nums: List[int]) -> List[int]: + dp = [-1] * len(nums) + stack = [] + for i in range(len(nums)*2): + while(len(stack) != 0 and nums[i%len(nums)] > nums[stack[-1]]): + dp[stack[-1]] = nums[i%len(nums)] + stack.pop() + stack.append(i%len(nums)) + return dp +``` + +> 版本二:针对版本一的优化 + +```python +class Solution: + def nextGreaterElements(self, nums: List[int]) -> List[int]: + res = [-1] * len(nums) + stack = [] + #第一次遍历nums + for i, num in enumerate(nums): + while stack and num > nums[stack[-1]]: + res[stack[-1]] = num + stack.pop() + stack.append(i) + #此时stack仍有剩余,有部分数‘无下一个更大元素’待修正 + #第二次遍历nums + for num in nums: + #一旦stack为空,就表明所有数都有下一个更大元素,可以返回结果 + if not stack: + return res + while stack and num > nums[stack[-1]]: + res[stack[-1]] = num + stack.pop() + #不要将已经有下一个更大元素的数加入栈,这样会重复赋值,只需对第一次遍历剩余的数再尝试寻找下一个更大元素即可 + #最后仍有部分最大数无法有下一个更大元素,返回结果 + return res +``` + +### Go: + +```go +// 版本一 +func nextGreaterElements(nums []int) []int { + // 拼接一个新的nums + numsNew := make([]int, len(nums) * 2) + copy(numsNew, nums) + copy(numsNew[len(nums):], nums) + // 用新的nums大小来初始化result + result := make([]int, len(numsNew)) + for i := range result { + result[i] = -1 + } + + // 开始单调栈 + st := []int{0} + for i := 1; i < len(numsNew); i++ { + if numsNew[i] < numsNew[st[len(st)-1]] { + st = append(st, i) + } else if numsNew[i] == numsNew[st[len(st)-1]] { + st = append(st, i) + } else { + for len(st) > 0 && numsNew[i] > numsNew[st[len(st)-1]] { + result[st[len(st)-1]] = numsNew[i] + st = st[:len(st)-1] + } + st = append(st, i) + } + } + result = result[:len(result)/2] + return result +} +``` + +```go +// 版本二 +func nextGreaterElements(nums []int) []int { + length := len(nums) + result := make([]int,length) + for i:=0;i0&&nums[i%length]>nums[stack[len(stack)-1]]{ + index := stack[len(stack)-1] + stack = stack[:len(stack)-1] // pop + result[index] = nums[i%length] + } + stack = append(stack,i%length) + } + return result +} +``` +### JavaScript: + +```JS +/** + * @param {number[]} nums + * @return {number[]} + */ +var nextGreaterElements = function (nums) { + const len = nums.length; + let stack = []; + let res = Array(len).fill(-1); + for (let i = 0; i < len * 2; i++) { + while ( + stack.length && + nums[i % len] > nums[stack[stack.length - 1]] + ) { + const index = stack.pop(); + res[index] = nums[i % len]; + } + stack.push(i % len); + } + return res; +}; +``` +### TypeScript: + +```typescript +function nextGreaterElements(nums: number[]): number[] { + const length: number = nums.length; + const stack: number[] = []; + stack.push(0); + const resArr: number[] = new Array(length).fill(-1); + for (let i = 1; i < length * 2; i++) { + const index = i % length; + let top = stack[stack.length - 1]; + while (stack.length > 0 && nums[top] < nums[index]) { + resArr[top] = nums[index]; + stack.pop(); + top = stack[stack.length - 1]; + } + if (i < length) { + stack.push(i); + } + } + return resArr; +}; +``` + +### Rust: + +```rust +impl Solution { + pub fn next_greater_elements(nums: Vec) -> Vec { + let mut ans = vec![-1; nums.len() * 2]; + let mut stack = vec![]; + let double = nums.repeat(2); + for (idx, &i) in double.iter().enumerate() { + while !stack.is_empty() && double[*stack.last().unwrap()] < i { + let pos = stack.pop().unwrap(); + ans[pos] = i; + } + stack.push(idx); + } + ans.into_iter().take(nums.len()).collect() + } +} +``` + +> 版本二: + +```rust +impl Solution { + pub fn next_greater_elements(nums: Vec) -> Vec { + let (mut stack, mut res) = (vec![], vec![-1; nums.len()]); + + for i in 0..nums.len() * 2 { + while let Some(&top) = stack.last() { + if nums[i % nums.len()] <= nums[top] { + break; + } + let saved_index = stack.pop().unwrap(); + res[saved_index] = nums[i % nums.len()]; + } + stack.push(i % nums.len()); + } + + res + } +} +``` + + diff --git "a/problems/0509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" "b/problems/0509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" new file mode 100755 index 0000000000..b2e56a613c --- /dev/null +++ "b/problems/0509.\346\226\220\346\263\242\351\202\243\345\245\221\346\225\260.md" @@ -0,0 +1,476 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 509. 斐波那契数 + +[力扣题目链接](https://leetcode.cn/problems/fibonacci-number/) + +斐波那契数,通常用 F(n) 表示,形成的序列称为 斐波那契数列 。该数列由 0 和 1 开始,后面的每一项数字都是前面两项数字的和。也就是: +F(0) = 0,F(1) = 1 +F(n) = F(n - 1) + F(n - 2),其中 n > 1 +给你n ,请计算 F(n) 。 + +示例 1: +* 输入:2 +* 输出:1 +* 解释:F(2) = F(1) + F(0) = 1 + 0 = 1 + +示例 2: +* 输入:3 +* 输出:2 +* 解释:F(3) = F(2) + F(1) = 1 + 1 = 2 + +示例 3: +* 输入:4 +* 输出:3 +* 解释:F(4) = F(3) + F(2) = 2 + 1 = 3 + +提示: + +* 0 <= n <= 30 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[手把手带你入门动态规划 | leetcode:509.斐波那契数](https://www.bilibili.com/video/BV1f5411K7mo),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +斐波那契数列大家应该非常熟悉不过了,非常适合作为动规第一道题目来练练手。 + +因为这道题目比较简单,可能一些同学并不需要做什么分析,直接顺手一写就过了。 + +**但「代码随想录」的风格是:简单题目是用来加深对解题方法论的理解的**。 + +通过这道题目让大家可以初步认识到,按照动规五部曲是如何解题的。 + +对于动规,如果没有方法论的话,可能简单题目可以顺手一写就过,难一点就不知道如何下手了。 + +所以我总结的动规五部曲,是要用来贯穿整个动态规划系列的,就像之前讲过[二叉树系列的递归三部曲](https://www.programmercarl.com/二叉树的递归遍历.html),[回溯法系列的回溯三部曲](https://programmercarl.com/回溯算法理论基础.html)一样。后面慢慢大家就会体会到,动规五部曲方法的重要性。 + +### 动态规划 + +动规五部曲: + +这里我们要用一个一维dp数组来保存递归的结果 + +1. 确定dp数组以及下标的含义 + +dp[i]的定义为:第i个数的斐波那契数值是dp[i] + +2. 确定递推公式 + +为什么这是一道非常简单的入门题目呢? + +**因为题目已经把递推公式直接给我们了:状态转移方程 dp[i] = dp[i - 1] + dp[i - 2];** + +3. dp数组如何初始化 + +**题目中把如何初始化也直接给我们了,如下:** + +``` +dp[0] = 0; +dp[1] = 1; +``` + +4. 确定遍历顺序 + +从递归公式dp[i] = dp[i - 1] + dp[i - 2];中可以看出,dp[i]是依赖 dp[i - 1] 和 dp[i - 2],那么遍历的顺序一定是从前到后遍历的 + +5. 举例推导dp数组 + +按照这个递推公式dp[i] = dp[i - 1] + dp[i - 2],我们来推导一下,当N为10的时候,dp数组应该是如下的数列: + +0 1 1 2 3 5 8 13 21 34 55 + +如果代码写出来,发现结果不对,就把dp数组打印出来看看和我们推导的数列是不是一致的。 + +以上我们用动规的方法分析完了,C++代码如下: + +```CPP +class Solution { +public: + int fib(int N) { + if (N <= 1) return N; + vector dp(N + 1); + dp[0] = 0; + dp[1] = 1; + for (int i = 2; i <= N; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[N]; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +当然可以发现,我们只需要维护两个数值就可以了,不需要记录整个序列。 + +代码如下: + +```CPP +class Solution { +public: + int fib(int N) { + if (N <= 1) return N; + int dp[2]; + dp[0] = 0; + dp[1] = 1; + for (int i = 2; i <= N; i++) { + int sum = dp[0] + dp[1]; + dp[0] = dp[1]; + dp[1] = sum; + } + return dp[1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +### 递归解法 + +本题还可以使用递归解法来做 + +代码如下: + +```CPP +class Solution { +public: + int fib(int N) { + if (N < 2) return N; + return fib(N - 1) + fib(N - 2); + } +}; +``` + +* 时间复杂度:O(2^n) +* 空间复杂度:O(n),算上了编程语言中实现递归的系统栈所占空间 + +这个递归的时间复杂度大家画一下树形图就知道了,如果不清晰的同学,可以看这篇:[通过一道面试题目,讲一讲递归算法的时间复杂度!](./前序/递归算法的时间复杂度.md) + + +## 总结 + +斐波那契数列这道题目是非常基础的题目,我在后面的动态规划的讲解中将会多次提到斐波那契数列! + +这里我严格按照[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)中的动规五部曲来分析了这道题目,一些分析步骤可能同学感觉没有必要搞的这么复杂,代码其实上来就可以撸出来。 + +但我还是强调一下,简单题是用来掌握方法论的,动规五部曲将在接下来的动态规划讲解中发挥重要作用,敬请期待! + +就酱,循序渐进学算法,认准「代码随想录」! + + + + + +## 其他语言版本 + + +### Java +```Java +class Solution { + public int fib(int n) { + if (n < 2) return n; + int a = 0, b = 1, c = 0; + for (int i = 1; i < n; i++) { + c = a + b; + a = b; + b = c; + } + return c; + } +} +``` +```java +//非压缩状态的版本 +class Solution { + public int fib(int n) { + if (n <= 1) return n; + int[] dp = new int[n + 1]; + dp[0] = 0; + dp[1] = 1; + for (int index = 2; index <= n; index++){ + dp[index] = dp[index - 1] + dp[index - 2]; + } + return dp[n]; + } +} +``` + +### Python +动态规划(版本一) +```python + +class Solution: + def fib(self, n: int) -> int: + + # 排除 Corner Case + if n == 0: + return 0 + + # 创建 dp table + dp = [0] * (n + 1) + + # 初始化 dp 数组 + dp[0] = 0 + dp[1] = 1 + + # 遍历顺序: 由前向后。因为后面要用到前面的状态 + for i in range(2, n + 1): + + # 确定递归公式/状态转移公式 + dp[i] = dp[i - 1] + dp[i - 2] + + # 返回答案 + return dp[n] + +``` +动态规划(版本二) +```python + +class Solution: + def fib(self, n: int) -> int: + if n <= 1: + return n + + dp = [0, 1] + + for i in range(2, n + 1): + total = dp[0] + dp[1] + dp[0] = dp[1] + dp[1] = total + + return dp[1] + + +``` +动态规划(版本三) +```python +class Solution: + def fib(self, n: int) -> int: + if n <= 1: + return n + + prev1, prev2 = 0, 1 + + for _ in range(2, n + 1): + curr = prev1 + prev2 + prev1, prev2 = prev2, curr + + return prev2 + + + + +``` +递归(版本一) +```python + +class Solution: + def fib(self, n: int) -> int: + if n < 2: + return n + return self.fib(n - 1) + self.fib(n - 2) +``` + +### Go +```Go +func fib(n int) int { + if n < 2 { + return n + } + a, b, c := 0, 1, 0 + for i := 1; i < n; i++ { + c = a + b + a, b = b, c + } + return c +} +``` +### JavaScript +解法一 +```Javascript +var fib = function(n) { + let dp = [0, 1] + for(let i = 2; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2] + } + console.log(dp) + return dp[n] +}; +``` +解法二:时间复杂度O(N),空间复杂度O(1) +```Javascript +var fib = function(n) { + // 动规状态转移中,当前结果只依赖前两个元素的结果,所以只要两个变量代替dp数组记录状态过程。将空间复杂度降到O(1) + let pre1 = 1 + let pre2 = 0 + let temp + if (n === 0) return 0 + if (n === 1) return 1 + for(let i = 2; i <= n; i++) { + temp = pre1 + pre1 = pre1 + pre2 + pre2 = temp + } + return pre1 +}; +``` + +### TypeScript + +```typescript +function fib(n: number): number { + /** + dp[i]: 第i个斐波那契数 + dp[0]: 0; + dp[1]:1; + ... + dp[i] = dp[i - 1] + dp[i - 2]; + */ + const dp: number[] = []; + dp[0] = 0; + dp[1] = 1; + for (let i = 2; i <= n; i++) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; +}; +``` + +### C + +动态规划: +```c +int fib(int n){ + //当n <= 1时,返回n + if(n <= 1) + return n; + //动态开辟一个int数组,大小为n+1 + int *dp = (int *)malloc(sizeof(int) * (n + 1)); + //设置0号位为0,1号为为1 + dp[0] = 0; + dp[1] = 1; + + //从前向后遍历数组(i=2; i <= n; ++i),下标为n时的元素为dp[i-1] + dp[i-2] + int i; + for(i = 2; i <= n; ++i) { + dp[i] = dp[i - 1] + dp[i - 2]; + } + return dp[n]; +} +``` + +递归实现: +```c +int fib(int n){ + //若n小于等于1,返回n + if(n <= 1) + return n; + //否则返回fib(n-1) + fib(n-2) + return fib(n-1) + fib(n-2); +} +``` +### Rust +动态规划: +```Rust +impl Solution { + pub fn fib(n: i32) -> i32 { + if n <= 1 { + return n; + } + let n = n as usize; + let mut dp = vec![0; n + 1]; + dp[1] = 1; + for i in 2..=n { + dp[i] = dp[i - 2] + dp[i - 1]; + } + dp[n] + } +} +``` + +递归实现: +```Rust +impl Solution { + pub fn fib(n: i32) -> i32 { + if n <= 1 { + n + } else { + Self::fib(n - 1) + Self::fib(n - 2) + } + } +} +``` + +### Scala + +动态规划: +```scala +object Solution { + def fib(n: Int): Int = { + if (n <= 1) return n + var dp = new Array[Int](n + 1) + dp(1) = 1 + for (i <- 2 to n) { + dp(i) = dp(i - 1) + dp(i - 2) + } + dp(n) + } +} +``` + +递归: +```scala +object Solution { + def fib(n: Int): Int = { + if (n <= 1) return n + fib(n - 1) + fib(n - 2) + } +} +``` + +### C# + +动态规划: + +```csharp +public class Solution +{ + public int Fib(int n) + { + if(n<2) return n; + int[] dp = new int[2] { 0, 1 }; + for (int i = 2; i <= n; i++) + { + int temp = dp[0] + dp[1]; + dp[0] = dp[1]; + dp[1] = temp; + } + return dp[1]; + } +} +``` + +递归: + +```csharp +public class Solution +{ + public int Fib(int n) + { + if(n<2) + return n; + return Fib(n-1)+Fib(n-2); + } +} +``` + + + + + + diff --git "a/problems/0513.\346\211\276\346\240\221\345\267\246\344\270\213\350\247\222\347\232\204\345\200\274.md" "b/problems/0513.\346\211\276\346\240\221\345\267\246\344\270\213\350\247\222\347\232\204\345\200\274.md" new file mode 100755 index 0000000000..03f076a2c9 --- /dev/null +++ "b/problems/0513.\346\211\276\346\240\221\345\267\246\344\270\213\350\247\222\347\232\204\345\200\274.md" @@ -0,0 +1,764 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 513.找树左下角的值 + +[力扣题目链接](https://leetcode.cn/problems/find-bottom-left-tree-value/) + +给定一个二叉树,在树的最后一行找到最左边的值。 + + +示例 1: + +![513.找树左下角的值](https://file1.kamacoder.com/i/algo/20210204152956836.png) + +示例 2: + +![513.找树左下角的值1](https://file1.kamacoder.com/i/algo/20210204153017586.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[怎么找二叉树的左下角? 递归中又带回溯了,怎么办?| LeetCode:513.找二叉树左下角的值](https://www.bilibili.com/video/BV1424y1Z7pn),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题要找出树的最后一行的最左边的值。此时大家应该想起用层序遍历是非常简单的了,反而用递归的话会比较难一点。 + +我们依然还是先介绍递归法。 + +### 递归 + +咋眼一看,这道题目用递归的话就就一直向左遍历,最后一个就是答案呗? + +没有这么简单,一直向左遍历到最后一个,它未必是最后一行啊。 + +我们来分析一下题目:在树的**最后一行**找到**最左边的值**。 + +首先要是最后一行,然后是最左边的值。 + +如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。 + +如果对二叉树深度和高度还有点疑惑的话,请看:[110.平衡二叉树](https://programmercarl.com/0110.平衡二叉树.html)。 + +所以要找深度最大的叶子节点。 + +那么如何找最左边的呢?可以使用前序遍历(当然中序,后序都可以,因为本题没有 中间节点的处理逻辑,只要左优先就行),保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。 + +递归三部曲: + +1. 确定递归函数的参数和返回值 + +参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。 + +本题还需要类里的两个全局变量,maxDepth用来记录最大深度,result记录最大深度最左节点的数值。 + +代码如下: + +```CPP +int maxDepth = INT_MIN; // 全局变量 记录最大深度 +int result; // 全局变量 最大深度最左节点的数值 +void traversal(TreeNode* root, int depth) +``` + + +2. 确定终止条件 + +当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度。 + +代码如下: + +```CPP +if (root->left == NULL && root->right == NULL) { + if (depth > maxDepth) { + maxDepth = depth; // 更新最大深度 + result = root->val; // 最大深度最左面的数值 + } + return; +} +``` + +3. 确定单层递归的逻辑 + +在找最大深度的时候,递归的过程中依然要使用回溯,代码如下: + +```CPP + // 中 +if (root->left) { // 左 + depth++; // 深度加一 + traversal(root->left, depth); + depth--; // 回溯,深度减一 +} +if (root->right) { // 右 + depth++; // 深度加一 + traversal(root->right, depth); + depth--; // 回溯,深度减一 +} +return; +``` + +完整代码如下: + +```CPP +class Solution { +public: + int maxDepth = INT_MIN; + int result; + void traversal(TreeNode* root, int depth) { + if (root->left == NULL && root->right == NULL) { + if (depth > maxDepth) { + maxDepth = depth; + result = root->val; + } + return; + } + if (root->left) { + depth++; + traversal(root->left, depth); + depth--; // 回溯 + } + if (root->right) { + depth++; + traversal(root->right, depth); + depth--; // 回溯 + } + return; + } + int findBottomLeftValue(TreeNode* root) { + traversal(root, 0); + return result; + } +}; +``` + +当然回溯的地方可以精简,精简代码如下: + +```CPP +class Solution { +public: + int maxDepth = INT_MIN; + int result; + void traversal(TreeNode* root, int depth) { + if (root->left == NULL && root->right == NULL) { + if (depth > maxDepth) { + maxDepth = depth; + result = root->val; + } + return; + } + if (root->left) { + traversal(root->left, depth + 1); // 隐藏着回溯 + } + if (root->right) { + traversal(root->right, depth + 1); // 隐藏着回溯 + } + return; + } + int findBottomLeftValue(TreeNode* root) { + traversal(root, 0); + return result; + } +}; +``` + +如果对回溯部分精简的代码 不理解的话,可以看这篇[257. 二叉树的所有路径](https://programmercarl.com/0257.二叉树的所有路径.html) + + +### 迭代法 + +本题使用层序遍历再合适不过了,比递归要好理解得多! + +只需要记录最后一行第一个节点的数值就可以了。 + +如果对层序遍历不了解,看这篇[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html),这篇里也给出了层序遍历的模板,稍作修改就一过刷了这道题了。 + +代码如下: + +```CPP +class Solution { +public: + int findBottomLeftValue(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + int result = 0; + while (!que.empty()) { + int size = que.size(); + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + if (i == 0) result = node->val; // 记录最后一行第一个元素 + if (node->left) que.push(node->left); + if (node->right) que.push(node->right); + } + } + return result; + } +}; +``` + +## 总结 + +本题涉及如下几点: + +* 递归求深度的写法,我们在[110.平衡二叉树](https://programmercarl.com/0110.平衡二叉树.html)中详细的分析了深度应该怎么求,高度应该怎么求。 +* 递归中其实隐藏了回溯,在[257. 二叉树的所有路径](https://programmercarl.com/0257.二叉树的所有路径.html)中讲解了究竟哪里使用了回溯,哪里隐藏了回溯。 +* 层次遍历,在[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html)深度讲解了二叉树层次遍历。 +所以本题涉及到的点,我们之前都讲解过,这些知识点需要同学们灵活运用,这样就举一反三了。 + + +## 其他语言版本 + + +### Java + +```java +// 递归法 +class Solution { + private int Deep = -1; + private int value = 0; + public int findBottomLeftValue(TreeNode root) { + value = root.val; + findLeftValue(root,0); + return value; + } + + private void findLeftValue (TreeNode root,int deep) { + if (root == null) return; + if (root.left == null && root.right == null) { + if (deep > Deep) { + value = root.val; + Deep = deep; + } + } + if (root.left != null) findLeftValue(root.left,deep + 1); + if (root.right != null) findLeftValue(root.right,deep + 1); + } +} +``` + +```java +//迭代法 +class Solution { + + public int findBottomLeftValue(TreeNode root) { + Queue queue = new LinkedList<>(); + queue.offer(root); + int res = 0; + while (!queue.isEmpty()) { + int size = queue.size(); + for (int i = 0; i < size; i++) { + TreeNode poll = queue.poll(); + if (i == 0) { + res = poll.val; + } + if (poll.left != null) { + queue.offer(poll.left); + } + if (poll.right != null) { + queue.offer(poll.right); + } + } + } + return res; + } +} +``` + + + +### Python +(版本一)递归法 + 回溯 +```python +class Solution: + def findBottomLeftValue(self, root: TreeNode) -> int: + self.max_depth = float('-inf') + self.result = None + self.traversal(root, 0) + return self.result + + def traversal(self, node, depth): + if not node.left and not node.right: + if depth > self.max_depth: + self.max_depth = depth + self.result = node.val + return + + if node.left: + depth += 1 + self.traversal(node.left, depth) + depth -= 1 + if node.right: + depth += 1 + self.traversal(node.right, depth) + depth -= 1 + +``` + +(版本二)递归法+精简 +```python +class Solution: + def findBottomLeftValue(self, root: TreeNode) -> int: + self.max_depth = float('-inf') + self.result = None + self.traversal(root, 0) + return self.result + + def traversal(self, node, depth): + if not node.left and not node.right: + if depth > self.max_depth: + self.max_depth = depth + self.result = node.val + return + + if node.left: + self.traversal(node.left, depth+1) + if node.right: + self.traversal(node.right, depth+1) +``` + +(版本三) 迭代法 +```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 +from collections import deque +class Solution: + def findBottomLeftValue(self, root): + if root is None: + return 0 + queue = deque() + queue.append(root) + result = 0 + while queue: + size = len(queue) + for i in range(size): + node = queue.popleft() + if i == 0: + result = node.val + if node.left: + queue.append(node.left) + if node.right: + queue.append(node.right) + return result + + + +``` + +### Go + +递归法: + +```go +var depth int // 全局变量 最大深度 +var res int // 记录最终结果 +func findBottomLeftValue(root *TreeNode) int { + depth, res = 0, 0 // 初始化 + dfs(root, 1) + return res +} + +func dfs(root *TreeNode, d int) { + if root == nil { + return + } + // 因为先遍历左边,所以左边如果有值,右边的同层不会更新结果 + if root.Left == nil && root.Right == nil && depth < d { + depth = d + res = root.Val + } + dfs(root.Left, d+1) // 隐藏回溯 + dfs(root.Right, d+1) +} +``` + +迭代法: + +```go +func findBottomLeftValue(root *TreeNode) int { + var gradation int + queue := list.New() + + queue.PushBack(root) + for queue.Len() > 0 { + length := queue.Len() + for i := 0; i < length; i++ { + node := queue.Remove(queue.Front()).(*TreeNode) + if i == 0 { + gradation = node.Val + } + if node.Left != nil { + queue.PushBack(node.Left) + } + if node.Right != nil { + queue.PushBack(node.Right) + } + } + } + return gradation +} +``` + +### JavaScript + +递归版本: + +```javascript +var findBottomLeftValue = function(root) { + //首先考虑递归遍历 前序遍历 找到最大深度的叶子节点即可 + let maxPath = 0, resNode = null; + // 1. 确定递归函数的函数参数 + const dfsTree = function(node, curPath) { + // 2. 确定递归函数终止条件 + if(node.left === null && node.right === null) { + if(curPath > maxPath) { + maxPath = curPath; + resNode = node.val; + } + } + node.left && dfsTree(node.left, curPath+1); + node.right && dfsTree(node.right, curPath+1); + } + dfsTree(root,1); + return resNode; +}; +``` +层序遍历: + +```javascript +var findBottomLeftValue = function(root) { + //考虑层序遍历 记录最后一行的第一个节点 + let queue = []; + if(root === null) { + return null; + } + queue.push(root); + let resNode; + while(queue.length) { + let length = queue.length; + for(let i = 0; i < length; i++) { + let node = queue.shift(); + if(i === 0) { + resNode = node.val; + } + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + } + return resNode; +}; +``` + +### TypeScript + +> 递归法: + +```typescript +function findBottomLeftValue(root: TreeNode | null): number { + function recur(root: TreeNode, depth: number): void { + if (root.left === null && root.right === null) { + if (depth > maxDepth) { + maxDepth = depth; + resVal = root.val; + } + return; + } + if (root.left !== null) recur(root.left, depth + 1); + if (root.right !== null) recur(root.right, depth + 1); + } + let maxDepth: number = 0; + let resVal: number = 0; + if (root === null) return resVal; + recur(root, 1); + return resVal; +}; +``` + +> 迭代法: + +```typescript +function findBottomLeftValue(root: TreeNode | null): number { + let helperQueue: TreeNode[] = []; + if (root !== null) helperQueue.push(root); + let resVal: number = 0; + let tempNode: TreeNode; + while (helperQueue.length > 0) { + resVal = helperQueue[0].val; + for (let i = 0, length = helperQueue.length; i < length; i++) { + tempNode = helperQueue.shift()!; + if (tempNode.left !== null) helperQueue.push(tempNode.left); + if (tempNode.right !== null) helperQueue.push(tempNode.right); + } + } + return resVal; +}; +``` + +### Swift + +递归版本: + +```swift +var maxLen = -1 +var maxLeftValue = 0 +func findBottomLeftValue_2(_ root: TreeNode?) -> Int { + traversal(root, 0) + return maxLeftValue +} + +func traversal(_ root: TreeNode?, _ deep: Int) { + guard let root = root else { + return + } + + if root.left == nil && root.right == nil { + if deep > maxLen { + maxLen = deep + maxLeftValue = root.val + } + return + } + + if root.left != nil { + traversal(root.left, deep + 1) + } + + if root.right != nil { + traversal(root.right, deep + 1) + } + return +} +``` +层序遍历: + +```swift +func findBottomLeftValue(_ root: TreeNode?) -> Int { + guard let root = root else { + return 0 + } + + var queue = [root] + var result = 0 + + while !queue.isEmpty { + let size = queue.count + for i in 0.. maxLen) { + maxLen = depth + maxLeftValue = node.value + } + if (node.left != null) traversal(node.left, depth + 1) + if (node.right != null) traversal(node.right, depth + 1) + } + traversal(root, 0) // 调用方法 + maxLeftValue // return关键字可以省略 + } +} +``` + +层序遍历: +```scala + import scala.collection.mutable + + def findBottomLeftValue(root: TreeNode): Int = { + val queue = mutable.Queue[TreeNode]() + queue.enqueue(root) + var res = 0 // 记录每层最左侧结果 + while (!queue.isEmpty) { + val len = queue.size + for (i <- 0 until len) { + val curNode = queue.dequeue() + if (i == 0) res = curNode.value // 记录最最左侧的节点 + if (curNode.left != null) queue.enqueue(curNode.left) + if (curNode.right != null) queue.enqueue(curNode.right) + } + } + res // 最终返回结果,return关键字可以省略 + } +``` + +### Rust + +**层序遍历** + +```rust +use std::cell::RefCell; +use std::collections::VecDeque; +use std::rc::Rc; +impl Solution { + pub fn find_bottom_left_value(root: Option>>) -> i32 { + let mut queue = VecDeque::new(); + let mut res = 0; + if root.is_some() { + queue.push_back(root); + } + while !queue.is_empty() { + for i in 0..queue.len() { + let node = queue.pop_front().unwrap().unwrap(); + if i == 0 { + res = node.borrow().val; + } + if node.borrow().left.is_some() { + queue.push_back(node.borrow().left.clone()); + } + if node.borrow().right.is_some() { + queue.push_back(node.borrow().right.clone()); + } + } + } + res + } +} +``` + +**递归** + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + //*递归*/ + pub fn find_bottom_left_value(root: Option>>) -> i32 { + let mut res = 0; + let mut max_depth = i32::MIN; + Self::traversal(root, 0, &mut max_depth, &mut res); + res + } + fn traversal( + root: Option>>, + depth: i32, + max_depth: &mut i32, + res: &mut i32, + ) { + let node = root.unwrap(); + if node.borrow().left.is_none() && node.borrow().right.is_none() { + if depth > *max_depth { + *max_depth = depth; + *res = node.borrow().val; + } + return; + } + if node.borrow().left.is_some() { + Self::traversal(node.borrow().left.clone(), depth + 1, max_depth, res); + } + if node.borrow().right.is_some() { + Self::traversal(node.borrow().right.clone(), depth + 1, max_depth, res); + } + } +} +``` +### C# +```csharp +//递归 +int maxDepth = -1; +int res = 0; +public int FindBottomLeftValue(TreeNode root) +{ + Traversal(root, 0); + return res; +} +public void Traversal(TreeNode root, int depth) +{ + if (root.left == null && root.right == null) + { + if (depth > maxDepth) + { + maxDepth = depth; + res = root.val; + } + return; + } + if (root.left != null) + { + Traversal(root.left, depth + 1); + } + if (root.right != null) + { + Traversal(root.right, depth + 1); + } + return; +} +``` +```csharp +/* + * @lc app=leetcode id=513 lang=csharp + * 迭代法 + * [513] Find Bottom Left Tree Value + */ + +// @lc code=start +public class Solution +{ + public int FindBottomLeftValue(TreeNode root) + { + Queue que = new Queue(); + + if (root != null) + { + que.Enqueue(root); + } + + int ans = 0; + while (que.Count != 0) + { + + int size = que.Count; + for (var i = 0; i < size; i++) + { + var curNode = que.Peek(); + que.Dequeue(); + if(i == 0){ + ans = curNode.val; + } + if (curNode.left != null) + { + que.Enqueue(curNode.left); + } + if (curNode.right != null) + { + que.Enqueue(curNode.right); + } + } + + } + return ans; + } +} +// @lc code=end +``` + diff --git "a/problems/0516.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227.md" "b/problems/0516.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..882c36bb05 --- /dev/null +++ "b/problems/0516.\346\234\200\351\225\277\345\233\236\346\226\207\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,299 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 516.最长回文子序列 + +[力扣题目链接](https://leetcode.cn/problems/longest-palindromic-subsequence/) + +给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。 + +示例 1: +输入: "bbbab" +输出: 4 +一个可能的最长回文子序列为 "bbbb"。 + +示例 2: +输入:"cbbd" +输出: 2 +一个可能的最长回文子序列为 "bb"。 + +提示: + +* 1 <= s.length <= 1000 +* s 只包含小写英文字母 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划再显神通,LeetCode:516.最长回文子序列](https://www.bilibili.com/video/BV1d8411K7W6/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +我们刚刚做过了 [动态规划:回文子串](https://programmercarl.com/0647.回文子串.html),求的是回文子串,而本题要求的是回文子序列, 要搞清楚这两者之间的区别。 + +**回文子串是要连续的,回文子序列可不是连续的!** 回文子串,回文子序列都是动态规划经典题目。 + +回文子串,可以做这两题: + +* 647.回文子串 +* 5.最长回文子串 + +思路其实是差不多的,但本题要比求回文子串简单一点,因为情况少了一点。 + +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i][j]:字符串s在[i, j]范围内最长的回文子序列的长度为dp[i][j]**。 + +2. 确定递推公式 + +在判断回文子串的题目中,关键逻辑就是看s[i]与s[j]是否相同。 + +如果s[i]与s[j]相同,那么dp[i][j] = dp[i + 1][j - 1] + 2; + +如图: +![516.最长回文子序列](https://file1.kamacoder.com/i/algo/20210127151350563.jpg) + +(如果这里看不懂,回忆一下dp[i][j]的定义) + +如果s[i]与s[j]不相同,说明s[i]和s[j]的同时加入 并不能增加[i,j]区间回文子序列的长度,那么分别加入s[i]、s[j]看看哪一个可以组成最长的回文子序列。 + +加入s[j]的回文子序列长度为dp[i + 1][j]。 + +加入s[i]的回文子序列长度为dp[i][j - 1]。 + +那么dp[i][j]一定是取最大的,即:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); + +![516.最长回文子序列1](https://file1.kamacoder.com/i/algo/20210127151420476.jpg) + +代码如下: + +```CPP +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]); +} +``` + +3. dp数组如何初始化 + +首先要考虑当i 和j 相同的情况,从递推公式:dp[i][j] = dp[i + 1][j - 1] + 2; 可以看出 递推公式是计算不到 i 和j相同时候的情况。 + +所以需要手动初始化一下,当i与j相同,那么dp[i][j]一定是等于1的,即:一个字符的回文子序列长度就是1。 + +其他情况dp[i][j]初始为0就行,这样递推公式:dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]); 中dp[i][j]才不会被初始值覆盖。 + +```CPP +vector> dp(s.size(), vector(s.size(), 0)); +for (int i = 0; i < s.size(); i++) dp[i][i] = 1; +``` + +4. 确定遍历顺序 + +从递归公式中,可以看出,dp[i][j] 依赖于 dp[i + 1][j - 1] ,dp[i + 1][j] 和 dp[i][j - 1],如图: + +![](https://file1.kamacoder.com/i/algo/20230102172155.png) + +**所以遍历i的时候一定要从下到上遍历,这样才能保证下一行的数据是经过计算的**。 + +j的话,可以正常从左向右遍历。 + +代码如下: + +```CPP +for (int i = s.size() - 1; i >= 0; i--) { + for (int j = i + 1; j < s.size(); 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]); + } + } +} +``` + +5. 举例推导dp数组 + +输入s:"cbbd" 为例,dp数组状态如图: + +![516.最长回文子序列3](https://file1.kamacoder.com/i/algo/20210127151521432.jpg) + +红色框即:dp[0][s.size() - 1]; 为最终结果。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int longestPalindromeSubseq(string s) { + vector> dp(s.size(), vector(s.size(), 0)); + for (int i = 0; i < s.size(); i++) dp[i][i] = 1; + for (int i = s.size() - 1; i >= 0; i--) { + for (int j = i + 1; j < s.size(); 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]); + } + } + } + return dp[0][s.size() - 1]; + } +}; +``` +* 时间复杂度: O(n^2) +* 空间复杂度: O(n^2) + + + + +## 其他语言版本 + +### Java: + +```java +public class Solution { + public int longestPalindromeSubseq(String s) { + int len = s.length(); + int[][] dp = new int[len + 1][len + 1]; + for (int i = len - 1; i >= 0; i--) { // 从后往前遍历 保证情况不漏 + dp[i][i] = 1; // 初始化 + for (int j = i + 1; j < len; 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], Math.max(dp[i][j], dp[i][j - 1])); + } + } + } + return dp[0][len - 1]; + } +} +``` + +### Python: + +```python +class Solution: + def longestPalindromeSubseq(self, s: str) -> int: + dp = [[0] * len(s) for _ in range(len(s))] + for i in range(len(s)): + dp[i][i] = 1 + for i in range(len(s)-1, -1, -1): + for j in range(i+1, len(s)): + 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]) + return dp[0][-1] +``` + +### Go: + +```Go +func longestPalindromeSubseq(s string) int { + size := len(s) + max := func(a, b int) int { + if a > b { + return a + } + return b + } + dp := make([][]int, size) + for i := 0; i < size; i++ { + dp[i] = make([]int, size) + dp[i][i] = 1 + } + for i := size - 1; i >= 0; i-- { + for j := i + 1; j < size; j++ { + if s[i] == s[j] { + dp[i][j] = dp[i+1][j-1] + 2 + } else { + dp[i][j] = max(dp[i][j-1], dp[i+1][j]) + } + } + } + return dp[0][size-1] +} +``` + +### JavaScript: + +```javascript +const longestPalindromeSubseq = (s) => { + const strLen = s.length; + let dp = Array.from(Array(strLen), () => Array(strLen).fill(0)); + + for(let i = 0; i < strLen; i++) { + dp[i][i] = 1; + } + + for(let i = strLen - 1; i >= 0; i--) { + for(let j = i + 1; j < strLen; j++) { + if(s[i] === s[j]) { + dp[i][j] = dp[i+1][j-1] + 2; + } else { + dp[i][j] = Math.max(dp[i+1][j], dp[i][j-1]); + } + } + } + + return dp[0][strLen - 1]; +}; +``` + +### TypeScript: + +```typescript +function longestPalindromeSubseq(s: string): number { + /** + dp[i][j]:[i,j]区间内,最长回文子序列的长度 + */ + const length: number = s.length; + const dp: number[][] = new Array(length).fill(0) + .map(_ => new Array(length).fill(0)); + for (let i = 0; i < length; i++) { + dp[i][i] = 1; + } + // 自下而上,自左往右遍历 + for (let i = length - 1; i >= 0; i--) { + for (let j = i + 1; j < length; j++) { + if (s[i] === s[j]) { + dp[i][j] = dp[i + 1][j - 1] + 2; + } else { + dp[i][j] = Math.max(dp[i][j - 1], dp[i + 1][j]); + } + } + } + return dp[0][length - 1]; +}; +``` + +Rust: + +```rust +impl Solution { + pub fn longest_palindrome_subseq(s: String) -> i32 { + let mut dp = vec![vec![0; s.len()]; s.len()]; + for i in (0..s.len()).rev() { + dp[i][i] = 1; + for j in i + 1..s.len() { + if s[i..=i] == s[j..=j] { + dp[i][j] = dp[i + 1][j - 1] + 2; + continue; + } + dp[i][j] = dp[i + 1][j].max(dp[i][j - 1]); + } + } + dp[0][s.len() - 1] + } +} +``` + + + diff --git "a/problems/0518.\351\233\266\351\222\261\345\205\221\346\215\242II.md" "b/problems/0518.\351\233\266\351\222\261\345\205\221\346\215\242II.md" new file mode 100755 index 0000000000..7e4bbb9a81 --- /dev/null +++ "b/problems/0518.\351\233\266\351\222\261\345\205\221\346\215\242II.md" @@ -0,0 +1,589 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 518.零钱兑换II + +[力扣题目链接](https://leetcode.cn/problems/coin-change-ii/) + +给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。  + +示例 1: + +* 输入: amount = 5, coins = [1, 2, 5] +* 输出: 4 + +解释: 有四种方式可以凑成总金额: + +* 5=5 +* 5=2+2+1 +* 5=2+1+1+1 +* 5=1+1+1+1+1 + +示例 2: + +* 输入: amount = 3, coins = [2] +* 输出: 0 +* 解释: 只用面额2的硬币不能凑成总金额3。 + +示例 3: +* 输入: amount = 10, coins = [10] +* 输出: 1 + +注意,你可以假设: + +* 0 <= amount (总金额) <= 5000 +* 1 <= coin (硬币面额) <= 5000 +* 硬币种类不超过 500 种 +* 结果符合 32 位符号整数 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[装满背包有多少种方法?组合与排列有讲究!| LeetCode:518.零钱兑换II](https://www.bilibili.com/video/BV1KM411k75j/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 二维dp讲解 + +如果大家认真做完:[分割等和子集](https://www.programmercarl.com/0416.%E5%88%86%E5%89%B2%E7%AD%89%E5%92%8C%E5%AD%90%E9%9B%86.html) , [最后一块石头的重量II](https://www.programmercarl.com/1049.%E6%9C%80%E5%90%8E%E4%B8%80%E5%9D%97%E7%9F%B3%E5%A4%B4%E7%9A%84%E9%87%8D%E9%87%8FII.html) 和 [目标和](https://www.programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html) + +应该会知道类似这种题目:给出一个总数,一些物品,问能否凑成这个总数。 + +这是典型的背包问题! + +本题求的是装满这个背包的物品组合数是多少。 + +因为每一种面额的硬币有无限个,所以这是完全背包。 + +对完全背包还不了解的同学,可以看这篇:[完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) + +但本题和纯完全背包不一样,**纯完全背包是凑成背包最大价值是多少,而本题是要求凑成总金额的物品组合个数!** + +注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢? + +例如示例一: + +5 = 2 + 2 + 1 + +5 = 2 + 1 + 2 + +这是一种组合,都是 2 2 1。 + +如果问的是排列数,那么上面就是两种排列了。 + +**组合不强调元素之间的顺序,排列强调元素之间的顺序**。 其实这一点我们在讲解回溯算法专题的时候就讲过。 + +那我为什么要介绍这些呢,因为这和下文讲解遍历顺序息息相关! + +本题其实与我们讲过 [494. 目标和](https://programmercarl.com/0494.目标和.html) 十分类似。 + +[494. 目标和](https://programmercarl.com/0494.目标和.html) 求的是装满背包有多少种方法,而本题是求装满背包有多少种组合。 + +这有啥区别? + +**求装满背包有几种方法其实就是求组合数**。 不过 [494. 目标和](https://programmercarl.com/0494.目标和.html) 是 01背包,即每一类物品只有一个。 + +以下动规五部曲: + +### 1、确定dp数组以及下标的含义 + +定义二维dp数值 dp[i][j]:使用 下标为[0, i]的coins[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种组合方法。 + +很多录友也会疑惑,凭什么上来就定义 dp数组,思考过程是什么样的, 这个思考过程我在 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 “确定dp数组以及下标的含义” 有详细讲解。 + +(**强烈建议按照代码随想录的顺序学习,否则可能看不懂我的讲解**) + + +### 2、确定递推公式 + +> **注意**: 这里的公式推导,与之前讲解过的 [494. 目标和](https://programmercarl.com/0494.目标和.html) 、[完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 有极大重复,所以我不在重复讲解原理,而是只讲解区别。 + +我们再回顾一下,[01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html),中二维DP数组的递推公式为: + +`dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i])` + +在 [完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 详细讲解了完全背包二维DP数组的递推公式为: + +`dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])` + + +看去完全背包 和 01背包的差别在哪里? + +在于01背包是 `dp[i - 1][j - weight[i]] + value[i]` ,完全背包是 `dp[i][j - weight[i]] + value[i])` + +主要原因就是 完全背包单类物品有无限个。 + +具体原因我在 [完全背包理论基础(二维)](https://programmercarl.com/背包问题理论基础完全背包.html) 的 「确定递推公式」有详细讲解,如果大家忘了,再回顾一下。 + +我上面有说过,本题和 [494. 目标和](https://programmercarl.com/0494.目标和.html) 是一样的,唯一区别就是 [494. 目标和](https://programmercarl.com/0494.目标和.html) 是 01背包,本题是完全背包。 + + +在[494. 目标和](https://programmercarl.com/0494.目标和.html)中详解讲解了装满背包有几种方法,二维DP数组的递推公式: +`dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]]` + +所以本题递推公式:`dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]` ,区别依然是 ` dp[i - 1][j - nums[i]]` 和 `dp[i][j - nums[i]]` + +这个 ‘所以’ 我省略了很多推导的内容,因为这些内容在 [494. 目标和](https://programmercarl.com/0494.目标和.html) 和 [完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 都详细讲过。 + +这里不再重复讲解。 + +大家主要疑惑点 + +1、 `dp[i][j] = dp[i - 1][j] + dp[i][j - nums[i]]` 这个递归公式框架怎么来的,在 [494. 目标和](https://programmercarl.com/0494.目标和.html) 有详细讲解。 + +2、为什么是 ` dp[i][j - nums[i]]` 而不是 ` dp[i - 1][j - nums[i]]` ,在[完全背包理论基础(二维)](https://programmercarl.com/背包问题理论基础完全背包.html) 有详细讲解 + + +### 3. dp数组如何初始化 + +那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础,如图红色部分: + +![](https://file1.kamacoder.com/i/algo/20240827103507.png) + + +这里首先要关注的就是 dp[0][0] 应该是多少? + +背包空间为0,装满「物品0」 的组合数有多少呢? + +应该是 0 个, 但如果 「物品0」 的 数值就是0呢? 岂不是可以有无限个0 组合 和为0! + +题目描述中说了`1 <= coins.length <= 300` ,所以不用考虑 物品数值为0的情况。 + +那么最上行dp[0][j] 如何初始化呢? + +dp[0][j]的含义:用「物品0」(即coins[0]) 装满 背包容量为j的背包,有几种组合方法。 (如果看不懂dp数组的含义,建议先学习[494. 目标和](https://programmercarl.com/0494.目标和.html)) + +如果 j 可以整除 物品0,那么装满背包就有1种组合方法。 + +初始化代码: + +```CPP +for (int j = 0; j <= bagSize; j++) { + if (j % coins[0] == 0) dp[0][j] = 1; +} +``` + +最左列如何初始化呢? + +dp[i][0] 的含义:用物品i(即coins[i]) 装满容量为0的背包 有几种组合方法。 + +都有一种方法,即不装。 + +所以 dp[i][0] 都初始化为1 + +### 4. 确定遍历顺序 + +二维DP数组的完全背包的两个for循环先后顺序是无所谓的。 + +先遍历背包,还是先遍历物品都是可以的。 + +原理和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 「遍历顺序」是一样的,都是因为 两个for循环的先后顺序不影响 递推公式 所需要的数值。 + +具体分析过程看 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 「遍历顺序」 + +### 5. 打印DP数组 + +以amount为5,coins为:[2,3,5] 为例: + +dp数组应该是这样的: + +``` +1 0 1 0 1 0 +1 0 1 1 1 1 +1 0 1 1 1 2 +``` + +### 代码实现: + +```CPP +class Solution { +public: + int change(int amount, vector& coins) { + int bagSize = amount; + + vector> dp(coins.size(), vector(bagSize + 1, 0)); + + // 初始化最上行 + for (int j = 0; j <= bagSize; j++) { + if (j % coins[0] == 0) dp[0][j] = 1; + } + // 初始化最左列 + for (int i = 0; i < coins.size(); i++) { + dp[i][0] = 1; + } + // 以下遍历顺序行列可以颠倒 + for (int i = 1; i < coins.size(); i++) { // 行,遍历物品 + for (int j = 0; j <= bagSize; j++) { // 列,遍历背包 + if (coins[i] > j) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]; + } + } + return dp[coins.size() - 1][bagSize]; + } +}; +``` + +## 一维dp讲解 + +### 1、确定dp数组以及下标的含义 + +dp[j]:凑成总金额j的货币组合数为dp[j] + +### 2、确定递推公式 + +本题 二维dp 递推公式: `dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i]]` + +压缩成一维:`dp[j] += dp[j - coins[i]]` + +这个递推公式大家应该不陌生了,我在讲解01背包题目的时候在这篇[494. 目标和](https://programmercarl.com/0494.目标和.html)中就讲解了,求装满背包有几种方法,公式都是:`dp[j] += dp[j - nums[i]]` + +### 3. dp数组如何初始化 + +装满背包容量为0 的方法是1,即不放任何物品,`dp[0] = 1` + +### 4. 确定遍历顺序 + + +本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢? + +我在[完全背包(一维DP)](./背包问题完全背包一维.md)中讲解了完全背包的两个for循环的先后顺序都是可以的。 + +**但本题就不行了!** + +因为纯完全背包求得装满背包的最大价值是多少,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行! + +而本题要求凑成总和的组合数,元素之间明确要求没有顺序。 + +所以纯完全背包是能凑成总和就行,不用管怎么凑的。 + +本题是求凑出来的方案个数,且每个方案个数是组合数。 + +那么本题,两个for循环的先后顺序可就有说法了。 + +我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。 + +代码如下: + +```CPP +for (int i = 0; i < coins.size(); i++) { // 遍历物品 + for (int j = coins[i]; j <= amount; j++) { // 遍历背包容量 + dp[j] += dp[j - coins[i]]; + } +} +``` + +假设:coins[0] = 1,coins[1] = 5。 + +那么就是先把1加入计算,然后再把5加入计算,得到的方法数量只有{1, 5}这种情况。而不会出现{5, 1}的情况。 + +**所以这种遍历顺序中dp[j]里计算的是组合数!** + +如果把两个for交换顺序,代码如下: + +```CPP +for (int j = 0; j <= amount; j++) { // 遍历背包容量 + for (int i = 0; i < coins.size(); i++) { // 遍历物品 + if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]]; + } +} +``` + +背包容量的每一个值,都是经过 1 和 5 的计算,包含了{1, 5} 和 {5, 1}两种情况。 + +**此时dp[j]里算出来的就是排列数!** + +可能这里很多同学还不是很理解,**建议动手把这两种方案的dp数组数值变化打印出来,对比看一看!(实践出真知)** + +### 5. 举例推导dp数组 + +输入: amount = 5, coins = [1, 2, 5] ,dp状态图如下: + +![518.零钱兑换II](https://file1.kamacoder.com/i/algo/20210120181331461.jpg) + +最后红色框dp[amount]为最终结果。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int change(int amount, vector& coins) { + vector dp(amount + 1, 0); // 防止相加数据超int + dp[0] = 1; // 只有一种方式达到0 + for (int i = 0; i < coins.size(); i++) { // 遍历物品 + for (int j = coins[i]; j <= amount; j++) { // 遍历背包 + dp[j] += dp[j - coins[i]]; + } + } + return dp[amount]; // 返回组合数 + } +}; +``` + +C++测试用例有两个数相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。 + +* 时间复杂度: O(mn),其中 m 是amount,n 是 coins 的长度 +* 空间复杂度: O(m) + +为了防止相加的数据 超int 也可以这么写: + +```CPP +class Solution { +public: + int change(int amount, vector& coins) { + vector dp(amount + 1, 0); + dp[0] = 1; // 只有一种方式达到0 + for (int i = 0; i < coins.size(); i++) { // 遍历物品 + for (int j = coins[i]; j <= amount; j++) { // 遍历背包 + if (dp[j] < INT_MAX - dp[j - coins[i]]) { //防止相加数据超int + dp[j] += dp[j - coins[i]]; + } + } + } + return dp[amount]; // 返回组合数 + } +}; +``` + + +## 总结 + +本题我们从 二维 分析到 一维。 + +大家在刚开始学习的时候,从二维开始学习 容易理解。 + +之后,推荐大家直接掌握一维的写法,熟练后更容易写出来。 + +本题中,二维dp主要是就要 想清楚和我们之前讲解的 [01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html)、[494. 目标和](https://programmercarl.com/0494.目标和.html)、 [完全背包理论基础](https://programmercarl.com/背包问题理论基础完全背包.html) 联系与区别。 + +这也是代码随想录安排刷题顺序的精髓所在。 + +本题的一维dp中,难点在于理解便利顺序。 + +在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +可能说到排列数录友们已经有点懵了,后面我还会安排求排列数的题目,到时候在对比一下,大家就会发现神奇所在! + + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public int change(int amount, int[] coins) { + //递推表达式 + int[] dp = new int[amount + 1]; + //初始化dp数组,表示金额为0时只有一种情况,也就是什么都不装 + dp[0] = 1; + for (int i = 0; i < coins.length; i++) { + for (int j = coins[i]; j <= amount; j++) { + dp[j] += dp[j - coins[i]]; + } + } + return dp[amount]; + } +} +``` +```Java +// 二维dp数组版本,方便理解 +class Solution { + public int change(int amount, int[] coins) { + int[][] dp = new int[coins.length][amount+1]; + + // 初始化边界值 + for(int i = 0; i < coins.length; i++){ + // 第一列的初始值为1 + dp[i][0] = 1; + } + for(int j = coins[0]; j <= amount; j++){ + // 初始化第一行 + dp[0][j] += dp[0][j-coins[0]]; + } + + for(int i = 1; i < coins.length; i++){ + for(int j = 1; j <= amount; j++){ + if(j < coins[i]) dp[i][j] = dp[i-1][j]; + else dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j]; + } + } + + return dp[coins.length-1][amount]; + } +} +``` + +### Python: + + +```python +class Solution: + def change(self, amount: int, coins: List[int]) -> int: + dp = [0]*(amount + 1) + dp[0] = 1 + # 遍历物品 + for i in range(len(coins)): + # 遍历背包 + for j in range(coins[i], amount + 1): + dp[j] += dp[j - coins[i]] + return dp[amount] +``` + + + +### Go: + +一维dp +```go +func change(amount int, coins []int) int { + // 定义dp数组 + dp := make([]int, amount+1) + // 初始化,0大小的背包, 当然是不装任何东西了, 就是1种方法 + dp[0] = 1 + // 遍历顺序 + // 遍历物品 + for i := 0 ;i < len(coins);i++ { + // 遍历背包 + for j:= coins[i] ; j <= amount ;j++ { + // 推导公式 + dp[j] += dp[j-coins[i]] + } + } + return dp[amount] +} +``` +二维dp +```go +func change(amount int, coins []int) int { + dp := make([][]int, len(coins)) + for i := range dp { + dp[i] = make([]int, amount + 1) + dp[i][0] = 1 + } + for j := coins[0]; j <= amount; j++ { + dp[0][j] += dp[0][j-coins[0]] + } + for i := 1; i < len(coins); i++ { + for j := 1; j <= amount; j++ { + if j < coins[i] { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j] + } + } + } + return dp[len(coins)-1][amount] +} +``` + +### Rust: + +```rust +impl Solution { + pub fn change(amount: i32, coins: Vec) -> i32 { + let amount = amount as usize; + let mut dp = vec![0; amount + 1]; + dp[0] = 1; + for coin in coins { + for j in coin as usize..=amount { + dp[j] += dp[j - coin as usize]; + } + } + dp[amount] + } +} +``` + +### JavaScript: + +```javascript +const change = (amount, coins) => { + let dp = Array(amount + 1).fill(0); + dp[0] = 1; + + for(let i =0; i < coins.length; i++) { + for(let j = coins[i]; j <= amount; j++) { + dp[j] += dp[j - coins[i]]; + } + } + + return dp[amount]; +} +``` + +### TypeScript: + +```typescript +function change(amount: number, coins: number[]): number { + const dp: number[] = new Array(amount + 1).fill(0); + dp[0] = 1; + for (let i = 0, length = coins.length; i < length; i++) { + for (let j = coins[i]; j <= amount; j++) { + dp[j] += dp[j - coins[i]]; + } + } + return dp[amount]; +}; +``` + +### Scala: + +```scala +object Solution { + def change(amount: Int, coins: Array[Int]): Int = { + var dp = new Array[Int](amount + 1) + dp(0) = 1 + for (i <- 0 until coins.length) { + for (j <- coins(i) to amount) { + dp(j) += dp(j - coins(i)) + } + } + dp(amount) + } +} +``` +### C + +```c +int change(int amount, int* coins, int coinsSize) { + int dp[amount + 1]; + memset(dp, 0, sizeof (dp)); + dp[0] = 1; + // 遍历物品 + for(int i = 0; i < coinsSize; i++){ + // 遍历背包 + for(int j = coins[i]; j <= amount; j++){ + dp[j] += dp[j - coins[i]]; + } + } + return dp[amount]; +} +``` + + + +### C# + +```csharp +public class Solution +{ + public int Change(int amount, int[] coins) + { + int[] dp = new int[amount + 1]; + dp[0] = 1; + for (int i = 0; i < coins.Length; i++) + { + for (int j = coins[i]; j <= amount; j++) + { + if (j >= coins[i]) + dp[j] += dp[j - coins[i]]; + } + } + return dp[amount]; + } +} +``` + + diff --git "a/problems/0530.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\345\260\217\347\273\235\345\257\271\345\267\256.md" "b/problems/0530.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\345\260\217\347\273\235\345\257\271\345\267\256.md" new file mode 100755 index 0000000000..a8eca862ef --- /dev/null +++ "b/problems/0530.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\347\232\204\346\234\200\345\260\217\347\273\235\345\257\271\345\267\256.md" @@ -0,0 +1,680 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 利用二叉搜索树的特性搞起! + +# 530.二叉搜索树的最小绝对差 + +[力扣题目链接](https://leetcode.cn/problems/minimum-absolute-difference-in-bst/) + +给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。 + +示例: + +![530二叉搜索树的最小绝对差](https://file1.kamacoder.com/i/algo/20201014223400123.png) + +提示:树中至少有 2 个节点。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[二叉搜索树中,需要掌握如何双指针遍历!| LeetCode:530.二叉搜索树的最小绝对差](https://www.bilibili.com/video/BV1DD4y11779),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。 + +**注意是二叉搜索树**,二叉搜索树可是有序的。 + +遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。 + +### 递归 + +那么二叉搜索树采用中序遍历,其实就是一个有序数组。 + +**在一个有序数组上求两个数最小差值,这是不是就是一道送分题了。** + +最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了。 + +代码如下: + +```CPP +class Solution { +private: +vector vec; +void traversal(TreeNode* root) { + if (root == NULL) return; + traversal(root->left); + vec.push_back(root->val); // 将二叉搜索树转换为有序数组 + traversal(root->right); +} +public: + int getMinimumDifference(TreeNode* root) { + vec.clear(); + traversal(root); + if (vec.size() < 2) return 0; + int result = INT_MAX; + for (int i = 1; i < vec.size(); i++) { // 统计有序数组的最小差值 + result = min(result, vec[i] - vec[i-1]); + } + return result; + } +}; +``` + +以上代码是把二叉搜索树转化为有序数组了,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了。 + +需要用一个pre节点记录一下cur节点的前一个节点。 + +如图: + +![530.二叉搜索树的最小绝对差](https://file1.kamacoder.com/i/algo/20210204153247458.png) + +一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了。 + +代码如下: + +```CPP +class Solution { +private: +int result = INT_MAX; +TreeNode* pre = NULL; +void traversal(TreeNode* cur) { + if (cur == NULL) return; + traversal(cur->left); // 左 + if (pre != NULL){ // 中 + result = min(result, cur->val - pre->val); + } + pre = cur; // 记录前一个 + traversal(cur->right); // 右 +} +public: + int getMinimumDifference(TreeNode* root) { + traversal(root); + return result; + } +}; +``` + +是不是看上去也并不复杂! + +### 迭代 + +看过这两篇[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html),[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://programmercarl.com/二叉树的统一迭代法.html)文章之后,不难写出两种中序遍历的迭代法。 + +下面我给出其中的一种中序遍历的迭代法,代码如下: + +```CPP +class Solution { +public: + int getMinimumDifference(TreeNode* root) { + stack st; + TreeNode* cur = root; + TreeNode* pre = NULL; + int result = INT_MAX; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { // 指针来访问节点,访问到最底层 + st.push(cur); // 将访问的节点放进栈 + cur = cur->left; // 左 + } else { + cur = st.top(); + st.pop(); + if (pre != NULL) { // 中 + result = min(result, cur->val - pre->val); + } + pre = cur; + cur = cur->right; // 右 + } + } + return result; + } +}; +``` + +## 总结 + +**遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。** + +同时要学会在递归遍历的过程中如何记录前后两个指针,这也是一个小技巧,学会了还是很受用的。 + +后面我将继续介绍一系列利用二叉搜索树特性的题目。 + + + +## 其他语言版本 + + +### Java + +递归 +```java +class Solution { + TreeNode pre; // 记录上一个遍历的结点 + int result = Integer.MAX_VALUE; + + public int getMinimumDifference(TreeNode root) { + if (root == null) + return 0; + traversal(root); + return result; + } + + public void traversal(TreeNode root) { + if (root == null) + return; + // 左 + traversal(root.left); + // 中 + if (pre != null) { + result = Math.min(result, root.val - pre.val); + } + pre = root; + // 右 + traversal(root.right); + } +} +``` +統一迭代法-中序遍历 +```Java +class Solution { + public int getMinimumDifference(TreeNode root) { + Stack stack = new Stack<>(); + TreeNode pre = null; + int result = Integer.MAX_VALUE; + + if (root != null) + stack.add(root); + + // 中序遍历(左中右),由于栈先入后出,反序(右中左) + while (!stack.isEmpty()) { + TreeNode curr = stack.peek(); + if (curr != null) { + stack.pop(); + // 右 + if (curr.right != null) + stack.add(curr.right); + // 中(先用null标记) + stack.add(curr); + stack.add(null); + // 左 + if (curr.left != null) + stack.add(curr.left); + } else { // 中(遇到null再处理) + stack.pop(); + TreeNode temp = stack.pop(); + if (pre != null) + result = Math.min(result, temp.val - pre.val); + pre = temp; + } + } + return result; + } +} +``` + +迭代法-中序遍历 + +```java +class Solution { + TreeNode pre; + Stack stack; + public int getMinimumDifference(TreeNode root) { + if (root == null) return 0; + stack = new Stack<>(); + TreeNode cur = root; + int result = Integer.MAX_VALUE; + while (cur != null || !stack.isEmpty()) { + if (cur != null) { + stack.push(cur); // 将访问的节点放进栈 + cur = cur.left; // 左 + }else { + cur = stack.pop(); + if (pre != null) { // 中 + result = Math.min(result, cur.val - pre.val); + } + pre = cur; + cur = cur.right; // 右 + } + } + return result; + } +} +``` +### Python + +递归法(版本一)利用中序递增,结合数组 +```python +class Solution: + def __init__(self): + self.vec = [] + + def traversal(self, root): + if root is None: + return + self.traversal(root.left) + self.vec.append(root.val) # 将二叉搜索树转换为有序数组 + self.traversal(root.right) + + def getMinimumDifference(self, root): + self.vec = [] + self.traversal(root) + if len(self.vec) < 2: + return 0 + result = float('inf') + for i in range(1, len(self.vec)): + # 统计有序数组的最小差值 + result = min(result, self.vec[i] - self.vec[i - 1]) + return result + +``` + + + +递归法(版本二)利用中序递增,找到该树最小值 +```python +class Solution: + def __init__(self): + self.result = float('inf') + self.pre = None + + def traversal(self, cur): + if cur is None: + return + self.traversal(cur.left) # 左 + if self.pre is not None: # 中 + self.result = min(self.result, cur.val - self.pre.val) + self.pre = cur # 记录前一个 + self.traversal(cur.right) # 右 + + def getMinimumDifference(self, root): + self.traversal(root) + return self.result + + +``` + +迭代法 +```python +class Solution: + def getMinimumDifference(self, root): + stack = [] + cur = root + pre = None + result = float('inf') + + while cur is not None or len(stack) > 0: + if cur is not None: + stack.append(cur) # 将访问的节点放进栈 + cur = cur.left # 左 + else: + cur = stack.pop() + if pre is not None: # 中 + result = min(result, cur.val - pre.val) + pre = cur + cur = cur.right # 右 + + return result + + + +``` +### Go + +中序遍历,然后计算最小差值 +```go +// 中序遍历的同时计算最小值 +func getMinimumDifference(root *TreeNode) int { + // 保留前一个节点的指针 + var prev *TreeNode + // 定义一个比较大的值 + min := math.MaxInt64 + var travel func(node *TreeNode) + travel = func(node *TreeNode) { + if node == nil { + return + } + travel(node.Left) + if prev != nil && node.Val - prev.Val < min { + min = node.Val - prev.Val + } + prev = node + travel(node.Right) + } + travel(root) + return min +} +``` + +### JavaScript +递归 先转换为有序数组 +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @return {number} + */ +var getMinimumDifference = function (root) { + let arr = []; + const buildArr = (root) => { + if (root) { + buildArr(root.left); + arr.push(root.val); + buildArr(root.right); + } + } + buildArr(root); + let diff = arr[arr.length - 1]; + for (let i = 1; i < arr.length; ++i) { + if (diff > arr[i] - arr[i - 1]) + diff = arr[i] - arr[i - 1]; + } + return diff; +}; +``` +递归 在递归的过程中更新最小值 +```js +var getMinimumDifference = function(root) { + let res = Infinity + let preNode = null + // 中序遍历 + const inorder = (node) => { + if(!node) return + inorder(node.left) + // 更新res + if(preNode) res = Math.min(res, node.val - preNode.val) + // 记录前一个节点 + preNode = node + inorder(node.right) + } + inorder(root) + return res +} +``` + +迭代 中序遍历 +```js +var getMinimumDifference = function(root) { + let stack = [] + let cur = root + let res = Infinity + let pre = null + while(cur || stack.length) { + if(cur) { + stack.push(cur) + cur = cur.left + } else { + cur = stack.pop() + if(pre) res = Math.min(res, cur.val - pre.val) + pre = cur + cur = cur.right + } + } + return res +} +``` + +### TypeScript + +> 辅助数组解决 + +```typescript +function getMinimumDifference(root: TreeNode | null): number { + let helperArr: number[] = []; + function recur(root: TreeNode | null): void { + if (root === null) return; + recur(root.left); + helperArr.push(root.val); + recur(root.right); + } + recur(root); + let resMin: number = Infinity; + for (let i = 0, length = helperArr.length; i < length - 1; i++) { + resMin = Math.min(resMin, helperArr[i + 1] - helperArr[i]); + } + return resMin; +}; +``` + +> 递归中解决 + +```typescript +function getMinimumDifference(root: TreeNode | null): number { + let preNode: TreeNode | null= null; + let resMin: number = Infinity; + function recur(root: TreeNode | null): void { + if (root === null) return; + recur(root.left); + if (preNode !== null) { + resMin = Math.min(resMin, root.val - preNode.val); + } + preNode = root; + recur(root.right); + } + recur(root); + return resMin; +}; +``` + +> 迭代法-中序遍历 + +```typescript +function getMinimumDifference(root: TreeNode | null): number { + const helperStack: TreeNode[] = []; + let curNode: TreeNode | null = root; + let resMin: number = Infinity; + let preNode: TreeNode | null = null; + while (curNode !== null || helperStack.length > 0) { + if (curNode !== null) { + helperStack.push(curNode); + curNode = curNode.left; + } else { + curNode = helperStack.pop()!; + if (preNode !== null) { + resMin = Math.min(resMin, curNode.val - preNode.val); + } + preNode = curNode; + curNode = curNode.right; + } + } + return resMin; +}; +``` + +### Scala + +构建二叉树的有序数组: + +```scala +object Solution { + import scala.collection.mutable + def getMinimumDifference(root: TreeNode): Int = { + val arr = mutable.ArrayBuffer[Int]() + def traversal(node: TreeNode): Unit = { + if (node == null) return + traversal(node.left) + arr.append(node.value) + traversal(node.right) + } + traversal(root) + // 在有序数组上求最小差值 + var result = Int.MaxValue + for (i <- 1 until arr.size) { + result = math.min(result, arr(i) - arr(i - 1)) + } + result // 返回最小差值 + } +} +``` + +递归记录前一个节点: + +```scala +object Solution { + def getMinimumDifference(root: TreeNode): Int = { + var result = Int.MaxValue // 初始化为最大值 + var pre: TreeNode = null // 记录前一个节点 + + def traversal(cur: TreeNode): Unit = { + if (cur == null) return + traversal(cur.left) + if (pre != null) { + // 对比result与节点之间的差值 + result = math.min(result, cur.value - pre.value) + } + pre = cur + traversal(cur.right) + } + + traversal(root) + result // return关键字可以省略 + } +} +``` + +迭代解决: + +```scala +object Solution { + import scala.collection.mutable + def getMinimumDifference(root: TreeNode): Int = { + var result = Int.MaxValue // 初始化为最大值 + var pre: TreeNode = null // 记录前一个节点 + var cur = root + var stack = mutable.Stack[TreeNode]() + while (cur != null || !stack.isEmpty) { + if (cur != null) { + stack.push(cur) + cur = cur.left + } else { + cur = stack.pop() + if (pre != null) { + result = math.min(result, cur.value - pre.value) + } + pre = cur + cur = cur.right + } + } + result // return关键字可以省略 + } +} +``` + +### Rust + +构建二叉树的有序数组: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn get_minimum_difference(root: Option>>) -> i32 { + let mut vec = vec![]; + Self::traversal(root, &mut vec); + let mut min = i32::MAX; + for i in 1..vec.len() { + min = min.min(vec[i] - vec[i - 1]) + } + min + } + pub fn traversal(root: Option>>, v: &mut Vec) { + if root.is_none() { + return; + } + let node = root.as_ref().unwrap().borrow(); + Self::traversal(node.left.clone(), v); + v.push(node.val); + Self::traversal(node.right.clone(), v); + } +} +``` + +递归中解决 + +```rust +impl Solution { + pub fn get_minimum_difference(root: Option>>) -> i32 { + let mut pre = None; + let mut min = i32::MAX; + Self::inorder(root, &mut pre, &mut min); + min + } + pub fn inorder(root: Option>>, pre: &mut Option, min: &mut i32) { + if root.is_none() { + return; + } + let node = root.as_ref().unwrap().borrow(); + Self::inorder(node.left.clone(), pre, min); + if let Some(pre) = pre { + *min = (node.val - *pre).min(*min); + } + *pre = Some(node.val); + + Self::inorder(node.right.clone(), pre, min); + } +} +``` + +迭代 + +```rust +impl Solution { + pub fn get_minimum_difference(mut root: Option>>) -> i32 { + if root.is_none() { + return 0; + } + let mut stack = vec![]; + let mut pre = -1; + let mut res = i32::MAX; + while root.is_some() || !stack.is_empty() { + while let Some(node) = root { + root = node.borrow().left.clone(); + stack.push(node); + } + + let node = stack.pop().unwrap(); + + if pre >= 0 { + res = res.min(node.borrow().val - pre); + } + + pre = node.borrow().val; + + root = node.borrow().right.clone(); + } + res + } +} +``` +### C# +```csharp +// 递归 +public class Solution +{ + public List res = new List(); + public int GetMinimumDifference(TreeNode root) + { + Traversal(root); + return res.SelectMany((x, i) => res.Skip(i + 1).Select(y => Math.Abs(x - y))).Min(); + + } + public void Traversal(TreeNode root) + { + if (root == null) return; + Traversal(root.left); + res.Add(root.val); + Traversal(root.right); + } +} +``` + + + diff --git "a/problems/0538.\346\212\212\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\350\275\254\346\215\242\344\270\272\347\264\257\345\212\240\346\240\221.md" "b/problems/0538.\346\212\212\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\350\275\254\346\215\242\344\270\272\347\264\257\345\212\240\346\240\221.md" new file mode 100755 index 0000000000..c4bfeae4b8 --- /dev/null +++ "b/problems/0538.\346\212\212\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\350\275\254\346\215\242\344\270\272\347\264\257\345\212\240\346\240\221.md" @@ -0,0 +1,549 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 538.把二叉搜索树转换为累加树 + +[力扣题目链接](https://leetcode.cn/problems/convert-bst-to-greater-tree/) + +给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。 + +提醒一下,二叉搜索树满足下列约束条件: + +节点的左子树仅包含键 小于 节点键的节点。 +节点的右子树仅包含键 大于 节点键的节点。 +左右子树也必须是二叉搜索树。 + +示例 1: + + +![538.把二叉搜索树转换为累加树](https://file1.kamacoder.com/i/algo/20201023160751832.png) + +* 输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8] +* 输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8] + +示例 2: +* 输入:root = [0,null,1] +* 输出:[1,null,1] + +示例 3: +* 输入:root = [1,0,2] +* 输出:[3,3,2] + +示例 4: +* 输入:root = [3,2,4,1] +* 输出:[7,9,4,10] + +提示: + +* 树中的节点数介于 0 和 104 之间。 +* 每个节点的值介于 -104 和 104 之间。 +* 树中的所有值 互不相同 。 +* 给定的树为二叉搜索树。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[普大喜奔!二叉树章节已全部更完啦!| LeetCode:538.把二叉搜索树转换为累加树](https://www.bilibili.com/video/BV1d44y1f7wP?share_source=copy_web),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +一看到累加树,相信很多小伙伴都会疑惑:如何累加?遇到一个节点,然后再遍历其他节点累加?怎么一想这么麻烦呢。 + +然后再发现这是一棵二叉搜索树,二叉搜索树啊,这是有序的啊。 + +那么有序的元素如何求累加呢? + +**其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。** + +为什么变成数组就是感觉简单了呢? + +因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。 + +那么知道如何遍历这个二叉树,也就迎刃而解了,**从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了**。 + +### 递归 + +遍历顺序如图所示: + + +![538.把二叉搜索树转换为累加树](https://file1.kamacoder.com/i/algo/20210204153440666.png) + +本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。 + +pre指针的使用技巧,我们在[二叉树:搜索树的最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html)和[二叉树:我的众数是多少?](https://programmercarl.com/0501.二叉搜索树中的众数.html)都提到了,这是常用的操作手段。 + +* 递归函数参数以及返回值 + +这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。 + +同时需要定义一个全局变量pre,用来保存cur节点的前一个节点的数值,定义为int型就可以了。 + +代码如下: + +``` +int pre = 0; // 记录前一个节点的数值 +void traversal(TreeNode* cur) +``` + +* 确定终止条件 + +遇空就终止。 + +``` +if (cur == NULL) return; +``` + +* 确定单层递归的逻辑 + +注意**要右中左来遍历二叉树**, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。 + +代码如下: + +``` +traversal(cur->right); // 右 +cur->val += pre; // 中 +pre = cur->val; +traversal(cur->left); // 左 +``` + +递归法整体代码如下: + +```CPP +class Solution { +private: + int pre = 0; // 记录前一个节点的数值 + void traversal(TreeNode* cur) { // 右中左遍历 + if (cur == NULL) return; + traversal(cur->right); + cur->val += pre; + pre = cur->val; + traversal(cur->left); + } +public: + TreeNode* convertBST(TreeNode* root) { + pre = 0; + traversal(root); + return root; + } +}; +``` + +### 迭代法 + +迭代法其实就是中序模板题了,在[二叉树:前中后序迭代法](https://programmercarl.com/二叉树的迭代遍历.html)和[二叉树:前中后序统一方式迭代法](https://programmercarl.com/二叉树的统一迭代法.html)可以选一种自己习惯的写法。 + +这里我给出其中的一种,代码如下: + +```CPP +class Solution { +private: + int pre; // 记录前一个节点的数值 + void traversal(TreeNode* root) { + stack st; + TreeNode* cur = root; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { + st.push(cur); + cur = cur->right; // 右 + } else { + cur = st.top(); // 中 + st.pop(); + cur->val += pre; + pre = cur->val; + cur = cur->left; // 左 + } + } + } +public: + TreeNode* convertBST(TreeNode* root) { + pre = 0; + traversal(root); + return root; + } +}; +``` + +## 总结 + +经历了前面各种二叉树增删改查的洗礼之后,这道题目应该比较简单了。 + +**好了,二叉树已经接近尾声了,接下来就是要对二叉树来一个大总结了**。 + + +## 其他语言版本 + + +### Java +**递归** + +```Java +class Solution { + int sum; + public TreeNode convertBST(TreeNode root) { + sum = 0; + convertBST1(root); + return root; + } + + // 按右中左顺序遍历,累加即可 + public void convertBST1(TreeNode root) { + if (root == null) { + return; + } + convertBST1(root.right); + sum += root.val; + root.val = sum; + convertBST1(root.left); + } +} +``` +**迭代** + +```Java +class Solution { + //DFS iteraion統一迭代法 + public TreeNode convertBST(TreeNode root) { + int pre = 0; + Stack stack = new Stack<>(); + if(root == null) //edge case check + return null; + + stack.add(root); + + while(!stack.isEmpty()){ + TreeNode curr = stack.peek(); + //curr != null的狀況,只負責存node到stack中 + if(curr != null){ + stack.pop(); + if(curr.left != null) //左 + stack.add(curr.left); + stack.add(curr); //中 + stack.add(null); + if(curr.right != null) //右 + stack.add(curr.right); + }else{ + //curr == null的狀況,只負責做單層邏輯 + stack.pop(); + TreeNode temp = stack.pop(); + temp.val += pre; + pre = temp.val; + } + } + return root; + } +} +``` + +### Python +递归法(版本一) +```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 convertBST(self, root: TreeNode) -> TreeNode: + self.pre = 0 # 记录前一个节点的数值 + self.traversal(root) + return root + def traversal(self, cur): + if cur is None: + return + self.traversal(cur.right) + cur.val += self.pre + self.pre = cur.val + self.traversal(cur.left) + + +``` +递归法(版本二) +```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 __init__(self): + self.count = 0 + + def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + if root == None: + return + ''' + 倒序累加替换: + ''' + # 右 + self.convertBST(root.right) + + # 中 + # 中节点:用当前root的值加上pre的值 + self.count += root.val + + root.val = self.count + + # 左 + self.convertBST(root.left) + + return root + +``` +迭代法(版本一) +```python +class Solution: + def __init__(self): + self.pre = 0 # 记录前一个节点的数值 + + def traversal(self, root): + stack = [] + cur = root + while cur or stack: + if cur: + stack.append(cur) + cur = cur.right # 右 + else: + cur = stack.pop() # 中 + cur.val += self.pre + self.pre = cur.val + cur = cur.left # 左 + + def convertBST(self, root): + self.pre = 0 + self.traversal(root) + return root + +``` +迭代法(版本二) +```python +class Solution: + def convertBST(self, root: Optional[TreeNode]) -> Optional[TreeNode]: + if not root: return root + stack = [] + result = [] + cur = root + pre = 0 + while cur or stack: + if cur: + stack.append(cur) + cur = cur.right + else: + cur = stack.pop() + cur.val+= pre + pre = cur.val + cur =cur.left + return root +``` + +### Go + +弄一个sum暂存其和值 +```go +var pre int +func convertBST(root *TreeNode) *TreeNode { + pre = 0 + traversal(root) + return root +} + +func traversal(cur *TreeNode) { + if cur == nil { + return + } + traversal(cur.Right) + cur.Val += pre + pre = cur.Val + traversal(cur.Left) +} +``` + +### JavaScript + +递归 +```javascript +var convertBST = function(root) { + let pre = 0; + const ReverseInOrder = (cur) => { + if(cur) { + ReverseInOrder(cur.right); + cur.val += pre; + pre = cur.val; + ReverseInOrder(cur.left); + } + } + ReverseInOrder(root); + return root; +}; +``` + +迭代 +```javascript +var convertBST = function (root) { + let pre = 0; + let cur = root; + let stack = []; + while (cur !== null || stack.length !== 0) { + while (cur !== null) { + stack.push(cur); + cur = cur.right; + } + cur = stack.pop(); + cur.val += pre; + pre = cur.val; + cur = cur.left; + } + return root; +}; +``` + +### C + +递归 +```c +int pre; +void traversal(struct TreeNode* node) { + if(!node) + return ; + traversal(node->right); + node->val = node->val + pre; + pre = node->val; + traversal(node->left); +} + +struct TreeNode* convertBST(struct TreeNode* root){ + pre = 0; + traversal(root); + return root; +} +``` + +### TypeScript + +> 递归法 + +```typescript +function convertBST(root: TreeNode | null): TreeNode | null { + let pre: number = 0; + function recur(root: TreeNode | null): void { + if (root === null) return; + recur(root.right); + root.val += pre; + pre = root.val; + recur(root.left); + } + recur(root); + return root; +}; +``` + +> 迭代法 + +```typescript +function convertBST(root: TreeNode | null): TreeNode | null { + const helperStack: TreeNode[] = []; + let curNode: TreeNode | null = root; + let pre: number = 0; + while (curNode !== null || helperStack.length > 0) { + while (curNode !== null) { + helperStack.push(curNode); + curNode = curNode.right; + } + curNode = helperStack.pop()!; + curNode.val += pre; + pre = curNode.val; + curNode = curNode.left; + } + return root; +}; +``` + +### Scala + +```scala +object Solution { + def convertBST(root: TreeNode): TreeNode = { + var sum = 0 + def convert(node: TreeNode): Unit = { + if (node == null) return + convert(node.right) + sum += node.value + node.value = sum + convert(node.left) + } + convert(root) + root + } +} +``` + +### Rust + +递归: + +```rust +impl Solution { + pub fn convert_bst(root: Option>>) -> Option>> { + let mut pre = 0; + Self::traversal(&root, &mut pre); + root + } + + pub fn traversal(cur: &Option>>, pre: &mut i32) { + if cur.is_none() { + return; + } + let mut node = cur.as_ref().unwrap().borrow_mut(); + Self::traversal(&node.right, pre); + *pre += node.val; + node.val = *pre; + Self::traversal(&node.left, pre); + } +} +``` + +迭代: + +```rust +impl Solution { + pub fn convert_bst(root: Option>>) -> Option>> { + let mut cur = root.clone(); + let mut stack = vec![]; + let mut pre = 0; + while !stack.is_empty() || cur.is_some() { + while let Some(node) = cur { + cur = node.borrow().right.clone(); + stack.push(node); + } + if let Some(node) = stack.pop() { + pre += node.borrow().val; + node.borrow_mut().val = pre; + cur = node.borrow().left.clone(); + } + } + root + } +} +``` +### C# +```csharp +// 递归 +public class Solution +{ + int pre = 0; + public TreeNode ConvertBST(TreeNode root) + { + if (root == null) return null; + ConvertBST(root.right); + root.val += pre; + pre = root.val; + ConvertBST(root.left); + return root; + } +} +``` + + + diff --git "a/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" "b/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" new file mode 100755 index 0000000000..d5ad95c112 --- /dev/null +++ "b/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" @@ -0,0 +1,517 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +> 简单的反转还不够,我要花式反转 + +# 541. 反转字符串II + +[力扣题目链接](https://leetcode.cn/problems/reverse-string-ii/) + +给定一个字符串 s 和一个整数 k,从字符串开头算起, 每计数至 2k 个字符,就反转这 2k 个字符中的前 k 个字符。 + +如果剩余字符少于 k 个,则将剩余字符全部反转。 + +如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。 + +示例: + +输入: s = "abcdefg", k = 2 +输出: "bacdfeg" + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[字符串操作进阶! | LeetCode:541. 反转字符串II](https://www.bilibili.com/video/BV1dT411j7NN),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这道题目其实也是模拟,实现题目中规定的反转规则就可以了。 + +一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 + +其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。 + +因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。 + +**所以当需要固定规律一段一段去处理字符串的时候,要想想在for循环的表达式上做做文章。** + +性能如下: + + +那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用reverse来实现反转也没毛病,毕竟不是解题关键部分。 + +使用C++库函数reverse的版本如下: + +```CPP +class Solution { +public: + string reverseStr(string s, int k) { + for (int i = 0; i < s.size(); i += (2 * k)) { + // 1. 每隔 2k 个字符的前 k 个字符进行反转 + // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 + if (i + k <= s.size()) { + reverse(s.begin() + i, s.begin() + i + k ); + } else { + // 3. 剩余字符少于 k 个,则将剩余字符全部反转。 + reverse(s.begin() + i, s.end()); + } + } + return s; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + + + +那么我们也可以实现自己的reverse函数,其实和题目[344. 反转字符串](https://programmercarl.com/0344.反转字符串.html)道理是一样的。 + +下面我实现的reverse函数区间是左闭右闭区间,代码如下: + +```CPP +class Solution { +public: + void reverse(string& s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + swap(s[i], s[j]); + } + } + string reverseStr(string s, int k) { + for (int i = 0; i < s.size(); i += (2 * k)) { + // 1. 每隔 2k 个字符的前 k 个字符进行反转 + // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 + if (i + k <= s.size()) { + reverse(s, i, i + k - 1); + continue; + } + // 3. 剩余字符少于 k 个,则将剩余字符全部反转。 + reverse(s, i, s.size() - 1); + } + return s; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1)或O(n), 取决于使用的语言中字符串是否可以修改. + + +另一种思路的解法 + +```CPP +class Solution { +public: + string reverseStr(string s, int k) { + int n = s.size(),pos = 0; + while(pos < n){ + //剩余字符串大于等于k的情况 + if(pos + k < n) reverse(s.begin() + pos, s.begin() + pos + k); + //剩余字符串不足k的情况 + else reverse(s.begin() + pos,s.end()); + pos += 2 * k; + } + return s; + } +}; +``` + +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + + +## 其他语言版本 + +### C: + +```c +char * reverseStr(char * s, int k){ + int len = strlen(s); + + for (int i = 0; i < len; i += (2 * k)) { + //判断剩余字符是否少于 k + k = i + k > len ? len - i : k; + + int left = i; + int right = i + k - 1; + while (left < right) { + char temp = s[left]; + s[left++] = s[right]; + s[right--] = temp; + } + } + + return s; +} +``` + +### Java: + +```Java +//解法一 +class Solution { + public String reverseStr(String s, int k) { + StringBuffer res = new StringBuffer(); + int length = s.length(); + int start = 0; + while (start < length) { + // 找到k处和2k处 + StringBuffer temp = new StringBuffer(); + // 与length进行判断,如果大于length了,那就将其置为length + int firstK = (start + k > length) ? length : start + k; + int secondK = (start + (2 * k) > length) ? length : start + (2 * k); + + //无论start所处位置,至少会反转一次 + temp.append(s.substring(start, firstK)); + res.append(temp.reverse()); + + // 如果firstK到secondK之间有元素,这些元素直接放入res里即可。 + if (firstK < secondK) { //此时剩余长度一定大于k。 + res.append(s.substring(firstK, secondK)); + } + start += (2 * k); + } + return res.toString(); + } +} + +//解法二(似乎更容易理解点) +//题目的意思其实概括为 每隔2k个反转前k个,尾数不够k个时候全部反转 +class Solution { + public String reverseStr(String s, int k) { + char[] ch = s.toCharArray(); + for(int i = 0; i < ch.length; i += 2 * k){ + int start = i; + //这里是判断尾数够不够k个来取决end指针的位置 + int end = Math.min(ch.length - 1, start + k - 1); + //用异或运算反转 + while(start < end){ + ch[start] ^= ch[end]; + ch[end] ^= ch[start]; + ch[start] ^= ch[end]; + start++; + end--; + } + } + return new String(ch); + } +} + + +// 解法二还可以用temp来交换数值,会的人更多些 +class Solution { + public String reverseStr(String s, int k) { + char[] ch = s.toCharArray(); + for(int i = 0;i < ch.length;i += 2 * k){ + int start = i; + // 判断尾数够不够k个来取决end指针的位置 + int end = Math.min(ch.length - 1,start + k - 1); + while(start < end){ + + char temp = ch[start]; + ch[start] = ch[end]; + ch[end] = temp; + + start++; + end--; + } + } + return new String(ch); + } +} +``` +```java +// 解法3 +class Solution { + public String reverseStr(String s, int k) { + char[] ch = s.toCharArray(); + // 1. 每隔 2k 个字符的前 k 个字符进行反转 + for (int i = 0; i< ch.length; i += 2 * k) { + // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 + if (i + k <= ch.length) { + reverse(ch, i, i + k -1); + continue; + } + // 3. 剩余字符少于 k 个,则将剩余字符全部反转 + reverse(ch, i, ch.length - 1); + } + return new String(ch); + + } + // 定义翻转函数 + public void reverse(char[] ch, int i, int j) { + for (; i < j; i++, j--) { + char temp = ch[i]; + ch[i] = ch[j]; + ch[j] = temp; + } + + } +} +``` +### Python: + +```python +class Solution: + def reverseStr(self, s: str, k: int) -> str: + """ + 1. 使用range(start, end, step)来确定需要调换的初始位置 + 2. 对于字符串s = 'abc',如果使用s[0:999] ===> 'abc'。字符串末尾如果超过最大长度,则会返回至字符串最后一个值,这个特性可以避免一些边界条件的处理。 + 3. 用切片整体替换,而不是一个个替换. + """ + def reverse_substring(text): + left, right = 0, len(text) - 1 + while left < right: + text[left], text[right] = text[right], text[left] + left += 1 + right -= 1 + return text + + res = list(s) + + for cur in range(0, len(s), 2 * k): + res[cur: cur + k] = reverse_substring(res[cur: cur + k]) + + return ''.join(res) +``` + +#### Python3 (v2): + +```python +class Solution: + def reverseStr(self, s: str, k: int) -> str: + # Two pointers. Another is inside the loop. + p = 0 + while p < len(s): + p2 = p + k + # Written in this could be more pythonic. + s = s[:p] + s[p: p2][::-1] + s[p2:] + p = p + 2 * k + return s +``` + +#### Python3 (v3): + +```python +class Solution: + def reverseStr(self, s: str, k: int) -> str: + i = 0 + chars = list(s) + + while i < len(chars): + chars[i:i + k] = chars[i:i + k][::-1] # 反转后,更改原值为反转后值 + i += k * 2 + + return ''.join(chars) +``` + +### Go: + +```go +func reverseStr(s string, k int) string { + ss := []byte(s) + length := len(s) + for i := 0; i < length; i += 2 * k { + // 1. 每隔 2k 个字符的前 k 个字符进行反转 + // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 + if i + k <= length { + reverse(ss[i:i+k]) + } else { + reverse(ss[i:length]) + } + } + return string(ss) +} + +func reverse(b []byte) { + left := 0 + right := len(b) - 1 + for left < right { + b[left], b[right] = b[right], b[left] + left++ + right-- + } +} +``` + +### JavaScript: + +```js + +/** + * @param {string} s + * @param {number} k + * @return {string} + */ +var reverseStr = function(s, k) { + const len = s.length; + let resArr = s.split(""); + for(let i = 0; i < len; i += 2 * k) { // 每隔 2k 个字符的前 k 个字符进行反转 + let l = i - 1, r = i + k > len ? len : i + k; + while(++l < --r) [resArr[l], resArr[r]] = [resArr[r], resArr[l]]; + } + return resArr.join(""); +}; + +``` + +### TypeScript: + +```typescript +function reverseStr(s: string, k: number): string { + let left: number, right: number; + let arr: string[] = s.split(''); + let temp: string; + for (let i = 0, length = arr.length; i < length; i += 2 * k) { + left = i; + right = (i + k - 1) >= length ? length - 1 : i + k - 1; + while (left < right) { + temp = arr[left]; + arr[left] = arr[right]; + arr[right] = temp; + left++; + right--; + } + } + return arr.join(''); +}; +``` + +### Swift: + +```swift +func reverseStr(_ s: String, _ k: Int) -> String { + var ch = Array(s) + + for i in stride(from: 0, to: ch.count, by: 2 * k) { + var left = i + var right = min(s.count - 1, left + k - 1) + + while left < right { + (ch[left], ch[right]) = (ch[right], ch[left]) + left += 1 + right -= 1 + } + } + return String(ch) +} +``` + +### C#: + +```csharp +public class Solution +{ + public string ReverseStr(string s, int k) + { + Span span = s.ToCharArray().AsSpan(); + for (int i = 0; i < span.Length; i += 2 * k) + { + span[i + k < span.Length ? i..(i + k) : i..].Reverse(); + } + return span.ToString(); + } +} +``` +### Scala: + +版本一: (正常解法) +```scala +object Solution { + def reverseStr(s: String, k: Int): String = { + val res = s.toCharArray // 转换为Array好处理 + for (i <- s.indices by 2 * k) { + // 如果i+k大于了res的长度,则需要全部翻转 + if (i + k > res.length) { + reverse(res, i, s.length - 1) + } else { + reverse(res, i, i + k - 1) + } + } + new String(res) + } + // 翻转字符串,从start到end + def reverse(s: Array[Char], start: Int, end: Int): Unit = { + var (left, right) = (start, end) + while (left < right) { + var tmp = s(left) + s(left) = s(right) + s(right) = tmp + left += 1 + right -= 1 + } + } +} +``` +版本二: 首先利用grouped每隔k个进行分割,再使用zipWithIndex添加每个数组的索引,紧接着利用map做变换,如果索引%2==0则说明需要翻转,否则原封不动,最后再转换为String +```scala +object Solution { + def reverseStr(s: String, k: Int): String = { + // s = "abcdefg", k = 2 + s.grouped(k) // Iterator ["ab", "cd", "ef", "g"] + .zipWithIndex // Iterator [("ab", 0), ("cd", 1), ("ef", 2), ("g", 3)] + .map { + case (subStr, index) => + if (index % 2 == 0) subStr.reverse else subStr + } + .mkString + } +} +``` + +版本三: (递归) +```scala +import scala.annotation.tailrec + +object Solution { + def reverseStr(s: String, k: Int): String = { + @tailrec // 这个函数已经优化成了尾递归 + def reverse(s: String, needToReverse: Boolean, history: String): String = { + // 截取前k个字符(判断是否翻转) + val subStr = if (needToReverse) s.take(k).reverse else s.take(k) + // 如果字符串长度小于k,返回结果 + // 否则,对于剩余字符串进行同样的操作 + if (s.length < k) history + subStr + else reverse(s.drop(k), !needToReverse, history + subStr) + } + reverse(s, true, "") + } +} +``` + +### Rust: + +```Rust +impl Solution { + pub fn reverse(s: &mut Vec, mut begin: usize, mut end: usize){ + while begin < end { + let temp = s[begin]; + s[begin] = s[end]; + s[end] = temp; + begin += 1; + end -= 1; + } + } + pub fn reverse_str(s: String, k: i32) -> String { + let len = s.len(); + let k = k as usize; + let mut s = s.chars().collect::>(); + for i in (0..len).step_by(2 * k) { + if i + k < len { + Self::reverse(&mut s, i, i + k - 1); + } + else { + Self::reverse(&mut s, i, len - 1); + } + } + s.iter().collect::() + } +} +``` + + diff --git "a/problems/0583.\344\270\244\344\270\252\345\255\227\347\254\246\344\270\262\347\232\204\345\210\240\351\231\244\346\223\215\344\275\234.md" "b/problems/0583.\344\270\244\344\270\252\345\255\227\347\254\246\344\270\262\347\232\204\345\210\240\351\231\244\346\223\215\344\275\234.md" new file mode 100755 index 0000000000..8208d9a1eb --- /dev/null +++ "b/problems/0583.\344\270\244\344\270\252\345\255\227\347\254\246\344\270\262\347\232\204\345\210\240\351\231\244\346\223\215\344\275\234.md" @@ -0,0 +1,470 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 583. 两个字符串的删除操作 + +[力扣题目链接](https://leetcode.cn/problems/delete-operation-for-two-strings/) + +给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。 + +示例: + +* 输入: "sea", "eat" +* 输出: 2 +* 解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea" + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[LeetCode:583.两个字符串的删除操](https://www.bilibili.com/video/BV1we4y157wB/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 +### 动态规划一 + +本题和[动态规划:115.不同的子序列](https://programmercarl.com/0115.不同的子序列.html)相比,其实就是两个字符串都可以删除了,情况虽说复杂一些,但整体思路是不变的。 + +这次是两个字符串可以相互删了,这种题目也知道用动态规划的思路来解,动规五部曲,分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j]:以i-1为结尾的字符串word1,和以j-1位结尾的字符串word2,想要达到相等,所需要删除元素的最少次数。 + +这里dp数组的定义有点点绕,大家要理清思路。 + +2. 确定递推公式 + +* 当word1[i - 1] 与 word2[j - 1]相同的时候 +* 当word1[i - 1] 与 word2[j - 1]不相同的时候 + +当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1]; + +当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况: + +情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1 + +情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1 + +情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2 + +那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1}); + + +因为 dp[i][j - 1] + 1 = dp[i - 1][j - 1] + 2,所以递推公式可简化为:dp[i][j] = min(dp[i - 1][j] + 1, dp[i][j - 1] + 1); + +这里可能不少录友有点迷糊,从字面上理解 就是 当 同时删word1[i - 1]和word2[j - 1],dp[i][j-1] 本来就不考虑 word2[j - 1]了,那么我在删 word1[i - 1],是不是就达到两个元素都删除的效果,即 dp[i][j-1] + 1。 + + +3. dp数组如何初始化 + +从递推公式中,可以看出来,dp[i][0] 和 dp[0][j]是一定要初始化的。 + +dp[i][0]:word2为空字符串,以i-1为结尾的字符串word1要删除多少个元素,才能和word2相同呢,很明显dp[i][0] = i。 + +dp[0][j]的话同理,所以代码如下: + +```CPP +vector> dp(word1.size() + 1, vector(word2.size() + 1)); +for (int i = 0; i <= word1.size(); i++) dp[i][0] = i; +for (int j = 0; j <= word2.size(); j++) dp[0][j] = j; +``` + +4. 确定遍历顺序 + +从递推公式 dp[i][j] = min(dp[i - 1][j - 1] + 2, min(dp[i - 1][j], dp[i][j - 1]) + 1); 和dp[i][j] = dp[i - 1][j - 1]可以看出dp[i][j]都是根据左上方、正上方、正左方推出来的。 + +所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。 + + +5. 举例推导dp数组 + +以word1:"sea",word2:"eat"为例,推导dp数组状态图如下: + +![583.两个字符串的删除操作1](https://file1.kamacoder.com/i/algo/20210714101750205.png) + + +以上分析完毕,代码如下: + +```CPP +class Solution { +public: + int minDistance(string word1, string word2) { + vector> dp(word1.size() + 1, vector(word2.size() + 1)); + for (int i = 0; i <= word1.size(); i++) dp[i][0] = i; + for (int j = 0; j <= word2.size(); j++) dp[0][j] = j; + for (int i = 1; i <= word1.size(); i++) { + for (int j = 1; j <= word2.size(); 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, dp[i][j - 1] + 1); + } + } + } + return dp[word1.size()][word2.size()]; + } +}; + +``` +* 时间复杂度: O(n * m) +* 空间复杂度: O(n * m) + + + +### 动态规划二 + +本题和[动态规划:1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html)基本相同,只要求出两个字符串的最长公共子序列长度即可,那么除了最长公共子序列之外的字符都是必须删除的,最后用两个字符串的总长度减去两个最长公共子序列的长度就是删除的最少步数。 + +代码如下: + +```CPP +class Solution { +public: + int minDistance(string word1, string word2) { + vector> dp(word1.size()+1, vector(word2.size()+1, 0)); + for (int i=1; i<=word1.size(); i++){ + for (int j=1; j<=word2.size(); j++){ + if (word1[i-1] == word2[j-1]) dp[i][j] = dp[i-1][j-1] + 1; + else dp[i][j] = max(dp[i-1][j], dp[i][j-1]); + } + } + return word1.size()+word2.size()-dp[word1.size()][word2.size()]*2; + } +}; + +``` +* 时间复杂度: O(n * m) +* 空间复杂度: O(n * m) + + + +## 其他语言版本 + +### Java: + +```java +// dp数组中存储word1和word2最长相同子序列的长度 +class Solution { + public int minDistance(String word1, String word2) { + int len1 = word1.length(); + int len2 = word2.length(); + int[][] dp = new int[len1 + 1][len2 + 1]; + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return len1 + len2 - dp[len1][len2] * 2; + } +} + +// dp数组中存储需要删除的字符个数 +class Solution { + public int minDistance(String word1, String word2) { + int[][] dp = new int[word1.length() + 1][word2.length() + 1]; + for (int i = 0; i < word1.length() + 1; i++) dp[i][0] = i; + for (int j = 0; j < word2.length() + 1; j++) dp[0][j] = j; + + for (int i = 1; i < word1.length() + 1; i++) { + for (int j = 1; j < word2.length() + 1; j++) { + if (word1.charAt(i - 1) == word2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + }else{ + dp[i][j] = Math.min(dp[i - 1][j - 1] + 2, + Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1)); + } + } + } + + return dp[word1.length()][word2.length()]; + } +} +``` +```java +//DP - longest common subsequence (用最長公共子序列反推) +class Solution { + public int minDistance(String word1, String word2) { + char[] char1 = word1.toCharArray(); + char[] char2 = word2.toCharArray(); + + int len1 = char1.length; + int len2 = char2.length; + + int dp[][] = new int [len1 + 1][len2 + 1]; + + for(int i = 1; i <= len1; i++){ + for(int j = 1; j <= len2; j++){ + if(char1[i - 1] == char2[j - 1]) + dp[i][j] = dp[i - 1][j - 1] + 1; + else + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + + return len1 + len2 - (2 * dp[len1][len2]);//和leetcode 1143只差在這一行。 + } +} +``` + +### Python: + +```python +class Solution: + def minDistance(self, word1: str, word2: str) -> int: + dp = [[0] * (len(word2)+1) for _ in range(len(word1)+1)] + for i in range(len(word1)+1): + dp[i][0] = i + for j in range(len(word2)+1): + dp[0][j] = j + for i in range(1, len(word1)+1): + for j in range(1, len(word2)+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] + 2, dp[i-1][j] + 1, dp[i][j-1] + 1) + return dp[-1][-1] +``` + +> 版本 2 + +```python +class Solution(object): + def minDistance(self, word1, word2): + m, n = len(word1), len(word2) + + # dp 求解两字符串最长公共子序列 + dp = [[0] * (n+1) for _ in range(m+1)] + 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] + 1 + else: + dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + + # 删去最长公共子序列以外元素 + return m + n - 2 * dp[-1][-1] +``` +### Go: + +动态规划一 + +```go +func minDistance(word1 string, word2 string) int { + dp := make([][]int, len(word1)+1) + for i := 0; i < len(dp); i++ { + dp[i] = make([]int, len(word2)+1) + } + //初始化 + for i := 0; i < len(dp); i++ { + dp[i][0] = i + } + for j := 0; j < len(dp[0]); j++ { + dp[0][j] = j + } + for i := 1; i < len(dp); i++ { + for j := 1; j < len(dp[i]); j++ { + if word1[i-1] == word2[j-1] { + dp[i][j] = dp[i-1][j-1] + } else { + dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), dp[i-1][j-1]+2) + } + } + } + return dp[len(dp)-1][len(dp[0])-1] +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + + +动态规划二 + +```go +func minDistance(word1 string, word2 string) int { + dp := make([][]int, len(word1) + 1) + for i := range dp { + dp[i] = make([]int, len(word2) + 1) + } + for i := 1; i <= len(word1); i++ { + for j := 1; j <= len(word2); j++ { + if word1[i-1] == word2[j-1] { + dp[i][j] = dp[i-1][j-1] + 1 + } else { + dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + } + } + } + return len(word1) + len(word2) - dp[len(word1)][len(word2)] * 2 +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + + +### JavaScript: + + +```javascript +// 方法一 +var minDistance = (word1, word2) => { + let dp = Array.from(new Array(word1.length + 1), () => + Array(word2.length + 1).fill(0) + ); + for (let i = 1; i <= word1.length; i++) { + dp[i][0] = i; + } + for (let j = 1; j <= word2.length; j++) { + dp[0][j] = j; + } + for (let i = 1; i <= word1.length; i++) { + for (let j = 1; j <= word2.length; j++) { + if (word1[i - 1] === word2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min( + dp[i - 1][j] + 1, + dp[i][j - 1] + 1, + dp[i - 1][j - 1] + 2 + ); + } + } + } + return dp[word1.length][word2.length]; +}; + +// 方法二 +var minDistance = function (word1, word2) { + let dp = new Array(word1.length + 1) + .fill(0) + .map((_) => new Array(word2.length + 1).fill(0)); + for (let i = 1; i <= word1.length; i++) + for (let j = 1; j <= word2.length; j++) + if (word1[i - 1] === word2[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; + else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + return word1.length + word2.length - dp[word1.length][word2.length] * 2; +}; +``` + +### TypeScript: + +> dp版本一: + +```typescript +function minDistance(word1: string, word2: string): number { + /** + dp[i][j]: word1前i个字符,word2前j个字符,所需最小步数 + dp[0][0]=0: word1前0个字符为'', word2前0个字符为'' + */ + const length1: number = word1.length, + length2: number = word2.length; + const dp: number[][] = new Array(length1 + 1).fill(0) + .map(_ => new Array(length2 + 1).fill(0)); + for (let i = 0; i <= length1; i++) { + dp[i][0] = i; + } + for (let i = 0; i <= length2; i++) { + dp[0][i] = i; + } + for (let i = 1; i <= length1; i++) { + for (let j = 1; j <= length2; j++) { + if (word1[i - 1] === word2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1]) + 1; + } + } + } + return dp[length1][length2]; +}; +``` + +> dp版本二: + +```typescript +function minDistance(word1: string, word2: string): number { + /** + dp[i][j]: word1前i个字符,word2前j个字符,最长公共子序列的长度 + dp[0][0]=0: word1前0个字符为'', word2前0个字符为'' + */ + const length1: number = word1.length, + length2: number = word2.length; + const dp: number[][] = new Array(length1 + 1).fill(0) + .map(_ => new Array(length2 + 1).fill(0)); + for (let i = 1; i <= length1; i++) { + for (let j = 1; j <= length2; j++) { + if (word1[i - 1] === word2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + const maxLen: number = dp[length1][length2]; + return length1 + length2 - maxLen * 2; +}; +``` + +Rust: + +```rust +impl Solution { + pub fn min_distance(word1: String, word2: String) -> i32 { + let mut dp = vec![vec![0; word2.len() + 1]; word1.len() + 1]; + for i in 0..word1.len() { + dp[i + 1][0] = i + 1; + } + for j in 0..word2.len() { + dp[0][j + 1] = j + 1; + } + for (i, char1) in word1.chars().enumerate() { + for (j, char2) in word2.chars().enumerate() { + if char1 == char2 { + dp[i + 1][j + 1] = dp[i][j]; + continue; + } + dp[i + 1][j + 1] = dp[i][j + 1].min(dp[i + 1][j]) + 1; + } + } + dp[word1.len()][word2.len()] as i32 + } +} +``` + +> 版本 2 + +```rust +impl Solution { + pub fn min_distance(word1: String, word2: String) -> i32 { + let mut dp = vec![vec![0; word2.len() + 1]; word1.len() + 1]; + for (i, char1) in word1.chars().enumerate() { + for (j, char2) in word2.chars().enumerate() { + if char1 == char2 { + dp[i + 1][j + 1] = dp[i][j] + 1; + continue; + } + dp[i + 1][j + 1] = dp[i][j + 1].max(dp[i + 1][j]); + } + } + (word1.len() + word2.len() - 2 * dp[word1.len()][word2.len()]) as i32 + } +} +``` + + diff --git "a/problems/0617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" "b/problems/0617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" new file mode 100755 index 0000000000..3ca5feb9da --- /dev/null +++ "b/problems/0617.\345\220\210\345\271\266\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,804 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 617.合并二叉树 + +[力扣题目链接](https://leetcode.cn/problems/merge-two-binary-trees/) + +给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。 + +你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。 + +示例 1: + +![617.合并二叉树](https://file1.kamacoder.com/i/algo/20230310000854.png) + +注意: 合并必须从两个树的根节点开始。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[一起操作两个二叉树?有点懵!| LeetCode:617.合并二叉树](https://www.bilibili.com/video/BV1m14y1Y7JK),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢? + +其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。 + +### 递归 + +二叉树使用递归,就要想使用前中后哪种遍历方式? + +**本题使用哪种遍历都是可以的!** + +我们下面以前序遍历为例。 + +动画如下: + +![617.合并二叉树](https://file1.kamacoder.com/i/algo/617.%E5%90%88%E5%B9%B6%E4%BA%8C%E5%8F%89%E6%A0%91.gif) + +那么我们来按照递归三部曲来解决: + +1. **确定递归函数的参数和返回值:** + +首先要合入两个二叉树,那么参数至少是要传入两个二叉树的根节点,返回值就是合并之后二叉树的根节点。 + +代码如下: + +``` +TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { +``` + +2. **确定终止条件:** + +因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了(如果t2也为NULL也无所谓,合并之后就是NULL)。 + +反过来如果t2 == NULL,那么两个数合并就是t1(如果t1也为NULL也无所谓,合并之后就是NULL)。 + +代码如下: + +``` +if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 +if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 +``` + + +3. **确定单层递归的逻辑:** + +单层递归的逻辑就比较好写了,这里我们重复利用一下t1这个树,t1就是合并之后树的根节点(就是修改了原来树的结构)。 + +那么单层递归中,就要把两棵树的元素加到一起。 +``` +t1->val += t2->val; +``` + +接下来t1 的左子树是:合并 t1左子树 t2左子树之后的左子树。 + +t1 的右子树:是 合并 t1右子树 t2右子树之后的右子树。 + +最终t1就是合并之后的根节点。 + +代码如下: + +``` +t1->left = mergeTrees(t1->left, t2->left); +t1->right = mergeTrees(t1->right, t2->right); +return t1; +``` + +此时前序遍历,完整代码就写出来了,如下: + +```CPP +class Solution { +public: + TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { + if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 + if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 + // 修改了t1的数值和结构 + t1->val += t2->val; // 中 + t1->left = mergeTrees(t1->left, t2->left); // 左 + t1->right = mergeTrees(t1->right, t2->right); // 右 + return t1; + } +}; +``` + +那么中序遍历也是可以的,代码如下: + +```CPP +class Solution { +public: + TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { + if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 + if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 + // 修改了t1的数值和结构 + t1->left = mergeTrees(t1->left, t2->left); // 左 + t1->val += t2->val; // 中 + t1->right = mergeTrees(t1->right, t2->right); // 右 + return t1; + } +}; +``` + +后序遍历依然可以,代码如下: + +```CPP +class Solution { +public: + TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { + if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2 + if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1 + // 修改了t1的数值和结构 + t1->left = mergeTrees(t1->left, t2->left); // 左 + t1->right = mergeTrees(t1->right, t2->right); // 右 + t1->val += t2->val; // 中 + return t1; + } +}; +``` + +**但是前序遍历是最好理解的,我建议大家用前序遍历来做就OK。** + +如上的方法修改了t1的结构,当然也可以不修改t1和t2的结构,重新定义一个树。 + +不修改输入树的结构,前序遍历,代码如下: + +```CPP +class Solution { +public: + TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { + if (t1 == NULL) return t2; + if (t2 == NULL) return t1; + // 重新定义新的节点,不修改原有两个树的结构 + TreeNode* root = new TreeNode(0); + root->val = t1->val + t2->val; + root->left = mergeTrees(t1->left, t2->left); + root->right = mergeTrees(t1->right, t2->right); + return root; + } +}; +``` + +### 迭代法 + +使用迭代法,如何同时处理两棵树呢? + +思路我们在[二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)中的迭代法已经讲过一次了,求二叉树对称的时候就是把两个树的节点同时加入队列进行比较。 + +本题我们也使用队列,模拟的层序遍历,代码如下: + +```CPP +class Solution { +public: + TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { + if (t1 == NULL) return t2; + if (t2 == NULL) return t1; + queue que; + que.push(t1); + que.push(t2); + while(!que.empty()) { + TreeNode* node1 = que.front(); que.pop(); + TreeNode* node2 = que.front(); que.pop(); + // 此时两个节点一定不为空,val相加 + node1->val += node2->val; + + // 如果两棵树左节点都不为空,加入队列 + if (node1->left != NULL && node2->left != NULL) { + que.push(node1->left); + que.push(node2->left); + } + // 如果两棵树右节点都不为空,加入队列 + if (node1->right != NULL && node2->right != NULL) { + que.push(node1->right); + que.push(node2->right); + } + + // 当t1的左节点 为空 t2左节点不为空,就赋值过去 + if (node1->left == NULL && node2->left != NULL) { + node1->left = node2->left; + } + // 当t1的右节点 为空 t2右节点不为空,就赋值过去 + if (node1->right == NULL && node2->right != NULL) { + node1->right = node2->right; + } + } + return t1; + } +}; +``` + +## 拓展 + +当然也可以秀一波指针的操作,这是我写的野路子,大家就随便看看就行了,以防带跑偏了。 + +如下代码中,想要更改二叉树的值,应该传入指向指针的指针。 + +代码如下:(前序遍历) +```CPP +class Solution { +public: + void process(TreeNode** t1, TreeNode** t2) { + if ((*t1) == NULL && (*t2) == NULL) return; + if ((*t1) != NULL && (*t2) != NULL) { + (*t1)->val += (*t2)->val; + } + if ((*t1) == NULL && (*t2) != NULL) { + *t1 = *t2; + return; + } + if ((*t1) != NULL && (*t2) == NULL) { + return; + } + process(&((*t1)->left), &((*t2)->left)); + process(&((*t1)->right), &((*t2)->right)); + } + TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { + process(&t1, &t2); + return t1; + } +}; +``` + +## 总结 + +合并二叉树,也是二叉树操作的经典题目,如果没有接触过的话,其实并不简单,因为我们习惯了操作一个二叉树,一起操作两个二叉树,还会有点懵懵的。 + +这不是我们第一次操作两棵二叉树了,在[二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)中也一起操作了两棵二叉树。 + +迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。 + +最后拓展中,我给了一个操作指针的野路子,大家随便看看就行了,如果学习C++的话,可以再去研究研究。 + + +## 其他语言版本 + + +### Java + +```Java +class Solution { + // 递归 + public TreeNode mergeTrees(TreeNode root1, TreeNode root2) { + if (root1 == null) return root2; + if (root2 == null) return root1; + + root1.val += root2.val; + root1.left = mergeTrees(root1.left,root2.left); + root1.right = mergeTrees(root1.right,root2.right); + return root1; + } +} +``` + +```Java +class Solution { + // 使用栈迭代 + public TreeNode mergeTrees(TreeNode root1, TreeNode root2) { + if (root1 == null) { + return root2; + } + if (root2 == null) { + return root1; + } + Stack stack = new Stack<>(); + stack.push(root2); + stack.push(root1); + while (!stack.isEmpty()) { + TreeNode node1 = stack.pop(); + TreeNode node2 = stack.pop(); + node1.val += node2.val; + if (node2.right != null && node1.right != null) { + stack.push(node2.right); + stack.push(node1.right); + } else { + if (node1.right == null) { + node1.right = node2.right; + } + } + if (node2.left != null && node1.left != null) { + stack.push(node2.left); + stack.push(node1.left); + } else { + if (node1.left == null) { + node1.left = node2.left; + } + } + } + return root1; + } +} +``` +```java +class Solution { + // 使用队列迭代 + public TreeNode mergeTrees(TreeNode root1, TreeNode root2) { + if (root1 == null) return root2; + if (root2 ==null) return root1; + Queue queue = new LinkedList<>(); + queue.offer(root1); + queue.offer(root2); + while (!queue.isEmpty()) { + TreeNode node1 = queue.poll(); + TreeNode node2 = queue.poll(); + // 此时两个节点一定不为空,val相加 + node1.val = node1.val + node2.val; + // 如果两棵树左节点都不为空,加入队列 + if (node1.left != null && node2.left != null) { + queue.offer(node1.left); + queue.offer(node2.left); + } + // 如果两棵树右节点都不为空,加入队列 + if (node1.right != null && node2.right != null) { + queue.offer(node1.right); + queue.offer(node2.right); + } + // 若node1的左节点为空,直接赋值 + if (node1.left == null && node2.left != null) { + node1.left = node2.left; + } + // 若node1的右节点为空,直接赋值 + if (node1.right == null && node2.right != null) { + node1.right = node2.right; + } + } + return root1; + } +} +``` + +### Python +(版本一) 递归 - 前序 - 修改root1 +```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 mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode: + # 递归终止条件: + # 但凡有一个节点为空, 就立刻返回另外一个. 如果另外一个也为None就直接返回None. + if not root1: + return root2 + if not root2: + return root1 + # 上面的递归终止条件保证了代码执行到这里root1, root2都非空. + root1.val += root2.val # 中 + root1.left = self.mergeTrees(root1.left, root2.left) #左 + root1.right = self.mergeTrees(root1.right, root2.right) # 右 + + return root1 # ⚠️ 注意: 本题我们重复使用了题目给出的节点而不是创建新节点. 节省时间, 空间. + +``` +(版本二) 递归 - 前序 - 新建root +```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 mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode: + # 递归终止条件: + # 但凡有一个节点为空, 就立刻返回另外一个. 如果另外一个也为None就直接返回None. + if not root1: + return root2 + if not root2: + return root1 + # 上面的递归终止条件保证了代码执行到这里root1, root2都非空. + root = TreeNode() # 创建新节点 + root.val += root1.val + root2.val# 中 + root.left = self.mergeTrees(root1.left, root2.left) #左 + root.right = self.mergeTrees(root1.right, root2.right) # 右 + + return root # ⚠️ 注意: 本题我们创建了新节点. + +``` + +(版本三) 迭代 +```python +class Solution: + def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode: + if not root1: + return root2 + if not root2: + return root1 + + queue = deque() + queue.append(root1) + queue.append(root2) + + while queue: + node1 = queue.popleft() + node2 = queue.popleft() + # 更新queue + # 只有两个节点都有左节点时, 再往queue里面放. + if node1.left and node2.left: + queue.append(node1.left) + queue.append(node2.left) + # 只有两个节点都有右节点时, 再往queue里面放. + if node1.right and node2.right: + queue.append(node1.right) + queue.append(node2.right) + + # 更新当前节点. 同时改变当前节点的左右孩子. + node1.val += node2.val + if not node1.left and node2.left: + node1.left = node2.left + if not node1.right and node2.right: + node1.right = node2.right + + return root1 +``` +(版本四) 迭代 + 代码优化 +```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 +from collections import deque + +class Solution: + def mergeTrees(self, root1: TreeNode, root2: TreeNode) -> TreeNode: + if not root1: + return root2 + if not root2: + return root1 + + queue = deque() + queue.append((root1, root2)) + + while queue: + node1, node2 = queue.popleft() + node1.val += node2.val + + if node1.left and node2.left: + queue.append((node1.left, node2.left)) + elif not node1.left: + node1.left = node2.left + + if node1.right and node2.right: + queue.append((node1.right, node2.right)) + elif not node1.right: + node1.right = node2.right + + return root1 + + +``` +### Go + +```go +// 前序遍历 +func mergeTrees(root1 *TreeNode, root2 *TreeNode) *TreeNode { + if root1 == nil { + return root2 + } + if root2 == nil { + return root1 + } + root1.Val += root2.Val + root1.Left = mergeTrees(root1.Left, root2.Left) + root1.Right = mergeTrees(root1.Right, root2.Right) + return root1 +} + +// 迭代版本 +func mergeTrees(root1 *TreeNode, root2 *TreeNode) *TreeNode { + queue := make([]*TreeNode,0) + if root1 == nil{ + return root2 + } + if root2 == nil{ + return root1 + } + queue = append(queue,root1) + queue = append(queue,root2) + + for size := len(queue); size>0; size=len(queue) { + node1 := queue[0] + queue = queue[1:] + node2 := queue[0] + queue = queue[1:] + node1.Val += node2.Val + // 左子树都不为空 + if node1.Left != nil && node2.Left != nil { + queue = append(queue,node1.Left) + queue = append(queue,node2.Left) + } + // 右子树都不为空 + if node1.Right !=nil && node2.Right !=nil { + queue = append(queue, node1.Right) + queue = append(queue, node2.Right) + } + // 树 1 的左子树为 nil,直接接上树 2 的左子树 + if node1.Left == nil { + node1.Left = node2.Left + } + // 树 1 的右子树为 nil,直接接上树 2 的右子树 + if node1.Right == nil { + node1.Right = node2.Right + } + } + return root1 +} +``` + +### JavaScript + +> 递归法: + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root1 + * @param {TreeNode} root2 + * @return {TreeNode} + */ +var mergeTrees = function (root1, root2) { + const preOrder = (root1, root2) => { + if (!root1) + return root2 + if (!root2) + return root1; + root1.val += root2.val; + root1.left = preOrder(root1.left, root2.left); + root1.right = preOrder(root1.right, root2.right); + return root1; + } + return preOrder(root1, root2); +}; +``` +> 迭代法: + +```javascript + +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root1 + * @param {TreeNode} root2 + * @return {TreeNode} + */ +var mergeTrees = function(root1, root2) { + if (root1 === null) return root2; + if (root2 === null) return root1; + + let queue = []; + queue.push(root1); + queue.push(root2); + while (queue.length) { + let node1 = queue.shift(); + let node2 = queue.shift();; + node1.val += node2.val; + if (node1.left !== null && node2.left !== null) { + queue.push(node1.left); + queue.push(node2.left); + } + if (node1.right !== null && node2.right !== null) { + queue.push(node1.right); + queue.push(node2.right); + } + if (node1.left === null && node2.left !== null) { + node1.left = node2.left; + } + if (node1.right === null && node2.right !== null) { + node1.right = node2.right; + } + } + return root1; +}; + +``` + +### TypeScript + +> 递归法: + +```typescript +function mergeTrees(root1: TreeNode | null, root2: TreeNode | null): TreeNode | null { + if (root1 === null) return root2; + if (root2 === null) return root1; + const resNode: TreeNode = new TreeNode(root1.val + root2.val); + resNode.left = mergeTrees(root1.left, root2.left); + resNode.right = mergeTrees(root1.right, root2.right); + return resNode; +}; +``` + +> 迭代法: + +```typescript +function mergeTrees(root1: TreeNode | null, root2: TreeNode | null): TreeNode | null { + if (root1 === null) return root2; + if (root2 === null) return root1; + const helperQueue1: TreeNode[] = [], + helperQueue2: TreeNode[] = []; + helperQueue1.push(root1); + helperQueue2.push(root2); + let tempNode1: TreeNode, + tempNode2: TreeNode; + while (helperQueue1.length > 0) { + tempNode1 = helperQueue1.shift()!; + tempNode2 = helperQueue2.shift()!; + tempNode1.val += tempNode2.val; + if (tempNode1.left !== null && tempNode2.left !== null) { + helperQueue1.push(tempNode1.left); + helperQueue2.push(tempNode2.left); + } else if (tempNode1.left === null) { + tempNode1.left = tempNode2.left; + } + if (tempNode1.right !== null && tempNode2.right !== null) { + helperQueue1.push(tempNode1.right); + helperQueue2.push(tempNode2.right); + } else if (tempNode1.right === null) { + tempNode1.right = tempNode2.right; + } + } + return root1; +}; +``` + +### Scala + +递归: +```scala +object Solution { + def mergeTrees(root1: TreeNode, root2: TreeNode): TreeNode = { + if (root1 == null) return root2 // 如果root1为空,返回root2 + if (root2 == null) return root1 // 如果root2为空,返回root1 + // 新建一个节点,值为两个节点的和 + var node = new TreeNode(root1.value + root2.value) + // 往下递归 + node.left = mergeTrees(root1.left, root2.left) + node.right = mergeTrees(root1.right, root2.right) + node // 返回node,return关键字可以省略 + } +} +``` + +迭代: +```scala +object Solution { + import scala.collection.mutable + def mergeTrees(root1: TreeNode, root2: TreeNode): TreeNode = { + if (root1 == null) return root2 + if (root2 == null) return root1 + var stack = mutable.Stack[TreeNode]() + // 先放node2再放node1 + stack.push(root2) + stack.push(root1) + while (!stack.isEmpty) { + var node1 = stack.pop() + var node2 = stack.pop() + node1.value += node2.value + if (node1.right != null && node2.right != null) { + stack.push(node2.right) + stack.push(node1.right) + } else { + if(node1.right == null){ + node1.right = node2.right + } + } + if (node1.left != null && node2.left != null) { + stack.push(node2.left) + stack.push(node1.left) + } else { + if(node1.left == null){ + node1.left = node2.left + } + } + } + root1 + } +} +``` + +### Rust + +递归: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn merge_trees( + root1: Option>>, + root2: Option>>, + ) -> Option>> { + if root1.is_none() { + return root2; + } + if root2.is_none() { + return root1; + } + let binding = root1.clone(); + let mut node1 = binding.as_ref().unwrap().borrow_mut(); + let node2 = root2.as_ref().unwrap().borrow_mut(); + node1.left = Self::merge_trees(node1.left.clone(), node2.left.clone()); + node1.right = Self::merge_trees(node1.right.clone(), node2.right.clone()); + node1.val += node2.val; + + root1 + } +} +``` + +迭代: + +```rust +impl Solution { + pub fn merge_trees( + root1: Option>>, + root2: Option>>, + ) -> Option>> { + if root1.is_none() { + return root2; + } + if root2.is_none() { + return root1; + } + let mut stack = vec![]; + stack.push(root2); + stack.push(root1.clone()); + while !stack.is_empty() { + let node1 = stack.pop().unwrap().unwrap(); + let node2 = stack.pop().unwrap().unwrap(); + let mut node1 = node1.borrow_mut(); + let node2 = node2.borrow(); + node1.val += node2.val; + if node1.left.is_some() && node2.left.is_some() { + stack.push(node2.left.clone()); + stack.push(node1.left.clone()); + } + if node1.right.is_some() && node2.right.is_some() { + stack.push(node2.right.clone()); + stack.push(node1.right.clone()); + } + if node1.left.is_none() && node2.left.is_some() { + node1.left = node2.left.clone(); + } + if node1.right.is_none() && node2.right.is_some() { + node1.right = node2.right.clone(); + } + } + root1 + } +} +``` +### C# +```csharp +public TreeNode MergeTrees(TreeNode root1, TreeNode root2) +{ + if (root1 == null) return root2; + if (root2 == null) return root1; + + root1.val += root2.val; + root1.left = MergeTrees(root1.left, root2.left); + root1.right = MergeTrees(root1.right, root2.right); + + return root1; +} +``` + + diff --git "a/problems/0647.\345\233\236\346\226\207\345\255\220\344\270\262.md" "b/problems/0647.\345\233\236\346\226\207\345\255\220\344\270\262.md" new file mode 100755 index 0000000000..fd2ae43886 --- /dev/null +++ "b/problems/0647.\345\233\236\346\226\207\345\255\220\344\270\262.md" @@ -0,0 +1,613 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 647. 回文子串 + +[力扣题目链接](https://leetcode.cn/problems/palindromic-substrings/) + +给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。 + +具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。 + +示例 1: + +* 输入:"abc" +* 输出:3 +* 解释:三个回文子串: "a", "b", "c" + +示例 2: + +* 输入:"aaa" +* 输出:6 +* 解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa" + +提示:输入的字符串长度不会超过 1000 。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划,字符串性质决定了DP数组的定义 | LeetCode:647.回文子串](https://www.bilibili.com/video/BV17G4y1y7z9/),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 暴力解法 + +两层for循环,遍历区间起始位置和终止位置,然后还需要一层遍历判断这个区间是不是回文。所以时间复杂度:O(n^3) + +### 动态规划 + +动规五部曲: + +1. 确定dp数组(dp table)以及下标的含义 + +如果大家做了很多这种子序列相关的题目,在定义dp数组的时候 很自然就会想题目求什么,我们就如何定义dp数组。 + +绝大多数题目确实是这样,不过本题如果我们定义,dp[i] 为 下标i结尾的字符串有 dp[i]个回文串的话,我们会发现很难找到递归关系。 + +dp[i] 和 dp[i-1] ,dp[i + 1] 看上去都没啥关系。 + +所以我们要看回文串的性质。 如图: + +![](https://file1.kamacoder.com/i/algo/20230102170752.png) + +我们在判断字符串S是否是回文,那么如果我们知道 s[1],s[2],s[3] 这个子串是回文的,那么只需要比较 s[0]和s[4]这两个元素是否相同,如果相同的话,这个字符串s 就是回文串。 + + +那么此时我们是不是能找到一种递归关系,也就是判断一个子字符串(字符串下标范围[i,j])是否回文,依赖于,子字符串(下标范围[i + 1, j - 1])) 是否是回文。 + +所以为了明确这种递归关系,我们的dp数组是要定义成一位二维dp数组。 + +布尔类型的dp[i][j]:表示区间范围[i,j] (注意是左闭右闭)的子串是否是回文子串,如果是dp[i][j]为true,否则为false。 + + +2. 确定递推公式 + +在确定递推公式时,就要分析如下几种情况。 + +整体上是两种,就是s[i]与s[j]相等,s[i]与s[j]不相等这两种。 + +当s[i]与s[j]不相等,那没啥好说的了,dp[i][j]一定是false。 + +当s[i]与s[j]相等时,这就复杂一些了,有如下三种情况 + +* 情况一:下标i 与 j相同,同一个字符例如a,当然是回文子串 +* 情况二:下标i 与 j相差为1,例如aa,也是回文子串 +* 情况三:下标:i 与 j相差大于1的时候,例如cabac,此时s[i]与s[j]已经相同了,我们看i到j区间是不是回文子串就看aba是不是回文就可以了,那么aba的区间就是 i+1 与 j-1区间,这个区间是不是回文就看dp[i + 1][j - 1]是否为true。 + +以上三种情况分析完了,那么递归公式如下: + +```CPP +if (s[i] == s[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + result++; + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { // 情况三 + result++; + dp[i][j] = true; + } +} +``` + +result就是统计回文子串的数量。 + +注意这里我没有列出当s[i]与s[j]不相等的时候,因为在下面dp[i][j]初始化的时候,就初始为false。 + +3. dp数组如何初始化 + +dp[i][j]可以初始化为true么? 当然不行,怎能刚开始就全都匹配上了。 + +所以dp[i][j]初始化为false。 + +4. 确定遍历顺序 + +遍历顺序可就有点讲究了。 + +首先从递推公式中可以看出,情况三是根据dp[i + 1][j - 1]是否为true,在对dp[i][j]进行赋值true的。 + +dp[i + 1][j - 1] 在 dp[i][j]的左下角,如图: + +![647.回文子串](https://file1.kamacoder.com/i/algo/20210121171032473-20230310132134822.jpg) + +如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。 + +**所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的**。 + +有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。 + +代码如下: + +```CPP +for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序 + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + result++; + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { // 情况三 + result++; + dp[i][j] = true; + } + } + } +} +``` + +5. 举例推导dp数组 + +举例,输入:"aaa",dp[i][j]状态如下: + +![647.回文子串1](https://file1.kamacoder.com/i/algo/20210121171059951-20230310132153163.jpg) + +图中有6个true,所以就是有6个回文子串。 + +**注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分**。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int countSubstrings(string s) { + vector> dp(s.size(), vector(s.size(), false)); + int result = 0; + for (int i = s.size() - 1; i >= 0; i--) { // 注意遍历顺序 + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + result++; + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { // 情况三 + result++; + dp[i][j] = true; + } + } + } + } + return result; + } +}; +``` +以上代码是为了凸显情况一二三,当然是可以简洁一下的,如下: + +```CPP +class Solution { +public: + int countSubstrings(string s) { + vector> dp(s.size(), vector(s.size(), false)); + int result = 0; + for (int i = s.size() - 1; i >= 0; i--) { + for (int j = i; j < s.size(); j++) { + if (s[i] == s[j] && (j - i <= 1 || dp[i + 1][j - 1])) { + result++; + dp[i][j] = true; + } + } + } + return result; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n^2) + +### 双指针法 + +动态规划的空间复杂度是偏高的,我们再看一下双指针法。 + +首先确定回文串,就是找中心然后向两边扩散看是不是对称的就可以了。 + +**在遍历中心点的时候,要注意中心点有两种情况**。 + +一个元素可以作为中心点,两个元素也可以作为中心点。 + +那么有人同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。 + +所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。 + +**这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算**,代码如下: + +```CPP +class Solution { +public: + int countSubstrings(string s) { + int result = 0; + for (int i = 0; i < s.size(); i++) { + result += extend(s, i, i, s.size()); // 以i为中心 + result += extend(s, i, i + 1, s.size()); // 以i和i+1为中心 + } + return result; + } + int extend(const string& s, int i, int j, int n) { + int res = 0; + while (i >= 0 && j < n && s[i] == s[j]) { + i--; + j++; + res++; + } + return res; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + +## 其他语言版本 + +### Java: + +动态规划: + +```java +class Solution { + public int countSubstrings(String s) { + char[] chars = s.toCharArray(); + int len = chars.length; + boolean[][] dp = new boolean[len][len]; + int result = 0; + for (int i = len - 1; i >= 0; i--) { + for (int j = i; j < len; j++) { + if (chars[i] == chars[j]) { + if (j - i <= 1) { // 情况一 和 情况二 + result++; + dp[i][j] = true; + } else if (dp[i + 1][j - 1]) { //情况三 + result++; + dp[i][j] = true; + } + } + } + } + return result; + } +} + +``` + +动态规划:简洁版 +```java +class Solution { + public int countSubstrings(String s) { + boolean[][] dp = new boolean[s.length()][s.length()]; + + int res = 0; + for (int i = s.length() - 1; i >= 0; i--) { + for (int j = i; j < s.length(); j++) { + if (s.charAt(i) == s.charAt(j) && (j - i <= 1 || dp[i + 1][j - 1])) { + res++; + dp[i][j] = true; + } + } + } + return res; + } +} +``` + +中心扩散法: + +```java +class Solution { + public int countSubstrings(String s) { + int len, ans = 0; + if (s == null || (len = s.length()) < 1) return 0; + //总共有2 * len - 1个中心点 + for (int i = 0; i < 2 * len - 1; i++) { + //通过遍历每个回文中心,向两边扩散,并判断是否回文字串 + //有两种情况,left == right,right = left + 1,这两种回文中心是不一样的 + int left = i / 2, right = left + i % 2; + while (left >= 0 && right < len && s.charAt(left) == s.charAt(right)) { + //如果当前是一个回文串,则记录数量 + ans++; + left--; + right++; + } + } + return ans; + } +} +``` +LeetCode 5. Longest Palindromic Substring(LeetCode 647. 同一題的思路改一下、加一點,就能通過LeetCode 5) +```java +class Solution { + public String longestPalindrome(String s) { + //題目要求要return 最長的回文連續子串,故需要記錄當前最長的連續回文子串長度、最終起點、最終終點。 + int finalStart = 0; + int finalEnd = 0; + int finalLen = 0; + + char[] chars = s.toCharArray(); + int len = chars.length; + + boolean[][] dp = new boolean[len][len]; + for (int i = len - 1; i >= 0; i--) { + for (int j = i; j < len; j++) { + if (chars[i] == chars[j] && (j - i <= 1 || dp[i + 1][j - 1])) + dp[i][j] = true; + //和LeetCode 647,差別就在這個if statement。 + //如果當前[i, j]範圍內的substring是回文子串(dp[i][j]) 且(&&) 長度大於當前要記錄的最終長度(j - i + 1 > finalLen) + //我們就更新 當前最長的連續回文子串長度、最終起點、最終終點 + if (dp[i][j] && j - i + 1 > finalLen) { + finalLen = j - i + 1; + finalStart = i; + finalEnd = j; + } + } + } + //String.substring這個method的用法是[起點, 終點),包含起點,不包含終點(左閉右開區間),故終點 + 1。 + return s.substring(finalStart, finalEnd + 1); + } +} +``` + +### Python: + +> 动态规划: +```python +class Solution: + def countSubstrings(self, s: str) -> int: + dp = [[False] * len(s) for _ in range(len(s))] + result = 0 + for i in range(len(s)-1, -1, -1): #注意遍历顺序 + for j in range(i, len(s)): + if s[i] == s[j]: + if j - i <= 1: #情况一 和 情况二 + result += 1 + dp[i][j] = True + elif dp[i+1][j-1]: #情况三 + result += 1 + dp[i][j] = True + return result +``` + +> 动态规划:简洁版 +```python +class Solution: + def countSubstrings(self, s: str) -> int: + dp = [[False] * len(s) for _ in range(len(s))] + result = 0 + for i in range(len(s)-1, -1, -1): #注意遍历顺序 + for j in range(i, len(s)): + if s[i] == s[j] and (j - i <= 1 or dp[i+1][j-1]): + result += 1 + dp[i][j] = True + return result +``` + +> 双指针法: +```python +class Solution: + def countSubstrings(self, s: str) -> int: + result = 0 + for i in range(len(s)): + result += self.extend(s, i, i, len(s)) #以i为中心 + result += self.extend(s, i, i+1, len(s)) #以i和i+1为中心 + return result + + def extend(self, s, i, j, n): + res = 0 + while i >= 0 and j < n and s[i] == s[j]: + i -= 1 + j += 1 + res += 1 + return res +``` + +### Go: +> 动态规划: + +```Go +func countSubstrings(s string) int { + res:=0 + dp:=make([][]bool,len(s)) + for i:=0;i=0;i--{ + for j:=i;j 动态规划:简洁版 +```Go +func countSubstrings(s string) int { + res := 0 + dp := make([][]bool, len(s)) + for i := 0; i < len(s); i++ { + dp[i] = make([]bool, len(s)) + } + + for i := len(s) - 1; i >= 0; i-- { + for j := i; j < len(s); j++ { + if s[i] == s[j] && (j-i <= 1 || dp[i+1][j-1]) { + res++ + dp[i][j] = true + } + } + } + return res +} +``` + +> 双指针法: +```Go +func countSubstrings(s string) int { + extend := func(i, j int) int { + res := 0 + for i >= 0 && j < len(s) && s[i] == s[j] { + i -- + j ++ + res ++ + } + return res + } + res := 0 + for i := 0; i < len(s); i++ { + res += extend(i, i) // 以i为中心 + res += extend(i, i+1) // 以i和i+1为中心 + } + return res +} +``` + +### JavaScript: + +> 动态规划 +```javascript +const countSubstrings = (s) => { + const strLen = s.length; + let numOfPalindromicStr = 0; + let dp = Array.from(Array(strLen), () => Array(strLen).fill(false)); + + for(let j = 0; j < strLen; j++) { + for(let i = 0; i <= j; i++) { + if(s[i] === s[j]) { + if((j - i) < 2) { + dp[i][j] = true; + } else { + dp[i][j] = dp[i+1][j-1]; + } + numOfPalindromicStr += dp[i][j] ? 1 : 0; + } + } + } + + return numOfPalindromicStr; +} +``` + +> 双指针法: +```javascript +const countSubstrings = (s) => { + const strLen = s.length; + let numOfPalindromicStr = 0; + + for(let i = 0; i < 2 * strLen - 1; i++) { + let left = Math.floor(i/2); + let right = left + i % 2; + + while(left >= 0 && right < strLen && s[left] === s[right]){ + numOfPalindromicStr++; + left--; + right++; + } + } + + return numOfPalindromicStr; +} +``` + +### TypeScript: + +> 动态规划: + +```typescript +function countSubstrings(s: string): number { + /** + dp[i][j]: [i,j]区间内的字符串是否为回文(左闭右闭) + */ + const length: number = s.length; + const dp: boolean[][] = new Array(length).fill(0) + .map(_ => new Array(length).fill(false)); + let resCount: number = 0; + // 自下而上,自左向右遍历 + for (let i = length - 1; i >= 0; i--) { + for (let j = i; j < length; j++) { + if ( + s[i] === s[j] && + (j - i <= 1 || dp[i + 1][j - 1] === true) + ) { + dp[i][j] = true; + resCount++; + } + } + } + return resCount; +}; +``` + +> 双指针法: + +```typescript +function countSubstrings(s: string): number { + const length: number = s.length; + let resCount: number = 0; + for (let i = 0; i < length; i++) { + resCount += expandRange(s, i, i); + resCount += expandRange(s, i, i + 1); + } + return resCount; +}; +function expandRange(s: string, left: number, right: number): number { + let palindromeNum: number = 0; + while ( + left >= 0 && right < s.length && + s[left] === s[right] + ) { + palindromeNum++; + left--; + right++; + } + return palindromeNum; +} +``` + +Rust: + +```rust +impl Solution { + pub fn count_substrings(s: String) -> i32 { + let mut dp = vec![vec![false; s.len()]; s.len()]; + let mut res = 0; + + for i in (0..s.len()).rev() { + for j in i..s.len() { + if s[i..=i] == s[j..=j] && (j - i <= 1 || dp[i + 1][j - 1]) { + dp[i][j] = true; + res += 1; + } + } + } + res + } +} +``` + +> 双指针 + +```rust +impl Solution { + pub fn count_substrings(s: String) -> i32 { + let mut res = 0; + for i in 0..s.len() { + res += Self::extend(&s, i, i, s.len()); + res += Self::extend(&s, i, i + 1, s.len()); + } + res + } + + fn extend(s: &str, mut i: usize, mut j: usize, len: usize) -> i32 { + let mut res = 0; + while i < len && j < len && s[i..=i] == s[j..=j] { + res += 1; + i = i.wrapping_sub(1); + j += 1; + } + res + } +} +``` + diff --git "a/problems/0649.Dota2\345\217\202\350\256\256\351\231\242.md" "b/problems/0649.Dota2\345\217\202\350\256\256\351\231\242.md" new file mode 100755 index 0000000000..e77070fcf8 --- /dev/null +++ "b/problems/0649.Dota2\345\217\202\350\256\256\351\231\242.md" @@ -0,0 +1,284 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 649. Dota2 参议院 + +[力扣题目链接](https://leetcode.cn/problems/dota2-senate/) + + +Dota2 的世界里有两个阵营:Radiant(天辉)和 Dire(夜魇) + +Dota2 参议院由来自两派的参议员组成。现在参议院希望对一个 Dota2 游戏里的改变作出决定。他们以一个基于轮为过程的投票进行。在每一轮中,每一位参议员都可以行使两项权利中的一项: + +1. 禁止一名参议员的权利:参议员可以让另一位参议员在这一轮和随后的几轮中丧失所有的权利。 + +2. 宣布胜利:如果参议员发现有权利投票的参议员都是同一个阵营的,他可以宣布胜利并决定在游戏中的有关变化。 + +给定一个字符串代表每个参议员的阵营。字母 “R” 和 “D” 分别代表了 Radiant(天辉)和 Dire(夜魇)。然后,如果有 n 个参议员,给定字符串的大小将是 n。 + +以轮为基础的过程从给定顺序的第一个参议员开始到最后一个参议员结束。这一过程将持续到投票结束。所有失去权利的参议员将在过程中被跳过。 + +假设每一位参议员都足够聪明,会为自己的政党做出最好的策略,你需要预测哪一方最终会宣布胜利并在 Dota2 游戏中决定改变。输出应该是 Radiant 或 Dire。 + +  + +示例 1: +* 输入:"RD" +* 输出:"Radiant" +* 解释:第一个参议员来自 Radiant 阵营并且他可以使用第一项权利让第二个参议员失去权力,因此第二个参议员将被跳过因为他没有任何权利。然后在第二轮的时候,第一个参议员可以宣布胜利,因为他是唯一一个有投票权的人 + +示例 2: +* 输入:"RDD" +* 输出:"Dire" +* 解释: +第一轮中,第一个来自 Radiant 阵营的参议员可以使用第一项权利禁止第二个参议员的权利, +第二个来自 Dire 阵营的参议员会被跳过因为他的权利被禁止, +第三个来自 Dire 阵营的参议员可以使用他的第一项权利禁止第一个参议员的权利, +因此在第二轮只剩下第三个参议员拥有投票的权利,于是他可以宣布胜利。 + + +## 思路 + +这道题 题意太绕了,我举一个更形象的例子给大家捋顺一下。 + +例如输入"RRDDD",执行过程应该是什么样呢? + +* 第一轮:senate[0]的R消灭senate[2]的D,senate[1]的R消灭senate[3]的D,senate[4]的D消灭senate[0]的R,此时剩下"RD",第一轮结束! +* 第二轮:senate[0]的R消灭senate[1]的D,第二轮结束 +* 第三轮:只有R了,R胜利 + +估计不少同学都困惑,R和D数量相同怎么办,究竟谁赢,**其实这是一个持续消灭的过程!** 即:如果同时存在R和D就继续进行下一轮消灭,轮数直到只剩下R或者D为止! + +那么每一轮消灭的策略应该是什么呢? + +例如:RDDRD + +第一轮:senate[0]的R消灭senate[1]的D,那么senate[2]的D,是消灭senate[0]的R还是消灭senate[3]的R呢? + +当然是消灭senate[3]的R,因为当轮到这个R的时候,它可以消灭senate[4]的D。 + +**所以消灭的策略是,尽量消灭自己后面的对手,因为前面的对手已经使用过权利了,而后序的对手依然可以使用权利消灭自己的同伴!** + +那么局部最优:有一次权利机会,就消灭自己后面的对手。全局最优:为自己的阵营赢取最大利益。 + +局部最优可以退出全局最优,举不出反例,那么试试贪心。 + +如果对贪心算法理论基础还不了解的话,可以看看这篇:[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html) ,相信看完之后对贪心就有基本的了解了。 + +## 代码实现 + +实现代码,在每一轮循环的过程中,去过模拟优先消灭身后的对手,其实是比较麻烦的。 + +这里有一个技巧,就是用一个变量记录当前参议员之前有几个敌对对手了,进而判断自己是否被消灭了。这个变量我用flag来表示。 + +C++代码如下: + + +```CPP +class Solution { +public: + string predictPartyVictory(string senate) { + // R = true表示本轮循环结束后,字符串里依然有R。D同理 + bool R = true, D = true; + // 当flag大于0时,R在D前出现,R可以消灭D。当flag小于0时,D在R前出现,D可以消灭R + int flag = 0; + while (R && D) { // 一旦R或者D为false,就结束循环,说明本轮结束后只剩下R或者D了 + R = false; + D = false; + for (int i = 0; i < senate.size(); i++) { + if (senate[i] == 'R') { + if (flag < 0) senate[i] = 0; // 消灭R,R此时为false + else R = true; // 如果没被消灭,本轮循环结束有R + flag++; + } + if (senate[i] == 'D') { + if (flag > 0) senate[i] = 0; + else D = true; + flag--; + } + } + } + // 循环结束之后,R和D只能有一个为true + return R == true ? "Radiant" : "Dire"; + } +}; +``` + + + +## 其他语言版本 + +### Java + +```java +class Solution { + public String predictPartyVictory(String senateStr) { + // R = true表示本轮循环结束后,字符串里依然有R。D同理 + Boolean R = true, D = true; + // 当flag大于0时,R在D前出现,R可以消灭D。当flag小于0时,D在R前出现,D可以消灭R + int flag = 0; + byte[] senate = senateStr.getBytes(); + while (R && D) { // 一旦R或者D为false,就结束循环,说明本轮结束后只剩下R或者D了 + R = false; + D = false; + for (int i = 0; i < senate.length; i++) { + if (senate[i] == 'R') { + if (flag < 0) senate[i] = 0; // 消灭R,R此时为false + else R = true; // 如果没被消灭,本轮循环结束有R + flag++; + } + if (senate[i] == 'D') { + if (flag > 0) senate[i] = 0; + else D = true; + flag--; + } + } + } + // 循环结束之后,R和D只能有一个为true + return R == true ? "Radiant" : "Dire"; + } +} +``` + +### Python + +```python +class Solution: + def predictPartyVictory(self, senate: str) -> str: + # R = true表示本轮循环结束后,字符串里依然有R。D同理 + R , D = True, True + + # 当flag大于0时,R在D前出现,R可以消灭D。当flag小于0时,D在R前出现,D可以消灭R + flag = 0 + + senate = list(senate) + while R and D: # 一旦R或者D为false,就结束循环,说明本轮结束后只剩下R或者D了 + R = False + D = False + for i in range(len(senate)) : + if senate[i] == 'R' : + if flag < 0: senate[i] = '0' # 消灭R,R此时为false + else: R = True # 如果没被消灭,本轮循环结束有R + flag += 1 + if senate[i] == 'D': + if flag > 0: senate[i] = '0' + else: D = True + flag -= 1 + # 循环结束之后,R和D只能有一个为true + return "Radiant" if R else "Dire" +``` + +### Go + +```go + +func predictPartyVictory(senateStr string) string { + // R = true表示本轮循环结束后,字符串里依然有R。D同理 + R, D := true, true + // 当flag大于0时,R在D前出现,R可以消灭D。当flag小于0时,D在R前出现,D可以消灭R + flag := 0 + + senate := []byte(senateStr) + for R && D { // 一旦R或者D为false,就结束循环,说明本轮结束后只剩下R或者D了 + R = false + D = false + for i := 0; i < len(senate); i++ { + if senate[i] == 'R' { + if flag < 0 { + senate[i] = 0 // 消灭R,R此时为false + } else { + R = true // 如果没被消灭,本轮循环结束有R + } + flag++; + } + if (senate[i] == 'D') { + if flag > 0 { + senate[i] = 0 + } else { + D = true + } + flag-- + } + } + } + // 循环结束之后,R和D只能有一个为true + if R { + return "Radiant" + } + return "Dire"; +} +``` + +### JavaScript + +```js +var predictPartyVictory = function(senateStr) { + // R = true表示本轮循环结束后,字符串里依然有R;D同理。 + let R = true, D = true; + // 当flag大于0时,R在D前出现,R可以消灭D。当flag小于0时,D在R前出现,D可以消灭R + let flag = 0; + let senate = senateStr.split(''); + while(R && D){ // 一旦R或者D为false,就结束循环,说明本轮结束后只剩下R或者D了 + R = false; + D = false; + for(let i = 0; i < senate.length; i++){ + if(senate[i] === 'R'){ + if(flag < 0) senate[i] = 0;// 消灭R,R此时为false + else R = true;// 如果没被消灭,本轮循环结束有R + flag++; + } + if(senate[i] === 'D'){ + if(flag > 0) senate[i] = 0; + else D = true; + flag--; + } + } + } + // 循环结束之后,R和D只能有一个为true + return R ? "Radiant" : "Dire"; +}; +``` + +### TypeScript + +```typescript +function predictPartyVictory(senate: string): string { + // 数量差:Count(Radiant) - Count(Dire) + let deltaRDCnt: number = 0; + let hasR: boolean = true, + hasD: boolean = true; + const senateArr: string[] = senate.split(''); + while (hasR && hasD) { + hasR = false; + hasD = false; + for (let i = 0, length = senateArr.length; i < length; i++) { + if (senateArr[i] === 'R') { + if (deltaRDCnt < 0) { + senateArr[i] = ''; + } else { + hasR = true; + } + deltaRDCnt++; + } else if (senateArr[i] === 'D') { + if (deltaRDCnt > 0) { + senateArr[i] = ''; + } else { + hasD = true; + } + deltaRDCnt--; + } + } + } + return hasR ? 'Radiant' : 'Dire'; +}; +``` + + + + + + + diff --git "a/problems/0654.\346\234\200\345\244\247\344\272\214\345\217\211\346\240\221.md" "b/problems/0654.\346\234\200\345\244\247\344\272\214\345\217\211\346\240\221.md" new file mode 100755 index 0000000000..49ccc9cdea --- /dev/null +++ "b/problems/0654.\346\234\200\345\244\247\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,599 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 654.最大二叉树 + +[力扣题目地址](https://leetcode.cn/problems/maximum-binary-tree/) + +给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下: + +* 二叉树的根是数组中的最大元素。 +* 左子树是通过数组中最大值左边部分构造出的最大二叉树。 +* 右子树是通过数组中最大值右边部分构造出的最大二叉树。 + +通过给定的数组构建最大二叉树,并且输出这个树的根节点。 + +示例 : + +![654.最大二叉树](https://file1.kamacoder.com/i/algo/20210204154534796.png) + +提示: + +给定的数组的大小在 [1, 1000] 之间。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[又是构造二叉树,又有很多坑!| LeetCode:654.最大二叉树](https://www.bilibili.com/video/BV1MG411G7ox),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +最大二叉树的构建过程如下: + +![654.最大二叉树](https://file1.kamacoder.com/i/algo/654.%E6%9C%80%E5%A4%A7%E4%BA%8C%E5%8F%89%E6%A0%91.gif) + +构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。 + +* 确定递归函数的参数和返回值 + +参数传入的是存放元素的数组,返回该数组构造的二叉树的头结点,返回类型是指向节点的指针。 + +代码如下: + +```CPP +TreeNode* constructMaximumBinaryTree(vector& nums) + +``` +* 确定终止条件 + +题目中说了输入的数组大小一定是大于等于1的,所以我们不用考虑小于1的情况,那么当递归遍历的时候,如果传入的数组大小为1,说明遍历到了叶子节点了。 + +那么应该定义一个新的节点,并把这个数组的数值赋给新的节点,然后返回这个节点。 这表示一个数组大小是1的时候,构造了一个新的节点,并返回。 + +代码如下: + +```CPP +TreeNode* node = new TreeNode(0); +if (nums.size() == 1) { +    node->val = nums[0]; +    return node; +} +``` + +* 确定单层递归的逻辑 + +这里有三步工作 + +1. 先要找到数组中最大的值和对应的下标, 最大的值构造根节点,下标用来下一步分割数组。 + +代码如下: +```CPP +int maxValue = 0; +int maxValueIndex = 0; +for (int i = 0; i < nums.size(); i++) { +    if (nums[i] > maxValue) { +        maxValue = nums[i]; +        maxValueIndex = i; +    } +} +TreeNode* node = new TreeNode(0); +node->val = maxValue; +``` + +2. 最大值所在的下标左区间 构造左子树 + +这里要判断maxValueIndex > 0,因为要保证左区间至少有一个数值。 + +代码如下: +```CPP +if (maxValueIndex > 0) { +    vector newVec(nums.begin(), nums.begin() + maxValueIndex); +    node->left = constructMaximumBinaryTree(newVec); +} +``` + +3. 最大值所在的下标右区间 构造右子树 + +判断maxValueIndex < (nums.size() - 1),确保右区间至少有一个数值。 + +代码如下: + +```CPP +if (maxValueIndex < (nums.size() - 1)) { +    vector newVec(nums.begin() + maxValueIndex + 1, nums.end()); +    node->right = constructMaximumBinaryTree(newVec); +} +``` +这样我们就分析完了,整体代码如下:(详细注释) + +```CPP +class Solution { +public: + TreeNode* constructMaximumBinaryTree(vector& nums) { + TreeNode* node = new TreeNode(0); + if (nums.size() == 1) { + node->val = nums[0]; + return node; + } + // 找到数组中最大的值和对应的下标 + int maxValue = 0; + int maxValueIndex = 0; + for (int i = 0; i < nums.size(); i++) { + if (nums[i] > maxValue) { + maxValue = nums[i]; + maxValueIndex = i; + } + } + node->val = maxValue; + // 最大值所在的下标左区间 构造左子树 + if (maxValueIndex > 0) { + vector newVec(nums.begin(), nums.begin() + maxValueIndex); + node->left = constructMaximumBinaryTree(newVec); + } + // 最大值所在的下标右区间 构造右子树 + if (maxValueIndex < (nums.size() - 1)) { + vector newVec(nums.begin() + maxValueIndex + 1, nums.end()); + node->right = constructMaximumBinaryTree(newVec); + } + return node; + } +}; +``` + +以上代码比较冗余,效率也不高,每次还要切割的时候每次都要定义新的vector(也就是数组),但逻辑比较清晰。 + +和文章[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中一样的优化思路,就是每次分隔不用定义新的数组,而是通过下标索引直接在原数组上操作。 + +优化后代码如下: + +```CPP +class Solution { +private: + // 在左闭右开区间[left, right),构造二叉树 + TreeNode* traversal(vector& nums, int left, int right) { + if (left >= right) return nullptr; + + // 分割点下标:maxValueIndex + int maxValueIndex = left; + for (int i = left + 1; i < right; ++i) { + if (nums[i] > nums[maxValueIndex]) maxValueIndex = i; + } + + TreeNode* root = new TreeNode(nums[maxValueIndex]); + + // 左闭右开:[left, maxValueIndex) + root->left = traversal(nums, left, maxValueIndex); + + // 左闭右开:[maxValueIndex + 1, right) + root->right = traversal(nums, maxValueIndex + 1, right); + + return root; + } +public: + TreeNode* constructMaximumBinaryTree(vector& nums) { + return traversal(nums, 0, nums.size()); + } +}; +``` + +## 拓展 + +可以发现上面的代码看上去简洁一些,**主要是因为第二版其实是允许空节点进入递归,所以不用在递归的时候加判断节点是否为空** + +第一版递归过程:(加了if判断,为了不让空节点进入递归) +```CPP + +if (maxValueIndex > 0) { // 这里加了判断是为了不让空节点进入递归 + vector newVec(nums.begin(), nums.begin() + maxValueIndex); + node->left = constructMaximumBinaryTree(newVec); +} + +if (maxValueIndex < (nums.size() - 1)) { // 这里加了判断是为了不让空节点进入递归 + vector newVec(nums.begin() + maxValueIndex + 1, nums.end()); + node->right = constructMaximumBinaryTree(newVec); +} +``` + +第二版递归过程: (如下代码就没有加if判断) + +``` +root->left = traversal(nums, left, maxValueIndex); + +root->right = traversal(nums, maxValueIndex + 1, right); +``` + +第二版代码是允许空节点进入递归,所以没有加if判断,当然终止条件也要有相应的改变。 + +第一版终止条件,是遇到叶子节点就终止,因为空节点不会进入递归。 + +第二版相应的终止条件,是遇到空节点,也就是数组区间为0,就终止了。 + +## 总结 + + +这道题目其实和 [二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) 是一个思路,比[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) 还简单一些。 + +**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。** + +一些同学也会疑惑,什么时候递归函数前面加if,什么时候不加if,这个问题我在最后也给出了解释。 + +其实就是不同代码风格的实现,**一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。** + +## 其他语言版本 + + +### Java + +```Java +class Solution { + public TreeNode constructMaximumBinaryTree(int[] nums) { + return constructMaximumBinaryTree1(nums, 0, nums.length); + } + + public TreeNode constructMaximumBinaryTree1(int[] nums, int leftIndex, int rightIndex) { + if (rightIndex - leftIndex < 1) {// 没有元素了 + return null; + } + if (rightIndex - leftIndex == 1) {// 只有一个元素 + return new TreeNode(nums[leftIndex]); + } + int maxIndex = leftIndex;// 最大值所在位置 + int maxVal = nums[maxIndex];// 最大值 + for (int i = leftIndex + 1; i < rightIndex; i++) { + if (nums[i] > maxVal){ + maxVal = nums[i]; + maxIndex = i; + } + } + TreeNode root = new TreeNode(maxVal); + // 根据maxIndex划分左右子树 + root.left = constructMaximumBinaryTree1(nums, leftIndex, maxIndex); + root.right = constructMaximumBinaryTree1(nums, maxIndex + 1, rightIndex); + return root; + } +} +``` + +### Python +(版本一) 基础版 +```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: + if len(nums) == 1: + return TreeNode(nums[0]) + node = TreeNode(0) + # 找到数组中最大的值和对应的下标 + maxValue = 0 + maxValueIndex = 0 + for i in range(len(nums)): + if nums[i] > maxValue: + maxValue = nums[i] + maxValueIndex = i + node.val = maxValue + # 最大值所在的下标左区间 构造左子树 + if maxValueIndex > 0: + new_list = nums[:maxValueIndex] + node.left = self.constructMaximumBinaryTree(new_list) + # 最大值所在的下标右区间 构造右子树 + if maxValueIndex < len(nums) - 1: + new_list = nums[maxValueIndex+1:] + node.right = self.constructMaximumBinaryTree(new_list) + return node + +``` +(版本二) 使用下标 +```python + +class Solution: + def traversal(self, nums: List[int], left: int, right: int) -> TreeNode: + if left >= right: + return None + maxValueIndex = left + for i in range(left + 1, right): + if nums[i] > nums[maxValueIndex]: + maxValueIndex = i + root = TreeNode(nums[maxValueIndex]) + root.left = self.traversal(nums, left, maxValueIndex) + root.right = self.traversal(nums, maxValueIndex + 1, right) + return root + + def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode: + return self.traversal(nums, 0, len(nums)) + +``` + +(版本三) 使用切片 + +```python + +class Solution: + def constructMaximumBinaryTree(self, nums: List[int]) -> TreeNode: + if not nums: + return None + max_val = max(nums) + max_index = nums.index(max_val) + node = TreeNode(max_val) + node.left = self.constructMaximumBinaryTree(nums[:max_index]) + node.right = self.constructMaximumBinaryTree(nums[max_index+1:]) + return node + +``` +### Go + + +```go +func constructMaximumBinaryTree(nums []int) *TreeNode { + if len(nums) == 0 { + return nil + } + // 找到最大值 + index := findMax(nums) + // 构造二叉树 + root := &TreeNode { + Val: nums[index], + Left: constructMaximumBinaryTree(nums[:index]), //左半边 + Right: constructMaximumBinaryTree(nums[index+1:]),//右半边 + } + return root +} +func findMax(nums []int) (index int) { + for i, v := range nums { + if nums[index] < v { + index = i + } + } + return +} +``` + +### JavaScript + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {number[]} nums + * @return {TreeNode} + */ +var constructMaximumBinaryTree = function (nums) { + const BuildTree = (arr, left, right) => { + if (left > right) + return null; + let maxValue = -1; + let maxIndex = -1; + for (let i = left; i <= right; ++i) { + if (arr[i] > maxValue) { + maxValue = arr[i]; + maxIndex = i; + } + } + let root = new TreeNode(maxValue); + root.left = BuildTree(arr, left, maxIndex - 1); + root.right = BuildTree(arr, maxIndex + 1, right); + return root; + } + let root = BuildTree(nums, 0, nums.length - 1); + return root; +}; +``` + +### TypeScript + +> 新建数组法: + +```typescript +function constructMaximumBinaryTree(nums: number[]): TreeNode | null { + if (nums.length === 0) return null; + let maxIndex: number = 0; + let maxVal: number = nums[0]; + for (let i = 1, length = nums.length; i < length; i++) { + if (nums[i] > maxVal) { + maxIndex = i; + maxVal = nums[i]; + } + } + const rootNode: TreeNode = new TreeNode(maxVal); + rootNode.left = constructMaximumBinaryTree(nums.slice(0, maxIndex)); + rootNode.right = constructMaximumBinaryTree(nums.slice(maxIndex + 1)); + return rootNode; +}; +``` + +> 使用数组索引法: + +```typescript +function constructMaximumBinaryTree(nums: number[]): TreeNode | null { + // 左闭右开区间[begin, end) + function recur(nums: number[], begin: number, end: number): TreeNode | null { + if (begin === end) return null; + let maxIndex: number = begin; + let maxVal: number = nums[begin]; + for (let i = begin + 1; i < end; i++) { + if (nums[i] > maxVal) { + maxIndex = i; + maxVal = nums[i]; + } + } + const rootNode: TreeNode = new TreeNode(maxVal); + rootNode.left = recur(nums, begin, maxIndex); + rootNode.right = recur(nums, maxIndex + 1, end); + return rootNode; + } + return recur(nums, 0, nums.length); +}; +``` + + + +### C + +```c +struct TreeNode* traversal(int* nums, int left, int right) { + //若左边界大于右边界,返回NULL + if(left >= right) + return NULL; + + //找出数组中最大数坐标 + int maxIndex = left; + int i; + for(i = left + 1; i < right; i++) { + if(nums[i] > nums[maxIndex]) + maxIndex = i; + } + + //开辟结点 + struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode)); + //将结点的值设为最大数组数组元素 + node->val = nums[maxIndex]; + //递归定义左孩子结点和右孩子结点 + node->left = traversal(nums, left, maxIndex); + node->right = traversal(nums, maxIndex + 1, right); + return node; +} + +struct TreeNode* constructMaximumBinaryTree(int* nums, int numsSize){ + return traversal(nums, 0, numsSize); +} +``` + +### Swift +```swift +func constructMaximumBinaryTree(_ nums: inout [Int]) -> TreeNode? { + return traversal(&nums, 0, nums.count) +} + +func traversal(_ nums: inout [Int], _ left: Int, _ right: Int) -> TreeNode? { + if left >= right { + return nil + } + + var maxValueIndex = left + for i in (left + 1).. nums[maxValueIndex] { + maxValueIndex = i + } + } + + let root = TreeNode(nums[maxValueIndex]) + + root.left = traversal(&nums, left, maxValueIndex) + root.right = traversal(&nums, maxValueIndex + 1, right) + return root +} +``` + +### Scala + +```scala +object Solution { + def constructMaximumBinaryTree(nums: Array[Int]): TreeNode = { + if (nums.size == 0) return null + // 找到数组最大值 + var maxIndex = 0 + var maxValue = Int.MinValue + for (i <- nums.indices) { + if (nums(i) > maxValue) { + maxIndex = i + maxValue = nums(i) + } + } + + // 构建一棵树 + var root = new TreeNode(maxValue, null, null) + + // 递归寻找左右子树 + root.left = constructMaximumBinaryTree(nums.slice(0, maxIndex)) + root.right = constructMaximumBinaryTree(nums.slice(maxIndex + 1, nums.length)) + + root // 返回root + } +} +``` + +### Rust + +新建数组: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution{ + pub fn construct_maximum_binary_tree(mut nums: Vec) -> Option>> { + if nums.is_empty() { + return None; + } + let mut max_value_index = 0; + for i in 0..nums.len() { + if nums[max_value_index] < nums[i] { + max_value_index = i; + } + } + let right = Self::construct_maximum_binary_tree(nums.split_off(max_value_index + 1)); + let root = nums.pop().unwrap(); + let left = Self::construct_maximum_binary_tree(nums); + Some(Rc::new(RefCell::new(TreeNode { + val: root, + left, + right, + }))) + } +} +``` + +数组索引: +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn construct_maximum_binary_tree(nums: Vec) -> Option>> { + Self::traversal(&nums, 0, nums.len()) + } + + pub fn traversal(nums: &Vec, left: usize, right: usize) -> Option>> { + if left >= right { + return None; + } + let mut max_value_index = left; + for i in left + 1..right { + if nums[max_value_index] < nums[i] { + max_value_index = i; + } + } + let mut root = TreeNode::new(nums[max_value_index]); + root.left = Self::traversal(nums, left, max_value_index); + root.right = Self::traversal(nums, max_value_index + 1, right); + Some(Rc::new(RefCell::new(root))) + } +} +``` +### C# +```csharp +public TreeNode ConstructMaximumBinaryTree(int[] nums) +{ + if (nums.Length == 0) return null; + int rootValue = nums.Max(); + TreeNode root = new TreeNode(rootValue); + int rootIndex = Array.IndexOf(nums, rootValue); + + root.left = ConstructMaximumBinaryTree(nums.Take(rootIndex).ToArray()); + root.right = ConstructMaximumBinaryTree(nums.Skip(rootIndex + 1).ToArray()); + return root; + +} +``` + + diff --git "a/problems/0657.\346\234\272\345\231\250\344\272\272\350\203\275\345\220\246\350\277\224\345\233\236\345\216\237\347\202\271.md" "b/problems/0657.\346\234\272\345\231\250\344\272\272\350\203\275\345\220\246\350\277\224\345\233\236\345\216\237\347\202\271.md" new file mode 100755 index 0000000000..c1706df4fa --- /dev/null +++ "b/problems/0657.\346\234\272\345\231\250\344\272\272\350\203\275\345\220\246\350\277\224\345\233\236\345\216\237\347\202\271.md" @@ -0,0 +1,182 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 657. 机器人能否返回原点 + +[力扣题目链接](https://leetcode.cn/problems/robot-return-to-origin/) + +在二维平面上,有一个机器人从原点 (0, 0) 开始。给出它的移动顺序,判断这个机器人在完成移动后是否在 (0, 0) 处结束。 + +移动顺序由字符串表示。字符 move[i] 表示其第 i 次移动。机器人的有效动作有 R(右),L(左),U(上)和 D(下)。如果机器人在完成所有动作后返回原点,则返回 true。否则,返回 false。 + +注意:机器人“面朝”的方向无关紧要。 “R” 将始终使机器人向右移动一次,“L” 将始终向左移动等。此外,假设每次移动机器人的移动幅度相同。 + +  + +示例 1: +* 输入: "UD" +* 输出: true +* 解释:机器人向上移动一次,然后向下移动一次。所有动作都具有相同的幅度,因此它最终回到它开始的原点。因此,我们返回 true。 + +示例 2: +* 输入: "LL" +* 输出: false +* 解释:机器人向左移动两次。它最终位于原点的左侧,距原点有两次 “移动” 的距离。我们返回 false,因为它在移动结束时没有返回原点。 + + + +## 思路 + +这道题目还是挺简单的,大家不要想复杂了,一波哈希法又一波图论算法之类的。 + +其实就是,x,y坐标,初始为0,然后: +* if (moves[i] == 'U') y++; +* if (moves[i] == 'D') y--; +* if (moves[i] == 'L') x--; +* if (moves[i] == 'R') x++; + +最后判断一下x,y是否回到了(0, 0)位置就可以了。 + +如图所示: + + +C++代码如下: + +```CPP +class Solution { +public: + bool judgeCircle(string moves) { + int x = 0, y = 0; + for (int i = 0; i < moves.size(); i++) { + if (moves[i] == 'U') y++; + if (moves[i] == 'D') y--; + if (moves[i] == 'L') x--; + if (moves[i] == 'R') x++; + } + if (x == 0 && y == 0) return true; + return false; + } +}; +``` + + +## 其他语言版本 + +### Java + +```java +// 时间复杂度:O(n) +// 空间复杂度:如果采用 toCharArray,则是 O(n);如果使用 charAt,则是 O(1) +class Solution { + public boolean judgeCircle(String moves) { + int x = 0; + int y = 0; + for (char c : moves.toCharArray()) { + if (c == 'U') y++; + if (c == 'D') y--; + if (c == 'L') x++; + if (c == 'R') x--; + } + return x == 0 && y == 0; + } +} +``` + +### Python + +```python +# 时间复杂度:O(n) +# 空间复杂度:O(1) +class Solution: + def judgeCircle(self, moves: str) -> bool: + x = 0 # 记录当前位置 + y = 0 + for i in range(len(moves)): + if (moves[i] == 'U'): + y += 1 + if (moves[i] == 'D'): + y -= 1 + if (moves[i] == 'L'): + x += 1 + if (moves[i] == 'R'): + x -= 1 + return x == 0 and y == 0 +``` + +### Go + +```go +func judgeCircle(moves string) bool { + x := 0 + y := 0 + for i := 0; i < len(moves); i++ { + if moves[i] == 'U' { + y++ + } + if moves[i] == 'D' { + y-- + } + if moves[i] == 'L' { + x++ + } + if moves[i] == 'R' { + x-- + } + } + return x == 0 && y == 0; +} +``` + +### JavaScript + +```js +// 时间复杂度:O(n) +// 空间复杂度:O(1) +var judgeCircle = function(moves) { + var x = 0; // 记录当前位置 + var y = 0; + for (var i = 0; i < moves.length; i++) { + if (moves[i] == 'U') y++; + if (moves[i] == 'D') y--; + if (moves[i] == 'L') x++; + if (moves[i] == 'R') x--; + } + return x == 0 && y == 0; +}; +``` + + +### TypeScript + +```ts +var judgeCircle = function (moves) { + let x = 0 + let y = 0 + for (let i = 0; i < moves.length; i++) { + switch (moves[i]) { + case 'L': { + x-- + break + } + case 'R': { + x++ + break + } + case 'U': { + y-- + break + } + case 'D': { + y++ + break + } + } + } + return x === 0 && y === 0 +}; +``` + + + diff --git "a/problems/0669.\344\277\256\345\211\252\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" "b/problems/0669.\344\277\256\345\211\252\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" new file mode 100755 index 0000000000..dbcc6ed63d --- /dev/null +++ "b/problems/0669.\344\277\256\345\211\252\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221.md" @@ -0,0 +1,587 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +> 如果不对递归有深刻的理解,本题有点难 +> 单纯移除一个节点那还不够,要修剪! + +# 669. 修剪二叉搜索树 + +[力扣题目链接](https://leetcode.cn/problems/trim-a-binary-search-tree/) + +给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。 + +![669.修剪二叉搜索树](https://file1.kamacoder.com/i/algo/20201014173115788.png) + +![669.修剪二叉搜索树1](https://file1.kamacoder.com/i/algo/20201014173219142.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[你修剪的方式不对,我来给你纠正一下!| LeetCode:669. 修剪二叉搜索树](https://www.bilibili.com/video/BV17P41177ud?share_source=copy_web),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +相信看到这道题目大家都感觉是一道简单题(事实上leetcode上也标明是简单)。 + +但还真的不简单! + +### 递归法 + +直接想法就是:递归处理,然后遇到 `root->val < low || root->val > high` 的时候直接return NULL,一波修改,赶紧利落。 + +不难写出如下代码: + +```CPP +class Solution { +public: + TreeNode* trimBST(TreeNode* root, int low, int high) { + if (root == nullptr || root->val < low || root->val > high) return nullptr; + root->left = trimBST(root->left, low, high); + root->right = trimBST(root->right, low, high); + return root; + } +}; +``` + +**然而[1, 3]区间在二叉搜索树的中可不是单纯的节点3和左孩子节点0就决定的,还要考虑节点0的右子树**。 + +我们在重新关注一下第二个示例,如图: + +![669.修剪二叉搜索树](https://file1.kamacoder.com/i/algo/20210204155302751.png) + +**所以以上的代码是不可行的!** + +从图中可以看出需要重构二叉树,想想是不是本题就有点复杂了。 + +其实不用重构那么复杂。 + +在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图: + +![669.修剪二叉搜索树1](https://file1.kamacoder.com/i/algo/20210204155327203.png) + + +理解了最关键部分了我们再递归三部曲: + +* 确定递归函数的参数以及返回值 + +这里我们为什么需要返回值呢? + +因为是要遍历整棵树,做修改,其实不需要返回值也可以,我们也可以完成修剪(其实就是从二叉树中移除节点)的操作。 + +但是有返回值,更方便,可以通过递归函数的返回值来移除节点。 + +这样的做法在[二叉树:搜索树中的插入操作](https://programmercarl.com/0701.二叉搜索树中的插入操作.html)和[二叉树:搜索树中的删除操作](https://programmercarl.com/0450.删除二叉搜索树中的节点.html)中大家已经了解过了。 + +代码如下: + +```cpp +TreeNode* trimBST(TreeNode* root, int low, int high) +``` + +* 确定终止条件 + +修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。 + +```cpp +if (root == nullptr ) return nullptr; +``` + +* 确定单层递归的逻辑 + +如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。 + +代码如下: + +```cpp +if (root->val < low) { + TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 + return right; +} +``` + +如果root(当前节点)的元素大于high的,那么应该递归左子树,并返回左子树符合条件的头结点。 + +代码如下: + +```cpp +if (root->val > high) { + TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点 + return left; +} +``` + +接下来要将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right。 + +最后返回root节点,代码如下: + +```cpp +root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子 +root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子 +return root; +``` + +此时大家是不是还没发现这多余的节点究竟是如何从二叉树中移除的呢? + +在回顾一下上面的代码,针对下图中二叉树的情况: + +![669.修剪二叉搜索树1](https://file1.kamacoder.com/i/algo/20210204155327203-20230310120126738.png) + +如下代码相当于把节点0的右孩子(节点2)返回给上一层, + +```cpp +if (root->val < low) { + TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 + return right; +} +``` + +然后如下代码相当于用节点3的左孩子 把下一层返回的 节点0的右孩子(节点2) 接住。 + +``` cpp +root->left = trimBST(root->left, low, high); +``` + +此时节点3的左孩子就变成了节点2,将节点0从二叉树中移除了。 + +最后整体代码如下: + +```CPP +class Solution { +public: + TreeNode* trimBST(TreeNode* root, int low, int high) { + if (root == nullptr ) return nullptr; + if (root->val < low) { + TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点 + return right; + } + if (root->val > high) { + TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点 + return left; + } + root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子 + root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子 + return root; + } +}; +``` + +精简之后代码如下: + +```CPP +class Solution { +public: + TreeNode* trimBST(TreeNode* root, int low, int high) { + if (root == nullptr) return nullptr; + if (root->val < low) return trimBST(root->right, low, high); + if (root->val > high) return trimBST(root->left, low, high); + root->left = trimBST(root->left, low, high); + root->right = trimBST(root->right, low, high); + return root; + } +}; +``` + +只看代码,其实不太好理解节点是如何移除的,这一块大家可以自己再模拟模拟! + +### 迭代法 + +因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。 + +在剪枝的时候,可以分为三步: + +* 将root移动到[L, R] 范围内,注意是左闭右闭区间 +* 剪枝左子树 +* 剪枝右子树 + +代码如下: + +```CPP +class Solution { +public: + TreeNode* trimBST(TreeNode* root, int L, int R) { + if (!root) return nullptr; + + // 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭 + while (root != nullptr && (root->val < L || root->val > R)) { + if (root->val < L) root = root->right; // 小于L往右走 + else root = root->left; // 大于R往左走 + } + TreeNode *cur = root; + // 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况 + while (cur != nullptr) { + while (cur->left && cur->left->val < L) { + cur->left = cur->left->right; + } + cur = cur->left; + } + cur = root; + + // 此时root已经在[L, R] 范围内,处理右孩子大于R的情况 + while (cur != nullptr) { + while (cur->right && cur->right->val > R) { + cur->right = cur->right->left; + } + cur = cur->right; + } + return root; + } +}; +``` + +## 总结 + +修剪二叉搜索树其实并不难,但在递归法中大家可看出我费了很大的功夫来讲解如何删除节点的,这个思路其实是比较绕的。 + +最终的代码倒是很简洁。 + +**如果不对递归有深刻的理解,这道题目还是有难度的!** + +本题我依然给出递归法和迭代法,初学者掌握递归就可以了,如果想进一步学习,就把迭代法也写一写。 + +## 其他语言版本 + + +### Java + +**递归** + +```Java +class Solution { + public TreeNode trimBST(TreeNode root, int low, int high) { + if (root == null) { + return null; + } + if (root.val < low) { + return trimBST(root.right, low, high); + } + if (root.val > high) { + return trimBST(root.left, low, high); + } + // root在[low,high]范围内 + root.left = trimBST(root.left, low, high); + root.right = trimBST(root.right, low, high); + return root; + } +} + +``` + +**迭代** + +```Java +class Solution { + //iteration + public TreeNode trimBST(TreeNode root, int low, int high) { + if(root == null) + return null; + while(root != null && (root.val < low || root.val > high)){ + if(root.val < low) + root = root.right; + else + root = root.left; + } + + TreeNode curr = root; + + //deal with root's left sub-tree, and deal with the value smaller than low. + while(curr != null){ + while(curr.left != null && curr.left.val < low){ + curr.left = curr.left.right; + } + curr = curr.left; + } + //go back to root; + curr = root; + + //deal with root's righg sub-tree, and deal with the value bigger than high. + while(curr != null){ + while(curr.right != null && curr.right.val > high){ + curr.right = curr.right.left; + } + curr = curr.right; + } + return root; + } +} + +```` + +### Python + +递归法(版本一) +```python +class Solution: + def trimBST(self, root: TreeNode, low: int, high: int) -> TreeNode: + if root is None: + return None + if root.val < low: + # 寻找符合区间 [low, high] 的节点 + return self.trimBST(root.right, low, high) + if root.val > high: + # 寻找符合区间 [low, high] 的节点 + return self.trimBST(root.left, low, high) + root.left = self.trimBST(root.left, low, high) # root.left 接入符合条件的左孩子 + root.right = self.trimBST(root.right, low, high) # root.right 接入符合条件的右孩子 + return root + +``` +迭代法 +```python +class Solution: + def trimBST(self, root: TreeNode, L: int, R: int) -> TreeNode: + if not root: + return None + + # 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭 + while root and (root.val < L or root.val > R): + if root.val < L: + root = root.right # 小于L往右走 + else: + root = root.left # 大于R往左走 + + cur = root + + # 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况 + while cur: + while cur.left and cur.left.val < L: + cur.left = cur.left.right + cur = cur.left + + cur = root + + # 此时root已经在[L, R] 范围内,处理右孩子大于R的情况 + while cur: + while cur.right and cur.right.val > R: + cur.right = cur.right.left + cur = cur.right + + return root + +``` + +### Go + +```go +// 递归 +func trimBST(root *TreeNode, low int, high int) *TreeNode { + if root == nil { + return nil + } + if root.Val < low { //如果该节点值小于最小值,则该节点更换为该节点的右节点值,继续遍历 + right := trimBST(root.Right, low, high) + return right + } + if root.Val > high { //如果该节点的值大于最大值,则该节点更换为该节点的左节点值,继续遍历 + left := trimBST(root.Left, low, high) + return left + } + root.Left = trimBST(root.Left, low, high) + root.Right = trimBST(root.Right, low, high) + return root +} + +// 迭代 +func trimBST(root *TreeNode, low int, high int) *TreeNode { + if root == nil { + return nil + } + // 处理 root,让 root 移动到[low, high] 范围内,注意是左闭右闭 + for root != nil && (root.Val < low || root.Val > high) { + if root.Val < low { + root = root.Right + } else { + root = root.Left + } + } + cur := root + // 此时 root 已经在[low, high] 范围内,处理左孩子元素小于 low 的情况(左节点是一定小于 root.Val,因此天然小于 high) + for cur != nil { + for cur.Left != nil && cur.Left.Val < low { + cur.Left = cur.Left.Right + } + cur = cur.Left + } + cur = root + // 此时 root 已经在[low, high] 范围内,处理右孩子大于 high 的情况 + for cur != nil { + for cur.Right != nil && cur.Right.Val > high { + cur.Right = cur.Right.Left + } + cur = cur.Right + } + return root +} +``` + + +### JavaScript + +迭代: + +```js +var trimBST = function(root, low, high) { + if(root === null) { + return null; + } + while(root !== null && (root.val < low || root.val > high)) { + if(root.val < low) { + root = root.right; + }else { + root = root.left; + } + } + let cur = root; + while(cur !== null) { + while(cur.left && cur.left.val < low) { + cur.left = cur.left.right; + } + cur = cur.left; + } + cur = root; + //判断右子树大于high的情况 + while(cur !== null) { + while(cur.right && cur.right.val > high) { + cur.right = cur.right.left; + } + cur = cur.right; + } + return root; +}; +``` + +递归: + +```js +var trimBST = function (root,low,high) { + if(root === null) { + return null; + } + if(root.val < low) { + let right = trimBST(root.right, low, high); + return right; + } + if(root.val > high) { + let left = trimBST(root.left, low, high); + return left; + } + root.left = trimBST(root.left, low, high); + root.right = trimBST(root.right, low, high); + return root; + } +``` + +### TypeScript + +> 递归法 + +```typescript +function trimBST(root: TreeNode | null, low: number, high: number): TreeNode | null { + if (root === null) return null; + if (root.val < low) { + return trimBST(root.right, low, high); + } + if (root.val > high) { + return trimBST(root.left, low, high); + } + root.left = trimBST(root.left, low, high); + root.right = trimBST(root.right, low, high); + return root; +}; +``` + +> 迭代法 + +```typescript +function trimBST(root: TreeNode | null, low: number, high: number): TreeNode | null { + while (root !== null && (root.val < low || root.val > high)) { + if (root.val < low) { + root = root.right; + } else if (root.val > high) { + root = root.left; + } + } + let curNode: TreeNode | null = root; + while (curNode !== null) { + while (curNode.left !== null && curNode.left.val < low) { + curNode.left = curNode.left.right; + } + curNode = curNode.left; + } + curNode = root; + while (curNode !== null) { + while (curNode.right !== null && curNode.right.val > high) { + curNode.right = curNode.right.left; + } + curNode = curNode.right; + } + return root; +}; +``` + +### Scala + +递归法: + +```scala +object Solution { + def trimBST(root: TreeNode, low: Int, high: Int): TreeNode = { + if (root == null) return null + if (root.value < low) return trimBST(root.right, low, high) + if (root.value > high) return trimBST(root.left, low, high) + root.left = trimBST(root.left, low, high) + root.right = trimBST(root.right, low, high) + root + } +} +``` + +### Rust + +// 递归 + +```rust +impl Solution { + pub fn trim_bst( + root: Option>>, + low: i32, + high: i32, + ) -> Option>> { + root.as_ref()?; + let mut node = root.as_ref().unwrap().borrow_mut(); + if node.val < low { + return Self::trim_bst(node.right.clone(), low, high); + } + if node.val > high { + return Self::trim_bst(node.left.clone(), low, high); + } + + node.left = Self::trim_bst(node.left.clone(), low, high); + node.right = Self::trim_bst(node.right.clone(), low, high); + drop(node); + root + } +} +``` +### C# +```csharp +// 递归 +public TreeNode TrimBST(TreeNode root, int low, int high) +{ + if (root == null) return null; + if (root.val < low) + return TrimBST(root.right, low, high); + + if (root.val > high) + return TrimBST(root.left, low, high); + + root.left = TrimBST(root.left, low, high); + root.right = TrimBST(root.right, low, high); + return root; +} +``` + + + diff --git "a/problems/0673.\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227\347\232\204\344\270\252\346\225\260.md" "b/problems/0673.\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227\347\232\204\344\270\252\346\225\260.md" new file mode 100755 index 0000000000..9e61229abb --- /dev/null +++ "b/problems/0673.\346\234\200\351\225\277\351\200\222\345\242\236\345\255\220\345\272\217\345\210\227\347\232\204\344\270\252\346\225\260.md" @@ -0,0 +1,361 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 673.最长递增子序列的个数 + + +[力扣题目链接](https://leetcode.cn/problems/number-of-longest-increasing-subsequence/) + + +给定一个未排序的整数数组,找到最长递增子序列的个数。 + +示例 1: +* 输入: [1,3,5,4,7] +* 输出: 2 +* 解释: 有两个最长递增子序列,分别是 [1, 3, 4, 7] 和[1, 3, 5, 7]。 + +示例 2: +* 输入: [2,2,2,2,2] +* 输出: 5 +* 解释: 最长递增子序列的长度是1,并且存在5个子序列的长度为1,因此输出5。 + + +## 思路 + +这道题可以说是 [300.最长上升子序列](https://programmercarl.com/0300.最长上升子序列.html) 的进阶版本 + +1. 确定dp数组(dp table)以及下标的含义 + +这道题目我们要一起维护两个数组。 + +dp[i]:i之前(包括i)最长递增子序列的长度为dp[i] + +count[i]:以nums[i]为结尾的字符串,最长递增子序列的个数为count[i] + +2. 确定递推公式 + +在300.最长上升子序列 中,我们给出的状态转移是: + +if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); + +即:位置i的最长递增子序列长度 等于j从0到i-1各个位置的最长升序子序列 + 1的最大值。 + +本题就没那么简单了,我们要考虑两个维度,一个是dp[i]的更新,一个是count[i]的更新。 + +那么如何更新count[i]呢? + +以nums[i]为结尾的字符串,最长递增子序列的个数为count[i]。 + +那么在nums[i] > nums[j]前提下,如果在[0, i-1]的范围内,找到了j,使得dp[j] + 1 > dp[i],说明找到了一个更长的递增子序列。 + +那么以j为结尾的子串的最长递增子序列的个数,就是最新的以i为结尾的子串的最长递增子序列的个数,即:count[i] = count[j]。 + +在nums[i] > nums[j]前提下,如果在[0, i-1]的范围内,找到了j,使得dp[j] + 1 == dp[i],说明找到了两个相同长度的递增子序列。 + +那么以i为结尾的子串的最长递增子序列的个数 就应该加上以j为结尾的子串的最长递增子序列的个数,即:count[i] += count[j]; + +代码如下: + +```CPP +if (nums[i] > nums[j]) { + if (dp[j] + 1 > dp[i]) { + count[i] = count[j]; + } else if (dp[j] + 1 == dp[i]) { + count[i] += count[j]; + } + dp[i] = max(dp[i], dp[j] + 1); +} +``` + +当然也可以这么写: + +```CPP +if (nums[i] > nums[j]) { + if (dp[j] + 1 > dp[i]) { + dp[i] = dp[j] + 1; // 更新dp[i]放在这里,就不用max了 + count[i] = count[j]; + } else if (dp[j] + 1 == dp[i]) { + count[i] += count[j]; + } +} +``` + +这里count[i]记录了以nums[i]为结尾的字符串,最长递增子序列的个数。dp[i]记录了i之前(包括i)最长递增序列的长度。 + +题目要求最长递增序列的长度的个数,我们应该把最长长度记录下来。 + +代码如下: + +```CPP +for (int i = 1; i < nums.size(); i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + if (dp[j] + 1 > dp[i]) { + count[i] = count[j]; + } else if (dp[j] + 1 == dp[i]) { + count[i] += count[j]; + } + dp[i] = max(dp[i], dp[j] + 1); + } + if (dp[i] > maxCount) maxCount = dp[i]; // 记录最长长度 + } +} +``` + +3. dp数组如何初始化 + +再回顾一下dp[i]和count[i]的定义 + +count[i]记录了以nums[i]为结尾的字符串,最长递增子序列的个数。 + +那么最少也就是1个,所以count[i]初始为1。 + +dp[i]记录了i之前(包括i)最长递增序列的长度。 + +最小的长度也是1,所以dp[i]初始为1。 + +代码如下: + +``` +vector dp(nums.size(), 1); +vector count(nums.size(), 1); +``` + +**其实动规的题目中,初始化很有讲究,也很考察对dp数组定义的理解**。 + +4. 确定遍历顺序 + +dp[i] 是由0到i-1各个位置的最长升序子序列 推导而来,那么遍历i一定是从前向后遍历。 + +j其实就是0到i-1,遍历i的循环里外层,遍历j则在内层,代码如下: + +```CPP +for (int i = 1; i < nums.size(); i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + if (dp[j] + 1 > dp[i]) { + count[i] = count[j]; + } else if (dp[j] + 1 == dp[i]) { + count[i] += count[j]; + } + dp[i] = max(dp[i], dp[j] + 1); + } + if (dp[i] > maxCount) maxCount = dp[i]; + } +} +``` + + +最后还有再遍历一遍dp[i],把最长递增序列长度对应的count[i]累计下来就是结果了。 + +代码如下: + +```CPP +for (int i = 1; i < nums.size(); i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + if (dp[j] + 1 > dp[i]) { + count[i] = count[j]; + } else if (dp[j] + 1 == dp[i]) { + count[i] += count[j]; + } + dp[i] = max(dp[i], dp[j] + 1); + } + if (dp[i] > maxCount) maxCount = dp[i]; + } +} +int result = 0; // 统计结果 +for (int i = 0; i < nums.size(); i++) { + if (maxCount == dp[i]) result += count[i]; +} +``` + +统计结果,可能有的同学又有点看懵了,那么就再回顾一下dp[i]和count[i]的定义。 + +5. 举例推导dp数组 + +输入:[1,3,5,4,7] + +![673.最长递增子序列的个数](https://file1.kamacoder.com/i/algo/20230310000656.png) + +**如果代码写出来了,怎么改都通过不了,那么把dp和count打印出来看看对不对!** + +以上分析完毕,C++整体代码如下: + +```CPP +class Solution { +public: + int findNumberOfLIS(vector& nums) { + if (nums.size() <= 1) return nums.size(); + vector dp(nums.size(), 1); + vector count(nums.size(), 1); + int maxCount = 0; + for (int i = 1; i < nums.size(); i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + if (dp[j] + 1 > dp[i]) { + dp[i] = dp[j] + 1; + count[i] = count[j]; + } else if (dp[j] + 1 == dp[i]) { + count[i] += count[j]; + } + } + if (dp[i] > maxCount) maxCount = dp[i]; + } + } + int result = 0; + for (int i = 0; i < nums.size(); i++) { + if (maxCount == dp[i]) result += count[i]; + } + return result; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n) + +还有O(nlog n)的解法,使用树状数组,今天有点忙就先不写了,感兴趣的同学可以自行学习一下,这里有我之前写的树状数组系列博客:https://blog.csdn.net/youngyangyang04/category_871105.html (十年前的陈年老文了) + +## 其他语言版本 + +### Java + +```java +class Solution { + public int findNumberOfLIS(int[] nums) { + if (nums.length <= 1) return nums.length; + int[] dp = new int[nums.length]; + for(int i = 0; i < dp.length; i++) dp[i] = 1; + int[] count = new int[nums.length]; + for(int i = 0; i < count.length; i++) count[i] = 1; + + int maxCount = 0; + for (int i = 1; i < nums.length; i++) { + for (int j = 0; j < i; j++) { + if (nums[i] > nums[j]) { + if (dp[j] + 1 > dp[i]) { + dp[i] = dp[j] + 1; + count[i] = count[j]; + } else if (dp[j] + 1 == dp[i]) { + count[i] += count[j]; + } + } + if (dp[i] > maxCount) maxCount = dp[i]; + } + } + int result = 0; + for (int i = 0; i < nums.length; i++) { + if (maxCount == dp[i]) result += count[i]; + } + return result; + } +} +``` + +### Python + +```python +class Solution: + def findNumberOfLIS(self, nums: List[int]) -> int: + size = len(nums) + if size<= 1: return size + + dp = [1 for i in range(size)] + count = [1 for i in range(size)] + + maxCount = 0 + for i in range(1, size): + for j in range(i): + if nums[i] > nums[j]: + if dp[j] + 1 > dp[i] : + dp[i] = dp[j] + 1 + count[i] = count[j] + elif dp[j] + 1 == dp[i] : + count[i] += count[j] + if dp[i] > maxCount: + maxCount = dp[i]; + result = 0 + for i in range(size): + if maxCount == dp[i]: + result += count[i] + return result; +``` + +### Go + +```go + +func findNumberOfLIS(nums []int) int { + size := len(nums) + if size <= 1 { + return size + } + + dp := make([]int, size); + for i, _ := range dp { + dp[i] = 1 + } + count := make([]int, size); + for i, _ := range count { + count[i] = 1 + } + + maxCount := 0 + for i := 1; i < size; i++ { + for j := 0; j < i; j++ { + if nums[i] > nums[j] { + if dp[j] + 1 > dp[i] { + dp[i] = dp[j] + 1 + count[i] = count[j] + } else if dp[j] + 1 == dp[i] { + count[i] += count[j] + } + } + if dp[i] > maxCount { + maxCount = dp[i] + } + } + } + + result := 0 + for i := 0; i < size; i++ { + if maxCount == dp[i] { + result += count[i] + } + } + return result +} +``` + +### JavaScript + +```js +var findNumberOfLIS = function(nums) { + const len = nums.length; + if(len <= 1) return len; + let dp = new Array(len).fill(1); // i之前(包括i)最长递增子序列的长度为dp[i] + let count = new Array(len).fill(1); // 以nums[i]为结尾的字符串,最长递增子序列的个数为count[i] + let res = 0; + for(let i = 1; i < len; i++){ + for(let j = 0; j < i; j++){ + if(nums[i] > nums[j]){ + if(dp[j] + 1 > dp[i]){ // 第 j 个数字为前一个数字的子序列是否更更长 + dp[i] = dp[j] + 1; //更新 dp[i] + count[i] = count[j]; // 重置count[i] + } else if(dp[j] + 1 === dp[i]){ // 和原来一样长 + count[i] += count[j]; //更新count[i] + } + } + } + } + let max = Math.max(...dp); //扩展运算符找到最大长度 + for(let i = 0; i < len; i++) if(dp[i] === max) res += count[i]; // 累加 + return res; +}; +``` + + + diff --git "a/problems/0674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" "b/problems/0674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..dae64a11ac --- /dev/null +++ "b/problems/0674.\346\234\200\351\225\277\350\277\236\347\273\255\351\200\222\345\242\236\345\272\217\345\210\227.md" @@ -0,0 +1,514 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 674. 最长连续递增序列 + +[力扣题目链接](https://leetcode.cn/problems/longest-continuous-increasing-subsequence/) + +给定一个未经排序的整数数组,找到最长且 连续递增的子序列,并返回该序列的长度。 + +连续递增的子序列 可以由两个下标 l 和 r(l < r)确定,如果对于每个 l <= i < r,都有 nums[i] < nums[i + 1] ,那么子序列 [nums[l], nums[l + 1], ..., nums[r - 1], nums[r]] 就是连续递增子序列。 + +示例 1: +* 输入:nums = [1,3,5,4,7] +* 输出:3 +* 解释:最长连续递增序列是 [1,3,5], 长度为3。尽管 [1,3,5,7] 也是升序的子序列, 但它不是连续的,因为 5 和 7 在原数组里被 4 隔开。 + +示例 2: +* 输入:nums = [2,2,2,2,2] +* 输出:1 +* 解释:最长连续递增序列是 [2], 长度为1。 + +提示: + +* 0 <= nums.length <= 10^4 +* -10^9 <= nums[i] <= 10^9 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划之子序列问题,重点在于连续!| LeetCode:674.最长连续递增序列](https://www.bilibili.com/video/BV1bD4y1778v),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题相对于昨天的[动态规划:300.最长递增子序列](https://programmercarl.com/0300.最长上升子序列.html)最大的区别在于“连续”。 + +本题要求的是最长**连续**递增序列 + +### 动态规划 + +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i]:以下标i为结尾的连续递增的子序列长度为dp[i]**。 + +注意这里的定义,一定是以下标i为结尾,并不是说一定以下标0为起始位置。 + +2. 确定递推公式 + +如果 nums[i] > nums[i - 1],那么以 i 为结尾的连续递增的子序列长度 一定等于 以i - 1为结尾的连续递增的子序列长度 + 1 。 + +即:dp[i] = dp[i - 1] + 1; + +**注意这里就体现出和[动态规划:300.最长递增子序列](https://programmercarl.com/0300.最长上升子序列.html)的区别!** + +因为本题要求连续递增子序列,所以就只要比较nums[i]与nums[i - 1],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。 + +既然不用j了,那么也不用两层for循环,本题一层for循环就行,比较nums[i] 和 nums[i - 1]。 + +这里大家要好好体会一下! + +3. dp数组如何初始化 + +以下标i为结尾的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素。 + +所以dp[i]应该初始1; + +4. 确定遍历顺序 + +从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。 + +本文在确定递推公式的时候也说明了为什么本题只需要一层for循环,代码如下: + +```CPP +for (int i = 1; i < nums.size(); i++) { + if (nums[i] > nums[i - 1]) { // 连续记录 + dp[i] = dp[i - 1] + 1; + } +} +``` + +5. 举例推导dp数组 + +已输入nums = [1,3,5,4,7]为例,dp数组状态如下: + + +![674.最长连续递增序列](https://file1.kamacoder.com/i/algo/20210204103529742.jpg) + +**注意这里要取dp[i]里的最大值,所以dp[2]才是结果!** + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int findLengthOfLCIS(vector& nums) { + if (nums.size() == 0) return 0; + int result = 1; + vector dp(nums.size() ,1); + for (int i = 1; i < nums.size(); i++) { + if (nums[i] > nums[i - 1]) { // 连续记录 + dp[i] = dp[i - 1] + 1; + } + if (dp[i] > result) result = dp[i]; + } + return result; + } +}; + +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +### 贪心 + +这道题目也可以用贪心来做,也就是遇到nums[i] > nums[i - 1]的情况,count就++,否则count为1,记录count的最大值就可以了。 + +代码如下: + +```CPP +class Solution { +public: + int findLengthOfLCIS(vector& nums) { + if (nums.size() == 0) return 0; + int result = 1; // 连续子序列最少也是1 + int count = 1; + for (int i = 1; i < nums.size(); i++) { + if (nums[i] > nums[i - 1]) { // 连续记录 + count++; + } else { // 不连续,count从头开始 + count = 1; + } + if (count > result) result = count; + } + return result; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +## 总结 + +本题也是动规里子序列问题的经典题目,但也可以用贪心来做,大家也会发现贪心好像更简单一点,而且空间复杂度仅是O(1)。 + +在动规分析中,关键是要理解和[动态规划:300.最长递增子序列](https://programmercarl.com/0300.最长上升子序列.html)的区别。 + +**要联动起来,才能理解递增子序列怎么求,递增连续子序列又要怎么求**。 + +概括来说:不连续递增子序列的跟前0-i 个状态有关,连续递增的子序列只跟前一个状态有关 + +本篇我也把区别所在之处重点介绍了,关键在递推公式和遍历方法上,大家可以仔细体会一波! + +## 其他语言版本 + +### Java: + +> 动态规划: +```java + /** + * 1.dp[i] 代表当前下标最大连续值 + * 2.递推公式 if(nums[i+1]>nums[i]) dp[i+1] = dp[i]+1 + * 3.初始化 都为1 + * 4.遍历方向,从其那往后 + * 5.结果推导 。。。。 + * @param nums + * @return + */ + public static int findLengthOfLCIS(int[] nums) { + int[] dp = new int[nums.length]; + for (int i = 0; i < dp.length; i++) { + dp[i] = 1; + } + int res = 1; + //可以注意到,這邊的 i 是從 0 開始,所以會出現和卡哥的C++ code有差異的地方,在一些地方會看到有 i + 1 的偏移。 + for (int i = 0; i < nums.length - 1; i++) { + if (nums[i + 1] > nums[i]) { + dp[i + 1] = dp[i] + 1; + } + res = res > dp[i + 1] ? res : dp[i + 1]; + } + return res; + } +``` +> 动态规划状态压缩 +```java +class Solution { + public int findLengthOfLCIS(int[] nums) { + // 记录以 前一个元素结尾的最长连续递增序列的长度 和 以当前 结尾的...... + int beforeOneMaxLen = 1, currentMaxLen = 0; + // res 赋最小值返回的最小值1 + int res = 1; + for (int i = 1; i < nums.length; i ++) { + currentMaxLen = nums[i] > nums[i - 1] ? beforeOneMaxLen + 1 : 1; + beforeOneMaxLen = currentMaxLen; + res = Math.max(res, currentMaxLen); + } + return res; + } +} +``` +> 贪心法: + +```Java +public static int findLengthOfLCIS(int[] nums) { + if (nums.length == 0) return 0; + int res = 1; // 连续子序列最少也是1 + int count = 1; + for (int i = 0; i < nums.length - 1; i++) { + if (nums[i + 1] > nums[i]) { // 连续记录 + count++; + } else { // 不连续,count从头开始 + count = 1; + } + if (count > res) res = count; + } + return res; +} +``` + +### Python: + +DP +```python +class Solution: + def findLengthOfLCIS(self, nums: List[int]) -> int: + if len(nums) == 0: + return 0 + result = 1 + dp = [1] * len(nums) + for i in range(len(nums)-1): + if nums[i+1] > nums[i]: #连续记录 + dp[i+1] = dp[i] + 1 + result = max(result, dp[i+1]) + return result +``` + +DP(优化版) +```python +class Solution: + def findLengthOfLCIS(self, nums: List[int]) -> int: + if not nums: + return 0 + + max_length = 1 + current_length = 1 + + for i in range(1, len(nums)): + if nums[i] > nums[i - 1]: + current_length += 1 + max_length = max(max_length, current_length) + else: + current_length = 1 + + return max_length + +``` +贪心 +```python +class Solution: + def findLengthOfLCIS(self, nums: List[int]) -> int: + if len(nums) == 0: + return 0 + result = 1 #连续子序列最少也是1 + count = 1 + for i in range(len(nums)-1): + if nums[i+1] > nums[i]: #连续记录 + count += 1 + else: #不连续,count从头开始 + count = 1 + result = max(result, count) + return result +``` + +### Go: + +> 动态规划: +```go +func findLengthOfLCIS(nums []int) int { + if len(nums) == 0 {return 0} + res, count := 1, 1 + for i := 0; i < len(nums)-1; i++ { + if nums[i+1] > nums[i] { + count++ + }else { + count = 1 + } + if count > res { + res = count + } + } + return res +} +``` + +> 贪心算法: +```go +func findLengthOfLCIS(nums []int) int { + if len(nums) == 0 {return 0} + dp := make([]int, len(nums)) + for i := 0; i < len(dp); i++ { + dp[i] = 1 + } + res := 1 + for i := 0; i < len(nums)-1; i++ { + if nums[i+1] > nums[i] { + dp[i+1] = dp[i] + 1 + } + if dp[i+1] > res { + res = dp[i+1] + } + } + return res +} +``` + + +### Rust: +>动态规划 +```rust +pub fn find_length_of_lcis(nums: Vec) -> i32 { + if nums.is_empty() { + return 0; + } + let mut result = 1; + let mut dp = vec![1; nums.len()]; + for i in 1..nums.len() { + if nums[i - 1] < nums[i] { + dp[i] = dp[i - 1] + 1; + result = result.max(dp[i]); + } + } + result +} +``` + + +> 贪心 + +```rust +impl Solution { + pub fn find_length_of_lcis(nums: Vec) -> i32 { + let (mut res, mut count) = (1, 1); + for i in 1..nums.len() { + if nums[i] > nums[i - 1] { + count += 1; + res = res.max(count); + continue; + } + count = 1; + } + res + } +} +``` + + +### JavaScript: + +> 动态规划: +```javascript +const findLengthOfLCIS = (nums) => { + let dp = new Array(nums.length).fill(1); + + + for(let i = 0; i < nums.length - 1; i++) { + if(nums[i+1] > nums[i]) { + dp[i+1] = dp[i]+ 1; + } + } + + return Math.max(...dp); +}; +``` + +> 贪心法: +```javascript +const findLengthOfLCIS = (nums) => { + if(nums.length === 1) { + return 1; + } + + let maxLen = 1; + let curMax = 1; + let cur = nums[0]; + + for(let num of nums) { + if(num > cur) { + curMax += 1; + maxLen = Math.max(maxLen, curMax); + } else { + curMax = 1; + } + cur = num; + } + + return maxLen; +}; +``` + +### TypeScript: + +> 动态规划: + +```typescript +function findLengthOfLCIS(nums: number[]): number { + /** + dp[i]: 前i个元素,以nums[i]结尾,最长连续子序列的长度 + */ + const dp: number[] = new Array(nums.length).fill(1); + let resMax: number = 1; + for (let i = 1, length = nums.length; i < length; i++) { + if (nums[i] > nums[i - 1]) { + dp[i] = dp[i - 1] + 1; + } + resMax = Math.max(resMax, dp[i]); + } + return resMax; +}; +``` + +> 贪心: + +```typescript +function findLengthOfLCIS(nums: number[]): number { + let resMax: number = 1; + let count: number = 1; + for (let i = 0, length = nums.length; i < length - 1; i++) { + if (nums[i] < nums[i + 1]) { + count++; + } else { + count = 1; + } + resMax = Math.max(resMax, count); + } + return resMax; +}; +``` + +### C: + +> 动态规划: + +```c +int findLengthOfLCIS(int* nums, int numsSize) { + if(numsSize == 0){ + return 0; + } + int dp[numsSize]; + for(int i = 0; i < numsSize; i++){ + dp[i] = 1; + } + int result = 1; + for (int i = 1; i < numsSize; ++i) { + if(nums[i] > nums[i - 1]){ + dp[i] = dp[i - 1] + 1; + } + if(dp[i] > result){ + result = dp[i]; + } + } + return result; +} +``` + + + +> 贪心: + +```c +int findLengthOfLCIS(int* nums, int numsSize) { + int result = 1; + int count = 1; + if(numsSize == 0){ + return result; + } + for (int i = 1; i < numsSize; ++i) { + if(nums[i] > nums[i - 1]){ + count++; + } else{ + count = 1; + } + if(count > result){ + result = count; + } + } + return result; +} +``` + +### Cangjie + +```cangjie +func findLengthOfLCIS(nums: Array): Int64 { + let n = nums.size + if (n <= 1) { + return n + } + let dp = Array(n, repeat: 1) + var res = 0 + for (i in 1..n) { + if (nums[i] > nums[i - 1]) { + dp[i] = dp[i - 1] + 1 + } + res = max(res, dp[i]) + } + return res +} +``` + + + diff --git "a/problems/0684.\345\206\227\344\275\231\350\277\236\346\216\245.md" "b/problems/0684.\345\206\227\344\275\231\350\277\236\346\216\245.md" new file mode 100755 index 0000000000..2f939d0827 --- /dev/null +++ "b/problems/0684.\345\206\227\344\275\231\350\277\236\346\216\245.md" @@ -0,0 +1,379 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 684.冗余连接 + +[力扣题目链接](https://leetcode.cn/problems/redundant-connection/) + +树可以看成是一个连通且 无环 的 无向 图。 + +给定往一棵 n 个节点 (节点值 1~n) 的树中添加一条边后的图。添加的边的两个顶点包含在 1 到 n 中间,且这条附加的边不属于树中已存在的边。图的信息记录于长度为 n 的二维数组 edges ,edges[i] = [ai, bi] 表示图中在 ai 和 bi 之间存在一条边。 + +请找出一条可以删去的边,删除后可使得剩余部分是一个有着 n 个节点的树。如果有多个答案,则返回数组 edges 中最后出现的边。 + +![](https://file1.kamacoder.com/i/algo/20210727150215.png) + +提示: +* n == edges.length +* 3 <= n <= 1000 +* edges[i].length == 2 +* 1 <= ai < bi <= edges.length +* ai != bi +* edges 中无重复元素 +* 给定的图是连通的  + +## 思路 + +这道题目也是并查集基础题目。 + +首先要知道并查集可以解决什么问题呢? + +主要就是集合问题,两个节点在不在一个集合,也可以将两个节点添加到一个集合中。 + +这里整理出我的并查集模板如下: + +```CPP +int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 +vector father = vector (n, 0); // C++里的一种数组结构 + +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} + +``` + +以上模板 只要修改 n 就可以了,本题 节点数量不会超过1000。 + +并查集主要有三个功能。 + +1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个 +2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上 +3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点 + +如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/kamacoder/图论并查集理论基础.html) + +我们再来看一下这道题目。 + +题目说是无向图,返回一条可以删去的边,使得结果图是一个有着N个节点的树(即:只有一个根节点)。 + +如果有多个答案,则返回二维数组中最后出现的边。 + +那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。 + +如图所示: + +![](https://file1.kamacoder.com/i/algo/20230604104720.png) + +节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。 + +(如果题目中说:如果有多个答案,则返回二维数组中最前出现的边。 那我们就要 从后向前遍历每一条边了) + +如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。 + +如图所示: + +![](https://file1.kamacoder.com/i/algo/20230604104330.png) + +已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。 + +这个思路清晰之后,代码就很好写了。 + +并查集C++代码如下: + +```CPP +class Solution { +private: + int n = 1005; // 节点数量3 到 1000 + vector father = vector (n, 0); // C++里的一种数组结构 + + // 并查集初始化 + void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } + } + // 并查集里寻根的过程 + int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); + } + // 判断 u 和 v是否找到同一个根 + bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; + } + // 将v->u 这条边加入并查集 + void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +public: + vector findRedundantConnection(vector>& edges) { + init(); + for (int i = 0; i < edges.size(); i++) { + if (isSame(edges[i][0], edges[i][1])) return edges[i]; + else join(edges[i][0], edges[i][1]); + } + return {}; + } +}; + +``` + +可以看出,主函数的代码很少,就判断一下边的两个节点在不在同一个集合就可以了。 + + +## 其他语言版本 + +### Java + +```java +class Solution { + private int n; // 节点数量3 到 1000 + private int[] father; + public Solution() { + n = 1005; + father = new int[n]; + + // 并查集初始化 + for (int i = 0; i < n; ++i) { + father[i] = i; + } + } + + // 并查集里寻根的过程 + private int find(int u) { + if(u == father[u]) { + return u; + } + father[u] = find(father[u]); + return father[u]; + } + + // 将v->u 这条边加入并查集 + private void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return ; + father[v] = u; + } + + // 判断 u 和 v是否找到同一个根,本题用不上 + private Boolean same(int u, int v) { + u = find(u); + v = find(v); + return u == v; + } + + public int[] findRedundantConnection(int[][] edges) { + for (int i = 0; i < edges.length; i++) { + if (same(edges[i][0], edges[i][1])) { + return edges[i]; + } else { + join(edges[i][0], edges[i][1]); + } + } + return null; + } +} +``` + +### Python + +```python + +class Solution: + + def __init__(self): + """ + 初始化 + """ + self.n = 1005 + self.father = [i for i in range(self.n)] + + + def find(self, u): + """ + 并查集里寻根的过程 + """ + if u == self.father[u]: + return u + self.father[u] = self.find(self.father[u]) + return self.father[u] + + def join(self, u, v): + """ + 将v->u 这条边加入并查集 + """ + u = self.find(u) + v = self.find(v) + if u == v : return + self.father[v] = u + pass + + + def same(self, u, v ): + """ + 判断 u 和 v是否找到同一个根,本题用不上 + """ + u = self.find(u) + v = self.find(v) + return u == v + + def findRedundantConnection(self, edges: List[List[int]]) -> List[int]: + for i in range(len(edges)): + if self.same(edges[i][0], edges[i][1]) : + return edges[i] + else : + self.join(edges[i][0], edges[i][1]) + return [] +``` + +### Python简洁写法: + +```python +class Solution: + def findRedundantConnection(self, edges: List[List[int]]) -> List[int]: + n = len(edges) + p = [i for i in range(n+1)] + def find(i): + if p[i] != i: + p[i] = find(p[i]) + return p[i] + for u, v in edges: + if p[find(u)] == find(v): + return [u, v] + p[find(u)] = find(v) +``` + +### Go + +```go + +// 全局变量 +var ( + n = 1005 // 节点数量3 到 1000 + father = make([]int, 1005) +) + +// 并查集初始化 +func initialize() { + for i := 0; i < n; i++ { + father[i] = i + } +} + +// 并查集里寻根的过程 +func find(u int) int { + if u == father[u] { + return u + } + father[u] = find(father[u]) + return father[u] +} + +// 将v->u 这条边加入并查集 +func join(u, v int) { + u = find(u) + v = find(v) + if u == v { + return + } + father[v] = u +} + +// 判断 u 和 v是否找到同一个根,本题用不上 +func same(u, v int) bool { + u = find(u) + v = find(v) + return u == v +} + +func findRedundantConnection(edges [][]int) []int { + initialize() + for i := 0; i < len(edges); i++ { + if same(edges[i][0], edges[i][1]) { + return edges[i] + } else { + join(edges[i][0], edges[i][1]) + } + } + return []int{} +} +``` + +### JavaScript + +```js +const n = 1005; +const father = new Array(n); +// 并查集里寻根的过程 +const find = u => { + return u == father[u] ? u : father[u] = find(father[u]); +}; + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u); + v = find(v); + if(u == v) return; + father[v] = u; +}; + +// 判断 u 和 v是否找到同一个根,本题用不上 +const same = (u, v) => { + u = find(u); + v = find(v); + return u == v; +}; + +/** + * @param {number[][]} edges + * @return {number[]} + */ +var findRedundantConnection = function(edges) { + // 并查集初始化 + for(let i = 0; i < n; i++){ + father[i] = i; + } + for(let i = 0; i < edges.length; i++){ + if(same(edges[i][0], edges[i][1])) return edges[i]; + else join(edges[i][0], edges[i][1]); + } + return null; +}; +``` + + + + + + + + + diff --git "a/problems/0685.\345\206\227\344\275\231\350\277\236\346\216\245II.md" "b/problems/0685.\345\206\227\344\275\231\350\277\236\346\216\245II.md" new file mode 100755 index 0000000000..27161d174c --- /dev/null +++ "b/problems/0685.\345\206\227\344\275\231\350\277\236\346\216\245II.md" @@ -0,0 +1,620 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 685.冗余连接II + +[力扣题目链接](https://leetcode.cn/problems/redundant-connection-ii/) + +在本问题中,有根树指满足以下条件的 有向 图。该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。 + +输入一个有向图,该图由一个有着 n 个节点(节点值不重复,从 1 到 n)的树及一条附加的有向边构成。附加的边包含在 1 到 n 中的两个不同顶点间,这条附加的边不属于树中已存在的边。 + +结果图是一个以边组成的二维数组 edges 。 每个元素是一对 [ui, vi],用以表示 有向 图中连接顶点 ui 和顶点 vi 的边,其中 ui 是 vi 的一个父节点。 + +返回一条能删除的边,使得剩下的图是有 n 个节点的有根树。若有多个答案,返回最后出现在给定二维数组的答案。 + + +![](https://file1.kamacoder.com/i/algo/20210727151057.png) + +![](https://file1.kamacoder.com/i/algo/20210727151118.png) + +提示: + +* n == edges.length +* 3 <= n <= 1000 +* edges[i].length == 2 +* 1 <= ui, vi <= n + +## 思路 + +先重点读懂题目中的这句**该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。** + +**这说明题目中的图原本是是一棵树,只不过在不增加节点的情况下多加了一条边!** + +还有**若有多个答案,返回最后出现在给定二维数组的答案。**这说明在两条边都可以删除的情况下,要删顺序靠后的! + + +那么有如下三种情况,前两种情况是出现入度为2的点,如图: + + + +且只有一个节点入度为2,为什么不看出度呢,出度没有意义,一棵树中随便一个父节点就有多个出度。 + +第三种情况是没有入度为2的点,那么图中一定出现了有向环(**注意这里强调是有向环!**) + +如图: + + + + +首先先计算节点的入度,这里不少录友在计算入度的时候就搞蒙了,分不清 edges[i][j] 表示的都是什么。 + +例如题目示例一给的是:edges = [[1,2],[1,3],[2,3]] + +那大家很自然就想 对应二维数组的数值是: edges[1][2] ,edges[1][3],edges[2][3],但又想不出来 edges[1][2] 数值又是什么呢? 越想约懵。 + +其实 edges = [[1,2],[1,3],[2,3]],表示的是 + +edges[0][0] = 1,edges[0][1] = 2, + +edges[1][0] = 1,edges[1][1] = 3, + +edges[2][0] = 2,edges[2][1] = 3, + +二维数组大家都学过,但是往往和图结合在一起的时候,就非常容易搞混,哪里是数组,哪里是下标了。 + +搞清楚之后,我们如何统计入度呢? + +即 edges[i][1] 表示的节点都是 箭头指向的节点,即这个节点有一个入度! (如果想统计出度,那么就是 edges[i][0])。 + +所以,统计入度的代码如下: + +```cpp +int inDegree[N] = {0}; // 记录节点入度 +n = edges.size(); // 边的数量 +for (int i = 0; i < n; i++) { + inDegree[edges[i][1]]++; // 统计入度 +} +``` + +前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条,如果删了一条,判断这个图是一个树,那么这条边就是答案,同时注意要从后向前遍历,因为如果两条边删哪一条都可以成为树,就删最后那一条。 + +代码如下: + +```cpp +vector vec; // 记录入度为2的边(如果有的话就两条边) +// 找入度为2的节点所对应的边,注意要倒序,因为优先返回最后出现在二维数组中的答案 +for (int i = n - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push_back(i); + } +} +// 处理图中情况1 和 情况2 +// 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 +if (vec.size() > 0) { + if (isTreeAfterRemoveEdge(edges, vec[0])) { + return edges[vec[0]]; + } else { + return edges[vec[1]]; + } +} +``` + +在来看情况三,明确没有入度为2的情况,那么一定有向环,找到构成环的边就是要删除的边。 + +可以定义一个函数,代码如下: + +```cpp +// 在有向图里找到删除的那条边,使其变成树,返回值就是要删除的边 +vector getRemoveEdge(const vector>& edges) +``` + +大家应该知道了,我们要实现两个最为关键的函数: + +* `isTreeAfterRemoveEdge()` 判断删一个边之后是不是树了 +* `getRemoveEdge` 确定图中一定有了有向环,那么要找到需要删除的那条边 + +此时应该是用到**并查集**了,并查集为什么可以判断 一个图是不是树呢? + +**因为如果两个点所在的边在添加图之前如果就可以在并查集里找到了相同的根,那么这条边添加上之后 这个图一定不是树了** + +如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/图论并查集理论基础.html) + +本题C++代码如下:(详细注释了) + + +```cpp +class Solution { +private: + static const int N = 1010; // 如题:二维数组大小的在3到1000范围内 + int father[N]; + int n; // 边的数量 + // 并查集初始化 + void init() { + for (int i = 1; i <= n; ++i) { + father[i] = i; + } + } + // 并查集里寻根的过程 + int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); + } + // 将v->u 这条边加入并查集 + void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return ; + father[v] = u; + } + // 判断 u 和 v是否找到同一个根 + bool same(int u, int v) { + u = find(u); + v = find(v); + return u == v; + } + // 在有向图里找到删除的那条边,使其变成树 + vector getRemoveEdge(const vector>& edges) { + init(); // 初始化并查集 + for (int i = 0; i < n; i++) { // 遍历所有的边 + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 + return edges[i]; + } + join(edges[i][0], edges[i][1]); + } + return {}; + } + + // 删一条边之后判断是不是树 + bool isTreeAfterRemoveEdge(const vector>& edges, int deleteEdge) { + init(); // 初始化并查集 + for (int i = 0; i < n; i++) { + if (i == deleteEdge) continue; + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 + return false; + } + join(edges[i][0], edges[i][1]); + } + return true; + } +public: + + vector findRedundantDirectedConnection(vector>& edges) { + int inDegree[N] = {0}; // 记录节点入度 + n = edges.size(); // 边的数量 + for (int i = 0; i < n; i++) { + inDegree[edges[i][1]]++; // 统计入度 + } + vector vec; // 记录入度为2的边(如果有的话就两条边) + // 找入度为2的节点所对应的边,注意要倒序,因为优先返回最后出现在二维数组中的答案 + for (int i = n - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push_back(i); + } + } + // 处理图中情况1 和 情况2 + // 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 + if (vec.size() > 0) { + if (isTreeAfterRemoveEdge(edges, vec[0])) { + return edges[vec[0]]; + } else { + return edges[vec[1]]; + } + } + // 处理图中情况3 + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + return getRemoveEdge(edges); + + } +}; +``` + + +## 其他语言版本 + +### Java + +```java + +class Solution { + + private static final int N = 1010; // 如题:二维数组大小的在3到1000范围内 + private int[] father; + public Solution() { + father = new int[N]; + + // 并查集初始化 + for (int i = 0; i < N; ++i) { + father[i] = i; + } + } + + // 并查集里寻根的过程 + private int find(int u) { + if(u == father[u]) { + return u; + } + father[u] = find(father[u]); + return father[u]; + } + + // 将v->u 这条边加入并查集 + private void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return ; + father[v] = u; + } + + // 判断 u 和 v是否找到同一个根,本题用不上 + private Boolean same(int u, int v) { + u = find(u); + v = find(v); + return u == v; + } + + /** + * 初始化并查集 + */ + private void initFather() { + // 并查集初始化 + for (int i = 0; i < N; ++i) { + father[i] = i; + } + } + + /** + * 在有向图里找到删除的那条边,使其变成树 + * @param edges + * @return 要删除的边 + */ + private int[] getRemoveEdge(int[][] edges) { + initFather(); + for(int i = 0; i < edges.length; i++) { + if(same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 + return edges[i]; + } + join(edges[i][0], edges[i][1]); + } + return null; + } + + /** + * 删一条边之后判断是不是树 + * @param edges + * @param deleteEdge 要删除的边 + * @return true: 是树, false: 不是树 + */ + private Boolean isTreeAfterRemoveEdge(int[][] edges, int deleteEdge) + { + initFather(); + for(int i = 0; i < edges.length; i++) + { + if(i == deleteEdge) continue; + if(same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 + return false; + } + join(edges[i][0], edges[i][1]); + } + return true; + } + + public int[] findRedundantDirectedConnection(int[][] edges) { + int[] inDegree = new int[N]; + for(int i = 0; i < edges.length; i++) + { + // 入度 + inDegree[ edges[i][1] ] += 1; + } + + // 找入度为2的节点所对应的边,注意要倒序,因为优先返回最后出现在二维数组中的答案 + ArrayList twoDegree = new ArrayList(); + for(int i = edges.length - 1; i >= 0; i--) + { + if(inDegree[edges[i][1]] == 2) { + twoDegree.add(i); + } + } + + // 处理图中情况1 和 情况2 + // 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 + if(!twoDegree.isEmpty()) + { + if(isTreeAfterRemoveEdge(edges, twoDegree.get(0))) { + return edges[ twoDegree.get(0)]; + } + return edges[ twoDegree.get(1)]; + } + + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + return getRemoveEdge(edges); + } +} +``` + +### Python + +```python + +class Solution: + + def __init__(self): + self.n = 1010 + self.father = [i for i in range(self.n)] + + + def find(self, u: int): + """ + 并查集里寻根的过程 + """ + if u == self.father[u]: + return u + self.father[u] = self.find(self.father[u]) + return self.father[u] + + def join(self, u: int, v: int): + """ + 将v->u 这条边加入并查集 + """ + u = self.find(u) + v = self.find(v) + if u == v : return + self.father[v] = u + pass + + + def same(self, u: int, v: int ): + """ + 判断 u 和 v是否找到同一个根,本题用不上 + """ + u = self.find(u) + v = self.find(v) + return u == v + + def init_father(self): + self.father = [i for i in range(self.n)] + pass + + def getRemoveEdge(self, edges: List[List[int]]) -> List[int]: + """ + 在有向图里找到删除的那条边,使其变成树 + """ + + self.init_father() + for i in range(len(edges)): + if self.same(edges[i][0], edges[i][1]): # 构成有向环了,就是要删除的边 + return edges[i] + self.join(edges[i][0], edges[i][1]); + return [] + + def isTreeAfterRemoveEdge(self, edges: List[List[int]], deleteEdge: int) -> bool: + """ + 删一条边之后判断是不是树 + """ + + self.init_father() + for i in range(len(edges)): + if i == deleteEdge: continue + if self.same(edges[i][0], edges[i][1]): # 构成有向环了,一定不是树 + return False + self.join(edges[i][0], edges[i][1]); + return True + + def findRedundantDirectedConnection(self, edges: List[List[int]]) -> List[int]: + inDegree = [0 for i in range(self.n)] + + for i in range(len(edges)): + inDegree[ edges[i][1] ] += 1 + + # 找入度为2的节点所对应的边,注意要倒序,因为优先返回最后出现在二维数组中的答案 + towDegree = [] + for i in range(len(edges))[::-1]: + if inDegree[edges[i][1]] == 2 : + towDegree.append(i) + + # 处理图中情况1 和 情况2 + # 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 + if len(towDegree) > 0: + if(self.isTreeAfterRemoveEdge(edges, towDegree[0])) : + return edges[towDegree[0]] + return edges[towDegree[1]] + + # 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + return self.getRemoveEdge(edges) +``` + +### Go + +```go + +// 全局变量 +var ( + n = 1010// 节点数量3 到 1000 + father = make([]int, n) +) + +// 并查集初始化 +func initialize() { + for i := 0; i < n; i++ { + father[i] = i + } +} + +// 并查集里寻根的过程 +func find(u int) int { + if u == father[u] { + return u + } + father[u] = find(father[u]) + return father[u] +} + +// 将v->u 这条边加入并查集 +func join(u, v int) { + u = find(u) + v = find(v) + if u == v { + return + } + father[v] = u +} + +// 判断 u 和 v是否找到同一个根,本题用不上 +func same(u, v int) bool { + u = find(u) + v = find(v) + return u == v +} + +// getRemoveEdge 在有向图里找到删除的那条边,使其变成树 +func getRemoveEdge(edges [][]int) []int { + initialize() + for i := 0; i < len(edges); i++ { // 遍历所有的边 + if same(edges[i][0], edges[i][1]) { // 构成有向环了,就是要删除的边 + return edges[i] + } + join(edges[i][0], edges[i][1]) + } + return []int{} +} + +// isTreeAfterRemoveEdge 删一条边之后判断是不是树 +func isTreeAfterRemoveEdge(edges [][]int, deleteEdge int) bool { + initialize() + for i := 0; i < len(edges); i++ { + if i == deleteEdge { + continue + } + if same(edges[i][0], edges[i][1]) { // 构成有向环了,一定不是树 + return false + } + join(edges[i][0], edges[i][1]) + } + return true +} + +func findRedundantDirectedConnection(edges [][]int) []int { + inDegree := make([]int, len(father)) + for i := 0; i < len(edges); i++ { + // 统计入度 + inDegree[edges[i][1]] += 1 + } + // 记录入度为2的边(如果有的话就两条边) + // 找入度为2的节点所对应的边,注意要倒序,因为优先返回最后出现在二维数组中的答案 + twoDegree := make([]int, 0) + for i := len(edges) - 1; i >= 0; i-- { + if inDegree[edges[i][1]] == 2 { + twoDegree = append(twoDegree, i) + } + } + + // 处理图中情况1 和 情况2 + // 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 + if len(twoDegree) > 0 { + if isTreeAfterRemoveEdge(edges, twoDegree[0]) { + return edges[twoDegree[0]] + } + return edges[twoDegree[1]] + } + + // 处理图中情况3 + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + return getRemoveEdge(edges) +} + +``` + +### JavaScript + +```js +const N = 1010; // 如题:二维数组大小的在3到1000范围内 +const father = new Array(N); +let n; // 边的数量 + +// 并查集里寻根的过程 +const find = u => { + return u == father[u] ? u : father[u] = find(father[u]); +}; + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u); + v = find(v); + if(u == v) return; + father[v] = u; +}; + +// 判断 u 和 v是否找到同一个根 +const same = (u, v) => { + u = find(u); + v = find(v); + return u == v; +}; + +// 在有向图里找到删除的那条边,使其变成树 +const getRemoveEdge = edges => { + // 初始化并查集 + for (let i = 1; i <= n; i++) { + father[i] = i; + } + for (let i = 0; i < n; i++) { // 遍历所有的边 + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 + return edges[i]; + } + join(edges[i][0], edges[i][1]); + } + return []; +} + +// 删一条边之后判断是不是树 +const isTreeAfterRemoveEdge = (edges, deleteEdge) => { + // 初始化并查集 + for (let i = 1; i <= n; i++) { + father[i] = i; + } + for (let i = 0; i < n; i++) { + if (i == deleteEdge) continue; + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 + return false; + } + join(edges[i][0], edges[i][1]); + } + return true; +} + +/** + * @param {number[][]} edges + * @return {number[]} + */ +var findRedundantDirectedConnection = function(edges) { + n = edges.length;// 边的数量 + const inDegree = new Array(n+1).fill(0); // 记录节点入度 + for (let i = 0; i < n; i++) { + inDegree[edges[i][1]]++; // 统计入度 + } + let vec = [];// 记录入度为2的边(如果有的话就两条边) + // 找入度为2的节点所对应的边,注意要倒序,因为优先返回最后出现在二维数组中的答案 + for (let i = n - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push(i); + } + } + // 处理图中情况1 和 情况2 + // 如果有入度为2的节点,那么一定是两条边里删一个,看删哪个可以构成树 + if (vec.length > 0) { + if (isTreeAfterRemoveEdge(edges, vec[0])) { + return edges[vec[0]]; + } else { + return edges[vec[1]]; + } + } + // 处理图中情况3 + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + return getRemoveEdge(edges); +}; +``` + + + + diff --git "a/problems/0695.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" "b/problems/0695.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" new file mode 100755 index 0000000000..a63d2b0e06 --- /dev/null +++ "b/problems/0695.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" @@ -0,0 +1,709 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 695. 岛屿的最大面积 + +[力扣题目链接](https://leetcode.cn/problems/max-area-of-island/) + +给你一个大小为 m x n 的二进制矩阵 grid 。 + +岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。 + +岛屿的面积是岛上值为 1 的单元格的数目。 + +计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0 。 + +![](https://file1.kamacoder.com/i/algo/20220729111528.png) + +* 输入:grid = [[0,0,1,0,0,0,0,1,0,0,0,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,1,1,0,1,0,0,0,0,0,0,0,0],[0,1,0,0,1,1,0,0,1,0,1,0,0],[0,1,0,0,1,1,0,0,1,1,1,0,0],[0,0,0,0,0,0,0,0,0,0,1,0,0],[0,0,0,0,0,0,0,1,1,1,0,0,0],[0,0,0,0,0,0,0,1,1,0,0,0,0]] +* 输出:6 +* 解释:答案不应该是 11 ,因为岛屿只能包含水平或垂直这四个方向上的 1 。 + +## 思路 + + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题目也是 dfs bfs基础类题目,就是搜索每个岛屿上“1”的数量,然后取一个最大的。 + +本题思路上比较简单,难点其实都是 dfs 和 bfs的理论基础,关于理论基础我在这里都有详细讲解 : + +* [DFS理论基础](https://programmercarl.com/图论深搜理论基础.html) +* [BFS理论基础](https://programmercarl.com/图论广搜理论基础.html) + +### DFS + +很多同学,写dfs其实也是凭感觉来,有的时候dfs函数中写终止条件才能过,有的时候 dfs函数不写终止添加也能过! + +这里其实涉及到dfs的两种写法。 + +写法一,dfs处理当前节点的相邻节点,即在主函数遇到岛屿就计数为1,dfs处理接下来的相邻陆地 + +```CPP +// 版本一 +class Solution { +private: + int count; + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void dfs(vector>& grid, vector>& visited, int x, int y) { + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { // 没有访问过的 同时 是陆地的 + + visited[nextx][nexty] = true; + count++; + dfs(grid, visited, nextx, nexty); + } + } + } + +public: + int maxAreaOfIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + count = 1; // 因为dfs处理下一个节点,所以这里遇到陆地了就先计数,dfs处理接下来的相邻陆地 + visited[i][j] = true; + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + return result; + } +}; +``` + +写法二,dfs处理当前节点,即在主函数遇到岛屿就计数为0,dfs处理接下来的全部陆地 + +dfs +```CPP +// 版本二 +class Solution { +private: + int count; + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty); + } + } + +public: + int maxAreaOfIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; // 因为dfs处理当前节点,所以遇到陆地计数为0,进dfs之后在开始从1计数 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + return result; + } +}; +``` + +大家通过注释可以发现,两种写法,版本一,在主函数遇到陆地就计数为1,接下来的相邻陆地都在dfs中计算。 版本二 在主函数遇到陆地 计数为0,也就是不计数,陆地数量都去dfs里做计算。 + +这也是为什么大家看了很多,dfs的写法,发现写法怎么都不一样呢? 其实这就是根本原因。 + +以上两种写法的区别,我在题解: [DFS,BDF 你没注意的细节都给你列出来了!LeetCode:200. 岛屿数量](https://leetcode.cn/problems/number-of-islands/solution/by-carlsun-2-n72a/)做了详细介绍。 + + +### BFS + +关于广度优先搜索,如果大家还不了解的话,看这里:[广度优先搜索精讲](https://programmercarl.com/图论广搜理论基础.html) + +本题BFS代码如下: + +```CPP +class Solution { +private: + int count; + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void bfs(vector>& grid, vector>& visited, int x, int y) { + queue que; + que.push(x); + que.push(y); + visited[x][y] = true; // 加入队列就意味节点是陆地可到达的点 + count++; + while(!que.empty()) { + int xx = que.front();que.pop(); + int yy = que.front();que.pop(); + for (int i = 0 ;i < 4; i++) { + int nextx = xx + dir[i][0]; + int nexty = yy + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { // 节点没有被访问过且是陆地 + visited[nextx][nexty] = true; + count++; + que.push(nextx); + que.push(nexty); + } + } + } + } + +public: + int maxAreaOfIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + return result; + } +}; + +``` + +## 其它语言版本 +### Java +#### DFS +```java +// DFS +class Solution { + int[][] dir = { + {0, 1}, //right + {1, 0}, //down + {0, -1}, //left + {-1, 0} //up + }; + boolean visited[][]; + int count; + public int maxAreaOfIsland(int[][] grid) { + int res = 0; + visited = new boolean[grid.length][grid[0].length]; + for(int i = 0; i < grid.length; i++){ + for(int j = 0; j < grid[0].length; j++){ + if(visited[i][j] == false && grid[i][j] == 1){ + count = 0; + dfs(grid, i, j); + res = Math.max(res, count); + } + } + } + return res; + } + private void dfs(int[][] grid, int x, int y){ + if(visited[x][y] == true || grid[x][y] == 0) + return; + + visited[x][y] = true; + count++; + + for(int i = 0; i < 4; i++){ + int nextX = x + dir[i][0]; + int nextY = y + dir[i][1]; + + if(nextX < 0 || nextY < 0 || nextX >= grid.length || nextY >= grid[0].length) + continue; + dfs(grid, nextX, nextY); + } + } +} + + +``` +#### BFS +```java +//BFS +class Solution { + int[][] dir = { + {0, 1}, {1, 0}, {0, -1}, {-1, 0} + }; + + int count; + boolean visited[][]; + + public int maxAreaOfIsland(int[][] grid) { + int res = 0; + visited = new boolean[grid.length][grid[0].length]; + + for(int i = 0; i < grid.length; i++){ + for(int j = 0; j < grid[0].length; j++){ + if(visited[i][j] == false && grid[i][j] == 1){ + count = 0; + bfs(grid, i, j); + res = Math.max(res, count); + } + } + } + return res; + } + private void bfs(int[][] grid, int x, int y){ + Queue que = new LinkedList<>(); + que.offer(x); + que.offer(y); + visited[x][y] = true; + count++; + + while(!que.isEmpty()){ + int currX = que.poll(); + int currY = que.poll(); + + for(int i = 0; i < 4; i++){ + int nextX = currX + dir[i][0]; + int nextY = currY + dir[i][1]; + + if(nextX < 0 || nextY < 0 || nextX >= grid.length || nextY >= grid[0].length) + continue; + if(visited[nextX][nextY] == false && grid[nextX][nextY] == 1){ + que.offer(nextX); + que.offer(nextY); + visited[nextX][nextY] = true; + count++; + } + } + } + } +} +``` +#### DFS 優化(遇到島嶼後,就把他淹沒) +```java +//这里使用深度优先搜索 DFS 来完成本道题目。我们使用 DFS 计算一个岛屿的面积,同时维护计算过的最大的岛屿面积。同时,为了避免对岛屿重复计算,我们在 DFS 的时候对岛屿进行 “淹没” 操作,即将岛屿所占的地方置为 0。 +public int maxAreaOfIsland(int[][] grid) { + int res = 0; + for(int i = 0;i < grid.length;i++){ + for(int j = 0;j < grid[0].length;j++){ + //每遇到一个岛屿就计算这个岛屿的面积同时”淹没“这个岛屿 + if(grid[i][j] == 1){ + //每次计算一个岛屿的面积都要与res比较,维护最大的岛屿面积作为最后的答案 + res = Math.max(res,dfs(grid,i,j)); + } + } + } + return res; +} +public int dfs(int[][] grid,int i,int j){ + //搜索边界:i,j超过grid的范围或者当前元素为0,即当前所在的地方已经是海洋 + if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == 0) return 0; + //淹没土地,防止后续被重复计算 + grid[i][j] = 0; + //递归的思路:要求当前土地(i,j)所在的岛屿的面积,则等于1加上下左右相邻的土地的总面积 + return 1 + dfs(grid,i - 1,j) + + dfs(grid,i + 1,j) + + dfs(grid,i,j + 1) + + dfs(grid,i,j - 1); +} +``` + +### Python +#### BFS +```python +class Solution: + def __init__(self): + self.count = 0 + + def maxAreaOfIsland(self, grid: List[List[int]]) -> int: + # 与200.独立岛屿不同的是:此题grid列表内是int!!! + + # BFS + if not grid: return 0 + + m, n = len(grid), len(grid[0]) + visited = [[False for i in range(n)] for j in range(m)] + + result = 0 + for i in range(m): + for j in range(n): + if not visited[i][j] and grid[i][j] == 1: + # 每一个新岛屿 + self.count = 0 + self.bfs(grid, visited, i, j) + result = max(result, self.count) + + return result + + def bfs(self, grid, visited, i, j): + self.count += 1 + visited[i][j] = True + + queue = collections.deque([(i, j)]) + while queue: + x, y = queue.popleft() + for new_x, new_y in [(x + 1, y), (x - 1, y), (x, y - 1), (x, y + 1)]: + if 0 <= new_x < len(grid) and 0 <= new_y < len(grid[0]) and not visited[new_x][new_y] and grid[new_x][new_y] == 1: + visited[new_x][new_y] = True + self.count += 1 + queue.append((new_x, new_y)) +``` +#### DFS +```python +class Solution: + def __init__(self): + self.count = 0 + + def maxAreaOfIsland(self, grid: List[List[int]]) -> int: + # DFS + if not grid: return 0 + + m, n = len(grid), len(grid[0]) + visited = [[False for _ in range(n)] for _ in range(m)] + + result = 0 + for i in range(m): + for j in range(n): + if not visited[i][j] and grid[i][j] == 1: + self.count = 0 + self.dfs(grid, visited, i, j) + result = max(result, self.count) + return result + + def dfs(self, grid, visited, x, y): + if visited[x][y] or grid[x][y] == 0: + return + visited[x][y] = True + self.count += 1 + for new_x, new_y in [(x + 1, y), (x - 1, y), (x, y + 1), (x, y - 1)]: + if 0 <= new_x < len(grid) and 0 <= new_y < len(grid[0]): + self.dfs(grid, visited, new_x, new_y) +``` + +### JavaScript +```javascript +var maxAreaOfIsland = function (grid) { + let dir = [[0, 1], [1, 0], [-1, 0], [0, -1]]; // 四个方向 + + let visited = new Array(grid.length).fill().map(() => Array(grid[0].length).fill(false)) + + let dfs = (grid, visited, x, y, m) => { + for (let i = 0; i < 4; i++) { + let nextX = x + dir[i][0] + let nextY = y + dir[i][1] + if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) + continue; + if (!visited[nextX][nextY] && grid[nextX][nextY] === 1) { + visited[nextX][nextY] = true + m = dfs(grid, visited, nextX, nextY,m+1) + } + } + return m + } + + let max = 0 + + for (let i = 0; i < grid.length; i++) { + for (let j = 0; j < grid[i].length; j++) { + if (!visited[i][j] && grid[i][j] === 1) { + // 深度优先 + visited[i][j] = true; + let m = dfs(grid, visited, i, j, 1); + if (m > max) max = m; + } + } + } + return max +}; +``` + +### Go + +dsf: 版本一 + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} +var count int = 0 + +func maxAreaOfIsland(grid [][]int) int { + res := 0 + visited := make([][]bool, len(grid)) + for i := 0; i < len(grid); i++ { + visited[i] = make([]bool, len(grid[0])) + } + + for i, rows := range grid { + for j, v := range rows { + if v == 1 && !visited[i][j] { + // 第一种写法,重制 count,必定有 1 个 + count = 1 + dfs(grid, visited, i, j) + res = max(res, count) + } + + } + } + + return res +} + +// 第一种写法 +func dfs(grid [][]int, visited [][]bool, i, j int) { + visited[i][j] = true // 标记已访问,循环中未标记会导致重复 + for _, d := range DIRECTIONS { + x, y := i+d[0], j+d[1] + if x < 0 || x >= len(grid) || y < 0 || y >= len(grid[0]) { + continue + } + if grid[x][y] == 1 && !visited[x][y] { + count++ + dfs(grid, visited, x, y) + } + } +} +``` + +dfs:版本二 + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} +var count int = 0 + +func maxAreaOfIsland(grid [][]int) int { + res := 0 + visited := make([][]bool, len(grid)) + for i := 0; i < len(grid); i++ { + visited[i] = make([]bool, len(grid[0])) + } + + for i, rows := range grid { + for j, v := range rows { + if v == 1 && !visited[i][j] { + // 第二种写法 + count = 0 + dfs(grid, visited, i, j) + res = max(res, count) + } + + } + } + + return res +} + +// 第二种写法 +func dfs(grid [][]int, visited [][]bool, i, j int) { + if visited[i][j] || grid[i][j] == 0 { + return + } + visited[i][j] = true + count++ + for _, d := range DIRECTIONS { + x, y := i+d[0], j+d[1] + if x < 0 || x >= len(grid) || y < 0 || y >= len(grid[0]) { + continue + } + dfs(grid, visited, x, y) + } +} +``` + +bfs: + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} +var count int = 0 + +func maxAreaOfIsland(grid [][]int) int { + res := 0 + visited := make([][]bool, len(grid)) + for i := 0; i < len(grid); i++ { + visited[i] = make([]bool, len(grid[0])) + } + + for i, rows := range grid { + for j, v := range rows { + if v == 1 && !visited[i][j] { + count = 0 + bfs(grid, visited, i, j) + res = max(res, count) + } + + } + } + + return res +} + +// bfs +func bfs(grid [][]int, visited [][]bool, i, j int) { + visited[i][j] = true + count++ + queue := [][2]int{{i, j}} + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + for _, d := range DIRECTIONS { + x, y := cur[0]+d[0], cur[1]+d[1] + if x < 0 || x >= len(grid) || y < 0 || y >= len(grid[0]) { + continue + } + if grid[x][y] == 1 && !visited[x][y] { + count++ + queue = append(queue, [2]int{x, y}) + visited[x][y] = true + } + } + } +} +``` + +### Rust + +dfs: 版本一 + +```rust +impl Solution { + const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + + pub fn max_area_of_island(grid: Vec>) -> i32 { + let mut visited = vec![vec![false; grid[0].len()]; grid.len()]; + + let mut res = 0; + for (i, nums) in grid.iter().enumerate() { + for (j, &num) in nums.iter().enumerate() { + if !visited[i][j] && num == 1 { + let mut count = 1; + visited[i][j] = true; + Self::dfs(&grid, &mut visited, (i as i32, j as i32), &mut count); + res = res.max(count); + } + } + } + + res + } + + pub fn dfs( + grid: &[Vec], + visited: &mut [Vec], + (x, y): (i32, i32), + count: &mut i32, + ) { + for (dx, dy) in Self::DIRECTIONS { + let (nx, ny) = (x + dx, y + dy); + if nx < 0 || nx >= grid.len() as i32 || ny < 0 || ny >= grid[0].len() as i32 { + continue; + } + let (nx, ny) = (nx as usize, ny as usize); + if !visited[nx][ny] && grid[nx][ny] == 1 { + visited[nx][ny] = true; + *count += 1; + Self::dfs(grid, visited, (nx as i32, ny as i32), count); + } + } + } +} +``` + +dfs: 版本二 + +```rust +impl Solution { + const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + + pub fn max_area_of_island(grid: Vec>) -> i32 { + let mut visited = vec![vec![false; grid[0].len()]; grid.len()]; + + let mut res = 0; + for (i, nums) in grid.iter().enumerate() { + for (j, &num) in nums.iter().enumerate() { + if !visited[i][j] && num == 1 { + let mut count = 0; + Self::dfs(&grid, &mut visited, (i as i32, j as i32), &mut count); + res = res.max(count); + } + } + } + + res + } + + pub fn dfs( + grid: &[Vec], + visited: &mut [Vec], + (x, y): (i32, i32), + count: &mut i32, + ) { + if visited[x as usize][y as usize] || grid[x as usize][y as usize] == 0 { + return; + } + visited[x as usize][y as usize] = true; + *count += 1; + for (dx, dy) in Self::DIRECTIONS { + let (nx, ny) = (x + dx, y + dy); + if nx < 0 || nx >= grid.len() as i32 || ny < 0 || ny >= grid[0].len() as i32 { + continue; + } + Self::dfs(grid, visited, (nx, ny), count); + } + } +} +``` + +bfs: + +```rust +use std::collections::VecDeque; +impl Solution { + const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + + pub fn max_area_of_island(grid: Vec>) -> i32 { + let mut visited = vec![vec![false; grid[0].len()]; grid.len()]; + + let mut res = 0; + for (i, nums) in grid.iter().enumerate() { + for (j, &num) in nums.iter().enumerate() { + if !visited[i][j] && num == 1 { + let mut count = 0; + Self::bfs(&grid, &mut visited, (i as i32, j as i32), &mut count); + res = res.max(count); + } + } + } + + res + } + + pub fn bfs(grid: &[Vec], visited: &mut [Vec], (x, y): (i32, i32), count: &mut i32) { + let mut queue = VecDeque::new(); + queue.push_back((x, y)); + visited[x as usize][y as usize] = true; + *count += 1; + while let Some((cur_x, cur_y)) = queue.pop_front() { + for (dx, dy) in Self::DIRECTIONS { + let (nx, ny) = (cur_x + dx, cur_y + dy); + if nx < 0 || nx >= grid.len() as i32 || ny < 0 || ny >= grid[0].len() as i32 { + continue; + } + let (nx, ny) = (nx as usize, ny as usize); + if !visited[nx][ny] && grid[nx][ny] == 1 { + visited[nx][ny] = true; + queue.push_back((nx as i32, ny as i32)); + *count += 1; + } + } + } + } +} +``` + diff --git "a/problems/0700.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\220\234\347\264\242.md" "b/problems/0700.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\220\234\347\264\242.md" new file mode 100755 index 0000000000..40777a67a2 --- /dev/null +++ "b/problems/0700.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\220\234\347\264\242.md" @@ -0,0 +1,508 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 700.二叉搜索树中的搜索 + +[力扣题目地址](https://leetcode.cn/problems/search-in-a-binary-search-tree/) + +给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。 + +例如, + + +![700.二叉搜索树中的搜索](https://file1.kamacoder.com/i/algo/20210204155522476.png) + +在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[不愧是搜索树,这次搜索有方向了!| LeetCode:700.二叉搜索树中的搜索](https://www.bilibili.com/video/BV1wG411g7sF),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +之前我们讲的都是普通二叉树,那么接下来看看二叉搜索树。 + +在[关于二叉树,你该了解这些!](https://programmercarl.com/二叉树理论基础.html)中,我们已经讲过了二叉搜索树。 + +二叉搜索树是一个有序树: + +* 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; +* 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; +* 它的左、右子树也分别为二叉搜索树 + +这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。 + +本题,其实就是在二叉搜索树中搜索一个节点。那么我们来看看应该如何遍历。 + +### 递归法 + +1. 确定递归函数的参数和返回值 + +递归函数的参数传入的就是根节点和要搜索的数值,返回的就是以这个搜索数值所在的节点。 + +代码如下: + +```CPP +TreeNode* searchBST(TreeNode* root, int val) +``` + +2. 确定终止条件 + +如果root为空,或者找到这个数值了,就返回root节点。 + +```CPP +if (root == NULL || root->val == val) return root; +``` + +3. 确定单层递归的逻辑 + +看看二叉搜索树的单层递归逻辑有何不同。 + +因为二叉搜索树的节点是有序的,所以可以有方向的去搜索。 + +如果root->val > val,搜索左子树,如果root->val < val,就搜索右子树,最后如果都没有搜索到,就返回NULL。 + +代码如下: + +```CPP +TreeNode* result = NULL; +if (root->val > val) result = searchBST(root->left, val); +if (root->val < val) result = searchBST(root->right, val); +return result; +``` + +很多录友写递归函数的时候 习惯直接写 `searchBST(root->left, val)`,却忘了 递归函数还有返回值。 + +递归函数的返回值是什么? 是 左子树如果搜索到了val,要将该节点返回。 如果不用一个变量将其接住,那么返回值不就没了。 + +所以要 `result = searchBST(root->left, val)`。 + +整体代码如下: + +```CPP +class Solution { +public: + TreeNode* searchBST(TreeNode* root, int val) { + if (root == NULL || root->val == val) return root; + TreeNode* result = NULL; + if (root->val > val) result = searchBST(root->left, val); + if (root->val < val) result = searchBST(root->right, val); + return result; + } +}; +``` + +或者我们也可以这么写 + +```CPP +class Solution { +public: + TreeNode* searchBST(TreeNode* root, int val) { + if (root == NULL || root->val == val) return root; + if (root->val > val) return searchBST(root->left, val); + if (root->val < val) return searchBST(root->right, val); + return NULL; + } +}; +``` + + +### 迭代法 + +一提到二叉树遍历的迭代法,可能立刻想起使用栈来模拟深度遍历,使用队列来模拟广度遍历。 + +对于二叉搜索树可就不一样了,因为二叉搜索树的特殊性,也就是节点的有序性,可以不使用辅助栈或者队列就可以写出迭代法。 + +对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。 + +而**对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。** + +例如要搜索元素为3的节点,**我们不需要搜索其他节点,也不需要做回溯,查找的路径已经规划好了。** + +中间节点如果大于3就向左走,如果小于3就向右走,如图: + +![二叉搜索树](https://file1.kamacoder.com/i/algo/20200812190213280.png) + +所以迭代法代码如下: + +```CPP +class Solution { +public: + TreeNode* searchBST(TreeNode* root, int val) { + while (root != NULL) { + if (root->val > val) root = root->left; + else if (root->val < val) root = root->right; + else return root; + } + return NULL; + } +}; +``` + +第一次看到了如此简单的迭代法,是不是感动的痛哭流涕,哭一会~ + +## 总结 + +本篇我们介绍了二叉搜索树的遍历方式,因为二叉搜索树的有序性,遍历的时候要比普通二叉树简单很多。 + +但是一些同学很容易忽略二叉搜索树的特性,所以写出遍历的代码就未必真的简单了。 + +所以针对二叉搜索树的题目,一样要利用其特性。 + +文中我依然给出递归和迭代两种方式,可以看出写法都非常简单,就是利用了二叉搜索树有序的特点。 + + + + +## 其他语言版本 + +### Java + +```Java +class Solution { + // 递归,普通二叉树 + public TreeNode searchBST(TreeNode root, int val) { + if (root == null || root.val == val) { + return root; + } + TreeNode left = searchBST(root.left, val); + if (left != null) { + return left; + } + return searchBST(root.right, val); + } +} + +class Solution { + // 递归,利用二叉搜索树特点,优化 + public TreeNode searchBST(TreeNode root, int val) { + if (root == null || root.val == val) { + return root; + } + if (val < root.val) { + return searchBST(root.left, val); + } else { + return searchBST(root.right, val); + } + } +} + +class Solution { + // 迭代,普通二叉树 + public TreeNode searchBST(TreeNode root, int val) { + if (root == null || root.val == val) { + return root; + } + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()) { + TreeNode pop = stack.pop(); + if (pop.val == val) { + return pop; + } + if (pop.right != null) { + stack.push(pop.right); + } + if (pop.left != null) { + stack.push(pop.left); + } + } + return null; + } +} + +class Solution { + // 迭代,利用二叉搜索树特点,优化,可以不需要栈 + public TreeNode searchBST(TreeNode root, int val) { + while (root != null) + if (val < root.val) root = root.left; + else if (val > root.val) root = root.right; + else return root; + return null; + } +} +``` + +### Python + +(方法一) 递归 + +```python +class Solution: + def searchBST(self, root: TreeNode, val: int) -> TreeNode: + # 为什么要有返回值: + # 因为搜索到目标节点就要立即return, + # 这样才是找到节点就返回(搜索某一条边),如果不加return,就是遍历整棵树了。 + + if not root or root.val == val: + return root + + if root.val > val: + return self.searchBST(root.left, val) + + if root.val < val: + return self.searchBST(root.right, val) + +``` + +(方法二)迭代 + +```python +class Solution: + def searchBST(self, root: TreeNode, val: int) -> TreeNode: + while root: + if val < root.val: root = root.left + elif val > root.val: root = root.right + else: return root + return None +``` + +(方法三) 栈-遍历 +```python +class Solution: + def searchBST(self, root: TreeNode, val: int) -> TreeNode: + stack = [root] + while stack: + node = stack.pop() + # 根据TreeNode的定义 + # node携带有三类信息 node.left/node.right/node.val + # 找到val直接返回node 即是找到了该节点为根的子树 + # 此处node.left/node.right/val的前后顺序可打乱 + if node.val == val: + return node + if node.right: + stack.append(node.right) + if node.left: + stack.append(node.left) + return None +``` + + +### Go + +递归法: + +```go + //递归法 +func searchBST(root *TreeNode, val int) *TreeNode { + if root == nil || root.Val == val { + return root + } + if root.Val > val { + return searchBST(root.Left, val) + } + return searchBST(root.Right, val) +} +``` + +迭代法: + +```go + //迭代法 +func searchBST(root *TreeNode, val int) *TreeNode { + for root != nil { + if root.Val > val { + root = root.Left + } else if root.Val < val { + root = root.Right + } else { + return root + } + } + return nil +} +``` + +### JavaScript + +递归: + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @param {number} val + * @return {TreeNode} + */ +var searchBST = function (root, val) { + if (!root || root.val === val) { + return root; + } + if (root.val > val) + return searchBST(root.left, val); + if (root.val < val) + return searchBST(root.right, val); +}; +``` + +迭代: + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @param {number} val + * @return {TreeNode} + */ +var searchBST = function (root, val) { + while (root !== null) { + if (root.val > val) + root = root.left; + else if (root.val < val) + root = root.right; + else + return root; + } + return null; +}; +``` + +### TypeScript + +> 递归法 + +```typescript +function searchBST(root: TreeNode | null, val: number): TreeNode | null { + if (root === null || root.val === val) return root; + if (root.val < val) return searchBST(root.right, val); + if (root.val > val) return searchBST(root.left, val); + return null; +}; +``` + +> 迭代法 + +```typescript +function searchBST(root: TreeNode | null, val: number): TreeNode | null { + let resNode: TreeNode | null = root; + while (resNode !== null) { + if (resNode.val === val) return resNode; + if (resNode.val < val) { + resNode = resNode.right; + } else { + resNode = resNode.left; + } + } + return null; +}; +``` + +### Scala + +递归: +```scala +object Solution { + def searchBST(root: TreeNode, value: Int): TreeNode = { + if (root == null || value == root.value) return root + // 相当于三元表达式,在Scala中if...else有返回值 + if (value < root.value) searchBST(root.left, value) else searchBST(root.right, value) + } +} +``` + +迭代: +```scala +object Solution { + def searchBST(root: TreeNode, value: Int): TreeNode = { + // 因为root是不可变量,所以需要赋值给一个可变量 + var node = root + while (node != null) { + if (value < node.value) node = node.left + else if (value > node.value) node = node.right + else return node + } + null // 没有返回就返回空 + } +} +``` + +### Rust + +递归: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn search_bst( + root: Option>>, + val: i32, + ) -> Option>> { + if root.is_none() || root.as_ref().unwrap().borrow().val == val { + return root; + } + let node_val = root.as_ref().unwrap().borrow().val; + if node_val > val { + return Self::search_bst(root.as_ref().unwrap().borrow().left.clone(), val); + } + if node_val < val { + return Self::search_bst(root.unwrap().borrow().right.clone(), val); + } + None + } +} +``` + +迭代: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +use std::cmp; +impl Solution { + pub fn search_bst( + mut root: Option>>, + val: i32, + ) -> Option>> { + while let Some(ref node) = root.clone() { + match val.cmp(&node.borrow().val) { + cmp::Ordering::Less => root = node.borrow().left.clone(), + cmp::Ordering::Equal => return root, + cmp::Ordering::Greater => root = node.borrow().right.clone(), + }; + } + None + } +} +``` +### C# +```csharp +// 递归 +public TreeNode SearchBST(TreeNode root, int val) +{ + if (root == null || root.val == val) return root; + if (root.val > val) return SearchBST(root.left, val); + if (root.val < val) return SearchBST(root.right, val); + return null; +} +// 迭代 +public TreeNode SearchBST(TreeNode root, int val) +{ + while (root != null) + { + if (root.val > val) root = root.left; + else if (root.val < val) root = root.right; + else return root; + } + return null; +} +``` + + diff --git "a/problems/0701.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\217\222\345\205\245\346\223\215\344\275\234.md" "b/problems/0701.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\217\222\345\205\245\346\223\215\344\275\234.md" new file mode 100755 index 0000000000..fec287449c --- /dev/null +++ "b/problems/0701.\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\344\270\255\347\232\204\346\217\222\345\205\245\346\223\215\344\275\234.md" @@ -0,0 +1,724 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 701.二叉搜索树中的插入操作 + +[力扣题目链接](https://leetcode.cn/problems/insert-into-a-binary-search-tree/) + +给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。 + +注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。 + + +![701.二叉搜索树中的插入操作](https://file1.kamacoder.com/i/algo/20201019173259554.png) + +提示: + +* 给定的树上的节点数介于 0 和 10^4 之间 +* 每个节点都有一个唯一整数值,取值范围从 0 到 10^8 +* -10^8 <= val <= 10^8 +* 新值和原始二叉搜索树中的任意节点值都不同 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[原来这么简单? | LeetCode:701.二叉搜索树中的插入操作](https://www.bilibili.com/video/BV1Et4y1c78Y?share_source=copy_web),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这道题目其实是一道简单题目,**但是题目中的提示:有多种有效的插入方式,还可以重构二叉搜索树,一下子吓退了不少人**,瞬间感觉题目复杂了很多。 + +其实**可以不考虑题目中提示所说的改变树的结构的插入方式。** + +如下演示视频中可以看出:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。 + +![701.二叉搜索树中的插入操作](https://file1.kamacoder.com/i/algo/701.%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91%E4%B8%AD%E7%9A%84%E6%8F%92%E5%85%A5%E6%93%8D%E4%BD%9C.gif) + +例如插入元素10 ,需要找到末尾节点插入便可,一样的道理来插入元素15,插入元素0,插入元素6,**需要调整二叉树的结构么? 并不需要。**。 + +只要遍历二叉搜索树,找到空节点 插入元素就可以了,那么这道题其实就简单了。 + +接下来就是遍历二叉搜索树的过程了。 + +### 递归 + +递归三部曲: + +* 确定递归函数参数以及返回值 + +参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢? + +可以有,也可以没有,但递归函数如果没有返回值的话,实现是比较麻烦的,下面也会给出其具体实现代码。 + +**有返回值的话,可以利用返回值完成新加入的节点与其父节点的赋值操作**。(下面会进一步解释) + +递归函数的返回类型为节点类型TreeNode * 。 + +代码如下: + +```cpp +TreeNode* insertIntoBST(TreeNode* root, int val) +``` + +* 确定终止条件 + +终止条件就是找到遍历的节点为null的时候,就是要插入节点的位置了,并把插入的节点返回。 + +代码如下: + +```cpp +if (root == NULL) { + TreeNode* node = new TreeNode(val); + return node; +} +``` + +这里把添加的节点返回给上一层,就完成了父子节点的赋值操作了,详细再往下看。 + +* 确定单层递归的逻辑 + +此时要明确,需要遍历整棵树么? + +别忘了这是搜索树,遍历整棵搜索树简直是对搜索树的侮辱。 + +搜索树是有方向了,可以根据插入元素的数值,决定递归方向。 + +代码如下: + +```cpp +if (root->val > val) root->left = insertIntoBST(root->left, val); +if (root->val < val) root->right = insertIntoBST(root->right, val); +return root; +``` + +**到这里,大家应该能感受到,如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了,下一层将加入节点返回,本层用root->left或者root->right将其接住**。 + + +整体代码如下: + +```CPP +class Solution { +public: + TreeNode* insertIntoBST(TreeNode* root, int val) { + if (root == NULL) { + TreeNode* node = new TreeNode(val); + return node; + } + if (root->val > val) root->left = insertIntoBST(root->left, val); + if (root->val < val) root->right = insertIntoBST(root->right, val); + return root; + } +}; +``` + +可以看出代码并不复杂。 + +刚刚说了递归函数不用返回值也可以,找到插入的节点位置,直接让其父节点指向插入节点,结束递归,也是可以的。 + +那么递归函数定义如下: + +```cpp +TreeNode* parent; // 记录遍历节点的父节点 +void traversal(TreeNode* cur, int val) +``` + +没有返回值,需要记录上一个节点(parent),遇到空节点了,就让parent左孩子或者右孩子指向新插入的节点。然后结束递归。 + +代码如下: + +```CPP +class Solution { +private: + TreeNode* parent; + void traversal(TreeNode* cur, int val) { + if (cur == NULL) { + TreeNode* node = new TreeNode(val); + if (val > parent->val) parent->right = node; + else parent->left = node; + return; + } + parent = cur; + if (cur->val > val) traversal(cur->left, val); + if (cur->val < val) traversal(cur->right, val); + return; + } + +public: + TreeNode* insertIntoBST(TreeNode* root, int val) { + parent = new TreeNode(0); + if (root == NULL) { + root = new TreeNode(val); + } + traversal(root, val); + return root; + } +}; +``` + +可以看出还是麻烦一些的。 + +我之所以举这个例子,是想说明通过递归函数的返回值完成父子节点的赋值是可以带来便利的。 + +**网上千篇一律的代码,可能会误导大家认为通过递归函数返回节点 这样的写法是天经地义,其实这里是有优化的!** + + +### 迭代 + +再来看看迭代法,对二叉搜索树迭代写法不熟悉,可以看这篇:[二叉树:二叉搜索树登场!](https://programmercarl.com/0700.二叉搜索树中的搜索.html) + +在迭代法遍历的过程中,需要记录一下当前遍历的节点的父节点,这样才能做插入节点的操作。 + +在[二叉树:搜索树的最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html)和[二叉树:我的众数是多少?](https://programmercarl.com/0501.二叉搜索树中的众数.html)中,都是用了记录pre和cur两个指针的技巧,本题也是一样的。 + +代码如下: + +```CPP +class Solution { +public: + TreeNode* insertIntoBST(TreeNode* root, int val) { + if (root == NULL) { + TreeNode* node = new TreeNode(val); + return node; + } + TreeNode* cur = root; + TreeNode* parent = root; // 这个很重要,需要记录上一个节点,否则无法赋值新节点 + while (cur != NULL) { + parent = cur; + if (cur->val > val) cur = cur->left; + else cur = cur->right; + } + TreeNode* node = new TreeNode(val); + if (val < parent->val) parent->left = node;// 此时是用parent节点的进行赋值 + else parent->right = node; + return root; + } +}; +``` + +## 总结 + +首先在二叉搜索树中的插入操作,大家不用恐惧其重构搜索树,其实根本不用重构。 + +然后在递归中,我们重点讲了如何通过递归函数的返回值完成新加入节点和其父节点的赋值操作,并强调了搜索树的有序性。 + +最后依然给出了迭代的方法,迭代的方法就需要记录当前遍历节点的父节点了,这个和没有返回值的递归函数实现的代码逻辑是一样的。 + + +## 其他语言版本 + +### Java + +```java +class Solution { + public TreeNode insertIntoBST(TreeNode root, int val) { + if (root == null) return new TreeNode(val); + TreeNode newRoot = root; + TreeNode pre = root; + while (root != null) { + pre = root; + if (root.val > val) { + root = root.left; + } else if (root.val < val) { + root = root.right; + } + } + if (pre.val > val) { + pre.left = new TreeNode(val); + } else { + pre.right = new TreeNode(val); + } + + return newRoot; + } +} +``` + +递归法 + +```java +class Solution { + public TreeNode insertIntoBST(TreeNode root, int val) { + if (root == null) // 如果当前节点为空,也就意味着val找到了合适的位置,此时创建节点直接返回。 + return new TreeNode(val); + + if (root.val < val){ + root.right = insertIntoBST(root.right, val); // 递归创建右子树 + }else if (root.val > val){ + root.left = insertIntoBST(root.left, val); // 递归创建左子树 + } + return root; + } +} +``` +----- +### Python + +递归法(版本一) +```python +class Solution: + def __init__(self): + self.parent = None + + def traversal(self, cur, val): + if cur is None: + node = TreeNode(val) + if val > self.parent.val: + self.parent.right = node + else: + self.parent.left = node + return + + self.parent = cur + if cur.val > val: + self.traversal(cur.left, val) + if cur.val < val: + self.traversal(cur.right, val) + + def insertIntoBST(self, root, val): + self.parent = TreeNode(0) + if root is None: + return TreeNode(val) + self.traversal(root, val) + return root +``` + +递归法(版本二) +```python +class Solution: + def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: + if root is None or root.val == val: + return TreeNode(val) + elif root.val > val: + if root.left is None: + root.left = TreeNode(val) + else: + self.insertIntoBST(root.left, val) + elif root.val < val: + if root.right is None: + root.right = TreeNode(val) + else: + self.insertIntoBST(root.right, val) + return root +``` + +递归法(版本三) +```python +class Solution: + def insertIntoBST(self, root, val): + if root is None: + node = TreeNode(val) + return node + + if root.val > val: + root.left = self.insertIntoBST(root.left, val) + if root.val < val: + root.right = self.insertIntoBST(root.right, val) + + return root +``` + +迭代法(版本一) +```python +class Solution: + def insertIntoBST(self, root, val): + if root is None: # 如果根节点为空,创建新节点作为根节点并返回 + node = TreeNode(val) + return node + + cur = root + parent = root # 记录上一个节点,用于连接新节点 + while cur is not None: + parent = cur + if cur.val > val: + cur = cur.left + else: + cur = cur.right + + node = TreeNode(val) + if val < parent.val: + parent.left = node # 将新节点连接到父节点的左子树 + else: + parent.right = node # 将新节点连接到父节点的右子树 + + return root +``` + +迭代法(版本二) +```python +class Solution: + def insertIntoBST(self, root, val): + if root is None: + return TreeNode(val) + parent = None + cur = root + while cur: + parent = cur + if val < cur.val: + cur = cur.left + else: + cur = cur.right + if val < parent.val: + parent.left = TreeNode(val) + else: + parent.right = TreeNode(val) + return root +``` + +迭代法(精简) +```python +class Solution: + def insertIntoBST(self, root: Optional[TreeNode], val: int) -> Optional[TreeNode]: + if not root: # 如果根节点为空,创建新节点作为根节点并返回 + return TreeNode(val) + cur = root + while cur: + if val < cur.val: + if not cur.left: # 如果此时父节点的左子树为空 + cur.left = TreeNode(val) # 将新节点连接到父节点的左子树 + return root + else: + cur = cur.left + elif val > cur.val: + if not cur.right: # 如果此时父节点的左子树为空 + cur.right = TreeNode(val) # 将新节点连接到父节点的右子树 + return root + else: + cur = cur.right + +``` + +----- +### Go + +递归法 + +```Go +func insertIntoBST(root *TreeNode, val int) *TreeNode { + if root == nil { + root = &TreeNode{Val: val} + return root + } + if root.Val > val { + root.Left = insertIntoBST(root.Left, val) + } else { + root.Right = insertIntoBST(root.Right, val) + } + return root +} +``` + +迭代法 + +```go +func insertIntoBST(root *TreeNode, val int) *TreeNode { + if root == nil { + return &TreeNode{Val:val} + } + node := root + var pnode *TreeNode + for node != nil { + if val > node.Val { + pnode = node + node = node.Right + } else { + pnode = node + node = node.Left + } + } + if val > pnode.Val { + pnode.Right = &TreeNode{Val: val} + } else { + pnode.Left = &TreeNode{Val: val} + } + return root +} +``` +----- +### JavaScript + +有返回值的递归写法 + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @param {number} val + * @return {TreeNode} + */ +var insertIntoBST = function (root, val) { + const setInOrder = (root, val) => { + if (root === null) { + let node = new TreeNode(val); + return node; + } + if (root.val > val) + root.left = setInOrder(root.left, val); + else if (root.val < val) + root.right = setInOrder(root.right, val); + return root; + } + return setInOrder(root, val); +}; +``` + +无返回值的递归 + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @param {number} val + * @return {TreeNode} + */ +var insertIntoBST = function (root, val) { + let parent = new TreeNode(0); + const preOrder = (cur, val) => { + if (cur === null) { + let node = new TreeNode(val); + if (parent.val > val) + parent.left = node; + else + parent.right = node; + return; + } + parent = cur; + if (cur.val > val) + preOrder(cur.left, val); + if (cur.val < val) + preOrder(cur.right, val); + } + if (root === null) + root = new TreeNode(val); + preOrder(root, val); + return root; +}; +``` + +迭代 + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @param {number} val + * @return {TreeNode} + */ +var insertIntoBST = function (root, val) { + if (root === null) { + root = new TreeNode(val); + } else { + let parent = new TreeNode(0); + let cur = root; + while (cur) { + parent = cur; + if (cur.val > val) + cur = cur.left; + else + cur = cur.right; + } + let node = new TreeNode(val); + if (parent.val > val) + parent.left = node; + else + parent.right = node; + } + return root; +}; +``` + +### TypeScript + +> 递归-有返回值 + +```typescript +function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null { + if (root === null) return new TreeNode(val); + if (root.val > val) { + root.left = insertIntoBST(root.left, val); + } else { + root.right = insertIntoBST(root.right, val); + } + return root; +}; +``` + +> 递归-无返回值 + +```typescript +function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null { + if (root === null) return new TreeNode(val); + function recur(root: TreeNode | null, val: number) { + if (root === null) { + if (parentNode.val > val) { + parentNode.left = new TreeNode(val); + } else { + parentNode.right = new TreeNode(val); + } + return; + } + parentNode = root; + if (root.val > val) recur(root.left, val); + if (root.val < val) recur(root.right, val); + } + let parentNode: TreeNode = root; + recur(root, val); + return root; +}; +``` + +> 迭代法 + +```typescript +function insertIntoBST(root: TreeNode | null, val: number): TreeNode | null { + if (root === null) return new TreeNode(val); + let curNode: TreeNode | null = root; + let parentNode: TreeNode = root; + while (curNode !== null) { + parentNode = curNode; + if (curNode.val > val) { + curNode = curNode.left + } else { + curNode = curNode.right; + } + } + if (parentNode.val > val) { + parentNode.left = new TreeNode(val); + } else { + parentNode.right = new TreeNode(val); + } + return root; +}; +``` + + +### Scala + +递归: + +```scala +object Solution { + def insertIntoBST(root: TreeNode, `val`: Int): TreeNode = { + if (root == null) return new TreeNode(`val`) + if (`val` < root.value) root.left = insertIntoBST(root.left, `val`) + else root.right = insertIntoBST(root.right, `val`) + root // 返回根节点 + } +} +``` + +迭代: + +```scala +object Solution { + def insertIntoBST(root: TreeNode, `val`: Int): TreeNode = { + if (root == null) { + return new TreeNode(`val`) + } + var parent = root // 记录当前节点的父节点 + var curNode = root + while (curNode != null) { + parent = curNode + if(`val` < curNode.value) curNode = curNode.left + else curNode = curNode.right + } + if(`val` < parent.value) parent.left = new TreeNode(`val`) + else parent.right = new TreeNode(`val`) + root // 最终返回根节点 + } +} +``` + +### Rust + +迭代: + +```rust +impl Solution { + pub fn insert_into_bst( + root: Option>>, + val: i32, + ) -> Option>> { + if root.is_none() { + return Some(Rc::new(RefCell::new(TreeNode::new(val)))); + } + let mut cur = root.clone(); + let mut pre = None; + while let Some(node) = cur.clone() { + pre = cur; + if node.borrow().val > val { + cur = node.borrow().left.clone(); + } else { + cur = node.borrow().right.clone(); + }; + } + let r = Some(Rc::new(RefCell::new(TreeNode::new(val)))); + let mut p = pre.as_ref().unwrap().borrow_mut(); + if val < p.val { + p.left = r; + } else { + p.right = r; + } + + root + } +} +``` + +递归: + +```rust +impl Solution { + pub fn insert_into_bst( + root: Option>>, + val: i32, + ) -> Option>> { + if let Some(node) = &root { + if node.borrow().val > val { + let left = Self::insert_into_bst(node.borrow_mut().left.take(), val); + node.borrow_mut().left = left; + } else { + let right = Self::insert_into_bst(node.borrow_mut().right.take(), val); + node.borrow_mut().right = right; + } + root + } else { + Some(Rc::new(RefCell::new(TreeNode::new(val)))) + } + } +} +``` +### C# +``` C# +// 递归 +public TreeNode InsertIntoBST(TreeNode root, int val) { + if (root == null) return new TreeNode(val); + + if (root.val > val) root.left = InsertIntoBST(root.left, val); + if (root.val < val) root.right = InsertIntoBST(root.right, val); + return root; +} +``` + + diff --git "a/problems/0704.\344\272\214\345\210\206\346\237\245\346\211\276.md" "b/problems/0704.\344\272\214\345\210\206\346\237\245\346\211\276.md" new file mode 100755 index 0000000000..e529629492 --- /dev/null +++ "b/problems/0704.\344\272\214\345\210\206\346\237\245\346\211\276.md" @@ -0,0 +1,837 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 704. 二分查找 + +[力扣题目链接](https://leetcode.cn/problems/binary-search/) + +给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。 + +示例 1: + +``` +输入: nums = [-1,0,3,5,9,12], target = 9 +输出: 4 +解释: 9 出现在 nums 中并且下标为 4 +``` + +示例 2: + +``` +输入: nums = [-1,0,3,5,9,12], target = 2 +输出: -1 +解释: 2 不存在 nums 中因此返回 -1 +``` + +提示: + +* 你可以假设 nums 中的所有元素是不重复的。 +* n 将在 [1, 10000]之间。 +* nums 的每个元素都将在 [-9999, 9999]之间。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[手把手带你撕出正确的二分法](https://www.bilibili.com/video/BV1fA4y1o715),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +**这道题目的前提是数组为有序数组**,同时题目还强调**数组中无重复元素**,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。 + +二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 `while(left < right)` 还是 `while(left <= right)`,到底是`right = middle`呢,还是要`right = middle - 1`呢? + +大家写二分法经常写乱,主要是因为**对区间的定义没有想清楚,区间的定义就是不变量**。要在二分查找的过程中,保持不变量,就是在while寻找中每一次边界的处理都要坚持根据区间的定义来操作,这就是**循环不变量**规则。 + +写二分法,区间的定义一般为两种,左闭右闭即[left, right],或者左闭右开即[left, right)。 + +下面我用这两种区间的定义分别讲解两种不同的二分写法。 + +### 二分法第一种写法 + +第一种写法,我们定义 target 是在一个在左闭右闭的区间里,**也就是[left, right] (这个很重要非常重要)**。 + +区间的定义这就决定了二分法的代码应该如何写,**因为定义target在[left, right]区间,所以有如下两点:** + +* while (left <= right) 要使用 <= ,因为left == right是有意义的,所以使用 <= +* if (nums[middle] > target) right 要赋值为 middle - 1,因为当前这个nums[middle]一定不是target,那么接下来要查找的左区间结束下标位置就是 middle - 1 + +例如在数组:1,2,3,4,7,9,10中查找元素2,如图所示: + +![704.二分查找](https://file1.kamacoder.com/i/algo/20210311153055723.jpg) + +代码如下:(详细注释) + +```CPP +// 版本一 +class Solution { +public: + int search(vector& nums, int target) { + int left = 0; + int right = nums.size() - 1; // 定义target在左闭右闭的区间里,[left, right] + while (left <= right) { // 当left==right,区间[left, right]依然有效,所以用 <= + int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2 + if (nums[middle] > target) { + right = middle - 1; // target 在左区间,所以[left, middle - 1] + } else if (nums[middle] < target) { + left = middle + 1; // target 在右区间,所以[middle + 1, right] + } else { // nums[middle] == target + return middle; // 数组中找到目标值,直接返回下标 + } + } + // 未找到目标值 + return -1; + } +}; + +``` +* 时间复杂度:O(log n) +* 空间复杂度:O(1) + + +### 二分法第二种写法 + +如果说定义 target 是在一个在左闭右开的区间里,也就是[left, right) ,那么二分法的边界处理方式则截然不同。 + +有如下两点: + +* while (left < right),这里使用 < ,因为left == right在区间[left, right)是没有意义的 +* if (nums[middle] > target) right 更新为 middle,因为当前nums[middle]不等于target,去左区间继续寻找,而寻找区间是左闭右开区间,所以right更新为middle,即:下一个查询区间不会去比较nums[middle] + +在数组:1,2,3,4,7,9,10中查找元素2,如图所示:(**注意和方法一的区别**) + + +![704.二分查找1](https://file1.kamacoder.com/i/algo/20210311153123632.jpg) + +代码如下:(详细注释) + +```CPP +// 版本二 +class Solution { +public: + int search(vector& nums, int target) { + int left = 0; + int right = nums.size(); // 定义target在左闭右开的区间里,即:[left, right) + while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间,所以使用 < + int middle = left + ((right - left) >> 1); + if (nums[middle] > target) { + right = middle; // target 在左区间,在[left, middle)中 + } else if (nums[middle] < target) { + left = middle + 1; // target 在右区间,在[middle + 1, right)中 + } else { // nums[middle] == target + return middle; // 数组中找到目标值,直接返回下标 + } + } + // 未找到目标值 + return -1; + } +}; +``` +* 时间复杂度:O(log n) +* 空间复杂度:O(1) + + +## 总结 + +二分法是非常重要的基础算法,为什么很多同学对于二分法都是**一看就会,一写就废**? + +其实主要就是对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。 + +区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。 + +本篇根据两种常见的区间定义,给出了两种二分法的写法,每一个边界为什么这么处理,都根据区间的定义做了详细介绍。 + +相信看完本篇应该对二分法有更深刻的理解了。 + +## 相关题目推荐 + +* [35.搜索插入位置](https://programmercarl.com/0035.搜索插入位置.html) +* [34.在排序数组中查找元素的第一个和最后一个位置](https://programmercarl.com/0034.%E5%9C%A8%E6%8E%92%E5%BA%8F%E6%95%B0%E7%BB%84%E4%B8%AD%E6%9F%A5%E6%89%BE%E5%85%83%E7%B4%A0%E7%9A%84%E7%AC%AC%E4%B8%80%E4%B8%AA%E5%92%8C%E6%9C%80%E5%90%8E%E4%B8%80%E4%B8%AA%E4%BD%8D%E7%BD%AE.html) +* [69.x 的平方根](https://leetcode.cn/problems/sqrtx/) +* [367.有效的完全平方数](https://leetcode.cn/problems/valid-perfect-square/) + + + + + + +## 其他语言版本 + +### **Java:** + +(版本一)左闭右闭区间 + +```java +class Solution { + public int search(int[] nums, int target) { + // 避免当 target 小于nums[0] nums[nums.length - 1]时多次循环运算 + if (target < nums[0] || target > nums[nums.length - 1]) { + return -1; + } + int left = 0, right = nums.length - 1; + while (left <= right) { + int mid = left + ((right - left) >> 1); + if (nums[mid] == target) { + return mid; + } + else if (nums[mid] < target) { + left = mid + 1; + } + else { // nums[mid] > target + right = mid - 1; + } + } + // 未找到目标值 + return -1; + } +} +``` + +(版本二)左闭右开区间 + +```java +class Solution { + public int search(int[] nums, int target) { + int left = 0, right = nums.length; + while (left < right) { + int mid = left + ((right - left) >> 1); + if (nums[mid] == target) { + return mid; + } + else if (nums[mid] < target) { + left = mid + 1; + } + else { // nums[mid] > target + right = mid; + } + } + // 未找到目标值 + return -1; + } +} +``` + +### **Python:** + +(版本一)左闭右闭区间 + +```python +class Solution: + def search(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) - 1 # 定义target在左闭右闭的区间里,[left, right] + + while left <= right: + middle = left + (right - left) // 2 + + if nums[middle] > target: + right = middle - 1 # target在左区间,所以[left, middle - 1] + elif nums[middle] < target: + left = middle + 1 # target在右区间,所以[middle + 1, right] + else: + return middle # 数组中找到目标值,直接返回下标 + return -1 # 未找到目标值 +``` + +(版本二)左闭右开区间 + +```python +class Solution: + def search(self, nums: List[int], target: int) -> int: + left, right = 0, len(nums) # 定义target在左闭右开的区间里,即:[left, right) + + while left < right: # 因为left == right的时候,在[left, right)是无效的空间,所以使用 < + middle = left + (right - left) // 2 + + if nums[middle] > target: + right = middle # target 在左区间,在[left, middle)中 + elif nums[middle] < target: + left = middle + 1 # target 在右区间,在[middle + 1, right)中 + else: + return middle # 数组中找到目标值,直接返回下标 + return -1 # 未找到目标值 +``` + +### **Go:** + +(版本一)左闭右闭区间 + +```go +// 时间复杂度 O(logn) +func search(nums []int, target int) int { + // 初始化左右边界 + left := 0 + right := len(nums) - 1 + + // 循环逐步缩小区间范围 + for left <= right { + // 求区间中点 + mid := left + (right-left)>>1 + + // 根据 nums[mid] 和 target 的大小关系 + // 调整区间范围 + if nums[mid] == target { + return mid + } else if nums[mid] < target { + left = mid + 1 + } else { + right = mid - 1 + } + } + + // 在输入数组内没有找到值等于 target 的元素 + return -1 +} +``` + +(版本二)左闭右开区间 + +```go +// 时间复杂度 O(logn) +func search(nums []int, target int) int { + // 初始化左右边界 + left := 0 + right := len(nums) + + // 循环逐步缩小区间范围 + for left < right { + // 求区间中点 + mid := left + (right-left)>>1 + + // 根据 nums[mid] 和 target 的大小关系 + // 调整区间范围 + if nums[mid] == target { + return mid + } else if nums[mid] < target { + left = mid + 1 + } else { + right = mid + } + } + + // 在输入数组内没有找到值等于 target 的元素 + return -1 +} +``` + +### **JavaScript:** +(版本一)左闭右闭区间 [left, right] + +```js +/** + * @param {number[]} nums + * @param {number} target + * @return {number} + */ +var search = function(nums, target) { + // right是数组最后一个数的下标,num[right]在查找范围内,是左闭右闭区间 + let mid, left = 0, right = nums.length - 1; + // 当left=right时,由于nums[right]在查找范围内,所以要包括此情况 + while (left <= right) { + // 位运算 + 防止大数溢出 + mid = left + ((right - left) >> 1); + // 如果中间数大于目标值,要把中间数排除查找范围,所以右边界更新为mid-1;如果右边界更新为mid,那中间数还在下次查找范围内 + if (nums[mid] > target) { + right = mid - 1; // 去左面闭区间寻找 + } else if (nums[mid] < target) { + left = mid + 1; // 去右面闭区间寻找 + } else { + return mid; + } + } + return -1; +}; +``` +(版本二)左闭右开区间 [left, right) + +```js +/** + * @param {number[]} nums + * @param {number} target + * @return {number} + */ +var search = function(nums, target) { + // right是数组最后一个数的下标+1,nums[right]不在查找范围内,是左闭右开区间 + let mid, left = 0, right = nums.length; + // 当left=right时,由于nums[right]不在查找范围,所以不必包括此情况 + while (left < right) { + // 位运算 + 防止大数溢出 + mid = left + ((right - left) >> 1); + // 如果中间值大于目标值,中间值不应在下次查找的范围内,但中间值的前一个值应在; + // 由于right本来就不在查找范围内,所以将右边界更新为中间值,如果更新右边界为mid-1则将中间值的前一个值也踢出了下次寻找范围 + if (nums[mid] > target) { + right = mid; // 去左区间寻找 + } else if (nums[mid] < target) { + left = mid + 1; // 去右区间寻找 + } else { + return mid; + } + } + return -1; +}; +``` + +### **TypeScript** + +(版本一)左闭右闭区间 + +```typescript +function search(nums: number[], target: number): number { + let mid: number, left: number = 0, right: number = nums.length - 1; + while (left <= right) { + // 位运算 + 防止大数溢出 + mid = left + ((right - left) >> 1); + if (nums[mid] > target) { + right = mid - 1; + } else if (nums[mid] < target) { + left = mid + 1; + } else { + return mid; + } + } + return -1; +}; +``` + +(版本二)左闭右开区间 + +```typescript +function search(nums: number[], target: number): number { + let mid: number, left: number = 0, right: number = nums.length; + while (left < right) { + // 位运算 + 防止大数溢出 + mid = left +((right - left) >> 1); + if (nums[mid] > target) { + right = mid; + } else if (nums[mid] < target) { + left = mid + 1; + } else { + return mid; + } + } + return -1; +}; +``` + +### **Ruby:** + +```ruby +# (版本一)左闭右闭区间 + +def search(nums, target) + left, right = 0, nums.length - 1 + while left <= right # 由于定义target在一个在左闭右闭的区间里,因此极限情况下存在left==right + middle = (left + right) / 2 + if nums[middle] > target + right = middle - 1 + elsif nums[middle] < target + left = middle + 1 + else + return middle # return兼具返回与跳出循环的作用 + end + end + -1 +end + +# (版本二)左闭右开区间 + +def search(nums, target) + left, right = 0, nums.length + while left < right # 由于定义target在一个在左闭右开的区间里,因此极限情况下right=left+1 + middle = (left + right) / 2 + if nums[middle] > target + right = middle + elsif nums[middle] < target + left = middle + 1 + else + return middle + end + end + -1 +end +``` + +### **Swift:** + +```swift +// (版本一)左闭右闭区间 +func search(nums: [Int], target: Int) -> Int { + // 1. 先定义区间。这里的区间是[left, right] + var left = 0 + var right = nums.count - 1 + + while left <= right {// 因为taeget是在[left, right]中,包括两个边界值,所以这里的left == right是有意义的 + // 2. 计算区间中间的下标(如果left、right都比较大的情况下,left + right就有可能会溢出) + // let middle = (left + right) / 2 + // 防溢出: + let middle = left + (right - left) / 2 + + // 3. 判断 + if target < nums[middle] { + // 当目标在区间左侧,就需要更新右边的边界值,新区间为[left, middle - 1] + right = middle - 1 + } else if target > nums[middle] { + // 当目标在区间右侧,就需要更新左边的边界值,新区间为[middle + 1, right] + left = middle + 1 + } else { + // 当目标就是在中间,则返回中间值的下标 + return middle + } + } + + // 如果找不到目标,则返回-1 + return -1 +} + +// (版本二)左闭右开区间 +func search(nums: [Int], target: Int) -> Int { + var left = 0 + var right = nums.count + + while left < right { + let middle = left + ((right - left) >> 1) + + if target < nums[middle] { + right = middle + } else if target > nums[middle] { + left = middle + 1 + } else { + return middle + } + } + + return -1 +} + +``` + +### **Rust:** + +(版本一)左闭右闭区间 + +```rust +use std::cmp::Ordering; +impl Solution { + pub fn search(nums: Vec, target: i32) -> i32 { + let (mut left, mut right) = (0_i32, nums.len() as i32 - 1); + while left <= right { + let mid = (right + left) / 2; + match nums[mid as usize].cmp(&target) { + Ordering::Less => left = mid + 1, + Ordering::Greater => right = mid - 1, + Ordering::Equal => return mid, + } + } + -1 + } +} +``` + +//(版本二)左闭右开区间 + +```rust +use std::cmp::Ordering; +impl Solution { + pub fn search(nums: Vec, target: i32) -> i32 { + let (mut left, mut right) = (0_i32, nums.len() as i32); + while left < right { + let mid = (right + left) / 2; + match nums[mid as usize].cmp(&target) { + Ordering::Less => left = mid + 1, + Ordering::Greater => right = mid, + Ordering::Equal => return mid, + } + } + -1 + } +} +``` + +### **C:** + +```c +// (版本一) 左闭右闭区间 [left, right] +int search(int* nums, int numsSize, int target){ + int left = 0; + int right = numsSize-1; + int middle = 0; + //若left小于等于right,说明区间中元素不为0 + while(left<=right) { + //更新查找下标middle的值 + middle = (left+right)/2; + //此时target可能会在[left,middle-1]区间中 + if(nums[middle] > target) { + right = middle-1; + } + //此时target可能会在[middle+1,right]区间中 + else if(nums[middle] < target) { + left = middle+1; + } + //当前下标元素等于target值时,返回middle + else if(nums[middle] == target){ + return middle; + } + } + //若未找到target元素,返回-1 + return -1; +} +``` +```C +// (版本二) 左闭右开区间 [left, right) +int search(int* nums, int numsSize, int target){ + int length = numsSize; + int left = 0; + int right = length; //定义target在左闭右开的区间里,即:[left, right) + int middle = 0; + while(left < right){ // left == right时,区间[left, right)属于空集,所以用 < 避免该情况 + int middle = left + (right - left) / 2; + if(nums[middle] < target){ + //target位于(middle , right) 中为保证集合区间的左闭右开性,可等价为[middle + 1,right) + left = middle + 1; + }else if(nums[middle] > target){ + //target位于[left, middle)中 + right = middle ; + }else{ // nums[middle] == target ,找到目标值target + return middle; + } + } + //未找到目标值,返回-1 + return -1; +} +``` + +### **PHP:** + +```php +// 左闭右闭区间 +class Solution { + /** + * @param Integer[] $nums + * @param Integer $target + * @return Integer + */ + function search($nums, $target) { + if (count($nums) == 0) { + return -1; + } + $left = 0; + $right = count($nums) - 1; + while ($left <= $right) { + $mid = floor(($left + $right) / 2); + if ($nums[$mid] == $target) { + return $mid; + } + if ($nums[$mid] > $target) { + $right = $mid - 1; + } + else { + $left = $mid + 1; + } + } + return -1; + } +} +``` + +### **C#:** + +```csharp +//左闭右闭 +public class Solution { + public int Search(int[] nums, int target) { + int left = 0; + int right = nums.Length - 1; + while(left <= right){ + int mid = (right - left ) / 2 + left; + if(nums[mid] == target){ + return mid; + } + else if(nums[mid] < target){ + left = mid+1; + } + else if(nums[mid] > target){ + right = mid-1; + } + } + return -1; + } +} + +//左闭右开 +public class Solution{ + public int Search(int[] nums, int target){ + int left = 0; + int right = nums.Length; + while(left < right){ + int mid = (right - left) / 2 + left; + if(nums[mid] == target){ + return mid; + } + else if(nums[mid] < target){ + left = mid + 1; + } + else if(nums[mid] > target){ + right = mid; + } + } + return -1; + } +} +``` + +### **Kotlin:** + +```kotlin +class Solution { + fun search(nums: IntArray, target: Int): Int { + // leftBorder + var left:Int = 0 + // rightBorder + var right:Int = nums.size - 1 + // 使用左闭右闭区间 + while (left <= right) { + var middle:Int = left + (right - left)/2 + // taget 在左边 + if (nums[middle] > target) { + right = middle - 1 + } + else { + // target 在右边 + if (nums[middle] < target) { + left = middle + 1 + } + // 找到了,返回 + else return middle + } + } + // 没找到,返回 + return -1 + } +} +``` + +### **Kotlin:** + +```Kotlin +// (版本一)左闭右开区间 +class Solution { + fun search(nums: IntArray, target: Int): Int { + var left = 0 + var right = nums.size // [left,right) 右侧为开区间,right 设置为 nums.size + while (left < right) { + val mid = (left + right) / 2 + if (nums[mid] < target) left = mid + 1 + else if (nums[mid] > target) right = mid // 代码的核心,循环中 right 是开区间,这里也应是开区间 + else return mid + } + return -1 // 没有找到 target ,返回 -1 + } +} +// (版本二)左闭右闭区间 +class Solution { + fun search(nums: IntArray, target: Int): Int { + var left = 0 + var right = nums.size - 1 // [left,right] 右侧为闭区间,right 设置为 nums.size - 1 + while (left <= right) { + val mid = (left + right) / 2 + if (nums[mid] < target) left = mid + 1 + else if (nums[mid] > target) right = mid - 1 // 代码的核心,循环中 right 是闭区间,这里也应是闭区间 + else return mid + } + return -1 // 没有找到 target ,返回 -1 + } +} +``` +### **Scala:** + +(版本一)左闭右闭区间 +```scala +object Solution { + def search(nums: Array[Int], target: Int): Int = { + var left = 0 + var right = nums.length - 1 + while (left <= right) { + var mid = left + ((right - left) / 2) + if (target == nums(mid)) { + return mid + } else if (target < nums(mid)) { + right = mid - 1 + } else { + left = mid + 1 + } + } + -1 + } +} +``` +(版本二)左闭右开区间 +```scala +object Solution { + def search(nums: Array[Int], target: Int): Int = { + var left = 0 + var right = nums.length + while (left < right) { + val mid = left + (right - left) / 2 + if (target == nums(mid)) { + return mid + } else if (target < nums(mid)) { + right = mid + } else { + left = mid + 1 + } + } + -1 + } +} +``` +**Dart:** + + + +```dart +(版本一)左闭右闭区间 +class Solution { + int search(List nums, int target) { + int left = 0; + int right = nums.length - 1; + while (left <= right) { + int middle = ((left + right)/2).truncate(); + switch (nums[middle].compareTo(target)) { + case 1: + right = middle - 1; + continue; + case -1: + left = middle + 1; + continue; + default: + return middle; + } + } + return -1; + } +} + +(版本二)左闭右开区间 +class Solution { + int search(List nums, int target) { + int left = 0; + int right = nums.length; + while (left < right) { + int middle = left + ((right - left) >> 1); + switch (nums[middle].compareTo(target)) { + case 1: + right = middle; + continue; + case -1: + left = middle + 1; + continue; + default: + return middle; + } + } + return -1; + } +} +``` + + diff --git "a/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" "b/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" new file mode 100755 index 0000000000..72e35f430f --- /dev/null +++ "b/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" @@ -0,0 +1,1846 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 听说这道题目把链表常见的五个操作都覆盖了? + +# 707.设计链表 + +[力扣题目链接](https://leetcode.cn/problems/design-linked-list/) + +题意: + +在链表类中实现这些功能: + +* get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。 +* addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。 +* addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。 +* addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。 +* deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。 + + +![707示例](https://file1.kamacoder.com/i/algo/20200814200558953.png) + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[帮你把链表操作学个通透!LeetCode:707.设计链表](https://www.bilibili.com/video/BV1FU4y1X7WD),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +如果对链表的基础知识还不太懂,可以看这篇文章:[关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html) + +如果对链表的虚拟头结点不清楚,可以看这篇文章:[链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html) + +删除链表节点: +![链表-删除节点](https://file1.kamacoder.com/i/algo/20200806195114541.png) + +添加链表节点: +![链表-添加节点](https://file1.kamacoder.com/i/algo/20200806195134331.png) + +这道题目设计链表的五个接口: +* 获取链表第index个节点的数值 +* 在链表的最前面插入一个节点 +* 在链表的最后面插入一个节点 +* 在链表第index个节点前面插入一个节点 +* 删除链表的第index个节点 + +可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目 + +**链表操作的两种方式:** + +1. 直接使用原来的链表来进行操作。 +2. 设置一个虚拟头结点在进行操作。 + +下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)。 + +```CPP +class MyLinkedList { +public: + // 定义链表节点结构体 + struct LinkedNode { + int val; + LinkedNode* next; + LinkedNode(int val):val(val), next(nullptr){} + }; + + // 初始化链表 + MyLinkedList() { + _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点 + _size = 0; + } + + // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点 + int get(int index) { + if (index > (_size - 1) || index < 0) { + return -1; + } + LinkedNode* cur = _dummyHead->next; + while(index--){ // 如果--index 就会陷入死循环 + cur = cur->next; + } + return cur->val; + } + + // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点 + void addAtHead(int val) { + LinkedNode* newNode = new LinkedNode(val); + newNode->next = _dummyHead->next; + _dummyHead->next = newNode; + _size++; + } + + // 在链表最后面添加一个节点 + void addAtTail(int val) { + LinkedNode* newNode = new LinkedNode(val); + LinkedNode* cur = _dummyHead; + while(cur->next != nullptr){ + cur = cur->next; + } + cur->next = newNode; + _size++; + } + + // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 + // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点 + // 如果index大于链表的长度,则返回空 + // 如果index小于0,则在头部插入节点 + void addAtIndex(int index, int val) { + + if(index > _size) return; + if(index < 0) index = 0; + LinkedNode* newNode = new LinkedNode(val); + LinkedNode* cur = _dummyHead; + while(index--) { + cur = cur->next; + } + newNode->next = cur->next; + cur->next = newNode; + _size++; + } + + // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的 + void deleteAtIndex(int index) { + if (index >= _size || index < 0) { + return; + } + LinkedNode* cur = _dummyHead; + while(index--) { + cur = cur ->next; + } + LinkedNode* tmp = cur->next; + cur->next = cur->next->next; + delete tmp; + //delete命令指示释放了tmp指针原本所指的那部分内存, + //被delete后的指针tmp的值(地址)并非就是NULL,而是随机值。也就是被delete后, + //如果不再加上一句tmp=nullptr,tmp会成为乱指的野指针 + //如果之后的程序不小心使用了tmp,会指向难以预想的内存空间 + tmp=nullptr; + _size--; + } + + // 打印链表 + void printLinkedList() { + LinkedNode* cur = _dummyHead; + while (cur->next != nullptr) { + cout << cur->next->val << " "; + cur = cur->next; + } + cout << endl; + } +private: + int _size; + LinkedNode* _dummyHead; + +}; +``` + +* 时间复杂度: 涉及 `index` 的相关操作为 O(index), 其余为 O(1) +* 空间复杂度: O(n) + + + +## 其他语言版本 +### C++双链表法: + +```CPP +//采用循环虚拟结点的双链表实现 +class MyLinkedList { +public: + // 定义双向链表节点结构体 + struct DList { + int elem; // 节点存储的元素 + DList *next; // 指向下一个节点的指针 + DList *prev; // 指向上一个节点的指针 + // 构造函数,创建一个值为elem的新节点 + DList(int elem) : elem(elem), next(nullptr), prev(nullptr) {}; + }; + + // 构造函数,初始化链表 + MyLinkedList() { + sentinelNode = new DList(0); // 创建哨兵节点,不存储有效数据 + sentinelNode->next = sentinelNode; // 哨兵节点的下一个节点指向自身,形成循环 + sentinelNode->prev = sentinelNode; // 哨兵节点的上一个节点指向自身,形成循环 + size = 0; // 初始化链表大小为0 + } + + // 获取链表中第index个节点的值 + int get(int index) { + if (index > (size - 1) || index < 0) { // 检查索引是否超出范围 + return -1; // 如果超出范围,返回-1 + } + int num; + int mid = size >> 1; // 计算链表中部位置 + DList *curNode = sentinelNode; // 从哨兵节点开始 + if (index < mid) { // 如果索引小于中部位置,从前往后遍历 + for (int i = 0; i < index + 1; i++) { + curNode = curNode->next; // 移动到目标节点 + } + } else { // 如果索引大于等于中部位置,从后往前遍历 + for (int i = 0; i < size - index; i++) { + curNode = curNode->prev; // 移动到目标节点 + } + } + num = curNode->elem; // 获取目标节点的值 + return num; // 返回节点的值 + } + + // 在链表头部添加节点 + void addAtHead(int val) { + DList *newNode = new DList(val); // 创建新节点 + DList *next = sentinelNode->next; // 获取当前头节点的下一个节点 + newNode->prev = sentinelNode; // 新节点的上一个节点指向哨兵节点 + newNode->next = next; // 新节点的下一个节点指向原来的头节点 + size++; // 链表大小加1 + sentinelNode->next = newNode; // 哨兵节点的下一个节点指向新节点 + next->prev = newNode; // 原来的头节点的上一个节点指向新节点 + } + + // 在链表尾部添加节点 + void addAtTail(int val) { + DList *newNode = new DList(val); // 创建新节点 + DList *prev = sentinelNode->prev; // 获取当前尾节点的上一个节点 + newNode->next = sentinelNode; // 新节点的下一个节点指向哨兵节点 + newNode->prev = prev; // 新节点的上一个节点指向原来的尾节点 + size++; // 链表大小加1 + sentinelNode->prev = newNode; // 哨兵节点的上一个节点指向新节点 + prev->next = newNode; // 原来的尾节点的下一个节点指向新节点 + } + + // 在链表中的第index个节点之前添加值为val的节点 + void addAtIndex(int index, int val) { + if (index > size) { // 检查索引是否超出范围 + return; // 如果超出范围,直接返回 + } + if (index <= 0) { // 如果索引为0或负数,在头部添加节点 + addAtHead(val); + return; + } + int num; + int mid = size >> 1; // 计算链表中部位置 + DList *curNode = sentinelNode; // 从哨兵节点开始 + if (index < mid) { // 如果索引小于中部位置,从前往后遍历 + for (int i = 0; i < index; i++) { + curNode = curNode->next; // 移动到目标位置的前一个节点 + } + DList *temp = curNode->next; // 获取目标位置的节点 + DList *newNode = new DList(val); // 创建新节点 + curNode->next = newNode; // 在目标位置前添加新节点 + temp->prev = newNode; // 目标位置的节点的前一个节点指向新节点 + newNode->next = temp; // 新节点的下一个节点指向目标位置的结点 + newNode->prev = curNode; // 新节点的上一个节点指向当前节点 + } else { // 如果索引大于等于中部位置,从后往前遍历 + for (int i = 0; i < size - index; i++) { + curNode = curNode->prev; // 移动到目标位置的后一个节点 + } + DList *temp = curNode->prev; // 获取目标位置的节点 + DList *newNode = new DList(val); // 创建新节点 + curNode->prev = newNode; // 在目标位置后添加新节点 + temp->next = newNode; // 目标位置的节点的下一个节点指向新节点 + newNode->prev = temp; // 新节点的上一个节点指向目标位置的节点 + newNode->next = curNode; // 新节点的下一个节点指向当前节点 + } + size++; // 链表大小加1 + } + + // 删除链表中的第index个节点 + void deleteAtIndex(int index) { + if (index > (size - 1) || index < 0) { // 检查索引是否超出范围 + return; // 如果超出范围,直接返回 + } + int num; + int mid = size >> 1; // 计算链表中部位置 + DList *curNode = sentinelNode; // 从哨兵节点开始 + if (index < mid) { // 如果索引小于中部位置,从前往后遍历 + for (int i = 0; i < index; i++) { + curNode = curNode->next; // 移动到目标位置的前一个节点 + } + DList *next = curNode->next->next; // 获取目标位置的下一个节点 + curNode->next = next; // 删除目标位置的节点 + next->prev = curNode; // 目标位置的下一个节点的前一个节点指向当前节点 + } else { // 如果索引大于等于中部位置,从后往前遍历 + for (int i = 0; i < size - index - 1; i++) { + curNode = curNode->prev; // 移动到目标位置的后一个节点 + } + DList *prev = curNode->prev->prev; // 获取目标位置的下一个节点 + curNode->prev = prev; // 删除目标位置的节点 + prev->next = curNode; // 目标位置的下一个节点的下一个节点指向当前节点 + } + size--; // 链表大小减1 + } + +private: + int size; // 链表的大小 + DList *sentinelNode; // 哨兵节点的指针 +}; +``` + +### C: + +```C +typedef struct Node { + int val; + struct Node* next; +} Node; + + +typedef struct { + int size; + Node* data; +} MyLinkedList; + +/** Initialize your data structure here. */ + +MyLinkedList* myLinkedListCreate() { + MyLinkedList* obj = (MyLinkedList*)malloc(sizeof(MyLinkedList)); + Node* head = (Node*)malloc(sizeof(Node)); + head->next = (void*)0; + obj->data = head; + obj->size = 0; + return obj; +} + +/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */ +int myLinkedListGet(MyLinkedList* obj, int index) { + if (index < 0 || index >= obj->size) return -1; + + Node* cur = obj->data; + while (index-- >= 0) { + cur = cur->next; + } + + return cur->val; +} + +/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */ +void myLinkedListAddAtHead(MyLinkedList* obj, int val) { + Node* node = (Node*)malloc(sizeof(Node)); + node->val = val; + + node->next = obj->data->next; + obj->data->next = node; + obj->size++; +} + +/** Append a node of value val to the last element of the linked list. */ +void myLinkedListAddAtTail(MyLinkedList* obj, int val) { + Node* cur = obj->data; + while (cur->next != ((void*)0)) { + cur = cur->next; + } + + Node* tail = (Node*)malloc(sizeof(Node)); + tail->val = val; + tail->next = (void*)0; + cur->next = tail; + obj->size++; +} + +/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */ +void myLinkedListAddAtIndex(MyLinkedList* obj, int index, int val) { + if (index > obj->size) return; + + Node* cur = obj->data; + while (index-- > 0) { + cur = cur->next; + } + + Node* node = (Node*)malloc(sizeof(Node)); + node->val = val; + node->next = cur->next; + cur->next = node; + obj->size++; +} + +/** Delete the index-th node in the linked list, if the index is valid. */ +void myLinkedListDeleteAtIndex(MyLinkedList* obj, int index) { + if (index < 0 || index >= obj->size) return; + + Node* cur = obj->data; + while (index-- > 0) { + cur = cur->next; + } + + Node* temp = cur->next; + cur->next = temp->next; + free(temp); + obj->size--; +} + +void myLinkedListFree(MyLinkedList* obj) { + Node* tmp = obj->data; + while (tmp != NULL) { + Node* n = tmp; + tmp = tmp->next; + free(n); + } + free(obj); +} + +/** + * Your MyLinkedList struct will be instantiated and called as such: + * MyLinkedList* obj = myLinkedListCreate(); + * int param_1 = myLinkedListGet(obj, index); + + * myLinkedListAddAtHead(obj, val); + + * myLinkedListAddAtTail(obj, val); + + * myLinkedListAddAtIndex(obj, index, val); + + * myLinkedListDeleteAtIndex(obj, index); + + * myLinkedListFree(obj); +*/ +``` + +### Java: + +```Java +//单链表 +class MyLinkedList { + + class ListNode { + int val; + ListNode next; + ListNode(int val) { + this.val=val; + } + } + //size存储链表元素的个数 + private int size; + //注意这里记录的是虚拟头结点 + private ListNode head; + + //初始化链表 + public MyLinkedList() { + this.size = 0; + this.head = new ListNode(0); + } + + //获取第index个节点的数值,注意index是从0开始的,第0个节点就是虚拟头结点 + public int get(int index) { + //如果index非法,返回-1 + if (index < 0 || index >= size) { + return -1; + } + ListNode cur = head; + //第0个节点是虚拟头节点,所以查找第 index+1 个节点 + for (int i = 0; i <= index; i++) { + cur = cur.next; + } + return cur.val; + } + + public void addAtHead(int val) { + ListNode newNode = new ListNode(val); + newNode.next = head.next; + head.next = newNode; + size++; + + // 在链表最前面插入一个节点,等价于在第0个元素前添加 + // addAtIndex(0, val); + } + + + public void addAtTail(int val) { + ListNode newNode = new ListNode(val); + ListNode cur = head; + while (cur.next != null) { + cur = cur.next; + } + cur.next = newNode; + size++; + + // 在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加 + // addAtIndex(size, val); + } + + // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 + // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点 + // 如果 index 大于链表的长度,则返回空 + public void addAtIndex(int index, int val) { + if (index < 0 || index > size) { + return; + } + + //找到要插入节点的前驱 + ListNode pre = head; + for (int i = 0; i < index; i++) { + pre = pre.next; + } + ListNode newNode = new ListNode(val); + newNode.next = pre.next; + pre.next = newNode; + size++; + } + + public void deleteAtIndex(int index) { + if (index < 0 || index >= size) { + return; + } + + //因为有虚拟头节点,所以不用对index=0的情况进行特殊处理 + ListNode pre = head; + for (int i = 0; i < index ; i++) { + pre = pre.next; + } + pre.next = pre.next.next; + size--; + } +} +``` + +```Java +//双链表 +class MyLinkedList { + + class ListNode{ + int val; + ListNode next, prev; + ListNode(int val){ + this.val = val; + } + } + + //记录链表中元素的数量 + private int size; + //记录链表的虚拟头结点和尾结点 + private ListNode head, tail; + + public MyLinkedList() { + //初始化操作 + this.size = 0; + this.head = new ListNode(0); + this.tail = new ListNode(0); + //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!! + this.head.next = tail; + this.tail.prev = head; + } + + public int get(int index) { + //判断index是否有效 + if(index < 0 || index >= size){ + return -1; + } + ListNode cur = head; + //判断是哪一边遍历时间更短 + if(index >= size / 2){ + //tail开始 + cur = tail; + for(int i = 0; i < size - index; i++){ + cur = cur.prev; + } + }else{ + for(int i = 0; i <= index; i++){ + cur = cur.next; + } + } + return cur.val; + } + + public void addAtHead(int val) { + //等价于在第0个元素前添加 + addAtIndex(0, val); + } + + public void addAtTail(int val) { + //等价于在最后一个元素(null)前添加 + addAtIndex(size, val); + } + + public void addAtIndex(int index, int val) { + //判断index是否有效 + if(index < 0 || index > size){ + return; + } + + //找到前驱 + ListNode pre = head; + for(int i = 0; i < index; i++){ + pre = pre.next; + } + //新建结点 + ListNode newNode = new ListNode(val); + newNode.next = pre.next; + pre.next.prev = newNode; + newNode.prev = pre; + pre.next = newNode; + size++; + + } + + public void deleteAtIndex(int index) { + //判断index是否有效 + if(index < 0 || index >= size){ + return; + } + + //删除操作 + ListNode pre = head; + for(int i = 0; i < index; i++){ + pre = pre.next; + } + pre.next.next.prev = pre; + pre.next = pre.next.next; + size--; + } +} + +/** + * Your MyLinkedList object will be instantiated and called as such: + * MyLinkedList obj = new MyLinkedList(); + * int param_1 = obj.get(index); + * obj.addAtHead(val); + * obj.addAtTail(val); + * obj.addAtIndex(index,val); + * obj.deleteAtIndex(index); + */ +``` + +### Python: + +```python +(版本一)单链表法 +class ListNode: + def __init__(self, val=0, next=None): + self.val = val + self.next = next + +class MyLinkedList: + def __init__(self): + self.dummy_head = ListNode() + self.size = 0 + + def get(self, index: int) -> int: + if index < 0 or index >= self.size: + return -1 + + current = self.dummy_head.next + for i in range(index): + current = current.next + + return current.val + + def addAtHead(self, val: int) -> None: + self.dummy_head.next = ListNode(val, self.dummy_head.next) + self.size += 1 + + def addAtTail(self, val: int) -> None: + current = self.dummy_head + while current.next: + current = current.next + current.next = ListNode(val) + self.size += 1 + + def addAtIndex(self, index: int, val: int) -> None: + if index < 0 or index > self.size: + return + + current = self.dummy_head + for i in range(index): + current = current.next + current.next = ListNode(val, current.next) + self.size += 1 + + def deleteAtIndex(self, index: int) -> None: + if index < 0 or index >= self.size: + return + + current = self.dummy_head + for i in range(index): + current = current.next + current.next = current.next.next + self.size -= 1 + + +# Your MyLinkedList object will be instantiated and called as such: +# obj = MyLinkedList() +# param_1 = obj.get(index) +# obj.addAtHead(val) +# obj.addAtTail(val) +# obj.addAtIndex(index,val) +# obj.deleteAtIndex(index) +``` + + +```python +(版本二)双链表法 +class ListNode: + def __init__(self, val=0, prev=None, next=None): + self.val = val + self.prev = prev + self.next = next + +class MyLinkedList: + def __init__(self): + self.head = None + self.tail = None + self.size = 0 + + def get(self, index: int) -> int: + if index < 0 or index >= self.size: + return -1 + + if index < self.size // 2: + current = self.head + for i in range(index): + current = current.next + else: + current = self.tail + for i in range(self.size - index - 1): + current = current.prev + + return current.val + + def addAtHead(self, val: int) -> None: + new_node = ListNode(val, None, self.head) + if self.head: + self.head.prev = new_node + else: + self.tail = new_node + self.head = new_node + self.size += 1 + + def addAtTail(self, val: int) -> None: + new_node = ListNode(val, self.tail, None) + if self.tail: + self.tail.next = new_node + else: + self.head = new_node + self.tail = new_node + self.size += 1 + + def addAtIndex(self, index: int, val: int) -> None: + if index < 0 or index > self.size: + return + + if index == 0: + self.addAtHead(val) + elif index == self.size: + self.addAtTail(val) + else: + if index < self.size // 2: + current = self.head + for i in range(index - 1): + current = current.next + else: + current = self.tail + for i in range(self.size - index): + current = current.prev + new_node = ListNode(val, current, current.next) + current.next.prev = new_node + current.next = new_node + self.size += 1 + + def deleteAtIndex(self, index: int) -> None: + if index < 0 or index >= self.size: + return + + if index == 0: + self.head = self.head.next + if self.head: + self.head.prev = None + else: + self.tail = None + elif index == self.size - 1: + self.tail = self.tail.prev + if self.tail: + self.tail.next = None + else: + self.head = None + else: + if index < self.size // 2: + current = self.head + for i in range(index): + current = current.next + else: + current = self.tail + for i in range(self.size - index - 1): + current = current.prev + current.prev.next = current.next + current.next.prev = current.prev + self.size -= 1 + + + +# Your MyLinkedList object will be instantiated and called as such: +# obj = MyLinkedList() +# param_1 = obj.get(index) +# obj.addAtHead(val) +# obj.addAtTail(val) +# obj.addAtIndex(index,val) +# obj.deleteAtIndex(index) +``` + +### Go: + +```go +//单链表实现 +package main + +import ( + "fmt" +) + +type SingleNode struct { + Val int // 节点的值 + Next *SingleNode // 下一个节点的指针 +} + +type MyLinkedList struct { + dummyHead *SingleNode // 虚拟头节点 + Size int // 链表大小 +} + +func main() { + list := Constructor() // 初始化链表 + list.AddAtHead(100) // 在头部添加元素 + list.AddAtTail(242) // 在尾部添加元素 + list.AddAtTail(777) // 在尾部添加元素 + list.AddAtIndex(1, 99999) // 在指定位置添加元素 + list.printLinkedList() // 打印链表 +} + +/** Initialize your data structure here. */ +func Constructor() MyLinkedList { + newNode := &SingleNode{ // 创建新节点 + -999, + nil, + } + return MyLinkedList{ // 返回链表 + dummyHead: newNode, + Size: 0, + } + +} + +/** Get the value of the index-th node in the linked list. If the index is + invalid, return -1. */ +func (this *MyLinkedList) Get(index int) int { + /*if this != nil || index < 0 || index > this.Size { + return -1 + }*/ + if this == nil || index < 0 || index >= this.Size { // 如果索引无效则返回-1 + return -1 + } + // 让cur等于真正头节点 + cur := this.dummyHead.Next // 设置当前节点为真实头节点 + for i := 0; i < index; i++ { // 遍历到索引所在的节点 + cur = cur.Next + } + return cur.Val // 返回节点值 +} + +/** Add a node of value val before the first element of the linked list. After + the insertion, the new node will be the first node of the linked list. */ +func (this *MyLinkedList) AddAtHead(val int) { + // 以下两行代码可用一行代替 + // newNode := new(SingleNode) + // newNode.Val = val + newNode := &SingleNode{Val: val} // 创建新节点 + newNode.Next = this.dummyHead.Next // 新节点指向当前头节点 + this.dummyHead.Next = newNode // 新节点变为头节点 + this.Size++ // 链表大小增加1 +} + +/** Append a node of value val to the last element of the linked list. */ +func (this *MyLinkedList) AddAtTail(val int) { + newNode := &SingleNode{Val: val} // 创建新节点 + cur := this.dummyHead // 设置当前节点为虚拟头节点 + for cur.Next != nil { // 遍历到最后一个节点 + cur = cur.Next + } + cur.Next = newNode // 在尾部添加新节点 + this.Size++ // 链表大小增加1 +} + +/** Add a node of value val before the index-th node in the linked list. If + index equals to the length of linked list, the node will be appended to the + end of linked list. If index is greater than the length, the node will not be + inserted. */ +func (this *MyLinkedList) AddAtIndex(index int, val int) { + if index < 0 { // 如果索引小于0,设置为0 + index = 0 + } else if index > this.Size { // 如果索引大于链表长度,直接返回 + return + } + + newNode := &SingleNode{Val: val} // 创建新节点 + cur := this.dummyHead // 设置当前节点为虚拟头节点 + for i := 0; i < index; i++ { // 遍历到指定索引的前一个节点 + cur = cur.Next + } + newNode.Next = cur.Next // 新节点指向原索引节点 + cur.Next = newNode // 原索引的前一个节点指向新节点 + this.Size++ // 链表大小增加1 +} + +/** Delete the index-th node in the linked list, if the index is valid. */ +func (this *MyLinkedList) DeleteAtIndex(index int) { + if index < 0 || index >= this.Size { // 如果索引无效则直接返回 + return + } + cur := this.dummyHead // 设置当前节点为虚拟头节点 + for i := 0; i < index; i++ { // 遍历到要删除节点的前一个节点 + cur = cur.Next + } + if cur.Next != nil { + cur.Next = cur.Next.Next // 当前节点直接指向下下个节点,即删除了下一个节点 + } + this.Size-- // 注意删除节点后应将链表大小减一 +} + +// 打印链表 +func (list *MyLinkedList) printLinkedList() { + cur := list.dummyHead // 设置当前节点为虚拟头节点 + for cur.Next != nil { // 遍历链表 + fmt.Println(cur.Next.Val) // 打印节点值 + cur = cur.Next // 切换到下一个节点 + } +} + + +``` + +```go +//循环双链表 +type MyLinkedList struct { + dummy *Node +} + +type Node struct { + Val int + Next *Node + Pre *Node +} + +//仅保存哑节点,pre-> rear, next-> head +/** Initialize your data structure here. */ +func Constructor() MyLinkedList { + rear := &Node{ + Val: -1, + Next: nil, + Pre: nil, + } + rear.Next = rear + rear.Pre = rear + return MyLinkedList{rear} +} + +/** Get the value of the index-th node in the linked list. If the index is invalid, return -1. */ +func (this *MyLinkedList) Get(index int) int { + head := this.dummy.Next + //head == this, 遍历完全 + for head != this.dummy && index > 0 { + index-- + head = head.Next + } + //否则, head == this, 索引无效 + if 0 != index { + return -1 + } + return head.Val +} + +/** Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. */ +func (this *MyLinkedList) AddAtHead(val int) { + dummy := this.dummy + node := &Node{ + Val: val, + //head.Next指向原头节点 + Next: dummy.Next, + //head.Pre 指向哑节点 + Pre: dummy, + } + + //更新原头节点 + dummy.Next.Pre = node + //更新哑节点 + dummy.Next = node + //以上两步不能反 +} + +/** Append a node of value val to the last element of the linked list. */ +func (this *MyLinkedList) AddAtTail(val int) { + dummy := this.dummy + rear := &Node{ + Val: val, + //rear.Next = dummy(哑节点) + Next: dummy, + //rear.Pre = ori_rear + Pre: dummy.Pre, + } + + //ori_rear.Next = rear + dummy.Pre.Next = rear + //update dummy + dummy.Pre = rear + //以上两步不能反 +} + +/** Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. */ +func (this *MyLinkedList) AddAtIndex(index int, val int) { + head := this.dummy.Next + //head = MyLinkedList[index] + for head != this.dummy && index > 0 { + head = head.Next + index-- + } + if index > 0 { + return + } + node := &Node{ + Val: val, + //node.Next = MyLinkedList[index] + Next: head, + //node.Pre = MyLinkedList[index-1] + Pre: head.Pre, + } + //MyLinkedList[index-1].Next = node + head.Pre.Next = node + //MyLinkedList[index].Pre = node + head.Pre = node + //以上两步不能反 +} + +/** Delete the index-th node in the linked list, if the index is valid. */ +func (this *MyLinkedList) DeleteAtIndex(index int) { + //链表为空 + if this.dummy.Next == this.dummy { + return + } + head := this.dummy.Next + //head = MyLinkedList[index] + for head.Next != this.dummy && index > 0 { + head = head.Next + index-- + } + //验证index有效 + if index == 0 { + //MyLinkedList[index].Pre = index[index-2] + head.Next.Pre = head.Pre + //MyLinedList[index-2].Next = index[index] + head.Pre.Next = head.Next + //以上两步顺序无所谓 + } +} +``` + +### JavaScript: + +```js + +class LinkNode { + constructor(val, next) { + this.val = val; + this.next = next; + } +} + +/** + * Initialize your data structure here. + * 单链表 储存头尾节点 和 节点数量 + */ +var MyLinkedList = function() { + this._size = 0; + this._tail = null; + this._head = null; +}; + +/** + * Get the value of the index-th node in the linked list. If the index is invalid, return -1. + * @param {number} index + * @return {number} + */ +MyLinkedList.prototype.getNode = function(index) { + if(index < 0 || index >= this._size) return null; + // 创建虚拟头节点 + let cur = new LinkNode(0, this._head); + // 0 -> head + while(index-- >= 0) { + cur = cur.next; + } + return cur; +}; +MyLinkedList.prototype.get = function(index) { + if(index < 0 || index >= this._size) return -1; + // 获取当前节点 + return this.getNode(index).val; +}; + +/** + * Add a node of value val before the first element of the linked list. After the insertion, the new node will be the first node of the linked list. + * @param {number} val + * @return {void} + */ +MyLinkedList.prototype.addAtHead = function(val) { + const node = new LinkNode(val, this._head); + this._head = node; + this._size++; + if(!this._tail) { + this._tail = node; + } +}; + +/** + * Append a node of value val to the last element of the linked list. + * @param {number} val + * @return {void} + */ +MyLinkedList.prototype.addAtTail = function(val) { + const node = new LinkNode(val, null); + this._size++; + if(this._tail) { + this._tail.next = node; + this._tail = node; + return; + } + this._tail = node; + this._head = node; +}; + +/** + * Add a node of value val before the index-th node in the linked list. If index equals to the length of linked list, the node will be appended to the end of linked list. If index is greater than the length, the node will not be inserted. + * @param {number} index + * @param {number} val + * @return {void} + */ +MyLinkedList.prototype.addAtIndex = function(index, val) { + if(index > this._size) return; + if(index <= 0) { + this.addAtHead(val); + return; + } + if(index === this._size) { + this.addAtTail(val); + return; + } + // 获取目标节点的上一个的节点 + const node = this.getNode(index - 1); + node.next = new LinkNode(val, node.next); + this._size++; +}; + +/** + * Delete the index-th node in the linked list, if the index is valid. + * @param {number} index + * @return {void} + */ +MyLinkedList.prototype.deleteAtIndex = function(index) { + if(index < 0 || index >= this._size) return; + if(index === 0) { + this._head = this._head.next; + // 如果删除的这个节点同时是尾节点,要处理尾节点 + if(index === this._size - 1){ + this._tail = this._head + } + this._size--; + return; + } + // 获取目标节点的上一个的节点 + const node = this.getNode(index - 1); + node.next = node.next.next; + // 处理尾节点 + if(index === this._size - 1) { + this._tail = node; + } + this._size--; +}; + +// MyLinkedList.prototype.out = function() { +// let cur = this._head; +// const res = []; +// while(cur) { +// res.push(cur.val); +// cur = cur.next; +// } +// }; +/** + * Your MyLinkedList object will be instantiated and called as such: + * var obj = new MyLinkedList() + * var param_1 = obj.get(index) + * obj.addAtHead(val) + * obj.addAtTail(val) + * obj.addAtIndex(index,val) + * obj.deleteAtIndex(index) + */ +``` + +```js +/** + 定义双头节点的结构:同时包含前指针`prev`和后指针next` +*/ +class Node { + constructor(val, prev, next) { + this.val = val + this.prev = prev + this.next = next + } +} + +/** + 双链表:维护 `head` 和 `tail` 两个哨兵节点,这样可以简化对于中间节点的操作 + 并且维护 `size`,使得能够以O(1)时间判断操作是否合法 +*/ +var MyLinkedList = function () { + this.tail = new Node(-1) + this.head = new Node(-1) + this.tail.prev = this.head + this.head.next = this.tail + this.size = 0 +}; + +/** + * 获取在index处节点的值 + * + * @param {number} index + * @return {number} + * + * 时间复杂度: O(n) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.get = function (index) { + // 当索引超出范围时,返回-1 + if (index > this.size) { + return -1 + } + + let cur = this.head + for (let i = 0; i <= index; i++) { + cur = cur.next + } + + return cur.val +}; + +/** + * 在链表头部添加一个新节点 + * + * @param {number} val + * @return {void} + * + * 时间复杂度: O(1) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.addAtHead = function (val) { + /** + head <-> [newNode] <-> originNode + */ + this.size++ + const originNode = this.head.next + // 创建新节点,并建立连接 + const newNode = new Node(val, this.head, originNode) + + // 取消原前后结点的连接 + this.head.next = newNode + originNode.prev = newNode +}; + +/** + * 在链表尾部添加一个新节点 + * + * @param {number} val + * @return {void} + * + * 时间复杂度: O(1) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.addAtTail = function (val) { + /** + originNode <-> [newNode] <-> tail + */ + this.size++ + const originNode = this.tail.prev + + // 创建新节点,并建立连接 + const newNode = new Node(val, originNode, this.tail) + + // 取消原前后结点的连接 + this.tail.prev = newNode + originNode.next = newNode +}; + +/** + * 在指定索引位置前添加一个新节点 + * + * @param {number} index + * @param {number} val + * @return {void} + * + * 时间复杂度: O(n) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.addAtIndex = function (index, val) { + // 当索引超出范围时,直接返回 + if (index > this.size) { + return + } + this.size++ + + let cur = this.head + for (let i = 0; i < index; i++) { + cur = cur.next + } + + const new_next = cur.next + + // 创建新节点,并建立连接 + const node = new Node(val, cur, new_next) + + // 取消原前后结点的连接 + cur.next = node + new_next.prev = node +}; + +/** + * 删除指定索引位置的节点 + * + * @param {number} index + * @return {void} + * + * 时间复杂度: O(n) + * 空间复杂度: O(1) + */ +MyLinkedList.prototype.deleteAtIndex = function (index) { + // 当索引超出范围时,直接返回 + if (index >= this.size) { + return + } + + this.size-- + let cur = this.head + for (let i = 0; i < index; i++) { + cur = cur.next + } + + const new_next = cur.next.next + // 取消原前后结点的连接 + new_next.prev = cur + cur.next = new_next +}; +``` + +### TypeScript: + +```TypeScript +class ListNode { + public val: number; + public next: ListNode | null; + constructor(val?: number, next?: ListNode | null) { + this.val = val === undefined ? 0 : val; + this.next = next === undefined ? null : next; + } +} + +class MyLinkedList { + // 记录链表长度 + private size: number; + private head: ListNode | null; + private tail: ListNode | null; + constructor() { + this.size = 0; + this.head = null; + this.tail = null; + } + + // 获取链表中第 index个节点的值 + get(index: number): number { + // 索引无效的情况 + if (index < 0 || index >= this.size) { + return -1; + } + let curNode = this.getNode(index); + // 这里在前置条件下,理论上不会出现 null的情况 + return curNode.val; + } + + // 在链表的第一个元素之前添加一个值为 val的节点。插入后,新节点将成为链表的第一个节点。 + addAtHead(val: number): void { + let node: ListNode = new ListNode(val, this.head); + this.head = node; + if (!this.tail) { + this.tail = node; + } + this.size++; + } + + // 将值为 val 的节点追加到链表的最后一个元素。 + addAtTail(val: number): void { + let node: ListNode = new ListNode(val, null); + if (this.tail) { + this.tail.next = node; + } else { + // 还没有尾节点,说明一个节点都还没有 + this.head = node; + } + this.tail = node; + this.size++; + } + + // 在链表中的第 index个节点之前添加值为 val的节点。 + // 如果 index等于链表的长度,则该节点将附加到链表的末尾。如果 index大于链表长度,则不会插入节点。如果 index小于0,则在头部插入节点。 + addAtIndex(index: number, val: number): void { + if (index === this.size) { + this.addAtTail(val); + return; + } + if (index > this.size) { + return; + } + // <= 0 的情况都是在头部插入 + if (index <= 0) { + this.addAtHead(val); + return; + } + // 正常情况 + // 获取插入位置的前一个 node + let curNode = this.getNode(index - 1); + let node: ListNode = new ListNode(val, curNode.next); + curNode.next = node; + this.size++; + } + + // 如果索引 index有效,则删除链表中的第 index个节点。 + deleteAtIndex(index: number): void { + if (index < 0 || index >= this.size) { + return; + } + // 处理头节点 + if (index === 0) { + this.head = this.head!.next; + // 如果链表中只有一个元素,删除头节点后,需要处理尾节点 + if (index === this.size - 1) { + this.tail = null + } + this.size--; + return; + } + // 索引有效 + let curNode: ListNode = this.getNode(index - 1); + curNode.next = curNode.next!.next; + // 处理尾节点 + if (index === this.size - 1) { + this.tail = curNode; + } + this.size--; + } + + // 获取指定 Node节点 + private getNode(index: number): ListNode { + // 这里不存在没办法获取到节点的情况,都已经在前置方法做过判断 + // 创建虚拟头节点 + let curNode: ListNode = new ListNode(0, this.head); + for (let i = 0; i <= index; i++) { + // 理论上不会出现 null + curNode = curNode.next!; + } + return curNode; + } +} +``` + +### Kotlin: + +```kotlin +class MyLinkedList { + + var next: ListNode? = null + + var size: Int = 0 + + fun get(index: Int): Int { + if (index + 1 > size) return -1 + var cur = this.next + for (i in 0 until index) { + cur = cur?.next + } + return cur?.`val` ?: -1 + } + + fun addAtHead(`val`: Int) { + val head = ListNode(`val`) + head.next = this.next + this.next = head + size++ + } + + fun addAtTail(`val`: Int) { + val pre = ListNode(0) + pre.next = this.next + var cur: ListNode? = pre + while (cur?.next != null) { + cur = cur.next + } + cur?.next = ListNode(`val`) + this.next = pre.next + size++ + } + + fun addAtIndex(index: Int, `val`: Int) { + if (index > size) return + val pre = ListNode(0) + pre.next = this.next + var cur:ListNode? = pre + for (i in 0 until index) { + cur = cur?.next + } + val temp = cur?.next + cur?.next = ListNode(`val`) + cur?.next?.next = temp + this.next = pre.next + size++ + } + + fun deleteAtIndex(index: Int) { + if (index + 1 > size) return + val pre = ListNode(0) + pre.next = this.next + var cur: ListNode? = pre + for (i in 0 until index) { + cur = cur?.next + } + val temp = cur?.next?.next + cur?.next?.next = null + cur?.next = temp + this.next = pre.next + size-- + } +} +``` + +### Swift: + +```swift +class MyLinkedList { + var dummyHead: ListNode? + var size: Int + + init() { + dummyHead = ListNode(0) + size = 0 + } + + func get(_ index: Int) -> Int { + if index >= size || index < 0 { + return -1 + } + + var curNode = dummyHead?.next + var curIndex = index + + while curIndex > 0 { + curNode = curNode?.next + curIndex -= 1 + } + + return curNode?.value ?? -1 + } + + func addAtHead(_ val: Int) { + let newHead = ListNode(val) + newHead.next = dummyHead?.next + dummyHead?.next = newHead + size += 1 + } + + func addAtTail(_ val: Int) { + let newNode = ListNode(val) + var curNode = dummyHead + while curNode?.next != nil { + curNode = curNode?.next + } + + curNode?.next = newNode + size += 1 + } + + func addAtIndex(_ index: Int, _ val: Int) { + if index > size { + return + } + + let newNode = ListNode(val) + var curNode = dummyHead + var curIndex = index + + while curIndex > 0 { + curNode = curNode?.next + curIndex -= 1 + } + + newNode.next = curNode?.next + curNode?.next = newNode + size += 1 + } + + func deleteAtIndex(_ index: Int) { + if index >= size || index < 0 { + return + } + + var curNode = dummyHead + for _ in 0..= size) { + return -1; + } + var cur = dummy + for (i <- 0 to index) { + cur = cur.next + } + cur.x // 返回cur的值 + } + + // 在链表最前面插入一个节点 + def addAtHead(`val`: Int) { + addAtIndex(0, `val`) + } + + // 在链表最后面插入一个节点 + def addAtTail(`val`: Int) { + addAtIndex(size, `val`) + } + + // 在第index个节点之前插入一个新节点 + // 如果index等于链表长度,则说明新插入的节点是尾巴 + // 如果index等于0,则说明新插入的节点是头 + // 如果index>链表长度,则说明为空 + def addAtIndex(index: Int, `val`: Int) { + if (index > size) { + return + } + var loc = index // 因为参数index是val不可变类型,所以需要赋值给一个可变类型 + if (index < 0) { + loc = 0 + } + size += 1 //链表尺寸+1 + var pre = dummy + for (i <- 0 until loc) { + pre = pre.next + } + val node: ListNode = new ListNode(`val`, pre.next) + pre.next = node + } + // 删除第index个节点 + def deleteAtIndex(index: Int) { + if (index < 0 || index >= size) { + return + } + size -= 1 + var pre = dummy + for (i <- 0 until index) { + pre = pre.next + } + pre.next = pre.next.next + } + +} +``` + +### Rust: + +```rust +#[derive(Debug)] +pub struct MyLinkedList { + pub val: i32, + pub next: Option>, +} + +impl MyLinkedList { + fn new() -> Self { + // 增加头节点 + MyLinkedList { val: 0, next: None } + } + + fn get(&self, index: i32) -> i32 { + if index < 0 { + return -1; + } + let mut i = 0; + let mut cur = &self.next; + while let Some(node) = cur { + if i == index { + return node.val; + } + i += 1; + cur = &node.next; + } + -1 + } + + fn add_at_head(&mut self, val: i32) { + let new_node = Box::new(MyLinkedList { + val, + next: self.next.take(), + }); + self.next = Some(new_node); + } + + fn add_at_tail(&mut self, val: i32) { + let new_node = Box::new(MyLinkedList { val, next: None }); + let mut last_node = &mut self.next; + while let Some(node) = last_node { + last_node = &mut node.next; + } + *last_node = Some(new_node); + } + + fn add_at_index(&mut self, index: i32, val: i32) { + if index <= 0 { + self.add_at_head(val); + } else { + let mut i = 0; + let mut cur = &mut self.next; + while let Some(node) = cur { + if i + 1 == index { + let new_node = Box::new(MyLinkedList { + val, + next: node.next.take(), + }); + node.next = Some(new_node); + break; + } + i += 1; + cur = &mut node.next; + } + } + } + + fn delete_at_index(&mut self, index: i32) { + if index < 0 { + return; + } + + let mut i = 0; + let mut cur = self; + while let Some(node) = cur.next.take() { + if i == index { + cur.next = node.next; + break; + } + i += 1; + cur.next = Some(node); + cur = cur.next.as_mut().unwrap(); + } + } +} +``` + +### C# +```csharp +class ListNode +{ + public int val; + public ListNode next; + public ListNode(int val) { this.val = val; } +} +public class MyLinkedList +{ + ListNode dummyHead; + int count; + + public MyLinkedList() + { + dummyHead = new ListNode(0); + count = 0; + } + + public int Get(int index) + { + if (index < 0 || count <= index) return -1; + ListNode current = dummyHead; + for (int i = 0; i <= index; i++) + { + current = current.next; + } + return current.val; + } + + public void AddAtHead(int val) + { + AddAtIndex(0, val); + } + + public void AddAtTail(int val) + { + AddAtIndex(count, val); + } + + public void AddAtIndex(int index, int val) + { + if (index > count) return; + index = Math.Max(0, index); + count++; + ListNode tmp1 = dummyHead; + for (int i = 0; i < index; i++) + { + tmp1 = tmp1.next; + } + ListNode tmp2 = new ListNode(val); + tmp2.next = tmp1.next; + tmp1.next = tmp2; + } + + public void DeleteAtIndex(int index) + { + + if (index >= count || index < 0) return; + var tmp1 = dummyHead; + for (int i = 0; i < index; i++) + { + tmp1 = tmp1.next; + } + tmp1.next = tmp1.next.next; + count--; + + } +} +``` + + diff --git "a/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271.md" "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271.md" new file mode 100755 index 0000000000..fb095d7518 --- /dev/null +++ "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271.md" @@ -0,0 +1,361 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 714. 买卖股票的最佳时机含手续费 + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) + +给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。 + +你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。 + +返回获得利润的最大值。 + +注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。 + +示例 1: +* 输入: prices = [1, 3, 2, 8, 4, 9], fee = 2 +* 输出: 8 + +解释: 能够达到的最大利润: +* 在此处买入 prices[0] = 1 +* 在此处卖出 prices[3] = 8 +* 在此处买入 prices[4] = 4 +* 在此处卖出 prices[5] = 9 +* 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8. + +注意: +* 0 < prices.length <= 50000. +* 0 < prices[i] < 50000. +* 0 <= fee < 50000. + +## 思路 + +本题优先掌握动态规划解法,在动态规划章节中,还会详细讲解本题。 + +本题相对于[贪心算法:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html),多添加了一个条件就是手续费。 + +### 贪心算法 + +在[贪心算法:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html)中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。 + +而本题有了手续费,就要关心什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以扣减手续费的情况。 + +如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。 + +此时无非就是要找到两个点,买入日期,和卖出日期。 + +* 买入日期:其实很好想,遇到更低点就记录一下。 +* 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。 + +所以我们在做收获利润操作的时候其实有三种情况: + +* 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。 +* 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。 +* 情况三:不作操作,保持原有状态(买入,卖出,不买不卖) + +贪心算法C++代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices, int fee) { + int result = 0; + int minPrice = prices[0]; // 记录最低价格 + for (int i = 1; i < prices.size(); i++) { + // 情况二:相当于买入 + if (prices[i] < minPrice) minPrice = prices[i]; + + // 情况三:保持原有状态(因为此时买则不便宜,卖则亏本) + if (prices[i] >= minPrice && prices[i] <= minPrice + fee) { + continue; + } + + // 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出 + if (prices[i] > minPrice + fee) { + result += prices[i] - minPrice - fee; + minPrice = prices[i] - fee; // 情况一,这一步很关键,避免重复扣手续费 + } + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,**所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!** + +大家也可以发现,情况三,那块代码是可以删掉的,我是为了让代码表达清晰,所以没有精简。 + +### 动态规划 + +我在公众号「代码随想录」里将在下一个系列详细讲解动态规划,所以本题解先给出我的C++代码(带详细注释),感兴趣的同学可以自己先学习一下。 + +相对于[贪心算法:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html)的动态规划解法中,只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 + +C++代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices, int fee) { + // dp[i][1]第i天持有的最多现金 + // dp[i][0]第i天持有股票所剩的最多现金 + int n = prices.size(); + vector> dp(n, vector(2, 0)); + dp[0][0] -= prices[0]; // 持股票 + for (int i = 1; i < n; i++) { + 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 max(dp[n - 1][0], dp[n - 1][1]); + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +当然可以对空间进行优化,因为当前状态只是依赖前一个状态。 + +C++ 代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices, int fee) { + int n = prices.size(); + int holdStock = (-1) * prices[0]; // 持股票 + int saleStock = 0; // 卖出股票 + for (int i = 1; i < n; i++) { + int previousHoldStock = holdStock; + holdStock = max(holdStock, saleStock - prices[i]); + saleStock = max(saleStock, previousHoldStock + prices[i] - fee); + } + return saleStock; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +## 总结 + +本题贪心的思路其实是比较难的,动态规划才是常规做法,但也算是给大家拓展一下思路,感受一下贪心的魅力。 + +后期我们在讲解 股票问题系列的时候,会用动规的方式把股票问题穿个线。 + + +## 其他语言版本 + +### Java +```java +// 贪心思路 +class Solution { + public int maxProfit(int[] prices, int fee) { + int buy = prices[0] + fee; + int sum = 0; + for (int p : prices) { + if (p + fee < buy) { + buy = p + fee; + } else if (p > buy){ + sum += p - buy; + buy = p; + } + } + return sum; + } +} +``` + +```java +class Solution { // 动态规划 + public int maxProfit(int[] prices, int fee) { + if (prices == null || prices.length < 2) { + return 0; + } + + int[][] dp = new int[prices.length][2]; + + // base case + dp[0][0] = 0; + dp[0][1] = -prices[0]; + + for (int i = 1; i < prices.length; i++) { + 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], dp[i - 1][0] - prices[i]); + } + + return dp[prices.length - 1][0]; + } +} +``` + + + +### Python + +```python +class Solution: # 贪心思路 + def maxProfit(self, prices: List[int], fee: int) -> int: + result = 0 + minPrice = prices[0] + for i in range(1, len(prices)): + if prices[i] < minPrice: # 此时有更低的价格,可以买入 + minPrice = prices[i] + elif prices[i] > (minPrice + fee): # 此时有利润,同时假买入高价的股票,看看是否继续盈利 + result += prices[i] - (minPrice + fee) + minPrice = prices[i] - fee + else: # minPrice<= prices[i] <= minPrice + fee, 价格处于minPrice和minPrice+fee之间,不做操作 + continue + return result +``` + +### Go +```go +func maxProfit(prices []int, fee int) int { + var minBuy int = prices[0] //第一天买入 + var res int + for i := 0; i < len(prices); i++ { + //如果当前价格小于最低价,则在此处买入 + if prices[i] < minBuy { + minBuy = prices[i] + } + //如果以当前价格卖出亏本,则不卖,继续找下一个可卖点 + if prices[i] >= minBuy && prices[i]-fee-minBuy <= 0 { + continue + } + //可以售卖了 + if prices[i] > minBuy+fee { + //累加每天的收益 + res += prices[i]-minBuy-fee + //更新最小值(如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,所以要让minBuy = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!) + minBuy = prices[i]-fee + } + } + return res +} +``` +### JavaScript +```Javascript +// 贪心思路 +var maxProfit = function(prices, fee) { + let result = 0 + let minPrice = prices[0] + for(let i = 1; i < prices.length; i++) { + if(prices[i] < minPrice) { + minPrice = prices[i] + } + if(prices[i] >= minPrice && prices[i] <= minPrice + fee) { + continue + } + + if(prices[i] > minPrice + fee) { + result += prices[i] - minPrice - fee + // 买入和卖出只需要支付一次手续费 + minPrice = prices[i] -fee + } + } + return result +}; + +// 动态规划 +/** + * @param {number[]} prices + * @param {number} fee + * @return {number} + */ +var maxProfit = function(prices, fee) { + // 滚动数组 + // have表示当天持有股票的最大收益 + // notHave表示当天不持有股票的最大收益 + // 把手续费算在买入价格中 + let n = prices.length, + have = -prices[0]-fee, // 第0天持有股票的最大收益 + notHave = 0; // 第0天不持有股票的最大收益 + for (let i = 1; i < n; i++) { + // 第i天持有股票的最大收益由两种情况组成 + // 1、第i-1天就已经持有股票,第i天什么也没做 + // 2、第i-1天不持有股票,第i天刚买入 + have = Math.max(have, notHave - prices[i] - fee); + // 第i天不持有股票的最大收益由两种情况组成 + // 1、第i-1天就已经不持有股票,第i天什么也没做 + // 2、第i-1天持有股票,第i天刚卖出 + notHave = Math.max(notHave, have + prices[i]); + } + // 最后手中不持有股票,收益才能最大化 + return notHave; +}; +``` + +### TypeScript + +> 贪心 + +```typescript +function maxProfit(prices: number[], fee: number): number { + if (prices.length === 0) return 0; + let minPrice: number = prices[0]; + let profit: number = 0; + for (let i = 1, length = prices.length; i < length; i++) { + if (minPrice > prices[i]) { + minPrice = prices[i]; + } + if (minPrice + fee < prices[i]) { + profit += prices[i] - minPrice - fee; + minPrice = prices[i] - fee; + } + } + return profit; +}; +``` + +> 动态规划 + +```typescript +function maxProfit(prices: number[], fee: number): number { + /** + dp[i][1]: 第i天不持有股票的最大所剩现金 + dp[i][0]: 第i天持有股票的最大所剩现金 + */ + const length: number = prices.length; + const dp: number[][] = new Array(length).fill(0).map(_ => []); + dp[0][1] = 0; + dp[0][0] = -prices[0]; + for (let i = 1, length = prices.length; i < length; i++) { + dp[i][1] = Math.max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + } + return Math.max(dp[length - 1][0], dp[length - 1][1]); +}; +``` + +### Scala + +贪心思路: + +```scala +object Solution { + def maxProfit(prices: Array[Int], fee: Int): Int = { + var result = 0 + var minPrice = prices(0) + for (i <- 1 until prices.length) { + if (prices(i) < minPrice) { + minPrice = prices(i) // 比当前最小值还小 + } + if (prices(i) > minPrice + fee) { + result += prices(i) - minPrice - fee + minPrice = prices(i) - fee + } + } + result + } +} +``` + + diff --git "a/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" new file mode 100755 index 0000000000..ebed4a0b30 --- /dev/null +++ "b/problems/0714.\344\271\260\345\215\226\350\202\241\347\245\250\347\232\204\346\234\200\344\275\263\346\227\266\346\234\272\345\220\253\346\211\213\347\273\255\350\264\271\357\274\210\345\212\250\346\200\201\350\247\204\345\210\222\357\274\211.md" @@ -0,0 +1,337 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 714.买卖股票的最佳时机含手续费 + +[力扣题目链接](https://leetcode.cn/problems/best-time-to-buy-and-sell-stock-with-transaction-fee/) + +给定一个整数数组 prices,其中第 i 个元素代表了第 i 天的股票价格 ;非负整数 fee 代表了交易股票的手续费用。 + +你可以无限次地完成交易,但是你每笔交易都需要付手续费。如果你已经购买了一个股票,在卖出它之前你就不能再继续购买股票了。 + +返回获得利润的最大值。 + +注意:这里的一笔交易指买入持有并卖出股票的整个过程,每笔交易你只需要为支付一次手续费。 + +示例 1: +* 输入: prices = [1, 3, 2, 8, 4, 9], fee = 2 +* 输出: 8 + +解释: 能够达到的最大利润: +* 在此处买入 prices[0] = 1 +* 在此处卖出 prices[3] = 8 +* 在此处买入 prices[4] = 4 +* 在此处卖出 prices[5] = 9 +* 总利润: ((8 - 1) - 2) + ((9 - 4) - 2) = 8. + +注意: +* 0 < prices.length <= 50000. +* 0 < prices[i] < 50000. +* 0 <= fee < 50000. + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划来决定最佳时机,这次含手续费!| LeetCode:714.买卖股票的最佳时机含手续费](https://www.bilibili.com/video/BV1z44y1Z7UR),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题贪心解法:[贪心算法:买卖股票的最佳时机含手续费](https://programmercarl.com/0714.买卖股票的最佳时机含手续费.html) + +性能是: + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +本题使用贪心算法并不好理解,也很容易出错,那么我们再来看看使用动规的方法如何解题。 + +相对于[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html),本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 + +唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。 + +这里重申一下dp数组的含义: + +dp[i][0] 表示第i天持有股票所得最多现金。 +dp[i][1] 表示第i天不持有股票所得最多现金 + + +如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 +* 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] +* 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i] + + +所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + + +在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来 +* 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] +* 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,**注意这里需要有手续费了**即:dp[i - 1][0] + prices[i] - fee + +所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); + +**本题和[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html)的区别就是这里需要多一个减去手续费的操作**。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices, int fee) { + int n = prices.size(); + vector> dp(n, vector(2, 0)); + dp[0][0] -= prices[0]; // 持股票 + for (int i = 1; i < n; i++) { + 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 max(dp[n - 1][0], dp[n - 1][1]); + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +## 其他语言版本 + +### Java: + +```java +/** + * 卖出时支付手续费 + * @param prices + * @param fee + * @return + */ +public int maxProfit(int[] prices, int fee) { + int len = prices.length; + // 0 : 持股(买入) + // 1 : 不持股(售出) + // dp 定义第i天持股/不持股 所得最多现金 + int[][] dp = new int[len][2]; + dp[0][0] = -prices[0]; + for (int i = 1; i < len; i++) { + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + dp[i][1] = Math.max(dp[i - 1][0] + prices[i] - fee, dp[i - 1][1]); + } + return Math.max(dp[len - 1][0], dp[len - 1][1]); +} + +/** + * 买入时支付手续费 + * @param prices + * @param fee + * @return + */ +public int maxProfit(int[] prices, int fee) { + int len = prices.length; + // 0 : 持股(买入) + // 1 : 不持股(售出) + // dp 定义第i天持股/不持股 所得最多现金 + int[][] dp = new int[len][2]; + // 考虑买入的时候就支付手续费 + dp[0][0] = -prices[0] - fee; + for (int i = 1; i < len; i++) { + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i] - fee); + dp[i][1] = Math.max(dp[i - 1][0] + prices[i], dp[i - 1][1]); + } + return Math.max(dp[len - 1][0], dp[len - 1][1]); +} + +// 一维数组优化 +class Solution { + public int maxProfit(int[] prices, int fee) { + int[] dp = new int[2]; + dp[0] = -prices[0]; + dp[1] = 0; + for (int i = 1; i <= prices.length; i++) { + dp[0] = Math.max(dp[0], dp[1] - prices[i - 1]); + dp[1] = Math.max(dp[1], dp[0] + prices[i - 1] - fee); + } + return dp[1]; + } +} +```Java +//使用 2*2 array +class Solution { + public int maxProfit(int[] prices, int fee) { + int dp[][] = new int[2][2]; + int len = prices.length; + //[i][0] = holding the stock + //[i][1] = not holding the stock + dp[0][0] = -prices[0]; + + for(int i = 1; i < len; i++){ + dp[i % 2][0] = Math.max(dp[(i - 1) % 2][0], dp[(i - 1) % 2][1] - prices[i]); + dp[i % 2][1] = Math.max(dp[(i - 1) % 2][1], dp[(i - 1) % 2][0] + prices[i] - fee); + } + + return dp[(len - 1) % 2][1]; + } +} +``` +### python + + +```python +class Solution: + def maxProfit(self, prices: List[int], fee: int) -> int: + n = len(prices) + dp = [[0] * 2 for _ in range(n)] + dp[0][0] = -prices[0] #持股票 + for i in range(1, n): + 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 max(dp[-1][0], dp[-1][1]) +``` + +```python +class Solution: + def maxProfit(self, prices: List[int], fee: int) -> int: + # 持有股票手上的最大現金 + hold = -prices[0] - fee + # 不持有股票手上的最大現金 + not_hold = 0 + for price in prices[1:]: + new_hold = max(hold, not_hold - price - fee) + new_not_hold = max(not_hold, hold + price) + hold, not_hold = new_hold, new_not_hold + return not_hold +``` + +### Go: + +```go +// 买卖股票的最佳时机含手续费 动态规划 +// 时间复杂度O(n) 空间复杂度O(n) +func maxProfit(prices []int, fee int) int { + n := len(prices) + dp := make([][2]int, n) + dp[0][0] = -prices[0] + for i := 1; i < n; i++ { + dp[i][1] = max(dp[i-1][1], dp[i-1][0] + prices[i] - fee) + dp[i][0] = max(dp[i-1][0], dp[i-1][1] - prices[i]) + } + return dp[n-1][1] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +### JavaScript: + +```javascript +const maxProfit = (prices,fee) => { + let dp = Array.from(Array(prices.length), () => Array(2).fill(0)); + dp[0][0] = 0 - prices[0]; + for (let i = 1; i < prices.length; i++) { + dp[i][0] = Math.max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + dp[i][1] = Math.max(dp[i - 1][0] + prices[i] - fee, dp[i - 1][1]); + } + return Math.max(dp[prices.length - 1][0], dp[prices.length - 1][1]); +} +``` + +### TypeScript: + +```typescript +function maxProfit(prices: number[], fee: number): number { + /** + dp[i][0]:持有股票 + dp[i][1]: 不持有 + */ + const length: number = prices.length; + if (length === 0) return 0; + const dp: number[][] = new Array(length).fill(0).map(_ => []); + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for (let i = 1; i < length; 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], dp[i - 1][0] + prices[i] - fee); + } + return dp[length - 1][1]; +}; +``` + +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +// dp[i][0] 表示第i天持有股票所省最多现金。 +// dp[i][1] 表示第i天不持有股票所得最多现金 +int maxProfit(int* prices, int pricesSize, int fee) { + int dp[pricesSize][2]; + dp[0][0] = -prices[0]; + dp[0][1] = 0; + for (int i = 1; i < pricesSize; ++i) { + 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[pricesSize - 1][1]; +} +``` + + + +### Rust: + +**贪心** + +```Rust +impl Solution { + pub fn max_profit(prices: Vec, fee: i32) -> i32 { + let mut result = 0; + let mut min_price = prices[0]; + for i in 1..prices.len() { + if prices[i] < min_price { min_price = prices[i]; } + + // if prices[i] >= min_price && prices[i] <= min_price + fee { continue; } + + if prices[i] > min_price + fee { + result += prices[i] - min_price - fee; + min_price = prices[i] - fee; + } + } + result + } +} +``` + +**动态规划** +```Rust +impl Solution { + pub fn max_profit(prices: Vec, fee: i32) -> i32 { + let mut dp = vec![vec![0; 2]; prices.len()]; + dp[0][0] = -prices[0]; + for (i, &p) in prices.iter().enumerate().skip(1) { + dp[i][0] = dp[i - 1][0].max(dp[i - 1][1] - p); + dp[i][1] = dp[i - 1][1].max(dp[i - 1][0] + p - fee); + } + dp[prices.len() - 1][1] + } +} +``` + +**动态规划空间优化** + +```rust +impl Solution { + pub fn max_profit(prices: Vec, fee: i32) -> i32 { + let (mut low, mut res) = (-prices[0], 0); + for p in prices { + low = low.max(res - p); + res = res.max(p + low - fee); + } + res + } +} +``` + + diff --git "a/problems/0718.\346\234\200\351\225\277\351\207\215\345\244\215\345\255\220\346\225\260\347\273\204.md" "b/problems/0718.\346\234\200\351\225\277\351\207\215\345\244\215\345\255\220\346\225\260\347\273\204.md" new file mode 100755 index 0000000000..12384a57a7 --- /dev/null +++ "b/problems/0718.\346\234\200\351\225\277\351\207\215\345\244\215\345\255\220\346\225\260\347\273\204.md" @@ -0,0 +1,602 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 718. 最长重复子数组 + +[力扣题目链接](https://leetcode.cn/problems/maximum-length-of-repeated-subarray/) + +给两个整数数组 A 和 B ,返回两个数组中公共的、长度最长的子数组的长度。 + +示例: + +输入: +* A: [1,2,3,2,1] +* B: [3,2,1,4,7] +* 输出:3 +* 解释:长度最长的公共子数组是 [3, 2, 1] 。 + +提示: + +* 1 <= len(A), len(B) <= 1000 +* 0 <= A[i], B[i] < 100 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划之子序列问题,想清楚DP数组的定义 | LeetCode:718.最长重复子数组](https://www.bilibili.com/video/BV178411H7hV),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +注意题目中说的子数组,其实就是连续子序列。 + +要求两个数组中最长重复子数组,如果是暴力的解法 只需要先两层for循环确定两个数组起始位置,然后再来一个循环可以是for或者while,来从两个起始位置开始比较,取得重复子数组的长度。 + +本题其实是动规解决的经典题目,我们只要想到 用二维数组可以记录两个字符串的所有比较情况,这样就比较好推 递推公式了。 +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 (**特别注意**: “以下标i - 1为结尾的A” 标明一定是 以A[i-1]为结尾的字符串 ) + +此时细心的同学应该发现,那dp[0][0]是什么含义呢?总不能是以下标-1为结尾的A数组吧。 + +其实dp[i][j]的定义也就决定着,我们在遍历dp[i][j]的时候i 和 j都要从1开始。 + +那有同学问了,我就定义dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,最长重复子数组长度。不行么? + +行倒是行! 但实现起来就麻烦一点,需要单独处理初始化部分,在本题解下面的拓展内容里,我给出了 第二种 dp数组的定义方式所对应的代码和讲解,大家比较一下就了解了。 + +2. 确定递推公式 + +根据dp[i][j]的定义,dp[i][j]的状态只能由dp[i - 1][j - 1]推导出来。 + +即当A[i - 1] 和B[j - 1]相等的时候,dp[i][j] = dp[i - 1][j - 1] + 1; + +根据递推公式可以看出,遍历i 和 j 要从1开始! + +3. dp数组如何初始化 + +根据dp[i][j]的定义,dp[i][0] 和dp[0][j]其实都是没有意义的! + +但dp[i][0] 和dp[0][j]要初始值,因为 为了方便递归公式dp[i][j] = dp[i - 1][j - 1] + 1; + +所以dp[i][0] 和dp[0][j]初始化为0。 + +举个例子A[0]如果和B[0]相同的话,dp[1][1] = dp[0][0] + 1,只有dp[0][0]初始为0,正好符合递推公式逐步累加起来。 + + +4. 确定遍历顺序 + +外层for循环遍历A,内层for循环遍历B。 + +那又有同学问了,外层for循环遍历B,内层for循环遍历A。不行么? + +也行,一样的,我这里就用外层for循环遍历A,内层for循环遍历B了。 + +同时题目要求长度最长的子数组的长度。所以在遍历的时候顺便把dp[i][j]的最大值记录下来。 + +代码如下: + +```CPP +for (int i = 1; i <= nums1.size(); i++) { + for (int j = 1; j <= nums2.size(); j++) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } + if (dp[i][j] > result) result = dp[i][j]; + } +} + +``` + + +5. 举例推导dp数组 + +拿示例1中,A: [1,2,3,2,1],B: [3,2,1,4,7]为例,画一个dp数组的状态变化,如下: + + +![718.最长重复子数组](https://file1.kamacoder.com/i/algo/2021011215282060.jpg) + +以上五部曲分析完毕,C++代码如下: + +```CPP +// 版本一 +class Solution { +public: + int findLength(vector& nums1, vector& nums2) { + vector> dp (nums1.size() + 1, vector(nums2.size() + 1, 0)); + int result = 0; + for (int i = 1; i <= nums1.size(); i++) { + for (int j = 1; j <= nums2.size(); j++) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } + if (dp[i][j] > result) result = dp[i][j]; + } + } + return result; + } +}; +``` + +* 时间复杂度:O(n × m),n 为A长度,m为B长度 +* 空间复杂度:O(n × m) + +### 滚动数组 + +在如下图中: + + +![718.最长重复子数组](https://file1.kamacoder.com/i/algo/2021011215282060-20230310134554486.jpg) + +我们可以看出dp[i][j]都是由dp[i - 1][j - 1]推出。那么压缩为一维数组,也就是dp[j]都是由dp[j - 1]推出。 + +也就是相当于可以把上一层dp[i - 1][j]拷贝到下一层dp[i][j]来继续用。 + +**此时遍历B数组的时候,就要从后向前遍历,这样避免重复覆盖**。 + +```CPP +// 版本二 +class Solution { +public: + int findLength(vector& A, vector& B) { + vector dp(vector(B.size() + 1, 0)); + int result = 0; + for (int i = 1; i <= A.size(); i++) { + for (int j = B.size(); j > 0; j--) { + if (A[i - 1] == B[j - 1]) { + dp[j] = dp[j - 1] + 1; + } else dp[j] = 0; // 注意这里不相等的时候要有赋0的操作 + if (dp[j] > result) result = dp[j]; + } + } + return result; + } +}; +``` + +* 时间复杂度:$O(n × m)$,n 为A长度,m为B长度 +* 空间复杂度:$O(m)$ + +## 拓展 + +前面讲了 dp数组为什么定义:以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 + +我就定义dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,最长重复子数组长度。不行么? + +当然可以,就是实现起来麻烦一些。 + +如果定义 dp[i][j]为 以下标i为结尾的A,和以下标j 为结尾的B,那么 第一行和第一列毕竟要进行初始化,如果nums1[i] 与 nums2[0] 相同的话,对应的 dp[i][0]就要初始为1, 因为此时最长重复子数组为1。 nums2[j] 与 nums1[0]相同的话,同理。 + +所以代码如下: + +```CPP +// 版本三 +class Solution { +public: + int findLength(vector& nums1, vector& nums2) { + vector> dp (nums1.size() + 1, vector(nums2.size() + 1, 0)); + int result = 0; + + // 要对第一行,第一列经行初始化 + for (int i = 0; i < nums1.size(); i++) if (nums1[i] == nums2[0]) dp[i][0] = 1; + for (int j = 0; j < nums2.size(); j++) if (nums1[0] == nums2[j]) dp[0][j] = 1; + + for (int i = 0; i < nums1.size(); i++) { + for (int j = 0; j < nums2.size(); j++) { + if (nums1[i] == nums2[j] && i > 0 && j > 0) { // 防止 i-1 出现负数 + dp[i][j] = dp[i - 1][j - 1] + 1; + } + if (dp[i][j] > result) result = dp[i][j]; + } + } + return result; + } +}; +``` + +大家会发现 这种写法 一定要多写一段初始化的过程。 + +而且为了让 `if (dp[i][j] > result) result = dp[i][j];` 收集到全部结果,两层for训练一定从0开始遍历,这样需要加上 `&& i > 0 && j > 0`的判断。 + +对于基础不牢的小白来说,在推导出转移方程后可能疑惑上述代码为什么要从`i=0,j=0`遍历而不是从`i=1,j=1`开始遍历,原因在于这里如果不是从`i=0,j=0`位置开始遍历,会漏掉如下样例结果: +```txt +nums1 = [70,39,25,40,7] +nums2 = [52,20,67,5,31] +``` + +当然,如果你愿意也可以使用如下代码,与上面那个c++是同一思路: +```java +class Solution { + public int findLength(int[] nums1, int[] nums2) { + int len1 = nums1.length; + int len2 = nums2.length; + int[][] result = new int[len1][len2]; + + int maxresult = Integer.MIN_VALUE; + + for(int i=0;i 0; j--) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[j] = dp[j - 1] + 1; + } else { + dp[j] = 0; + } + result = Math.max(result, dp[j]); + } + } + return result; + } +} +``` + +### Python: + +2维DP +```python +class Solution: + def findLength(self, nums1: List[int], nums2: List[int]) -> int: + # 创建一个二维数组 dp,用于存储最长公共子数组的长度 + dp = [[0] * (len(nums2) + 1) for _ in range(len(nums1) + 1)] + # 记录最长公共子数组的长度 + result = 0 + + # 遍历数组 nums1 + for i in range(1, len(nums1) + 1): + # 遍历数组 nums2 + for j in range(1, len(nums2) + 1): + # 如果 nums1[i-1] 和 nums2[j-1] 相等 + if nums1[i - 1] == nums2[j - 1]: + # 在当前位置上的最长公共子数组长度为前一个位置上的长度加一 + dp[i][j] = dp[i - 1][j - 1] + 1 + # 更新最长公共子数组的长度 + if dp[i][j] > result: + result = dp[i][j] + + # 返回最长公共子数组的长度 + return result + +``` + +1维DP +```python +class Solution: + def findLength(self, nums1: List[int], nums2: List[int]) -> int: + # 创建一个一维数组 dp,用于存储最长公共子数组的长度 + dp = [0] * (len(nums2) + 1) + # 记录最长公共子数组的长度 + result = 0 + + # 遍历数组 nums1 + for i in range(1, len(nums1) + 1): + # 用于保存上一个位置的值 + prev = 0 + # 遍历数组 nums2 + for j in range(1, len(nums2) + 1): + # 保存当前位置的值,因为会在后面被更新 + current = dp[j] + # 如果 nums1[i-1] 和 nums2[j-1] 相等 + if nums1[i - 1] == nums2[j - 1]: + # 在当前位置上的最长公共子数组长度为上一个位置的长度加一 + dp[j] = prev + 1 + # 更新最长公共子数组的长度 + if dp[j] > result: + result = dp[j] + else: + # 如果不相等,将当前位置的值置为零 + dp[j] = 0 + # 更新 prev 变量为当前位置的值,供下一次迭代使用 + prev = current + + # 返回最长公共子数组的长度 + return result + +``` + +2维DP 扩展 +```python +class Solution: + def findLength(self, nums1: List[int], nums2: List[int]) -> int: + # 创建一个二维数组 dp,用于存储最长公共子数组的长度 + dp = [[0] * (len(nums2) + 1) for _ in range(len(nums1) + 1)] + # 记录最长公共子数组的长度 + result = 0 + + # 对第一行和第一列进行初始化 + for i in range(len(nums1)): + if nums1[i] == nums2[0]: + dp[i + 1][1] = 1 + for j in range(len(nums2)): + if nums1[0] == nums2[j]: + dp[1][j + 1] = 1 + + # 填充dp数组 + for i in range(1, len(nums1) + 1): + for j in range(1, len(nums2) + 1): + if nums1[i - 1] == nums2[j - 1]: + # 如果 nums1[i-1] 和 nums2[j-1] 相等,则当前位置的最长公共子数组长度为左上角位置的值加一 + dp[i][j] = dp[i - 1][j - 1] + 1 + if dp[i][j] > result: + # 更新最长公共子数组的长度 + result = dp[i][j] + + # 返回最长公共子数组的长度 + return result + + +``` +### Go: + +```Go +func findLength(A []int, B []int) int { + m, n := len(A), len(B) + res := 0 + 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++ { + if A[i-1] == B[j-1] { + dp[i][j] = dp[i-1][j-1] + 1 + } + if dp[i][j] > res { + res = dp[i][j] + } + } + } + return res +} + +// 滚动数组 +func findLength(nums1 []int, nums2 []int) int { + n, m, res := len(nums1), len(nums2), 0 + dp := make([]int, m+1) + for i := 1; i <= n; i++ { + for j := m; j >= 1; j-- { + if nums1[i-1] == nums2[j-1] { + dp[j] = dp[j-1] + 1 + } else { + dp[j] = 0 // 注意这里不相等要赋值为0,供下一层使用 + } + res = max(res, dp[j]) + } + } + return res +} +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +### JavaScript: + +> 动态规划 + +```javascript +const findLength = (A, B) => { + // A、B数组的长度 + const [m, n] = [A.length, B.length]; + // dp数组初始化,都初始化为0 + const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0)); + // 初始化最大长度为0 + let res = 0; + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + // 遇到A[i - 1] === B[j - 1],则更新dp数组 + if (A[i - 1] === B[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } + // 更新res + res = dp[i][j] > res ? dp[i][j] : res; + } + } + // 遍历完成,返回res + return res; +}; +``` +> 滚动数组 +```javascript +const findLength = (nums1, nums2) => { + let len1 = nums1.length, len2 = nums2.length; + // dp[i][j]: 以nums1[i-1]、nums2[j-1]为结尾的最长公共子数组的长度 + let dp = new Array(len2+1).fill(0); + let res = 0; + for (let i = 1; i <= len1; i++) { + for (let j = len2; j > 0; j--) { + if (nums1[i-1] === nums2[j-1]) { + dp[j] = dp[j-1] + 1; + } else { + dp[j] = 0; + } + res = Math.max(res, dp[j]); + } + } + return res; +} +``` + +### TypeScript: + +> 动态规划: + +```typescript +function findLength(nums1: number[], nums2: number[]): number { + /** + dp[i][j]:nums[i-1]和nums[j-1]结尾,最长重复子数组的长度 + */ + const length1: number = nums1.length, + length2: number = nums2.length; + const dp: number[][] = new Array(length1 + 1).fill(0) + .map(_ => new Array(length2 + 1).fill(0)); + let resMax: number = 0; + for (let i = 1; i <= length1; i++) { + for (let j = 1; j <= length2; j++) { + if (nums1[i - 1] === nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + resMax = Math.max(resMax, dp[i][j]); + } + } + } + return resMax; +}; +``` + +> 滚动数组: + +```typescript +function findLength(nums1: number[], nums2: number[]): number { + const length1: number = nums1.length, + length2: number = nums2.length; + const dp: number[] = new Array(length1 + 1).fill(0); + let resMax: number = 0; + for (let i = 1; i <= length1; i++) { + for (let j = length2; j >= 1; j--) { + if (nums1[i - 1] === nums2[j - 1]) { + dp[j] = dp[j - 1] + 1; + resMax = Math.max(resMax, dp[j]); + } else { + dp[j] = 0; + } + } + } + return resMax; +}; +``` + +Rust: + +> 滚动数组 + +```rust +impl Solution { + pub fn find_length(nums1: Vec, nums2: Vec) -> i32 { + let (mut res, mut dp) = (0, vec![0; nums2.len()]); + + for n1 in nums1 { + for j in (0..nums2.len()).rev() { + if n1 == nums2[j] { + dp[j] = if j == 0 { 1 } else { dp[j - 1] + 1 }; + res = res.max(dp[j]); + } else { + dp[j] = 0; + } + } + } + res + } +} +``` + +### C: + +```c +int findLength(int* nums1, int nums1Size, int* nums2, int nums2Size) { + int dp[nums1Size + 1][nums2Size + 1]; + memset(dp, 0, sizeof(dp)); + int result = 0; + for (int i = 1; i <= nums1Size; ++i) { + for (int j = 1; j <= nums2Size; ++j) { + if(nums1[i - 1] == nums2[j - 1]){ + dp[i][j] = dp[i - 1][j - 1] + 1; + } + if(dp[i][j] > result){ + result = dp[i][j]; + } + } + } + return result; +} +``` + +### Cangjie + +```cangjie +func findLength(nums1: Array, nums2: Array): Int64 { + let n = nums1.size + let m = nums2.size + let dp = Array(n + 1, {_ => Array(m + 1, item: 0)}) + var res = 0 + for (i in 1..=n) { + for (j in 1..=m) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } + res = max(res, dp[i][j]) + } + } + return res +} +``` + + diff --git "a/problems/0724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\347\264\242\345\274\225.md" "b/problems/0724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\347\264\242\345\274\225.md" new file mode 100755 index 0000000000..bccca4f2d4 --- /dev/null +++ "b/problems/0724.\345\257\273\346\211\276\346\225\260\347\273\204\347\232\204\344\270\255\345\277\203\347\264\242\345\274\225.md" @@ -0,0 +1,159 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 724.寻找数组的中心下标 + +[力扣题目链接](https://leetcode.cn/problems/find-pivot-index/) + +给你一个整数数组 nums ,请计算数组的 中心下标 。 + +数组 中心下标 是数组的一个下标,其左侧所有元素相加的和等于右侧所有元素相加的和。 + +如果中心下标位于数组最左端,那么左侧数之和视为 0 ,因为在下标的左侧不存在元素。这一点对于中心下标位于数组最右端同样适用。 + +如果数组有多个中心下标,应该返回 最靠近左边 的那一个。如果数组不存在中心下标,返回 -1 。 + +示例 1: +* 输入:nums = [1, 7, 3, 6, 5, 6] +* 输出:3 +* 解释:中心下标是 3。左侧数之和 sum = nums[0] + nums[1] + nums[2] = 1 + 7 + 3 = 11 ,右侧数之和 sum = nums[4] + nums[5] = 5 + 6 = 11 ,二者相等。 + +示例 2: +* 输入:nums = [1, 2, 3] +* 输出:-1 +* 解释:数组中不存在满足此条件的中心下标。 + +示例 3: +* 输入:nums = [2, 1, -1] +* 输出:0 +* 解释:中心下标是 0。左侧数之和 sum = 0 ,(下标 0 左侧不存在元素),右侧数之和 sum = nums[1] + nums[2] = 1 + -1 = 0 。 + + +## 思路 + +这道题目还是比较简单直接的 + +1. 遍历一遍求出总和sum +2. 遍历第二遍求中心索引左半和leftSum + * 同时根据sum和leftSum 计算中心索引右半和rightSum + * 判断leftSum和rightSum是否相同 + +C++代码如下: +```CPP +class Solution { +public: + int pivotIndex(vector& nums) { + int sum = 0; + for (int num : nums) sum += num; // 求和 + int leftSum = 0; // 中心索引左半和 + int rightSum = 0; // 中心索引右半和 + for (int i = 0; i < nums.size(); i++) { + leftSum += nums[i]; + rightSum = sum - leftSum + nums[i]; + if (leftSum == rightSum) return i; + } + return -1; + } +}; +``` + + +## 其他语言版本 + +### Java + +```java +class Solution { + public int pivotIndex(int[] nums) { + int sum = 0; + for (int i = 0; i < nums.length; i++) { + sum += nums[i]; // 总和 + } + int leftSum = 0; + int rightSum = 0; + for (int i = 0; i < nums.length; i++) { + leftSum += nums[i]; + rightSum = sum - leftSum + nums[i]; // leftSum 里面已经有 nums[i],多减了一次,所以加上 + if (leftSum == rightSum) { + return i; + } + } + return -1; // 不存在 + } +} +``` + +### Python3 + +```python +class Solution: + def pivotIndex(self, nums: List[int]) -> int: + numSum = sum(nums) #数组总和 + leftSum = 0 + for i in range(len(nums)): + if numSum - leftSum -nums[i] == leftSum: #左右和相等 + return i + leftSum += nums[i] + return -1 +``` + +### Go + +```go +func pivotIndex(nums []int) int { + sum := 0 + for _, v := range nums { + sum += v; + } + + leftSum := 0 // 中心索引左半和 + rightSum := 0 // 中心索引右半和 + for i := 0; i < len(nums); i++ { + leftSum += nums[i] + rightSum = sum - leftSum + nums[i] + if leftSum == rightSum{ + return i + } + } + return -1 +} + +``` + +### JavaScript + +```js +var pivotIndex = function(nums) { + const sum = nums.reduce((a,b) => a + b);//求和 + // 中心索引左半和 中心索引右半和 + let leftSum = 0, rightSum = 0; + for(let i = 0; i < nums.length; i++){ + leftSum += nums[i]; + rightSum = sum - leftSum + nums[i];// leftSum 里面已经有 nums[i],多减了一次,所以加上 + if(leftSum === rightSum) return i; + } + return -1; +}; +``` + +### TypeScript + +```typescript +function pivotIndex(nums: number[]): number { + const length: number = nums.length; + const sum: number = nums.reduce((a, b) => a + b); + let leftSum: number = 0; + for (let i = 0; i < length; i++) { + const rightSum: number = sum - leftSum - nums[i]; + if (leftSum === rightSum) return i; + leftSum += nums[i]; + } + return -1; +}; +``` + + + + diff --git "a/problems/0738.\345\215\225\350\260\203\351\200\222\345\242\236\347\232\204\346\225\260\345\255\227.md" "b/problems/0738.\345\215\225\350\260\203\351\200\222\345\242\236\347\232\204\346\225\260\345\255\227.md" new file mode 100755 index 0000000000..17182778ae --- /dev/null +++ "b/problems/0738.\345\215\225\350\260\203\351\200\222\345\242\236\347\232\204\346\225\260\345\255\227.md" @@ -0,0 +1,442 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 738.单调递增的数字 +[力扣题目链接](https://leetcode.cn/problems/monotone-increasing-digits/) + +给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。 + +(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。) + +示例 1: +* 输入: N = 10 +* 输出: 9 + +示例 2: +* 输入: N = 1234 +* 输出: 1234 + +示例 3: +* 输入: N = 332 +* 输出: 299 + +说明: N 是在 [0, 10^9] 范围内的一个整数。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,思路不难想,但代码不好写!LeetCode:738.单调自增的数字](https://www.bilibili.com/video/BV1Kv4y1x7tP),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + + +### 暴力解法 + +题意很简单,那么首先想的就是暴力解法了,来我替大家暴力一波,结果自然是超时! + +代码如下: +```CPP +class Solution { +private: + // 判断一个数字的各位上是否是递增 + bool checkNum(int num) { + int max = 10; + while (num) { + int t = num % 10; + if (max >= t) max = t; + else return false; + num = num / 10; + } + return true; + } +public: + int monotoneIncreasingDigits(int N) { + for (int i = N; i > 0; i--) { // 从大到小遍历 + if (checkNum(i)) return i; + } + return 0; + } +}; +``` +* 时间复杂度:O(n × m) m为n的数字长度 +* 空间复杂度:O(1) + +### 贪心算法 + +题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。 + +例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]--,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。 + +这一点如果想清楚了,这道题就好办了。 + +此时是从前向后遍历还是从后向前遍历呢? + +从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。 + +这么说有点抽象,举个例子,数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。 + +那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299 + +确定了遍历顺序之后,那么此时局部最优就可以推出全局,找不出反例,试试贪心。 + +C++代码如下: + +```CPP +class Solution { +public: + int monotoneIncreasingDigits(int N) { + string strNum = to_string(N); + // flag用来标记赋值9从哪里开始 + // 设置为这个默认值,为了防止第二个for循环在flag没有被赋值的情况下执行 + int flag = strNum.size(); + for (int i = strNum.size() - 1; i > 0; i--) { + if (strNum[i - 1] > strNum[i] ) { + flag = i; + strNum[i - 1]--; + } + } + for (int i = flag; i < strNum.size(); i++) { + strNum[i] = '9'; + } + return stoi(strNum); + } +}; + +``` + +* 时间复杂度:O(n),n 为数字长度 +* 空间复杂度:O(n),需要一个字符串,转化为字符串操作更方便 + +## 总结 + +本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。 + +想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。 + +最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。 + + +## 其他语言版本 + + +### Java +```java +版本1 +class Solution { + public int monotoneIncreasingDigits(int N) { + String[] strings = (N + "").split(""); + int start = strings.length; + for (int i = strings.length - 1; i > 0; i--) { + if (Integer.parseInt(strings[i]) < Integer.parseInt(strings[i - 1])) { + strings[i - 1] = (Integer.parseInt(strings[i - 1]) - 1) + ""; + start = i; + } + } + for (int i = start; i < strings.length; i++) { + strings[i] = "9"; + } + return Integer.parseInt(String.join("",strings)); + } +} +``` +java版本1中创建了String数组,多次使用Integer.parseInt了方法,这导致不管是耗时还是空间占用都非常高,用时12ms,下面提供一个版本在char数组上原地修改,用时1ms的版本 +```java +版本2 +class Solution { + public int monotoneIncreasingDigits(int n) { + String s = String.valueOf(n); + char[] chars = s.toCharArray(); + int start = s.length(); + for (int i = s.length() - 2; i >= 0; i--) { + if (chars[i] > chars[i + 1]) { + chars[i]--; + start = i+1; + } + } + for (int i = start; i < s.length(); i++) { + chars[i] = '9'; + } + return Integer.parseInt(String.valueOf(chars)); + } +} +``` + + +### Python +暴力 +```python +class Solution: + def checkNum(self, num): + max_digit = 10 + while num: + digit = num % 10 + if max_digit >= digit: + max_digit = digit + else: + return False + num //= 10 + return True + + def monotoneIncreasingDigits(self, N): + for i in range(N, 0, -1): + if self.checkNum(i): + return i + return 0 + +``` +贪心(版本一) +```python +class Solution: + def monotoneIncreasingDigits(self, n: int) -> int: + # 将整数转换为字符串 + strNum = str(n) + # flag用来标记赋值9从哪里开始 + # 设置为字符串长度,为了防止第二个for循环在flag没有被赋值的情况下执行 + flag = len(strNum) + + # 从右往左遍历字符串 + for i in range(len(strNum) - 1, 0, -1): + # 如果当前字符比前一个字符小,说明需要修改前一个字符 + if strNum[i - 1] > strNum[i]: + flag = i # 更新flag的值,记录需要修改的位置 + # 将前一个字符减1,以保证递增性质 + strNum = strNum[:i - 1] + str(int(strNum[i - 1]) - 1) + strNum[i:] + + # 将flag位置及之后的字符都修改为9,以保证最大的递增数字 + for i in range(flag, len(strNum)): + strNum = strNum[:i] + '9' + strNum[i + 1:] + + # 将最终的字符串转换回整数并返回 + return int(strNum) + +``` +贪心(版本二) +```python +class Solution: + def monotoneIncreasingDigits(self, n: int) -> int: + # 将整数转换为字符串 + strNum = list(str(n)) + + # 从右往左遍历字符串 + for i in range(len(strNum) - 1, 0, -1): + # 如果当前字符比前一个字符小,说明需要修改前一个字符 + if strNum[i - 1] > strNum[i]: + strNum[i - 1] = str(int(strNum[i - 1]) - 1) # 将前一个字符减1 + # 将修改位置后面的字符都设置为9,因为修改前一个字符可能破坏了递增性质 + for j in range(i, len(strNum)): + strNum[j] = '9' + + # 将列表转换为字符串,并将字符串转换为整数并返回 + return int(''.join(strNum)) + + +``` +贪心(版本三) + +```python +class Solution: + def monotoneIncreasingDigits(self, n: int) -> int: + # 将整数转换为字符串 + strNum = list(str(n)) + + # 从右往左遍历字符串 + for i in range(len(strNum) - 1, 0, -1): + # 如果当前字符比前一个字符小,说明需要修改前一个字符 + if strNum[i - 1] > strNum[i]: + strNum[i - 1] = str(int(strNum[i - 1]) - 1) # 将前一个字符减1 + # 将修改位置后面的字符都设置为9,因为修改前一个字符可能破坏了递增性质 + strNum[i:] = '9' * (len(strNum) - i) + + # 将列表转换为字符串,并将字符串转换为整数并返回 + return int(''.join(strNum)) + +``` +贪心(版本四)精简 + +```python +class Solution: + def monotoneIncreasingDigits(self, n: int) -> int: + strNum = str(n) + for i in range(len(strNum) - 1, 0, -1): + # 如果当前字符比前一个字符小,说明需要修改前一个字符 + if strNum[i - 1] > strNum[i]: + # 将前一个字符减1,以保证递增性质 + # 使用字符串切片操作将修改后的前面部分与后面部分进行拼接 + strNum = strNum[:i - 1] + str(int(strNum[i - 1]) - 1) + '9' * (len(strNum) - i) + return int(strNum) + + +``` +### Go +```go +func monotoneIncreasingDigits(n int) int { + s := strconv.Itoa(n) + // 从左到右遍历字符串,找到第一个不满足单调递增的位置 + for i := len(s) - 2; i >= 0; i-- { + if s[i] > s[i+1] { + // 将该位置的数字减1 + s = s[:i] + string(s[i]-1) + s[i+1:] + // 将该位置之后的所有数字置为9 + for j := i + 1; j < len(s); j++ { + s = s[:j] + "9" + s[j+1:] + } + } + } + result, _ := strconv.Atoi(s) + return result +} +``` + +### JavaScript +```Javascript +var monotoneIncreasingDigits = function(n) { + n = n.toString() + n = n.split('').map(item => { + return +item + }) + let flag = Infinity + for(let i = n.length - 1; i > 0; i--) { + if(n [i - 1] > n[i]) { + flag = i + n[i - 1] = n[i - 1] - 1 + n[i] = 9 + } + } + + for(let i = flag; i < n.length; i++) { + n[i] = 9 + } + + n = n.join('') + return +n +}; +``` + +### TypeScript + +```typescript +function monotoneIncreasingDigits(n: number): number { + let strArr: number[] = String(n).split('').map(i => parseInt(i)); + const length = strArr.length; + let flag: number = length; + for (let i = length - 2; i >= 0; i--) { + if (strArr[i] > strArr[i + 1]) { + strArr[i] -= 1; + flag = i + 1; + } + } + for (let i = flag; i < length; i++) { + strArr[i] = 9; + } + return parseInt(strArr.join('')); +}; +``` + + +### Scala + +直接转换为了整数数组: +```scala +object Solution { + import scala.collection.mutable + def monotoneIncreasingDigits(n: Int): Int = { + var digits = mutable.ArrayBuffer[Int]() + // 提取每位数字 + var temp = n // 因为 参数n 是不可变量所以需要赋值给一个可变量 + while (temp != 0) { + digits.append(temp % 10) + temp = temp / 10 + } + // 贪心 + var flag = -1 + for (i <- 0 until (digits.length - 1) if digits(i) < digits(i + 1)) { + flag = i + digits(i + 1) -= 1 + } + for (i <- 0 to flag) digits(i) = 9 + + // 拼接 + var res = 0 + for (i <- 0 until digits.length) { + res += digits(i) * math.pow(10, i).toInt + } + res + } +} +``` + +### Rust + +```Rust +impl Solution { + pub fn monotone_increasing_digits(n: i32) -> i32 { + let mut n_bytes = n.to_string().into_bytes(); + let mut flag = n_bytes.len(); + for i in (1..n_bytes.len()).rev() { + if n_bytes[i - 1] > n_bytes[i] { + flag = i; + n_bytes[i - 1] -= 1; + } + } + for v in n_bytes.iter_mut().skip(flag) { + *v = 57; + } + n_bytes + .into_iter() + .fold(0, |acc, x| acc * 10 + x as i32 - 48) + } +} +``` +### C + +```c +int monotoneIncreasingDigits(int n) { + char str[11]; + // 将数字转换为字符串 + sprintf(str, "%d", n); + int len = strlen(str); + int flag = strlen(str); + for(int i = len - 1; i > 0; i--){ + if(str[i] < str[i - 1]){ + str[i - 1]--; + flag = i; + } + } + for(int i = flag; i < len; i++){ + str[i] = '9'; + } + // 字符串转数字 + return atoi(str); +} +``` + + + +### C# + +```csharp +public class Solution +{ + public int MonotoneIncreasingDigits(int n) + { + char[] s = n.ToString().ToCharArray(); + int flag = s.Length; + for (int i = s.Length - 1; i > 0; i--) + { + if (s[i - 1] > s[i]) + { + flag = i; + s[i - 1]--; + } + } + for (int i = flag; i < s.Length; i++) + { + s[i] = '9'; + } + return int.Parse(new string(s)); + } +} +``` + + diff --git "a/problems/0739.\346\257\217\346\227\245\346\270\251\345\272\246.md" "b/problems/0739.\346\257\217\346\227\245\346\270\251\345\272\246.md" new file mode 100755 index 0000000000..2ad7e6b79b --- /dev/null +++ "b/problems/0739.\346\257\217\346\227\245\346\270\251\345\272\246.md" @@ -0,0 +1,515 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 739. 每日温度 + +[力扣题目链接](https://leetcode.cn/problems/daily-temperatures/) + +请根据每日 气温 列表,重新生成一个列表。对应位置的输出为:要想观测到更高的气温,至少需要等待的天数。如果气温在这之后都不会升高,请在该位置用 0 来代替。 + +例如,给定一个列表 temperatures = [73, 74, 75, 71, 69, 72, 76, 73],你的输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 + +提示:气温 列表长度的范围是 [1, 30000]。每个气温的值的均为华氏度,都是在 [30, 100] 范围内的整数。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[单调栈,你该了解的,这里都讲了!LeetCode:739.每日温度](https://www.bilibili.com/video/BV1my4y1Z7jj/),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +首先想到的当然是暴力解法,两层for循环,把至少需要等待的天数就搜出来了。时间复杂度是O(n^2) + +那么接下来在来看看使用单调栈的解法。 + +那有同学就问了,我怎么能想到用单调栈呢? 什么时候用单调栈呢? + +**通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了**。时间复杂度为O(n)。 + +例如本题其实就是找找到一个元素右边第一个比自己大的元素,此时就应该想到用单调栈了。 + +那么单调栈的原理是什么呢?为什么时间复杂度是O(n)就可以找到每一个元素的右边第一个比它大的元素位置呢? + +**单调栈的本质是空间换时间**,因为在遍历的过程中需要用一个栈来记录右边第一个比当前元素高的元素,优点是整个数组只需要遍历一次。 + +**更直白来说,就是用一个栈来记录我们遍历过的元素**,因为我们遍历数组的时候,我们不知道之前都遍历了哪些元素,以至于遍历一个元素找不到是不是之前遍历过一个更小的,所以我们需要用一个容器(这里用单调栈)来记录我们遍历过的元素。 + + +在使用单调栈的时候首先要明确如下几点: + +1. 单调栈里存放的元素是什么? + +单调栈里只需要存放元素的下标i就可以了,如果需要使用对应的元素,直接T[i]就可以获取。 + +2. 单调栈里元素是递增呢? 还是递减呢? + +**注意以下讲解中,顺序的描述为 从栈头到栈底的顺序**,因为单纯的说从左到右或者从前到后,不说栈头朝哪个方向的话,大家一定比较懵。 + +这里我们要使用递增循序(再强调一下是指从栈头到栈底的顺序),因为只有递增的时候,栈里要加入一个元素i的时候,才知道栈顶元素在数组中右面第一个比栈顶元素大的元素是i。 + +即:如果求一个元素右边第一个更大元素,单调栈就是递增的,如果求一个元素右边第一个更小元素,单调栈就是递减的。 + +文字描述理解起来有点费劲,接下来我画了一系列的图,来讲解单调栈的工作过程,大家再去思考,本题为什么是递增栈。 + +使用单调栈主要有三个判断条件。 + +* 当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况 +* 当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况 +* 当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况 + +**把这三种情况分析清楚了,也就理解透彻了**。 + +接下来我们用temperatures = [73, 74, 75, 71, 71, 72, 76, 73]为例来逐步分析,输出应该是 [1, 1, 4, 2, 1, 1, 0, 0]。 + +------- + +首先先将第一个遍历元素加入单调栈 + +![739.每日温度1](https://file1.kamacoder.com/i/algo/20210219124434172.jpg) + +--------- + +加入T[1] = 74,因为T[1] > T[0](当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况)。 + +我们要保持一个递增单调栈(从栈头到栈底),所以将T[0]弹出,T[1]加入,此时result数组可以记录了,result[0] = 1,即T[0]右面第一个比T[0]大的元素是T[1]。 + +![739.每日温度2](https://file1.kamacoder.com/i/algo/20210219124504299.jpg) + +----------- + +加入T[2],同理,T[1]弹出 + +![739.每日温度3](https://file1.kamacoder.com/i/algo/20210219124527361.jpg) + +------- + +加入T[3],T[3] < T[2] (当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况),加T[3]加入单调栈。 + +![739.每日温度4](https://file1.kamacoder.com/i/algo/20210219124610761.jpg) + +--------- + +加入T[4],T[4] == T[3] (当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况),此时依然要加入栈,不用计算距离,因为我们要求的是右面第一个大于本元素的位置,而不是大于等于! + +![739.每日温度5](https://file1.kamacoder.com/i/algo/20210219124633444.jpg) + +--------- + +加入T[5],T[5] > T[4] (当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况),将T[4]弹出,同时计算距离,更新result +![739.每日温度6](https://file1.kamacoder.com/i/algo/20210219124700567.jpg) + +---------- + +T[4]弹出之后, T[5] > T[3] (当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况),将T[3]继续弹出,同时计算距离,更新result +![739.每日温度7](https://file1.kamacoder.com/i/algo/20210219124726613.jpg) + +------- + +直到发现T[5]小于T[st.top()],终止弹出,将T[5]加入单调栈 + +![739.每日温度8](https://file1.kamacoder.com/i/algo/20210219124807715.jpg) + +------- + +加入T[6],同理,需要将栈里的T[5],T[2]弹出 + +![739.每日温度9](https://file1.kamacoder.com/i/algo/2021021912483374.jpg) + +------- + +同理,继续弹出 + +![739.每日温度10](https://file1.kamacoder.com/i/algo/2021021912490098.jpg) + +------ + +此时栈里只剩下了T[6] + +![739.每日温度11](https://file1.kamacoder.com/i/algo/20210219124930156.jpg) + +------------ + +加入T[7], T[7] < T[6] 直接入栈,这就是最后的情况,result数组也更新完了。 + +![739.每日温度12](https://file1.kamacoder.com/i/algo/20210219124957216.jpg) + + +此时有同学可能就疑惑了,那result[6] , result[7]怎么没更新啊,元素也一直在栈里。 + +其实定义result数组的时候,就应该直接初始化为0,如果result没有更新,说明这个元素右面没有更大的了,也就是为0。 + +以上在图解的时候,已经把,这三种情况都做了详细的分析。 + +* 情况一:当前遍历的元素T[i]小于栈顶元素T[st.top()]的情况 +* 情况二:当前遍历的元素T[i]等于栈顶元素T[st.top()]的情况 +* 情况三:当前遍历的元素T[i]大于栈顶元素T[st.top()]的情况 + +通过以上过程,大家可以自己再模拟一遍,就会发现:只有单调栈递增(从栈口到栈底顺序),就是求右边第一个比自己大的,单调栈递减的话,就是求右边第一个比自己小的。 + +C++代码如下: + +```CPP +// 版本一 +class Solution { +public: + vector dailyTemperatures(vector& T) { + // 递增栈 + stack st; + vector result(T.size(), 0); + st.push(0); + for (int i = 1; i < T.size(); i++) { + if (T[i] < T[st.top()]) { // 情况一 + st.push(i); + } else if (T[i] == T[st.top()]) { // 情况二 + st.push(i); + } else { + while (!st.empty() && T[i] > T[st.top()]) { // 情况三 + result[st.top()] = i - st.top(); + st.pop(); + } + st.push(i); + } + } + return result; + } +}; +``` + +**建议一开始 都把每种情况分析好,不要上来看简短的代码,关键逻辑都被隐藏了**。 + +精简代码如下: + +```CPP +// 版本二 +class Solution { +public: + vector dailyTemperatures(vector& T) { + stack st; // 递增栈 + vector result(T.size(), 0); + for (int i = 0; i < T.size(); i++) { + while (!st.empty() && T[i] > T[st.top()]) { // 注意栈不能为空 + result[st.top()] = i - st.top(); + st.pop(); + } + st.push(i); + + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +精简的代码是直接把情况一二三都合并到了一起,其实这种代码精简是精简,但思路不是很清晰。 + +建议大家把情况一二三想清楚了,先写出版本一的代码,然后在其基础上在做精简! + + +## 其他语言版本 + +### C: + +```C +/** + * Note: The returned array must be malloced, assume caller calls free(). + */ +int* dailyTemperatures(int* temperatures, int temperaturesSize, int* returnSize) { + int len = temperaturesSize; + *returnSize = len; + + int *result = (int *)malloc(sizeof(int) * len); + memset(result, 0x00, sizeof(int) * len); + + int stack[len]; + memset(stack, 0x00, sizeof(stack)); + int top = 0; + + for (int i = 1; i < len; i++) { + if (temperatures[i] <= temperatures[stack[top]]) { /* push */ + stack[++top] = i; + } else { + while (top >= 0 && temperatures[i] > temperatures[stack[top]]) { /* stack not empty */ + result[stack[top]] = i - stack[top]; + top--; /* pop */ + } + stack[++top] = i; /* push */ + } + } + return result; +} +``` + +### Java: + +```java +class Solution { + // 版本 1 + public int[] dailyTemperatures(int[] temperatures) { + + int lens=temperatures.length; + int []res=new int[lens]; + + /** + 如果当前遍历的元素 大于栈顶元素,表示 栈顶元素的 右边的最大的元素就是 当前遍历的元素, + 所以弹出 栈顶元素,并记录 + 如果栈不空的话,还要考虑新的栈顶与当前元素的大小关系 + 否则的话,可以直接入栈。 + 注意,单调栈里 加入的元素是 下标。 + */ + Deque stack=new LinkedList<>(); + stack.push(0); + for(int i=1;itemperatures[stack.peek()]){ + res[stack.peek()]=i-stack.peek(); + stack.pop(); + } + stack.push(i); + } + } + + return res; + } + + //--------这 是一条分界线 + // 版本 2 + class Solution { + public int[] dailyTemperatures(int[] temperatures) { + int lens=temperatures.length; + int []res=new int[lens]; + Deque stack=new LinkedList<>(); + for(int i=0;itemperatures[stack.peek()]){ + res[stack.peek()]=i-stack.peek(); + stack.pop(); + } + stack.push(i); + } + + return res; + } +} + +} +``` + +### Python: + +> 未精简版本 + +```python +class Solution: + def dailyTemperatures(self, temperatures: List[int]) -> List[int]: + answer = [0]*len(temperatures) + stack = [0] + for i in range(1,len(temperatures)): + # 情况一和情况二 + if temperatures[i]<=temperatures[stack[-1]]: + stack.append(i) + # 情况三 + else: + while len(stack) != 0 and temperatures[i]>temperatures[stack[-1]]: + answer[stack[-1]]=i-stack[-1] + stack.pop() + stack.append(i) + + return answer +``` + +> 精简版本 + +```python +class Solution: + def dailyTemperatures(self, temperatures: List[int]) -> List[int]: + answer = [0]*len(temperatures) + stack = [] + for i in range(len(temperatures)): + while len(stack)>0 and temperatures[i] > temperatures[stack[-1]]: + answer[stack[-1]] = i - stack[-1] + stack.pop() + stack.append(i) + return answer +``` + +### Go: + +> 暴力法 + +```go +func dailyTemperatures(t []int) []int { + var res []int + for i := 0; i < len(t)-1; i++ { + j := i + 1 + for ; j < len(t); j++ { + // 如果之后出现更高,说明找到了 + if t[j] > t[i] { + res = append(res, j-i) + break + } + } + // 找完了都没找到 + if j == len(t) { + res = append(res, 0) + } + } + // 最后一个肯定是 0 + return append(res, 0) +} +``` + +> 单调栈法(未精简版本) + +```go +func dailyTemperatures(temperatures []int) []int { + res := make([]int, len(temperatures)) + // 初始化栈顶元素为第一个下标索引0 + stack := []int{0} + + for i := 1; i < len(temperatures); i++ { + top := stack[len(stack)-1] + if temperatures[i] < temperatures[top] { + stack = append(stack, i) + } else if temperatures[i] == temperatures[top] { + stack = append(stack, i) + } else { + for len(stack) != 0 && temperatures[i] > temperatures[top] { + res[top] = i - top + stack = stack[:len(stack)-1] + if len(stack) != 0 { + top = stack[len(stack)-1] + } + } + stack = append(stack, i) + } + } + return res +} +``` + +> 单调栈法(精简版本) + +```go +// 单调递减栈 +func dailyTemperatures(num []int) []int { + ans := make([]int, len(num)) + stack := []int{} + for i, v := range num { + // 栈不空,且当前遍历元素 v 破坏了栈的单调性 + for len(stack) != 0 && v > num[stack[len(stack)-1]] { + // pop + top := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + ans[top] = i - top + } + stack = append(stack, i) + } + return ans +} +``` + +### JavaScript: + +```javascript +// 版本一 +var dailyTemperatures = function(temperatures) { + const n = temperatures.length; + const res = Array(n).fill(0); + const stack = []; // 递增栈:用于存储元素右面第一个比他大的元素下标 + stack.push(0); + for (let i = 1; i < n; i++) { + // 栈顶元素 + const top = stack[stack.length - 1]; + if (temperatures[i] < temperatures[top]) { + stack.push(i); + } else if (temperatures[i] === temperatures[top]) { + stack.push(i); + } else { + while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) { + const top = stack.pop(); + res[top] = i - top; + } + stack.push(i); + } + } + return res; +}; + + +// 版本二 +var dailyTemperatures = function(temperatures) { + const n = temperatures.length; + const res = Array(n).fill(0); + const stack = []; // 递增栈:用于存储元素右面第一个比他大的元素下标 + stack.push(0); + for (let i = 1; i < n; i++) { + while (stack.length && temperatures[i] > temperatures[stack[stack.length - 1]]) { + const top = stack.pop(); + res[top] = i - top; + } + stack.push(i); + } + return res; +}; +``` + +### TypeScript: + +> 精简版: + +```typescript +function dailyTemperatures(temperatures: number[]): number[] { + const length: number = temperatures.length; + const stack: number[] = []; + const resArr: number[] = new Array(length).fill(0); + stack.push(0); + for (let i = 1; i < length; i++) { + let top = stack[stack.length - 1]; + while ( + stack.length > 0 && + temperatures[top] < temperatures[i] + ) { + resArr[top] = i - top; + stack.pop(); + top = stack[stack.length - 1]; + } + stack.push(i); + } + return resArr; +}; +``` + +### Rust: + +```rust +impl Solution { + /// 单调栈的本质是以空间换时间,记录之前已访问过的非递增子序列下标 + pub fn daily_temperatures(temperatures: Vec) -> Vec { + let mut res = vec![0; temperatures.len()]; + let mut stack = vec![]; + for (idx, &value) in temperatures.iter().enumerate() { + while !stack.is_empty() && temperatures[*stack.last().unwrap()] < value { + // 弹出,并计算res中对应位置的值 + let i = stack.pop().unwrap(); + res[i] = (idx - i) as i32; + } + // 入栈 + stack.push(idx) + } + res + } +} +``` + + + diff --git "a/problems/0743.\347\275\221\347\273\234\345\273\266\350\277\237\346\227\266\351\227\264.md" "b/problems/0743.\347\275\221\347\273\234\345\273\266\350\277\237\346\227\266\351\227\264.md" new file mode 100644 index 0000000000..40b699c18f --- /dev/null +++ "b/problems/0743.\347\275\221\347\273\234\345\273\266\350\277\237\346\227\266\351\227\264.md" @@ -0,0 +1,1230 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 743.网络延迟时间 + +https://leetcode.cn/problems/network-delay-time/description/ + + +有 n 个网络节点,标记为 1 到 n。 + +给你一个列表 times,表示信号经过 有向 边的传递时间。 times[i] = (ui, vi, wi),其中 ui 是源节点,vi 是目标节点, wi 是一个信号从源节点传递到目标节点的时间。 + +现在,从某个节点 K 发出一个信号。需要多久才能使所有节点都收到信号?如果不能使所有节点收到信号,返回 -1 。 + +![](https://file1.kamacoder.com/i/algo/20240229104105.png) + +提示: + +* 1 <= k <= n <= 100 +* 1 <= times.length <= 6000 +* times[i].length == 3 +* 1 <= ui, vi <= n +* ui != vi +* 0 <= wi <= 100 +* 所有 (ui, vi) 对都 互不相同(即,不含重复边) + +# dijkstra 精讲 + +本题就是求最短路,最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。 + +接下来,我们来详细讲解最短路算法中的 dijkstra 算法。 + +dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。 + +需要注意两点: + +* dijkstra 算法可以同时求 起点到所有节点的最短路径 +* 权值不能为负数 + +(这两点后面我们会讲到) + +如本题示例中的图: + +![](https://file1.kamacoder.com/i/algo/20240125162647.png) + +起点(节点1)到终点(节点7) 的最短路径是 图中 标记绿线的部分。 + +最短路径的权值为12。 + +其实 dijkstra 算法 和 我们之前讲解的prim算法思路非常接近,如果大家认真学过[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w),那么理解 Dijkstra 算法会相对容易很多。(这也是我要先讲prim再讲dijkstra的原因) + +dijkstra 算法 同样是贪心的思路,不断寻找距离 源点最近的没有访问过的节点。 + +这里我也给出 **dijkstra三部曲**: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +大家此时已经会发现,这和prim算法 怎么这么像呢。 + +我在[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w)讲解中也给出了三部曲。 prim 和 dijkstra 确实很像,思路也是类似的,这一点我在后面还会详细来讲。 + +在dijkstra算法中,同样有一个数组很重要,起名为:minDist。 + +**minDist数组 用来记录 每一个节点距离源点的最小距离**。 + +理解这一点很重要,也是理解 dijkstra 算法的核心所在。 + +大家现在看着可能有点懵,不知道什么意思。 + +没关系,先让大家有一个印象,对理解后面讲解有帮助。 + +我们先来画图看一下 dijkstra 的工作过程,以本题示例为例: (以下为朴素版dijkstra的思路) + +(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了,避免大家搞混**) + +## 朴素版dijkstra + +### 模拟过程 + +----------- + +0、初始化 + +minDist数组数值初始化为int最大值。 + +这里在强点一下 **minDist数组的含义:记录所有节点到源点的最短路径**,那么初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。 + +![](https://file1.kamacoder.com/i/algo/20240130115306.png) + +(图中,max 表示默认值,节点0 不做处理,统一从下标1 开始计算,这样下标和节点数值统一, 方便大家理解,避免搞混) + +源点(节点1) 到自己的距离为0,所以 minDist[1] = 0 + +此时所有节点都没有被访问过,所以 visited数组都为0 + +--------------- + +以下为dijkstra 三部曲 + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240130115421.png) + + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为1,小于原minDist[2]的数值max,更新minDist[2] = 1 +* 源点到节点3的最短距离为4,小于原minDist[3]的数值max,更新minDist[4] = 4 + +可能有录友问:为啥和 minDist[2] 比较? + +再强调一下 minDist[2] 的含义,它表示源点到节点2的最短距离,那么目前我们得到了 源点到节点2的最短距离为1,小于默认值max,所以更新。 minDist[3]的更新同理 + + +------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点到节点2距离最近,选节点2 + +2、该最近节点被标记访问过 + +节点2被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + + +![](https://file1.kamacoder.com/i/algo/20240130121240.png) + +更新 minDist数组,即:源点(节点1) 到 节点6 、 节点3 和 节点4的距离。 + +**为什么更新这些节点呢? 怎么不更新其他节点呢**? + +因为 源点(节点1)通过 已经计算过的节点(节点2) 可以链接到的节点 有 节点3,节点4和节点6. + + +更新 minDist数组: + +* 源点到节点6的最短距离为5,小于原minDist[6]的数值max,更新minDist[6] = 5 +* 源点到节点3的最短距离为3,小于原minDist[3]的数值4,更新minDist[3] = 3 +* 源点到节点4的最短距离为6,小于原minDist[4]的数值max,更新minDist[4] = 6 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点距离哪些节点最近,怎么算的? + +其实就是看 minDist数组里的数值,minDist 记录了 源点到所有节点的最近距离,结合visited数组筛选出未访问的节点就好。 + +从 上面的图,或者 从minDist数组中,我们都能看出 未访问过的节点中,源点(节点1)到节点3距离最近。 + + +2、该最近节点被标记访问过 + +节点3被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240130120434.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +更新 minDist数组: + +* 源点到节点4的最短距离为5,小于原minDist[4]的数值6,更新minDist[4] = 5 + +------------------ + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,有节点4 和 节点6,距离源点距离都是 5 (minDist[4] = 5,minDist[6] = 5) ,选哪个节点都可以。 + +2、该最近节点被标记访问过 + +节点4被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201105335.png) + +由于节点4的加入,那么源点可以链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为8,小于原minDist[5]的数值max,更新minDist[5] = 8 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点6,距离源点距离是 5 (minDist[6] = 5) + + +2、该最近节点被标记访问过 + +节点6 被标记访问过 + + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110250.png) + +由于节点6的加入,那么源点可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为14,小于原minDist[7]的数值max,更新minDist[7] = 14 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点5,距离源点距离是 8 (minDist[5] = 8) + +2、该最近节点被标记访问过 + +节点5 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110651.png) + +由于节点5的加入,那么源点有新的路径可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为12,小于原minDist[7]的数值14,更新minDist[7] = 12 + +----------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点7(终点),距离源点距离是 12 (minDist[7] = 12) + +2、该最近节点被标记访问过 + +节点7 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110920.png) + +节点7加入,但节点7到节点7的距离为0,所以 不用更新minDist数组 + +-------------------- + +最后我们要求起点(节点1) 到终点 (节点7)的距离。 + +再来回顾一下minDist数组的含义:记录 每一个节点距离源点的最小距离。 + +那么起到(节点1)到终点(节点7)的最短距离就是 minDist[7] ,按上面举例讲解来说,minDist[7] = 12,节点1 到节点7的最短路径为 12。 + +路径如图: + +![](https://file1.kamacoder.com/i/algo/20240201111352.png) + +在上面的讲解中,每一步 我都是按照 dijkstra 三部曲来讲解的,理解了这三部曲,代码也就好懂的。 + +### 代码实现 + +本题代码如下,里面的 三部曲 我都做了注释,大家按照我上面的讲解 来看如下代码: + +```CPP +class Solution { +public: + int networkDelayTime(vector>& times, int n, int k) { + + // 注意题目中给的二维数组并不是领接矩阵 + // 需要邻接矩阵来存图 + // 因为本题处理方式是节点标号从1开始,所以数组的大小都是 n+1 + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + grid[p1][p2] = times[i][2]; + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + minDist[k] = 0; // 起始点到自身的距离为0 + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + // 遍历每个节点,选择未被访问的节点集合中哪个节点到源点的距离最小 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] <= minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 标记该顶点已被访问 + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + + + } + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n^2) + +### debug方法 + +写这种题目难免会有各种各样的问题,我们如何发现自己的代码是否有问题呢? + +最好的方式就是打日志,本题的话,就是将 minDist 数组打印出来,就可以很明显发现 哪里出问题了。 + +每次选择节点后,minDist数组的变化是否符合预期 ,是否和我上面讲的逻辑是对应的。 + +例如本题,如果想debug的话,打印日志可以这样写: + + +```CPP +class Solution { +public: + int networkDelayTime(vector>& times, int n, int k) { + + // 注意题目中给的二维数组并不是领接矩阵 + // 需要邻接矩阵来存图 + // 因为本题处理方式是节点标号从1开始,所以数组的大小都是 n+1 + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + grid[p1][p2] = times[i][2]; + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + minDist[k] = 0; // 起始点到自身的距离为0 + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + // 遍历每个节点,选择未被访问的节点集合中哪个节点到源点的距离最小 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] <= minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 标记该顶点已被访问 + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + // 打印日志: + cout << "select:" << cur << endl; + for (int v = 1; v <= n; v++) cout << v << ":" << minDist[v] << " "; + cout << endl << endl;; + + + + } + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; + + +``` + +打印后的结果: + +``` +select:2 +1:1 2:0 3:1 4:2147483647 + +select:3 +1:1 2:0 3:1 4:2 + +select:1 +1:1 2:0 3:1 4:2 + +select:4 +1:1 2:0 3:1 4:2 +``` + +打印日志可以和上面我讲解的过程进行对比,每一步的结果是完全对应的。 + +所以如果大家如果代码有问题,打日志来debug是最好的方法 + +### 出现负数 + +如果图中边的权值为负数,dijkstra 还合适吗? + +看一下这个图: (有负权值) + +![](https://file1.kamacoder.com/i/algo/20240227104334.png) + +节点1 到 节点5 的最短路径 应该是 节点1 -> 节点2 -> 节点3 -> 节点4 -> 节点5 + +那我们来看dijkstra 求解的路径是什么样的,继续dijkstra 三部曲来模拟 :(dijkstra模拟过程上面已经详细讲过,以下只模拟重要过程,例如如何初始化就省略讲解了) + +----------- + +初始化: + +![](https://file1.kamacoder.com/i/algo/20240227104801.png) + +--------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110217.png) + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为100,小于原minDist[2]的数值max,更新minDist[2] = 100 +* 源点到节点3的最短距离为1,小于原minDist[3]的数值max,更新minDist[4] = 1 + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点3最近,距离为1,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点3访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110330.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +* 源点到节点4的最短距离为2,小于原minDist[4]的数值max,更新minDist[4] = 2 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点4最近,距离为2,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点4访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110346.png) + +由于节点4的加入,那么源点可以有新的路径链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为3,小于原minDist[5]的数值max,更新minDist[5] = 5 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点5最近,距离为3,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点5访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110405.png) + +节点5的加入,而节点5 没有链接其他节点, 所以不用更新minDist数组,仅标记节点5被访问过了 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点2最近,距离为100,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点2访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110711.png) + +-------------- + +至此dijkstra的模拟过程就结束了,根据最后的minDist数组,我们求 节点1 到 节点5 的最短路径的权值总和为 3,路径: 节点1 -> 节点3 -> 节点4 -> 节点5 + +通过以上的过程模拟,我们可以发现 之所以 没有走有负权值的最短路径 是因为 在 访问 节点 2 的时候,节点 3 已经访问过了,就不会再更新了。 + +那有录友可能会想: 我可以改代码逻辑啊,访问过的节点,也让它继续访问不就好了? + +那么访问过的节点还能继续访问会不会有死循环的出现呢?控制逻辑不让其死循环?那特殊情况自己能都想清楚吗?(可以试试,实践出真知) + +对于负权值的出现,大家可以针对某一个场景 不断去修改 dijkstra 的代码,**但最终会发现只是 拆了东墙补西墙**,对dijkstra的补充逻辑只能满足某特定场景最短路求解。 + +对于求解带有负权值的最短路问题,可以使用 Floyd 算法 ,我在后序会详细讲解。 + +## dijkstra与prim算法的区别 + +> 这里再次提示,需要先看我的 [prim算法精讲](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w) ,否则可能不知道我下面讲的是什么。 + +大家可以发现 dijkstra的代码看上去 怎么和 prim算法这么像呢。 + +其实代码大体不差,唯一区别在 三部曲中的 第三步: 更新minDist数组 + +因为**prim是求 非访问节点到最小生成树的最小距离,而 dijkstra是求 非访问节点到源点的最小距离**。 + +prim 更新 minDist数组的写法: + + +```CPP +for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + } +} +``` + +因为 minDist表示 节点到最小生成树的最小距离,所以 新节点cur的加入,只需要 使用 grid[cur][j] ,grid[cur][j] 就表示 cur 加入生成树后,生成树到 节点j 的距离。 + +dijkstra 更新 minDist数组的写法: + +```CPP +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +因为 minDist表示 节点到源点的最小距离,所以 新节点 cur 的加入,需要使用 源点到cur的距离 (minDist[cur]) + cur 到 节点 v 的距离 (grid[cur][v]),才是 源点到节点v的距离。 + +此时大家可能不禁要想 prim算法 可以有负权值吗? + +当然可以! + +录友们可以自己思考思考一下,这是为什么? + +这里我提示一下:prim算法只需要将节点以最小权值和链接在一起,不涉及到单一路径。 + + + +## 总结 + +本篇,我们深入讲解的dijkstra算法,详细模拟其工作的流程。 + +这里我给出了 **dijkstra 三部曲 来 帮助大家理解 该算法**,不至于 每次写 dijkstra 都是黑盒操作,没有框架没有章法。 + +在给出的代码中,我也按照三部曲的逻辑来给大家注释,只要理解这三部曲,即使 过段时间 对 dijkstra 算法有些遗忘,依然可以写出一个框架出来,然后再去调试细节。 + +对于图论算法,一般代码都比较长,很难写出代码直接可以提交通过,都需要一个debug的过程,所以 **学习如何debug 非常重要**! + +这也是我为什么 在本文中 单独用来讲解 debug方法。 + +本题求的是最短路径和是多少,**同时我们也要掌握 如何把最短路径打印出来**。 + +我还写了大篇幅来讲解 负权值的情况, 只有画图带大家一步一步去 看 出现负权值 dijkstra的求解过程,才能帮助大家理解,问题出在哪里。 + +如果我直接讲:是**因为访问过的节点 不能再访问,导致错过真正的最短路**,我相信大家都不知道我在说啥。 + +最后我还讲解了 dijkstra 和 prim 算法的 相同 与 不同之处, 我在图论的讲解安排中 先讲 prim算法 再讲 dijkstra 是有目的的, **理解这两个算法的相同与不同之处 有助于大家学习的更深入**。 + +而不是 学了 dijkstra 就只看 dijkstra, 算法之间 都是有联系的,多去思考 算法之间的相互联系,会帮助大家思考的更深入,掌握的更彻底。 + +本篇写了这么长,我也只讲解了 朴素版dijkstra,**关于 堆优化dijkstra,我会在下一篇再来给大家详细讲解**。 + + + +## 堆优化版本dijkstra + +> 本篇我们来讲解 堆优化版dijkstra,看本篇之前,一定要先看 我讲解的 朴素版dijkstra,否则本篇会有部分内容看不懂。 + +在上一篇中,我们讲解了朴素版的dijkstra,该解法的时间复杂度为 O(n^2),可以看出时间复杂度 只和 n (节点数量)有关系。 + +如果n很大的话,我们可以换一个角度来优先性能。 + +在 讲解 最小生成树的时候,我们 讲了两个算法,[prim算法](https://mp.weixin.qq.com/s/yX936hHC6Z10K36Vm1Wl9w)(从点的角度来求最小生成树)、[Kruskal算法](https://mp.weixin.qq.com/s/rUVaBjCES_4eSjngceT5bw)(从边的角度来求最小生成树) + +这么在n 很大的时候,也有另一个思考维度,即:从边的数量出发。 + +当 n 很大,边 的数量 也很多的时候(稠密图),那么 上述解法没问题。 + +但 n 很大,边 的数量 很小的时候(稀疏图),是不是可以换成从边的角度来求最短路呢? + +毕竟边的数量少。 + +有的录友可能会想,n (节点数量)很大,边不就多吗? 怎么会边的数量少呢? + +别忘了,谁也没有规定 节点之间一定要有边连接着,例如有一万个节点,只有一条边,这也是一张图。 + +了解背景之后,再来看 解法思路。 + +### 图的存储 + +首先是 图的存储。 + +关于图的存储 主流有两种方式: 邻接矩阵和邻接表 + +#### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +例如: grid[2][5] = 6,表示 节点 2 链接 节点5 为有向图,节点2 指向 节点5,边的权值为6 (套在题意里,可能是距离为6 或者 消耗为6 等等) + +如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。 + + +如图: + +![](https://file1.kamacoder.com/i/algo/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间,有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点链接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +#### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4,节点4指向节点1。 + +有多少边 邻接表才会申请多少个对应的链表节点。 + +从图中可以直观看出 使用 数组 + 链表 来表达 边的链接情况 。 + +邻接表的优点: + +* 对于稀疏图的存储,只需要存储边,空间利用率高 +* 遍历节点链接情况相对容易 + +缺点: + +* 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点链接其他节点的数量。 +* 实现相对复杂,不易理解 + +#### 本题图的存储 + +接下来我们继续按照稀疏图的角度来分析本题。 + +在第一个版本的实现思路中,我们提到了三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +在第一个版本的代码中,这三部曲是套在一个 for 循环里,为什么? + +因为我们是从节点的角度来解决问题。 + +三部曲中第一步(选源点到哪个节点近且该节点未被访问过),这个操作本身需要for循环遍历 minDist 来寻找最近的节点。 + +同时我们需要 遍历所有 未访问过的节点,所以 我们从 节点角度出发,代码会有两层for循环,代码是这样的: (注意代码中的注释,标记两层for循环的用处) + +```CPP + +for (int i = 1; i <= n; i++) { // 遍历所有节点,第一层for循环 + + int minVal = INT_MAX; + int cur = 1; + + // 1、选距离源点最近且未访问过的节点 , 第二层for循环 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + +} +``` + +那么当从 边 的角度出发, 在处理 三部曲里的第一步(选源点到哪个节点近且该节点未被访问过)的时候 ,我们可以不用去遍历所有节点了。 + +而且 直接把 边(带权值)加入到 小顶堆(利用堆来自动排序),那么每次我们从 堆顶里 取出 边 自然就是 距离源点最近的节点所在的边。 + +这样我们就不需要两层for循环来寻找最近的节点了。 + +了解了大体思路,我们再来看代码实现。 + +首先是 如何使用 邻接表来表述图结构,这是摆在很多录友面前的第一个难题。 + +邻接表用 数组+链表 来表示,代码如下:(C++中 vector 为数组,list 为链表, 定义了 n+1 这么大的数组空间) + +```CPP +vector> grid(n + 1); +``` + +不少录友,不知道 如何定义的数据结构,怎么表示邻接表的,我来给大家画一个图: + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +图中邻接表表示: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4 指向 节点1 + +大家发现图中的边没有权值,而本题中 我们的边是有权值的,权值怎么表示?在哪里表示? + +所以 在`vector> grid(n + 1);` 中 就不能使用int了,而是需要一个键值对 来存两个数字,一个数表示节点,一个数表示 指向该节点的这条边的权值。 + +那么 代码可以改成这样: (pair 为键值对,可以存放两个int) + +```CPP +vector>> grid(n + 1); +``` + +举例来给大家展示 该代码表达的数据 如下: + +![](https://file1.kamacoder.com/i/algo/20240223103904.png) + +* 节点1 指向 节点3 权值为 1 +* 节点1 指向 节点5 权值为 2 +* 节点2 指向 节点4 权值为 7 +* 节点2 指向 节点3 权值为 6 +* 节点2 指向 节点5 权值为 3 +* 节点3 指向 节点4 权值为 3 +* 节点5 指向 节点1 权值为 10 + +这样 我们就把图中权值表示出来了。 + +但是在代码中 使用 `pair` 很容易让我们搞混了,第一个int 表示什么,第二个int表示什么,导致代码可读性很差,或者说别人看你的代码看不懂。 + +那么 可以 定一个类 来取代 `pair` + +类(或者说是结构体)定义如下: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +这个类里有两个成员变量,有对应的命名,这样不容易搞混 两个int的含义。 + +所以 本题中邻接表的定义如下: + +```CPP +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +vector> grid(n + 1); // 邻接表 + +``` + +(我们在下面的讲解中会直接使用这个邻接表的代码表示方式) + +### 堆优化细节 + +其实思路依然是 dijkstra 三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +只不过之前是 通过遍历节点来遍历边,通过两层for循环来寻找距离源点最近节点。 这次我们直接遍历边,且通过堆来对边进行排序,达到直接选择距离源点最近节点。 + +先来看一下针对这三部曲,如果用 堆来优化。 + +那么三部曲中的第一步(选源点到哪个节点近且该节点未被访问过),我们如何选? + +我们要选择距离源点近的节点(即:该边的权值最小),所以 我们需要一个 小顶堆 来帮我们对边的权值排序,每次从小顶堆堆顶 取边就是权值最小的边。 + +C++定义小顶堆,可以用优先级队列实现,代码如下: + +```CPP +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; +// 优先队列中存放 pair<节点编号,源点到该节点的权值> +priority_queue, vector>, mycomparison> pq; +``` + +(`pair`中 第二个int 为什么要存 源点到该节点的权值,因为 这个小顶堆需要按照权值来排序) + + +有了小顶堆自动对边的权值排序,那我们只需要直接从 堆里取堆顶元素(小顶堆中,最小的权值在上面),就可以取到离源点最近的节点了 (未访问过的节点,不会加到堆里进行排序) + +所以三部曲中的第一步,我们不用 for循环去遍历,直接取堆顶元素: + +```CPP +// pair<节点编号,源点到该节点的权值> +pair cur = pq.top(); pq.pop(); + +``` + +第二步(该最近节点被标记访问过) 这个就是将 节点做访问标记,和 朴素dijkstra 一样 ,代码如下: + +```CPP +// 2. 第二步,该最近节点被标记访问过 +visited[cur.first] = true; + +``` + +(`cur.first` 是指取 `pair` 里的第一个int,即节点编号 ) + +第三步(更新非访问节点到源点的距离),这里的思路 也是 和朴素dijkstra一样的。 + +但很多录友对这里是最懵的,主要是因为两点: + +* 没有理解透彻 dijkstra 的思路 +* 没有理解 邻接表的表达方式 + +我们来回顾一下 朴素dijkstra 在这一步的代码和思路(如果没看过我讲解的朴素版dijkstra,这里会看不懂) + +```CPP + +// 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +其中 for循环是用来做什么的? 是为了 找到 节点cur 链接指向了哪些节点,因为使用邻接矩阵的表达方式 所以把所有节点遍历一遍。 + +而在邻接表中,我们可以以相对高效的方式知道一个节点链接指向哪些节点。 + +再回顾一下邻接表的构造(数组 + 链表): + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +假如 加入的cur 是节点 2, 那么 grid[2] 表示的就是图中第二行链表。 (grid数组的构造我们在 上面 「图的存储」中讲过) + +所以在邻接表中,我们要获取 节点cur 链接指向哪些节点,就是遍历 grid[cur节点编号] 这个链表。 + +这个遍历方式,C++代码如下: + +```CPP +for (Edge edge : grid[cur.first]) +``` + +(如果不知道 Edge 是什么,看上面「图的存储」中邻接表的讲解) + +`cur.first` 就是cur节点编号, 参考上面pair的定义: pair<节点编号,源点到该节点的权值> + +接下来就是更新 非访问节点到源点的距离,代码实现和 朴素dijkstra 是一样的,代码如下: + +```CPP +// 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } +} +``` + +但为什么思路一样,有的录友能写出朴素dijkstra,但堆优化这里的逻辑就是写不出来呢? + +**主要就是因为对邻接表的表达方式不熟悉**! + +以上代码中,cur 链接指向的节点编号 为 edge.to, 这条边的权值为 edge.val ,如果对这里模糊的就再回顾一下 Edge的定义: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +确定该节点没有被访问过,`!visited[edge.to]` , 目前 源点到cur.first的最短距离(minDist) + cur.first 到 edge.to 的距离 (edge.val) 是否 小于 minDist已经记录的 源点到 edge.to 的距离 (minDist[edge.to]) + +如果是的话,就开始更新操作。 + +即: + +```CPP +if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); // 由于cur节点的加入,而新链接的边,加入到优先级队里中 +} + +``` + +同时,由于cur节点的加入,源点又有可以新链接到的边,将这些边加入到优先级队里中。 + + +以上代码思路 和 朴素版dijkstra 是一样一样的,主要区别是两点: + +* 邻接表的表示方式不同 +* 使用优先级队列(小顶堆)来对新链接的边排序 + +### 代码实现 + +堆优化dijkstra完整代码如下: + +```CPP +class Solution { +public: + // 小顶堆 + class mycomparison { + public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } + }; + // 定义一个结构体来表示带权重的边 + struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 + }; + int networkDelayTime(vector>& times, int n, int k) { + + + std::vector> grid(n + 1); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + // p1 指向 p2,权值为 times[i][2] + grid[p1].push_back(Edge(p2, times[i][2])); + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 pair<节点,源点到该节点的距离> + priority_queue, vector>, mycomparison> pq; + + pq.push(pair(k, 0)); + minDist[k] = 0; // 这个不要忘了 + + while (!pq.empty()) { + // <节点, 源点到该节点的距离> + // 1. 第一步,选源点到哪个节点近且该节点未被访问过 (通过优先级队列来实现) + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + // 2. 第二步,该最近节点被标记访问过 + visited[cur.first] = true; + + + // 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } + } + + } + + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; + +``` + +* 时间复杂度:O(ElogE) E 为边的数量 +* 空间复杂度:O(N + E) N 为节点的数量 + +堆优化的时间复杂度 只和边的数量有关 和节点数无关,在 优先级队列中 放的也是边。 + +以上代码中,`while (!pq.empty())` 里套了 `for (Edge edge : grid[cur.first])` + +`for` 里 遍历的是 当前节点 cur 所连接边。 + +那 当前节点cur 所连接的边 也是不固定的, 这就让大家分不清,这时间复杂度究竟是多少? + +其实 `for (Edge edge : grid[cur.first])` 里最终的数据走向 是 给队列里添加边。 + +那么跳出局部代码,整个队列 一定是 所有边添加了一次,同时也弹出了一次。 + +所以边添加一次时间复杂度是 O(E), `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。 + +(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总是是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出) + +所以 该算法整体时间复杂度为 O(ElogE) + +网上的不少分析 会把 n (节点的数量)算进来,这个分析是有问题的,举一个极端例子,在n 为 10000,且是有一条边的 图里,以上代码,大家感觉执行了多少次? + +`while (!pq.empty())` 中的 pq 存的是边,其实只执行了一次。 + +所以该算法时间复杂度 和 节点没有关系。 + +至于空间复杂度,邻接表是 数组 + 链表 数组的空间 是 N ,有E条边 就申请对应多少个链表节点,所以是 复杂度是 N + E + +## 拓展 + +当然也有录友可能想 堆优化dijkstra 中 我为什么一定要用邻接表呢,我就用邻接矩阵 行不行 ? + +也行的。 + +但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的。 + +如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。 + +这里我也给出 邻接矩阵版本的堆优化dijkstra代码: + +```CPP +class Solution { +public: + // 小顶堆(按照中的v 来从小到大排序) + class mycomparison { + public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } + }; + int networkDelayTime(vector>& times, int n, int k) { + + // 注意题目中给的二维数组并不是邻接矩阵 + // 需要邻接矩阵来存图 + // 因为本题处理方式是节点标号从1开始,所以数组的大小都是 n+1 + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < times.size(); i++){ + int p1 = times[i][0]; + int p2 = times[i][1]; + grid[p1][p2] = times[i][2]; + } + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 [节点,源点到该节点的距离] + priority_queue, vector>, mycomparison> pq; + + pq.push(pair(k, 0)); + minDist[k] = 0; // 这个不要忘了 + + while (!pq.empty()) { + // <节点, 源点到该节点的距离> + // 1、选距离源点最近且未访问过的节点 + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + // 2、标记该节点已被访问 + visited[cur.first] = true; + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + // 遍历 cur 可以链接的节点,更新 minDist[j] + for (int j = 1; j <= n; j++) { + if (!visited[j] && grid[cur.first][j] != INT_MAX && (minDist[cur.first] + grid[cur.first][j] < minDist[j])) { + minDist[j] = minDist[cur.first] + grid[cur.first][j]; + pq.push(pair(j, minDist[j])); + } + } + } + + // 源点到最远的节点的时间,也就是寻找 源点到所有节点最短路径的最大值 + int result = 0; + for (int i = 1; i <= n; i++) { + if (minDist[i] == INT_MAX) return -1;// 没有路径 + result = max(minDist[i], result); + } + + + return result; + + } +}; + +``` + +* 时间复杂度:O(E * (N + logE)) E为边的数量,N为节点数量 +* 空间复杂度:O(log(N^2)) + +`while (!pq.empty())` 时间复杂度为 E ,while 里面 每次取元素 时间复杂度 为 logE,和 一个for循环 时间复杂度 为 N 。 + +所以整体是 E * (N + logE) + + +## 总结 + +在学习一种优化思路的时候,首先就要知道为什么要优化,遇到了什么问题。 + +正如我在开篇就给大家交代清楚 堆优化方式的背景。 + +堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度触发,且利用堆来排序。 + +很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。 + +主要是因为两点: + +* 不熟悉邻接表的表达方式 +* 对dijkstra的实现思路还是不熟 + +这是我为什么 本篇花了大力气来讲解 图的存储,就是为了让大家彻底理解邻接表以及邻接表的代码写法。 + +至于 dijkstra的实现思路 ,朴素版 和 堆优化版本 都是 按照 dijkstra 三部曲来的。 + +理解了三部曲,dijkstra 的思路就是清晰的。 + +针对邻接表版本代码 我做了详细的 时间复杂度分析,也让录友们清楚,相对于 朴素版,时间都优化到哪了。 + +最后 我也给出了 邻接矩阵的版本代码,分析了这一版本的必要性以及时间复杂度。 + +至此通过 两篇dijkstra的文章,终于把 dijkstra 讲完了,如果大家对我讲解里所涉及的内容都吃透的话,详细对 dijkstra 算法也就理解到位了。 + +这里在给出本题的Bellman_ford解法,关于 Bellman_ford ,后面我会专门来讲解的,Bellman_ford 有其独特的应用场景 + +```CPP +class Solution { +public: + + int networkDelayTime(vector>& times, int n, int k) { + vector minDist(n + 1 , INT_MAX/2); + minDist[k] = 0; + //vector minDist_copy(n); // 用来记录每一次遍历的结果 + for (int i = 1; i <= n + 1; i++) { + //minDist_copy = minDist; // 获取上一次计算的结果 + for (auto &f : times) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + if (minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + + } + int result = 0; + for (int i = 1;i <= n; i++) { + if (minDist[i] == INT_MAX/2) return -1;// 没有路径 + result = max(minDist[i], result); + } + return result; + + } +}; +``` + diff --git "a/problems/0746.\344\275\277\347\224\250\346\234\200\345\260\217\350\212\261\350\264\271\347\210\254\346\245\274\346\242\257.md" "b/problems/0746.\344\275\277\347\224\250\346\234\200\345\260\217\350\212\261\350\264\271\347\210\254\346\245\274\346\242\257.md" new file mode 100755 index 0000000000..952d4d2ab7 --- /dev/null +++ "b/problems/0746.\344\275\277\347\224\250\346\234\200\345\260\217\350\212\261\350\264\271\347\210\254\346\245\274\346\242\257.md" @@ -0,0 +1,538 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 746. 使用最小花费爬楼梯 + +[力扣题目链接](https://leetcode.cn/problems/min-cost-climbing-stairs/) + +**旧题目描述**: + +数组的每个下标作为一个阶梯,第 i 个阶梯对应着一个非负数的体力花费值 cost[i](下标从 0 开始)。 + +每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。 + +请你找出达到楼层顶部的最低花费。在开始时,你可以选择从下标为 0 或 1 的元素作为初始阶梯。 + +示例 1: + +* 输入:cost = [10, 15, 20] +* 输出:15 +* 解释:最低花费是从 cost[1] 开始,然后走两步即可到阶梯顶,一共花费 15 。 + +示例 2: + +* 输入:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] +* 输出:6 +* 解释:最低花费方式是从 cost[0] 开始,逐个经过那些 1 ,跳过 cost[3] ,一共花费 6 。 + +提示: + +* cost 的长度范围是 [2, 1000]。 +* cost[i] 将会是一个整型数据,范围为 [0, 999] 。 + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)::[动态规划开更了!| LeetCode:746. 使用最小花费爬楼梯](https://www.bilibili.com/video/BV16G411c7yZ/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +----------- + +本题之前的题目描述是很模糊的,看不出来,第一步需要花费体力值,最后一步不用花费,还是说 第一步不花费体力值,最后一步花费。 + +后来力扣改了题目描述,**新题目描述**: + +给你一个整数数组 cost ,其中 cost[i] 是从楼梯第 i 个台阶向上爬需要支付的费用。一旦你支付此费用,即可选择向上爬一个或者两个台阶。 + +你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。 + +请你计算并返回达到楼梯顶部的最低花费。 + +![](https://file1.kamacoder.com/i/algo/20221031170131.png) + + +## 思路 + +(**在力扣修改了题目描述下,我又重新修改了题解**) + +修改之后的题意就比较明确了,题目中说 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯” 也就是相当于 跳到 下标 0 或者 下标 1 是不花费体力的, 从 下标 0 下标1 开始跳就要花费体力了。 + +1. 确定dp数组以及下标的含义 + +使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。 + +**dp[i]的定义:到达第i台阶所花费的最少体力为dp[i]**。 + +**对于dp数组的定义,大家一定要清晰!** + +2. 确定递推公式 + +**可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]**。 + +dp[i - 1] 跳到 dp[i] 需要花费 dp[i - 1] + cost[i - 1]。 + +dp[i - 2] 跳到 dp[i] 需要花费 dp[i - 2] + cost[i - 2]。 + +那么究竟是选从dp[i - 1]跳还是从dp[i - 2]跳呢? + +一定是选最小的,所以dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); + + +3. dp数组如何初始化 + +看一下递归公式,dp[i]由dp[i - 1],dp[i - 2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。 + +那么 dp[0] 应该是多少呢? 根据dp数组的定义,到达第0台阶所花费的最小体力为dp[0],那么有同学可能想,那dp[0] 应该是 cost[0],例如 cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] 的话,dp[0] 就是 cost[0] 应该是1。 + +这里就要说明本题力扣为什么改题意,而且修改题意之后 就清晰很多的原因了。 + +新题目描述中明确说了 “你可以选择从下标为 0 或下标为 1 的台阶开始爬楼梯。” 也就是说 到达 第 0 个台阶是不花费的,但从 第0 个台阶 往上跳的话,需要花费 cost[0]。 + +所以初始化 dp[0] = 0,dp[1] = 0; + + +4. 确定遍历顺序 + +最后一步,递归公式有了,初始化有了,如何遍历呢? + +本题的遍历顺序其实比较简单,简单到很多同学都忽略了思考这一步直接就把代码写出来了。 + +因为是模拟台阶,而且dp[i]由dp[i-1]dp[i-2]推出,所以是从前到后遍历cost数组就可以了。 + +> **但是稍稍有点难度的动态规划,其遍历顺序并不容易确定下来**。 +> 例如:01背包,都知道两个for循环,一个for遍历物品嵌套一个for遍历背包容量,那么为什么不是一个for遍历背包容量嵌套一个for遍历物品呢? 以及在使用一维dp数组的时候遍历背包容量为什么要倒序呢? + +**这些都与遍历顺序息息相关。当然背包问题后续「代码随想录」都会重点讲解的!** + +5. 举例推导dp数组 + +拿示例2:cost = [1, 100, 1, 1, 1, 100, 1, 1, 100, 1] ,来模拟一下dp数组的状态变化,如下: + +![](https://file1.kamacoder.com/i/algo/20221026175104.png) + +如果大家代码写出来有问题,就把dp数组打印出来,看看和如上推导的是不是一样的。 + +以上分析完毕,整体C++代码如下: + +```CPP +class Solution { +public: + int minCostClimbingStairs(vector& cost) { + vector dp(cost.size() + 1); + dp[0] = 0; // 默认第一步都是不花费体力的 + dp[1] = 0; + for (int i = 2; i <= cost.size(); i++) { + dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); + } + return dp[cost.size()]; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +还可以优化空间复杂度,因为dp[i]就是由前两位推出来的,那么也不用dp数组了,C++代码如下: + +```CPP +// 版本二 +class Solution { +public: + int minCostClimbingStairs(vector& cost) { + int dp0 = 0; + int dp1 = 0; + for (int i = 2; i <= cost.size(); i++) { + int dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]); + dp0 = dp1; // 记录一下前两位 + dp1 = dpi; + } + return dp1; + } +}; + +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +当然如果在面试中,能写出版本一就行,除非面试官额外要求 空间复杂度,那么再去思考版本二,因为版本二还是有点绕。版本一才是正常思路。 + + +## 拓展 + +旧力扣描述,如果按照 第一步是花费的,最后一步不花费,那么代码是这么写的,提交也可以通过 + + +```CPP +// 版本一 +class Solution { +public: + int minCostClimbingStairs(vector& cost) { + vector dp(cost.size()); + dp[0] = cost[0]; // 第一步有花费 + dp[1] = cost[1]; + for (int i = 2; i < cost.size(); i++) { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; + } + // 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值 + return min(dp[cost.size() - 1], dp[cost.size() - 2]); + } +}; +``` + +当然如果对 动态规划 理解不够深入的话,拓展内容就别看了,容易越看越懵。 + +## 总结 + +大家可以发现这道题目相对于 昨天的[动态规划:爬楼梯](https://programmercarl.com/0070.爬楼梯.html)又难了一点,但整体思路是一样的。 + +从[动态规划:斐波那契数](https://programmercarl.com/0509.斐波那契数.html)到 [动态规划:爬楼梯](https://programmercarl.com/0070.爬楼梯.html)再到今天这道题目,录友们感受到循序渐进的梯度了嘛。 + +每个系列开始的时候,都有录友和我反馈说题目太简单了,赶紧上难度,但也有录友和我说有点难了,快跟不上了。 + +其实我选的题目都是有目的性的,就算是简单题,也是为了练习方法论,然后难度都是梯度上来的,一环扣一环。 + +但我也可以随便选来一道难题讲呗,这其实是最省事的,不用管什么题目顺序,看心情找一道就讲。 + +难的是把题目按梯度排好,循序渐进,再按照统一方法论把这些都串起来,所以大家不要催我哈,按照我的节奏一步一步来就行了。 + +## 其他语言版本 + +以下版本其他语言版本,大多是按照旧力扣题解来写的,欢迎大家在[Github](https://github.com/youngyangyang04/leetcode-master)上[提交pr](https://mp.weixin.qq.com/s/tqCxrMEU-ajQumL1i8im9A),修正一波。 + +### Java + +```Java +// 方式一:第一步不支付费用 +class Solution { + public int minCostClimbingStairs(int[] cost) { + int len = cost.length; + int[] dp = new int[len + 1]; + + // 从下标为 0 或下标为 1 的台阶开始,因此支付费用为0 + dp[0] = 0; + dp[1] = 0; + + // 计算到达每一层台阶的最小费用 + for (int i = 2; i <= len; i++) { + dp[i] = Math.min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); + } + + return dp[len]; + } +} +``` + +```Java +// 方式二:第一步支付费用 +class Solution { + public int minCostClimbingStairs(int[] cost) { + int[] dp = new int[cost.length]; + dp[0] = cost[0]; + dp[1] = cost[1]; + for (int i = 2; i < cost.length; i++) { + dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]; + } + //最后一步,如果是由倒数第二步爬,则最后一步的体力花费可以不用算 + return Math.min(dp[cost.length - 1], dp[cost.length - 2]); + } +} +``` + +```Java +// 状态压缩,使用三个变量来代替数组 +class Solution { + public int minCostClimbingStairs(int[] cost) { + // 以下三个变量分别表示前两个台阶的最少费用、前一个的、当前的。 + int beforeTwoCost = 0, beforeOneCost = 0, currentCost = 0; + // 前两个台阶不需要费用就能上到,因此从下标2开始;因为最后一个台阶需要跨越,所以需要遍历到cost.length + for (int i = 2; i <= cost.length; i ++) { + // 此处遍历的是cost[i - 1],不会越界 + currentCost = Math.min(beforeOneCost + cost[i - 1], beforeTwoCost + cost[i - 2]); + beforeTwoCost = beforeOneCost; + beforeOneCost = currentCost; + } + return currentCost; + } +} +``` + +### Python + +动态规划(版本一) +```python + +class Solution: + def minCostClimbingStairs(self, cost: List[int]) -> int: + dp = [0] * (len(cost) + 1) + dp[0] = 0 # 初始值,表示从起点开始不需要花费体力 + dp[1] = 0 # 初始值,表示经过第一步不需要花费体力 + + for i in range(2, len(cost) + 1): + # 在第i步,可以选择从前一步(i-1)花费体力到达当前步,或者从前两步(i-2)花费体力到达当前步 + # 选择其中花费体力较小的路径,加上当前步的花费,更新dp数组 + dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]) + + return dp[len(cost)] # 返回到达楼顶的最小花费 + +``` +动态规划(版本二) + +```python +class Solution: + def minCostClimbingStairs(self, cost: List[int]) -> int: + dp0 = 0 # 初始值,表示从起点开始不需要花费体力 + dp1 = 0 # 初始值,表示经过第一步不需要花费体力 + + for i in range(2, len(cost) + 1): + # 在第i步,可以选择从前一步(i-1)花费体力到达当前步,或者从前两步(i-2)花费体力到达当前步 + # 选择其中花费体力较小的路径,加上当前步的花费,得到当前步的最小花费 + dpi = min(dp1 + cost[i - 1], dp0 + cost[i - 2]) + + dp0 = dp1 # 更新dp0为前一步的值,即上一次循环中的dp1 + dp1 = dpi # 更新dp1为当前步的最小花费 + + return dp1 # 返回到达楼顶的最小花费 + +``` +动态规划(版本三) + +```python +class Solution: + def minCostClimbingStairs(self, cost: List[int]) -> int: + dp = [0] * len(cost) + dp[0] = cost[0] # 第一步有花费 + dp[1] = cost[1] + for i in range(2, len(cost)): + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i] + # 注意最后一步可以理解为不用花费,所以取倒数第一步,第二步的最少值 + return min(dp[-1], dp[-2]) + +``` +动态规划(版本四) + +```python +class Solution: + def minCostClimbingStairs(self, cost: List[int]) -> int: + n = len(cost) + prev_1 = cost[0] # 前一步的最小花费 + prev_2 = cost[1] # 前两步的最小花费 + for i in range(2, n): + current = min(prev_1, prev_2) + cost[i] # 当前位置的最小花费 + prev_1, prev_2 = prev_2, current # 更新前一步和前两步的最小花费 + return min(prev_1, prev_2) # 最后一步可以理解为不用花费,取倒数第一步和第二步的最少值 + + +``` +### Go +```Go +func minCostClimbingStairs(cost []int) int { + f := make([]int, len(cost) + 1) + f[0], f[1] = 0, 0 + for i := 2; i <= len(cost); i++ { + f[i] = min(f[i-1] + cost[i-1], f[i-2] + cost[i-2]) + } + return f[len(cost)] +} +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` +``` GO +第二种思路: dp[i]表示从i层起跳所需要支付的最小费用 +递推公式: +i) -> i32 { + let mut dp = vec![0; cost.len() + 1]; + for i in 2..=cost.len() { + dp[i] = (dp[i - 1] + cost[i - 1]).min(dp[i - 2] + cost[i - 2]); + } + dp[cost.len()] + } +} +``` + +不使用 dp 数组 + +```rust +impl Solution { + pub fn min_cost_climbing_stairs(cost: Vec) -> i32 { + let (mut dp_before, mut dp_after) = (0, 0); + for i in 2..=cost.len() { + let dpi = (dp_before + cost[i - 2]).min(dp_after + cost[i - 1]); + dp_before = dp_after; + dp_after = dpi; + } + dp_after + } +} +``` + +### C + +```c +#include +int minCostClimbingStairs(int *cost, int costSize) { + int dp[costSize + 1]; + dp[0] = dp[1] = 0; + for (int i = 2; i <= costSize; i++) { + dp[i] = fmin(dp[i - 2] + cost[i - 2], dp[i - 1] + cost[i - 1]); + } + return dp[costSize]; +} +``` + +不使用 dp 数组 + +```c +#include +int minCostClimbingStairs(int *cost, int costSize) { + int dpBefore = 0, dpAfter = 0; + for (int i = 2; i <= costSize; i++) { + int dpi = fmin(dpBefore + cost[i - 2], dpAfter + cost[i - 1]); + dpBefore = dpAfter; + dpAfter = dpi; + } + return dpAfter; +} +``` + +### Scala + +```scala +object Solution { + def minCostClimbingStairs(cost: Array[Int]): Int = { + var dp = new Array[Int](cost.length) + dp(0) = cost(0) + dp(1) = cost(1) + for (i <- 2 until cost.length) { + dp(i) = math.min(dp(i - 1), dp(i - 2)) + cost(i) + } + math.min(dp(cost.length - 1), dp(cost.length - 2)) + } +} +``` + +第二种思路: dp[i] 表示爬到第i-1层所需的最小花费,状态转移方程为: dp[i] = min(dp[i-1]+cost[i-1],dp[i-2]+cost[i-2]) +```scala +object Solution { + def minCostClimbingStairs(cost: Array[Int]): Int = { + var dp = new Array[Int](cost.length + 1) + for (i <- 2 until cost.length + 1) { + dp(i) = math.min(dp(i - 1) + cost(i - 1), dp(i - 2) + cost(i - 2)) + } + dp(cost.length) + } +} +``` + +### C# + +```csharp +public class Solution +{ + public int MinCostClimbingStairs(int[] cost) + { + int[] dp=new int[2] { cost[0], cost[1] }; + for (int i = 2; i < cost.Length; i++) + { + int temp = Math.Min(dp[0], dp[1])+cost[i]; + dp[0]=dp[1]; + dp[1]=temp; + } + return Math.Min(dp[0],dp[1]); + } +} +``` + + + + diff --git "a/problems/0763.\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264.md" "b/problems/0763.\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264.md" new file mode 100755 index 0000000000..d17878381f --- /dev/null +++ "b/problems/0763.\345\210\222\345\210\206\345\255\227\346\257\215\345\214\272\351\227\264.md" @@ -0,0 +1,462 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 763.划分字母区间 + +[力扣题目链接](https://leetcode.cn/problems/partition-labels/) + +字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 + +示例: +* 输入:S = "ababcbacadefegdehijhklij" +* 输出:[9,7,8] +解释: +划分结果为 "ababcbaca", "defegde", "hijhklij"。 +每个字母最多出现在一个片段中。 +像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。 + +提示: + +* S的长度在[1, 500]之间。 +* S只包含小写字母 'a' 到 'z' 。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,寻找最远的出现位置! LeetCode:763.划分字母区间](https://www.bilibili.com/video/BV18G4y1K7d5),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索。 + +题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢? + +如果没有接触过这种题目的话,还挺有难度的。 + +在遍历的过程中相当于是要找每一个字母的边界,**如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了**。此时前面出现过所有字母,最远也就到这个边界了。 + +可以分为如下两步: + +* 统计每一个字符最后出现的位置 +* 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点 + +如图: + + +![763.划分字母区间](https://file1.kamacoder.com/i/algo/20201222191924417.png) + +明白原理之后,代码并不复杂,如下: + +```CPP +class Solution { +public: + vector partitionLabels(string S) { + int hash[27] = {0}; // i为字符,hash[i]为字符出现的最后位置 + for (int i = 0; i < S.size(); i++) { // 统计每一个字符最后出现的位置 + hash[S[i] - 'a'] = i; + } + vector result; + int left = 0; + int right = 0; + for (int i = 0; i < S.size(); i++) { + right = max(right, hash[S[i] - 'a']); // 找到字符出现的最远边界 + if (i == right) { + result.push_back(right - left + 1); + left = i + 1; + } + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1),使用的hash数组是固定大小 + +## 总结 + +这道题目leetcode标记为贪心算法,说实话,我没有感受到贪心,找不出局部最优推出全局最优的过程。就是用最远出现距离模拟了圈字符的行为。 + +但这道题目的思路是很巧妙的,所以有必要介绍给大家做一做,感受一下。 + +## 补充 + +这里提供一种与[452.用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)、[435.无重叠区间](https://programmercarl.com/0435.无重叠区间.html)相同的思路。 + +统计字符串中所有字符的起始和结束位置,记录这些区间(实际上也就是[435.无重叠区间](https://programmercarl.com/0435.无重叠区间.html)题目里的输入),**将区间按左边界从小到大排序,找到边界将区间划分成组,互不重叠。找到的边界就是答案。** + +```CPP +class Solution { +public: + static bool cmp(vector &a, vector &b) { + return a[0] < b[0]; + } + // 记录每个字母出现的区间 + vector> countLabels(string s) { + vector> hash(26, vector(2, INT_MIN)); + vector> hash_filter; + for (int i = 0; i < s.size(); ++i) { + if (hash[s[i] - 'a'][0] == INT_MIN) { + hash[s[i] - 'a'][0] = i; + } + hash[s[i] - 'a'][1] = i; + } + // 去除字符串中未出现的字母所占用区间 + for (int i = 0; i < hash.size(); ++i) { + if (hash[i][0] != INT_MIN) { + hash_filter.push_back(hash[i]); + } + } + return hash_filter; + } + vector partitionLabels(string s) { + vector res; + // 这一步得到的 hash 即为无重叠区间题意中的输入样例格式:区间列表 + // 只不过现在我们要求的是区间分割点 + vector> hash = countLabels(s); + // 按照左边界从小到大排序 + sort(hash.begin(), hash.end(), cmp); + // 记录最大右边界 + int rightBoard = hash[0][1]; + int leftBoard = 0; + for (int i = 1; i < hash.size(); ++i) { + // 由于字符串一定能分割,因此, + // 一旦下一区间左边界大于当前右边界,即可认为出现分割点 + if (hash[i][0] > rightBoard) { + res.push_back(rightBoard - leftBoard + 1); + leftBoard = hash[i][0]; + } + rightBoard = max(rightBoard, hash[i][1]); + } + // 最右端 + res.push_back(rightBoard - leftBoard + 1); + return res; + } +}; +``` + +## 其他语言版本 + + +### Java +```java +class Solution { + public List partitionLabels(String S) { + List list = new LinkedList<>(); + int[] edge = new int[26]; + char[] chars = S.toCharArray(); + for (int i = 0; i < chars.length; i++) { + edge[chars[i] - 'a'] = i; + } + int idx = 0; + int last = -1; + for (int i = 0; i < chars.length; i++) { + idx = Math.max(idx,edge[chars[i] - 'a']); + if (i == idx) { + list.add(i - last); + last = i; + } + } + return list; + } +} + +class Solution{ + /*解法二: 上述c++补充思路的Java代码实现*/ + + public int[][] findPartitions(String s) { + List temp = new ArrayList<>(); + int[][] hash = new int[26][2];//26个字母2列 表示该字母对应的区间 + + for (int i = 0; i < s.length(); i++) { + //更新字符c对应的位置i + char c = s.charAt(i); + if (hash[c - 'a'][0] == 0) hash[c - 'a'][0] = i; + + hash[c - 'a'][1] = i; + + //第一个元素区别对待一下 + hash[s.charAt(0) - 'a'][0] = 0; + } + + + List> h = new LinkedList<>(); + //组装区间 + for (int i = 0; i < 26; i++) { + //if (hash[i][0] != hash[i][1]) { + temp.clear(); + temp.add(hash[i][0]); + temp.add(hash[i][1]); + //System.out.println(temp); + h.add(new ArrayList<>(temp)); + // } + } + // System.out.println(h); + // System.out.println(h.size()); + int[][] res = new int[h.size()][2]; + for (int i = 0; i < h.size(); i++) { + List list = h.get(i); + res[i][0] = list.get(0); + res[i][1] = list.get(1); + } + + return res; + + } + + public List partitionLabels(String s) { + int[][] partitions = findPartitions(s); + List res = new ArrayList<>(); + Arrays.sort(partitions, (o1, o2) -> Integer.compare(o1[0], o2[0])); + int right = partitions[0][1]; + int left = 0; + for (int i = 0; i < partitions.length; i++) { + if (partitions[i][0] > right) { + //左边界大于右边界即可纪委一次分割 + res.add(right - left + 1); + left = partitions[i][0]; + } + right = Math.max(right, partitions[i][1]); + + } + //最右端 + res.add(right - left + 1); + return res; + + } +} +``` + +### Python +贪心(版本一) +```python +class Solution: + def partitionLabels(self, s: str) -> List[int]: + last_occurrence = {} # 存储每个字符最后出现的位置 + for i, ch in enumerate(s): + last_occurrence[ch] = i + + result = [] + start = 0 + end = 0 + for i, ch in enumerate(s): + end = max(end, last_occurrence[ch]) # 找到当前字符出现的最远位置 + if i == end: # 如果当前位置是最远位置,表示可以分割出一个区间 + result.append(end - start + 1) + start = i + 1 + + return result + +``` +贪心(版本二)与452.用最少数量的箭引爆气球 (opens new window)、435.无重叠区间 (opens new window)相同的思路。 +```python +class Solution: + def countLabels(self, s): + # 初始化一个长度为26的区间列表,初始值为负无穷 + hash = [[float('-inf'), float('-inf')] for _ in range(26)] + hash_filter = [] + for i in range(len(s)): + if hash[ord(s[i]) - ord('a')][0] == float('-inf'): + hash[ord(s[i]) - ord('a')][0] = i + hash[ord(s[i]) - ord('a')][1] = i + for i in range(len(hash)): + if hash[i][0] != float('-inf'): + hash_filter.append(hash[i]) + return hash_filter + + def partitionLabels(self, s): + res = [] + hash = self.countLabels(s) + hash.sort(key=lambda x: x[0]) # 按左边界从小到大排序 + rightBoard = hash[0][1] # 记录最大右边界 + leftBoard = 0 + for i in range(1, len(hash)): + if hash[i][0] > rightBoard: # 出现分割点 + res.append(rightBoard - leftBoard + 1) + leftBoard = hash[i][0] + rightBoard = max(rightBoard, hash[i][1]) + res.append(rightBoard - leftBoard + 1) # 最右端 + return res + +``` + +### Go + +```go + +func partitionLabels(s string) []int { + var res []int; + var marks [26]int; + size, left, right := len(s), 0, 0; + for i := 0; i < size; i++ { + marks[s[i] - 'a'] = i; + } + for i := 0; i < size; i++ { + right = max(right, marks[s[i] - 'a']); + if i == right { + res = append(res, right - left + 1); + left = i + 1; + } + } + return res; +} + +func max(a, b int) int { + if a < b { + a = b; + } + return a; +} +``` + +### JavaScript +```Javascript +var partitionLabels = function(s) { + let hash = {} + for(let i = 0; i < s.length; i++) { + hash[s[i]] = i + } + let result = [] + let left = 0 + let right = 0 + for(let i = 0; i < s.length; i++) { + right = Math.max(right, hash[s[i]]) + if(right === i) { + result.push(right - left + 1) + left = i + 1 + } + } + return result +}; +``` + +### TypeScript + +```typescript +function partitionLabels(s: string): number[] { + const length: number = s.length; + const resArr: number[] = []; + const helperMap: Map = new Map(); + for (let i = 0; i < length; i++) { + helperMap.set(s[i], i); + } + let left: number = 0; + let right: number = 0; + for (let i = 0; i < length; i++) { + right = Math.max(helperMap.get(s[i])!, right); + if (i === right) { + resArr.push(i - left + 1); + left = i + 1; + } + } + return resArr; +}; +``` + +### Scala + +```scala +object Solution { + import scala.collection.mutable + def partitionLabels(s: String): List[Int] = { + var hash = new Array[Int](26) + for (i <- s.indices) { + hash(s(i) - 'a') = i + } + + var res = mutable.ListBuffer[Int]() + var (left, right) = (0, 0) + for (i <- s.indices) { + right = math.max(hash(s(i) - 'a'), right) + if (i == right) { + res.append(right - left + 1) + left = i + 1 + } + } + + res.toList + } +} +``` + +### Rust + +```Rust +impl Solution { + pub fn partition_labels(s: String) -> Vec { + let mut hash = vec![0; 26]; + for (i, &c) in s.as_bytes().iter().enumerate() { + hash[(c - b'a') as usize] = i; + } + let mut res = vec![]; + let (mut left, mut right) = (0, 0); + for (i, &c) in s.as_bytes().iter().enumerate() { + right = right.max(hash[(c - b'a') as usize]); + if i == right { + res.push((right - left + 1) as i32); + left = i + 1; + } + } + res + } +} +``` +### C + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int* partitionLabels(char* s, int* returnSize) { + // 记录每个字符最远出现的位置 + int last[26] = {0}; + int len = strlen(s); + for (int i = 0; i < len; ++i) { + last[s[i] - 'a'] = i; + } + int left = 0, right = 0; + int * partition = malloc(sizeof (int ) * len); + // 初始化值 + *returnSize = 0; + for(int i = 0; i < len; i++){ + right = max(right, last[s[i] - 'a']); + // 到达最远位置,加入答案,并且更新左边下标 + if(i == right){ + partition[(*returnSize)++] = right - left + 1; + left = i + 1; + } + } + return partition; +} +``` + + + +### C# + +```csharp +public class Solution +{ + public IList PartitionLabels(string s) + { + int[] location = new int[27]; + for (int i = 0; i < s.Length; i++) + { + location[s[i] - 'a'] = i; + } + List res = new List(); + int left = 0, right = 0; + for (int i = 0; i < s.Length; i++) + { + right = Math.Max(right, location[s[i] - 'a']); + if (i == right) + { + res.Add(right - left + 1); + left = i + 1; + } + } + return res; + } +} +``` + diff --git "a/problems/0787.K\347\253\231\344\270\255\350\275\254\345\206\205\346\234\200\344\276\277\345\256\234\347\232\204\350\210\252\347\217\255.md" "b/problems/0787.K\347\253\231\344\270\255\350\275\254\345\206\205\346\234\200\344\276\277\345\256\234\347\232\204\350\210\252\347\217\255.md" new file mode 100644 index 0000000000..fb58c14816 --- /dev/null +++ "b/problems/0787.K\347\253\231\344\270\255\350\275\254\345\206\205\346\234\200\344\276\277\345\256\234\347\232\204\350\210\252\347\217\255.md" @@ -0,0 +1,180 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 787. K 站中转内最便宜的航班 + +有 n 个城市通过一些航班连接。给你一个数组 flights ,其中 flights[i] = [fromi, toi, pricei] ,表示该航班都从城市 fromi 开始,以价格 pricei 抵达 toi。 + +现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到出一条最多经过 k 站中转的路线,使得从 src 到 dst 的 价格最便宜 ,并返回该价格。 如果不存在这样的路线,则输出 -1。 + + +![](https://file1.kamacoder.com/i/algo/20240319103900.png) + +![](https://file1.kamacoder.com/i/algo/20240319103919.png) + +![](https://file1.kamacoder.com/i/algo/20240319104026.png) + + +## 思路 + + + +```CPP +class Solution { +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + minDist[src] = 0; + vector minDist_copy(n); // 用来记录每一次遍历的结果 + for (int i = 1; i <= k + 1; i++) { + minDist_copy = minDist; // 获取上一次计算的结果 + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + minDist[to] = min(minDist_copy[from] + price, minDist[to]); + // if (minDist[to] > minDist_copy[from] + price) minDist[to] = minDist_copy[from] + price; + } + + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; +``` + +下面是典型的错误写法 + +```CPP +class Solution { +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + minDist[src] = 0; + for (int i = 1; i <= k + 1; i++) { + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + if (minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; +``` + + +----------- + +SPFA + + +class Solution { +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + vector> grid(n + 1); // 邻接表 + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + grid[from].push_back(Edge(to, price)); + + } + minDist[src] = 0; + vector minDist_copy(n); // 用来记录每一次遍历的结果 + k++; + queue que; + que.push(src); + std::vector visited(n + 1, false); // 可加,可不加,加了效率高一些,防止重复访问 + int que_size; + while (k-- && !que.empty()) { + + minDist_copy = minDist; // 获取上一次计算的结果 + que_size = que.size(); + while (que_size--) { // 这个while循环的设计实在是妙啊 + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + que.push(to); + } + } + + } + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; + + + +队列加上 visited 不能重复访问 + +class Solution { +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +public: + int findCheapestPrice(int n, vector>& flights, int src, int dst, int k) { + vector minDist(n , INT_MAX/2); + vector> grid(n + 1); // 邻接表 + for (auto &f : flights) { + int from = f[0]; + int to = f[1]; + int price = f[2]; + grid[from].push_back(Edge(to, price)); + + } + minDist[src] = 0; + vector minDist_copy(n); // 用来记录每一次遍历的结果 + k++; + queue que; + que.push(src); + int que_size; + while (k-- && !que.empty()) { + // 注意这个数组放的位置 + vector visited(n + 1, false); // 可加,可不加,加了效率高一些,防止队列里重复访问,其数值已经算过了 + minDist_copy = minDist; // 获取上一次计算的结果 + que_size = que.size(); + while (que_size--) { + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + if(visited[to]) continue; // 不用重复放入队列,但需要重复计算,所以放在这里位置 + visited[to] = true; + que.push(to); + } + } + + } + } + int result = minDist[dst] == INT_MAX/2 ? -1 : minDist[dst]; + return result; + } +}; + + + diff --git "a/problems/0797.\346\211\200\346\234\211\345\217\257\350\203\275\347\232\204\350\267\257\345\276\204.md" "b/problems/0797.\346\211\200\346\234\211\345\217\257\350\203\275\347\232\204\350\267\257\345\276\204.md" new file mode 100755 index 0000000000..db4d249a15 --- /dev/null +++ "b/problems/0797.\346\211\200\346\234\211\345\217\257\350\203\275\347\232\204\350\267\257\345\276\204.md" @@ -0,0 +1,294 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 797.所有可能的路径 + +[力扣题目链接](https://leetcode.cn/problems/all-paths-from-source-to-target/) + +给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序) + +graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。 + +![](https://file1.kamacoder.com/i/algo/20221203135439.png) + +提示: + +* n == graph.length +* 2 <= n <= 15 +* 0 <= graph[i][j] < n +* graph[i][j] != i(即不存在自环) +* graph[i] 中的所有元素 互不相同 +* 保证输入为 有向无环图(DAG) + +## 思路 + +这道题目是深度优先搜索,比较好的入门题。 + +如果对深度优先搜索还不够了解,可以先看这里:[深度优先搜索的理论基础](https://programmercarl.com/图论深搜理论基础.html) + +我依然总结了深搜三部曲,如果按照代码随想录刷题的录友,应该刷过 二叉树的递归三部曲,回溯三部曲。 + +**大家可能有疑惑,深搜 和 二叉树和回溯算法 有什么区别呢**? 什么时候用深搜 什么时候用回溯? + +我在讲解[二叉树理论基础](https://programmercarl.com/二叉树理论基础.html)的时候,提到过,**二叉树的前中后序遍历其实就是深搜在二叉树这种数据结构上的应用**。 + +那么回溯算法呢,**其实 回溯算法就是 深搜,只不过 我们给他一个更细分的定义,叫做回溯算法**。 + +那有的录友可能说:那我以后称回溯算法为深搜,是不是没毛病? + +理论上来说,没毛病,但 就像是 二叉树 你不叫它二叉树,叫它数据结构,有问题不? 也没问题对吧。 + +建议是 有细分的场景,还是称其细分场景的名称。 所以回溯算法可以独立出来,但回溯确实就是深搜。 + +接下来我们使用深搜三部曲来分析题目: + +1. 确认递归函数,参数 + +首先我们dfs函数一定要存一个图,用来遍历的,还要存一个目前我们遍历的节点,定义为x + +至于 单一路径,和路径集合可以放在全局变量,那么代码是这样的: + +```CPP +vector> result; // 收集符合条件的路径 +vector path; // 0节点到终点的路径 +// x:目前遍历的节点 +// graph:存当前的图 +void dfs (vector>& graph, int x) +``` + +2. 确认终止条件 + +什么时候我们就找到一条路径了? + +当目前遍历的节点 为 最后一个节点的时候,就找到了一条,从 出发点到终止点的路径。 + +当前遍历的节点,我们定义为x,最后一点节点,就是 graph.size() - 1(因为题目描述是找出所有从节点 0 到节点 n-1 的路径并输出)。 + +所以 但 x 等于 graph.size() - 1 的时候就找到一条有效路径。 代码如下: + + +```CPP +// 要求从节点 0 到节点 n-1 的路径并输出,所以是 graph.size() - 1 +if (x == graph.size() - 1) { // 找到符合条件的一条路径 + result.push_back(path); // 收集有效路径 + return; +} +``` + +3. 处理目前搜索节点出发的路径 + +接下来是走 当前遍历节点x的下一个节点。 + +首先是要找到 x节点链接了哪些节点呢? 遍历方式是这样的: + +```c++ +for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点 +``` + +接下来就是将 选中的x所连接的节点,加入到 单一路径来。 + +```C++ +path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来 + +``` + +一些录友可以疑惑这里如果找到x 链接的节点的,例如如果x目前是节点0,那么目前的过程就是这样的: + +![](https://file1.kamacoder.com/i/algo/20221204111937.png) + +二维数组中,graph[x][i] 都是x链接的节点,当前遍历的节点就是 `graph[x][i]` 。 + +进入下一层递归 + +```CPP +dfs(graph, graph[x][i]); // 进入下一层递归 +``` + +最后就是回溯的过程,撤销本次添加节点的操作。 该过程整体代码: + +```CPP +for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点 + path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来 + dfs(graph, graph[x][i]); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 +} +``` + +本题整体代码如下: + +```CPP +class Solution { +private: + vector> result; // 收集符合条件的路径 + vector path; // 0节点到终点的路径 + // x:目前遍历的节点 + // graph:存当前的图 + void dfs (vector>& graph, int x) { + // 要求从节点 0 到节点 n-1 的路径并输出,所以是 graph.size() - 1 + if (x == graph.size() - 1) { // 找到符合条件的一条路径 + result.push_back(path); + return; + } + for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点 + path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来 + dfs(graph, graph[x][i]); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 + } + } +public: + vector> allPathsSourceTarget(vector>& graph) { + path.push_back(0); // 无论什么路径已经是从0节点出发 + dfs(graph, 0); // 开始遍历 + return result; + } +}; + +``` + +## 总结 + +本题是比较基础的深度优先搜索模板题,这种有向图路径问题,最合适使用深搜,当然本题也可以使用广搜,但广搜相对来说就麻烦了一些,需要记录一下路径。 + +而深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+标记,所以使用哪种遍历都是可以的。 + +至于广搜理论基础,我们在下一篇在好好讲解,敬请期待! + +## 其他语言版本 + +### Java + +```Java +// 深度优先遍历 +class Solution { + List> ans; // 用来存放满足条件的路径 + List cnt; // 用来保存 dfs 过程中的节点值 + + public void dfs(int[][] graph, int node) { + if (node == graph.length - 1) { // 如果当前节点是 n - 1,那么就保存这条路径 + ans.add(new ArrayList<>(cnt)); + return; + } + for (int index = 0; index < graph[node].length; index++) { + int nextNode = graph[node][index]; + cnt.add(nextNode); + dfs(graph, nextNode); + cnt.remove(cnt.size() - 1); // 回溯 + } + } + + public List> allPathsSourceTarget(int[][] graph) { + ans = new ArrayList<>(); + cnt = new ArrayList<>(); + cnt.add(0); // 注意,0 号节点要加入 cnt 数组中 + dfs(graph, 0); + return ans; + } +} +``` + +### Python + +```python +class Solution: + def __init__(self): + self.result = [] + self.path = [0] + + def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]: + if not graph: return [] + + self.dfs(graph, 0) + return self.result + + def dfs(self, graph, root: int): + if root == len(graph) - 1: # 成功找到一条路径时 + # ***Python的list是mutable类型*** + # ***回溯中必须使用Deep Copy*** + self.result.append(self.path[:]) + return + + for node in graph[root]: # 遍历节点n的所有后序节点 + self.path.append(node) + self.dfs(graph, node) + self.path.pop() # 回溯 +``` + + +### JavaScript +```javascript +var allPathsSourceTarget = function(graph) { + let res=[],path=[] + + function dfs(graph,start){ + if(start===graph.length-1){ + res.push([...path]) + return; + } + for(let i=0;i>) -> Vec> { + let (mut res, mut path) = (vec![], vec![0]); + Self::dfs(&graph, &mut path, &mut res, 0); + res + } + + pub fn dfs(graph: &Vec>, path: &mut Vec, res: &mut Vec>, node: usize) { + if node == graph.len() - 1 { + res.push(path.clone()); + return; + } + for &v in &graph[node] { + path.push(v); + Self::dfs(graph, path, res, v as usize); + path.pop(); + } + } +} +``` + diff --git "a/problems/0827.\346\234\200\345\244\247\344\272\272\345\267\245\345\262\233.md" "b/problems/0827.\346\234\200\345\244\247\344\272\272\345\267\245\345\262\233.md" new file mode 100755 index 0000000000..118735e943 --- /dev/null +++ "b/problems/0827.\346\234\200\345\244\247\344\272\272\345\267\245\345\262\233.md" @@ -0,0 +1,504 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 827.最大人工岛 + +[力扣链接](https://leetcode.cn/problems/making-a-large-island/) + +给你一个大小为 n x n 二进制矩阵 grid 。最多 只能将一格 0 变成 1 。 + +返回执行此操作后,grid 中最大的岛屿面积是多少? + +岛屿 由一组上、下、左、右四个方向相连的 1 形成。 + +示例 1: +* 输入: grid = [[1, 0], [0, 1]] +* 输出: 3 +* 解释: 将一格0变成1,最终连通两个小岛得到面积为 3 的岛屿。 + +示例 2: +* 输入: grid = [[1, 1], [1, 0]] +* 输出: 4 +* 解释: 将一格0变成1,岛屿的面积扩大为 4。 + +示例 3: +* 输入: grid = [[1, 1], [1, 1]] +* 输出: 4 +* 解释: 没有0可以让我们变成1,面积依然为 4。 + +## 思路 + +本题的一个暴力想法,应该是遍历地图尝试 将每一个 0 改成1,然后去搜索地图中的最大的岛屿面积。 + +计算地图的最大面积:遍历地图 + 深搜岛屿,时间复杂度为 n * n。 + +(其实使用深搜还是广搜都是可以的,其目的就是遍历岛屿做一个标记,相当于染色,那么使用哪个遍历方式都行,以下我用深搜来讲解) + +每改变一个0的方格,都需要重新计算一个地图的最大面积,所以 整体时间复杂度为:n^4。 + +如果对深度优先搜索不了解的录友,可以看这里:[深度优先搜索精讲](https://programmercarl.com/kamacoder/图论深搜理论基础.html) + + +## 优化思路 + +其实每次深搜遍历计算最大岛屿面积,我们都做了很多重复的工作。 + +只要用一次深搜把每个岛屿的面积记录下来就好。 + +第一步:一次遍历地图,得出各个岛屿的面积,并做编号记录。可以使用map记录,key为岛屿编号,value为岛屿面积 +第二步:在遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积。 + +拿如下地图的岛屿情况来举例: (1为陆地) + +![](https://file1.kamacoder.com/i/algo/20220829104834.png) + +第一步,则遍历题目,并将岛屿到编号和面积上的统计,过程如图所示: + +![](https://file1.kamacoder.com/i/algo/20220829105644.png) + + +本过程代码如下: + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y, int mark) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty, mark); + } +} + +int largestIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); // 标记访问过的点 + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + dfs(grid, visited, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } +} +``` + + +这个过程时间复杂度 n * n 。可能有录友想:分明是两个for循环下面套这一个dfs,时间复杂度怎么回事 n * n呢? + +其实大家可以仔细看一下代码,**n * n这个方格地图中,每个节点我们就遍历一次,并不会重复遍历**。 + +第二步过程如图所示: + +![](https://file1.kamacoder.com/i/algo/20220829105249.png) + +也就是遍历每一个0的方格,并统计其相邻岛屿面积,最后取一个最大值。 + +这个过程的时间复杂度也为 n * n。 + +所以整个解法的时间复杂度,为 n * n + n * n 也就是 n^2。 + +当然这里还有一个优化的点,就是 可以不用 visited数组,因为有mark来标记,所以遍历过的grid[i][j]是不等于1的。 + +代码如下: + +```CPP + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void dfs(vector>& grid, int x, int y, int mark) { + if (grid[x][y] != 1 || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, nextx, nexty, mark); + } + } + +public: + int largestIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (grid[i][j] == 1) { + count = 0; + dfs(grid, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } + } +} +``` + +不过为了让各个变量各司其事,代码清晰一些,完整代码还是使用visited数组来标记。 + +最后,整体代码如下: + +```CPP +class Solution { +private: + int count; + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void dfs(vector>& grid, vector>& visited, int x, int y, int mark) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty, mark); + } + } + +public: + int largestIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); // 标记访问过的点 + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + dfs(grid, visited, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } + if (isAllGrid) return n * m; // 如果都是陆地,返回全面积 + + // 以下逻辑是根据添加陆地的位置,计算周边岛屿面积之和 + int result = 0; // 记录最后结果 + unordered_set visitedGrid; // 标记访问过的岛屿 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + int count = 1; // 记录连接之后的岛屿数量 + visitedGrid.clear(); // 每次使用时,清空 + if (grid[i][j] == 0) { + for (int k = 0; k < 4; k++) { + int neari = i + dir[k][1]; // 计算相邻坐标 + int nearj = j + dir[k][0]; + if (neari < 0 || neari >= grid.size() || nearj < 0 || nearj >= grid[0].size()) continue; + if (visitedGrid.count(grid[neari][nearj])) continue; // 添加过的岛屿不要重复添加 + // 把相邻四面的岛屿数量加起来 + count += gridNum[grid[neari][nearj]]; + visitedGrid.insert(grid[neari][nearj]); // 标记该岛屿已经添加过 + } + } + result = max(result, count); + } + } + return result; + } +}; +``` + +## 其他语言版本 + +### Java + +```Java +class Solution { + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; // 四个方向 + + /** + * @param grid 矩阵数组 + * @param row 当前遍历的节点的行号 + * @param col 当前遍历的节点的列号 + * @param mark 当前区域的标记 + * @return 返回当前区域内 1 的数量 + */ + public int dfs(int[][] grid, int row, int col, int mark) { + int ans = 0; + grid[row][col] = mark; + for (int[] current: position) { + int curRow = row + current[0], curCol = col + current[1]; + if (curRow < 0 || curRow >= grid.length || curCol < 0 || curCol >= grid.length) continue; // 越界 + if (grid[curRow][curCol] == 1) + ans += 1 + dfs(grid, curRow, curCol, mark); + } + return ans; + } + + public int largestIsland(int[][] grid) { + int ans = Integer.MIN_VALUE, size = grid.length, mark = 2; + Map getSize = new HashMap<>(); + for (int row = 0; row < size; row++) { + for (int col = 0; col < size; col++) { + if (grid[row][col] == 1) { + int areaSize = 1 + dfs(grid, row, col, mark); + getSize.put(mark++, areaSize); + } + } + } + for (int row = 0; row < size; row++) { + for (int col = 0; col < size; col++) { + // 当前位置如果不是 0 那么直接跳过,因为我们只能把 0 变成 1 + if (grid[row][col] != 0) continue; + Set hashSet = new HashSet<>(); // 防止同一个区域被重复计算 + // 计算从当前位置开始获取的 1 的数量,初始化 1 是因为把当前位置的 0 转换成了 1 + int curSize = 1; + for (int[] current: position) { + int curRow = row + current[0], curCol = col + current[1]; + if (curRow < 0 || curRow >= grid.length || curCol < 0 || curCol >= grid.length) continue; + int curMark = grid[curRow][curCol]; // 获取对应位置的标记 + // 如果标记存在 hashSet 中说明该标记被记录过一次,如果不存在 getSize 中说明该标记是无效标记(此时 curMark = 0) + if (hashSet.contains(curMark) || !getSize.containsKey(curMark)) continue; + hashSet.add(curMark); + curSize += getSize.get(curMark); + } + ans = Math.max(ans, curSize); + } + } + // 当 ans == Integer.MIN_VALUE 说明矩阵数组中不存在 0,全都是有效区域,返回数组大小即可 + return ans == Integer.MIN_VALUE ? size * size : ans; + } +} +``` + +### Python + +```python + +class Solution: + def largestIsland(self, grid: List[List[int]]) -> int: + visited = set() #标记访问过的位置 + m, n = len(grid), len(grid[0]) + res = 0 + island_size = 0 #用于保存当前岛屿的尺寸 + directions = [[0, 1], [0, -1], [1, 0], [-1, 0]] #四个方向 + islands_size = defaultdict(int) #保存每个岛屿的尺寸 + + def dfs(island_num, r, c): + visited.add((r, c)) + grid[r][c] = island_num #访问过的位置标记为岛屿编号 + nonlocal island_size + island_size += 1 + for i in range(4): + nextR = r + directions[i][0] + nextC = c + directions[i][1] + if (nextR not in range(m) or #行坐标越界 + nextC not in range(n) or #列坐标越界 + (nextR, nextC) in visited): #坐标已访问 + continue + if grid[nextR][nextC] == 1: #遇到有效坐标,进入下一个层搜索 + dfs(island_num, nextR, nextC) + + island_num = 2 #初始岛屿编号设为2, 因为grid里的数据有0和1, 所以从2开始编号 + all_land = True #标记是否整个地图都是陆地 + for r in range(m): + for c in range(n): + if grid[r][c] == 0: + all_land = False #地图里不全是陆地 + if (r, c) not in visited and grid[r][c] == 1: + island_size = 0 #遍历每个位置前重置岛屿尺寸为0 + dfs(island_num, r, c) + islands_size[island_num] = island_size #保存当前岛屿尺寸 + island_num += 1 #下一个岛屿编号加一 + if all_land: + return m * n #如果全是陆地, 返回地图面积 + + count = 0 #某个位置0变成1后当前岛屿尺寸 + #因为后续计算岛屿面积要往四个方向遍历,但某2个或3个方向的位置可能同属于一个岛, + #所以为避免重复累加,把已经访问过的岛屿编号加入到这个集合 + visited_island = set() #保存访问过的岛屿 + for r in range(m): + for c in range(n): + if grid[r][c] == 0: + count = 1 #把由0转换为1的位置计算到面积里 + visited_island.clear() #遍历每个位置前清空集合 + for i in range(4): + nearR = r + directions[i][0] + nearC = c + directions[i][1] + if nearR not in range(m) or nearC not in range(n): #周围位置越界 + continue + if grid[nearR][nearC] in visited_island: #岛屿已访问 + continue + count += islands_size[grid[nearR][nearC]] #累加连在一起的岛屿面积 + visited_island.add(grid[nearR][nearC]) #标记当前岛屿已访问 + res = max(res, count) + return res + + +``` + + +### Go + +```go +func largestIsland(grid [][]int) int { + dir := [][]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} + n := len(grid) + m := len(grid[0]) + area := 0 + visited := make([][]bool, n) + for i := 0; i < n; i++ { + visited[i] = make([]bool, m) + } + gridNum := make(map[int]int, 0) // 记录每一个岛屿的面积 + mark := 2 // 记录每个岛屿的编号 + isAllGrid := true + res := 0 // 标记是否整个地图都是陆地 + + var dfs func(grid [][]int, visited [][]bool, x, y, mark int) + dfs = func(grid [][]int, visited [][]bool, x, y, mark int) { + // 终止条件:访问过的节点 或者 遇到海水 + if visited[x][y] || grid[x][y] == 0 { + return + } + visited[x][y] = true // 标记访问过 + grid[x][y] = mark // 给陆地标记新标签 + area++ + for i := 0; i < 4; i++ { + nextX := x + dir[i][0] + nextY := y + dir[i][1] + if nextX < 0 || nextX >= len(grid) || nextY < 0 || nextY >= len(grid[0]) { + continue + } + dfs(grid, visited, nextX, nextY, mark) + } + } + + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if grid[i][j] == 0 { + isAllGrid = false + } + if !visited[i][j] && grid[i][j] == 1 { + area = 0 + dfs(grid, visited, i, j, mark) // 将与其链接的陆地都标记上 true + gridNum[mark] = area // 记录每一个岛屿的面积 + mark++ // 更新下一个岛屿编号 + } + } + } + if isAllGrid { + return n * m + } + // 根据添加陆地的位置,计算周边岛屿面积之和 + visitedGrid := make(map[int]struct{}) // 标记访问过的岛屿 + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + count := 1 // 记录连接之后的岛屿数量 + visitedGrid = make(map[int]struct{}) // 每次使用时,清空 + if grid[i][j] == 0 { + for k := 0; k < 4; k++ { + // 计算相邻坐标 + nearI := i + dir[k][0] + nearJ := j + dir[k][1] + if nearI < 0 || nearI >= len(grid) || nearJ < 0 || nearJ >= len(grid[0]) { + continue + } + // 添加过的岛屿不要重复添加 + if _, ok := visitedGrid[grid[nearI][nearJ]]; ok { + continue + } + // 把相邻四面的岛屿数量加起来 + count += gridNum[grid[nearI][nearJ]] + // 标记该岛屿已经添加过 + visitedGrid[grid[nearI][nearJ]] = struct{}{} + } + } + res = max827(res, count) + } + } + return res +} + +func max827(x, y int) int { + if x > y { + return x + } + return y +} + +``` + + +### JavaScript + +```JavaScript + +var largestIsland = function(grid) { +let res = 0; +const m = grid.length; +const n = grid[0].length; +const tag = new Array(n).fill().map(_ => new Array(m).fill(0)); +const area = new Map(); +const dir = [[0,1],[0,-1],[1,0],[-1,0]]; +const dfs = (grid,tag,x,y,mark) => { + let res = 1; + tag[x][y] = mark; + for(let i = 0; i < dir.length; i++) { + let nextX = x + dir[i][0]; + let nextY = y + dir[i][1]; + if(nextX < 0 || nextX >= m || nextY < 0 || nextY >= n) { + continue; + } + if(grid[nextX][nextY] === 1 && tag[nextX][nextY] === 0) { + res += dfs(grid,tag,nextX,nextY,mark); + } + } + return res; +} +let mark = 2; +//将岛屿用mark标记 +for(let i = 0; i < m; i++) { + for(let j = 0; j < n; j++) { + if(grid[i][j] === 1 && tag[i][j] === 0) { + area.set(mark,dfs(grid,tag,i,j,mark)); + res = Math.max(res,area.get(mark)); + mark++; + } + } +} +//将一个非岛屿格子变为岛屿 +for(let i = 0; i < m; i++) { + for(let j = 0; j < n; j++) { + if(grid[i][j] === 0) { + let z = 1; + const connected = new Set(); + for(let k = 0; k < dir.length; k++) { + let nextX = i + dir[k][0]; + let nextY = j + dir[k][1]; + if(nextX < 0 || nextX >= m || nextY < 0 || nextY >= n || tag[nextX][nextY] === 0 || connected.has(tag[nextX][nextY])) { + continue; + } + z += area.get(tag[nextX][nextY]); + connected.add(tag[nextX][nextY]); + } + res = Math.max(res,z); + } + } +} +return res; +}; + + +``` + + diff --git "a/problems/0841.\351\222\245\345\214\231\345\222\214\346\210\277\351\227\264.md" "b/problems/0841.\351\222\245\345\214\231\345\222\214\346\210\277\351\227\264.md" new file mode 100755 index 0000000000..ffcf2fb919 --- /dev/null +++ "b/problems/0841.\351\222\245\345\214\231\345\222\214\346\210\277\351\227\264.md" @@ -0,0 +1,484 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 841.钥匙和房间 + +[力扣题目链接](https://leetcode.cn/problems/keys-and-rooms/) + +有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,...,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。 + +在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i][j] 由 [0,1,...,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms[i][j] = v 可以打开编号为 v 的房间。 + +最初,除 0 号房间外的其余所有房间都被锁住。 + +你可以自由地在房间之间来回走动。 + +如果能进入每个房间返回 true,否则返回 false。 + +示例 1: +* 输入: [[1],[2],[3],[]] +* 输出: true +* 解释: 我们从 0 号房间开始,拿到钥匙 1。 之后我们去 1 号房间,拿到钥匙 2。 然后我们去 2 号房间,拿到钥匙 3。 最后我们去了 3 号房间。 由于我们能够进入每个房间,我们返回 true。 + +示例 2: +* 输入:[[1,3],[3,0,1],[2],[0]] +* 输出:false +* 解释:我们不能进入 2 号房间。 + + +## 思路 + +本题其实给我们是一个有向图, 意识到这是有向图很重要! + +图中给我的两个示例: `[[1],[2],[3],[]]` `[[1,3],[3,0,1],[2],[0]]`,画成对应的图如下: + +![](https://file1.kamacoder.com/i/algo/20220714101414.png) + +我们可以看出图1的所有节点都是链接的,而图二中,节点2 是孤立的。 + +这就很容易让我们想起岛屿问题,只要发现独立的岛,就是不能进入所有房间。 + +此时也容易想到用并查集的方式去解决。 + +**但本题是有向图**,在有向图中,即使所有节点都是链接的,但依然不可能从0出发遍历所有边。 +给大家举一个例子: + +图3:[[5], [], [1, 3], [5]] ,如图: + +![](https://file1.kamacoder.com/i/algo/20220714102201.png) + +在图3中,大家可以发现,节点0只能到节点5,然后就哪也去不了了。 + +所以本题是一个有向图搜索全路径的问题。 只能用深搜(DFS)或者广搜(BFS)来搜。 + +关于DFS的理论,如果大家有困惑,可以先看我这篇题解: [DFS理论基础](https://programmercarl.com/图论深搜理论基础.html) + +**以下dfs分析 大家一定要仔细看,本题有两种dfs的解法,很多题解没有讲清楚**。 看完之后 相信你对dfs会有更深的理解。 + +深搜三部曲: + +1. 确认递归函数,参数 + +需要传入二维数组rooms来遍历地图,需要知道当前我们拿到的key,以至于去下一个房间。 + +同时还需要一个数组,用来记录我们都走过了哪些房间,这样好知道最后有没有把所有房间都遍历的,可以定义一个一维数组。 + +所以 递归函数参数如下: + +```C++ +// key 当前得到的可以 +// visited 记录访问过的房间 +void dfs(const vector>& rooms, int key, vector& visited) { +``` + +2. 确认终止条件 + +遍历的时候,什么时候终止呢? + +这里有一个很重要的逻辑,就是在递归中,**我们是处理当前访问的节点,还是处理下一个要访问的节点**。 + +这决定 终止条件怎么写。 + +首先明确,本题中什么叫做处理,就是 visited数组来记录访问过的节点,该节点默认 数组里元素都是false,把元素标记为true就是处理 本节点了。 + +如果我们是处理当前访问的节点,当前访问的节点如果是 true ,说明是访问过的节点,那就终止本层递归,如果不是true,我们就把它赋值为true,因为这是我们处理本层递归的节点。 + +代码就是这样: + +```C++ +// 写法一:处理当前访问的节点 +void dfs(const vector>& rooms, int key, vector& visited) { + if (visited[key]) { // 本层递归是true,说明访问过,立刻返回 + return; + } + visited[key] = true; // 给当前遍历的节点赋值true + vector keys = rooms[key]; + for (int key : keys) { + // 深度优先搜索遍历 + dfs(rooms, key, visited); + } +} +``` + +如果我们是处理下一层访问的节点,而不是当前层。那么就要在 深搜三部曲中第三步:处理目前搜索节点出发的路径的时候对 节点进行处理。 + +这样的话,就不需要终止条件,而是在 搜索下一个节点的时候,直接判断 下一个节点是否是我们要搜的节点。 + +代码就是这样的: + +```C++ +// 写法二:处理下一个要访问的节点 +void dfs(const vector>& rooms, int key, vector& visited) { + // 这里 没有终止条件,而是在 处理下一层节点的时候来判断 + vector keys = rooms[key]; + for (int key : keys) { + if (visited[key] == false) { // 处理下一层节点,判断是否要进行递归 + visited[key] = true; + dfs(rooms, key, visited); + } + } +} +``` + +可以看出,如果看待 我们要访问的节点,直接决定了两种不一样的写法,很多录友对这一块很模糊,可能做过这道题,但没有思考到这个维度上。 + + +3. 处理目前搜索节点出发的路径 + +其实在上面,深搜三部曲 第二部,就已经讲了,因为终止条件的两种写法, 直接决定了两种不一样的递归写法。 + +这里还有细节: + +看上面两个版本的写法中, 好像没有发现回溯的逻辑。 + +我们都知道,有递归就有回溯,回溯就在递归函数的下面, 那么之前我们做的dfs题目,都需要回溯操作,例如:[797.所有可能的路径](https://programmercarl.com/0797.%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E8%B7%AF%E5%BE%84.html), **为什么本题就没有回溯呢?** + +代码中可以看到dfs函数下面并没有回溯的操作。 + +此时就要在思考本题的要求了,本题是需要判断 0节点是否能到所有节点,那么我们就没有必要回溯去撤销操作了,只要遍历过的节点一律都标记上。 + +**那什么时候需要回溯操作呢?** + +当我们需要搜索一条可行路径的时候,就需要回溯操作了,因为没有回溯,就没法“调头”, 如果不理解的话,去看我写的 [797.所有可能的路径](https://programmercarl.com/0797.%E6%89%80%E6%9C%89%E5%8F%AF%E8%83%BD%E7%9A%84%E8%B7%AF%E5%BE%84.html) 的题解。 + + +以上分析完毕,DFS整体实现C++代码如下: + +```CPP +// 写法一:处理当前访问的节点 +class Solution { +private: + void dfs(const vector>& rooms, int key, vector& visited) { + if (visited[key]) { + return; + } + visited[key] = true; + vector keys = rooms[key]; + for (int key : keys) { + // 深度优先搜索遍历 + dfs(rooms, key, visited); + } + } +public: + bool canVisitAllRooms(vector>& rooms) { + vector visited(rooms.size(), false); + dfs(rooms, 0, visited); + //检查是否都访问到了 + for (int i : visited) { + if (i == false) return false; + } + return true; + } +}; + +``` + +```c++ +写法二:处理下一个要访问的节点 +class Solution { +private: + void dfs(const vector>& rooms, int key, vector& visited) { + vector keys = rooms[key]; + for (int key : keys) { + if (visited[key] == false) { + visited[key] = true; + dfs(rooms, key, visited); + } + } + } +public: + bool canVisitAllRooms(vector>& rooms) { + vector visited(rooms.size(), false); + visited[0] = true; // 0 节点是出发节点,一定被访问过 + dfs(rooms, 0, visited); + //检查是否都访问到了 + for (int i : visited) { + if (i == false) return false; + } + return true; + } +}; + +``` + +本题我也给出 BFS C++代码,[BFS理论基础](https://programmercarl.com/%E5%9B%BE%E8%AE%BA%E6%B7%B1%E6%90%9C%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html),代码如下: + +```CPP +class Solution { +bool bfs(const vector>& rooms) { + vector visited(rooms.size(), 0); // 标记房间是否被访问过 + visited[0] = 1; // 0 号房间开始 + queue que; + que.push(0); // 0 号房间开始 + + // 广度优先搜索的过程 + while (!que.empty()) { + int key = que.front(); que.pop(); + vector keys = rooms[key]; + for (int key : keys) { + if (!visited[key]) { + que.push(key); + visited[key] = 1; + } + } + } + // 检查房间是不是都遍历过了 + for (int i : visited) { + if (i == 0) return false; + } + return true; + +} +public: + bool canVisitAllRooms(vector>& rooms) { + return bfs(rooms); + } +}; +``` + +## 其他语言版本 + +### Java + +```java +class Solution { + private void dfs(int key, List> rooms, List visited) { + if (visited.get(key)) { + return; + } + visited.set(key, true); + for (int k : rooms.get(key)) { + // 深度优先搜索遍历 + dfs(k, rooms, visited); + } + } + public boolean canVisitAllRooms(List> rooms) { + List visited = new ArrayList(){{ + for(int i = 0 ; i < rooms.size(); i++){ + add(false); + } + }}; + dfs(0, rooms, visited); + //检查是否都访问到了 + for (boolean flag : visited) { + if (!flag) { + return false; + } + } + return true; + } +} +``` + +```Java +// 广度优先搜索 +class Solution { + public boolean canVisitAllRooms(List> rooms) { + boolean[] visited = new boolean[rooms.size()]; // 用一个 visited 数据记录房间是否被访问 + visited[0] = true; + Queue queue = new ArrayDeque<>(); + queue.add(0); // 第 0 个房间标记为已访问 + while (!queue.isEmpty()) { + int curKey = queue.poll(); + for (int key: rooms.get(curKey)) { + if (visited[key]) continue; + visited[key] = true; + queue.add(key); + } + } + for (boolean key: visited) + if (!key) return false; + return true; + } +} +``` + +```java +// 广度优先遍历(时间优化) +class Solution { + public boolean canVisitAllRooms(List> rooms) { + int count = 1; // 用来记录可以被访问的房间数目,因为初始状态下 0 号房间可以被访问,所以置为 1 + boolean[] visited = new boolean[rooms.size()]; // 用一个 visited 数据记录房间是否被访问 + visited[0] = true; // 第 0 个房间标记为已访问 + Queue queue = new ArrayDeque<>(); + queue.add(0); + while (!queue.isEmpty()) { + int curKey = queue.poll(); + for (int key: rooms.get(curKey)) { + if (visited[key]) continue; + ++count; // 每访问一个访问房间就让 count 加 1 + visited[key] = true; + queue.add(key); + } + } + return count == rooms.size(); // 如果 count 等于房间数目,表示能进入所有房间,反之不能 + } +} +``` + +### python3 + +```python +# 深度搜索优先 +class Solution: + def dfs(self, key: int, rooms: List[List[int]] , visited : List[bool] ) : + if visited[key] : + return + visited[key] = True + keys = rooms[key] + for i in range(len(keys)) : + # 深度优先搜索遍历 + self.dfs(keys[i], rooms, visited) + + def canVisitAllRooms(self, rooms: List[List[int]]) -> bool: + visited = [False for i in range(len(rooms))] + + self.dfs(0, rooms, visited) + + # 检查是否都访问到了 + for i in range(len(visited)): + if not visited[i] : + return False + return True + +# 广度搜索优先 +class Solution: + def canVisitAllRooms(self, rooms: List[List[int]]) -> bool: + visited = [False] * len(rooms) + self.bfs(rooms, 0, visited) + + for room in visited: + if room == False: + return False + + return True + + def bfs(self, rooms, index, visited): + q = collections.deque() + q.append(index) + + visited[0] = True + + while len(q) != 0: + index = q.popleft() + for nextIndex in rooms[index]: + if visited[nextIndex] == False: + q.append(nextIndex) + visited[nextIndex] = True + +``` + + +### Go + +```go + +func dfs(key int, rooms [][]int, visited []bool ) { + if visited[key] { + return; + } + + visited[key] = true + keys := rooms[key] + for _ , key := range keys { + // 深度优先搜索遍历 + dfs(key, rooms, visited); + } +} + +func canVisitAllRooms(rooms [][]int) bool { + visited := make([]bool, len(rooms)); + dfs(0, rooms, visited); + //检查是否都访问到了 + for i := 0; i < len(visited); i++ { + if !visited[i] { + return false; + } + } + return true; +} +``` + +### JavaScript +```javascript +//DFS +var canVisitAllRooms = function(rooms) { + const dfs = (key, rooms, visited) => { + if(visited[key]) return; + visited[key] = 1; + for(let k of rooms[key]){ + // 深度优先搜索遍历 + dfs(k, rooms, visited); + } + } + const visited = new Array(rooms.length).fill(false); + dfs(0, rooms, visited); + //检查是否都访问到了 + for (let i of visited) { + if (!i) { + return false; + } + } + return true; +}; + +//BFS +var canVisitAllRooms = function(rooms) { + const bfs = rooms => { + const visited = new Array(rooms.length).fill(0); // 标记房间是否被访问过 + visited[0] = 1; // 0 号房间开始 + const queue = []; //js数组作为队列使用 + queue.push(0); // 0 号房间开始 + // 广度优先搜索的过程 + while(queue.length !== 0){ + let key = queue[0]; + queue.shift(); + for(let k of rooms[key]){ + if(!visited[k]){ + queue.push(k); + visited[k] = 1; + } + } + } + // 检查房间是不是都遍历过了 + for(let i of visited){ + if(i === 0) return false; + } + return true; + } + return bfs(rooms); +}; +``` + + +### TypeScript +```ts +// BFS +// rooms :就是一个链接表 表示的有向图 +// 转换问题就是,一次遍历从 0开始 能不能 把所有的节点访问了,实质问题就是一个 +// 层序遍历 +function canVisitAllRooms(rooms: number[][]): boolean { + const n = rooms.length; + // cnt[i] 代表节点 i 的访问顺序, cnt[i] = 0, 代表没被访问过 + let cnt = new Array(n).fill(0); + let queue = [0]; + cnt[0]++; + while (queue.length > 0) { + const from = queue.shift(); + for (let i = 0; i < rooms[from].length; i++) { + const to = rooms[from][i]; + if (cnt[to] == 0) { + queue.push(to); + cnt[to]++; + } + } + } + // 只要其中有一个节点 没被访问过,那么就返回 false + return cnt.every((item) => item != 0); +} +``` + + diff --git "a/problems/0844.\346\257\224\350\276\203\345\220\253\351\200\200\346\240\274\347\232\204\345\255\227\347\254\246\344\270\262.md" "b/problems/0844.\346\257\224\350\276\203\345\220\253\351\200\200\346\240\274\347\232\204\345\255\227\347\254\246\344\270\262.md" new file mode 100755 index 0000000000..6d0cd68578 --- /dev/null +++ "b/problems/0844.\346\257\224\350\276\203\345\220\253\351\200\200\346\240\274\347\232\204\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,588 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 844.比较含退格的字符串 + +[力扣题目链接](https://leetcode.cn/problems/backspace-string-compare/) + +给定 S 和 T 两个字符串,当它们分别被输入到空白的文本编辑器后,判断二者是否相等,并返回结果。 # 代表退格字符。 + +注意:如果对空文本输入退格字符,文本继续为空。 + +示例 1: +* 输入:S = "ab#c", T = "ad#c" +* 输出:true +* 解释:S 和 T 都会变成 “ac”。 + +示例 2: +* 输入:S = "ab##", T = "c#d#" +* 输出:true +* 解释:S 和 T 都会变成 “”。 + +示例 3: +* 输入:S = "a##c", T = "#a#c" +* 输出:true +* 解释:S 和 T 都会变成 “c”。 + +示例 4: +* 输入:S = "a#c", T = "b" +* 输出:false +* 解释:S 会变成 “c”,但 T 仍然是 “b”。 + + +## 思路 + +本文将给出 空间复杂度O(n)的栈模拟方法 以及空间复杂度是O(1)的双指针方法。 + +### 普通方法(使用栈的思路) + +这道题目一看就是要使用栈的节奏,这种匹配(消除)问题也是栈的擅长所在,跟着一起刷题的同学应该知道,在[栈与队列:匹配问题都是栈的强项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html),我就已经提过了一次使用栈来做类似的事情了。 + +**那么本题,确实可以使用栈的思路,但是没有必要使用栈,因为最后比较的时候还要比较栈里的元素,有点麻烦**。 + +这里直接使用字符串string,来作为栈,末尾添加和弹出,string都有相应的接口,最后比较的时候,只要比较两个字符串就可以了,比比较栈里的元素方便一些。 + +代码如下: + +```CPP +class Solution { +public: + bool backspaceCompare(string S, string T) { + string s; // 当栈来用 + string t; // 当栈来用 + for (int i = 0; i < S.size(); i++) { + if (S[i] != '#') s += S[i]; + else if (!s.empty()) { + s.pop_back(); + + } + for (int i = 0; i < T.size(); i++) { + if (T[i] != '#') t += T[i]; + else if (!t.empty()) { + t.pop_back(); + } + } + if (s == t) return true; // 直接比较两个字符串是否相等,比用栈来比较方便多了 + return false; + } +}; +``` +* 时间复杂度:O(n + m),n为S的长度,m为T的长度 ,也可以理解是O(n)的时间复杂度 +* 空间复杂度:O(n + m) + +当然以上代码,大家可以发现有重复的逻辑处理S,处理T,可以把这块公共逻辑抽离出来,代码精简如下: + +```CPP +class Solution { +private: +string getString(const string& S) { + string s; + for (int i = 0; i < S.size(); i++) { + if (S[i] != '#') s += S[i]; + else if (!s.empty()) { + s.pop_back(); + } + } + return s; +} +public: + bool backspaceCompare(string S, string T) { + return getString(S) == getString(T); + } +}; +``` +性能依然是: + +* 时间复杂度:O(n + m) +* 空间复杂度:O(n + m) + +### 优化方法(从后向前双指针) + +当然还可以有使用 O(1) 的空间复杂度来解决该问题。 + +同时从后向前遍历S和T(i初始为S末尾,j初始为T末尾),记录#的数量,模拟消除的操作,如果#用完了,就开始比较S[i]和S[j]。 + +动画如下: + + + +如果S[i]和S[j]不相同返回false,如果有一个指针(i或者j)先走到的字符串头部位置,也返回false。 + +代码如下: + +```CPP +class Solution { +public: + bool backspaceCompare(string S, string T) { + int sSkipNum = 0; // 记录S的#数量 + int tSkipNum = 0; // 记录T的#数量 + int i = S.size() - 1; + int j = T.size() - 1; + while (1) { + while (i >= 0) { // 从后向前,消除S的# + if (S[i] == '#') sSkipNum++; + else { + if (sSkipNum > 0) sSkipNum--; + else break; + } + i--; + } + while (j >= 0) { // 从后向前,消除T的# + if (T[j] == '#') tSkipNum++; + else { + if (tSkipNum > 0) tSkipNum--; + else break; + } + j--; + } + // 后半部分#消除完了,接下来比较S[i] != T[j] + if (i < 0 || j < 0) break; // S 或者T 遍历到头了 + if (S[i] != T[j]) return false; + i--;j--; + } + // 说明S和T同时遍历完毕 + if (i == -1 && j == -1) return true; + return false; + } +}; +``` + +* 时间复杂度:O(n + m) +* 空间复杂度:O(1) + + +## 其他语言版本 + +### Java + +```java +// 普通方法(使用栈的思路) +class Solution { + public boolean backspaceCompare(String s, String t) { + StringBuilder ssb = new StringBuilder(); // 模拟栈 + StringBuilder tsb = new StringBuilder(); // 模拟栈 + // 分别处理两个 String + for (char c : s.toCharArray()) { + if (c != '#') { + ssb.append(c); // 模拟入栈 + } else if (ssb.length() > 0){ // 栈非空才能弹栈 + ssb.deleteCharAt(ssb.length() - 1); // 模拟弹栈 + } + } + for (char c : t.toCharArray()) { + if (c != '#') { + tsb.append(c); // 模拟入栈 + } else if (tsb.length() > 0){ // 栈非空才能弹栈 + tsb.deleteCharAt(tsb.length() - 1); // 模拟弹栈 + } + } + return ssb.toString().equals(tsb.toString()); + } +} +``` + +双指针: + +```java +class Solution { +public static boolean backspaceCompare(String s, String t) { + char[] sarray = s.toCharArray(); + char[] tarray = t.toCharArray(); + return generate(sarray).equals(generate(tarray)); + } + public static String generate(char[] a){ + int slow = -1; + int fast = 0; + if(a.length == 1){ + return new String(a); + } else{ + for(fast = 0; fast < a.length; fast++){ + if(a[fast] != '#') + a[++slow] = a[fast]; + else{ + if(slow >= 0) + slow--; + } + } + return new String(a,0,slow + 1); + } + } +} +``` + +```java +class Solution { + public static boolean backspaceCompare(String s, String t) { + return getStr(s).equals(getStr(t)); + } + + public static String getStr(String s) { //使用快慢双指针去除字符串中的# + int slowIndex; + int fastIndex = 0; + StringBuilder builder = new StringBuilder(s); //StringBuilder用于修改字符串 + for(slowIndex = 0; fastIndex < s.length(); fastIndex++) { + if(builder.charAt(fastIndex) != '#') { + builder.setCharAt(slowIndex++,builder.charAt(fastIndex)); + } else { + if(slowIndex > 0) { + slowIndex--; + } + } + } + return builder.toString().substring(0,slowIndex); //截取有效字符串 + } +} +``` + +从后往前双指针: + +```java +class Solution { + public static boolean backspaceCompare(String s, String t) { + int sSkipNum = 0; //记录s的#的个数 + int tSkipNum = 0; //记录t的#的个数 + int sIndex = s.length() - 1; + int tIndex = t.length() - 1; + while(true) { + while(sIndex >= 0) { //每次记录连续的#并跳过被删除的字符 + if(s.charAt(sIndex) == '#') { + sSkipNum++; + } else { + if(sSkipNum > 0) { + sSkipNum--; + } else { + break; + } + } + sIndex--; + } + while(tIndex >= 0) { //每次记录连续的#并跳过被删除的字符 + if(t.charAt(tIndex) == '#') { + tSkipNum++; + } else { + if(tSkipNum > 0) { + tSkipNum--; + } else { + break; + } + } + tIndex--; + } + if(sIndex < 0 || tIndex < 0) { //s 或者 t遍历完了 + break; + } + if(s.charAt(sIndex) != t.charAt(tIndex)) { //当前下标的字符不相等 + return false; + } + sIndex--; + tIndex--; + } + if(sIndex == -1 && tIndex == -1) { //同时遍历完 则相等 + return true; + } + return false; + } +} +``` + +### Python + +```python +class Solution: + + def get_string(self, s: str) -> str : + bz = [] + for i in range(len(s)) : + c = s[i] + if c != '#' : + bz.append(c) # 模拟入栈 + elif len(bz) > 0: # 栈非空才能弹栈 + bz.pop() # 模拟弹栈 + return str(bz) + + def backspaceCompare(self, s: str, t: str) -> bool: + return self.get_string(s) == self.get_string(t) + pass +``` +双指针 +```python +class Solution: + def backspaceCompare(self, s: str, t: str) -> bool: + s_index, t_index = len(s) - 1, len(t) - 1 + s_backspace, t_backspace = 0, 0 # 记录s,t的#数量 + while s_index >= 0 or t_index >= 0: # 使用or,以防长度不一致 + while s_index >= 0: # 从后向前,消除s的# + if s[s_index] == '#': + s_index -= 1 + s_backspace += 1 + else: + if s_backspace > 0: + s_index -= 1 + s_backspace -= 1 + else: + break + while t_index >= 0: # 从后向前,消除t的# + if t[t_index] == '#': + t_index -= 1 + t_backspace += 1 + else: + if t_backspace > 0: + t_index -= 1 + t_backspace -= 1 + else: + break + if s_index >= 0 and t_index >= 0: # 后半部分#消除完了,接下来比较当前位的值 + if s[s_index] != t[t_index]: + return False + elif s_index >= 0 or t_index >= 0: # 一个字符串找到了待比较的字符,另一个没有,返回False + return False + s_index -= 1 + t_index -= 1 + return True +``` + +### Go + +```go + +func getString(s string) string { + bz := []rune{} + for _, c := range s { + if c != '#' { + bz = append(bz, c); // 模拟入栈 + } else if len(bz) > 0 { // 栈非空才能弹栈 + bz = bz[:len(bz)-1] // 模拟弹栈 + } + } + return string(bz) +} + +func backspaceCompare(s string, t string) bool { + return getString(s) == getString(t) +} + +``` +双指针 +```go +func backspaceCompare(s string, t string) bool { + s_index, t_index := len(s) - 1, len(t) - 1 + s_backspace, t_backspace := 0, 0 // 记录s,t的#数量 + for s_index >= 0 || t_index >= 0 { // 使用or,以防长度不一致 + for s_index >= 0 { // 从后向前,消除s的# + if s[s_index] == '#' { + s_index-- + s_backspace++ + } else { + if s_backspace > 0 { + s_index-- + s_backspace-- + } else { + break + } + } + } + for t_index >= 0 { // 从后向前,消除t的# + if t[t_index] == '#' { + t_index-- + t_backspace++ + } else { + if t_backspace > 0 { + t_index-- + t_backspace-- + } else { + break + } + } + } + if s_index >= 0 && t_index >= 0 { // 后半部分#消除完了,接下来比较当前位的值 + if s[s_index] != t[t_index] { + return false + } + } else if s_index >= 0 || t_index >= 0 { // 一个字符串找到了待比较的字符,另一个没有,返回false + return false + } + s_index-- + t_index-- + } + return true +} +``` + +### JavaScript +```javascript +// 双栈 +var backspaceCompare = function(s, t) { + const arrS = [], arrT = []; // 数组作为栈使用 + for(let char of s){ + char === '#' ? arrS.pop() : arrS.push(char); + } + for(let char of t){ + char === '#' ? arrT.pop() : arrT.push(char); + } + return arrS.join('') === arrT.join(''); // 比较两个字符串是否相等 +}; + +//双栈精简 +var backspaceCompare = function(s, t) { + const getString = s => { + let arrS = []; + for(let char of s){ + char === '#' ? arrS.pop() : arrS.push(char); + } + return arrS.join(''); + } + return getString(s) === getString(t); +}; + +//双指针 +var backspaceCompare = function(s, t) { + let sSkipNum = 0; // 记录s的#数量 + let tSkipNum = 0; // 记录t的#数量 + let i = s.length - 1, j = t.length - 1; + while(true) { + while(i >= 0){ // 从后向前,消除s的# + if(s[i] === '#') sSkipNum++; + else { + if (sSkipNum > 0) sSkipNum--; + else break; + } + i--; + } + while (j >= 0) { // 从后向前,消除t的# + if (t[j] === '#') tSkipNum++; + else { + if (tSkipNum > 0) tSkipNum--; + else break; + } + j--; + } + // 后半部分#消除完了,接下来比较s[i] != t[j] + if (i < 0 || j < 0) break; // s 或者t 遍历到头了 + if (s[i] !== t[j]) return false; + i--;j--; + } + // 说明s和t同时遍历完毕 + if (i == -1 && j == -1) return true; + return false; +}; + +``` + +### TypeScript + +> 双栈法: + +```typescript +function backspaceCompare(s: string, t: string): boolean { + const stack1: string[] = [], + stack2: string[] = []; + for (let c of s) { + if (c === '#') { + stack1.pop(); + } else { + stack1.push(c); + } + } + for (let c of t) { + if (c === '#') { + stack2.pop(); + } else { + stack2.push(c); + } + } + if (stack1.length !== stack2.length) return false; + for (let i = 0, length = stack1.length; i < length; i++) { + if (stack1[i] !== stack2[i]) return false; + } + return true; +}; +``` + +> 双指针法: + +```typescript +function backspaceCompare(s: string, t: string): boolean { + let sIndex: number = s.length - 1, + tIndex: number = t.length - 1; + while (true) { + sIndex = getIndexAfterDel(s, sIndex); + tIndex = getIndexAfterDel(t, tIndex); + if (sIndex < 0 || tIndex < 0) break; + if (s[sIndex] !== t[tIndex]) return false; + sIndex--; + tIndex--; + } + return sIndex === -1 && tIndex === -1; +}; +function getIndexAfterDel(s: string, startIndex: number): number { + let backspaceNum: number = 0; + while (startIndex >= 0) { + // 不可消除 + if (s[startIndex] !== '#' && backspaceNum === 0) break; + // 可消除 + if (s[startIndex] === '#') { + backspaceNum++; + } else { + backspaceNum--; + } + startIndex--; + } + return startIndex; +} +``` + +### Rust + +> 双指针 + +```rust +impl Solution { + pub fn backspace_compare(s: String, t: String) -> bool { + let (s, t) = ( + s.chars().collect::>(), + t.chars().collect::>(), + ); + Self::get_string(s) == Self::get_string(t) + } + + pub fn get_string(mut chars: Vec) -> Vec { + let mut slow = 0; + for i in 0..chars.len() { + if chars[i] == '#' { + slow = (slow as u32).saturating_sub(1) as usize; + } else { + chars[slow] = chars[i]; + slow += 1; + } + } + chars.truncate(slow); + chars + } +} +``` + +> 双栈法 + +```rust +impl Solution { + pub fn backspace_compare(s: String, t: String) -> bool { + Self::get_string(s) == Self::get_string(t) + } + + pub fn get_string(string: String) -> String { + let mut s = String::new(); + for c in string.chars() { + if c != '#' { + s.push(c); + } else if !s.is_empty() { + s.pop(); + } + } + s + } +} +``` + + diff --git "a/problems/0860.\346\237\240\346\252\254\346\260\264\346\211\276\351\233\266.md" "b/problems/0860.\346\237\240\346\252\254\346\260\264\346\211\276\351\233\266.md" new file mode 100755 index 0000000000..aeb470fe5a --- /dev/null +++ "b/problems/0860.\346\237\240\346\252\254\346\260\264\346\211\276\351\233\266.md" @@ -0,0 +1,440 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 860.柠檬水找零 + +[力扣题目链接](https://leetcode.cn/problems/lemonade-change/) + +在柠檬水摊上,每一杯柠檬水的售价为 5 美元。 + +顾客排队购买你的产品,(按账单 bills 支付的顺序)一次购买一杯。 + +每位顾客只买一杯柠檬水,然后向你付 5 美元、10 美元或 20 美元。你必须给每个顾客正确找零,也就是说净交易是每位顾客向你支付 5 美元。 + +注意,一开始你手头没有任何零钱。 + +如果你能给每位顾客正确找零,返回 true ,否则返回 false 。 + +示例 1: +* 输入:[5,5,5,10,20] +* 输出:true +* 解释: + * 前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 + * 第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 + * 第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 + * 由于所有客户都得到了正确的找零,所以我们输出 true。 + +示例 2: +* 输入:[5,5,10] +* 输出:true + +示例 3: +* 输入:[10,10] +* 输出:false + +示例 4: +* 输入:[5,5,10,10,20] +* 输出:false +* 解释: + * 前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 + * 对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 + * 对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 10 美元的钞票。 + * 由于不是每位顾客都得到了正确的找零,所以答案是 false。 + +提示: + +* 0 <= bills.length <= 10000 +* bills[i] 不是 5 就是 10 或是 20  + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,看上去复杂,其实逻辑都是固定的!LeetCode:860.柠檬水找零](https://www.bilibili.com/video/BV12x4y1j7DD),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这是前几天的leetcode每日一题,感觉不错,给大家讲一下。 + +这道题目刚一看,可能会有点懵,这要怎么找零才能保证完成全部账单的找零呢? + +**但仔细一琢磨就会发现,可供我们做判断的空间非常少!** + +只需要维护三种金额的数量,5,10和20。 + +有如下三种情况: + +* 情况一:账单是5,直接收下。 +* 情况二:账单是10,消耗一个5,增加一个10 +* 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5 + +此时大家就发现 情况一,情况二,都是固定策略,都不用我们来做分析了,而唯一不确定的其实在情况三。 + +而情况三逻辑也不复杂甚至感觉纯模拟就可以了,其实情况三这里是有贪心的。 + +账单是20的情况,为什么要优先消耗一个10和一个5呢? + +**因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!** + +所以局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。 + +局部最优可以推出全局最优,并找不出反例,那么就试试贪心算法! + +C++代码如下: + +```CPP +class Solution { +public: + bool lemonadeChange(vector& bills) { + int five = 0, ten = 0, twenty = 0; + for (int bill : bills) { + // 情况一 + if (bill == 5) five++; + // 情况二 + if (bill == 10) { + if (five <= 0) return false; + ten++; + five--; + } + // 情况三 + if (bill == 20) { + // 优先消耗10美元,因为5美元的找零用处更大,能多留着就多留着 + if (five > 0 && ten > 0) { + five--; + ten--; + twenty++; // 其实这行代码可以删了,因为记录20已经没有意义了,不会用20来找零 + } else if (five >= 3) { + five -= 3; + twenty++; // 同理,这行代码也可以删了 + } else return false; + } + } + return true; + } +}; +``` +* 时间复杂度: O(n) +* 空间复杂度: O(1) + + +## 总结 + +咋眼一看好像很复杂,分析清楚之后,会发现逻辑其实非常固定。 + +这道题目可以告诉大家,遇到感觉没有思路的题目,可以静下心来把能遇到的情况分析一下,只要分析到具体情况了,一下子就豁然开朗了。 + +如果一直陷入想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。 + + +## 其他语言版本 + + +### Java +```java +class Solution { + public boolean lemonadeChange(int[] bills) { + int five = 0; + int ten = 0; + + for (int i = 0; i < bills.length; i++) { + if (bills[i] == 5) { + five++; + } else if (bills[i] == 10) { + five--; + ten++; + } else if (bills[i] == 20) { + if (ten > 0) { + ten--; + five--; + } else { + five -= 3; + } + } + if (five < 0 || ten < 0) return false; + } + + return true; + } +} +``` + +### Python +```python +class Solution: + def lemonadeChange(self, bills: List[int]) -> bool: + five = 0 + ten = 0 + twenty = 0 + + for bill in bills: + # 情况一:收到5美元 + if bill == 5: + five += 1 + + # 情况二:收到10美元 + if bill == 10: + if five <= 0: + return False + ten += 1 + five -= 1 + + # 情况三:收到20美元 + if bill == 20: + # 先尝试使用10美元和5美元找零 + if five > 0 and ten > 0: + five -= 1 + ten -= 1 + #twenty += 1 + # 如果无法使用10美元找零,则尝试使用三张5美元找零 + elif five >= 3: + five -= 3 + #twenty += 1 + else: + return False + + return True + + +``` + +### Go + +```go +func lemonadeChange(bills []int) bool { + ten, five := 0, 0 + for i := 0; i < len(bills); i++ { + if bills[i] == 5 { + five++ + } else if bills[i] == 10 { + if five == 0 { + return false + } + ten++; five-- + } else { + if ten >= 1 && five >= 1 { + ten--; five-- + } else if five >= 3 { + five -= 3 + } else { + return false + } + } + } + return true +} +``` + +### JavaScript +```Javascript +var lemonadeChange = function(bills) { + let fiveCount = 0 + let tenCount = 0 + + for(let i = 0; i < bills.length; i++) { + let bill = bills[i] + if(bill === 5) { + fiveCount += 1 + } else if (bill === 10) { + if(fiveCount > 0) { + fiveCount -=1 + tenCount += 1 + } else { + return false + } + } else { + if(tenCount > 0 && fiveCount > 0) { + tenCount -= 1 + fiveCount -= 1 + } else if(fiveCount >= 3) { + fiveCount -= 3 + } else { + return false + } + } + } + return true +}; + +``` + +### Rust + +```Rust +impl Solution { + pub fn lemonade_change(bills: Vec) -> bool { + let mut five = 0; + let mut ten = 0; + // let mut twenty = 0; + for bill in bills { + if bill == 5 { five += 1; } + if bill == 10 { + if five <= 0 { return false; } + ten += 1; + five -= 1; + } + if bill == 20 { + if five > 0 && ten > 0 { + five -= 1; + ten -= 1; + // twenty += 1; + } else if five >= 3 { + five -= 3; + // twenty += 1; + } else { return false; } + } + } + true + } +} +``` + +### C +```c +bool lemonadeChange(int* bills, int billsSize){ + // 分别记录五元、十元的数量(二十元不用记录,因为不会用到20元找零) + int fiveCount = 0; int tenCount = 0; + + int i; + for(i = 0; i < billsSize; ++i) { + // 分情况讨论每位顾客的付款 + switch(bills[i]) { + // 情况一:直接收款五元 + case 5: + fiveCount++; + break; + // 情况二:收款十元 + case 10: + // 若没有五元找零,返回false + if(fiveCount == 0) + return false; + // 收款十元并找零五元 + fiveCount--; + tenCount++; + break; + // 情况三:收款二十元 + case 20: + // 若可以,优先用十元和五元找零(因为十元只能找零20,所以需要尽量用掉。而5元能找零十元和二十元) + if(fiveCount > 0 && tenCount > 0) { + fiveCount--; + tenCount--; + } + // 若没有十元,但是有三张五元。用三张五元找零 + else if(fiveCount >= 3) + fiveCount-=3; + // 无法找开,返回false + else + return false; + break; + } + } + // 全部可以找开,返回true + return true; +} +``` + +### TypeScript + +```typescript +function lemonadeChange(bills: number[]): boolean { + let five: number = 0, + ten: number = 0; + for (let bill of bills) { + switch (bill) { + case 5: + five++; + break; + case 10: + if (five < 1) return false; + five--; + ten++ + break; + case 20: + if (ten > 0 && five > 0) { + five--; + ten--; + } else if (five > 2) { + five -= 3; + } else { + return false; + } + break; + } + } + return true; +}; +``` + + +### Scala + +```scala +object Solution { + def lemonadeChange(bills: Array[Int]): Boolean = { + var fiveNum = 0 + var tenNum = 0 + + for (i <- bills) { + if (i == 5) fiveNum += 1 + if (i == 10) { + if (fiveNum <= 0) return false + tenNum += 1 + fiveNum -= 1 + } + if (i == 20) { + if (fiveNum > 0 && tenNum > 0) { + tenNum -= 1 + fiveNum -= 1 + } else if (fiveNum >= 3) { + fiveNum -= 3 + } else { + return false + } + } + } + true + } +} +``` +### C# +```csharp +public class Solution +{ + public bool LemonadeChange(int[] bills) + { + int five = 0, ten = 0, twenty = 0; + foreach (var bill in bills) + { + if (bill == 5) five++; + if (bill == 10) + { + if (five == 0) return false; + five--; + ten++; + } + if (bill == 20) + { + if (ten > 0 && five > 0) + { + ten--; + five--; + twenty++; + } + else if (five >= 3) + { + five -= 3; + twenty++; + } + else + { + return false; + } + + } + } + return true; + } +} +``` + + + diff --git "a/problems/0922.\346\214\211\345\245\207\345\201\266\346\216\222\345\272\217\346\225\260\347\273\204II.md" "b/problems/0922.\346\214\211\345\245\207\345\201\266\346\216\222\345\272\217\346\225\260\347\273\204II.md" new file mode 100755 index 0000000000..484099f89f --- /dev/null +++ "b/problems/0922.\346\214\211\345\245\207\345\201\266\346\216\222\345\272\217\346\225\260\347\273\204II.md" @@ -0,0 +1,410 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 922. 按奇偶排序数组II + +[力扣题目链接](https://leetcode.cn/problems/sort-array-by-parity-ii/) + +给定一个非负整数数组 nums, nums 中一半整数是奇数,一半整数是偶数。 + +对数组进行排序,以便当 nums[i] 为奇数时,i 也是奇数;当 nums[i] 为偶数时, i 也是偶数。 + +你可以返回任何满足上述条件的数组作为答案。 + +示例: + +* 输入:[4,2,5,7] +* 输出:[4,5,2,7] +* 解释:[4,7,2,5],[2,5,4,7],[2,7,4,5] 也会被接受。 + + +## 思路 + +这道题目直接的想法可能是两层for循环再加上used数组表示使用过的元素。这样的的时间复杂度是O(n^2)。 + +### 方法一 + +其实这道题可以用很朴实的方法,时间复杂度就就是O(n)了,C++代码如下: + +```CPP +class Solution { +public: + vector sortArrayByParityII(vector& nums) { + vector even(nums.size() / 2); // 初始化就确定数组大小,节省开销 + vector odd(nums.size() / 2); + vector result(nums.size()); + int evenIndex = 0; + int oddIndex = 0; + int resultIndex = 0; + // 把nums数组放进偶数数组,和奇数数组 + for (int i = 0; i < nums.size(); i++) { + if (nums[i] % 2 == 0) even[evenIndex++] = nums[i]; + else odd[oddIndex++] = nums[i]; + } + // 把偶数数组,奇数数组分别放进result数组中 + for (int i = 0; i < evenIndex; i++) { + result[resultIndex++] = even[i]; + result[resultIndex++] = odd[i]; + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +### 方法二 + +以上代码我是建了两个辅助数组,而且nums数组还相当于遍历了两次,用辅助数组的好处就是思路清晰,优化一下就是不用这两个辅助数组,代码如下: + +```CPP +class Solution { +public: + vector sortArrayByParityII(vector& nums) { + vector result(nums.size()); + int evenIndex = 0; // 偶数下标 + int oddIndex = 1; // 奇数下标 + for (int i = 0; i < nums.size(); i++) { + if (nums[i] % 2 == 0) { + result[evenIndex] = nums[i]; + evenIndex += 2; + } + else { + result[oddIndex] = nums[i]; + oddIndex += 2; + } + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +### 方法三 + +当然还可以在原数组上修改,连result数组都不用了。 + +```CPP +class Solution { +public: + vector sortArrayByParityII(vector& nums) { + int oddIndex = 1; + for (int i = 0; i < nums.size(); i += 2) { + if (nums[i] % 2 == 1) { // 在偶数位遇到了奇数 + while(nums[oddIndex] % 2 != 0) oddIndex += 2; // 在奇数位找一个偶数 + swap(nums[i], nums[oddIndex]); // 替换 + } + } + return nums; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +这里时间复杂度并不是O(n^2),因为偶数位和奇数位都只操作一次,不是n/2 * n/2的关系,而是n/2 + n/2的关系! + + +## 其他语言版本 + +### Java + +```java +// 方法一 +class Solution { + public int[] sortArrayByParityII(int[] nums) { + // 分别存放 nums 中的奇数、偶数 + int len = nums.length; + int evenIndex = 0; + int oddIndex = 0; + int[] even = new int[len / 2]; + int[] odd = new int[len / 2]; + for (int i = 0; i < len; i++) { + if (nums[i] % 2 == 0) { + even[evenIndex++] = nums[i]; + } else { + odd[oddIndex++] = nums[i]; + } + } + // 把奇偶数组重新存回 nums + int index = 0; + for (int i = 0; i < even.length; i++) { + nums[index++] = even[i]; + nums[index++] = odd[i]; + } + return nums; + } +} +``` + +```java +//方法一:采用额外的数组空间 +class Solution { + public int[] sortArrayByParityII(int[] nums) { + //定义结果数组 result + int[] result = new int[nums.length]; + int even = 0, odd = 1; + for(int i = 0; i < nums.length; i++){ + //如果为偶数 + if(nums[i] % 2 == 0){ + result[even] = nums[i]; + even += 2; + }else{ + result[odd] = nums[i]; + odd += 2; + } + } + return result; + } +} +``` +```java +//方法二:不采用额外的数组空间 +class Solution922 { + public int[] sortArrayByParityII(int[] nums) { + //定义双指针 + int oddPoint = 1, evenPoint = 0; + //开始移动并交换,最后一层必然为相互交换后再移动或者相同直接移动 + while(oddPoint < nums.length && evenPoint < nums.length){ + //进行判断 + if(nums[oddPoint] % 2 == 0 && nums[evenPoint] % 2 == 1){ //如果均不满足 + int temp = 0; + temp = nums[oddPoint]; + nums[oddPoint] = nums[evenPoint]; + nums[evenPoint] = temp; + oddPoint += 2; + evenPoint += 2; + }else if(nums[oddPoint] % 2 == 0 && nums[evenPoint] % 2 == 0){ //偶数满足 + evenPoint += 2; + }else if(nums[oddPoint] % 2 == 1 && nums[evenPoint] % 2 == 1){ //奇数满足 + oddPoint += 2; + }else{ + oddPoint += 2; + evenPoint += 2; + } + } + return nums; + } +} +``` + +### Python3 + +```python +#方法2 +class Solution: + def sortArrayByParityII(self, nums: List[int]) -> List[int]: + result = [0]*len(nums) + evenIndex = 0 + oddIndex = 1 + for i in range(len(nums)): + if nums[i] % 2: #奇数 + result[oddIndex] = nums[i] + oddIndex += 2 + else: #偶数 + result[evenIndex] = nums[i] + evenIndex += 2 + return result + +#方法3 +class Solution: + def sortArrayByParityII(self, nums: List[int]) -> List[int]: + oddIndex = 1 + for i in range(0,len(nums),2): #步长为2 + if nums[i] % 2: #偶数位遇到奇数 + while nums[oddIndex] % 2: #奇数位找偶数 + oddIndex += 2 + nums[i], nums[oddIndex] = nums[oddIndex], nums[i] + return nums +``` + +### Go + +```go + +// 方法一 +func sortArrayByParityII(nums []int) []int { + // 分别存放 nums 中的奇数、偶数 + even, odd := []int{}, []int{} + for i := 0; i < len(nums); i++ { + if (nums[i] % 2 == 0) { + even = append(even, nums[i]) + } else { + odd = append(odd, nums[i]) + } + } + + // 把奇偶数组重新存回 nums + result := make([]int, len(nums)) + index := 0 + for i := 0; i < len(even); i++ { + result[index] = even[i]; index++; + result[index] = odd[i]; index++; + } + return result; +} + +// 方法二 +func sortArrayByParityII(nums []int) []int { + result := make([]int, len(nums)) + evenIndex := 0 // 偶数下标 + oddIndex := 1 // 奇数下标 + for _, v := range nums { + if v % 2 == 0 { + result[evenIndex] = v + evenIndex += 2 + } else { + result[oddIndex] = v + oddIndex += 2 + } + } + return result +} + +// 方法三 +func sortArrayByParityII(nums []int) []int { + oddIndex := 1 + for i := 0; i < len(nums); i += 2 { + if nums[i] % 2 == 1 { // 在偶数位遇到了奇数 + for nums[oddIndex] % 2 != 0 { + oddIndex += 2 // 在奇数位找一个偶数 + } + nums[i], nums[oddIndex] = nums[oddIndex], nums[i] + } + } + return nums +} +``` + +### JavaScript + +```js +//方法一 +var sortArrayByParityII = function(nums) { + const n = nums.length; + // 分别存放 nums 中的奇数、偶数 + let evenIndex = 0, oddIndex = 0; + // 初始化就确定数组大小,节省开销 + const even = new Array(Math.floor(n/2)); + const odd = new Array(Math.floor(n/2)); + // 把A数组放进偶数数组,和奇数数组 + for(let i = 0; i < n; i++){ + if(nums[i] % 2 === 0) even[evenIndex++] = nums[i]; + else odd[oddIndex++] = nums[i]; + } + // 把奇偶数组重新存回 nums + let index = 0; + for(let i = 0; i < even.length; i++){ + nums[index++] = even[i]; + nums[index++] = odd[i]; + } + return nums; +}; + +//方法二 +var sortArrayByParityII = function(nums) { + const n = nums.length; + const result = new Array(n); + // 偶数下标 和 奇数下标 + let evenIndex = 0, oddIndex = 1; + for(let i = 0; i < n; i++){ + if(nums[i] % 2 === 0) { + result[evenIndex] = nums[i]; + evenIndex += 2; + } else { + result[oddIndex] = nums[i]; + oddIndex += 2; + } + } + return result; +}; + +//方法三 +var sortArrayByParityII = function(nums) { + let oddIndex = 1; + for(let i = 0; i < nums.length; i += 2){ + if(nums[i] % 2 === 1){ // 在偶数位遇到了奇数 + while(nums[oddIndex] % 2 !== 0) oddIndex += 2;// 在奇数位找一个偶数 + [nums[oddIndex], nums[i]] = [nums[i], nums[oddIndex]]; // 解构赋值交换 + } + } + return nums; +}; +``` + +### TypeScript + +> 方法一: + +```typescript +function sortArrayByParityII(nums: number[]): number[] { + const evenArr: number[] = [], + oddArr: number[] = []; + for (let num of nums) { + if (num % 2 === 0) { + evenArr.push(num); + } else { + oddArr.push(num); + } + } + const resArr: number[] = []; + for (let i = 0, length = nums.length / 2; i < length; i++) { + resArr.push(evenArr[i]); + resArr.push(oddArr[i]); + } + return resArr; +}; +``` + +> 方法二: + +```typescript +function sortArrayByParityII(nums: number[]): number[] { + const length: number = nums.length; + const resArr: number[] = []; + let evenIndex: number = 0, + oddIndex: number = 1; + for (let i = 0; i < length; i++) { + if (nums[i] % 2 === 0) { + resArr[evenIndex] = nums[i]; + evenIndex += 2; + } else { + resArr[oddIndex] = nums[i]; + oddIndex += 2; + } + } + return resArr; +}; +``` + +> 方法三: + +```typescript +function sortArrayByParityII(nums: number[]): number[] { + const length: number = nums.length; + let oddIndex: number = 1; + for (let evenIndex = 0; evenIndex < length; evenIndex += 2) { + if (nums[evenIndex] % 2 === 1) { + // 在偶数位遇到了奇数 + while (oddIndex < length && nums[oddIndex] % 2 === 1) { + oddIndex += 2; + } + // 在奇数位遇到了偶数,交换 + let temp = nums[evenIndex]; + nums[evenIndex] = nums[oddIndex]; + nums[oddIndex] = temp; + } + } + return nums; +}; +``` + + diff --git "a/problems/0925.\351\225\277\346\214\211\351\224\256\345\205\245.md" "b/problems/0925.\351\225\277\346\214\211\351\224\256\345\205\245.md" new file mode 100755 index 0000000000..f653caef52 --- /dev/null +++ "b/problems/0925.\351\225\277\346\214\211\351\224\256\345\205\245.md" @@ -0,0 +1,227 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 925.长按键入 +[力扣题目链接](https://leetcode.cn/problems/long-pressed-name/) + +你的朋友正在使用键盘输入他的名字 name。偶尔,在键入字符 c 时,按键可能会被长按,而字符可能被输入 1 次或多次。 + +你将会检查键盘输入的字符 typed。如果它对应的可能是你的朋友的名字(其中一些字符可能被长按),那么就返回 True。 + +示例 1: +* 输入:name = "alex", typed = "aaleex" +* 输出:true +* 解释:'alex' 中的 'a' 和 'e' 被长按。 + +示例 2: +* 输入:name = "saeed", typed = "ssaaedd" +* 输出:false +* 解释:'e' 一定需要被键入两次,但在 typed 的输出中不是这样。 + + +示例 3: + +* 输入:name = "leelee", typed = "lleeelee" +* 输出:true + +示例 4: + +* 输入:name = "laiden", typed = "laiden" +* 输出:true +* 解释:长按名字中的字符并不是必要的。 + +## 思路 + +这道题目一看以为是哈希,仔细一看不行,要有顺序。 + +所以模拟同时遍历两个数组,进行对比就可以了。 + +对比的时候需要一下几点: + +* name[i] 和 typed[j]相同,则i++,j++ (继续向后对比) +* name[i] 和 typed[j]不相同 + * 看是不是第一位就不相同了,也就是j如果等于0,那么直接返回false + * 不是第一位不相同,就让j跨越重复项,移动到重复项之后的位置,再次比较name[i] 和typed[j] + * 如果 name[i] 和 typed[j]相同,则i++,j++ (继续向后对比) + * 不相同,返回false +* 对比完之后有两种情况 + * name没有匹配完,例如name:"pyplrzzzzdsfa" type:"ppyypllr" + * type没有匹配完,例如name:"alex" type:"alexxrrrrssda" + +动画如下: + + + +上面的逻辑想清楚了,不难写出如下C++代码: + +```CPP +class Solution { +public: + bool isLongPressedName(string name, string typed) { + int i = 0, j = 0; + while (i < name.size() && j < typed.size()) { + if (name[i] == typed[j]) { // 相同则同时向后匹配 + j++; i++; + } else { // 不相同 + if (j == 0) return false; // 如果是第一位就不相同直接返回false + // j跨越重复项,向后移动,同时防止j越界 + while(j < typed.size() && typed[j] == typed[j - 1]) j++; + if (name[i] == typed[j]) { // j跨越重复项之后再次和name[i]匹配 + j++; i++; // 相同则同时向后匹配 + } + else return false; + } + } + // 说明name没有匹配完,例如 name:"pyplrzzzzdsfa" type:"ppyypllr" + if (i < name.size()) return false; + + // 说明type没有匹配完,例如 name:"alex" type:"alexxrrrrssda" + while (j < typed.size()) { + if (typed[j] == typed[j - 1]) j++; + else return false; + } + return true; + } +}; + +``` + +时间复杂度:O(n) +空间复杂度:O(1) + + +## 其他语言版本 + +### Java +```java +class Solution { + public boolean isLongPressedName(String name, String typed) { + int i = 0, j = 0; + int m = name.length(), n = typed.length(); + while (i< m && j < n) { + if (name.charAt(i) == typed.charAt(j)) { // 相同则同时向后匹配 + i++; j++; + } + else { + if (j == 0) return false; // 如果是第一位就不相同直接返回false + // 判断边界为n-1,若为n会越界,例如name:"kikcxmvzi" typed:"kiikcxxmmvvzzz" + while (j < n-1 && typed.charAt(j) == typed.charAt(j-1)) j++; + if (name.charAt(i) == typed.charAt(j)) { // j跨越重复项之后再次和name[i]匹配 + i++; j++; // 相同则同时向后匹配 + } + else return false; + } + } + // 说明name没有匹配完 + if (i < m) return false; + // 说明type没有匹配完 + while (j < n) { + if (typed.charAt(j) == typed.charAt(j-1)) j++; + else return false; + } + return true; + } +} +``` +### Python +```python + i = j = 0 + while(i len(typed)) { + return false; + } + + idx := 0 // name的索引 + var last byte // 上个匹配字符 + for i := 0; i < len(typed); i++ { + if idx < len(name) && name[idx] == typed[i] { + last = name[idx] + idx++ + } else if last == typed[i] { + continue + } else { + return false + } + } + return idx == len(name) +} +``` + +### JavaScript: +```javascript +var isLongPressedName = function(name, typed) { + let i = 0, j = 0; + const m = name.length, n = typed.length; + while(i < m && j < n){ + if(name[i] === typed[j]){ // 相同则同时向后匹配 + i++; j++; + } else { + if(j === 0) return false; // 如果是第一位就不相同直接返回false + // 判断边界为n-1,若为n会越界,例如name:"kikcxmvzi" typed:"kiikcxxmmvvzzz" + while(j < n - 1 && typed[j] === typed[j-1]) j++; + if(name[i] === typed[j]){ // j跨越重复项之后再次和name[i]匹配,相同则同时向后匹配 + i++; j++; + } else { + return false; + } + } + } + // 说明name没有匹配完 例如 name:"pyplrzzzzdsfa" type:"ppyypllr" + if(i < m) return false; + // 说明type没有匹配完 例如 name:"alex" type:"alexxrrrrssda" + while(j < n) { + if(typed[j] === typed[j-1]) j++; + else return false; + } + return true; +}; +``` + +### TypeScript + +```typescript +function isLongPressedName(name: string, typed: string): boolean { + const nameLength: number = name.length, + typeLength: number = typed.length; + let i: number = 0, + j: number = 0; + while (i < nameLength && j < typeLength) { + if (name[i] !== typed[j]) return false; + i++; + j++; + if (i === nameLength || name[i] !== name[i - 1]) { + // 跳过typed中的连续相同字符 + while (j < typeLength && typed[j] === typed[j - 1]) { + j++; + } + } + } + return i === nameLength && j === typeLength; +}; +``` + + + + diff --git "a/problems/0941.\346\234\211\346\225\210\347\232\204\345\261\261\350\204\211\346\225\260\347\273\204.md" "b/problems/0941.\346\234\211\346\225\210\347\232\204\345\261\261\350\204\211\346\225\260\347\273\204.md" new file mode 100755 index 0000000000..9f000e2bb5 --- /dev/null +++ "b/problems/0941.\346\234\211\346\225\210\347\232\204\345\261\261\350\204\211\346\225\260\347\273\204.md" @@ -0,0 +1,213 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 941.有效的山脉数组 + +[力扣题目链接](https://leetcode.cn/problems/valid-mountain-array/) + +给定一个整数数组 arr,如果它是有效的山脉数组就返回 true,否则返回 false。 + +让我们回顾一下,如果 A 满足下述条件,那么它是一个山脉数组: + +* arr.length >= 3 +* 在 0 < i < arr.length - 1 条件下,存在 i 使得: + * arr[0] < arr[1] < ... arr[i-1] < arr[i] + * arr[i] > arr[i+1] > ... > arr[arr.length - 1] + +![](https://file1.kamacoder.com/i/algo/20210729103604.png) + +示例 1: +* 输入:arr = [2,1] +* 输出:false + +示例 2: +* 输入:arr = [3,5,5] +* 输出:false + +示例 3: +* 输入:arr = [0,3,2,1] +* 输出:true + + +## 思路 + +判断是山峰,主要就是要严格的保存左边到中间,和右边到中间是递增的。 + +这样可以使用两个指针,left和right,让其按照如下规则移动,如图: + + + +**注意这里还是有一些细节,例如如下两点:** + +* 因为left和right是数组下标,移动的过程中注意不要数组越界 +* 如果left或者right没有移动,说明是一个单调递增或者递减的数组,依然不是山峰 + +C++代码如下: + +```CPP +class Solution { +public: + bool validMountainArray(vector& A) { + if (A.size() < 3) return false; + int left = 0; + int right = A.size() - 1; + + // 注意防止越界 + while (left < A.size() - 1 && A[left] < A[left + 1]) left++; + + // 注意防止越界 + while (right > 0 && A[right] < A[right - 1]) right--; + + // 如果left或者right都在起始位置,说明不是山峰 + if (left == right && left != 0 && right != A.size() - 1) return true; + return false; + } +}; +``` + +如果想系统学一学双指针的话, 可以看一下这篇[双指针法:总结篇!](https://programmercarl.com/双指针总结.html) + +## 其他语言版本 + +### Java + +```java +class Solution { + public boolean validMountainArray(int[] arr) { + if (arr.length < 3) { // 此时,一定不是有效的山脉数组 + return false; + } + // 双指针 + int left = 0; + int right = arr.length - 1; + // 注意防止指针越界 + while (left + 1 < arr.length && arr[left] < arr[left + 1]) { + left++; + } + // 注意防止指针越界 + while (right > 0 && arr[right] < arr[right - 1]) { + right--; + } + // 如果left或者right都在起始位置,说明不是山峰 + if (left == right && left != 0 && right != arr.length - 1) { + return true; + } + return false; + } +} +``` + +### Python3 + +```python +class Solution: + def validMountainArray(self, arr: List[int]) -> bool: + left, right = 0, len(arr)-1 + + while left < len(arr)-1 and arr[left+1] > arr[left]: + left += 1 + + while right > 0 and arr[right-1] > arr[right]: + right -= 1 + + return left == right and right != 0 and left != len(arr)-1 + +``` + +### Go + +```go +func validMountainArray(arr []int) bool { + if len(arr) < 3 { + return false + } + + i := 1 + flagIncrease := false // 上升 + flagDecrease := false // 下降 + + for ; i < len(arr) && arr[i-1] < arr[i]; i++ { + flagIncrease = true; + } + + for ; i < len(arr) && arr[i-1] > arr[i]; i++ { + flagDecrease = true; + } + + return i == len(arr) && flagIncrease && flagDecrease; +} +``` + +### JavaScript + +```js +var validMountainArray = function(arr) { + if(arr.length < 3) return false;// 一定不是山脉数组 + let left = 0, right = arr.length - 1;// 双指针 + // 注意防止越界 + while(left < arr.length && arr[left] < arr[left+1]) left++; + while(right>0 && arr[right-1] > arr[right]) right--; + // 如果left或者right都在起始位置,说明不是山峰 + if(left === right && left !== 0 && right !== arr.length - 1) return true; + return false; +}; +``` + +### TypeScript + +```typescript +function validMountainArray(arr: number[]): boolean { + const length: number = arr.length; + if (length < 3) return false; + let left: number = 0, + right: number = length - 1; + while (left < (length - 1) && arr[left] < arr[left + 1]) { + left++; + } + while (right > 0 && arr[right] < arr[right - 1]) { + right--; + } + if (left === right && left !== 0 && right !== length - 1) + return true; + return false; +}; +``` + +### C# + +```csharp +public class Solution { + public bool ValidMountainArray(int[] arr) { + if (arr.Length < 3) return false; + + int left = 0; + int right = arr.Length - 1; + + while (left + 1< arr.Length && arr[left] < arr[left + 1]) left ++; + while (right > 0 && arr[right] < arr[right - 1]) right --; + if (left == right && left != 0 && right != arr.Length - 1) return true; + + return false; + } +} +``` + +### Rust +```rust +impl Solution { + pub fn valid_mountain_array(arr: Vec) -> bool { + let mut i = 0; + let mut j = arr.len() - 1; + while i < arr.len() - 1 && arr[i] < arr[i + 1] { + i += 1; + } + while j > 0 && arr[j] < arr[j - 1] { + j -= 1; + } + i > 0 && j < arr.len() - 1 && i == j + } +} +``` + diff --git "a/problems/0968.\347\233\221\346\216\247\344\272\214\345\217\211\346\240\221.md" "b/problems/0968.\347\233\221\346\216\247\344\272\214\345\217\211\346\240\221.md" new file mode 100755 index 0000000000..989993acf5 --- /dev/null +++ "b/problems/0968.\347\233\221\346\216\247\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,789 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 968.监控二叉树 + +[力扣题目链接](https://leetcode.cn/problems/binary-tree-cameras/) + +给定一个二叉树,我们在树的节点上安装摄像头。 + +节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 + +计算监控树的所有节点所需的最小摄像头数量。 + +示例 1: + +![](https://file1.kamacoder.com/i/algo/20201229175736596.png) + +* 输入:[0,0,null,0,0] +* 输出:1 +* 解释:如图所示,一台摄像头足以监控所有节点。 + +示例 2: + +![](https://file1.kamacoder.com/i/algo/2020122917584449.png) + +* 输入:[0,0,null,0,null,0,null,null,0] +* 输出:2 +* 解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。 + +提示: + +* 给定树的节点数的范围是 [1, 1000]。 +* 每个节点的值都是 0。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,二叉树与贪心的结合,有点难...... LeetCode:968.监督二叉树](https://www.bilibili.com/video/BV1SA411U75i),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +这道题目首先要想,如何放置,才能让摄像头最小的呢? + +从题目中示例,其实可以得到启发,**我们发现题目示例中的摄像头都没有放在叶子节点上!** + +这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。 + +所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。 + +那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢? + +因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。 + +**所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!** + +局部最优推出全局最优,找不出反例,那么就按照贪心来! + +此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。 + +此时这道题目还有两个难点: + +1. 二叉树的遍历 +2. 如何隔两个节点放一个摄像头 + + +### 确定遍历顺序 + +在二叉树中如何从低向上推导呢? + +可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。 + +后序遍历代码如下: + +```CPP +int traversal(TreeNode* cur) { + + // 空节点,该节点有覆盖 + if (终止条件) return ; + + int left = traversal(cur->left); // 左 + int right = traversal(cur->right); // 右 + + 逻辑处理 // 中 + return ; +} +``` + +**注意在以上代码中我们取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态** + +### 如何隔两个节点放一个摄像头 + +此时需要状态转移的公式,大家不要和动态的状态转移公式混到一起,本题状态转移没有择优的过程,就是单纯的状态转移! + +来看看这个状态应该如何转移,先来看看每个节点可能有几种状态: + +有如下三种: + +* 该节点无覆盖 +* 本节点有摄像头 +* 本节点有覆盖 + +我们分别有三个数字来表示: + +* 0:该节点无覆盖 +* 1:本节点有摄像头 +* 2:本节点有覆盖 + +大家应该找不出第四个节点的状态了。 + +**一些同学可能会想有没有第四种状态:本节点无摄像头,其实无摄像头就是 无覆盖 或者 有覆盖的状态,所以一共还是三个状态。** + +**因为在遍历树的过程中,就会遇到空节点,那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢?** + + +回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。 + +那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。 + +**所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了** + +接下来就是递推关系。 + +那么递归的终止条件应该是遇到了空节点,此时应该返回2(有覆盖),原因上面已经解释过了。 + +代码如下: + +```CPP +// 空节点,该节点有覆盖 +if (cur == NULL) return 2; +``` + +递归的函数,以及终止条件已经确定了,再来看单层逻辑处理。 + +主要有如下四类情况: + +* 情况1:左右节点都有覆盖 + +左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。 + +如图: + +![968.监控二叉树2](https://file1.kamacoder.com/i/algo/20201229203710729.png) + +代码如下: + +```CPP +// 左右节点都有覆盖 +if (left == 2 && right == 2) return 0; +``` + +* 情况2:左右节点至少有一个无覆盖的情况 + +如果是以下情况,则中间节点(父节点)应该放摄像头: + +* left == 0 && right == 0 左右节点无覆盖 +* left == 1 && right == 0 左节点有摄像头,右节点无覆盖 +* left == 0 && right == 1 左节点有无覆盖,右节点摄像头 +* left == 0 && right == 2 左节点无覆盖,右节点覆盖 +* left == 2 && right == 0 左节点覆盖,右节点无覆盖 + +这个不难理解,毕竟有一个孩子没有覆盖,父节点就应该放摄像头。 + +此时摄像头的数量要加一,并且return 1,代表中间节点放摄像头。 + +代码如下: + +```CPP +if (left == 0 || right == 0) { + result++; + return 1; +} +``` + +* 情况3:左右节点至少有一个有摄像头 + +如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态) + +* left == 1 && right == 2 左节点有摄像头,右节点有覆盖 +* left == 2 && right == 1 左节点有覆盖,右节点有摄像头 +* left == 1 && right == 1 左右节点都有摄像头 + +代码如下: + +```CPP +if (left == 1 || right == 1) return 2; +``` + +**从这个代码中,可以看出,如果left == 1, right == 0 怎么办?其实这种条件在情况2中已经判断过了**,如图: + +![968.监控二叉树1](https://file1.kamacoder.com/i/algo/2020122920362355.png) + +这种情况也是大多数同学容易迷惑的情况。 + +4. 情况4:头结点没有覆盖 + +以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图: + +![968.监控二叉树3](https://file1.kamacoder.com/i/algo/20201229203742446.png) + +所以递归结束之后,还要判断根节点,如果没有覆盖,result++,代码如下: + +```CPP +int minCameraCover(TreeNode* root) { + result = 0; + if (traversal(root) == 0) { // root 无覆盖 + result++; + } + return result; +} +``` + +以上四种情况我们分析完了,代码也差不多了,整体代码如下: + +(**以下我的代码注释很详细,为了把情况说清楚,特别把每种情况列出来。**) + +C++代码如下: + +```CPP +// 版本一 +class Solution { +private: + int result; + int traversal(TreeNode* cur) { + + // 空节点,该节点有覆盖 + if (cur == NULL) return 2; + + int left = traversal(cur->left); // 左 + int right = traversal(cur->right); // 右 + + // 情况1 + // 左右节点都有覆盖 + if (left == 2 && right == 2) return 0; + + // 情况2 + // left == 0 && right == 0 左右节点无覆盖 + // left == 1 && right == 0 左节点有摄像头,右节点无覆盖 + // left == 0 && right == 1 左节点有无覆盖,右节点摄像头 + // left == 0 && right == 2 左节点无覆盖,右节点覆盖 + // left == 2 && right == 0 左节点覆盖,右节点无覆盖 + if (left == 0 || right == 0) { + result++; + return 1; + } + + // 情况3 + // left == 1 && right == 2 左节点有摄像头,右节点有覆盖 + // left == 2 && right == 1 左节点有覆盖,右节点有摄像头 + // left == 1 && right == 1 左右节点都有摄像头 + // 其他情况前段代码均已覆盖 + if (left == 1 || right == 1) return 2; + + // 以上代码我没有使用else,主要是为了把各个分支条件展现出来,这样代码有助于读者理解 + // 这个 return -1 逻辑不会走到这里。 + return -1; + } + +public: + int minCameraCover(TreeNode* root) { + result = 0; + // 情况4 + if (traversal(root) == 0) { // root 无覆盖 + result++; + } + return result; + } +}; +``` + +在以上代码的基础上,再进行精简,代码如下: + +```CPP +// 版本二 +class Solution { +private: + int result; + int traversal(TreeNode* cur) { + if (cur == NULL) return 2; + int left = traversal(cur->left); // 左 + int right = traversal(cur->right); // 右 + if (left == 2 && right == 2) return 0; + else if (left == 0 || right == 0) { + result++; + return 1; + } else return 2; + } +public: + int minCameraCover(TreeNode* root) { + result = 0; + if (traversal(root) == 0) { // root 无覆盖 + result++; + } + return result; + } +}; + + +``` + +* 时间复杂度: O(n),需要遍历二叉树上的每个节点 +* 空间复杂度: O(n) + + + +大家可能会惊讶,居然可以这么简短,**其实就是在版本一的基础上,使用else把一些情况直接覆盖掉了**。 + +在网上关于这道题解可以搜到很多这种神级别的代码,但都没讲不清楚,如果直接看代码的话,指定越看越晕,**所以建议大家对着版本一的代码一步一步来,版本二中看不中用!**。 + +## 总结 + +本题的难点首先是要想到贪心的思路,然后就是遍历和状态推导。 + +在二叉树上进行状态推导,其实难度就上了一个台阶了,需要对二叉树的操作非常娴熟。 + +这道题目是名副其实的hard,大家感受感受。 + +## 其他语言版本 + + +### Java + +```java +class Solution { + int res=0; + public int minCameraCover(TreeNode root) { + // 对根节点的状态做检验,防止根节点是无覆盖状态 . + if(minCame(root)==0){ + res++; + } + return res; + } + /** + 节点的状态值: + 0 表示无覆盖 + 1 表示 有摄像头 + 2 表示有覆盖 + 后序遍历,根据左右节点的情况,来判读 自己的状态 + */ + public int minCame(TreeNode root){ + if(root==null){ + // 空节点默认为 有覆盖状态,避免在叶子节点上放摄像头 + return 2; + } + int left=minCame(root.left); + int right=minCame(root.right); + + // 如果左右节点都覆盖了的话, 那么本节点的状态就应该是无覆盖,没有摄像头 + if(left==2&&right==2){ + //(2,2) + return 0; + }else if(left==0||right==0){ + // 左右节点都是无覆盖状态,那 根节点此时应该放一个摄像头 + // (0,0) (0,1) (0,2) (1,0) (2,0) + // 状态值为 1 摄像头数 ++; + res++; + return 1; + }else{ + // 左右节点的 状态为 (1,1) (1,2) (2,1) 也就是左右节点至少存在 1个摄像头, + // 那么本节点就是处于被覆盖状态 + return 2; + } + } +} +``` + +简化分支版本: + +```java +class Solution { + static int ans; + public int minCameraCover(TreeNode root) { + ans = 0; // 初始化 + if(f(root) == 0) ans ++; + return ans; + } + // 定义 f 函数有三种返回值情况 + // 0:表示 x 节点没有被相机监控,只能依靠父节点放相机 + // 1:表示 x 节点被相机监控,但相机不是放在自身节点上 + // 2:表示 x 节点被相机监控,但相机放在自身节点上 + public static int f(TreeNode x) { + if(x == null) return 1; // 空树认为被监控,但没有相机 + // 左右递归到最深处 + int l = f(x.left); + int r = f(x.right); + // 有任意一个子节点为空,就需要当前节点放相机,不然以后没机会 + if(l == 0 || r == 0) { + ans ++; // 放相机 + return 2; + } + // 贪心策略,左右子树都被监控,且没有监控到当前节点, + // 那么最有利的情况就是将相机放置在当前节点父节点上, + // 因为这样能多监控可能的子树节点和父父节点 + if(l == 1 && r == 1) return 0; + // 剩下情况就是左右子树有可能为 2,即当前节点被监控 + return 1; + } +} +``` + + + +### Python + +贪心(版本一) +```python +class Solution: + # Greedy Algo: + # 从下往上安装摄像头:跳过leaves这样安装数量最少,局部最优 -> 全局最优 + # 先给leaves的父节点安装,然后每隔两层节点安装一个摄像头,直到Head + # 0: 该节点未覆盖 + # 1: 该节点有摄像头 + # 2: 该节点有覆盖 + def minCameraCover(self, root: TreeNode) -> int: + # 定义递归函数 + result = [0] # 用于记录摄像头的安装数量 + if self.traversal(root, result) == 0: + result[0] += 1 + + return result[0] + + + def traversal(self, cur: TreeNode, result: List[int]) -> int: + if not cur: + return 2 + + left = self.traversal(cur.left, result) + right = self.traversal(cur.right, result) + + # 情况1: 左右节点都有覆盖 + if left == 2 and right == 2: + return 0 + + # 情况2: + # left == 0 && right == 0 左右节点无覆盖 + # left == 1 && right == 0 左节点有摄像头,右节点无覆盖 + # left == 0 && right == 1 左节点无覆盖,右节点有摄像头 + # left == 0 && right == 2 左节点无覆盖,右节点覆盖 + # left == 2 && right == 0 左节点覆盖,右节点无覆盖 + if left == 0 or right == 0: + result[0] += 1 + return 1 + + # 情况3: + # left == 1 && right == 2 左节点有摄像头,右节点有覆盖 + # left == 2 && right == 1 左节点有覆盖,右节点有摄像头 + # left == 1 && right == 1 左右节点都有摄像头 + if left == 1 or right == 1: + return 2 + + +``` +贪心(版本二)利用elif精简代码 +```python +class Solution: + # Greedy Algo: + # 从下往上安装摄像头:跳过leaves这样安装数量最少,局部最优 -> 全局最优 + # 先给leaves的父节点安装,然后每隔两层节点安装一个摄像头,直到Head + # 0: 该节点未覆盖 + # 1: 该节点有摄像头 + # 2: 该节点有覆盖 + def minCameraCover(self, root: TreeNode) -> int: + # 定义递归函数 + result = [0] # 用于记录摄像头的安装数量 + if self.traversal(root, result) == 0: + result[0] += 1 + + return result[0] + + + def traversal(self, cur: TreeNode, result: List[int]) -> int: + if not cur: + return 2 + + left = self.traversal(cur.left, result) + right = self.traversal(cur.right, result) + + # 情况1: 左右节点都有覆盖 + if left == 2 and right == 2: + return 0 + + # 情况2: + # left == 0 && right == 0 左右节点无覆盖 + # left == 1 && right == 0 左节点有摄像头,右节点无覆盖 + # left == 0 && right == 1 左节点无覆盖,右节点有摄像头 + # left == 0 && right == 2 左节点无覆盖,右节点覆盖 + # left == 2 && right == 0 左节点覆盖,右节点无覆盖 + elif left == 0 or right == 0: + result[0] += 1 + return 1 + + # 情况3: + # left == 1 && right == 2 左节点有摄像头,右节点有覆盖 + # left == 2 && right == 1 左节点有覆盖,右节点有摄像头 + # left == 1 && right == 1 左右节点都有摄像头 + else: + return 2 + + + + +``` +### Go + +```go +const inf = math.MaxInt64 / 2 + +func minCameraCover(root *TreeNode) int { + var dfs func(*TreeNode) (a, b, c int) + dfs = func(node *TreeNode) (a, b, c int) { + if node == nil { + return inf, 0, 0 + } + lefta, leftb, leftc := dfs(node.Left) + righta, rightb, rightc := dfs(node.Right) + a = leftc + rightc + 1 + b = min(a, min(lefta+rightb, righta+leftb)) + c = min(a, leftb+rightb) + return + } + _, ans, _ := dfs(root) + return ans +} + +func min(a, b int) int { + if a <= b { + return a + } + return b +} + +``` + +### JavaScript + +```Javascript +var minCameraCover = function(root) { + let result = 0 + function traversal(cur) { + if(cur === null) { + return 2 + } + + let left = traversal(cur.left) + let right = traversal(cur.right) + + if(left === 2 && right === 2) { + return 0 + } + + if(left === 0 || right === 0) { + result++ + return 1 + } + + if(left === 1 || right === 1) { + return 2 + } + + return -1 + } + + if(traversal(root) === 0) { + result++ + } + + return result + +}; +``` + +### TypeScript + +```typescript +function minCameraCover(root: TreeNode | null): number { + /** 0-无覆盖, 1-有摄像头, 2-有覆盖 */ + type statusCode = 0 | 1 | 2; + let resCount: number = 0; + if (recur(root) === 0) resCount++; + return resCount; + function recur(node: TreeNode | null): statusCode { + if (node === null) return 2; + const left: statusCode = recur(node.left), + right: statusCode = recur(node.right); + let resStatus: statusCode = 0; + if (left === 0 || right === 0) { + resStatus = 1; + resCount++; + } else if (left === 1 || right === 1) { + resStatus = 2; + } else { + resStatus = 0; + } + return resStatus; + } +}; +``` + +### C + +```c +/* +**函数后序遍历二叉树。判断一个结点状态时,根据其左右孩子结点的状态进行判断 +**状态:0为没有被摄像头覆盖到。1为此结点处应设置摄像头。2为此结点已被摄像头覆盖 +*/ +int traversal(struct TreeNode* node, int* ans) { + //递归结束条件:传入结点为NULL,假设此结点能被摄像头覆盖。这样方便与对叶子结点的判断,将叶子结点设为0 + if(!node) + return 2; + //后序遍历二叉树,记录左右孩子的状态。根据左右孩子状态更新结点自身状态 + int left = traversal(node->left, ans); + int right = traversal(node->right, ans); + + //若左右孩子都可以被摄像头覆盖,将父亲结点状态设为0 + if(left == 2 && right == 2) { + return 0; + } + //若左右孩子有一个结点状态为没有被覆盖(0),则将父亲结点状态设置为摄像头 + if(left == 0 || right == 0) { + (*ans)++; + return 1; + } + //若左右孩子有一个为摄像头,证明父亲结点可以被覆盖。将父亲结点状态变为2 + if(left == 1 || right == 1) + return 2; + //逻辑不会走到-1,语句不会执行 + return -1; +} + +int minCameraCover(struct TreeNode* root){ + int ans = 0; + + //在对整个二叉树遍历后。头结点可能未被覆盖,这时候如果函数返回值为0,证明头结点未被覆盖。说明头结点也需要添置摄像头,ans++ + if(traversal(root, &ans) == 0) + ans++; + return ans; +} +``` + +### Scala + +```scala +object Solution { + def minCameraCover(root: TreeNode): Int = { + var result = 0 + def traversal(cur: TreeNode): Int = { + // 空节点,该节点有覆盖 + if (cur == null) return 2 + var left = traversal(cur.left) + var right = traversal(cur.right) + // 情况1,左右节点都有覆盖 + if (left == 2 && right == 2) { + return 0 + } + // 情况2 + if (left == 0 || right == 0) { + result += 1 + return 1 + } + // 情况3 + if (left == 1 || right == 1) { + return 2 + } + -1 + } + + if (traversal(root) == 0) { + result += 1 + } + result + } +} +``` + +### Rust + +```Rust +/// 版本一 +impl Solution { + pub fn min_camera_cover(root: Option>>) -> i32 { + let mut res = 0; + if Self::traversal(&root, &mut res) == 0 { + res += 1; + } + res + } + + pub fn traversal(cur: &Option>>, ans: &mut i32) -> i32 { + // 0 未覆盖 1 节点已设置摄像头 2 节点已覆盖 + if let Some(node) = cur { + let node = node.borrow(); + + let left = Self::traversal(&node.left, ans); + let right = Self::traversal(&node.right, ans); + + // 左右节点都被覆盖 + if left == 2 && right == 2 { + return 0; // 无覆盖 + } + + // left == 0 right == 0 左右无覆盖 + // left == 0 right == 1 左节点无覆盖 右节点有摄像头 + // left == 1 right == 0 左节点有摄像头 左节点无覆盖 + // left == 0 right == 2 左节点无覆盖 右节点有覆盖 + // left == 2 right == 0 左节点有覆盖 右节点无覆盖 + if left == 0 || right == 0 { + *ans += 1; + return 1; + } + + // left == 1 right == 1 左节点有摄像头 右节点有摄像头 + // left == 1 right == 2 左节点有摄像头 右节点覆盖 + // left == 2 right == 1 左节点覆盖 右节点有摄像头 + if left == 1 || right == 1 { + return 2; // 已覆盖 + } + } else { + return 2; + } + -1 + } +} + +/// 版本二 +enum NodeState { + NoCover = 0, + Camera = 1, + Covered = 2, +} + +impl Solution { + pub fn min_camera_cover(root: Option>>) -> i32 { + let mut res = 0; + let state = Self::traversal(&root, &mut res); + match state { + NodeState::NoCover => res + 1, + _ => res, + } + } + + pub fn traversal(cur: &Option>>, ans: &mut i32) -> NodeState { + if let Some(node) = cur { + let node = node.borrow(); + let left_state = Self::traversal(&node.left, ans); + let right_state = Self::traversal(&node.right, ans); + match (left_state, right_state) { + (NodeState::NoCover, _) | (_, NodeState::NoCover) => { + *ans += 1; + NodeState::Camera + } + (NodeState::Camera, _) | (_, NodeState::Camera) => NodeState::Covered, + (_, _) => NodeState::NoCover, + } + } else { + NodeState::Covered + } + } +} + +``` +### C# +```csharp +public class Solution +{ + public int res = 0; + public int MinCameraCover(TreeNode root) + { + if (Traversal(root) == 0) res++; + return res; + } + public int Traversal(TreeNode cur) + { + if (cur == null) return 2; + int left = Traversal(cur.left); + int right = Traversal(cur.right); + if (left == 2 && right == 2) return 0; + else if (left == 0 || right == 0) + { + res++; + return 1; + } + else return 2; + } +} +``` + diff --git "a/problems/0977.\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\345\271\263\346\226\271.md" "b/problems/0977.\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\345\271\263\346\226\271.md" new file mode 100755 index 0000000000..1f58fd5198 --- /dev/null +++ "b/problems/0977.\346\234\211\345\272\217\346\225\260\347\273\204\347\232\204\345\271\263\346\226\271.md" @@ -0,0 +1,589 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +> 双指针风骚起来,也是无敌 + +# 977.有序数组的平方 + +[力扣题目链接](https://leetcode.cn/problems/squares-of-a-sorted-array/) + +给你一个按 非递减顺序 排序的整数数组 nums,返回 每个数字的平方 组成的新数组,要求也按 非递减顺序 排序。 + +示例 1: +* 输入:nums = [-4,-1,0,3,10] +* 输出:[0,1,9,16,100] +* 解释:平方后,数组变为 [16,1,0,9,100],排序后,数组变为 [0,1,9,16,100] + +示例 2: +* 输入:nums = [-7,-3,2,3,11] +* 输出:[4,9,9,49,121] + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[双指针法经典题目!LeetCode:977.有序数组的平方](https://www.bilibili.com/video/BV1QB4y1D7ep),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +### 暴力排序 + +最直观的想法,莫过于:每个数平方之后,排个序,代码如下: + +```CPP +class Solution { +public: + vector sortedSquares(vector& A) { + for (int i = 0; i < A.size(); i++) { + A[i] *= A[i]; + } + sort(A.begin(), A.end()); // 快速排序 + return A; + } +}; +``` + +这个时间复杂度是 O(n + nlogn), 可以说是O(nlogn)的时间复杂度,但为了和下面双指针法算法时间复杂度有鲜明对比,我记为 O(n + nlog n)。 + +### 双指针法 + +数组其实是有序的, 只不过负数平方之后可能成为最大数了。 + +那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。 + +此时可以考虑双指针法了,i指向起始位置,j指向终止位置。 + +定义一个新数组result,和A数组一样的大小,让k指向result数组终止位置。 + +如果`A[i] * A[i] < A[j] * A[j]` 那么`result[k--] = A[j] * A[j];` 。 + +如果`A[i] * A[i] >= A[j] * A[j]` 那么`result[k--] = A[i] * A[i];` 。 + +如动画所示: + +![](https://file1.kamacoder.com/i/algo/977.有序数组的平方.gif) + +不难写出如下代码: + +```CPP +class Solution { +public: + vector sortedSquares(vector& A) { + int k = A.size() - 1; + vector result(A.size(), 0); + for (int i = 0, j = A.size() - 1; i <= j;) { // 注意这里要i <= j,因为最后要处理两个元素 + if (A[i] * A[i] < A[j] * A[j]) { + result[k--] = A[j] * A[j]; + j--; + } + else { + result[k--] = A[i] * A[i]; + i++; + } + } + return result; + } +}; +``` + +此时的时间复杂度为O(n),相对于暴力排序的解法O(n + nlog n)还是提升不少的。 + + +**这里还是说一下,大家不必太在意leetcode上执行用时,打败多少多少用户,这个就是一个玩具,非常不准确。** + +做题的时候自己能分析出来时间复杂度就可以了,至于leetcode上执行用时,大概看一下就行,只要达到最优的时间复杂度就可以了, + +一样的代码多提交几次可能就击败百分之百了..... + +## 其他语言版本 + +### Java: + +排序法 +```Java +class Solution { + public int[] sortedSquares(int[] nums) { + for (int i = 0; i < nums.length; i++) { + nums[i] = nums[i] * nums[i]; + } + Arrays.sort(nums); + return nums; + } +} +``` + +```Java +class Solution { + public int[] sortedSquares(int[] nums) { + int right = nums.length - 1; + int left = 0; + int[] result = new int[nums.length]; + int index = result.length - 1; + while (left <= right) { + if (nums[left] * nums[left] > nums[right] * nums[right]) { + // 正数的相对位置是不变的, 需要调整的是负数平方后的相对位置 + result[index--] = nums[left] * nums[left]; + ++left; + } else { + result[index--] = nums[right] * nums[right]; + --right; + } + } + return result; + } +} +``` + +```java +class Solution { + public int[] sortedSquares(int[] nums) { + int l = 0; + int r = nums.length - 1; + int[] res = new int[nums.length]; + int j = nums.length - 1; + while(l <= r){ + if(nums[l] * nums[l] > nums[r] * nums[r]){ + res[j--] = nums[l] * nums[l++]; + }else{ + res[j--] = nums[r] * nums[r--]; + } + } + return res; + } +} +``` + +### Python: + +```Python +(版本一)双指针法 +class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + l, r, i = 0, len(nums)-1, len(nums)-1 + res = [float('inf')] * len(nums) # 需要提前定义列表,存放结果 + while l <= r: + if nums[l] ** 2 < nums[r] ** 2: # 左右边界进行对比,找出最大值 + res[i] = nums[r] ** 2 + r -= 1 # 右指针往左移动 + else: + res[i] = nums[l] ** 2 + l += 1 # 左指针往右移动 + i -= 1 # 存放结果的指针需要往前平移一位 + return res +``` + +```Python +(版本二)暴力排序法 +class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + for i in range(len(nums)): + nums[i] *= nums[i] + nums.sort() + return nums +``` + +```Python +(版本三)暴力排序法+列表推导法 +class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + return sorted(x*x for x in nums) +``` + +```Python +(版本四) 双指针+ 反转列表 +class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + #根据list的先进排序在先原则 + #将nums的平方按从大到小的顺序添加进新的list + #最后反转list + new_list = [] + left, right = 0 , len(nums) -1 + while left <= right: + if abs(nums[left]) <= abs(nums[right]): + new_list.append(nums[right] ** 2) + right -= 1 + else: + new_list.append(nums[left] ** 2) + left += 1 + return new_list[::-1] +``` + +```python3 +(双指针优化版本) 三步优化 + class Solution: + def sortedSquares(self, nums: List[int]) -> List[int]: + """ + 整体思想:有序数组的绝对值最大值永远在两头,比较两头,平方大的插到新数组的最后 + 优 化:1. 优化所有元素为非正或非负的情况 + 2. 头尾平方的大小比较直接将头尾相加与0进行比较即可 + 3. 新的平方排序数组的插入索引可以用倒序插入实现(针对for循环,while循环不适用) + """ + + # 特殊情况, 元素都非负(优化1) + if nums[0] >= 0: + return [num ** 2 for num in nums] # 按顺序平方即可 + # 最后一个非正,全负有序的 + if nums[-1] <= 0: + return [x ** 2 for x in nums[::-1]] # 倒序平方后的数组 + + # 一般情况, 有正有负 + i = 0 # 原数组头索引 + j = len(nums) - 1 # 原数组尾部索引 + new_nums = [0] * len(nums) # 新建一个等长数组用于保存排序后的结果 + # end_index = len(nums) - 1 # 新的排序数组(是新数组)尾插索引, 每次需要减一(优化3优化了) + + for end_index in range(len(nums)-1, -1, -1): # (优化3,倒序,不用单独创建变量) + # if nums[i] ** 2 >= nums[j] ** 2: + if nums[i] + nums[j] <= 0: # (优化2) + new_nums[end_index] = nums[i] ** 2 + i += 1 + # end_index -= 1 (优化3) + else: + new_nums[end_index] = nums[j] ** 2 + j -= 1 + # end_index -= 1 (优化3) + return new_nums +``` + +### Go: + +```Go +// 排序法 +func sortedSquares(nums []int) []int { + for i, val := range nums { + nums[i] *= val + } + sort.Ints(nums) + return nums +} +``` +```Go +// 双指针法 +func sortedSquares(nums []int) []int { + n := len(nums) + i, j, k := 0, n-1, n-1 + ans := make([]int, n) + for i <= j { + lm, rm := nums[i]*nums[i], nums[j]*nums[j] + if lm > rm { + ans[k] = lm + i++ + } else { + ans[k] = rm + j-- + } + k-- + } + return ans +} +``` +### Rust: + +```rust +impl Solution { + pub fn sorted_squares(nums: Vec) -> Vec { + let n = nums.len(); + let (mut i,mut j,mut k) = (0,n - 1,n); + let mut ans = vec![0;n]; + while i <= j{ + if nums[i] * nums[i] < nums[j] * nums[j] { + ans[k-1] = nums[j] * nums[j]; + j -= 1; + }else{ + ans[k-1] = nums[i] * nums[i]; + i += 1; + } + k -= 1; + } + ans + } +} +``` +### JavaScript: + +```Javascript +/** + * @param {number[]} nums + * @return {number[]} + */ +var sortedSquares = function(nums) { + let n = nums.length; + let res = new Array(n).fill(0); + let i = 0, j = n - 1, k = n - 1; + while (i <= j) { + let left = nums[i] * nums[i], + right = nums[j] * nums[j]; + if (left < right) { + res[k--] = right; + j--; + } else { + res[k--] = left; + i++; + } + } + return res; +}; +``` + +### TypeScript: + +双指针法: + +```typescript +function sortedSquares(nums: number[]): number[] { + const ans: number[] = []; + let left = 0, + right = nums.length - 1; + + while (left <= right) { + // 右侧的元素不需要取绝对值,nums 为非递减排序的整数数组 + // 在同为负数的情况下,左侧的平方值一定大于右侧的平方值 + if (Math.abs(nums[left]) > nums[right]) { + // 使用 Array.prototype.unshift() 直接在数组的首项插入当前最大值 + ans.unshift(nums[left] ** 2); + left++; + } else { + ans.unshift(nums[right] ** 2); + right--; + } + } + + return ans; +}; +``` + +骚操作法(暴力思路): + +```typescript +function sortedSquares(nums: number[]): number[] { + return nums.map(i => i * i).sort((a, b) => a - b); +}; +``` + +### Swift: + +```swift +func sortedSquares(_ nums: [Int]) -> [Int] { + // 指向新数组最后一个元素 + var k = nums.count - 1 + // 指向原数组第一个元素 + var i = 0 + // 指向原数组最后一个元素 + var j = nums.count - 1 + // 初始化新数组(用-1填充) + var result = Array(repeating: -1, count: nums.count) + + for _ in 0.. nums[right]**2 + result << nums[left]**2 + left += 1 + else + result << nums[right]**2 + right -= 1 + end + end + result.reverse +end +``` + +### C: + +```c +int* sortedSquares(int* nums, int numsSize, int* returnSize){ + //返回的数组大小就是原数组大小 + *returnSize = numsSize; + //创建两个指针,right指向数组最后一位元素,left指向数组第一位元素 + int right = numsSize - 1; + int left = 0; + + //最后要返回的结果数组 + int* ans = (int*)malloc(sizeof(int) * numsSize); + int index; + for(index = numsSize - 1; index >= 0; index--) { + //左指针指向元素的平方 + int lSquare = nums[left] * nums[left]; + //右指针指向元素的平方 + int rSquare = nums[right] * nums[right]; + //若左指针指向元素平方比右指针指向元素平方大,将左指针指向元素平方放入结果数组。左指针右移一位 + if(lSquare > rSquare) { + ans[index] = lSquare; + left++; + } + //若右指针指向元素平方比左指针指向元素平方大,将右指针指向元素平方放入结果数组。右指针左移一位 + else { + ans[index] = rSquare; + right--; + } + } + //返回结果数组 + return ans; +} +``` + +### PHP: + +```php +class Solution { + /** + * @param Integer[] $nums + * @return Integer[] + */ + function sortedSquares($nums) { + // 双指针法 + $res = []; + for ($i = 0; $i < count($nums); $i++) { + $res[$i] = 0; + } + $k = count($nums) - 1; + for ($i = 0, $j = count($nums) - 1; $i <= $j; ) { + if ($nums[$i] ** 2 < $nums[$j] ** 2) { + $res[$k--] = $nums[$j] ** 2; + $j--; + } + else { + $res[$k--] = $nums[$i] ** 2; + $i++; + } + } + return $res; + } +} +``` + +### Kotlin: + +双指针法 +```kotlin +class Solution { + // 双指针法 + fun sortedSquares(nums: IntArray): IntArray { + var res = IntArray(nums.size) + var left = 0 // 指向数组的最左端 + var right = nums.size - 1 // 指向数组端最右端 + // 选择平方数更大的那一个往 res 数组中倒序填充 + for (index in nums.size - 1 downTo 0) { + if (nums[left] * nums[left] > nums[right] * nums[right]) { + res[index] = nums[left] * nums[left] + left++ + } else { + res[index] = nums[right] * nums[right] + right-- + } + } + return res + } +} +``` +骚操作(暴力思路) +```kotlin +class Solution { + fun sortedSquares(nums: IntArray): IntArray { + // left 与 right 用来控制循环,类似于滑动窗口 + var left: Int = 0; + var right: Int = nums.size - 1; + // 将每个数字的平方经过排序后加入result数值 + var result: IntArray = IntArray(nums.size); + var k: Int = nums.size - 1; + while (left <= right) { + // 从大到小,从后向前填满数组 + // [left, right] 控制循环 + if (nums[left] * nums[left] > nums[right] * nums[right]) { + result[k--] = nums[left] * nums[left] + left++ + } + else { + result[k--] = nums[right] * nums[right] + right-- + } + } + return result + } +} +``` + +### Scala: + +双指针: +```scala +object Solution { + def sortedSquares(nums: Array[Int]): Array[Int] = { + val res: Array[Int] = new Array[Int](nums.length) + var top = nums.length - 1 + var i = 0 + var j = nums.length - 1 + while (i <= j) { + if (nums(i) * nums(i) <= nums(j) * nums(j)) { + // 当左侧平方小于等于右侧,res数组顶部放右侧的平方,并且top下移,j左移 + res(top) = nums(j) * nums(j) + top -= 1 + j -= 1 + } else { + // 当左侧平方大于右侧,res数组顶部放左侧的平方,并且top下移,i右移 + res(top) = nums(i) * nums(i) + top -= 1 + i += 1 + } + } + res + } +} +``` +骚操作(暴力思路): +```scala +object Solution { + def sortedSquares(nums: Array[Int]): Array[Int] = { + nums.map(x=>{x*x}).sortWith(_ < _) + } +} +``` + +### C#: + +```csharp +public class Solution { + public int[] SortedSquares(int[] nums) { + int k = nums.Length - 1; + int[] result = new int[nums.Length]; + for (int i = 0, j = nums.Length - 1;i <= j;){ + if (nums[i] * nums[i] < nums[j] * nums[j]) { + result[k--] = nums[j] * nums[j]; + j--; + } else { + result[k--] = nums[i] * nums[i]; + i++; + } + } + return result; + } +} +``` +C# LINQ: +```csharp +public class Solution { + public int[] SortedSquares(int[] nums) { + return nums.Select(x => x * x).OrderBy(x => x).ToArray(); + } +} +``` + + diff --git "a/problems/1002.\346\237\245\346\211\276\345\270\270\347\224\250\345\255\227\347\254\246.md" "b/problems/1002.\346\237\245\346\211\276\345\270\270\347\224\250\345\255\227\347\254\246.md" new file mode 100755 index 0000000000..cbf5ecdb78 --- /dev/null +++ "b/problems/1002.\346\237\245\346\211\276\345\270\270\347\224\250\345\255\227\347\254\246.md" @@ -0,0 +1,582 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 1002. 查找常用字符 + +[力扣题目链接](https://leetcode.cn/problems/find-common-characters/) + +给你一个字符串数组 words ,请你找出所有在 words 的每个字符串中都出现的共用字符( 包括重复字符),并以数组形式返回。你可以按 任意顺序 返回答案。 + +示例 1: + +输入:words = ["bella","label","roller"] +输出:["e","l","l"] + +示例 2: + +输入:words = ["cool","lock","cook"] +输出:["c","o"] + +提示: + +1 <= words.length <= 100 +1 <= words[i].length <= 100 +words[i] 由小写英文字母组成 + + + +## 思路 + +这道题意一起就有点绕,不是那么容易懂,其实就是26个小写字符中有字符 在所有字符串里都出现的话,就输出,重复的也算。 + +例如: + +输入:["ll","ll","ll"] +输出:["l","l"] + +这道题目一眼看上去,就是用哈希法,**“小写字符”,“出现频率”, 这些关键字都是为哈希法量身定做的啊** + +首先可以想到的是暴力解法,一个字符串一个字符串去搜,时间复杂度是$O(n^m)$,n是字符串长度,m是有几个字符串。 + +可以看出这是指数级别的时间复杂度,非常高,而且代码实现也不容易,因为要统计 重复的字符,还要适当的替换或者去重。 + +那我们还是哈希法吧。如果对哈希法不了解,可以看这篇:[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)。 + +如果对用数组来做哈希法不了解的话,可以看这篇:[把数组当做哈希表来用,很巧妙!](https://programmercarl.com/0242.有效的字母异位词.html)。 + +了解了哈希法,理解了数组在哈希法中的应用之后,可以来看解题思路了。 + +整体思路就是统计出搜索字符串里26个字符的出现的频率,然后取每个字符频率最小值,最后转成输出格式就可以了。 + +如图: + +![1002.查找常用字符](https://file1.kamacoder.com/i/algo/1002.查找常用字符.png) + +先统计第一个字符串所有字符出现的次数,代码如下: + +```cpp +int hash[26] = {0}; // 用来统计所有字符串里字符出现的最小频率 +for (int i = 0; i < A[0].size(); i++) { // 用第一个字符串给hash初始化 + hash[A[0][i] - 'a']++; +} +``` + +接下来,把其他字符串里字符的出现次数也统计出来一次放在hashOtherStr中。 + +然后hash 和 hashOtherStr 取最小值,这是本题关键所在,此时取最小值,就是 一个字符在所有字符串里出现的最小次数了。 + +代码如下: + +```cpp +int hashOtherStr[26] = {0}; // 统计除第一个字符串外字符的出现频率 +for (int i = 1; i < A.size(); i++) { + memset(hashOtherStr, 0, 26 * sizeof(int)); + for (int j = 0; j < A[i].size(); j++) { + hashOtherStr[A[i][j] - 'a']++; + } + // 这是关键所在 + for (int k = 0; k < 26; k++) { // 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 + hash[k] = min(hash[k], hashOtherStr[k]); + } +} +``` +此时hash里统计着字符在所有字符串里出现的最小次数,那么把hash转成题目要求的输出格式就可以了。 + +代码如下: + +```cpp +// 将hash统计的字符次数,转成输出形式 +for (int i = 0; i < 26; i++) { + while (hash[i] != 0) { // 注意这里是while,多个重复的字符 + string s(1, i + 'a'); // char -> string + result.push_back(s); + hash[i]--; + } +} +``` + +整体C++代码如下: + +```CPP +class Solution { +public: + vector commonChars(vector& A) { + vector result; + if (A.size() == 0) return result; + int hash[26] = {0}; // 用来统计所有字符串里字符出现的最小频率 + for (int i = 0; i < A[0].size(); i++) { // 用第一个字符串给hash初始化 + hash[A[0][i] - 'a']++; + } + + int hashOtherStr[26] = {0}; // 统计除第一个字符串外字符的出现频率 + for (int i = 1; i < A.size(); i++) { + memset(hashOtherStr, 0, 26 * sizeof(int)); + for (int j = 0; j < A[i].size(); j++) { + hashOtherStr[A[i][j] - 'a']++; + } + // 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 + for (int k = 0; k < 26; k++) { + hash[k] = min(hash[k], hashOtherStr[k]); + } + } + // 将hash统计的字符次数,转成输出形式 + for (int i = 0; i < 26; i++) { + while (hash[i] != 0) { // 注意这里是while,多个重复的字符 + string s(1, i + 'a'); // char -> string + result.push_back(s); + hash[i]--; + } + } + + return result; + } +}; +``` + +## 其他语言版本 + +### Java: + +```Java +class Solution { + public List commonChars(String[] A) { + List result = new ArrayList<>(); + if (A.length == 0) return result; + int[] hash= new int[26]; // 用来统计所有字符串里字符出现的最小频率 + for (int i = 0; i < A[0].length(); i++) { // 用第一个字符串给hash初始化 + hash[A[0].charAt(i)- 'a']++; + } + // 统计除第一个字符串外字符的出现频率 + for (int i = 1; i < A.length; i++) { + int[] hashOtherStr= new int[26]; + for (int j = 0; j < A[i].length(); j++) { + hashOtherStr[A[i].charAt(j)- 'a']++; + } + // 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 + for (int k = 0; k < 26; k++) { + hash[k] = Math.min(hash[k], hashOtherStr[k]); + } + } + // 将hash统计的字符次数,转成输出形式 + for (int i = 0; i < 26; i++) { + while (hash[i] != 0) { // 注意这里是while,多个重复的字符 + char c= (char) (i+'a'); + result.add(String.valueOf(c)); + hash[i]--; + } + } + return result; + } +} +``` +### Python + +```python +class Solution: + def commonChars(self, words: List[str]) -> List[str]: + if not words: return [] + result = [] + hash = [0] * 26 # 用来统计所有字符串里字符出现的最小频率 + for i, c in enumerate(words[0]): # 用第一个字符串给hash初始化 + hash[ord(c) - ord('a')] += 1 + # 统计除第一个字符串外字符的出现频率 + for i in range(1, len(words)): + hashOtherStr = [0] * 26 + for j in range(len(words[i])): + hashOtherStr[ord(words[i][j]) - ord('a')] += 1 + # 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 + for k in range(26): + hash[k] = min(hash[k], hashOtherStr[k]) + # 将hash统计的字符次数,转成输出形式 + for i in range(26): + while hash[i] != 0: # 注意这里是while,多个重复的字符 + result.extend(chr(i + ord('a'))) + hash[i] -= 1 + return result +``` + +Python 3 使用collections.Counter +```python +class Solution: + def commonChars(self, words: List[str]) -> List[str]: + tmp = collections.Counter(words[0]) + l = [] + for i in range(1,len(words)): + # 使用 & 取交集 + tmp = tmp & collections.Counter(words[i]) + + # 剩下的就是每个单词都出现的字符(键),个数(值) + for j in tmp: + v = tmp[j] + while(v): + l.append(j) + v -= 1 + return l +``` + +### JavaScript + +```js +var commonChars = function (words) { + let res = [] + let size = 26 + let firstHash = new Array(size).fill(0) // 初始化 hash 数组 + + let a = "a".charCodeAt() + let firstWord = words[0] + for (let i = 0; i < firstWord.length; i++) { // 第 0 个单词的统计 + let idx = firstWord[i].charCodeAt() + firstHash[idx - a] += 1 + } + + let otherHash = new Array(size).fill(0) // 初始化 hash 数组 + for (let i = 1; i < words.length; i++) { // 1-n 个单词统计 + for (let j = 0; j < words[i].length; j++) { + let idx = words[i][j].charCodeAt() + otherHash[idx - a] += 1 + } + + for (let i = 0; i < size; i++) { + firstHash[i] = Math.min(firstHash[i], otherHash[i]) + } + otherHash.fill(0) + } + + for (let i = 0; i < size; i++) { + while (firstHash[i] > 0) { + res.push(String.fromCharCode(i + a)) + firstHash[i]-- + } + } + return res +}; + +// 方法二:map() +var commonChars = function(words) { + let min_count = new Map() + // 统计字符串中字符出现的最小频率,以第一个字符串初始化 + for(let str of words[0]) { + min_count.set(str, ((min_count.get(str) || 0) + 1)) + } + // 从第二个单词开始统计字符出现次数 + for(let i = 1; i < words.length; i++) { + let char_count = new Map() + for(let str of words[i]) { // 遍历字母 + char_count.set(str, (char_count.get(str) || 0) + 1) + } + // 比较出最小的字符次数 + for(let value of min_count) { // 注意这里遍历min_count!而不是单词 + min_count.set(value[0], Math.min((min_count.get(value[0]) || 0), (char_count.get(value[0]) || 0))) + } + } + // 遍历map + let res = [] + min_count.forEach((value, key) => { + if(value) { + for(let i=0; i() + //给所有map设置初始值为0 + let wordInitial: [string, number][] = words[0] + .split("") + .map((item) => [item, 0]) + //如果有重复字母,就把重复字母的数量加1 + for (let word of words[0]) { + map.set(word, map.has(word) ? map.get(word) + 1 : 1) + } + for (let i = 1; i < words.length; i++) { + const mapWord = new Map(wordInitial) + for (let j = 0; j < words[i].length; j++) { + if (!map.has(words[i][j])) continue + //mapWord中的字母的个数不能高于当前map的个数,多于则不能添加 + if (map.get(words[i][j]) > mapWord.get(words[i][j])) { + mapWord.set( + words[i][j], + mapWord.has(words[i][j]) ? mapWord!.get(words[i][j]) + 1 : 1 + ) + } + } + //每次重新初始化map + map = mapWord + } + for (let [key, value] of map) { + str += key.repeat(value) + } + console.timeEnd("test") + return str.split("") +``` + +### GO + +```golang +func commonChars(A []string) []string { + var result []string + if len(A) == 0 { + return result + } + + hash := make([]int, 26) // 用来统计所有字符串里字符出现的最小频率 + for _, c := range A[0] { // 用第一个字符串给hash初始化 + hash[c-'a']++ + } + + for i := 1; i < len(A); i++ { + hashOtherStr := make([]int, 26) // 统计除第一个字符串外字符的出现频率 + for _, c := range A[i] { + hashOtherStr[c-'a']++ + } + // 更新hash,保证hash里统计26个字符在所有字符串里出现的最小次数 + for k := 0; k < 26; k++ { + hash[k] = min(hash[k], hashOtherStr[k]) + } + } + + // 将hash统计的字符次数,转成输出形式 + for i := 0; i < 26; i++ { + for hash[i] > 0 { + s := string('a' + i) // rune -> string + result = append(result, s) + hash[i]-- + } + } + + return result +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} +``` + +### Swift: + +```swift +func commonChars(_ words: [String]) -> [String] { + var res = [String]() + if words.count < 1 { + return res + } + let aUnicodeScalarValue = "a".unicodeScalars.first!.value + let lettersMaxCount = 26 + // 用于统计所有字符串每个字母出现的 最小 频率 + var hash = Array(repeating: 0, count: lettersMaxCount) + // 统计第一个字符串每个字母出现的次数 + for unicodeScalar in words.first!.unicodeScalars { + hash[Int(unicodeScalar.value - aUnicodeScalarValue)] += 1 + } + // 统计除第一个字符串每个字母出现的次数 + for idx in 1 ..< words.count { + var hashOtherStr = Array(repeating: 0, count: lettersMaxCount) + for unicodeScalar in words[idx].unicodeScalars { + hashOtherStr[Int(unicodeScalar.value - aUnicodeScalarValue)] += 1 + } + // 更新hash,保证hash里统计的字母为出现的最小频率 + for k in 0 ..< lettersMaxCount { + hash[k] = min(hash[k], hashOtherStr[k]) + } + } + // 将hash统计的字符次数,转成输出形式 + for i in 0 ..< lettersMaxCount { + while hash[i] != 0 { // 注意这里是while,多个重复的字符 + let currentUnicodeScalarValue: UInt32 = UInt32(i) + aUnicodeScalarValue + let currentUnicodeScalar: UnicodeScalar = UnicodeScalar(currentUnicodeScalarValue)! + let outputStr = String(currentUnicodeScalar) // UnicodeScalar -> String + res.append(outputStr) + hash[i] -= 1 + } + } + return res +} +``` + +### C: + +```c +//若两个哈希表定义为char数组(每个单词的最大长度不会超过100,因此可以用char表示),可以提高时间和空间效率 +void updateHashTable(int* hashTableOne, int* hashTableTwo) { + int i; + for(i = 0; i < 26; i++) { + hashTableOne[i] = hashTableOne[i] < hashTableTwo[i] ? hashTableOne[i] : hashTableTwo[i]; + } +} + +char ** commonChars(char ** words, int wordsSize, int* returnSize){ + //用来统计所有字母出现的最小频率 + int hashTable[26] = { 0 }; + //初始化返回的char**数组以及返回数组长度 + *returnSize = 0; + char** ret = (char**)malloc(sizeof(char*) * 100); + + //如果输入数组长度为0,则返回NULL + if(!wordsSize) + return NULL; + + int i; + //更新第一个单词的字母频率 + for(i = 0; i < strlen(words[0]); i++) + hashTable[words[0][i] - 'a']++; + //更新从第二个单词开始的字母频率 + for(i = 1; i < wordsSize; i++) { + //创建新的哈希表,记录新的单词的字母频率 + int newHashTable[26] = { 0 }; + int j; + for(j = 0; j < strlen(words[i]); j++) { + newHashTable[words[i][j] - 'a']++; + } + //更新原哈希表 + updateHashTable(hashTable, newHashTable); + } + + //将哈希表中的字符变为字符串放入ret中 + for(i = 0; i < 26; i++) { + if(hashTable[i]) { + int j; + for(j = 0; j < hashTable[i]; j++) { + char* tempString = (char*)malloc(sizeof(char) * 2); + tempString[0] = i + 'a'; + tempString[1] = '\0'; + ret[(*returnSize)++] = tempString; + } + } + } + return ret; +} +``` +### Scala: + +```scala +object Solution { + def commonChars(words: Array[String]): List[String] = { + // 声明返回结果的不可变List集合,因为res要重新赋值,所以声明为var + var res = List[String]() + var hash = new Array[Int](26) // 统计字符出现的最小频率 + // 统计第一个字符串中字符出现的次数 + for (i <- 0 until words(0).length) { + hash(words(0)(i) - 'a') += 1 + } + // 统计其他字符串出现的频率 + for (i <- 1 until words.length) { + // 统计其他字符出现的频率 + var hashOtherStr = new Array[Int](26) + for (j <- 0 until words(i).length) { + hashOtherStr(words(i)(j) - 'a') += 1 + } + // 更新hash,取26个字母最小出现的频率 + for (k <- 0 until 26) { + hash(k) = math.min(hash(k), hashOtherStr(k)) + } + } + // 根据hash的结果转换输出的形式 + for (i <- 0 until 26) { + for (j <- 0 until hash(i)) { + res = res :+ (i + 'a').toChar.toString + } + } + res + } +} +``` + +### Rust: + +```rust +impl Solution { + pub fn common_chars(words: Vec) -> Vec { + if words.is_empty() { + return vec![]; + } + let mut res = vec![]; + let mut hash = vec![0; 26]; + for i in words[0].bytes() { + hash[(i - b'a') as usize] += 1; + } + for i in words.iter().skip(1) { + let mut other_hash_str = vec![0; 26]; + for j in i.bytes() { + other_hash_str[(j - b'a') as usize] += 1; + } + for k in 0..26 { + hash[k] = hash[k].min(other_hash_str[k]); + } + } + + for (i, v) in hash.iter_mut().enumerate() { + while *v > 0 { + res.push(((i as u8 + b'a') as char).to_string()); + *v -= 1; + } + } + + res + } +} +``` + +Ruby: +```ruby +def common_chars(words) + result = [] + #统计所有字符串里字符出现的最小频率 + hash = {} + #初始化标识 + is_first = true + + words.each do |word| + #记录共同字符 + chars = [] + word.split('').each do |chr| + #第一个字符串初始化 + if is_first + chars << chr + else + #字母之前出现过的最小次数 + if hash[chr] != nil && hash[chr] > 0 + hash[chr] -= 1 + chars << chr + end + end + end + + is_first = false + #清除hash,更新字符最小频率 + hash.clear + chars.each do |chr| + if hash[chr] != nil + hash[chr] += 1 + else + hash[chr] = 1 + end + end + end + + #字符最小频率hash转换为字符数组 + hash.keys.each do |key| + for i in 0..hash[key] - 1 + result << key + end + end + + return result +end +``` + + diff --git "a/problems/1005.K\346\254\241\345\217\226\345\217\215\345\220\216\346\234\200\345\244\247\345\214\226\347\232\204\346\225\260\347\273\204\345\222\214.md" "b/problems/1005.K\346\254\241\345\217\226\345\217\215\345\220\216\346\234\200\345\244\247\345\214\226\347\232\204\346\225\260\347\273\204\345\222\214.md" new file mode 100755 index 0000000000..6e908d5af6 --- /dev/null +++ "b/problems/1005.K\346\254\241\345\217\226\345\217\215\345\220\216\346\234\200\345\244\247\345\214\226\347\232\204\346\225\260\347\273\204\345\222\214.md" @@ -0,0 +1,377 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 1005.K次取反后最大化的数组和 + +[力扣题目链接](https://leetcode.cn/problems/maximize-sum-of-array-after-k-negations/) + +给定一个整数数组 A,我们只能用以下方法修改该数组:我们选择某个索引 i 并将 A[i] 替换为 -A[i],然后总共重复这个过程 K 次。(我们可以多次选择同一个索引 i。) + +以这种方式修改数组后,返回数组可能的最大和。 + +示例 1: +* 输入:A = [4,2,3], K = 1 +* 输出:5 +* 解释:选择索引 (1) ,然后 A 变为 [4,-2,3]。 + +示例 2: +* 输入:A = [3,-1,0,2], K = 3 +* 输出:6 +* 解释:选择索引 (1, 2, 2) ,然后 A 变为 [3,1,0,2]。 + +示例 3: +* 输入:A = [2,-3,-1,5,-4], K = 2 +* 输出:13 +* 解释:选择索引 (1, 4) ,然后 A 变为 [2,3,-1,5,4]。 + +提示: + +* 1 <= A.length <= 10000 +* 1 <= K <= 10000 +* -100 <= A[i] <= 100 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法,这不就是常识?还能叫贪心?LeetCode:1005.K次取反后最大化的数组和](https://www.bilibili.com/video/BV138411G7LY),相信结合视频在看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +本题思路其实比较好想了,如何可以让数组和最大呢? + +贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。 + +局部最优可以推出全局最优。 + +那么如果将负数都转变为正数了,K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。 + +那么又是一个贪心:局部最优:只找数值最小的正整数进行反转,当前数值和可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。 + +虽然这道题目大家做的时候,可能都不会去想什么贪心算法,一鼓作气,就AC了。 + +**我这里其实是为了给大家展现出来 经常被大家忽略的贪心思路,这么一道简单题,就用了两次贪心!** + +那么本题的解题步骤为: + +* 第一步:将数组按照绝对值大小从大到小排序,**注意要按照绝对值的大小** +* 第二步:从前向后遍历,遇到负数将其变为正数,同时K-- +* 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完 +* 第四步:求和 + +对应C++代码如下: + +```CPP +class Solution { +static bool cmp(int a, int b) { + return abs(a) > abs(b); +} +public: + int largestSumAfterKNegations(vector& A, int K) { + sort(A.begin(), A.end(), cmp); // 第一步 + for (int i = 0; i < A.size(); i++) { // 第二步 + if (A[i] < 0 && K > 0) { + A[i] *= -1; + K--; + } + } + if (K % 2 == 1) A[A.size() - 1] *= -1; // 第三步 + int result = 0; + for (int a : A) result += a; // 第四步 + return result; + } +}; +``` + +* 时间复杂度: O(nlogn) +* 空间复杂度: O(1) + + +## 总结 + +贪心的题目如果简单起来,会让人简单到开始怀疑:本来不就应该这么做么?这也算是算法?我认为这不是贪心? + +本题其实很简单,不会贪心算法的同学都可以做出来,但是我还是全程用贪心的思路来讲解。 + +因为贪心的思考方式一定要有! + +**如果没有贪心的思考方式(局部最优,全局最优),很容易陷入贪心简单题凭感觉做,贪心难题直接不会做,其实这样就锻炼不了贪心的思考方式了**。 + +所以明知道是贪心简单题,也要靠贪心的思考方式来解题,这样对培养解题感觉很有帮助。 + +## 其他语言版本 + + +### Java +```java +class Solution { + public int largestSumAfterKNegations(int[] nums, int K) { + // 将数组按照绝对值大小从大到小排序,注意要按照绝对值的大小 + nums = IntStream.of(nums) + .boxed() + .sorted((o1, o2) -> Math.abs(o2) - Math.abs(o1)) + .mapToInt(Integer::intValue).toArray(); + int len = nums.length; + for (int i = 0; i < len; i++) { + //从前向后遍历,遇到负数将其变为正数,同时K-- + if (nums[i] < 0 && K > 0) { + nums[i] = -nums[i]; + K--; + } + } + // 如果K还大于0,那么反复转变数值最小的元素,将K用完 + + if (K % 2 == 1) nums[len - 1] = -nums[len - 1]; + return Arrays.stream(nums).sum(); + + } +} + +// 版本二:排序数组并贪心地尽可能将负数翻转为正数,再根据剩余的k值调整最小元素的符号,从而最大化数组的总和。 +class Solution { + public int largestSumAfterKNegations(int[] nums, int k) { + if (nums.length == 1) return nums[0]; + + // 排序:先把负数处理了 + Arrays.sort(nums); + + for (int i = 0; i < nums.length && k > 0; i++) { // 贪心点, 通过负转正, 消耗尽可能多的k + if (nums[i] < 0) { + nums[i] = -nums[i]; + k--; + } + } + + // 退出循环, k > 0 || k < 0 (k消耗完了不用讨论) + if (k % 2 == 1) { // k > 0 && k is odd:对于负数:负-正-负-正 + Arrays.sort(nums); // 再次排序得到剩余的负数,或者最小的正数 + nums[0] = -nums[0]; + } + // k > 0 && k is even,flip数字不会产生影响: 对于负数: 负-正-负;对于正数:正-负-正 + + int sum = 0; + for (int num : nums) { // 计算最大和 + sum += num; + } + return sum; + } +} +``` + +### Python +贪心 +```python +class Solution: + def largestSumAfterKNegations(self, A: List[int], K: int) -> int: + A.sort(key=lambda x: abs(x), reverse=True) # 第一步:按照绝对值降序排序数组A + + for i in range(len(A)): # 第二步:执行K次取反操作 + if A[i] < 0 and K > 0: + A[i] *= -1 + K -= 1 + + if K % 2 == 1: # 第三步:如果K还有剩余次数,将绝对值最小的元素取反 + A[-1] *= -1 + + result = sum(A) # 第四步:计算数组A的元素和 + return result + +``` + +### Go +```Go +func largestSumAfterKNegations(nums []int, K int) int { + sort.Slice(nums, func(i, j int) bool { + return math.Abs(float64(nums[i])) > math.Abs(float64(nums[j])) + }) + + for i := 0; i < len(nums); i++ { + if K > 0 && nums[i] < 0 { + nums[i] = -nums[i] + K-- + } + } + + if K%2 == 1 { + nums[len(nums)-1] = -nums[len(nums)-1] + } + + result := 0 + for i := 0; i < len(nums); i++ { + result += nums[i] + } + return result +} +``` + + +### JavaScript +```Javascript +var largestSumAfterKNegations = function(nums, k) { + + nums.sort((a,b) => Math.abs(b) - Math.abs(a)) + + for(let i = 0 ;i < nums.length; i++){ + if(nums[i] < 0 && k > 0){ + nums[i] = - nums[i]; + k--; + } + } + + // 若k还大于0,则寻找最小的数进行不断取反 + while( k > 0 ){ + nums[nums.length-1] = - nums[nums.length-1] + k--; + } + + // 使用箭头函数的隐式返回值时,需使用简写省略花括号,否则要在 a + b 前加上 return + return nums.reduce((a, b) => a + b) +}; + +// 版本二 (优化: 一次遍历) +var largestSumAfterKNegations = function(nums, k) { + nums.sort((a, b) => Math.abs(b) - Math.abs(a)); // 排序 + let sum = 0; + for(let i = 0; i < nums.length; i++) { + if(nums[i] < 0 && k-- > 0) { // 负数取反(k 数量足够时) + nums[i] = -nums[i]; + } + sum += nums[i]; // 求和 + } + if(k % 2 > 0) { // k 有多余的(k若消耗完则应为 -1) + sum -= 2 * nums[nums.length - 1]; // 减去两倍的最小值(因为之前加过一次) + } + return sum; +}; +``` + +### Rust + +```Rust +impl Solution { + pub fn largest_sum_after_k_negations(mut nums: Vec, mut k: i32) -> i32 { + nums.sort_by_key(|b| std::cmp::Reverse(b.abs())); + for v in nums.iter_mut() { + if *v < 0 && k > 0 { + *v *= -1; + k -= 1; + } + } + if k % 2 == 1 { + *nums.last_mut().unwrap() *= -1; + } + nums.iter().sum() + } +} +``` + + +### C +```c +#define abs(a) (((a) > 0) ? (a) : (-(a))) + +// 对数组求和 +int sum(int *nums, int numsSize) { + int sum = 0; + + int i; + for(i = 0; i < numsSize; ++i) { + sum += nums[i]; + } + return sum; +} + +int cmp(const void* v1, const void* v2) { + return abs(*(int*)v2) - abs(*(int*)v1); +} + +int largestSumAfterKNegations(int* nums, int numsSize, int k){ + qsort(nums, numsSize, sizeof(int), cmp); + + int i; + for(i = 0; i < numsSize; ++i) { + // 遍历数组,若当前元素<0则将当前元素转变,k-- + if(nums[i] < 0 && k > 0) { + nums[i] *= -1; + --k; + } + } + + // 若遍历完数组后k还有剩余(此时所有元素应均为正),则将绝对值最小的元素nums[numsSize - 1]变为负 + if(k % 2 == 1) + nums[numsSize - 1] *= -1; + + return sum(nums, numsSize); +} +``` + +### TypeScript + +```typescript +function largestSumAfterKNegations(nums: number[], k: number): number { + nums.sort((a, b) => Math.abs(b) - Math.abs(a)); + let curIndex: number = 0; + const length = nums.length; + while (curIndex < length && k > 0) { + if (nums[curIndex] < 0) { + nums[curIndex] *= -1; + k--; + } + curIndex++; + } + while (k > 0) { + nums[length - 1] *= -1; + k--; + } + return nums.reduce((pre, cur) => pre + cur, 0); +}; +``` + +### Scala + +```scala +object Solution { + def largestSumAfterKNegations(nums: Array[Int], k: Int): Int = { + var num = nums.sortWith(math.abs(_) > math.abs(_)) + + var kk = k // 因为k是不可变量,所以要赋值给一个可变量 + for (i <- num.indices) { + if (num(i) < 0 && kk > 0) { + num(i) *= -1 // 取反 + kk -= 1 + } + } + + // kk对2取余,结果为0则为偶数不需要取反,结果为1为奇数,只需要对最后的数字进行反转就可以 + if (kk % 2 == 1) num(num.size - 1) *= -1 + + num.sum // 最后返回数字的和 + } +} +``` + +### C# +```csharp +public class Solution +{ + public int LargestSumAfterKNegations(int[] nums, int k) + { + int res = 0; + Array.Sort(nums, (a, b) => Math.Abs(b) - Math.Abs(a)); + for (int i = 0; i < nums.Length; i++) + { + if (nums[i] < 0 && k > 0) + { + nums[i] *= -1; + k--; + } + } + if (k % 2 == 1) nums[nums.Length - 1] *= -1; + foreach (var item in nums) res += item; + return res; + } +} +``` + + + diff --git "a/problems/1020.\351\243\236\345\234\260\347\232\204\346\225\260\351\207\217.md" "b/problems/1020.\351\243\236\345\234\260\347\232\204\346\225\260\351\207\217.md" new file mode 100755 index 0000000000..396e6c566b --- /dev/null +++ "b/problems/1020.\351\243\236\345\234\260\347\232\204\346\225\260\351\207\217.md" @@ -0,0 +1,754 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1020. 飞地的数量 + +[力扣链接](https://leetcode.cn/problems/number-of-enclaves/description/) + +给你一个大小为 m x n 的二进制矩阵 grid ,其中 0 表示一个海洋单元格、1 表示一个陆地单元格。 + +一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过 grid 的边界。 + +返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量。 + +![](https://file1.kamacoder.com/i/algo/20220830100710.png) + +* 输入:grid = [[0,0,0,0],[1,0,1,0],[0,1,1,0],[0,0,0,0]] +* 输出:3 +* 解释:有三个 1 被 0 包围。一个 1 没有被包围,因为它在边界上。 + +![](https://file1.kamacoder.com/i/algo/20220830100742.png) + +* 输入:grid = [[0,1,1,0],[0,0,1,0],[0,0,1,0],[0,0,0,0]] +* 输出:0 +* 解释:所有 1 都在边界上或可以到达边界。 + +## 思路 + +本题使用dfs,bfs,并查集都是可以的。 + +本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地然后 通过 dfs或者bfs 将周边靠陆地且相邻的陆地都变成海洋,然后再去重新遍历地图的时候,统计此时还剩下的陆地就可以了。 + +如图,在遍历地图周围四个边,靠地图四边的陆地,都为绿色, + +![](https://file1.kamacoder.com/i/algo/20220830104632.png) + +在遇到地图周边陆地的时候,将1都变为0,此时地图为这样: + +![](https://file1.kamacoder.com/i/algo/20220830104651.png) + +然后我们再去遍历这个地图,遇到有陆地的地方,去采用深搜或者广搜,边统计所有陆地。 + +如果对深搜或者广搜不够了解,建议先看这里:[深度优先搜索精讲](https://programmercarl.com/图论深搜理论基础.html),[广度优先搜索精讲](https://programmercarl.com/图论广搜理论基础.html)。 + + +采用深度优先搜索的代码如下: + +```CPP +class Solution { +private: + int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 + int count; // 统计符合题目要求的陆地空格数量 + void dfs(vector>& grid, int x, int y) { + grid[x][y] = 0; + count++; + for (int i = 0; i < 4; i++) { // 向四个方向遍历 + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + // 超过边界 + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; + // 不符合条件,不继续遍历 + if (grid[nextx][nexty] == 0) continue; + + dfs (grid, nextx, nexty); + } + return; + } + +public: + int numEnclaves(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) dfs(grid, i, 0); + if (grid[i][m - 1] == 1) dfs(grid, i, m - 1); + } + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) dfs(grid, 0, j); + if (grid[n - 1][j] == 1) dfs(grid, n - 1, j); + } + count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) dfs(grid, i, j); + } + } + return count; + } +}; +``` + +采用广度优先搜索的代码如下: + +```CPP +class Solution { +private: +int count = 0; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, int x, int y) { + queue> que; + que.push({x, y}); + grid[x][y] = 0; // 只要加入队列,立刻标记 + count++; + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (grid[nextx][nexty] == 1) { + que.push({nextx, nexty}); + count++; + grid[nextx][nexty] = 0; // 只要加入队列立刻标记 + } + } + } + +} + +public: + int numEnclaves(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) bfs(grid, i, 0); + if (grid[i][m - 1] == 1) bfs(grid, i, m - 1); + } + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) bfs(grid, 0, j); + if (grid[n - 1][j] == 1) bfs(grid, n - 1, j); + } + count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) bfs(grid, i, j); + } + } + return count; + } +}; +``` +## 其他语言版本 + +### Java + +深度优先遍历(没有终止条件 + 空間優化(淹沒島嶼,沒有使用visited數組)) +```java +//DFS +class Solution { + int count = 0; + int[][] dir ={ + {0, 1}, + {1, 0}, + {-1, 0}, + {0, -1} + }; + private void dfs(int[][] grid, int x, int y){ + if(grid[x][y] == 0) + return; + + grid[x][y] = 0; + count++; + + for(int i = 0; i < 4; i++){ + int nextX = x + dir[i][0]; + int nextY = y + dir[i][1]; + + if(nextX < 0 || nextY < 0 || nextX >= grid.length || nextY >= grid[0].length) + continue; + dfs(grid, nextX, nextY); + } + + } + + public int numEnclaves(int[][] grid) { + for(int i = 0; i < grid.length; i++){ + if(grid[i][0] == 1) + dfs(grid, i, 0); + if(grid[i][grid[0].length - 1] == 1) + dfs(grid, i, grid[0].length - 1); + } + //初始化的時候,j 的上下限有調整過,必免重複操作。 + for(int j = 1; j < grid[0].length - 1; j++){ + if(grid[0][j] == 1) + dfs(grid, 0, j); + if(grid[grid.length - 1][j] == 1) + dfs(grid, grid.length - 1, j); + } + count = 0; + + for(int i = 1; i < grid.length - 1; i++){ + for(int j = 1; j < grid[0].length - 1; j++){ + if(grid[i][j] == 1) + dfs(grid, i, j); + } + } + return count; + } +} +``` + +深度优先遍历(没有终止条件) + +```java +class Solution { + // 四个方向 + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; + + // 深度优先遍历,把可以通向边缘部分的 1 全部标记成 true + public void dfs(int[][] grid, int row, int col, boolean[][] visited) { + for (int[] current: position) { + int newRow = row + current[0], newCol = col + current[1]; + // 下标越界直接跳过 + if (newRow < 0 || newRow >= grid.length || newCol < 0 || newCol >= grid[0].length) continue; + // 当前位置不是 1 或者已经被访问了就直接跳过 + if (grid[newRow][newCol] != 1 || visited[newRow][newCol]) continue; + visited[newRow][newCol] = true; + dfs(grid, newRow, newCol, visited); + } + } + + public int numEnclaves(int[][] grid) { + int rowSize = grid.length, colSize = grid[0].length, ans = 0; // ans 记录答案 + // 标记数组记录每个值为 1 的位置是否可以到达边界,可以为 true,反之为 false + boolean[][] visited = new boolean[rowSize][colSize]; + // 左侧边界和右侧边界查找 1 进行标记并进行深度优先遍历 + for (int row = 0; row < rowSize; row++) { + if (grid[row][0] == 1 && !visited[row][0]) { + visited[row][0] = true; + dfs(grid, row, 0, visited); + } + if (grid[row][colSize - 1] == 1 && !visited[row][colSize - 1]) { + visited[row][colSize - 1] = true; + dfs(grid, row, colSize - 1, visited); + } + } + // 上边界和下边界遍历,但是四个角不用遍历,因为上面已经遍历到了 + for (int col = 1; col < colSize - 1; col++) { + if (grid[0][col] == 1 && !visited[0][col]) { + visited[0][col] = true; + dfs(grid, 0, col, visited); + } + if (grid[rowSize - 1][col] == 1 && !visited[rowSize - 1][col]) { + visited[rowSize - 1][col] = true; + dfs(grid, rowSize - 1, col, visited); + } + } + // 查找没有标记过的 1,记录到 ans 中 + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + if (grid[row][col] == 1 && !visited[row][col]) ++ans; + } + } + return ans; + } +} +``` + +广度优先遍历(使用visited數組) + +```java +class Solution { + // 四个方向 + private static final int[][] position = {{-1, 0}, {0, 1}, {1, 0}, {0, -1}}; + + // 广度优先遍历,把可以通向边缘部分的 1 全部标记成 true + public void bfs(int[][] grid, Queue queue, boolean[][] visited) { + while (!queue.isEmpty()) { + int[] curPos = queue.poll(); + for (int[] current: position) { + int row = curPos[0] + current[0], col = curPos[1] + current[1]; + // 下标越界直接跳过 + if (row < 0 || row >= grid.length || col < 0 || col >= grid[0].length) + continue; + // 当前位置不是 1 或者已经被访问了就直接跳过 + if (visited[row][col] || grid[row][col] == 0) continue; + visited[row][col] = true; + queue.add(new int[]{row, col}); + } + } + } + + public int numEnclaves(int[][] grid) { + int rowSize = grid.length, colSize = grid[0].length, ans = 0; // ans 记录答案 + // 标记数组记录每个值为 1 的位置是否可以到达边界,可以为 true,反之为 false + boolean[][] visited = new boolean[rowSize][colSize]; + Queue queue = new ArrayDeque<>(); + // 搜索左侧边界和右侧边界查找 1 存入队列 + for (int row = 0; row < rowSize; row++) { + if (grid[row][0] == 1) { + visited[row][0] = true; + queue.add(new int[]{row, 0}); + } + if (grid[row][colSize - 1] == 1) { + visited[row][colSize - 1] = true; + queue.add(new int[]{row, colSize - 1}); + } + } + // 搜索上边界和下边界遍历,但是四个角不用遍历,因为上面已经遍历到了 + for (int col = 1; col < colSize - 1; col++) { + if (grid[0][col] == 1) { + visited[0][col] = true; + queue.add(new int[]{0, col}); + } + if (grid[rowSize - 1][col] == 1 && !visited[rowSize - 1][col]) { + visited[rowSize - 1][col] = true; + queue.add(new int[]{rowSize - 1, col}); + } + } + bfs(grid, queue, visited); // 广度优先遍历 + // 查找没有标记过的 1,记录到 ans 中 + for (int row = 0; row < rowSize; row++) { + for (int col = 0; col < colSize; col++) { + if (grid[row][col] == 1 && !visited[row][col]) ++ans; + } + } + return ans; + } +} +``` + +廣度优先遍历(空間優化(淹沒島嶼,沒有使用visited數組)) +```java +//BFS +class Solution { + int count = 0; + int[][] dir ={ + {0, 1}, + {1, 0}, + {-1, 0}, + {0, -1} + }; + private void bfs(int[][] grid, int x, int y){ + Queue que = new LinkedList<>(); + que.offer(x); + que.offer(y); + count++; + grid[x][y] = 0; + + while(!que.isEmpty()){ + int currX = que.poll(); + int currY = que.poll(); + + for(int i = 0; i < 4; i++){ + int nextX = currX + dir[i][0]; + int nextY = currY + dir[i][1]; + + if(nextX < 0 || nextY < 0 || nextX >= grid.length || nextY >= grid[0].length) + continue; + + if(grid[nextX][nextY] == 1){ + que.offer(nextX); + que.offer(nextY); + count++; + grid[nextX][nextY] = 0; + } + } + } + } + + public int numEnclaves(int[][] grid) { + for(int i = 0; i < grid.length; i++){ + if(grid[i][0] == 1) + bfs(grid, i, 0); + if(grid[i][grid[0].length - 1] == 1) + bfs(grid, i, grid[0].length - 1); + } + for(int j = 1; j < grid[0].length; j++){ + if(grid[0][j] == 1) + bfs(grid, 0 , j); + if(grid[grid.length - 1][j] == 1) + bfs(grid, grid.length - 1, j); + } + count = 0; + for(int i = 1; i < grid.length - 1; i++){ + for(int j = 1; j < grid[0].length - 1; j++){ + if(grid[i][j] == 1) + bfs(grid,i ,j); + } + } + return count; + } +} + + +``` + +### Python + +深度优先遍历 + +```Python +class Solution: + def __init__(self): + self.position = [[-1, 0], [0, 1], [1, 0], [0, -1]] # 四个方向 + + # 深度优先遍历,把可以通向边缘部分的 1 全部标记成 true + def dfs(self, grid: List[List[int]], row: int, col: int, visited: List[List[bool]]) -> None: + for current in self.position: + newRow, newCol = row + current[0], col + current[1] + # 索引下标越界 + if newRow < 0 or newRow >= len(grid) or newCol < 0 or newCol >= len(grid[0]): + continue + # 当前位置值不是 1 或者已经被访问过了 + if grid[newRow][newCol] == 0 or visited[newRow][newCol]: continue + visited[newRow][newCol] = True + self.dfs(grid, newRow, newCol, visited) + + def numEnclaves(self, grid: List[List[int]]) -> int: + rowSize, colSize, ans = len(grid), len(grid[0]), 0 + # 标记数组记录每个值为 1 的位置是否可以到达边界,可以为 True,反之为 False + visited = [[False for _ in range(colSize)] for _ in range(rowSize)] + # 搜索左边界和右边界,对值为 1 的位置进行深度优先遍历 + for row in range(rowSize): + if grid[row][0] == 1: + visited[row][0] = True + self.dfs(grid, row, 0, visited) + if grid[row][colSize - 1] == 1: + visited[row][colSize - 1] = True + self.dfs(grid, row, colSize - 1, visited) + # 搜索上边界和下边界,对值为 1 的位置进行深度优先遍历,但是四个角不需要,因为上面遍历过了 + for col in range(1, colSize - 1): + if grid[0][col] == 1: + visited[0][col] = True + self.dfs(grid, 0, col, visited) + if grid[rowSize - 1][col] == 1: + visited[rowSize - 1][col] = True + self.dfs(grid, rowSize - 1, col, visited) + # 找出矩阵中值为 1 但是没有被标记过的位置,记录答案 + for row in range(rowSize): + for col in range(colSize): + if grid[row][col] == 1 and not visited[row][col]: + ans += 1 + return ans +``` + +广度优先遍历 + +```Python +class Solution: + def __init__(self): + self.position = [[-1, 0], [0, 1], [1, 0], [0, -1]] # 四个方向 + + # 广度优先遍历,把可以通向边缘部分的 1 全部标记成 true + def bfs(self, grid: List[List[int]], queue: deque, visited: List[List[bool]]) -> None: + while queue: + curPos = queue.popleft() + for current in self.position: + row, col = curPos[0] + current[0], curPos[1] + current[1] + # 索引下标越界 + if row < 0 or row >= len(grid) or col < 0 or col >= len(grid[0]): continue + # 当前位置值不是 1 或者已经被访问过了 + if grid[row][col] == 0 or visited[row][col]: continue + visited[row][col] = True + queue.append([row, col]) + + + def numEnclaves(self, grid: List[List[int]]) -> int: + rowSize, colSize, ans = len(grid), len(grid[0]), 0 + # 标记数组记录每个值为 1 的位置是否可以到达边界,可以为 True,反之为 False + visited = [[False for _ in range(colSize)] for _ in range(rowSize)] + queue = deque() # 队列 + # 搜索左侧边界和右侧边界查找 1 存入队列 + for row in range(rowSize): + if grid[row][0] == 1: + visited[row][0] = True + queue.append([row, 0]) + if grid[row][colSize - 1] == 1: + visited[row][colSize - 1] = True + queue.append([row, colSize - 1]) + # 搜索上边界和下边界查找 1 存入队列,但是四个角不用遍历,因为上面已经遍历到了 + for col in range(1, colSize - 1): + if grid[0][col] == 1: + visited[0][col] = True + queue.append([0, col]) + if grid[rowSize - 1][col] == 1: + visited[rowSize - 1][col] = True + queue.append([rowSize - 1, col]) + self.bfs(grid, queue, visited) # 广度优先遍历 + # 找出矩阵中值为 1 但是没有被标记过的位置,记录答案 + for row in range(rowSize): + for col in range(colSize): + if grid[row][col] == 1 and not visited[row][col]: + ans += 1 + return ans +``` + +### Go + +dfs: + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} +var count int = 0 + +func numEnclaves(grid [][]int) int { + rows, cols := len(grid), len(grid[0]) + // 行 + for i := range grid[0] { + if grid[0][i] == 1 { + dfs(grid, 0, i) + } + if grid[rows-1][i] == 1 { + dfs(grid, rows-1, i) + } + } + // 列 + for j := range grid { + if grid[j][0] == 1 { + dfs(grid, j, 0) + } + if grid[j][cols-1] == 1 { + dfs(grid, j, cols-1) + } + } + count = 0 + for i := range grid { + for j := range grid[0] { + if grid[i][j] == 1 { + dfs(grid, i, j) + } + } + } + return count +} + +func dfs(grid [][]int, i, j int) { + grid[i][j] = 0 + count++ + for _, d := range DIRECTIONS { + x, y := i+d[0], j+d[1] + if x < 0 || x >= len(grid) || y < 0 || y >= len(grid[0]) { + continue + } + if grid[x][y] == 1 { + dfs(grid, x, y) + } + } +} +``` + +bfs: + +```go +var DIRECTIONS = [4][2]int{{-1, 0}, {0, -1}, {1, 0}, {0, 1}} +var count int = 0 + +func numEnclaves(grid [][]int) int { + rows, cols := len(grid), len(grid[0]) + // 行 + for i := range grid[0] { + if grid[0][i] == 1 { + bfs(grid, 0, i) + } + if grid[rows-1][i] == 1 { + bfs(grid, rows-1, i) + } + } + // 列 + for j := range grid { + if grid[j][0] == 1 { + bfs(grid, j, 0) + } + if grid[j][cols-1] == 1 { + bfs(grid, j, cols-1) + } + } + count = 0 + for i := range grid { + for j := range grid[0] { + if grid[i][j] == 1 { + bfs(grid, i, j) + } + } + } + return count +} + +func bfs(grid [][]int, i, j int) { + queue := [][]int{} + queue = append(queue, []int{i, j}) + grid[i][j] = 0 + count++ + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + for _, d := range DIRECTIONS { + x, y := cur[0]+d[0], cur[1]+d[1] + if x < 0 || x >= len(grid) || y < 0 || y >= len(grid[0]) { + continue + } + if grid[x][y] == 1 { + count++ + queue = append(queue, []int{x, y}) + grid[x][y] = 0 + } + } + } +} +``` + +### JavaScript + +```js +/** + * @param {number[][]} grid + * @return {number} + */ +var numEnclaves = function (grid) { + let row = grid.length; + let col = grid[0].length; + let count = 0; + + // Check the first and last row, if there is a 1, then change all the connected 1s to 0 and don't count them. + for (let j = 0; j < col; j++) { + if (grid[0][j] === 1) { + dfs(0, j, false); + } + if (grid[row - 1][j] === 1) { + dfs(row - 1, j, false); + } + } + + // Check the first and last column, if there is a 1, then change all the connected 1s to 0 and don't count them. + for (let i = 0; i < row; i++) { + if (grid[i][0] === 1) { + dfs(i, 0, false); + } + if (grid[i][col - 1] === 1) { + dfs(i, col - 1, false); + } + } + + // Check the rest of the grid, if there is a 1, then change all the connected 1s to 0 and count them. + for (let i = 1; i < row - 1; i++) { + for (let j = 1; j < col - 1; j++) { + dfs(i, j, true); + } + } + + function dfs(i, j, isCounting) { + let condition = i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] === 0; + + if (condition) return; + if (isCounting) count++; + + grid[i][j] = 0; + + dfs(i - 1, j, isCounting); + dfs(i + 1, j, isCounting); + dfs(i, j - 1, isCounting); + dfs(i, j + 1, isCounting); + } + + return count; +}; +``` + +### Rust + +dfs: + +```rust +impl Solution { + const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + + pub fn num_enclaves(mut grid: Vec>) -> i32 { + for i in 0..grid.len() { + for j in 0..grid[0].len() { + if (i == 0 || i == grid.len() - 1 || j == 0 || j == grid[0].len() - 1) + && grid[i][j] == 1 + { + Self::dfs(&mut grid, (i as i32, j as i32)); + } + } + } + grid.iter() + .map(|nums| nums.iter().filter(|&&num| num == 1).count() as i32) + .sum() + } + + pub fn dfs(grid: &mut [Vec], (x, y): (i32, i32)) { + grid[x as usize][y as usize] = 0; + for (dx, dy) in Self::DIRECTIONS { + let (nx, ny) = (x + dx, y + dy); + if nx < 0 || nx >= grid.len() as i32 || ny < 0 || ny >= grid[0].len() as i32 { + continue; + } + if grid[nx as usize][ny as usize] == 0 { + continue; + } + Self::dfs(grid, (nx, ny)); + } + } +} +``` + +bfs: + +```rust +use std::collections::VecDeque; +impl Solution { + const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + + pub fn num_enclaves(mut grid: Vec>) -> i32 { + for i in 0..grid.len() { + for j in 0..grid[0].len() { + if (i == 0 || i == grid.len() - 1 || j == 0 || j == grid[0].len() - 1) + && grid[i][j] == 1 + { + // Self::dfs(&mut grid, (i as i32, j as i32)); + Self::bfs(&mut grid, (i as i32, j as i32)); + } + } + } + grid.iter() + .map(|nums| nums.iter().filter(|&&num| num == 1).count() as i32) + .sum() + } + + pub fn bfs(grid: &mut [Vec], (x, y): (i32, i32)) { + let mut queue = VecDeque::new(); + queue.push_back((x, y)); + grid[x as usize][y as usize] = 0; + while let Some((cur_x, cur_y)) = queue.pop_front() { + for (dx, dy) in Self::DIRECTIONS { + let (nx, ny) = (cur_x + dx, cur_y + dy); + if nx < 0 || nx >= grid.len() as i32 || ny < 0 || ny >= grid[0].len() as i32 { + continue; + } + + if grid[nx as usize][ny as usize] == 0 { + continue; + } + queue.push_back((nx, ny)); + grid[nx as usize][ny as usize] = 0; + } + } + } +} +``` + + +## 类似题目 + +* 1254. 统计封闭岛屿的数目 + + + + diff --git "a/problems/1035.\344\270\215\347\233\270\344\272\244\347\232\204\347\272\277.md" "b/problems/1035.\344\270\215\347\233\270\344\272\244\347\232\204\347\272\277.md" new file mode 100755 index 0000000000..16bf869ea6 --- /dev/null +++ "b/problems/1035.\344\270\215\347\233\270\344\272\244\347\232\204\347\272\277.md" @@ -0,0 +1,278 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1035.不相交的线 + +[力扣题目链接](https://leetcode.cn/problems/uncrossed-lines/) + +在两条独立的水平线上按给定的顺序写下 nums1 和 nums2 中的整数。 + +现在,可以绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,这些直线需要同时满足: + +* nums1[i] == nums2[j] +* 且绘制的直线不与任何其他连线(非水平线)相交。 + +请注意,连线即使在端点也不能相交:每个数字只能属于一条连线。 + +以这种方法绘制线条,并返回可以绘制的最大连线数。 + + +![1035.不相交的线](https://file1.kamacoder.com/i/algo/2021032116363533.png) + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划之子序列问题,换汤不换药 | LeetCode:1035.不相交的线](https://www.bilibili.com/video/BV1h84y1x7MP),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +相信不少录友看到这道题目都没啥思路,我们来逐步分析一下。 + +绘制一些连接两个数字 nums1[i] 和 nums2[j] 的直线,只要 nums1[i] == nums2[j],且直线不能相交! + +直线不能相交,这就是说明在字符串nums1中 找到一个与字符串nums2相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,连接相同数字的直线就不会相交。 + +拿示例一nums1 = [1,4,2], nums2 = [1,2,4]为例,相交情况如图: + + +![](https://file1.kamacoder.com/i/algo/20210914145158.png) + +其实也就是说nums1和nums2的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串nums1中数字1的后面,那么数字4也应该在字符串nums2数字1的后面) + +这么分析完之后,大家可以发现:**本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!** + +那么本题就和我们刚刚讲过的这道题目[动态规划:1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html)就是一样一样的了。 + +一样到什么程度呢? 把字符串名字改一下,其他代码都不用改,直接copy过来就行了。 + +其实本题就是求最长公共子序列的长度,介于我们刚刚讲过[动态规划:1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html),所以本题我就不再做动规五部曲分析了。 + +如果大家有点遗忘了最长公共子序列,就再看一下这篇:[动态规划:1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html) + +本题代码如下: + +```CPP +class Solution { +public: + int maxUncrossedLines(vector& nums1, vector& nums2) { + vector> dp(nums1.size() + 1, vector(nums2.size() + 1, 0)); + for (int i = 1; i <= nums1.size(); i++) { + for (int j = 1; j <= nums2.size(); j++) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[nums1.size()][nums2.size()]; + } +}; +``` +* 时间复杂度: O(n * m) +* 空间复杂度: O(n * m) + + + +## 总结 + +看到代码大家也可以发现其实就是求两个字符串的最长公共子序列,但如果没有做过[1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html),本题其实还有很有难度的。 + +这是Carl为什么要先讲[1143.最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html)再讲本题,大家会发现一个正确的刷题顺序对算法学习是非常重要的! + +这也是Carl做了很多题目(包括ACM和力扣)才总结出来的规律,大家仔细体会一下哈。 + +## 其他语言版本 + +### Java: + +```java + class Solution { + public int maxUncrossedLines(int[] nums1, int[] nums2) { + int len1 = nums1.length; + int len2 = nums2.length; + int[][] dp = new int[len1 + 1][len2 + 1]; + + for (int i = 1; i <= len1; i++) { + for (int j = 1; j <= len2; j++) { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + + return dp[len1][len2]; + } +} +``` + +### Python: + +```python +class Solution: + def maxUncrossedLines(self, nums1: List[int], nums2: List[int]) -> int: + dp = [[0] * (len(nums2)+1) for _ in range(len(nums1)+1)] + for i in range(1, len(nums1)+1): + for j in range(1, len(nums2)+1): + if nums1[i-1] == nums2[j-1]: + dp[i][j] = dp[i-1][j-1] + 1 + else: + dp[i][j] = max(dp[i-1][j], dp[i][j-1]) + return dp[-1][-1] +``` + +### Go: + +```go +func maxUncrossedLines(nums1 []int, nums2 []int) int { + dp := make([][]int, len(nums1) + 1) + for i := range dp { + dp[i] = make([]int, len(nums2) + 1) + } + + for i := 1; i <= len(nums1); i++ { + for j := 1; j <= len(nums2); j++ { + if (nums1[i - 1] == nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } else { + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + } + } + } + return dp[len(nums1)][len(nums2)] + +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +### Rust: + +```rust +impl Solution { + pub fn max_uncrossed_lines(nums1: Vec, nums2: Vec) -> i32 { + let mut dp = vec![vec![0; nums2.len() + 1]; nums1.len() + 1]; + for (i, num1) in nums1.iter().enumerate() { + for (j, num2) in nums2.iter().enumerate() { + if num1 == num2 { + dp[i + 1][j + 1] = dp[i][j] + 1; + } else { + dp[i + 1][j + 1] = dp[i][j + 1].max(dp[i + 1][j]); + } + } + } + dp[nums1.len()][nums2.len()] + } +} +``` + +> 滚动数组 + +```rust +impl Solution { + pub fn max_uncrossed_lines(nums1: Vec, nums2: Vec) -> i32 { + let mut dp = vec![0; nums2.len() + 1]; + for num1 in nums1 { + let mut prev = 0; + for (j, &num2) in nums2.iter().enumerate() { + let temp = dp[j + 1]; + if num1 == num2 { + // 使用上一次的状态,防止重复计算 + dp[j + 1] = prev + 1; + } else { + dp[j + 1] = dp[j + 1].max(dp[j]); + } + prev = temp; + } + } + dp[nums2.len()] + } +} +``` + +### JavaScript: + +```javascript +const maxUncrossedLines = (nums1, nums2) => { + // 两个数组长度 + const [m, n] = [nums1.length, nums2.length]; + // 创建dp数组并都初始化为0 + const dp = new Array(m + 1).fill(0).map(x => new Array(n + 1).fill(0)); + for (let i = 1; i <= m; i++) { + for (let j = 1; j <= n; j++) { + // 根据两种情况更新dp[i][j] + if (nums1[i - 1] === nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + // 返回dp数组中右下角的元素 + return dp[m][n]; +}; +``` + +### TypeScript: + +> 二维数组 + +```typescript +function maxUncrossedLines(nums1: number[], nums2: number[]): number { + /** + dp[i][j]: nums1前i-1个,nums2前j-1个,最大连线数 + */ + const length1: number = nums1.length, + length2: number = nums2.length; + const dp: number[][] = new Array(length1 + 1).fill(0) + .map(_ => new Array(length2 + 1).fill(0)); + for (let i = 1; i <= length1; i++) { + for (let j = 1; j <= length2; j++) { + if (nums1[i - 1] === nums2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[length1][length2]; +}; +``` + +> 滚动数组 +```typescript +function maxUncrossedLines(nums1: number[], nums2: number[]): number { + const len1 = nums1.length + const len2 = nums2.length + + const dp: number[] = new Array(len2 + 1).fill(0) + + for (let i = 1; i <= len1; i++) { + let prev: number = 0; + let temp: number = 0; + for (let j = 1; j <= len2; j++) { + // 备份一下当前状态(经过上层迭代后的) + temp = dp[j] + // prev 相当于 dp[j-1](累加了上层的状态) + // 如果单纯 dp[j-1] 则不会包含上层状态 + if (nums1[i - 1] === nums2[j - 1]) dp[j] = prev + 1 + // dp[j] 表示之前的 dp[i][j-1],dp[j-1] 表示 dp[i-1][j] + else dp[j] = Math.max(dp[j], dp[j - 1]) + // 继续使用上一层状态更新参数用于当前层下一个状态 + prev = temp + } + } + return dp[len2] +} +``` + + + diff --git "a/problems/1047.\345\210\240\351\231\244\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\346\211\200\346\234\211\347\233\270\351\202\273\351\207\215\345\244\215\351\241\271.md" "b/problems/1047.\345\210\240\351\231\244\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\346\211\200\346\234\211\347\233\270\351\202\273\351\207\215\345\244\215\351\241\271.md" new file mode 100755 index 0000000000..36702194be --- /dev/null +++ "b/problems/1047.\345\210\240\351\231\244\345\255\227\347\254\246\344\270\262\344\270\255\347\232\204\346\211\200\346\234\211\347\233\270\351\202\273\351\207\215\345\244\215\351\241\271.md" @@ -0,0 +1,523 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 匹配问题都是栈的强项 + +# 1047. 删除字符串中的所有相邻重复项 + +[力扣题目链接](https://leetcode.cn/problems/remove-all-adjacent-duplicates-in-string/) + +给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。 + +在 S 上反复执行重复项删除操作,直到无法继续删除。 + +在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。 + + +示例: +* 输入:"abbaca" +* 输出:"ca" +* 解释:例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。 + + +提示: +* 1 <= S.length <= 20000 +* S 仅由小写英文字母组成。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[栈的好戏还要继续!| LeetCode:1047. 删除字符串中的所有相邻重复项](https://www.bilibili.com/video/BV12a411P7mw),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +### 正题 + +本题要删除相邻相同元素,相对于[20. 有效的括号](https://programmercarl.com/0020.%E6%9C%89%E6%95%88%E7%9A%84%E6%8B%AC%E5%8F%B7.html)来说其实也是匹配问题,20. 有效的括号 是匹配左右括号,本题是匹配相邻元素,最后都是做消除的操作。 + +本题也是用栈来解决的经典题目。 + +那么栈里应该放的是什么元素呢? + +我们在删除相邻重复项的时候,其实就是要知道当前遍历的这个元素,我们在前一位是不是遍历过一样数值的元素,那么如何记录前面遍历过的元素呢? + +所以就是用栈来存放,那么栈的目的,就是存放遍历过的元素,当遍历当前的这个元素的时候,去栈里看一下我们是不是遍历过相同数值的相邻元素。 + +然后再去做对应的消除操作。 如动画所示: + +![1047.删除字符串中的所有相邻重复项](https://file1.kamacoder.com/i/algo/1047.删除字符串中的所有相邻重复项.gif) + +从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒序的,所以再对字符串进行反转一下,就得到了最终的结果。 + +C++代码 : + +```CPP +class Solution { +public: + string removeDuplicates(string S) { + stack st; + for (char s : S) { + if (st.empty() || s != st.top()) { + st.push(s); + } else { + st.pop(); // s 与 st.top()相等的情况 + } + } + string result = ""; + while (!st.empty()) { // 将栈中元素放到result字符串汇总 + result += st.top(); + st.pop(); + } + reverse (result.begin(), result.end()); // 此时字符串需要反转一下 + return result; + + } +}; +``` +* 时间复杂度: O(n) +* 空间复杂度: O(n) + + +当然可以拿字符串直接作为栈,这样省去了栈还要转为字符串的操作。 + +代码如下: + +```CPP +class Solution { +public: + string removeDuplicates(string S) { + string result; + for(char s : S) { + if(result.empty() || result.back() != s) { + result.push_back(s); + } + else { + result.pop_back(); + } + } + return result; + } +}; +``` +* 时间复杂度: O(n) +* 空间复杂度: O(1),返回值不计空间复杂度 + +### 题外话 + +这道题目就像是我们玩过的游戏对对碰,如果相同的元素挨在一起就要消除。 + +可能我们在玩游戏的时候感觉理所当然应该消除,但程序又怎么知道该如何消除呢,特别是消除之后又有新的元素可能挨在一起。 + +此时游戏的后端逻辑就可以用一个栈来实现(我没有实际考察对对碰或者爱消除游戏的代码实现,仅从原理上进行推断)。 + +游戏开发可能使用栈结构,编程语言的一些功能实现也会使用栈结构,实现函数递归调用就需要栈,但不是每种编程语言都支持递归,例如: + +**递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中**,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。 + +相信大家应该遇到过一种错误就是栈溢出,系统输出的异常是`Segmentation fault`(当然不是所有的`Segmentation fault` 都是栈溢出导致的) ,如果你使用了递归,就要想一想是不是无限递归了,那么系统调用栈就会溢出。 + +而且**在企业项目开发中,尽量不要使用递归**!在项目比较大的时候,由于参数多,全局变量等等,使用递归很容易判断不充分return的条件,非常容易无限递归(或者递归层级过深),**造成栈溢出错误(这种问题还不好排查!)** + + + +## 其他语言版本 + +### Java: + +使用 Deque 作为堆栈 +```Java +class Solution { + public String removeDuplicates(String S) { + //ArrayDeque会比LinkedList在除了删除元素这一点外会快一点 + //参考:https://stackoverflow.com/questions/6163166/why-is-arraydeque-better-than-linkedlist + ArrayDeque deque = new ArrayDeque<>(); + char ch; + for (int i = 0; i < S.length(); i++) { + ch = S.charAt(i); + if (deque.isEmpty() || deque.peek() != ch) { + deque.push(ch); + } else { + deque.pop(); + } + } + String str = ""; + //剩余的元素即为不重复的元素 + while (!deque.isEmpty()) { + str = deque.pop() + str; + } + return str; + } +} +``` +拿字符串直接作为栈,省去了栈还要转为字符串的操作。 +```Java +class Solution { + public String removeDuplicates(String s) { + // 将 res 当做栈 + // 也可以用 StringBuilder 来修改字符串,速度更快 + // StringBuilder res = new StringBuilder(); + StringBuffer res = new StringBuffer(); + // top为 res 的长度 + int top = -1; + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + // 当 top >= 0,即栈中有字符时,当前字符如果和栈中字符相等,弹出栈顶字符,同时 top-- + if (top >= 0 && res.charAt(top) == c) { + res.deleteCharAt(top); + top--; + // 否则,将该字符 入栈,同时top++ + } else { + res.append(c); + top++; + } + } + return res.toString(); + } +} +``` + +拓展:双指针 +```java +class Solution { + public String removeDuplicates(String s) { + char[] ch = s.toCharArray(); + int fast = 0; + int slow = 0; + while(fast < s.length()){ + // 直接用fast指针覆盖slow指针的值 + ch[slow] = ch[fast]; + // 遇到前后相同值的,就跳过,即slow指针后退一步,下次循环就可以直接被覆盖掉了 + if(slow > 0 && ch[slow] == ch[slow - 1]){ + slow--; + }else{ + slow++; + } + fast++; + } + return new String(ch,0,slow); + } +} +``` + +### Python: + +```python +# 方法一,使用栈 +class Solution: + def removeDuplicates(self, s: str) -> str: + res = list() + for item in s: + if res and res[-1] == item: + res.pop() + else: + res.append(item) + return "".join(res) # 字符串拼接 +``` + +```python +# 方法二,使用双指针模拟栈,如果不让用栈可以作为备选方法。 +class Solution: + def removeDuplicates(self, s: str) -> str: + res = list(s) + slow = fast = 0 + length = len(res) + + while fast < length: + # 如果一样直接换,不一样会把后面的填在slow的位置 + res[slow] = res[fast] + + # 如果发现和前一个一样,就退一格指针 + if slow > 0 and res[slow] == res[slow - 1]: + slow -= 1 + else: + slow += 1 + fast += 1 + + return ''.join(res[0: slow]) +``` + +### Go: + +使用栈 +```go +func removeDuplicates(s string) string { + stack := make([]rune, 0) + for _, val := range s { + if len(stack) == 0 || val != stack[len(stack)-1] { + stack = append(stack, val) + } else { + stack = stack[:len(stack)-1] + } + } + var res []rune + for len(stack) != 0 { // 将栈中元素放到result字符串汇总 + res = append(res, stack[len(stack)-1]) + stack = stack[:len(stack)-1] + } + // 此时字符串需要反转一下 + l, r := 0, len(res)-1 + for l < r { + res[l], res[r] = res[r], res[l] + l++ + r-- + } + return string(res) +} +``` +拿字符串直接作为栈,省去了栈还要转为字符串的操作 +```go +func removeDuplicates(s string) string { + var stack []byte + for i := 0; i < len(s);i++ { + // 栈不空 且 与栈顶元素不等 + if len(stack) > 0 && stack[len(stack)-1] == s[i] { + // 弹出栈顶元素 并 忽略当前元素(s[i]) + stack = stack[:len(stack)-1] + }else{ + // 入栈 + stack = append(stack, s[i]) + } + } + return string(stack) +} +``` + +### JavaScript: + +法一:使用栈 + +```js +var removeDuplicates = function(s) { + const result = [] + for(const i of s){ + if(i === result[result.length-1]){ + result.pop() + }else{ + result.push(i) + } + } + return result.join('') +}; +``` + +法二:双指针(模拟栈) + +```js +// 原地解法(双指针模拟栈) +var removeDuplicates = function(s) { + s = [...s]; + let top = -1; // 指向栈顶元素的下标 + for(let i = 0; i < s.length; i++) { + if(top === -1 || s[top] !== s[i]) { // top === -1 即空栈 + s[++top] = s[i]; // 入栈 + } else { + top--; // 推出栈 + } + } + s.length = top + 1; // 栈顶元素下标 + 1 为栈的长度 + return s.join(''); +}; +``` + +### TypeScript: + +```typescript +function removeDuplicates(s: string): string { + const helperStack: string[] = []; + let i: number = 0; + while (i < s.length) { + let top: string = helperStack[helperStack.length - 1]; + if (top === s[i]) { + helperStack.pop(); + } else { + helperStack.push(s[i]); + } + i++; + } + let res: string = ''; + while (helperStack.length > 0) { + res = helperStack.pop() + res; + } + return res; +}; +``` + +### C: +方法一:使用栈 + +```c +char * removeDuplicates(char * s){ + //求出字符串长度 + int strLength = strlen(s); + //开辟栈空间。栈空间长度应为字符串长度+1(为了存放字符串结束标志'\0') + char* stack = (char*)malloc(sizeof(char) * strLength + 1); + int stackTop = 0; + + int index = 0; + //遍历整个字符串 + while(index < strLength) { + //取出当前index对应字母,之后index+1 + char letter = s[index++]; + //若栈中有元素,且栈顶字母等于当前字母(两字母相邻)。将栈顶元素弹出 + if(stackTop > 0 && letter == stack[stackTop - 1]) + stackTop--; + //否则将字母入栈 + else + stack[stackTop++] = letter; + } + //存放字符串结束标志'\0' + stack[stackTop] = '\0'; + //返回栈本身作为字符串 + return stack; +} +``` +方法二:双指针法 +```c +char * removeDuplicates(char * s){ + //创建快慢指针 + int fast = 0; + int slow = 0; + //求出字符串长度 + int strLength = strlen(s); + //遍历字符串 + while(fast < strLength) { + //将当前slow指向字符改为fast指向字符。fast指针+1 + char letter = s[slow] = s[fast++]; + //若慢指针大于0,且慢指针指向元素等于字符串中前一位元素,删除慢指针指向当前元素 + if(slow > 0 && letter == s[slow - 1]) + slow--; + else + slow++; + } + //在字符串结束加入字符串结束标志'\0' + s[slow] = 0; + return s; +} +``` + +### Swift: + +```swift +func removeDuplicates(_ s: String) -> String { + var stack = [Character]() + for c in s { + if stack.last == c { + stack.removeLast() + } else { + stack.append(c) + } + } + return String(stack) +} +``` + +### C#: + +```csharp +public string RemoveDuplicates(string s) { + //拿字符串直接作为栈,省去了栈还要转为字符串的操作 + StringBuilder res = new StringBuilder(); + + foreach(char c in s){ + if(res.Length > 0 && res[res.Length-1] == c){ + res.Remove(res.Length-1, 1); + }else{ + res.Append(c); + } + } + + return res.ToString(); + } +``` + +### PHP: + +```php +class Solution { + function removeDuplicates($s) { + $stack = new SplStack(); + for($i=0;$iisEmpty() || $s[$i] != $stack->top()){ + $stack->push($s[$i]); + }else{ + $stack->pop(); + } + } + + $result = ""; + while(!$stack->isEmpty()){ + $result.= $stack->top(); + $stack->pop(); + } + + // 此时字符串需要反转一下 + return strrev($result); + } +} +``` + +### Scala: + +```scala +object Solution { + import scala.collection.mutable + def removeDuplicates(s: String): String = { + var stack = mutable.Stack[Int]() + var str = "" // 保存最终结果 + for (i <- s.indices) { + var tmp = s(i) + // 如果栈非空并且栈顶元素等于当前字符,那么删掉栈顶和字符串最后一个元素 + if (!stack.isEmpty && tmp == stack.head) { + str = str.take(str.length - 1) + stack.pop() + } else { + stack.push(tmp) + str += tmp + } + } + str + } +} +``` + +### Rust: + +```rust +impl Solution { + pub fn remove_duplicates(s: String) -> String { + let mut stack = vec![]; + let mut chars: Vec = s.chars().collect(); + while let Some(c) = chars.pop() { + if stack.is_empty() || stack[stack.len() - 1] != c { + stack.push(c); + } else { + stack.pop(); + } + } + stack.into_iter().rev().collect() + } +} +``` + +### Ruby + +```ruby +def remove_duplicates(s) + #数组模拟栈 + stack = [] + s.each_char do |chr| + if stack.empty? + stack.push chr + else + head = stack.pop + #重新进栈 + stack.push head, chr if head != chr + end + end + + return stack.join +end +``` + + diff --git "a/problems/1049.\346\234\200\345\220\216\344\270\200\345\235\227\347\237\263\345\244\264\347\232\204\351\207\215\351\207\217II.md" "b/problems/1049.\346\234\200\345\220\216\344\270\200\345\235\227\347\237\263\345\244\264\347\232\204\351\207\215\351\207\217II.md" new file mode 100755 index 0000000000..ddc9f313db --- /dev/null +++ "b/problems/1049.\346\234\200\345\220\216\344\270\200\345\235\227\347\237\263\345\244\264\347\232\204\351\207\215\351\207\217II.md" @@ -0,0 +1,535 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1049.最后一块石头的重量II + +[力扣题目链接](https://leetcode.cn/problems/last-stone-weight-ii/) + +题目难度:中等 + +有一堆石头,每块石头的重量都是正整数。 + +每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下: + +如果 x == y,那么两块石头都会被完全粉碎; + +如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。 + +最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。 + +示例: +* 输入:[2,7,4,1,8,1] +* 输出:1 + +解释: +* 组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1], +* 组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1], +* 组合 2 和 1,得到 1,所以数组转化为 [1,1,1], +* 组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。 + +提示: + +* 1 <= stones.length <= 30 +* 1 <= stones[i] <= 1000 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[这个背包最多能装多少?LeetCode:1049.最后一块石头的重量II](https://www.bilibili.com/video/BV14M411C7oV/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +如果对背包问题不熟悉的话先看这两篇: + +* [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) +* [01背包理论基础(一维数组)](https://programmercarl.com/背包理论基础01背包-2.html) + +本题其实是尽量让石头分成重量相同的两堆(尽可能相同),相撞之后剩下的石头就是最小的。 + +一堆的石头重量是sum,那么我们就尽可能拼成 重量为 sum / 2 的石头堆。 这样剩下的石头堆也是 尽可能接近 sum/2 的重量。 +那么此时问题就是有一堆石头,每个石头都有自己的重量,是否可以 装满 最大重量为 sum / 2的背包。 + +看到这里,大家是否感觉和昨天讲解的 [416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)非常像了,简直就是同一道题。 + +本题**这样就化解成01背包问题了**。 + +**[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html) 是求背包是否正好装满,而本题是求背包最多能装多少**。 + +物品就是石头,物品的重量为stones[i],物品的价值也为stones[i]。 + +接下来进行动规五步曲: + +### 1. 确定dp数组以及下标的含义 + +**dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]**。 + +相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] 。 + +“最多可以装的价值为 dp[j]” 等同于 “最多可以背的重量为dp[j]” + +### 2. 确定递推公式 + +01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + +本题则是:**dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);** + +### 3. dp数组如何初始化 + +既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。 + +因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。 + +而我们要求的target其实只是最大重量的一半,所以dp数组开到15000大小就可以了。 + +当然也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小。 + +我这里就直接用15000了。 + +接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖。 + +代码为: + +``` +vector dp(15001, 0); +``` + +### 4. 确定遍历顺序 + + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历! + +代码如下: + +```CPP +for (int i = 0; i < stones.size(); i++) { // 遍历物品 + for (int j = target; j >= stones[i]; j--) { // 遍历背包 + dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]); + } +} + +``` + +### 5. 举例推导dp数组 + +举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下: + +![1049.最后一块石头的重量II](https://file1.kamacoder.com/i/algo/20210121115805904.jpg) + + +最后dp[target]里是容量为target的背包所能背的最大重量。 + +那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。 + +**在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的**。 + +那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int lastStoneWeightII(vector& stones) { + vector dp(15001, 0); + int sum = 0; + for (int i = 0; i < stones.size(); i++) sum += stones[i]; + int target = sum / 2; + for (int i = 0; i < stones.size(); i++) { // 遍历物品 + for (int j = target; j >= stones[i]; j--) { // 遍历背包 + dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]); + } + } + return sum - dp[target] - dp[target]; + } +}; + +``` + +* 时间复杂度:O(m × n) , m是石头总重量(准确的说是总重量的一半),n为石头块数 +* 空间复杂度:O(m) + +## 总结 + +本题其实和[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)几乎是一样的,只是最后对dp[target]的处理方式不同。 + +**[416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)相当于是求背包是否正好装满,而本题是求背包最多能装多少**。 + + +## 其他语言版本 + + +### Java: + +一维数组版本 +```Java +class Solution { + public int lastStoneWeightII(int[] stones) { + int sum = 0; + for (int i : stones) { + sum += i; + } + int target = sum >> 1; + //初始化dp数组 + int[] dp = new int[target + 1]; + for (int i = 0; i < stones.length; i++) { + //采用倒序 + for (int j = target; j >= stones[i]; j--) { + //两种情况,要么放,要么不放 + dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]); + } + } + return sum - 2 * dp[target]; + } +} +``` +二维数组版本(便于理解) +```Java +class Solution { + public int lastStoneWeightII(int[] stones) { + int sum = 0; + for (int s : stones) { + sum += s; + } + + int target = sum / 2; + //初始化,dp[i][j]为可以放0-i物品,背包容量为j的情况下背包中的最大价值 + int[][] dp = new int[stones.length][target + 1]; + //dp[i][0]默认初始化为0 + //dp[0][j]取决于stones[0] + for (int j = stones[0]; j <= target; j++) { + dp[0][j] = stones[0]; + } + + for (int i = 1; i < stones.length; i++) { + for (int j = 1; j <= target; j++) {//注意是等于 + if (j >= stones[i]) { + //不放:dp[i - 1][j] 放:dp[i - 1][j - stones[i]] + stones[i] + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - stones[i]] + stones[i]); + } else { + dp[i][j] = dp[i - 1][j]; + } + } + } + + System.out.println(dp[stones.length - 1][target]); + return (sum - dp[stones.length - 1][target]) - dp[stones.length - 1][target]; + } +} + +``` + +### Python: +卡哥版 +```python +class Solution: + def lastStoneWeightII(self, stones: List[int]) -> int: + dp = [0] * 15001 + total_sum = sum(stones) + target = total_sum // 2 + + for stone in stones: # 遍历物品 + for j in range(target, stone - 1, -1): # 遍历背包 + dp[j] = max(dp[j], dp[j - stone] + stone) + + return total_sum - dp[target] - dp[target] + +``` + +卡哥版(简化版) +```python +class Solution: + def lastStoneWeightII(self, stones): + total_sum = sum(stones) + target = total_sum // 2 + dp = [0] * (target + 1) + for stone in stones: + for j in range(target, stone - 1, -1): + dp[j] = max(dp[j], dp[j - stone] + stone) + return total_sum - 2* dp[-1] + + +``` +二维DP版 +```python +class Solution: + def lastStoneWeightII(self, stones: List[int]) -> int: + total_sum = sum(stones) + target = total_sum // 2 + + # 创建二维dp数组,行数为石头的数量加1,列数为target加1 + # dp[i][j]表示前i个石头能否组成总重量为j + dp = [[False] * (target + 1) for _ in range(len(stones) + 1)] + + # 初始化第一列,表示总重量为0时,前i个石头都能组成 + for i in range(len(stones) + 1): + dp[i][0] = True + + for i in range(1, len(stones) + 1): + for j in range(1, target + 1): + # 如果当前石头重量大于当前目标重量j,则无法选择该石头 + if stones[i - 1] > j: + dp[i][j] = dp[i - 1][j] + else: + # 可选择该石头或不选择该石头 + dp[i][j] = dp[i - 1][j] or dp[i - 1][j - stones[i - 1]] + + # 找到最大的重量i,使得dp[len(stones)][i]为True + # 返回总重量减去两倍的最接近总重量一半的重量 + for i in range(target, -1, -1): + if dp[len(stones)][i]: + return total_sum - 2 * i + + return 0 + + +``` +一维DP版 +```python +class Solution: + def lastStoneWeightII(self, stones): + total_sum = sum(stones) + target = total_sum // 2 + dp = [False] * (target + 1) + dp[0] = True + + for stone in stones: + for j in range(target, stone - 1, -1): + # 判断当前重量是否可以通过选择之前的石头得到或选择当前石头和之前的石头得到 + dp[j] = dp[j] or dp[j - stone] + + for i in range(target, -1, -1): + if dp[i]: + # 返回剩余石头的重量,即总重量减去两倍的最接近总重量一半的重量 + return total_sum - 2 * i + + return 0 + + + +``` +### Go: + +一维dp +```go +func lastStoneWeightII(stones []int) int { + // 15001 = 30 * 1000 /2 +1 + dp := make([]int, 15001) + // 求target + sum := 0 + for _, v := range stones { + sum += v + } + target := sum / 2 + // 遍历顺序 + for i := 0; i < len(stones); i++ { + for j := target; j >= stones[i]; j-- { + // 推导公式 + dp[j] = max(dp[j], dp[j-stones[i]]+stones[i]) + } + } + return sum - 2 * dp[target] +} + +func max(a, b int) int { + if a > b { + return a + } + return b +} +``` + +二维dp +```go +func lastStoneWeightII(stones []int) int { + sum := 0 + for _, val := range stones { + sum += val + } + target := sum / 2 + + dp := make([][]int, len(stones)) + for i := range dp { + dp[i] = make([]int, target + 1) + } + for j := stones[0]; j <= target; j++ { + dp[0][j] = stones[0] + } + + for i := 1; i < len(stones); i++ { + for j := 0; j <= target; j++ { + if stones[i] > j { + dp[i][j] = dp[i-1][j] + } else { + dp[i][j] = max(dp[i-1][j], dp[i-1][j-stones[i]] + stones[i]) + } + } + } + return (sum - dp[len(stones)-1][target]) - dp[len(stones)-1][target] +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} +``` + +### JavaScript: + +```javascript +/** + * @param {number[]} stones + * @return {number} + */ +var lastStoneWeightII = function (stones) { + let sum = stones.reduce((s, n) => s + n); + + let dpLen = Math.floor(sum / 2); + let dp = new Array(dpLen + 1).fill(0); + + for (let i = 0; i < stones.length; ++i) { + for (let j = dpLen; j >= stones[i]; --j) { + dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]); + } + } + + return sum - dp[dpLen] - dp[dpLen]; +}; +``` + +### C: +```c +#define MAX(a, b) (((a) > (b)) ? (a) : (b)) + +int getSum(int *stones, int stoneSize) { + int sum = 0, i; + for (i = 0; i < stoneSize; ++i) + sum += stones[i]; + return sum; +} + +int lastStoneWeightII(int* stones, int stonesSize){ + int sum = getSum(stones, stonesSize); + int target = sum / 2; + int i, j; + + // 初始化dp数组 + int *dp = (int*)malloc(sizeof(int) * (target + 1)); + memset(dp, 0, sizeof(int) * (target + 1)); + for (j = stones[0]; j <= target; ++j) + dp[j] = stones[0]; + + // 递推公式:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]) + for (i = 1; i < stonesSize; ++i) { + for (j = target; j >= stones[i]; --j) + dp[j] = MAX(dp[j], dp[j - stones[i]] + stones[i]); + } + return sum - dp[target] - dp[target]; +} +``` + +### TypeScript: + +```ts +function lastStoneWeightII(stones: number[]): number { + const sum: number = stones.reduce((a: number, b:number): number => a + b); + const target: number = Math.floor(sum / 2); + const n: number = stones.length; + // dp[j]表示容量(总数和)为j的背包所能装下的数(下标[0, i]之间任意取)的总和(<= 容量)的最大值 + const dp: number[] = new Array(target + 1).fill(0); + for (let i: number = 0; i < n; i++ ) { + for (let j: number = target; j >= stones[i]; j--) { + dp[j] = Math.max(dp[j], dp[j - stones[i]] + stones[i]); + } + } + return sum - dp[target] - dp[target]; +}; +``` + +### Scala: + +滚动数组: +```scala +object Solution { + def lastStoneWeightII(stones: Array[Int]): Int = { + var sum = stones.sum + var half = sum / 2 + var dp = new Array[Int](half + 1) + + // 遍历 + for (i <- 0 until stones.length; j <- half to stones(i) by -1) { + dp(j) = math.max(dp(j), dp(j - stones(i)) + stones(i)) + } + + sum - 2 * dp(half) + } +} +``` + +二维数组: +```scala +object Solution { + def lastStoneWeightII(stones: Array[Int]): Int = { + var sum = stones.sum + var half = sum / 2 + var dp = Array.ofDim[Int](stones.length, half + 1) + + // 初始化 + for (j <- stones(0) to half) dp(0)(j) = stones(0) + + // 遍历 + for (i <- 1 until stones.length; j <- 1 to half) { + if (j - stones(i) >= 0) dp(i)(j) = stones(i) + dp(i - 1)(j - stones(i)) + dp(i)(j) = math.max(dp(i)(j), dp(i - 1)(j)) + } + + sum - 2 * dp(stones.length - 1)(half) + } +} +``` + +### Rust: + +```rust +impl Solution { + pub fn last_stone_weight_ii(stones: Vec) -> i32 { + let sum = stones.iter().sum::(); + let target = sum as usize / 2; + let mut dp = vec![0; target + 1]; + for s in stones { + for j in (s as usize..=target).rev() { + dp[j] = dp[j].max(dp[j - s as usize] + s); + } + } + sum - dp[target] * 2 + } +} +``` +### C# +```csharp +public class Solution +{ + public int LastStoneWeightII(int[] stones) + { + int[] dp = new int[15001]; + int sum = 0; + foreach (int stone in stones) + { + sum += stone; + } + int target = sum / 2; + for (int i = 0; i < stones.Length; i++) + { + for (int j = target; j >= stones[i]; j--) + { + dp[j] = Math.Max(dp[j], dp[j - stones[i]] + stones[i]); + } + } + return sum - 2 * dp[target]; + } +} +``` + + diff --git "a/problems/1143.\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" "b/problems/1143.\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" new file mode 100755 index 0000000000..424f403938 --- /dev/null +++ "b/problems/1143.\346\234\200\351\225\277\345\205\254\345\205\261\345\255\220\345\272\217\345\210\227.md" @@ -0,0 +1,420 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1143.最长公共子序列 + +[力扣题目链接](https://leetcode.cn/problems/longest-common-subsequence/) + +给定两个字符串 text1 和 text2,返回这两个字符串的最长公共子序列的长度。 + +一个字符串的 子序列 是指这样一个新的字符串:它是由原字符串在不改变字符的相对顺序的情况下删除某些字符(也可以不删除任何字符)后组成的新字符串。 + +例如,"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。两个字符串的「公共子序列」是这两个字符串所共同拥有的子序列。 + +若这两个字符串没有公共子序列,则返回 0。 + +示例 1: + +* 输入:text1 = "abcde", text2 = "ace" +* 输出:3 +* 解释:最长公共子序列是 "ace",它的长度为 3。 + +示例 2: +* 输入:text1 = "abc", text2 = "abc" +* 输出:3 +* 解释:最长公共子序列是 "abc",它的长度为 3。 + +示例 3: +* 输入:text1 = "abc", text2 = "def" +* 输出:0 +* 解释:两个字符串没有公共子序列,返回 0。 + +提示: +* 1 <= text1.length <= 1000 +* 1 <= text2.length <= 1000 +输入的字符串只含有小写英文字符。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划子序列问题经典题目 | LeetCode:1143.最长公共子序列](https://www.bilibili.com/video/BV1ye4y1L7CQ),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本题和[动态规划:718. 最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html)区别在于这里不要求是连续的了,但要有相对顺序,即:"ace" 是 "abcde" 的子序列,但 "aec" 不是 "abcde" 的子序列。 + +继续动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j]:长度为[0, i - 1]的字符串text1与长度为[0, j - 1]的字符串text2的最长公共子序列为dp[i][j] + +有同学会问:为什么要定义长度为[0, i - 1]的字符串text1,定义为长度为[0, i]的字符串text1不香么? + +这样定义是为了后面代码实现方便,如果非要定义为长度为[0, i]的字符串text1也可以,我在 [动态规划:718. 最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html) 中的「拓展」里 详细讲解了区别所在,其实就是简化了dp数组第一行和第一列的初始化逻辑。 + +2. 确定递推公式 + +主要就是两大情况: text1[i - 1] 与 text2[j - 1]相同,text1[i - 1] 与 text2[j - 1]不相同 + +如果text1[i - 1] 与 text2[j - 1]相同,那么找到了一个公共元素,所以dp[i][j] = dp[i - 1][j - 1] + 1; + +如果text1[i - 1] 与 text2[j - 1]不相同,那就看看text1[0, i - 2]与text2[0, j - 1]的最长公共子序列 和 text1[0, i - 1]与text2[0, j - 2]的最长公共子序列,取最大的。 + +即:dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); + +代码如下: + +```CPP +if (text1[i - 1] == text2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; +} else { + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); +} +``` + +3. dp数组如何初始化 + +先看看dp[i][0]应该是多少呢? + +text1[0, i-1]和空串的最长公共子序列自然是0,所以dp[i][0] = 0; + +同理dp[0][j]也是0。 + +其他下标都是随着递推公式逐步覆盖,初始为多少都可以,那么就统一初始为0。 + +代码: + +``` +vector> dp(text1.size() + 1, vector(text2.size() + 1, 0)); +``` + +4. 确定遍历顺序 + +从递推公式,可以看出,有三个方向可以推出dp[i][j],如图: + +![1143.最长公共子序列](https://file1.kamacoder.com/i/algo/20210204115139616.jpg) + +那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。 + +5. 举例推导dp数组 + +以输入:text1 = "abcde", text2 = "ace" 为例,dp状态如图: + + +![1143.最长公共子序列1](https://file1.kamacoder.com/i/algo/20210210150215918.jpg) + +最后红框dp[text1.size()][text2.size()]为最终结果 + +以上分析完毕,C++代码如下: + +```CPP +class Solution { +public: + int longestCommonSubsequence(string text1, string text2) { + vector> dp(text1.size() + 1, vector(text2.size() + 1, 0)); + for (int i = 1; i <= text1.size(); i++) { + for (int j = 1; j <= text2.size(); j++) { + if (text1[i - 1] == text2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[text1.size()][text2.size()]; + } +}; +``` +* 时间复杂度: O(n * m),其中 n 和 m 分别为 text1 和 text2 的长度 +* 空间复杂度: O(n * m) + + + +## 其他语言版本 + +### Java: + +```java +/* + 二维dp数组 +*/ +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + // char[] char1 = text1.toCharArray(); + // char[] char2 = text2.toCharArray(); + // 可以在一開始的時候就先把text1, text2 轉成char[],之後就不需要有這麼多爲了處理字串的調整 + // 就可以和卡哥的code更一致 + + int[][] dp = new int[text1.length() + 1][text2.length() + 1]; // 先对dp数组做初始化操作 + for (int i = 1 ; i <= text1.length() ; i++) { + char char1 = text1.charAt(i - 1); + for (int j = 1; j <= text2.length(); j++) { + char char2 = text2.charAt(j - 1); + if (char1 == char2) { // 开始列出状态转移方程 + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[text1.length()][text2.length()]; + } +} + + + +/** + 一维dp数组 +*/ +class Solution { + public int longestCommonSubsequence(String text1, String text2) { + int n1 = text1.length(); + int n2 = text2.length(); + + // 多从二维dp数组过程分析 + // 关键在于 如果记录 dp[i - 1][j - 1] + // 因为 dp[i - 1][j - 1] dp[j - 1] <=> dp[i][j - 1] + int [] dp = new int[n2 + 1]; + + for(int i = 1; i <= n1; i++){ + + // 这里pre相当于 dp[i - 1][j - 1] + int pre = dp[0]; + for(int j = 1; j <= n2; j++){ + + //用于给pre赋值 + int cur = dp[j]; + if(text1.charAt(i - 1) == text2.charAt(j - 1)){ + //这里pre相当于dp[i - 1][j - 1] 千万不能用dp[j - 1] !! + dp[j] = pre + 1; + } else{ + // dp[j] 相当于 dp[i - 1][j] + // dp[j - 1] 相当于 dp[i][j - 1] + dp[j] = Math.max(dp[j], dp[j - 1]); + } + + //更新dp[i - 1][j - 1], 为下次使用做准备 + pre = cur; + } + } + + return dp[n2]; + } +} +``` + +### Python: +2维DP + +```python +class Solution: + def longestCommonSubsequence(self, text1: str, text2: str) -> int: + # 创建一个二维数组 dp,用于存储最长公共子序列的长度 + dp = [[0] * (len(text2) + 1) for _ in range(len(text1) + 1)] + + # 遍历 text1 和 text2,填充 dp 数组 + for i in range(1, len(text1) + 1): + for j in range(1, len(text2) + 1): + if text1[i - 1] == text2[j - 1]: + # 如果 text1[i-1] 和 text2[j-1] 相等,则当前位置的最长公共子序列长度为左上角位置的值加一 + dp[i][j] = dp[i - 1][j - 1] + 1 + else: + # 如果 text1[i-1] 和 text2[j-1] 不相等,则当前位置的最长公共子序列长度为上方或左方的较大值 + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + + # 返回最长公共子序列的长度 + return dp[len(text1)][len(text2)] + +``` +1维DP +```python +class Solution: + def longestCommonSubsequence(self, text1: str, text2: str) -> int: + m, n = len(text1), len(text2) + dp = [0] * (n + 1) # 初始化一维DP数组 + + for i in range(1, m + 1): + prev = 0 # 保存上一个位置的最长公共子序列长度 + for j in range(1, n + 1): + curr = dp[j] # 保存当前位置的最长公共子序列长度 + if text1[i - 1] == text2[j - 1]: + # 如果当前字符相等,则最长公共子序列长度加一 + dp[j] = prev + 1 + else: + # 如果当前字符不相等,则选择保留前一个位置的最长公共子序列长度中的较大值 + dp[j] = max(dp[j], dp[j - 1]) + prev = curr # 更新上一个位置的最长公共子序列长度 + + return dp[n] # 返回最后一个位置的最长公共子序列长度作为结果 + +``` + +### Go: + +```Go +func longestCommonSubsequence(text1 string, text2 string) int { + t1 := len(text1) + t2 := len(text2) + dp:=make([][]int,t1+1) + for i:=range dp{ + dp[i]=make([]int,t2+1) + } + + for i := 1; i <= t1; i++ { + for j := 1; j <=t2; j++ { + if text1[i-1]==text2[j-1]{ + dp[i][j]=dp[i-1][j-1]+1 + }else{ + dp[i][j]=max(dp[i-1][j],dp[i][j-1]) + } + } + } + return dp[t1][t2] +} + +func max(a,b int)int { + if a>b{ + return a + } + return b +} + +``` + +### JavaScript: + +```javascript +const longestCommonSubsequence = (text1, text2) => { + let dp = Array.from(Array(text1.length+1), () => Array(text2.length+1).fill(0)); + + for(let i = 1; i <= text1.length; i++) { + for(let j = 1; j <= text2.length; j++) { + if(text1[i-1] === text2[j-1]) { + dp[i][j] = dp[i-1][j-1] +1;; + } else { + dp[i][j] = Math.max(dp[i-1][j], dp[i][j-1]) + } + } + } + + return dp[text1.length][text2.length]; +}; +``` + +### TypeScript: + +```typescript +function longestCommonSubsequence(text1: string, text2: string): number { + /** + dp[i][j]: text1中前i-1个和text2中前j-1个,最长公共子序列的长度 + */ + const length1: number = text1.length, + length2: number = text2.length; + const dp: number[][] = new Array(length1 + 1).fill(0) + .map(_ => new Array(length2 + 1).fill(0)); + for (let i = 1; i <= length1; i++) { + for (let j = 1; j <= length2; j++) { + if (text1[i - 1] === text2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1; + } else { + dp[i][j] = Math.max(dp[i][j - 1], dp[i - 1][j]); + } + } + } + return dp[length1][length2]; +}; +``` + +### Rust + +```rust +impl Solution { + pub fn longest_common_subsequence(text1: String, text2: String) -> i32 { + let mut dp = vec![vec![0; text2.len() + 1]; text1.len() + 1]; + for (i, c1) in text1.chars().enumerate() { + for (j, c2) in text2.chars().enumerate() { + if c1 == c2 { + dp[i + 1][j + 1] = dp[i][j] + 1; + } else { + dp[i + 1][j + 1] = dp[i][j + 1].max(dp[i + 1][j]); + } + } + } + dp[text1.len()][text2.len()] + } +} +``` + +一维: + +```rust +impl Solution { + pub fn longest_common_subsequence(text1: String, text2: String) -> i32 { + let mut dp = vec![0; text2.len() + 1]; + for c1 in text1.chars() { + // 初始化 prev + let mut prev = 0; + + for (j, c2) in text2.chars().enumerate() { + let temp = dp[j + 1]; + if c1 == c2 { + // 使用上一次的状态,防止重复计算 + dp[j + 1] = prev + 1; + } else { + dp[j + 1] = dp[j + 1].max(dp[j]); + } + // 使用上一次的状态更新 prev + prev = temp; + } + } + dp[text2.len()] + } +} +``` + +### C: + +```c +#define max(a, b) ((a) > (b) ? (a) : (b)) + +int longestCommonSubsequence(char* text1, char* text2) { + int text1Len = strlen(text1); + int text2Len = strlen(text2); + int dp[text1Len + 1][text2Len + 1]; + memset(dp, 0, sizeof (dp)); + for (int i = 1; i <= text1Len; ++i) { + for (int j = 1; j <= text2Len; ++j) { + if(text1[i - 1] == text2[j - 1]){ + dp[i][j] = dp[i - 1][j - 1] + 1; + } else{ + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]); + } + } + } + return dp[text1Len][text2Len]; +} +``` + +### Cangjie + +```cangjie +func longestCommonSubsequence(text1: String, text2: String): Int64 { + let n = text1.size + let m = text2.size + let dp = Array(n + 1, {_ => Array(m + 1, repeat: 0)}) + for (i in 1..=n) { + for (j in 1..=m) { + if (text1[i - 1] == text2[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + 1 + } else { + dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]) + } + } + } + return dp[n][m] +} +``` + + diff --git "a/problems/1207.\347\213\254\344\270\200\346\227\240\344\272\214\347\232\204\345\207\272\347\216\260\346\254\241\346\225\260.md" "b/problems/1207.\347\213\254\344\270\200\346\227\240\344\272\214\347\232\204\345\207\272\347\216\260\346\254\241\346\225\260.md" new file mode 100755 index 0000000000..72462f1119 --- /dev/null +++ "b/problems/1207.\347\213\254\344\270\200\346\227\240\344\272\214\347\232\204\345\207\272\347\216\260\346\254\241\346\225\260.md" @@ -0,0 +1,247 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1207.独一无二的出现次数 + +[力扣题目链接](https://leetcode.cn/problems/unique-number-of-occurrences/) + +给你一个整数数组 arr,请你帮忙统计数组中每个数的出现次数。 + +如果每个数的出现次数都是独一无二的,就返回 true;否则返回 false。 + +示例 1: +* 输入:arr = [1,2,2,1,1,3] +* 输出:true +* 解释:在该数组中,1 出现了 3 次,2 出现了 2 次,3 只出现了 1 次。没有两个数的出现次数相同。 + +示例 2: +* 输入:arr = [1,2] +* 输出:false + +示例 3: +* 输入:arr = [-3,0,1,-3,1,1,1,-3,10,0] +* 输出:true + +提示: + +* 1 <= arr.length <= 1000 +* -1000 <= arr[i] <= 1000 + + +## 思路 + +这道题目数组在是哈希法中的经典应用,如果对数组在哈希法中的使用还不熟悉的同学可以看这两篇:[数组在哈希法中的应用](https://programmercarl.com/0242.有效的字母异位词.html)和[哈希法:383. 赎金信](https://programmercarl.com/0383.赎金信.html) + +进而可以学习一下[set在哈希法中的应用](https://programmercarl.com/0349.两个数组的交集.html),以及[map在哈希法中的应用](https://programmercarl.com/0001.两数之和.html) + +回归本题,**本题强调了-1000 <= arr[i] <= 1000**,那么就可以用数组来做哈希,arr[i]作为哈希表(数组)的下标,那么arr[i]可以是负数,怎么办?负数不能做数组下标。 + + +**此时可以定义一个2001大小的数组,例如int count[2001];**,统计的时候,将arr[i]统一加1000,这样就可以统计arr[i]的出现频率了。 + +题目中要求的是是否有相同的频率出现,那么需要再定义一个哈希表(数组)用来记录频率是否重复出现过,bool fre[1001]; 定义布尔类型的就可以了,**因为题目中强调1 <= arr.length <= 1000,所以哈希表大小为1000就可以了**。 + +如图所示: + + + + +C++代码如下: + +```CPP +class Solution { +public: + bool uniqueOccurrences(vector& arr) { + int count[2001] = {0}; // 统计数字出现的频率 + for (int i = 0; i < arr.size(); i++) { + count[arr[i] + 1000]++; + } + bool fre[1001] = {false}; // 看相同频率是否重复出现 + for (int i = 0; i <= 2000; i++) { + if (count[i]) { + if (fre[count[i]] == false) fre[count[i]] = true; + else return false; + } + } + return true; + } +}; +``` + +## 其他语言版本 + +### Java: + +```java +class Solution { + public boolean uniqueOccurrences(int[] arr) { + int[] count = new int[2001]; + for (int i = 0; i < arr.length; i++) { + count[arr[i] + 1000]++; // 防止负数作为下标 + } + boolean[] flag = new boolean[1002]; // 标记相同频率是否重复出现 + for (int i = 0; i <= 2000; i++) { + if (count[i] > 0) { + if (flag[count[i]] == false) { + flag[count[i]] = true; + } else { + return false; + } + } + } + return true; + } +} +``` + +### Python: + +```python +# 方法 1: 数组在哈西法的应用 +class Solution: + def uniqueOccurrences(self, arr: List[int]) -> bool: + count = [0] * 2001 + for i in range(len(arr)): + count[arr[i] + 1000] += 1 # 防止负数作为下标 + freq = [False] * 1001 # 标记相同频率是否重复出现 + for i in range(2001): + if count[i] > 0: + if freq[count[i]] == False: + freq[count[i]] = True + else: + return False + return True +``` +```python +# 方法 2: map 在哈西法的应用 +class Solution: + def uniqueOccurrences(self, arr: List[int]) -> bool: + ref = dict() + + for i in range(len(arr)): + ref[arr[i]] = ref.get(arr[i], 0) + 1 + + value_list = sorted(ref.values()) + + for i in range(len(value_list) - 1): + if value_list[i + 1] == value_list[i]: + return False + + return True + +``` + +### JavaScript: + +``` javascript +// 方法一:使用数组记录元素出现次数 +var uniqueOccurrences = function(arr) { + const count = new Array(2001).fill(0);// -1000 <= arr[i] <= 1000 + for(let i = 0; i < arr.length; i++){ + count[arr[i] + 1000]++;// 防止负数作为下标 + } + // 标记相同频率是否重复出现 + const fre = new Array(1001).fill(false);// 1 <= arr.length <= 1000 + for(let i = 0; i <= 2000; i++){ + if(count[i] > 0){//有i出现过 + if(fre[count[i]] === false) fre[count[i]] = true;//之前未出现过,标记为出现 + else return false;//之前就出现了,重复出现 + } + } + return true; +}; + +// 方法二:使用Map 和 Set +/** + * @param {number[]} arr + * @return {boolean} + */ +var uniqueOccurrences = function(arr) { + // 记录每个元素出现次数 + let map = new Map(); + arr.forEach( x => { + map.set(x, (map.get(x) || 0) + 1); + }) + // Set() 里的元素是不重复的。如果有元素出现次数相同,则最后的set的长度不等于map的长度 + return map.size === new Set(map.values()).size +}; +``` + +### TypeScript: + +> 借用数组: + +```typescript +function uniqueOccurrences(arr: number[]): boolean { + const countArr: number[] = new Array(2001).fill(0); + for (let i = 0, length = arr.length; i < length; i++) { + countArr[arr[i] + 1000]++; + } + const flagArr: boolean[] = new Array(1001).fill(false); + for (let count of countArr) { + if (count === 0) continue; + if (flagArr[count] === true) return false; + flagArr[count] = true; + } + return true; +}; +``` + +> 借用map、set + +```typescript +function uniqueOccurrences(arr: number[]): boolean { + const countMap: Map = new Map(); + arr.forEach(val => { + countMap.set(val, (countMap.get(val) || 0) + 1); + }) + return countMap.size === new Set(countMap.values()).size; +}; +``` + + +### Go: +```Go +func uniqueOccurrences(arr []int) bool { + count := make(map[int]int) // 统计数字出现的频率 + for _, v := range arr { + count[v] += 1 + } + fre := make(map[int]struct{}) // 看相同频率是否重复出现 + for _, v := range count { + if _, ok := fre[v]; ok { + return false + } + fre[v] = struct{}{} + } + return true +} +``` + +### Rust + +```rust +use std::collections::{HashMap, HashSet}; +impl Solution { + pub fn unique_occurrences(arr: Vec) -> bool { + let mut hash = HashMap::::new(); + for x in arr { + *hash.entry(x).or_insert(0) += 1; + } + let mut set = HashSet::::new(); + for (_k, v) in hash { + if set.contains(&v) { + return false + } else { + set.insert(v); + } + } + true + } +} +``` + + + + diff --git "a/problems/1221.\345\210\206\345\211\262\345\271\263\350\241\241\345\255\227\347\254\246\344\270\262.md" "b/problems/1221.\345\210\206\345\211\262\345\271\263\350\241\241\345\255\227\347\254\246\344\270\262.md" new file mode 100755 index 0000000000..a9e275d92d --- /dev/null +++ "b/problems/1221.\345\210\206\345\211\262\345\271\263\350\241\241\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,172 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1221. 分割平衡字符串 + +[力扣题目链接](https://leetcode.cn/problems/split-a-string-in-balanced-strings/) + +在一个 平衡字符串 中,'L' 和 'R' 字符的数量是相同的。 + +给你一个平衡字符串 s,请你将它分割成尽可能多的平衡字符串。 + +注意:分割得到的每个字符串都必须是平衡字符串。 + +返回可以通过分割得到的平衡字符串的 最大数量 。 + + +示例 1: + +* 输入:s = "RLRRLLRLRL" +* 输出:4 +* 解释:s 可以分割为 "RL"、"RRLL"、"RL"、"RL" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。 + +示例 2: +* 输入:s = "RLLLLRRRLR" +* 输出:3 +* 解释:s 可以分割为 "RL"、"LLLRRR"、"LR" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。 + +示例 3: +* 输入:s = "LLLLRRRR" +* 输出:1 +* 解释:s 只能保持原样 "LLLLRRRR". + +示例 4: +* 输入:s = "RLRRRLLRLL" +* 输出:2 +* 解释:s 可以分割为 "RL"、"RRRLLRLL" ,每个子字符串中都包含相同数量的 'L' 和 'R' 。 + +## 思路 + +这道题目看起来好像很复杂,其实是非常简单的贪心,关于贪心,我在这里[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html)有详细的讲解。 + +从前向后遍历,只要遇到平衡子串,计数就+1,遍历一遍即可。 + +局部最优:从前向后遍历,只要遇到平衡子串 就统计 + +全局最优:统计了最多的平衡子串。 + +局部最优可以推出全局最优,举不出反例,那么就试试贪心。 + + +例如,LRLR 这本身就是平衡子串 , 但要遇到LR就可以分割。 + +C++代码如下: + +```CPP +class Solution { +public: + int balancedStringSplit(string s) { + int result = 0; + int count = 0; + for (int i = 0; i < s.size(); i++) { + if (s[i] == 'R') count++; + else count--; + if (count == 0) result++; + } + return result; + } +}; +``` + +## 拓展 + +一些同学可能想,你这个推理不靠谱,都没有数学证明。怎么就能说是合理的呢,怎么就能说明 局部最优可以推出全局最优呢? + +一般数学证明有如下两种方法: + +* 数学归纳法 +* 反证法 + +如果真的去严格数学证明其实不是在我们刷题或者 面试的考察范围内了。 + +所以贪心题目的思考过程是: 如果发现局部最优好像可以推出全局最优,那么就 尝试一下举反例,如果举不出反例,那么就试试贪心。 + + + +## 其他语言版本 + +### Java + +```java +class Solution { + public int balancedStringSplit(String s) { + int result = 0; + int count = 0; + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) == 'R') count++; + else count--; + if (count == 0) result++; + } + return result; + } +} +``` + +### Python + +```python +class Solution: + def balancedStringSplit(self, s: str) -> int: + diff = 0 #右左差值 + ans = 0 + for c in s: + if c == "L": + diff -= 1 + else: + diff += 1 + if diff == 0: + ans += 1 + return ans +``` + +### Go + +```go +func balancedStringSplit(s string) int { + diff := 0 // 右左差值 + ans := 0 + for _, c := range s { + if c == 'L' { + diff-- + }else { + diff++ + } + if diff == 0 { + ans++ + } + } + return ans +} +``` + +### JavaScript + +```js +var balancedStringSplit = function(s) { + let res = 0, total = 0;//res为平衡字符串数量 total为当前"R"字符和"L"字符的数量差 + for(let c of s){// 遍历字符串每个字符 + //因为开始字符数量差就是0,遍历的时候要先改变数量差,否则会影响结果数量 + total += c === 'R' ? 1:-1;//遇到"R",total++;遇到"L",total-- + if(total === 0) res++;//只要"R""L"数量一样就可以算是一个平衡字符串 + } + return res; +}; +``` + +### TypeScript + +```ts +function balancedStringSplit(s: string): number { + let count: number = 0 + let res: number = 0 + for(let i of s){ + if(i === 'R') count++ + else count-- + if(count === 0) res++ + } + + return res +}; +``` + diff --git "a/problems/1254.\347\273\237\350\256\241\345\260\201\351\227\255\345\262\233\345\261\277\347\232\204\346\225\260\347\233\256.md" "b/problems/1254.\347\273\237\350\256\241\345\260\201\351\227\255\345\262\233\345\261\277\347\232\204\346\225\260\347\233\256.md" new file mode 100755 index 0000000000..b440c32648 --- /dev/null +++ "b/problems/1254.\347\273\237\350\256\241\345\260\201\351\227\255\345\262\233\345\261\277\347\232\204\346\225\260\347\233\256.md" @@ -0,0 +1,136 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1254. 统计封闭岛屿的数目 + +[力扣题目链接](https://leetcode.cn/problems/number-of-closed-islands/) + +二维矩阵 grid 由 0 (土地)和 1 (水)组成。岛是由最大的4个方向连通的 0 组成的群,封闭岛是一个 完全 由1包围(左、上、右、下)的岛。 + +请返回 封闭岛屿 的数目。 + +![](https://file1.kamacoder.com/i/algo/20220830111533.png) + +* 输入:grid = [[1,1,1,1,1,1,1,0],[1,0,0,0,0,1,1,0],[1,0,1,0,1,1,1,0],[1,0,0,0,0,1,0,1],[1,1,1,1,1,1,1,0]] +* 输出:2 +* 解释:灰色区域的岛屿是封闭岛屿,因为这座岛屿完全被水域包围(即被 1 区域包围)。 + +![](https://file1.kamacoder.com/i/algo/20220830111601.png) + +* 输入:grid = [[0,0,1,0,0],[0,1,0,1,0],[0,1,1,1,0]] +* 输出:1 + +提示: + +* 1 <= grid.length, grid[0].length <= 100 +* 0 <= grid[i][j] <=1 + +## 思路 + +和 [1020. 飞地的数量](https://leetcode.cn/problems/number-of-enclaves/solution/by-carlsun-2-7lt9/) 思路是一样的,代码也基本一样 + +```CPP +class Solution { +private: + int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 + void dfs(vector>& grid, int x, int y) { + grid[x][y] = 1; + for (int i = 0; i < 4; i++) { // 向四个方向遍历 + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + // 超过边界 + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; + // 不符合条件,不继续遍历 + if (grid[nextx][nexty] == 1) continue; + + dfs (grid, nextx, nexty); + } + return; + } + +public: + int closedIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 0) dfs(grid, i, 0); + if (grid[i][m - 1] == 0) dfs(grid, i, m - 1); + } + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 0) dfs(grid, 0, j); + if (grid[n - 1][j] == 0) dfs(grid, n - 1, j); + } + int count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) { + count++; + dfs(grid, i, j); + } + } + } + return count; + } +}; +``` +## 其他语言版本 + +### JavaScript: + +```js +/** + * @param {number[][]} grid + * @return {number} + */ +var closedIsland = function(grid) { + let rows = grid.length; + let cols = grid[0].length; + // 存储四个方向 + let dir = [[-1, 0], [0, -1], [1, 0], [0, 1]]; + // 深度优先 + function dfs(x, y) { + grid[x][y] = 1; + // 向四个方向遍历 + for(let i = 0; i < 4; i++) { + let nextX = x + dir[i][0]; + let nextY = y + dir[i][1]; + // 判断是否越界 + if (nextX < 0 || nextX >= rows || nextY < 0 || nextY >= cols) continue; + // 不符合条件 + if (grid[nextX][nextY] === 1) continue; + // 继续递归 + dfs(nextX, nextY); + } + } + // 从边界岛屿开始 + // 从左侧和右侧出发 + for(let i = 0; i < rows; i++) { + if (grid[i][0] === 0) dfs(i, 0); + if (grid[i][cols - 1] === 0) dfs(i, cols - 1); + } + // 从上侧和下侧出发 + for(let j = 0; j < cols; j++) { + if (grid[0][j] === 0) dfs(0, j); + if (grid[rows - 1][j] === 0) dfs(rows - 1, j); + } + let count = 0; + // 排除所有与边界相连的陆地之后 + // 依次遍历网格中的每个元素,如果遇到一个元素是陆地且状态是未访问,则遇到一个新的岛屿,将封闭岛屿的数目加 1 + // 并访问与当前陆地连接的所有陆地 + for(let i = 0; i < rows; i++) { + for(let j = 0; j < cols; j++) { + if (grid[i][j] === 0) { + count++; + dfs(i, j); + } + } + } + return count; +}; +``` + + + + diff --git "a/problems/1334.\351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202.md" "b/problems/1334.\351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202.md" new file mode 100644 index 0000000000..bea47a2e63 --- /dev/null +++ "b/problems/1334.\351\230\210\345\200\274\350\267\235\347\246\273\345\206\205\351\202\273\345\261\205\346\234\200\345\260\221\347\232\204\345\237\216\345\270\202.md" @@ -0,0 +1,49 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +floyd + + +class Solution { +public: + int findTheCity(int n, vector>& edges, int distanceThreshold) { + vector> grid(n, vector(n, 10005)); // 因为边的最大距离是10^4 + + // 节点到自己的距离为0 + for (int i = 0; i < n; i++) grid[i][i] = 0; + // 构造邻接矩阵 + for (const vector& e : edges) { + int from = e[0]; + int to = e[1]; + int val = e[2]; + grid[from][to] = val; + grid[to][from] = val; // 注意这里是双向图 + } + + // 开始 floyd + // 思考 为什么 p 要放在最外面一层 + for (int p = 0; p < n; p++) { + for (int i = 0; i < n; i++) { + for (int j = 0; j < n; j++) { + grid[i][j] = min(grid[i][j], grid[i][p] + grid[p][j]); + } + } + } + + int result = 0; + int count = n + 10; // 记录所有城市在范围内连接的最小城市数量 + for (int i = 0; i < n; i++) { + int curCount = 0; // 统计一个城市在范围内可以连接几个城市 + for (int j = 0; j < n; j++) { + if (i != j && grid[i][j] <= distanceThreshold) curCount++; + // cout << "i:" << i << ", j:" << j << ", val: " << grid[i][j] << endl; + } + if (curCount <= count) { // 注意这里是 <= + count = curCount; + result = i; + } + } + return result; + } +}; diff --git "a/problems/1356.\346\240\271\346\215\256\346\225\260\345\255\227\344\272\214\350\277\233\345\210\266\344\270\2131\347\232\204\346\225\260\347\233\256\346\216\222\345\272\217.md" "b/problems/1356.\346\240\271\346\215\256\346\225\260\345\255\227\344\272\214\350\277\233\345\210\266\344\270\2131\347\232\204\346\225\260\347\233\256\346\216\222\345\272\217.md" new file mode 100755 index 0000000000..11f947a184 --- /dev/null +++ "b/problems/1356.\346\240\271\346\215\256\346\225\260\345\255\227\344\272\214\350\277\233\345\210\266\344\270\2131\347\232\204\346\225\260\347\233\256\346\216\222\345\272\217.md" @@ -0,0 +1,217 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 1356. 根据数字二进制下 1 的数目排序 + +[力扣题目链接](https://leetcode.cn/problems/sort-integers-by-the-number-of-1-bits/) + +题目链接:https://leetcode.cn/problems/sort-integers-by-the-number-of-1-bits/ + +给你一个整数数组 arr 。请你将数组中的元素按照其二进制表示中数字 1 的数目升序排序。 + +如果存在多个数字二进制中 1 的数目相同,则必须将它们按照数值大小升序排列。 + +请你返回排序后的数组。 + +示例 1: +* 输入:arr = [0,1,2,3,4,5,6,7,8] +* 输出:[0,1,2,4,8,3,5,6,7] +* 解释:[0] 是唯一一个有 0 个 1 的数。 +[1,2,4,8] 都有 1 个 1 。 +[3,5,6] 有 2 个 1 。 +[7] 有 3 个 1 。按照 1 的个数排序得到的结果数组为 [0,1,2,4,8,3,5,6,7] + + +示例 2: +* 输入:arr = [1024,512,256,128,64,32,16,8,4,2,1] +* 输出:[1,2,4,8,16,32,64,128,256,512,1024] +* 解释:数组中所有整数二进制下都只有 1 个 1 ,所以你需要按照数值大小将它们排序。 + +示例 3: +* 输入:arr = [10000,10000] +* 输出:[10000,10000] + +示例 4: +* 输入:arr = [2,3,5,7,11,13,17,19] +* 输出:[2,3,5,17,7,11,13,19] + +示例 5: +* 输入:arr = [10,100,1000,10000] +* 输出:[10,100,10000,1000] + + + +## 思路 + +这道题其实是考察如何计算一个数的二进制中1的数量。 + +我提供两种方法: + +* 方法一: + +朴实无华挨个计算1的数量,最多就是循环n的二进制位数,32位。 + +```C++ +int bitCount(int n) { + int count = 0; // 计数器 + while (n > 0) { + if((n & 1) == 1) count++; // 当前位是1,count++ + n >>= 1 ; // n向右移位 + } + return count; +} +``` + +* 方法二 + +这种方法,只循环n的二进制中1的个数次,比方法一高效的多 + +```C++ +int bitCount(int n) { + int count = 0; + while (n) { + n &= (n - 1); // 清除最低位的1 + count++; + } + return count; +} +``` +以计算12的二进制1的数量为例,如图所示: + + + +下面我就使用方法二,来做这道题目: + + + +```C++ +class Solution { +private: + static int bitCount(int n) { // 计算n的二进制中1的数量 + int count = 0; + while(n) { + n &= (n -1); // 清除最低位的1 + count++; + } + return count; + } + static bool cmp(int a, int b) { + int bitA = bitCount(a); + int bitB = bitCount(b); + if (bitA == bitB) return a < b; // 如果bit中1数量相同,比较数值大小 + return bitA < bitB; // 否则比较bit中1数量大小 + } +public: + vector sortByBits(vector& arr) { + sort(arr.begin(), arr.end(), cmp); + return arr; + } +}; +``` + + + +## 其他语言版本 + +### Java + +```java +class Solution { + private int cntInt(int val){ + int count = 0; + while(val > 0) { + val = val & (val - 1); + count ++; + } + + return count; + } + + public int[] sortByBits(int[] arr) { + return Arrays.stream(arr).boxed() + .sorted(new Comparator(){ + @Override + public int compare(Integer o1, Integer o2) { + int cnt1 = cntInt(o1); + int cnt2 = cntInt(o2); + return (cnt1 == cnt2) ? Integer.compare(o1, o2) : Integer.compare(cnt1, cnt2); + } + }) + .mapToInt(Integer::intValue) + .toArray(); + } +} +``` + + + + +### Python + +```python +class Solution: + def sortByBits(self, arr: List[int]) -> List[int]: + arr.sort(key=lambda num: (self.count_bits(num), num)) + return arr + + def count_bits(self, num: int) -> int: + count = 0 + while num: + num &= num - 1 + count += 1 + return count +``` + +### Go + +```go +func sortByBits(arr []int) []int { + // 是否arr[i]<=arr[j] + // 先比较1的数量,后比较值本身 + cmp := func(i, j int) bool { + c1, c2 := bitCount(arr[i]), bitCount(arr[j]) + if c1 == c2 { + return arr[i] <= arr[j] + } + return c1 <= c2 + } + + // 调用库函数 + // 第一个参数是待排序切片,第二个是第i位是否小于第j位的函数 + sort.Slice(arr, cmp) + + return arr +} + +func bitCount(num int) (count int) { + for num != 0 { + num &= num-1 // 每次运算将最右侧的1变成0 + count++ + } + + return count +} +``` + +### JavaScript + +```js +var sortByBits = function(arr) { + const bitCount = n =>{// 计算n的二进制中1的数量 + let count = 0; + while(n){ + n &= (n - 1);// 清除最低位的1 + count++; + } + return count; + } + // 如果有差,则按bits数排,如果无差,则按原值排 + return arr.sort((a,b) => bitCount(a) - bitCount(b) || a - b); +}; +``` + + + diff --git "a/problems/1365.\346\234\211\345\244\232\345\260\221\345\260\217\344\272\216\345\275\223\345\211\215\346\225\260\345\255\227\347\232\204\346\225\260\345\255\227.md" "b/problems/1365.\346\234\211\345\244\232\345\260\221\345\260\217\344\272\216\345\275\223\345\211\215\346\225\260\345\255\227\347\232\204\346\225\260\345\255\227.md" new file mode 100755 index 0000000000..61b548abf8 --- /dev/null +++ "b/problems/1365.\346\234\211\345\244\232\345\260\221\345\260\217\344\272\216\345\275\223\345\211\215\346\225\260\345\255\227\347\232\204\346\225\260\345\255\227.md" @@ -0,0 +1,311 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 1365.有多少小于当前数字的数字 + +[力扣题目链接](https://leetcode.cn/problems/how-many-numbers-are-smaller-than-the-current-number/) + +给你一个数组 nums,对于其中每个元素 nums[i],请你统计数组中比它小的所有数字的数目。 + +换而言之,对于每个 nums[i] 你必须计算出有效的 j 的数量,其中 j 满足 j != i 且 nums[j] < nums[i] 。 + +以数组形式返回答案。 + + +示例 1: +* 输入:nums = [8,1,2,2,3] +* 输出:[4,0,1,1,3] +* 解释: +对于 nums[0]=8 存在四个比它小的数字:(1,2,2 和 3)。 +对于 nums[1]=1 不存在比它小的数字。 +对于 nums[2]=2 存在一个比它小的数字:(1)。 +对于 nums[3]=2 存在一个比它小的数字:(1)。 +对于 nums[4]=3 存在三个比它小的数字:(1,2 和 2)。 + +示例 2: +* 输入:nums = [6,5,4,8] +* 输出:[2,1,0,3] + +示例 3: +* 输入:nums = [7,7,7,7] +* 输出:[0,0,0,0] + +提示: +* 2 <= nums.length <= 500 +* 0 <= nums[i] <= 100 + +## 思路 + +两层for循环暴力查找,时间复杂度明显为$O(n^2)$。 + +那么我们来看一下如何优化。 + +首先要找小于当前数字的数字,那么从小到大排序之后,该数字之前的数字就都是比它小的了。 + +所以可以定义一个新数组,将数组排个序。 + +**排序之后,其实每一个数值的下标就代表这前面有几个比它小的了**。 + +代码如下: + +``` +vector vec = nums; +sort(vec.begin(), vec.end()); // 从小到大排序之后,元素下标就是小于当前数字的数字 +``` + +用一个哈希表hash(本题可以就用一个数组)来做数值和下标的映射。这样就可以通过数值快速知道下标(也就是前面有几个比它小的)。 + +此时有一个情况,就是数值相同怎么办? + +例如,数组:1 2 3 4 4 4 ,第一个数值4的下标是3,第二个数值4的下标是4了。 + +这里就需要一个技巧了,**在构造数组hash的时候,从后向前遍历,这样hash里存放的就是相同元素最左面的数值和下标了**。 +代码如下: + +```CPP +int hash[101]; +for (int i = vec.size() - 1; i >= 0; i--) { // 从后向前,记录 vec[i] 对应的下标 + hash[vec[i]] = i; +} +``` + +最后在遍历原数组nums,用hash快速找到每一个数值 对应的 小于这个数值的个数。存放在将结果存放在另一个数组中。 + +代码如下: + +```CPP +// 此时hash里保存的每一个元素数值 对应的 小于这个数值的个数 +for (int i = 0; i < nums.size(); i++) { + vec[i] = hash[nums[i]]; +} +``` + +流程如图: + + + +关键地方讲完了,整体C++代码如下: + +```CPP +class Solution { +public: + vector smallerNumbersThanCurrent(vector& nums) { + vector vec = nums; + sort(vec.begin(), vec.end()); // 从小到大排序之后,元素下标就是小于当前数字的数字 + int hash[101]; + for (int i = vec.size() - 1; i >= 0; i--) { // 从后向前,记录 vec[i] 对应的下标 + hash[vec[i]] = i; + } + // 此时hash里保存的每一个元素数值 对应的 小于这个数值的个数 + for (int i = 0; i < nums.size(); i++) { + vec[i] = hash[nums[i]]; + } + return vec; + } +}; +``` + +可以排序之后加哈希,时间复杂度为$O(n\log n)$ + + +## 其他语言版本 + +### Java: + +```Java +public int[] smallerNumbersThanCurrent(int[] nums) { + Map map = new HashMap<>(); // 记录数字 nums[i] 有多少个比它小的数字 + int[] res = Arrays.copyOf(nums, nums.length); + Arrays.sort(res); + for (int i = 0; i < res.length; i++) { + if (!map.containsKey(res[i])) { // 遇到了相同的数字,那么不需要更新该 number 的情况 + map.put(res[i], i); + } + } + + for (int i = 0; i < nums.length; i++) { + res[i] = map.get(nums[i]); + } + + return res; + } +``` + +### Python: + +> 暴力法: + +```python3 +class Solution: + def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]: + res = [0 for _ in range(len(nums))] + for i in range(len(nums)): + cnt = 0 + for j in range(len(nums)): + if j == i: + continue + if nums[i] > nums[j]: + cnt += 1 + res[i] = cnt + return res +``` + +> 排序+hash: + +```python3 +class Solution: + # 方法一:使用字典 + def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]: + res = nums[:] + hash_dict = dict() + res.sort() # 从小到大排序之后,元素下标就是小于当前数字的数字 + for i, num in enumerate(res): + if num not in hash_dict.keys(): # 遇到了相同的数字,那么不需要更新该 number 的情况 + hash_dict[num] = i + for i, num in enumerate(nums): + res[i] = hash_dict[num] + return res + + # 方法二:使用数组 + def smallerNumbersThanCurrent(self, nums: List[int]) -> List[int]: + # 同步进行排序和创建新数组的操作,这样可以减少一次冗余的数组复制操作,以减少一次O(n) 的复制时间开销 + sort_nums = sorted(nums) + # 题意中 0 <= nums[i] <= 100,故range的参数设为101 + hash_lst = [0 for _ in range(101)] + # 从后向前遍历,这样hash里存放的就是相同元素最左面的数值和下标了 + for i in range(len(sort_nums)-1,-1,-1): + hash_lst[sort_nums[i]] = i + for i in range(len(nums)): + nums[i] = hash_lst[nums[i]] + return nums +``` + +### Go: + +```go +func smallerNumbersThanCurrent(nums []int) []int { + // map,key[数组中出现的数] value[比这个数小的个数] + m := make(map[int]int) + // 拷贝一份原始数组 + rawNums := make([]int,len(nums)) + copy(rawNums,nums) + // 将数组排序 + sort.Ints(nums) + // 循环遍历排序后的数组,值为map的key,索引为value + for i,v := range nums { + _,contains := m[v] + if !contains { + m[v] = i + } + + } + // 返回值结果 + result := make([]int,len(nums)) + // 根据原始数组的位置,存放对应的比它小的数 + for i,v := range rawNums { + result[i] = m[v] + } + + return result +} +``` + +### JavaScript: + +```javascript +// 方法一:使用哈希表记录位置 +var smallerNumbersThanCurrent = function(nums) { + const map = new Map();// 记录数字 nums[i] 有多少个比它小的数字 + const res = nums.slice(0);//深拷贝nums + res.sort((a,b) => a - b); + for(let i = 0; i < res.length; i++){ + if(!map.has(res[i])){// 遇到了相同的数字,那么不需要更新该 number 的情况 + map.set(res[i],i); + } + } + // 此时map里保存的每一个元素数值 对应的 小于这个数值的个数 + for(let i = 0; i < nums.length; i++){ + res[i] = map.get(nums[i]); + } + return res; +}; + +// 方法二:不使用哈希表,只使用一个额外数组 +/** + * @param {number[]} nums + * @return {number[]} + */ +var smallerNumbersThanCurrent = function(nums) { + let array = [...nums]; // 深拷贝 + // 升序排列,此时数组元素下标即是比他小的元素的个数 + array = array.sort((a, b) => a-b); + let res = []; + nums.forEach( x => { + // 即使元素重复也不怕,indexOf 只返回找到的第一个元素的下标 + res.push(array.indexOf(x)); + }) + return res; +}; +``` + +### TypeScript: + +> 暴力法: + +```typescript +function smallerNumbersThanCurrent(nums: number[]): number[] { + const length: number = nums.length; + const resArr: number[] = []; + for (let i = 0; i < length; i++) { + let count: number = 0; + for (let j = 0; j < length; j++) { + if (nums[j] < nums[i]) { + count++; + } + } + resArr[i] = count; + } + return resArr; +}; +``` + +> 排序+hash: + +```typescript +function smallerNumbersThanCurrent(nums: number[]): number[] { + const length: number = nums.length; + const sortedArr: number[] = [...nums]; + sortedArr.sort((a, b) => a - b); + const hashMap: Map = new Map(); + for (let i = length - 1; i >= 0; i--) { + hashMap.set(sortedArr[i], i); + } + const resArr: number[] = []; + for (let i = 0; i < length; i++) { + resArr[i] = hashMap.get(nums[i]); + } + return resArr; +}; +``` + +### Rust: +```rust +use std::collections::HashMap; +impl Solution { + pub fn smaller_numbers_than_current(nums: Vec) -> Vec { + let mut v = nums.clone(); + v.sort(); + let mut hash = HashMap::new(); + for i in 0..v.len() { + // rust中使用or_insert插入值, 如果存在就不插入,可以使用正序遍历 + hash.entry(v[i]).or_insert(i as i32); + } + nums.into_iter().map(|x| *hash.get(&x).unwrap()).collect() + } +} +``` + + diff --git "a/problems/1382.\345\260\206\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\345\217\230\345\271\263\350\241\241.md" "b/problems/1382.\345\260\206\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\345\217\230\345\271\263\350\241\241.md" new file mode 100755 index 0000000000..551766ff0d --- /dev/null +++ "b/problems/1382.\345\260\206\344\272\214\345\217\211\346\220\234\347\264\242\346\240\221\345\217\230\345\271\263\350\241\241.md" @@ -0,0 +1,218 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 1382.将二叉搜索树变平衡 + +[力扣题目链接](https://leetcode.cn/problems/balance-a-binary-search-tree/) + +给你一棵二叉搜索树,请你返回一棵 平衡后 的二叉搜索树,新生成的树应该与原来的树有着相同的节点值。 + +如果一棵二叉搜索树中,每个节点的两棵子树高度差不超过 1 ,我们就称这棵二叉搜索树是 平衡的 。 + +如果有多种构造方法,请你返回任意一种。 + +示例: + +![](https://file1.kamacoder.com/i/algo/20210726154512.png) + +* 输入:root = [1,null,2,null,3,null,4,null,null] +* 输出:[2,1,3,null,null,null,4] +* 解释:这不是唯一的正确答案,[3,1,4,null,2,null,null] 也是一个可行的构造方案。 + +提示: + +* 树节点的数目在 1 到 10^4 之间。 +* 树节点的值互不相同,且在 1 到 10^5 之间。 + +## 思路 + +这道题目,可以中序遍历把二叉树转变为有序数组,然后在根据有序数组构造平衡二叉搜索树。 + +建议做这道题之前,先看如下两篇题解: +* [98.验证二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html) 学习二叉搜索树的特性 +* [108.将有序数组转换为二叉搜索树](https://programmercarl.com/0108.将有序数组转换为二叉搜索树.html) 学习如何通过有序数组构造二叉搜索树 + +这两道题目做过之后,本题分分钟就可以做出来了。 + +代码如下: + +```CPP +class Solution { +private: + vector vec; + // 有序树转成有序数组 + void traversal(TreeNode* cur) { + if (cur == nullptr) { + return; + } + traversal(cur->left); + vec.push_back(cur->val); + traversal(cur->right); + } + // 有序数组转平衡二叉树 + TreeNode* getTree(vector& nums, int left, int right) { + if (left > right) return nullptr; + int mid = left + ((right - left) / 2); + TreeNode* root = new TreeNode(nums[mid]); + root->left = getTree(nums, left, mid - 1); + root->right = getTree(nums, mid + 1, right); + return root; + } + +public: + TreeNode* balanceBST(TreeNode* root) { + traversal(root); + return getTree(vec, 0, vec.size() - 1); + } +}; +``` + +## 其他语言版本 + +### Java: + +```java +class Solution { + ArrayList res = new ArrayList(); + // 有序树转成有序数组 + private void travesal(TreeNode cur) { + if (cur == null) return; + travesal(cur.left); + res.add(cur.val); + travesal(cur.right); + } + // 有序数组转成平衡二叉树 + private TreeNode getTree(ArrayList nums, int left, int right) { + if (left > right) return null; + int mid = left + (right - left) / 2; + TreeNode root = new TreeNode(nums.get(mid)); + root.left = getTree(nums, left, mid - 1); + root.right = getTree(nums, mid + 1, right); + return root; + } + public TreeNode balanceBST(TreeNode root) { + travesal(root); + return getTree(res, 0, res.size() - 1); + } +} +``` +### Python: + +```python +class Solution: + def balanceBST(self, root: TreeNode) -> TreeNode: + res = [] + # 有序树转成有序数组 + def traversal(cur: TreeNode): + if not cur: return + traversal(cur.left) + res.append(cur.val) + traversal(cur.right) + # 有序数组转成平衡二叉树 + def getTree(nums: List, left, right): + if left > right: return + mid = left + (right -left) // 2 + root = TreeNode(nums[mid]) + root.left = getTree(nums, left, mid - 1) + root.right = getTree(nums, mid + 1, right) + return root + traversal(root) + return getTree(res, 0, len(res) - 1) +``` +### Go: + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func balanceBST(root *TreeNode) *TreeNode { + // 二叉搜索树中序遍历得到有序数组 + nums := []int{} + // 中序递归遍历二叉树 + var travel func(node *TreeNode) + travel = func(node *TreeNode) { + if node == nil { + return + } + travel(node.Left) + nums = append(nums, node.Val) + travel(node.Right) + } + // 二分法保证左右子树高度差不超过一(题目要求返回的仍是二叉搜索树) + var buildTree func(nums []int, left, right int) *TreeNode + buildTree = func(nums []int, left, right int) *TreeNode { + if left > right { + return nil + } + mid := left + (right-left) >> 1 + root := &TreeNode{Val: nums[mid]} + root.Left = buildTree(nums, left, mid-1) + root.Right = buildTree(nums, mid+1, right) + return root + } + travel(root) + return buildTree(nums, 0, len(nums)-1) +} + +``` + +### JavaScript: + +```javascript +var balanceBST = function(root) { + const res = []; + // 中序遍历转成有序数组 + const travesal = cur => { + if(!cur) return; + travesal(cur.left); + res.push(cur.val); + travesal(cur.right); + } + // 有序数组转成平衡二叉树 + const getTree = (nums, left, right) => { + if(left > right) return null; + let mid = left + ((right - left) >> 1); + let root = new TreeNode(nums[mid]);// 中心位置作为当前节点的值 + root.left = getTree(nums, left, mid - 1);// 递归地将区间[left,mid−1] 作为当前节点的左子树 + root.right = getTree(nums, mid + 1, right);// 递归地将区间[mid+1,right] 作为当前节点的左子树 + return root; + } + travesal(root); + return getTree(res, 0, res.length - 1); +}; +``` + +### TypeScript: + +```typescript +function balanceBST(root: TreeNode | null): TreeNode | null { + const inorderArr: number[] = []; + inorderTraverse(root, inorderArr); + return buildTree(inorderArr, 0, inorderArr.length - 1); +}; +function inorderTraverse(node: TreeNode | null, arr: number[]): void { + if (node === null) return; + inorderTraverse(node.left, arr); + arr.push(node.val); + inorderTraverse(node.right, arr); +} +function buildTree(arr: number[], left: number, right: number): TreeNode | null { + if (left > right) return null; + const mid = (left + right) >> 1; + const resNode: TreeNode = new TreeNode(arr[mid]); + resNode.left = buildTree(arr, left, mid - 1); + resNode.right = buildTree(arr, mid + 1, right); + return resNode; +} +``` + + + + diff --git "a/problems/1791.\346\211\276\345\207\272\346\230\237\345\236\213\345\233\276\347\232\204\344\270\255\345\277\203\350\212\202\347\202\271.md" "b/problems/1791.\346\211\276\345\207\272\346\230\237\345\236\213\345\233\276\347\232\204\344\270\255\345\277\203\350\212\202\347\202\271.md" new file mode 100755 index 0000000000..9991249f5a --- /dev/null +++ "b/problems/1791.\346\211\276\345\207\272\346\230\237\345\236\213\345\233\276\347\232\204\344\270\255\345\277\203\350\212\202\347\202\271.md" @@ -0,0 +1,77 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1791.找出星型图的中心节点 + +[题目链接](https://leetcode.cn/problems/find-center-of-star-graph/) + +本题思路就是统计各个节点的度(这里没有区别入度和出度),如果某个节点的度等于这个图边的数量。 那么这个节点一定是中心节点。 + +什么是度,可以理解为,链接节点的边的数量。 题目中度如图所示: + +![1791.找出星型图的中心节点](https://file1.kamacoder.com/i/algo/20220704113207.png) + +至于出度和入度,那就是在有向图里的概念了,本题是无向图。 + +本题代码如下: + +```c++ + +class Solution { +public: + int findCenter(vector>& edges) { + unordered_map du; + for (int i = 0; i < edges.size(); i++) { // 统计各个节点的度 + du[edges[i][1]]++; + du[edges[i][0]]++; + } + unordered_map::iterator iter; // 找出度等于边熟练的节点 + for (iter = du.begin(); iter != du.end(); iter++) { + if (iter->second == edges.size()) return iter->first; + } + return -1; + } +}; +``` + +其实可以只记录度不用最后统计,因为题目说了一定是星状图,所以 一旦有 节点的度 大于1,就返回该节点数值就行,只有中心节点的度会大于1。 + +代码如下: + +```c++ +class Solution { +public: + int findCenter(vector>& edges) { + vector du(edges.size() + 2); // edges.size() + 1 为节点数量,下标表示节点数,所以+2 + for (int i = 0; i < edges.size(); i++) { + du[edges[i][1]]++; + du[edges[i][0]]++; + if (du[edges[i][1]] > 1) return edges[i][1]; + if (du[edges[i][0]] > 1) return edges[i][0]; + + } + return -1; + } +}; +``` + +以上代码中没有使用 unordered_map,因为遍历的时候,开辟新空间会浪费时间,而采用 vector,这是 空间换时间的一种策略。 + +代码其实可以再精简: + +```c++ +class Solution { +public: + int findCenter(vector>& edges) { + vector du(edges.size() + 2); + for (int i = 0; i < edges.size(); i++) { + if (++du[edges[i][1]] > 1) return edges[i][1]; + if (++du[edges[i][0]] > 1) return edges[i][0]; + } + return -1; + } +}; +``` + + diff --git "a/problems/1971.\345\257\273\346\211\276\345\233\276\344\270\255\346\230\257\345\220\246\345\255\230\345\234\250\350\267\257\345\276\204.md" "b/problems/1971.\345\257\273\346\211\276\345\233\276\344\270\255\346\230\257\345\220\246\345\255\230\345\234\250\350\267\257\345\276\204.md" new file mode 100755 index 0000000000..9048b0f6af --- /dev/null +++ "b/problems/1971.\345\257\273\346\211\276\345\233\276\344\270\255\346\230\257\345\220\246\345\255\230\345\234\250\350\267\257\345\276\204.md" @@ -0,0 +1,335 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 1971. 寻找图中是否存在路径 + +[题目链接](https://leetcode.cn/problems/find-if-path-exists-in-graph/) + +有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。 + +请你确定是否存在从顶点 start 开始,到顶点 end 结束的 有效路径 。 + +给你数组 edges 和整数 n、start 和 end,如果从 start 到 end 存在 有效路径 ,则返回 true,否则返回 false 。 + +![](https://file1.kamacoder.com/i/algo/20220705101442.png) + + + +## 思路 + +本题是并查集基础题目。 如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/图论并查集理论基础.html) + +并查集可以解决什么问题呢? + +主要就是集合问题,两个节点在不在一个集合,也可以将两个节点添加到一个集合中。 + +这里整理出我的并查集模板如下: + +```CPP +int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 +vector father = vector (n, 0); // C++里的一种数组结构 + +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} +// 并查集里寻根的过程,这里递归调用当题目数据过多,递归调用可能会发生栈溢出 + +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 +} + +// 使用迭代的方法可以避免栈溢出问题 +int find(int x) { + while (x != parent[x]) { + // 路径压缩,直接将x链接到其祖先节点,减少树的高度 + parent[x] = parent[parent[x]]; + x = parent[x]; + } +return x; +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +``` + +以上模板中,只要修改 n 大小就可以,本题 n 不会超过 2 \* 10^5。 + +并查集主要有三个功能。 + +1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个 +2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上 +3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点 + +简单介绍并查集之后,我们再来看一下这道题目。 + +为什么说这道题目是并查集基础题目,题目中各个点是双向图链接,那么判断 一个顶点到另一个顶点有没有有效路径其实就是看这两个顶点是否在同一个集合里。 + +如何算是同一个集合呢,有边连在一起,就算是一个集合。 + +此时我们就可以直接套用并查集模板。 + +本题在join函数调用find函数时如果是递归调用会发生栈溢出提示,建议使用迭代方法 + +使用 join(int u, int v)将每条边加入到并查集。 + +最后 isSame(int u, int v) 判断是否是同一个根 就可以了。 + +C++代码如下: + +```CPP +class Solution { +private: + int n = 200005; // 节点数量 20000 + vector father = vector (n, 0); // C++里的一种数组结构 + + // 并查集初始化 + void init() { + for (int i = 0; i < n; ++i) { father[i] = i; + } + } + // 并查集里寻根的过程 + int find(int x) { + while (x != parent[x]) { + // 路径压缩,直接将x链接到其祖先节点,减少树的高度 + parent[x] = parent[parent[x]]; + x = parent[x]; + } + return x; + } + + // 判断 u 和 v是否找到同一个根 + bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; + } + + // 将v->u 这条边加入并查集 + void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; + } + +public: + bool validPath(int n, vector>& edges, int source, int destination) { + init(); + for (int i = 0; i < edges.size(); i++) { + join(edges[i][0], edges[i][1]); + } + return isSame(source, destination); + + } +}; +``` + +## 其他语言版本 + +### Java: + +```java +class Solution { + + int[] father; + public boolean validPath(int n, int[][] edges, int source, int destination) { + father = new int[n]; + init(); + for (int i = 0; i < edges.length; i++) { + join(edges[i][0], edges[i][1]); + } + + return isSame(source, destination); + } + + // 并查集初始化 + public void init() { + for (int i = 0; i < father.length; i++) { + father[i] = i; + } + } + + // 并查集里寻根的过程 + public int find(int u) { + if (u == father[u]) { + return u; + } else { + father[u] = find(father[u]); + return father[u]; + } + } + + // 判断 u 和 v是否找到同一个根 + public boolean isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; + } + + // 将v->u 这条边加入并查集 + public void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + + father[v] = u; + } + +} +``` + +### Python: + +PYTHON 并查集解法如下: + +```PYTHON +class Solution: + def validPath(self, n: int, edges: List[List[int]], source: int, destination: int) -> bool: + p = [i for i in range(n)] + def find(i): + if p[i] != i: + p[i] = find(p[i]) + return p[i] + for u, v in edges: + p[find(u)] = find(v) + return find(source) == find(destination) +``` + +### JavaScript: + +Javascript 并查集解法如下: + +```Javascript +class unionF{ + constructor(n){ + this.count = n + this.roots = new Array(n).fill(0).map((item,index)=>index) + } + + findRoot(x){ + if(this.roots[x]!==x){ + this.roots[x] = this.findRoot(this.roots[x]) + } + return this.roots[x] + } + + union(x,y){ + const rx = this.findRoot(x) + const ry = this.findRoot(y) + this.roots[rx] = ry + this.count-- + } + + isConnected(x,y){ + return this.findRoot(x)===this.findRoot(y) + } +} + +var validPath = function(n, edges, source, destination) { + const UF = new unionF(n) + for(const [s,t] of edges){ + UF.union(s,t) + } + return UF.isConnected(source,destination) +}; +``` + +Javascript 双向 bfs 解法如下: + +```Javascript +var validPath = function(n, edges, source, destination) { + const graph = new Array(n).fill(0).map(()=>[]) + for(const [s,t] of edges){ + graph[s].push(t) + graph[t].push(s) + } + + const visited = new Array(n).fill(false) + function bfs(start,end,graph){ + const startq = [start] + const endq = [end] + while(startq.length&&endq.length){ + const slen = startq.length + for(let i = 0;iu 这条边加入并查集 + join = func(u, v int) { + u = find(u) + v = find(v) + if u == v { + return + } + father[v] = u + } + + for i := 0; i < len(edges); i++ { + join(edges[i][0], edges[i][1]) + } + + source = find(source) + destination = find(destination) + return source == destination +} + +``` + diff --git "a/problems/O(n)\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" "b/problems/O(n)\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" new file mode 100644 index 0000000000..d74f1a01aa --- /dev/null +++ "b/problems/O(n)\347\232\204\347\256\227\346\263\225\345\261\205\347\204\266\350\266\205\346\227\266\344\272\206\357\274\214\346\255\244\346\227\266\347\232\204n\347\251\266\347\253\237\346\230\257\345\244\232\345\244\247\357\274\237.md" @@ -0,0 +1,224 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 程序提交之后为什么会超时?O(n)的算法会超时,n究竟是多大? + + + +一些同学可能对计算机运行的速度还没有概念,就是感觉计算机运行速度应该会很快,那么在leetcode上做算法题目的时候为什么会超时呢? + +计算机究竟1s可以执行多少次操作呢? 接下来探讨一下这个问题。 + +## 超时是怎么回事 + +![程序超时](https://file1.kamacoder.com/i/algo/20200729112716117.png) + +大家在leetcode上练习算法的时候应该都遇到过一种错误是“超时”。 + +也就是说程序运行的时间超过了规定的时间,一般OJ(online judge)的超时时间就是1s,也就是用例数据输入后最多要1s内得到结果,暂时还不清楚leetcode的判题规则,下文为了方便讲解,暂定超时时间就是1s。 + +如果写出了一个O(n)的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。 + +如果n的规模已经足够让O(n)的算法运行时间超过了1s,就应该考虑log(n)的解法了。 + +## 从硬件配置看计算机的性能 + +计算机的运算速度主要看CPU的配置,以2015年MacPro为例,CPU配置:2.7 GHz Dual-Core Intel Core i5 。 + +也就是 2.7 GHz 奔腾双核,i5处理器,GHz是指什么呢,1Hz = 1/s,1Hz 是CPU的一次脉冲(可以理解为一次改变状态,也叫时钟周期),称之为为赫兹,那么1GHz等于多少赫兹呢 + +* 1GHz(兆赫)= 1000MHz(兆赫) +* 1MHz(兆赫)= 1百万赫兹 + +所以 1GHz = 10亿Hz,表示CPU可以一秒脉冲10亿次(有10亿个时钟周期),这里不要简单理解一个时钟周期就是一次CPU运算。 + +例如1 + 2 = 3,cpu要执行四次才能完整这个操作,步骤一:把1放入寄存机,步骤二:把2放入寄存器,步骤三:做加法,步骤四:保存3。 + + +而且计算机的cpu也不会只运行我们自己写的程序上,同时cpu也要执行计算机的各种进程任务等等,我们的程序仅仅是其中的一个进程而已。 + + +所以我们的程序在计算机上究竟1s真正能执行多少次操作呢? + +## 做个测试实验 + +在写测试程序测1s内处理多大数量级数据的时候,有三点需要注意: + +* CPU执行每条指令所需的时间实际上并不相同,例如CPU执行加法和乘法操作的耗时实际上都是不一样的。 +* 现在大多计算机系统的内存管理都有缓存技术,所以频繁访问相同地址的数据和访问不相邻元素所需的时间也是不同的。 +* 计算机同时运行多个程序,每个程序里还有不同的进程线程在抢占资源。 + +尽管有很多因素影响,但是还是可以对自己程序的运行时间有一个大体的评估的。 + +引用算法4里面的一段话: + +* 火箭科学家需要大致知道一枚试射火箭的着陆点是在大海里还是在城市中; +* 医学研究者需要知道一次药物测试是会杀死还是会治愈实验对象; + +所以**任何开发计算机程序员的软件工程师都应该能够估计这个程序的运行时间是一秒钟还是一年**。 + +这个是最基本的,所以以上误差就不算事了。 + +以下以C++代码为例: + +测试硬件:2015年MacPro,CPU配置:2.7 GHz Dual-Core Intel Core i5 + +实现三个函数,时间复杂度分别是 O(n) , O(n^2), O(nlog n),使用加法运算来统一测试。 + +```CPP +// O(n) +void function1(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + k++; + } +} + +``` + +```CPP +// O(n^2) +void function2(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long j = 0; j < n; j++) { + k++; + } + } + +} +``` + +```CPP +// O(nlogn) +void function3(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long long j = 1; j < n; j = j*2) { // 注意这里j=1 + k++; + } + } +} + +``` + +来看一下这三个函数随着n的规模变化,耗时会产生多大的变化,先测function1 ,就把 function2 和 function3 注释掉 + +```CPP +int main() { + long long n; // 数据规模 + while (1) { + cout << "输入n:"; + cin >> n; + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + function1(n); +// function2(n); +// function3(n); + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << "耗时:" << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +``` + +来看一下运行的效果,如下图: + +![程序超时2](https://file1.kamacoder.com/i/algo/20200729200018460.png) + +O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下O(n^2) 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。 + +![程序超时3](https://file1.kamacoder.com/i/algo/2020072919590970.png) + +O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。 + +在推测一下O(nlogn)的话, 1s可以处理的数据规模是什么呢? + +理论上应该是比 O(n)少一个数量级,因为logn的复杂度 其实是很快,看一下实验数据。 + +![程序超时4](https://file1.kamacoder.com/i/algo/20200729195729407.png) + +O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 + +这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。 + +**整体测试数据整理如下:** + +![程序超时1](https://file1.kamacoder.com/i/algo/20201208231559175.png) + +至于O(log n)和O(n^3) 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。 + +## 完整测试代码 + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +// O(n) +void function1(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + k++; + } +} + +// O(n^2) +void function2(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long j = 0; j < n; j++) { + k++; + } + } + +} +// O(nlogn) +void function3(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long long j = 1; j < n; j = j*2) { // 注意这里j=1 + k++; + } + } +} +int main() { + long long n; // 数据规模 + while (1) { + cout << "输入n:"; + cin >> n; + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + function1(n); +// function2(n); +// function3(n); + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << "耗时:" << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} + + +``` + +## 总结 + +本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看O(n)的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。 + +建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。 + +这样,大家应该对程序超时时候的数据规模有一个整体的认识了。 + + + + + diff --git "a/problems/images/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231-03.png" "b/problems/images/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231-03.png" new file mode 100644 index 0000000000..7ad2ced690 Binary files /dev/null and "b/problems/images/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231-03.png" differ diff --git a/problems/images/test b/problems/images/test new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pics/.DS_Store b/problems/kamacoder/.DS_Store similarity index 100% rename from pics/.DS_Store rename to problems/kamacoder/.DS_Store diff --git "a/problems/kamacoder/0044.\345\274\200\345\217\221\345\225\206\350\264\255\344\271\260\345\234\237\345\234\260.md" "b/problems/kamacoder/0044.\345\274\200\345\217\221\345\225\206\350\264\255\344\271\260\345\234\237\345\234\260.md" new file mode 100644 index 0000000000..cb5fbb7468 --- /dev/null +++ "b/problems/kamacoder/0044.\345\274\200\345\217\221\345\225\206\350\264\255\344\271\260\345\234\237\345\234\260.md" @@ -0,0 +1,616 @@ + +# 44. 开发商购买土地 + +> 本题为代码随想录后续扩充题目,还没有视频讲解,顺便让大家练习一下ACM输入输出模式(笔试面试必备) + +[题目链接](https://kamacoder.com/problempage.php?pid=1044) + +【题目描述】 + +在一个城市区域内,被划分成了n * m个连续的区块,每个区块都拥有不同的权值,代表着其土地价值。目前,有两家开发公司,A 公司和 B 公司,希望购买这个城市区域的土地。 + +现在,需要将这个城市区域的所有区块分配给 A 公司和 B 公司。 + +然而,由于城市规划的限制,只允许将区域按横向或纵向划分成两个子区域,而且每个子区域都必须包含一个或多个区块。 + +为了确保公平竞争,你需要找到一种分配方式,使得 A 公司和 B 公司各自的子区域内的土地总价值之差最小。 + +注意:区块不可再分。 + +【输入描述】 + +第一行输入两个正整数,代表 n 和 m。 + +接下来的 n 行,每行输出 m 个正整数。 + +输出描述 + +请输出一个整数,代表两个子区域内土地总价值之间的最小差距。 + +【输入示例】 + +3 3 +1 2 3 +2 1 3 +1 2 3 + +【输出示例】 + +0 + +【提示信息】 + +如果将区域按照如下方式划分: + +1 2 | 3 +2 1 | 3 +1 2 | 3 + +两个子区域内土地总价值之间的最小差距可以达到 0。 + +【数据范围】: + +* 1 <= n, m <= 100; +* n 和 m 不同时为 1。 + +## 思路 + +看到本题,大家如果想暴力求解,应该是 n^3 的时间复杂度, + +一个 for 枚举分割线, 嵌套 两个for 去累加区间里的和。 + +如果本题要求 任何两个行(或者列)之间的数值总和,大家在[0058.区间和](./0058.区间和.md) 的基础上 应该知道怎么求。 + +就是前缀和的思路,先统计好,前n行的和 q[n],如果要求矩阵 a行 到 b行 之间的总和,那么就 q[b] - q[a - 1]就好。 + +至于为什么是 a - 1,大家去看 [0058.区间和](./0058.区间和.md) 的分析,使用 前缀和 要注意 区间左右边的开闭情况。 + +本题也可以使用 前缀和的思路来求解,先将 行方向,和 列方向的和求出来,这样可以方便知道 划分的两个区间的和。 + +代码如下: + + +```CPP +#include +#include +#include + +using namespace std; +int main () { + int n, m; + cin >> n >> m; + int sum = 0; + vector> vec(n, vector(m, 0)) ; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> vec[i][j]; + sum += vec[i][j]; + } + } + // 统计横向 + vector horizontal(n, 0); + for (int i = 0; i < n; i++) { + for (int j = 0 ; j < m; j++) { + horizontal[i] += vec[i][j]; + } + } + // 统计纵向 + vector vertical(m , 0); + for (int j = 0; j < m; j++) { + for (int i = 0 ; i < n; i++) { + vertical[j] += vec[i][j]; + } + } + int result = INT_MAX; + int horizontalCut = 0; + for (int i = 0 ; i < n; i++) { + horizontalCut += horizontal[i]; + result = min(result, abs(sum - horizontalCut - horizontalCut)); + } + int verticalCut = 0; + for (int j = 0; j < m; j++) { + verticalCut += vertical[j]; + result = min(result, abs(sum - verticalCut - verticalCut)); + } + cout << result << endl; +} + +``` + +时间复杂度: O(n^2) + +其实本题可以在暴力求解的基础上,优化一下,就不用前缀和了,在行向遍历的时候,遇到行末尾就统一一下, 在列向遍历的时候,遇到列末尾就统计一下。 + +时间复杂度也是 O(n^2) + +代码如下: + +```CPP +#include +#include +#include + +using namespace std; +int main () { + int n, m; + cin >> n >> m; + int sum = 0; + vector> vec(n, vector(m, 0)) ; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> vec[i][j]; + sum += vec[i][j]; + } + } + + int result = INT_MAX; + int count = 0; // 统计遍历过的行 + for (int i = 0; i < n; i++) { + for (int j = 0 ; j < m; j++) { + count += vec[i][j]; + // 遍历到行末尾时候开始统计 + if (j == m - 1) result = min (result, abs(sum - count - count)); + + } + } + + count = 0; // 统计遍历过的列 + for (int j = 0; j < m; j++) { + for (int i = 0 ; i < n; i++) { + count += vec[i][j]; + // 遍历到列末尾的时候开始统计 + if (i == n - 1) result = min (result, abs(sum - count - count)); + } + } + cout << result << endl; +} + +``` + + + +## 其他语言版本 + +### Java + +前缀和 + +```Java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int sum = 0; + int[][] vec = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + vec[i][j] = scanner.nextInt(); + sum += vec[i][j]; + } + } + + // 统计横向 + int[] horizontal = new int[n]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + horizontal[i] += vec[i][j]; + } + } + + // 统计纵向 + int[] vertical = new int[m]; + for (int j = 0; j < m; j++) { + for (int i = 0; i < n; i++) { + vertical[j] += vec[i][j]; + } + } + + int result = Integer.MAX_VALUE; + int horizontalCut = 0; + for (int i = 0; i < n; i++) { + horizontalCut += horizontal[i]; + result = Math.min(result, Math.abs((sum - horizontalCut) - horizontalCut)); + // 更新result。其中,horizontalCut表示前i行的和,sum - horizontalCut表示剩下的和,作差、取绝对值,得到题目需要的“A和B各自的子区域内的土地总价值之差”。下同。 + } + + int verticalCut = 0; + for (int j = 0; j < m; j++) { + verticalCut += vertical[j]; + result = Math.min(result, Math.abs((sum - verticalCut) - verticalCut)); + } + + System.out.println(result); + scanner.close(); + } +} + +``` + +优化暴力 + +```Java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int sum = 0; + int[][] vec = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + vec[i][j] = scanner.nextInt(); + sum += vec[i][j]; + } + } + + int result = Integer.MAX_VALUE; + int count = 0; // 统计遍历过的行 + + // 行切分 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + count += vec[i][j]; + // 遍历到行末尾时候开始统计 + if (j == m - 1) { + result = Math.min(result, Math.abs(sum - 2 * count)); + } + } + } + + count = 0; + // 列切分 + for (int j = 0; j < m; j++) { + for (int i = 0; i < n; i++) { + count += vec[i][j]; + // 遍历到列末尾时候开始统计 + if (i == n - 1) { + result = Math.min(result, Math.abs(sum - 2 * count)); + } + } + } + + System.out.println(result); + scanner.close(); + } +} + +``` + +### python + +前缀和 +```python +def main(): + import sys + input = sys.stdin.read + data = input().split() + + idx = 0 + n = int(data[idx]) + idx += 1 + m = int(data[idx]) + idx += 1 + sum = 0 + vec = [] + for i in range(n): + row = [] + for j in range(m): + num = int(data[idx]) + idx += 1 + row.append(num) + sum += num + vec.append(row) + + # 统计横向 + horizontal = [0] * n + for i in range(n): + for j in range(m): + horizontal[i] += vec[i][j] + + # 统计纵向 + vertical = [0] * m + for j in range(m): + for i in range(n): + vertical[j] += vec[i][j] + + result = float('inf') + horizontalCut = 0 + for i in range(n): + horizontalCut += horizontal[i] + result = min(result, abs(sum - 2 * horizontalCut)) + + verticalCut = 0 + for j in range(m): + verticalCut += vertical[j] + result = min(result, abs(sum - 2 * verticalCut)) + + print(result) + +if __name__ == "__main__": + main() + + +``` + +优化暴力 + +```python +def main(): + import sys + input = sys.stdin.read + data = input().split() + + idx = 0 + n = int(data[idx]) + idx += 1 + m = int(data[idx]) + idx += 1 + sum = 0 + vec = [] + for i in range(n): + row = [] + for j in range(m): + num = int(data[idx]) + idx += 1 + row.append(num) + sum += num + vec.append(row) + + result = float('inf') + + count = 0 + # 行切分 + for i in range(n): + + for j in range(m): + count += vec[i][j] + # 遍历到行末尾时候开始统计 + if j == m - 1: + result = min(result, abs(sum - 2 * count)) + + count = 0 + # 列切分 + for j in range(m): + + for i in range(n): + count += vec[i][j] + # 遍历到列末尾时候开始统计 + if i == n - 1: + result = min(result, abs(sum - 2 * count)) + + print(result) + +if __name__ == "__main__": + main() + +``` + +### JavaScript + +前缀和 +```js +function func() { + const readline = require('readline') + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }) + let inputLines = [] + rl.on('line', function (line) { + inputLines.push(line.trim()) + }) + + rl.on('close', function () { + let [n, m] = inputLines[0].split(" ").map(Number) + let c = new Array(n).fill(0) + let r = new Array(m).fill(0) + let arr = new Array(n) + let sum = 0//数组总和 + let min = Infinity//设置最小值的初始值为无限大 + //定义数组 + for (let s = 0; s < n; s++) { + arr[s] = inputLines[s + 1].split(" ").map(Number) + } + //每一行的和 + for (let i = 0; i < n; i++) { + for (let j = 0; j < m; j++) { + c[i] += arr[i][j] + sum += arr[i][j] + } + } + //每一列的和 + for (let i = 0; i < n; i++) { + for (let j = 0; j < m; j++) { + r[j] += arr[i][j] + } + } + let sum1 = 0, sum2 = 0 + //横向切割 + for (let i = 0; i < n; i++) { + sum1 += c[i] + min = min < Math.abs(sum - 2 * sum1) ? min : Math.abs(sum - 2 * sum1) + } + //纵向切割 + for (let j = 0; j < m; j++) { + sum2 += r[j] + min = min < Math.abs(sum - 2 * sum2) ? min : Math.abs(sum - 2 * sum2) + } + console.log(min); + }) +} +``` + +### C + +前缀和 +```c +#include +#include + +int main() +{ + int n = 0, m = 0, ret_ver = 0, ret_hor = 0; + + // 读取行和列的值 + scanf("%d%d", &n, &m); + // 动态分配数组a(横)和b(纵)的空间 + int *a = (int *)malloc(sizeof(int) * n); + int *b = (int *)malloc(sizeof(int) * m); + + // 初始化数组a和b + for (int i = 0; i < n; i++) + { + a[i] = 0; + } + for (int i = 0; i < m; i++) + { + b[i] = 0; + } + + // 读取区块权值并计算每行和每列的总权值 + for (int i = 0; i < n; i++) + { + for (int j = 0; j < m; j++) + { + int tmp; + scanf("%d", &tmp); + a[i] += tmp; + b[j] += tmp; + } + } + + // 计算每列以及每行的前缀和 + for (int i = 1; i < n; i++) + { + a[i] += a[i - 1]; + } + for (int i = 1; i < m; i++) + { + b[i] += b[i - 1]; + } + + // 初始化ret_ver和ret_hor为最大可能值 + ret_hor = a[n - 1]; + ret_ver = b[m - 1]; + + // 计算按行划分的最小差异 + int ret2 = 0; + while (ret2 < n) + { + ret_hor = (ret_hor > abs(a[n - 1] - 2 * a[ret2])) ? abs(a[n - 1] - 2 * a[ret2]) : ret_hor; + // 原理同列,但更高级 + ret2++; + } + // 计算按列划分的最小差异 + int ret1 = 0; + while (ret1 < m) + { + if (ret_ver > abs(b[m - 1] - 2 * b[ret1])) + { + ret_ver = abs(b[m - 1] - 2 * b[ret1]); + } + ret1++; + } + + // 输出最小差异 + printf("%d\n", (ret_ver <= ret_hor) ? ret_ver : ret_hor); + + // 释放分配的内存 + free(a); + free(b); + return 0; +} + +``` + +### Go + +前缀和 + +```go +package main + +import ( + "fmt" + "os" + "bufio" + "strings" + "strconv" + "math" +) + +func main() { + var n, m int + + reader := bufio.NewReader(os.Stdin) + + line, _ := reader.ReadString('\n') + line = strings.TrimSpace(line) + params := strings.Split(line, " ") + + n, _ = strconv.Atoi(params[0]) + m, _ = strconv.Atoi(params[1])//n和m读取完成 + + land := make([][]int, n)//土地矩阵初始化 + + for i := 0; i < n; i++ { + line, _ := reader.ReadString('\n') + line = strings.TrimSpace(line) + values := strings.Split(line, " ") + land[i] = make([]int, m) + for j := 0; j < m; j++ { + value, _ := strconv.Atoi(values[j]) + land[i][j] = value + } + }//所有读取完成 + + //初始化前缀和矩阵 + preMatrix := make([][]int, n+1) + for i := 0; i <= n; i++ { + preMatrix[i] = make([]int, m+1) + } + + for a := 1; a < n+1; a++ { + for b := 1; b < m+1; b++ { + preMatrix[a][b] = land[a-1][b-1] + preMatrix[a-1][b] + preMatrix[a][b-1] - preMatrix[a-1][b-1] + } + } + + totalSum := preMatrix[n][m] + + minDiff := math.MaxInt32//初始化极大数,用于比较 + + //按行分割 + for i := 1; i < n; i++ { + topSum := preMatrix[i][m] + + bottomSum := totalSum - topSum + + diff := int(math.Abs(float64(topSum - bottomSum))) + if diff < minDiff { + minDiff = diff + } + } + + //按列分割 + for j := 1; j < m; j++ { + topSum := preMatrix[n][j] + + bottomSum := totalSum - topSum + + diff := int(math.Abs(float64(topSum - bottomSum))) + if diff < minDiff { + minDiff = diff + } + } + + fmt.Println(minDiff) +} +``` + +
diff --git "a/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\345\240\206.md" "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\345\240\206.md" new file mode 100644 index 0000000000..80d7851eaf --- /dev/null +++ "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\345\240\206.md" @@ -0,0 +1,930 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# dijkstra(堆优化版)精讲 + +[卡码网:47. 参加科学大会](https://kamacoder.com/problempage.php?pid=1047) + +【题目描述】 + +小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。 + +小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。 + +小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。 + +接下来为 M 行,每行包括三个整数,S、E 和 V,代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。 + +【输出描述】 + +输出一个整数,代表小明从起点到终点所花费的最小时间。 + +输入示例 + +``` +7 9 +1 2 1 +1 3 4 +2 3 2 +2 4 5 +3 4 2 +4 5 3 +2 6 4 +5 7 4 +6 7 9 +``` + +输出示例:12 + +【提示信息】 + +能够到达的情况: + +如下图所示,起始车站为 1 号车站,终点车站为 7 号车站,绿色路线为最短的路线,路线总长度为 12,则输出 12。 + +![](https://file1.kamacoder.com/i/algo/20240227101345.png) + +不能到达的情况: + +如下图所示,当从起始车站不能到达终点车站时,则输出 -1。 + +![](https://file1.kamacoder.com/i/algo/20240227101401.png) + +数据范围: + +1 <= N <= 500; +1 <= M <= 5000; + + +## 思路 + +> 本篇我们来讲解 堆优化版dijkstra,看本篇之前,一定要先看 我讲解的 朴素版dijkstra,否则本篇会有部分内容看不懂。 + +在上一篇中,我们讲解了朴素版的dijkstra,该解法的时间复杂度为 O(n^2),可以看出时间复杂度 只和 n (节点数量)有关系。 + +如果n很大的话,我们可以换一个角度来优先性能。 + +在 讲解 最小生成树的时候,我们 讲了两个算法,[prim算法](./0053.寻宝-prim.md)(从点的角度来求最小生成树)、[Kruskal算法](./0053.寻宝-Kruskal.md)(从边的角度来求最小生成树) + +这么在n 很大的时候,也有另一个思考维度,即:从边的数量出发。 + +当 n 很大,边 的数量 也很多的时候(稠密图),那么 上述解法没问题。 + +但 n 很大,边 的数量 很小的时候(稀疏图),是不是可以换成从边的角度来求最短路呢? + +毕竟边的数量少。 + +有的录友可能会想,n (节点数量)很大,边不就多吗? 怎么会边的数量少呢? + +别忘了,谁也没有规定 节点之间一定要有边连接着,例如有一万个节点,只有一条边,这也是一张图。 + +了解背景之后,再来看 解法思路。 + +### 图的存储 + +首先是 图的存储。 + +关于图的存储 主流有两种方式: 邻接矩阵和邻接表 + +#### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +例如: grid[2][5] = 6,表示 节点 2 链接 节点5 为有向图,节点2 指向 节点5,边的权值为6 (套在题意里,可能是距离为6 或者 消耗为6 等等) + +如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。 + + +如图: + +![](https://file1.kamacoder.com/i/algo/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间,有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点链接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +#### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4,节点4指向节点1。 + +有多少边 邻接表才会申请多少个对应的链表节点。 + +从图中可以直观看出 使用 数组 + 链表 来表达 边的链接情况 。 + +邻接表的优点: + +* 对于稀疏图的存储,只需要存储边,空间利用率高 +* 遍历节点链接情况相对容易 + +缺点: + +* 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点链接其他节点的数量。 +* 实现相对复杂,不易理解 + +#### 本题图的存储 + +接下来我们继续按照稀疏图的角度来分析本题。 + +在第一个版本的实现思路中,我们提到了三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +在第一个版本的代码中,这三部曲是套在一个 for 循环里,为什么? + +因为我们是从节点的角度来解决问题。 + +三部曲中第一步(选源点到哪个节点近且该节点未被访问过),这个操作本身需要for循环遍历 minDist 来寻找最近的节点。 + +同时我们需要 遍历所有 未访问过的节点,所以 我们从 节点角度出发,代码会有两层for循环,代码是这样的: (注意代码中的注释,标记两层for循环的用处) + +```CPP + +for (int i = 1; i <= n; i++) { // 遍历所有节点,第一层for循环 + + int minVal = INT_MAX; + int cur = 1; + + // 1、选距离源点最近且未访问过的节点 , 第二层for循环 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + +} +``` + +那么当从 边 的角度出发, 在处理 三部曲里的第一步(选源点到哪个节点近且该节点未被访问过)的时候 ,我们可以不用去遍历所有节点了。 + +而且 直接把 边(带权值)加入到 小顶堆(利用堆来自动排序),那么每次我们从 堆顶里 取出 边 自然就是 距离源点最近的节点所在的边。 + +这样我们就不需要两层for循环来寻找最近的节点了。 + +了解了大体思路,我们再来看代码实现。 + +首先是 如何使用 邻接表来表述图结构,这是摆在很多录友面前的第一个难题。 + +邻接表用 数组+链表 来表示,代码如下:(C++中 vector 为数组,list 为链表, 定义了 n+1 这么大的数组空间) + +```CPP +vector> grid(n + 1); +``` + +不少录友,不知道 如何定义的数据结构,怎么表示邻接表的,我来给大家画一个图: + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +图中邻接表表示: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4 指向 节点1 + +大家发现图中的边没有权值,而本题中 我们的边是有权值的,权值怎么表示?在哪里表示? + +所以 在`vector> grid(n + 1);` 中 就不能使用int了,而是需要一个键值对 来存两个数字,一个数表示节点,一个数表示 指向该节点的这条边的权值。 + +那么 代码可以改成这样: (pair 为键值对,可以存放两个int) + +```CPP +vector>> grid(n + 1); +``` + +举例来给大家展示 该代码表达的数据 如下: + +![](https://file1.kamacoder.com/i/algo/20240223103904.png) + +* 节点1 指向 节点3 权值为 1 +* 节点1 指向 节点5 权值为 2 +* 节点2 指向 节点4 权值为 7 +* 节点2 指向 节点3 权值为 6 +* 节点2 指向 节点5 权值为 3 +* 节点3 指向 节点4 权值为 3 +* 节点5 指向 节点1 权值为 10 + +这样 我们就把图中权值表示出来了。 + +但是在代码中 使用 `pair` 很容易让我们搞混了,第一个int 表示什么,第二个int表示什么,导致代码可读性很差,或者说别人看你的代码看不懂。 + +那么 可以 定一个类 来取代 `pair` + +类(或者说是结构体)定义如下: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +这个类里有两个成员变量,有对应的命名,这样不容易搞混 两个int的含义。 + +所以 本题中邻接表的定义如下: + +```CPP +struct Edge { + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +vector> grid(n + 1); // 邻接表 + +``` + +(我们在下面的讲解中会直接使用这个邻接表的代码表示方式) + +### 堆优化细节 + +其实思路依然是 dijkstra 三部曲: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +只不过之前是 通过遍历节点来遍历边,通过两层for循环来寻找距离源点最近节点。 这次我们直接遍历边,且通过堆来对边进行排序,达到直接选择距离源点最近节点。 + +先来看一下针对这三部曲,如果用 堆来优化。 + +那么三部曲中的第一步(选源点到哪个节点近且该节点未被访问过),我们如何选? + +我们要选择距离源点近的节点(即:该边的权值最小),所以 我们需要一个 小顶堆 来帮我们对边的权值排序,每次从小顶堆堆顶 取边就是权值最小的边。 + +C++定义小顶堆,可以用优先级队列实现,代码如下: + +```CPP +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; +// 优先队列中存放 pair<节点编号,源点到该节点的权值> +priority_queue, vector>, mycomparison> pq; +``` + +(`pair`中 第二个int 为什么要存 源点到该节点的权值,因为 这个小顶堆需要按照权值来排序) + + +有了小顶堆自动对边的权值排序,那我们只需要直接从 堆里取堆顶元素(小顶堆中,最小的权值在上面),就可以取到离源点最近的节点了 (未访问过的节点,不会加到堆里进行排序) + +所以三部曲中的第一步,我们不用 for循环去遍历,直接取堆顶元素: + +```CPP +// pair<节点编号,源点到该节点的权值> +pair cur = pq.top(); pq.pop(); + +``` + +第二步(该最近节点被标记访问过) 这个就是将 节点做访问标记,和 朴素dijkstra 一样 ,代码如下: + +```CPP +// 2. 第二步,该最近节点被标记访问过 +visited[cur.first] = true; + +``` + +(`cur.first` 是指取 `pair` 里的第一个int,即节点编号 ) + +第三步(更新非访问节点到源点的距离),这里的思路 也是 和朴素dijkstra一样的。 + +但很多录友对这里是最懵的,主要是因为两点: + +* 没有理解透彻 dijkstra 的思路 +* 没有理解 邻接表的表达方式 + +我们来回顾一下 朴素dijkstra 在这一步的代码和思路(如果没看过我讲解的朴素版dijkstra,这里会看不懂) + +```CPP + +// 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +其中 for循环是用来做什么的? 是为了 找到 节点cur 链接指向了哪些节点,因为使用邻接矩阵的表达方式 所以把所有节点遍历一遍。 + +而在邻接表中,我们可以以相对高效的方式知道一个节点链接指向哪些节点。 + +再回顾一下邻接表的构造(数组 + 链表): + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +假如 加入的cur 是节点 2, 那么 grid[2] 表示的就是图中第二行链表。 (grid数组的构造我们在 上面 「图的存储」中讲过) + +所以在邻接表中,我们要获取 节点cur 链接指向哪些节点,就是遍历 grid[cur节点编号] 这个链表。 + +这个遍历方式,C++代码如下: + +```CPP +for (Edge edge : grid[cur.first]) +``` + +(如果不知道 Edge 是什么,看上面「图的存储」中邻接表的讲解) + +`cur.first` 就是cur节点编号, 参考上面pair的定义: pair<节点编号,源点到该节点的权值> + +接下来就是更新 非访问节点到源点的距离,代码实现和 朴素dijkstra 是一样的,代码如下: + +```CPP +// 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) +for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } +} +``` + +但为什么思路一样,有的录友能写出朴素dijkstra,但堆优化这里的逻辑就是写不出来呢? + +**主要就是因为对邻接表的表达方式不熟悉**! + +以上代码中,cur 链接指向的节点编号 为 edge.to, 这条边的权值为 edge.val ,如果对这里模糊的就再回顾一下 Edge的定义: + +```CPP +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; +``` + +确定该节点没有被访问过,`!visited[edge.to]` , 目前 源点到cur.first的最短距离(minDist) + cur.first 到 edge.to 的距离 (edge.val) 是否 小于 minDist已经记录的 源点到 edge.to 的距离 (minDist[edge.to]) + +如果是的话,就开始更新操作。 + +即: + +```CPP +if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); // 由于cur节点的加入,而新链接的边,加入到优先级队里中 +} + +``` + +同时,由于cur节点的加入,源点又有可以新链接到的边,将这些边加入到优先级队里中。 + + +以上代码思路 和 朴素版dijkstra 是一样一样的,主要区别是两点: + +* 邻接表的表示方式不同 +* 使用优先级队列(小顶堆)来对新链接的边排序 + +### 代码实现 + +堆优化dijkstra完整代码如下: + +```CPP +#include +#include +#include +#include +#include +using namespace std; +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; +// 定义一个结构体来表示带权重的边 +struct Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + + } + + int start = 1; // 起点 + int end = n; // 终点 + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 pair<节点,源点到该节点的权值> + priority_queue, vector>, mycomparison> pq; + + + // 初始化队列,源点到源点的距离为0,所以初始为0 + pq.push(pair(start, 0)); + + minDist[start] = 0; // 起始点到自身的距离为0 + + while (!pq.empty()) { + // 1. 第一步,选源点到哪个节点近且该节点未被访问过 (通过优先级队列来实现) + // <节点, 源点到该节点的距离> + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + // 2. 第二步,该最近节点被标记访问过 + visited[cur.first] = true; + + // 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (Edge edge : grid[cur.first]) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.push(pair(edge.to, minDist[edge.to])); + } + } + + } + + if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 +} + +``` + +* 时间复杂度:O(ElogE) E 为边的数量 +* 空间复杂度:O(N + E) N 为节点的数量 + +堆优化的时间复杂度 只和边的数量有关 和节点数无关,在 优先级队列中 放的也是边。 + +以上代码中,`while (!pq.empty())` 里套了 `for (Edge edge : grid[cur.first])` + +`for` 里 遍历的是 当前节点 cur 所连接边。 + +那 当前节点cur 所连接的边 也是不固定的, 这就让大家分不清,这时间复杂度究竟是多少? + +其实 `for (Edge edge : grid[cur.first])` 里最终的数据走向 是 给队列里添加边。 + +那么跳出局部代码,整个队列 一定是 所有边添加了一次,同时也弹出了一次。 + +所以边添加一次时间复杂度是 O(E), `while (!pq.empty())` 里每次都要弹出一个边来进行操作,在优先级队列(小顶堆)中 弹出一个元素的时间复杂度是 O(logE) ,这是堆排序的时间复杂度。 + +(当然小顶堆里 是 添加元素的时候 排序,还是 取数元素的时候排序,这个无所谓,时间复杂度都是O(E),总之是一定要排序的,而小顶堆里也不会滞留元素,有多少元素添加 一定就有多少元素弹出) + +所以 该算法整体时间复杂度为 O(ElogE) + +网上的不少分析 会把 n (节点的数量)算进来,这个分析是有问题的,举一个极端例子,在n 为 10000,且是有一条边的 图里,以上代码,大家感觉执行了多少次? + +`while (!pq.empty())` 中的 pq 存的是边,其实只执行了一次。 + +所以该算法时间复杂度 和 节点没有关系。 + +至于空间复杂度,邻接表是 数组 + 链表 数组的空间 是 N ,有E条边 就申请对应多少个链表节点,所以是 复杂度是 N + E + +## 拓展 + +当然也有录友可能想 堆优化dijkstra 中 我为什么一定要用邻接表呢,我就用邻接矩阵 行不行 ? + +也行的。 + +但 正是因为稀疏图,所以我们使用堆优化的思路, 如果我们还用 邻接矩阵 去表达这个图的话,就是 **一个高效的算法 使用了低效的数据结构,那么 整体算法效率 依然是低的**。 + +如果还不清楚为什么要使用 邻接表,可以再看看上面 我在 「图的存储」标题下的讲解。 + +这里我也给出 邻接矩阵版本的堆优化dijkstra代码: + +```CPP +#include +#include +#include +#include +using namespace std; +// 小顶堆 +class mycomparison { +public: + bool operator()(const pair& lhs, const pair& rhs) { + return lhs.second > rhs.second; + } +}; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1][p2] = val; + } + + int start = 1; // 起点 + int end = n; // 终点 + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + // 优先队列中存放 pair<节点,源点到该节点的距离> + priority_queue, vector>, mycomparison> pq; + + + // 初始化队列,源点到源点的距离为0,所以初始为0 + pq.push(pair(start, 0)); + + minDist[start] = 0; // 起始点到自身的距离为0 + + while (!pq.empty()) { + // <节点, 源点到该节点的距离> + // 1、选距离源点最近且未访问过的节点 + pair cur = pq.top(); pq.pop(); + + if (visited[cur.first]) continue; + + visited[cur.first] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int j = 1; j <= n; j++) { + if (!visited[j] && grid[cur.first][j] != INT_MAX && (minDist[cur.first] + grid[cur.first][j] < minDist[j])) { + minDist[j] = minDist[cur.first] + grid[cur.first][j]; + pq.push(pair(j, minDist[j])); + } + } + } + + if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} + +``` + +* 时间复杂度:O(E * (N + logE)) E为边的数量,N为节点数量 +* 空间复杂度:O(log(N^2)) + +`while (!pq.empty())` 时间复杂度为 E ,while 里面 每次取元素 时间复杂度 为 logE,和 一个for循环 时间复杂度 为 N 。 + +所以整体是 E * (N + logE) + + +## 总结 + +在学习一种优化思路的时候,首先就要知道为什么要优化,遇到了什么问题。 + +正如我在开篇就给大家交代清楚 堆优化方式的背景。 + +堆优化的整体思路和 朴素版是大体一样的,区别是 堆优化从边的角度出发且利用堆来排序。 + +很多录友别说写堆优化 就是看 堆优化的代码也看的很懵。 + +主要是因为两点: + +* 不熟悉邻接表的表达方式 +* 对dijkstra的实现思路还是不熟 + +这是我为什么 本篇花了大力气来讲解 图的存储,就是为了让大家彻底理解邻接表以及邻接表的代码写法。 + +至于 dijkstra的实现思路 ,朴素版 和 堆优化版本 都是 按照 dijkstra 三部曲来的。 + +理解了三部曲,dijkstra 的思路就是清晰的。 + +针对邻接表版本代码 我做了详细的 时间复杂度分析,也让录友们清楚,相对于 朴素版,时间都优化到哪了。 + +最后 我也给出了 邻接矩阵的版本代码,分析了这一版本的必要性以及时间复杂度。 + +至此通过 两篇dijkstra的文章,终于把 dijkstra 讲完了,如果大家对我讲解里所涉及的内容都吃透的话,详细对 dijkstra 算法也就理解到位了。 + + + +## 其他语言版本 + +### Java + +```Java + +import java.util.*; + +class Edge { + int to; // 邻接顶点 + int val; // 边的权重 + + Edge(int to, int val) { + this.to = to; + this.val = val; + } +} + +class MyComparison implements Comparator> { + @Override + public int compare(Pair lhs, Pair rhs) { + return Integer.compare(lhs.second, rhs.second); + } +} + +class Pair { + public final U first; + public final V second; + + public Pair(U first, V second) { + this.first = first; + this.second = second; + } +} + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + List> grid = new ArrayList<>(n + 1); + for (int i = 0; i <= n; i++) { + grid.add(new ArrayList<>()); + } + + for (int i = 0; i < m; i++) { + int p1 = scanner.nextInt(); + int p2 = scanner.nextInt(); + int val = scanner.nextInt(); + grid.get(p1).add(new Edge(p2, val)); + } + + int start = 1; // 起点 + int end = n; // 终点 + + // 存储从源点到每个节点的最短距离 + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + + // 记录顶点是否被访问过 + boolean[] visited = new boolean[n + 1]; + + // 优先队列中存放 Pair<节点,源点到该节点的权值> + PriorityQueue> pq = new PriorityQueue<>(new MyComparison()); + + // 初始化队列,源点到源点的距离为0,所以初始为0 + pq.add(new Pair<>(start, 0)); + + minDist[start] = 0; // 起始点到自身的距离为0 + + while (!pq.isEmpty()) { + // 1. 第一步,选源点到哪个节点近且该节点未被访问过(通过优先级队列来实现) + // <节点, 源点到该节点的距离> + Pair cur = pq.poll(); + + if (visited[cur.first]) continue; + + // 2. 第二步,该最近节点被标记访问过 + visited[cur.first] = true; + + // 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (Edge edge : grid.get(cur.first)) { // 遍历 cur指向的节点,cur指向的节点为 edge + // cur指向的节点edge.to,这条边的权值为 edge.val + if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist + minDist[edge.to] = minDist[cur.first] + edge.val; + pq.add(new Pair<>(edge.to, minDist[edge.to])); + } + } + } + + if (minDist[end] == Integer.MAX_VALUE) { + System.out.println(-1); // 不能到达终点 + } else { + System.out.println(minDist[end]); // 到达终点最短路径 + } + } +} + +``` + + +### Python + +```python +import heapq + +class Edge: + def __init__(self, to, val): + self.to = to + self.val = val + +def dijkstra(n, m, edges, start, end): + grid = [[] for _ in range(n + 1)] + + for p1, p2, val in edges: + grid[p1].append(Edge(p2, val)) + + minDist = [float('inf')] * (n + 1) + visited = [False] * (n + 1) + + pq = [] + heapq.heappush(pq, (0, start)) + minDist[start] = 0 + + while pq: + cur_dist, cur_node = heapq.heappop(pq) + + if visited[cur_node]: + continue + + visited[cur_node] = True + + for edge in grid[cur_node]: + if not visited[edge.to] and cur_dist + edge.val < minDist[edge.to]: + minDist[edge.to] = cur_dist + edge.val + heapq.heappush(pq, (minDist[edge.to], edge.to)) + + return -1 if minDist[end] == float('inf') else minDist[end] + +# 输入 +n, m = map(int, input().split()) +edges = [tuple(map(int, input().split())) for _ in range(m)] +start = 1 # 起点 +end = n # 终点 + +# 运行算法并输出结果 +result = dijkstra(n, m, edges, start, end) +print(result) + +``` + +### Go + +```go +package main + +import ( + "container/heap" + "fmt" + "math" +) + +// Edge 表示带权重的边 +type Edge struct { + to, val int +} + +// PriorityQueue 实现一个小顶堆 +type Item struct { + node, dist int +} + +type PriorityQueue []*Item + +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{}) { + *pq = append(*pq, x.(*Item)) +} + +func (pq *PriorityQueue) Pop() interface{} { + old := *pq + n := len(old) + item := old[n-1] + *pq = old[0 : n-1] + return item +} + +func dijkstra(n, m int, edges [][]int, start, end int) int { + grid := make([][]Edge, n+1) + for _, edge := range edges { + p1, p2, val := edge[0], edge[1], edge[2] + grid[p1] = append(grid[p1], Edge{to: p2, val: val}) + } + + minDist := make([]int, n+1) + for i := range minDist { + minDist[i] = math.MaxInt64 + } + visited := make([]bool, n+1) + + pq := &PriorityQueue{} + heap.Init(pq) + heap.Push(pq, &Item{node: start, dist: 0}) + minDist[start] = 0 + + for pq.Len() > 0 { + cur := heap.Pop(pq).(*Item) + + if visited[cur.node] { + continue + } + + visited[cur.node] = true + + for _, edge := range grid[cur.node] { + if !visited[edge.to] && minDist[cur.node]+edge.val < minDist[edge.to] { + minDist[edge.to] = minDist[cur.node] + edge.val + heap.Push(pq, &Item{node: edge.to, dist: minDist[edge.to]}) + } + } + } + + if minDist[end] == math.MaxInt64 { + return -1 + } + return minDist[end] +} + +func main() { + var n, m int + fmt.Scan(&n, &m) + + edges := make([][]int, m) + for i := 0; i < m; i++ { + var p1, p2, val int + fmt.Scan(&p1, &p2, &val) + edges[i] = []int{p1, p2, val} + } + + start := 1 // 起点 + end := n // 终点 + + result := dijkstra(n, m, edges, start, end) + fmt.Println(result) +} + +``` + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\346\234\264\347\264\240.md" "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\346\234\264\347\264\240.md" new file mode 100644 index 0000000000..42099df92a --- /dev/null +++ "b/problems/kamacoder/0047.\345\217\202\344\274\232dijkstra\346\234\264\347\264\240.md" @@ -0,0 +1,945 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# dijkstra(朴素版)精讲 + +[卡码网:47. 参加科学大会](https://kamacoder.com/problempage.php?pid=1047) + +【题目描述】 + +小明是一位科学家,他需要参加一场重要的国际科学大会,以展示自己的最新研究成果。 + +小明的起点是第一个车站,终点是最后一个车站。然而,途中的各个车站之间的道路状况、交通拥堵程度以及可能的自然因素(如天气变化)等不同,这些因素都会影响每条路径的通行时间。 + +小明希望能选择一条花费时间最少的路线,以确保他能够尽快到达目的地。 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 N 表示一共有 N 个公共汽车站,第二个正整数 M 表示有 M 条公路。 + +接下来为 M 行,每行包括三个整数,S、E 和 V,代表了从 S 车站可以单向直达 E 车站,并且需要花费 V 单位的时间。 + +【输出描述】 + +输出一个整数,代表小明从起点到终点所花费的最小时间。 + +输入示例 + +``` +7 9 +1 2 1 +1 3 4 +2 3 2 +2 4 5 +3 4 2 +4 5 3 +2 6 4 +5 7 4 +6 7 9 +``` + +输出示例:12 + +【提示信息】 + +能够到达的情况: + +如下图所示,起始车站为 1 号车站,终点车站为 7 号车站,绿色路线为最短的路线,路线总长度为 12,则输出 12。 + +![](https://file1.kamacoder.com/i/algo/20240227101345.png) + +不能到达的情况: + +如下图所示,当从起始车站不能到达终点车站时,则输出 -1。 + +![](https://file1.kamacoder.com/i/algo/20240227101401.png) + +数据范围: + +1 <= N <= 500; +1 <= M <= 5000; + +## 思路 + +本题就是求最短路,最短路是图论中的经典问题即:给出一个有向图,一个起点,一个终点,问起点到终点的最短路径。 + +接下来,我们来详细讲解最短路算法中的 dijkstra 算法。 + +dijkstra算法:在有权图(权值非负数)中求从起点到其他节点的最短路径算法。 + +需要注意两点: + +* dijkstra 算法可以同时求 起点到所有节点的最短路径 +* 权值不能为负数 + +(这两点后面我们会讲到) + +如本题示例中的图: + +![](https://file1.kamacoder.com/i/algo/20240125162647.png) + +起点(节点1)到终点(节点7) 的最短路径是 图中 标记绿线的部分。 + +最短路径的权值为12。 + +其实 dijkstra 算法 和 我们之前讲解的prim算法思路非常接近,如果大家认真学过[prim算法](./0053.寻宝-prim.md),那么理解 Dijkstra 算法会相对容易很多。(这也是我要先讲prim再讲dijkstra的原因) + +dijkstra 算法 同样是贪心的思路,不断寻找距离 源点最近的没有访问过的节点。 + +这里我也给出 **dijkstra三部曲**: + +1. 第一步,选源点到哪个节点近且该节点未被访问过 +2. 第二步,该最近节点被标记访问过 +3. 第三步,更新非访问节点到源点的距离(即更新minDist数组) + +大家此时已经会发现,这和prim算法 怎么这么像呢。 + +我在[prim算法](./0053.寻宝-prim.md)讲解中也给出了三部曲。 prim 和 dijkstra 确实很像,思路也是类似的,这一点我在后面还会详细来讲。 + +在dijkstra算法中,同样有一个数组很重要,起名为:minDist。 + +**minDist数组 用来记录 每一个节点距离源点的最小距离**。 + +理解这一点很重要,也是理解 dijkstra 算法的核心所在。 + +大家现在看着可能有点懵,不知道什么意思。 + +没关系,先让大家有一个印象,对理解后面讲解有帮助。 + +我们先来画图看一下 dijkstra 的工作过程,以本题示例为例: (以下为朴素版dijkstra的思路) + +(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从 1 开始计数,下标0 就不使用了,这样 下标和节点标号就可以对应上了,避免大家搞混**) + +## 朴素版dijkstra + +### 模拟过程 + +----------- + +0、初始化 + +minDist数组数值初始化为int最大值。 + +这里在强点一下 **minDist数组的含义:记录所有节点到源点的最短路径**,那么初始化的时候就应该初始为最大值,这样才能在后续出现最短路径的时候及时更新。 + +![](https://file1.kamacoder.com/i/algo/20240130115306.png) + +(图中,max 表示默认值,节点0 不做处理,统一从下标1 开始计算,这样下标和节点数值统一, 方便大家理解,避免搞混) + +源点(节点1) 到自己的距离为0,所以 minDist[1] = 0 + +此时所有节点都没有被访问过,所以 visited数组都为0 + +--------------- + +以下为dijkstra 三部曲 + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240130115421.png) + + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为1,小于原minDist[2]的数值max,更新minDist[2] = 1 +* 源点到节点3的最短距离为4,小于原minDist[3]的数值max,更新minDist[3] = 4 + +可能有录友问:为啥和 minDist[2] 比较? + +再强调一下 minDist[2] 的含义,它表示源点到节点2的最短距离,那么目前我们得到了 源点到节点2的最短距离为1,小于默认值max,所以更新。 minDist[3]的更新同理 + + +------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点到节点2距离最近,选节点2 + +2、该最近节点被标记访问过 + +节点2被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + + +![](https://file1.kamacoder.com/i/algo/20240130121240.png) + +更新 minDist数组,即:源点(节点1) 到 节点6 、 节点3 和 节点4的距离。 + +**为什么更新这些节点呢? 怎么不更新其他节点呢**? + +因为 源点(节点1)通过 已经计算过的节点(节点2) 可以链接到的节点 有 节点3,节点4和节点6. + + +更新 minDist数组: + +* 源点到节点6的最短距离为5,小于原minDist[6]的数值max,更新minDist[6] = 5 +* 源点到节点3的最短距离为3,小于原minDist[3]的数值4,更新minDist[3] = 3 +* 源点到节点4的最短距离为6,小于原minDist[4]的数值max,更新minDist[4] = 6 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +未访问过的节点中,源点距离哪些节点最近,怎么算的? + +其实就是看 minDist数组里的数值,minDist 记录了 源点到所有节点的最近距离,结合visited数组筛选出未访问的节点就好。 + +从 上面的图,或者 从minDist数组中,我们都能看出 未访问过的节点中,源点(节点1)到节点3距离最近。 + + +2、该最近节点被标记访问过 + +节点3被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240130120434.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +更新 minDist数组: + +* 源点到节点4的最短距离为5,小于原minDist[4]的数值6,更新minDist[4] = 5 + +------------------ + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,有节点4 和 节点6,距离源点距离都是 5 (minDist[4] = 5,minDist[6] = 5) ,选哪个节点都可以。 + +2、该最近节点被标记访问过 + +节点4被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201105335.png) + +由于节点4的加入,那么源点可以链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为8,小于原minDist[5]的数值max,更新minDist[5] = 8 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点6,距离源点距离是 5 (minDist[6] = 5) + + +2、该最近节点被标记访问过 + +节点6 被标记访问过 + + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110250.png) + +由于节点6的加入,那么源点可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为14,小于原minDist[7]的数值max,更新minDist[7] = 14 + + + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点5,距离源点距离是 8 (minDist[5] = 8) + +2、该最近节点被标记访问过 + +节点5 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110651.png) + +由于节点5的加入,那么源点有新的路径可以链接到节点7 所以 更新minDist数组: + +* 源点到节点7的最短距离为12,小于原minDist[7]的数值14,更新minDist[7] = 12 + +----------------- + +1、选源点到哪个节点近且该节点未被访问过 + +距离源点最近且没有被访问过的节点,是节点7(终点),距离源点距离是 12 (minDist[7] = 12) + +2、该最近节点被标记访问过 + +节点7 被标记访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240201110920.png) + +节点7加入,但节点7到节点7的距离为0,所以 不用更新minDist数组 + +-------------------- + +最后我们要求起点(节点1) 到终点 (节点7)的距离。 + +再来回顾一下minDist数组的含义:记录 每一个节点距离源点的最小距离。 + +那么起到(节点1)到终点(节点7)的最短距离就是 minDist[7] ,按上面举例讲解来说,minDist[7] = 12,节点1 到节点7的最短路径为 12。 + +路径如图: + +![](https://file1.kamacoder.com/i/algo/20240201111352.png) + +在上面的讲解中,每一步 我都是按照 dijkstra 三部曲来讲解的,理解了这三部曲,代码也就好懂的。 + +### 代码实现 + +本题代码如下,里面的 三部曲 我都做了注释,大家按照我上面的讲解 来看如下代码: + +```CPP +#include +#include +#include +using namespace std; +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + // 存储从源点到每个节点的最短距离 + std::vector minDist(n + 1, INT_MAX); + + // 记录顶点是否被访问过 + std::vector visited(n + 1, false); + + minDist[start] = 0; // 起始点到自身的距离为0 + + for (int i = 1; i <= n; i++) { // 遍历所有节点 + + int minVal = INT_MAX; + int cur = 1; + + // 1、选距离源点最近且未访问过的节点 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + + } + + if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n^2) + +### debug方法 + +写这种题目难免会有各种各样的问题,我们如何发现自己的代码是否有问题呢? + +最好的方式就是打日志,本题的话,就是将 minDist 数组打印出来,就可以很明显发现 哪里出问题了。 + +每次选择节点后,minDist数组的变化是否符合预期 ,是否和我上面讲的逻辑是对应的。 + +例如本题,如果想debug的话,打印日志可以这样写: + + +```CPP +#include +#include +#include +using namespace std; +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + std::vector minDist(n + 1, INT_MAX); + + std::vector visited(n + 1, false); + + minDist[start] = 0; + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + + // 打印日志: + cout << "select:" << cur << endl; + for (int v = 1; v <= n; v++) cout << v << ":" << minDist[v] << " "; + cout << endl << endl;; + + } + if (minDist[end] == INT_MAX) cout << -1 << endl; + else cout << minDist[end] << endl; + +} + +``` + +打印后的结果: + +``` +select:1 +1:0 2:1 3:4 4:2147483647 5:2147483647 6:2147483647 7:2147483647 + +select:2 +1:0 2:1 3:3 4:6 5:2147483647 6:5 7:2147483647 + +select:3 +1:0 2:1 3:3 4:5 5:2147483647 6:5 7:2147483647 + +select:4 +1:0 2:1 3:3 4:5 5:8 6:5 7:2147483647 + +select:6 +1:0 2:1 3:3 4:5 5:8 6:5 7:14 + +select:5 +1:0 2:1 3:3 4:5 5:8 6:5 7:12 + +select:7 +1:0 2:1 3:3 4:5 5:8 6:5 7:12 +``` + +打印日志可以和上面我讲解的过程进行对比,每一步的结果是完全对应的。 + +所以如果大家如果代码有问题,打日志来debug是最好的方法 + +### 如何求路径 + +如果题目要求把最短路的路径打印出来,应该怎么办呢? + +这里还是有一些“坑”的,本题打印路径和 prim 打印路径是一样的,我在 [prim算法精讲](./0053.寻宝-prim.md) 【拓展】中 已经详细讲解了。 + +在这里就不再赘述。 + +打印路径只需要添加 几行代码, 打印路径的代码我都加上的日志,如下: + +```CPP +#include +#include +#include +using namespace std; +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, INT_MAX)); + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + std::vector minDist(n + 1, INT_MAX); + + std::vector visited(n + 1, false); + + minDist[start] = 0; + + //加上初始化 + vector parent(n + 1, -1); + + for (int i = 1; i <= n; i++) { + + int minVal = INT_MAX; + int cur = 1; + + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; + + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + parent[v] = cur; // 记录边 + } + } + + } + + // 输出最短情况 + for (int i = 1; i <= n; i++) { + cout << parent[i] << "->" << i << endl; + } +} +``` + +打印结果: + +``` +-1->1 +1->2 +2->3 +3->4 +4->5 +2->6 +5->7 +``` + +对应如图: + +![](https://file1.kamacoder.com/i/algo/20240201111352.png) + +### 出现负数 + +如果图中边的权值为负数,dijkstra 还合适吗? + +看一下这个图: (有负权值) + +![](https://file1.kamacoder.com/i/algo/20240227104334.png) + +节点1 到 节点5 的最短路径 应该是 节点1 -> 节点2 -> 节点3 -> 节点4 -> 节点5 + +那我们来看dijkstra 求解的路径是什么样的,继续dijkstra 三部曲来模拟 :(dijkstra模拟过程上面已经详细讲过,以下只模拟重要过程,例如如何初始化就省略讲解了) + +----------- + +初始化: + +![](https://file1.kamacoder.com/i/algo/20240227104801.png) + +--------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离源点最近,距离为0,且未被访问。 + +2、该最近节点被标记访问过 + +标记源点访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110217.png) + +更新 minDist数组,即:源点(节点1) 到 节点2 和 节点3的距离。 + +* 源点到节点2的最短距离为100,小于原minDist[2]的数值max,更新minDist[2] = 100 +* 源点到节点3的最短距离为1,小于原minDist[3]的数值max,更新minDist[3] = 1 + +------------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点3最近,距离为1,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点3访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110330.png) + +由于节点3的加入,那么源点可以有新的路径链接到节点4 所以更新minDist数组: + +* 源点到节点4的最短距离为2,小于原minDist[4]的数值max,更新minDist[4] = 2 + +-------------- + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点4最近,距离为2,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点4访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110346.png) + +由于节点4的加入,那么源点可以有新的路径链接到节点5 所以更新minDist数组: + +* 源点到节点5的最短距离为3,小于原minDist[5]的数值max,更新minDist[5] = 5 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点5最近,距离为3,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点5访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110405.png) + +节点5的加入,而节点5 没有链接其他节点, 所以不用更新minDist数组,仅标记节点5被访问过了 + +------------ + +1、选源点到哪个节点近且该节点未被访问过 + +源点距离节点2最近,距离为100,且未被访问。 + +2、该最近节点被标记访问过 + +标记节点2访问过 + +3、更新非访问节点到源点的距离(即更新minDist数组) ,如图: + +![](https://file1.kamacoder.com/i/algo/20240227110711.png) + +-------------- + +至此dijkstra的模拟过程就结束了,根据最后的minDist数组,我们求 节点1 到 节点5 的最短路径的权值总和为 3,路径: 节点1 -> 节点3 -> 节点4 -> 节点5 + +通过以上的过程模拟,我们可以发现 之所以 没有走有负权值的最短路径 是因为 在 访问 节点 2 的时候,节点 3 已经访问过了,就不会再更新了。 + +那有录友可能会想: 我可以改代码逻辑啊,访问过的节点,也让它继续访问不就好了? + +那么访问过的节点还能继续访问会不会有死循环的出现呢?控制逻辑不让其死循环?那特殊情况自己能都想清楚吗?(可以试试,实践出真知) + +对于负权值的出现,大家可以针对某一个场景 不断去修改 dijkstra 的代码,**但最终会发现只是 拆了东墙补西墙**,对dijkstra的补充逻辑只能满足某特定场景最短路求解。 + +对于求解带有负权值的最短路问题,可以使用 Bellman-Ford 算法 ,我在后序会详细讲解。 + +## dijkstra与prim算法的区别 + +> 这里再次提示,需要先看我的 [prim算法精讲](./0053.寻宝-prim.md) ,否则可能不知道我下面讲的是什么。 + +大家可以发现 dijkstra的代码看上去 怎么和 prim算法这么像呢。 + +其实代码大体不差,唯一区别在 三部曲中的 第三步: 更新minDist数组 + +因为**prim是求 非访问节点到最小生成树的最小距离,而 dijkstra是求 非访问节点到源点的最小距离**。 + +prim 更新 minDist数组的写法: + + +```CPP +for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + } +} +``` + +因为 minDist表示 节点到最小生成树的最小距离,所以 新节点cur的加入,只需要 使用 grid[cur][j] ,grid[cur][j] 就表示 cur 加入生成树后,生成树到 节点j 的距离。 + +dijkstra 更新 minDist数组的写法: + +```CPP +for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } +} +``` + +因为 minDist表示 节点到源点的最小距离,所以 新节点 cur 的加入,需要使用 源点到cur的距离 (minDist[cur]) + cur 到 节点 v 的距离 (grid[cur][v]),才是 源点到节点v的距离。 + +此时大家可能不禁要想 prim算法 可以有负权值吗? + +当然可以! + +录友们可以自己思考思考一下,这是为什么? + +这里我提示一下:prim算法只需要将节点以最小权值和链接在一起,不涉及到单一路径。 + + + +## 总结 + +本篇,我们深入讲解的dijkstra算法,详细模拟其工作的流程。 + +这里我给出了 **dijkstra 三部曲 来 帮助大家理解 该算法**,不至于 每次写 dijkstra 都是黑盒操作,没有框架没有章法。 + +在给出的代码中,我也按照三部曲的逻辑来给大家注释,只要理解这三部曲,即使 过段时间 对 dijkstra 算法有些遗忘,依然可以写出一个框架出来,然后再去调试细节。 + +对于图论算法,一般代码都比较长,很难写出代码直接可以提交通过,都需要一个debug的过程,所以 **学习如何debug 非常重要**! + +这也是我为什么 在本文中 单独用来讲解 debug方法。 + +本题求的是最短路径和是多少,**同时我们也要掌握 如何把最短路径打印出来**。 + +我还写了大篇幅来讲解 负权值的情况, 只有画图带大家一步一步去 看 出现负权值 dijkstra的求解过程,才能帮助大家理解,问题出在哪里。 + +如果我直接讲:是**因为访问过的节点 不能再访问,导致错过真正的最短路**,我相信大家都不知道我在说啥。 + +最后我还讲解了 dijkstra 和 prim 算法的 相同 与 不同之处, 我在图论的讲解安排中 先讲 prim算法 再讲 dijkstra 是有目的的, **理解这两个算法的相同与不同之处 有助于大家学习的更深入**。 + +而不是 学了 dijkstra 就只看 dijkstra, 算法之间 都是有联系的,多去思考 算法之间的相互联系,会帮助大家思考的更深入,掌握的更彻底。 + +本篇写了这么长,我也只讲解了 朴素版dijkstra,**关于 堆优化dijkstra,我会在下一篇再来给大家详细讲解**。 + +加油 + + + +## 其他语言版本 + +### Java + +```Java +import java.util.Arrays; +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + int[][] grid = new int[n + 1][n + 1]; + for (int i = 0; i <= n; i++) { + Arrays.fill(grid[i], Integer.MAX_VALUE); + } + + for (int i = 0; i < m; i++) { + int p1 = scanner.nextInt(); + int p2 = scanner.nextInt(); + int val = scanner.nextInt(); + grid[p1][p2] = val; + } + + int start = 1; + int end = n; + + // 存储从源点到每个节点的最短距离 + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + + // 记录顶点是否被访问过 + boolean[] visited = new boolean[n + 1]; + + minDist[start] = 0; // 起始点到自身的距离为0 + + for (int i = 1; i <= n; i++) { // 遍历所有节点 + + int minVal = Integer.MAX_VALUE; + int cur = 1; + + // 1、选距离源点最近且未访问过的节点 + for (int v = 1; v <= n; ++v) { + if (!visited[v] && minDist[v] < minVal) { + minVal = minDist[v]; + cur = v; + } + } + + visited[cur] = true; // 2、标记该节点已被访问 + + // 3、第三步,更新非访问节点到源点的距离(即更新minDist数组) + for (int v = 1; v <= n; v++) { + if (!visited[v] && grid[cur][v] != Integer.MAX_VALUE && minDist[cur] + grid[cur][v] < minDist[v]) { + minDist[v] = minDist[cur] + grid[cur][v]; + } + } + } + + if (minDist[end] == Integer.MAX_VALUE) { + System.out.println(-1); // 不能到达终点 + } else { + System.out.println(minDist[end]); // 到达终点最短路径 + } + } +} + +``` + +### Python + +``` +import sys + +def dijkstra(n, m, edges, start, end): + # 初始化邻接矩阵 + grid = [[float('inf')] * (n + 1) for _ in range(n + 1)] + for p1, p2, val in edges: + grid[p1][p2] = val + + # 初始化距离数组和访问数组 + minDist = [float('inf')] * (n + 1) + visited = [False] * (n + 1) + + minDist[start] = 0 # 起始点到自身的距离为0 + + for _ in range(1, n + 1): # 遍历所有节点 + minVal = float('inf') + cur = -1 + + # 选择距离源点最近且未访问过的节点 + for v in range(1, n + 1): + if not visited[v] and minDist[v] < minVal: + minVal = minDist[v] + cur = v + + if cur == -1: # 如果找不到未访问过的节点,提前结束 + break + + visited[cur] = True # 标记该节点已被访问 + + # 更新未访问节点到源点的距离 + for v in range(1, n + 1): + if not visited[v] and grid[cur][v] != float('inf') and minDist[cur] + grid[cur][v] < minDist[v]: + minDist[v] = minDist[cur] + grid[cur][v] + + return -1 if minDist[end] == float('inf') else minDist[end] + +if __name__ == "__main__": + input = sys.stdin.read + data = input().split() + n, m = int(data[0]), int(data[1]) + edges = [] + index = 2 + for _ in range(m): + p1 = int(data[index]) + p2 = int(data[index + 1]) + val = int(data[index + 2]) + edges.append((p1, p2, val)) + index += 3 + start = 1 # 起点 + end = n # 终点 + + result = dijkstra(n, m, edges, start, end) + print(result) + +``` + +### Go + +### Rust + +### JavaScript + +```js +function dijkstra(grid, start, end) { + const visited = Array.from({length: end + 1}, () => false) + const minDist = Array.from({length: end + 1}, () => Number.MAX_VALUE) + minDist[start] = 0 + + for (let i = 1 ; i < end + 1 ; i++) { + let cur = -1 + let tempMinDist = Number.MAX_VALUE + // 1. 找尋與起始點距離最近且未被訪的節點 + for (let j = 1 ; j < end + 1 ; j++) { + if (!visited[j] && minDist[j] < tempMinDist) { + cur = j + tempMinDist = minDist[j] + } + } + if (cur === -1) break; + + // 2. 更新節點狀態為已拜訪 + visited[cur] = true + + // 3. 更新未拜訪節點與起始點的最短距離 + for (let j = 1 ; j < end + 1 ; j++) { + if(!visited[j] && grid[cur][j] != Number.MAX_VALUE + && grid[cur][j] + minDist[cur] < minDist[j] + ) { + minDist[j] = grid[cur][j] + minDist[cur] + } + } + } + + return minDist[end] === Number.MAX_VALUE ? -1 : minDist[end] +} + + +async function main() { + // 輸入 + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [n, m] = (await readline()).split(" ").map(Number) + const grid = Array.from({length: n + 1}, + () => Array.from({length:n + 1}, () => Number.MAX_VALUE)) + for (let i = 0 ; i < m ; i++) { + const [s, e, w] = (await readline()).split(" ").map(Number) + grid[s][e] = w + } + + // dijkstra + const result = dijkstra(grid, 1, n) + + // 輸出 + console.log(result) +} + + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0053.\345\257\273\345\256\235-Kruskal.md" "b/problems/kamacoder/0053.\345\257\273\345\256\235-Kruskal.md" new file mode 100644 index 0000000000..53da7af9ee --- /dev/null +++ "b/problems/kamacoder/0053.\345\257\273\345\256\235-Kruskal.md" @@ -0,0 +1,763 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# kruskal算法精讲 + +[卡码网:53. 寻宝](https://kamacoder.com/problempage.php?pid=1053) + +题目描述: + +在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。 + +不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将 所有岛屿联通起来。 + +给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。 + +输入描述: + +第一行包含两个整数V 和 E,V代表顶点数,E代表边数 。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。 + +接下来共有 E 行,每行三个整数 v1,v2 和 val,v1 和 v2 为边的起点和终点,val代表边的权值。 + +输出描述: + +输出联通所有岛屿的最小路径总距离 + +输入示例: + +``` +7 11 +1 2 1 +1 3 1 +1 5 2 +2 6 1 +2 4 2 +2 3 2 +3 4 1 +4 5 1 +5 6 2 +5 7 1 +6 7 1 +``` + +输出示例: + +6 + +## 解题思路 + +在上一篇 我们讲解了 prim算法求解 最小生成树,本篇我们来讲解另一个算法:Kruskal,同样可以求最小生成树。 + +**prim 算法是维护节点的集合,而 Kruskal 是维护边的集合**。 + +上来就这么说,大家应该看不太懂,这里是先让大家有这么个印象,带着这个印象在看下文,理解的会更到位一些。 + +kruscal的思路: + +* 边的权值排序,因为要优先选最小的边加入到生成树里 +* 遍历排序后的边 + * 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环 + * 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合 + +下面我们画图举例说明kruscal的工作过程。 + +依然以示例中,如下这个图来举例。 + +![](https://file1.kamacoder.com/i/algo/20240111113514.png) + +将图中的边按照权值有小到大排序,这样从贪心的角度来说,优先选 权值小的边加入到 最小生成树中。 + +排序后的边顺序为[(1,2) (4,5) (1,3) (2,6) (3,4) (6,7) (5,7) (1,5) (3,2) (2,4) (5,6)] + +> (1,2) 表示节点1 与 节点2 之间的边。权值相同的边,先后顺序无所谓。 + +**开始从头遍历排序后的边**。 + +-------- + +选边(1,2),节点1 和 节点2 不在同一个集合,所以生成树可以添加边(1,2),并将 节点1,节点2 放在同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240111114204.png) + +-------- + +选边(4,5),节点4 和 节点 5 不在同一个集合,生成树可以添加边(4,5) ,并将节点4,节点5 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240111120458.png) + +**大家判断两个节点是否在同一个集合,就看图中两个节点是否有绿色的粗线连着就行** + +------ + +(这里在强调一下,以下选边是按照上面排序好的边的数组来选择的) + +选边(1,3),节点1 和 节点3 不在同一个集合,生成树添加边(1,3),并将节点1,节点3 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112105834.png) + +--------- + +选边(2,6),节点2 和 节点6 不在同一个集合,生成树添加边(2,6),并将节点2,节点6 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112110214.png) + +-------- + +选边(3,4),节点3 和 节点4 不在同一个集合,生成树添加边(3,4),并将节点3,节点4 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112110450.png) + +---------- + +选边(6,7),节点6 和 节点7 不在同一个集合,生成树添加边(6,7),并将 节点6,节点7 放到同一个集合。 + +![](https://file1.kamacoder.com/i/algo/20240112110637.png) + +----------- + +选边(5,7),节点5 和 节点7 在同一个集合,不做计算。 + +选边(1,5),两个节点在同一个集合,不做计算。 + +后面遍历 边(3,2),(2,4),(5,6) 同理,都因两个节点已经在同一集合,不做计算。 + + +------- + +此时 我们就已经生成了一个最小生成树,即: + +![](https://file1.kamacoder.com/i/algo/20240112110637.png) + +在上面的讲解中,看图的话 大家知道如何判断 两个节点 是否在同一个集合(是否有绿色的线连在一起),以及如何把两个节点加入集合(就在图中把两个节点连上) + +**但在代码中,如果将两个节点加入同一个集合,又如何判断两个节点是否在同一个集合呢**? + +这里就涉及到我们之前讲解的[并查集](./图论并查集理论基础.md)。 + +我们在并查集开篇的时候就讲了,并查集主要就两个功能: + +* 将两个元素添加到一个集合中 +* 判断两个元素在不在同一个集合 + +大家发现这正好符合 Kruskal算法的需求,这也是为什么 **我要先讲并查集,再讲 Kruskal**。 + +关于 并查集,我已经在[并查集精讲](./图论并查集理论基础.md) 详细讲解过了,所以这里不再赘述,我们直接用。 + +本题代码如下,已经详细注释: + +```CPP + +#include +#include +#include + +using namespace std; + +// l,r为 边两边的节点,val为边的数值 +struct Edge { + int l, r, val; +}; + +// 节点数量 +int n = 10001; +// 并查集标记节点关系的数组 +vector father(n, -1); // 节点编号是从1开始的,n要大一些 + +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} + +// 并查集的查找操作 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 +} + +// 并查集的加入集合 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} + +int main() { + + int v, e; + int v1, v2, val; + vector edges; + int result_val = 0; + cin >> v >> e; + while (e--) { + cin >> v1 >> v2 >> val; + edges.push_back({v1, v2, val}); + } + + // 执行Kruskal算法 + // 按边的权值对边进行从小到大排序 + sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) { + return a.val < b.val; + }); + + // 并查集初始化 + init(); + + // 从头开始遍历边 + for (Edge edge : edges) { + // 并查集,搜出两个节点的祖先 + int x = find(edge.l); + int y = find(edge.r); + + // 如果祖先不同,则不在同一个集合 + if (x != y) { + result_val += edge.val; // 这条边可以作为生成树的边 + join(x, y); // 两个节点加入到同一个集合 + } + } + cout << result_val << endl; + return 0; +} + +``` + +时间复杂度:nlogn (快排) + logn (并查集) ,所以最后依然是 nlogn 。n为边的数量。 + +关于并查集时间复杂度,可以看我在 [并查集理论基础](https://programmercarl.com/%E5%9B%BE%E8%AE%BA%E5%B9%B6%E6%9F%A5%E9%9B%86%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html) 的讲解。 + +## 拓展一 + +如果题目要求将最小生成树的边输出的话,应该怎么办呢? + +Kruskal 算法 输出边的话,相对prim 要容易很多,因为 Kruskal 本来就是直接操作边,边的结构自然清晰,不用像 prim一样 需要再将节点连成线输出边 (因为prim是对节点操作,而 Kruskal是对边操作,这是本质区别) + +本题中,边的结构为: + +```CPP +struct Edge { + int l, r, val; +}; +``` + +那么我们只需要找到 在哪里把生成树的边保存下来就可以了。 + +当判断两个节点不在同一个集合的时候,这两个节点的边就加入到最小生成树, 所以添加边的操作在这里: + +```CPP +vector result; // 存储最小生成树的边 +// 如果祖先不同,则不在同一个集合 +if (x != y) { + result.push_back(edge); // 记录最小生成树的边 + result_val += edge.val; // 这条边可以作为生成树的边 + join(x, y); // 两个节点加入到同一个集合 +} +``` + +整体代码如下,为了突出重点,我仅仅将 打印最小生成树的部分代码注释了,大家更容易看到哪些改动。 + +```CPP +#include +#include +#include + +using namespace std; + +struct Edge { + int l, r, val; +}; + + +int n = 10001; + +vector father(n, -1); + +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} + +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} + +void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return ; + father[v] = u; +} + +int main() { + + int v, e; + int v1, v2, val; + vector edges; + int result_val = 0; + cin >> v >> e; + while (e--) { + cin >> v1 >> v2 >> val; + edges.push_back({v1, v2, val}); + } + + sort(edges.begin(), edges.end(), [](const Edge& a, const Edge& b) { + return a.val < b.val; + }); + + vector result; // 存储最小生成树的边 + + init(); + + for (Edge edge : edges) { + + int x = find(edge.l); + int y = find(edge.r); + + + if (x != y) { + result.push_back(edge); // 保存最小生成树的边 + result_val += edge.val; + join(x, y); + } + } + + // 打印最小生成树的边 + for (Edge edge : result) { + cout << edge.l << " - " << edge.r << " : " << edge.val << endl; + } + + return 0; +} + + +``` + +按照题目中的示例,打印边的输出为: + +``` +1 - 2 : 1 +1 - 3 : 1 +2 - 6 : 1 +3 - 4 : 1 +4 - 5 : 1 +5 - 7 : 1 +``` + +大家可能发现 怎么和我们 模拟画的图不一样,差别在于 代码生成的最小生成树中 节点5 和 节点7相连的。 + +![](https://file1.kamacoder.com/i/algo/20240116163014.png) + + +其实造成这个差别 是对边排序的时候 权值相同的边先后顺序的问题导致的,无论相同权值边的顺序是什么样的,最后都能得出最小生成树。 + + +## 拓展二 + + +此时我们已经讲完了 Kruskal 和 prim 两个解法来求最小生成树。 + +什么情况用哪个算法更合适呢。 + +Kruskal 与 prim 的关键区别在于,prim维护的是节点的集合,而 Kruskal 维护的是边的集合。 +如果 一个图中,节点多,但边相对较少,那么使用Kruskal 更优。 + +有录友可能疑惑,一个图里怎么可能节点多,边却少呢? + +节点未必一定要连着边那, 例如 这个图,大家能明显感受到边没有那么多对吧,但节点数量 和 上述我们讲的例子是一样的。 + +![](https://file1.kamacoder.com/i/algo/20240116152211.png) + +为什么边少的话,使用 Kruskal 更优呢? + +因为 Kruskal 是对边进行排序的后 进行操作是否加入到最小生成树。 + +边如果少,那么遍历操作的次数就少。 + +在节点数量固定的情况下,图中的边越少,Kruskal 需要遍历的边也就越少。 + +而 prim 算法是对节点进行操作的,节点数量越少,prim算法效率就越优。 + +所以在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。 + +> 边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图 + + +Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。 + +Kruskal算法 时间复杂度 为 nlogn,其中n 为边的数量,适用稀疏图。 + +## 总结 + +如果学过了并查集,其实 kruskal 比 prim更好理解一些。 + +本篇,我们依然通过模拟 Kruskal 算法的过程,来带大家一步步了解其工作过程。 + +在 拓展一 中讲解了 如何输出最小生成树的边。 + +在拓展二 中讲解了 prim 和 Kruskal的区别。 + +录友们可以细细体会。 + + +## 其他语言版本 + +### Java + +```Java +import java.util.*; + +class Edge { + int l, r, val; + + Edge(int l, int r, int val) { + this.l = l; + this.r = r; + this.val = val; + } +} + +public class Main { + private static int n = 10001; + private static int[] father = new int[n]; + + // 并查集初始化 + public static void init() { + for (int i = 0; i < n; i++) { + father[i] = i; + } + } + + // 并查集的查找操作 + public static int find(int u) { + if (u == father[u]) return u; + return father[u] = find(father[u]); + } + + // 并查集的加入集合 + public static void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return; + father[v] = u; + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int v = scanner.nextInt(); + int e = scanner.nextInt(); + List edges = new ArrayList<>(); + int result_val = 0; + + for (int i = 0; i < e; i++) { + int v1 = scanner.nextInt(); + int v2 = scanner.nextInt(); + int val = scanner.nextInt(); + edges.add(new Edge(v1, v2, val)); + } + + // 执行Kruskal算法 + edges.sort(Comparator.comparingInt(edge -> edge.val)); + + // 并查集初始化 + init(); + + // 从头开始遍历边 + for (Edge edge : edges) { + int x = find(edge.l); + int y = find(edge.r); + + if (x != y) { + result_val += edge.val; + join(x, y); + } + } + System.out.println(result_val); + scanner.close(); + } +} + +``` + +### Python + +```python +class Edge: + def __init__(self, l, r, val): + self.l = l + self.r = r + self.val = val + +n = 10001 +father = list(range(n)) + +def init(): + global father + father = list(range(n)) + +def find(u): + if u != father[u]: + father[u] = find(father[u]) + return father[u] + +def join(u, v): + u = find(u) + v = find(v) + if u != v: + father[v] = u + +def kruskal(v, edges): + edges.sort(key=lambda edge: edge.val) + init() + result_val = 0 + + for edge in edges: + x = find(edge.l) + y = find(edge.r) + if x != y: + result_val += edge.val + join(x, y) + + return result_val + +if __name__ == "__main__": + import sys + input = sys.stdin.read + data = input().split() + + v = int(data[0]) + e = int(data[1]) + + edges = [] + index = 2 + for _ in range(e): + v1 = int(data[index]) + v2 = int(data[index + 1]) + val = int(data[index + 2]) + edges.append(Edge(v1, v2, val)) + index += 3 + + result_val = kruskal(v, edges) + print(result_val) + +``` + + +### Go + +### Rust + +### JavaScript + +```js +function kruskal(v, edges) { + const father = Array.from({ length: v + 1 }, (_, i) => i) + + function find(u){ + if (u === father[u]) { + return u + } else { + father[u] = find(father[u]) + return father[u] + } + + } + + function isSame(u, v) { + let s = find(u) + let t = find(v) + return s === t + } + + function join(u, v) { + let s = find(u) + let t = find(v) + if (s !== t) { + father[s] = t + } + } + + edges.sort((a, b) => a[2] - b[2]) + let result = 0 + for (const [v1, v2, w] of edges) { + if (!isSame(v1, v2)) { + result += w + join(v1 ,v2) + } + } + console.log(result) +} + + +async function main() { + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [v, e] = (await readline()).split(" ").map(Number) + const edges = [] + for (let i = 0 ; i < e ; i++) { + edges.push((await readline()).split(" ").map(Number)) + } + kruskal(v, edges) +} + + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C +并查集方法一 +```c +#include +#include + +// 定义边结构体,包含两个顶点vex1和vex2以及它们之间的权重val +struct Edge +{ + int vex1, vex2, val; +}; + +// 冒泡排序函数,用于按边的权重val不减序排序边数组 +void bubblesort(struct Edge *a, int numsize) +{ + for (int i = 0; i < numsize - 1; ++i) + { + + for (int j = 0; j < numsize - i - 1; ++j) + { + if (a[j].val > a[j + 1].val) + { + struct Edge temp = a[j]; + a[j] = a[j + 1]; + a[j + 1] = temp; + } + } + } +} + +int main() +{ + int v, e; + int v1, v2, val; + int ret = 0; + + scanf("%d%d", &v, &e); + struct Edge *edg = (struct Edge *)malloc(sizeof(struct Edge) * e); + int *conne_gra = (int *)malloc(sizeof(int) * (v + 1)); + + // 初始化连通图数组,每个顶点初始时只与自己相连通 + for (int i = 0; i <= v; ++i) + { + conne_gra[i] = i; + } + + // 读取所有边的信息并存储到edg(存储所有边的)数组中 + for (int i = 0; i < e; ++i) + { + scanf("%d%d%d", &v1, &v2, &val); + edg[i].vex1 = v1; + edg[i].vex2 = v2; + edg[i].val = val; + } + bubblesort(edg, e); // 调用冒泡排序函数对边进行排序 + + // 遍历所有边,执行Kruskal算法来找到最小生成树 + for (int i = 0; i < e; ++i) + { + if (conne_gra[edg[i].vex1] != conne_gra[edg[i].vex2]) + { // 如果当前边的两个顶点不在同一个连通分量中 + int tmp1 = conne_gra[edg[i].vex1], tmp2 = conne_gra[edg[i].vex2]; + for (int k = 1; k <= v; ++k) + { // 将所有属于tmp2的顶点合并到tmp1的连通分量中 + if (conne_gra[k] == tmp2) + conne_gra[k] = tmp1; + } + ret += edg[i].val; // 将当前边的权重加到最小生成树的权重中 + } + } + printf("%d", ret); + return 0; +} + +``` +并查集方法二 +```c +#include +#include + +// 定义边结构体,包含两个顶点vex1和vex2以及它们之间的权重val (略,同上) +// 冒泡排序函数,用于按边的权重val不减序排序边数组(略,同上) + +// 并查集的查找操作 +int find(int m, int *father) +{ // 如果当前节点是其自身的父节点,则直接返回该节点 + // 否则递归查找其父节点的根,并将当前节点直接连接到根节点 + return (m == father[m]) ? m : (father[m] = find(father[m], father)); // 路径压缩 +} + +// 并查集的加入集合 +void Union(int m, int n, int *father) +{ + int x = find(m, father); + int y = find(n, father); + if (x == y) + return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[y] = x; +} + +int main() +{ + int v, e; + int v1, v2, val; + int ret = 0; + + scanf("%d%d", &v, &e); + struct Edge *edg = (struct Edge *)malloc(sizeof(struct Edge) * e); + int *conne_gra = (int *)malloc(sizeof(int) * (v + 1)); + + // 初始化连通图数组,每个顶点初始时只与自己相连通 + for (int i = 0; i <= v; ++i) + { + conne_gra[i] = i; + } + // 读取所有边的信息并存储到edg(存储所有边的)数组中 + for (int i = 0; i < e; ++i) + { + scanf("%d%d%d", &v1, &v2, &val); + edg[i].vex1 = v1; + edg[i].vex2 = v2; + edg[i].val = val; + } + + bubblesort(edg, e); // 调用冒泡排序函数对边进行排序 + + // Kruskal算法的实现,通过边数组构建最小生成树 + int j = 0, count = 0; + while (v > 1) + { + if (find(edg[j].vex1, conne_gra) != find(edg[j].vex2, conne_gra)) + { + ret += edg[j].val; // 将当前边的权重加到最小生成树的权重中 + Union(edg[j].vex1, edg[j].vex2, conne_gra); + v--; + } + j++; + } + printf("%d", ret); + return 0; +} + +``` +
diff --git "a/problems/kamacoder/0053.\345\257\273\345\256\235-prim.md" "b/problems/kamacoder/0053.\345\257\273\345\256\235-prim.md" new file mode 100644 index 0000000000..df0129ee2a --- /dev/null +++ "b/problems/kamacoder/0053.\345\257\273\345\256\235-prim.md" @@ -0,0 +1,760 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# prim算法精讲 + +[卡码网:53. 寻宝](https://kamacoder.com/problempage.php?pid=1053) + +题目描述: + +在世界的某个区域,有一些分散的神秘岛屿,每个岛屿上都有一种珍稀的资源或者宝藏。国王打算在这些岛屿上建公路,方便运输。 + +不同岛屿之间,路途距离不同,国王希望你可以规划建公路的方案,如何可以以最短的总公路距离将所有岛屿联通起来。 + +给定一张地图,其中包括了所有的岛屿,以及它们之间的距离。以最小化公路建设长度,确保可以链接到所有岛屿。 + +输入描述: + +第一行包含两个整数V和E,V代表顶点数,E代表边数。顶点编号是从1到V。例如:V=2,一个有两个顶点,分别是1和2。 + +接下来共有E行,每行三个整数v1,v2和val,v1和v2为边的起点和终点,val代表边的权值。 + +输出描述: + +输出联通所有岛屿的最小路径总距离 + +输入示例: + +``` +7 11 +1 2 1 +1 3 1 +1 5 2 +2 6 1 +2 4 2 +2 3 2 +3 4 1 +4 5 1 +5 6 2 +5 7 1 +6 7 1 +``` + +输出示例: + +6 + + +## 解题思路 + +本题是最小生成树的模板题,那么我们来讲一讲最小生成树。 + +最小生成树可以使用prim算法也可以使用kruskal算法计算出来。 + +本篇我们先讲解prim算法。 + +最小生成树是所有节点的最小连通子图,即:以最小的成本(边的权值)将图中所有节点链接到一起。 + +图中有n个节点,那么一定可以用n-1条边将所有节点连接到一起。 + +那么如何选择这n-1条边就是最小生成树算法的任务所在。 + +例如本题示例中的无向有权图为: + +![](https://file1.kamacoder.com/i/algo/20231206164306.png) + +那么在这个图中,如何选取n-1条边使得图中所有节点连接到一起,并且边的权值和最小呢? + +(图中为n为7,即7个节点,那么只需要n-1即6条边就可以讲所有顶点连接到一起) + +prim算法是从节点的角度采用贪心的策略每次寻找距离最小生成树最近的节点并加入到最小生成树中。 + +prim算法核心就是三步,我称为**prim三部曲**,大家一定要熟悉这三步,代码相对会好些很多: + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +现在录友们会对这三步很陌生,不知道这是干啥的,没关系,下面将会画图举例来带大家把这**prim三部曲**理解到位。 + +在prim算法中,有一个数组特别重要,这里我起名为:minDist。 + +刚刚我有讲过“每次寻找距离最小生成树最近的节点并加入到最小生成树中”,那么如何寻找距离最小生成树最近的节点呢? + +这就用到了minDist数组,它用来作什么呢? + +**minDist数组用来记录每一个节点距离最小生成树的最近距离**。理解这一点非常重要,这也是prim算法最核心要点所在,很多录友看不懂prim算法的代码,都是因为没有理解透这个数组的含义。 + +接下来,我们来通过一步一步画图,来带大家巩固**prim三部曲**以及minDist数组的作用。 + +(**示例中节点编号是从1开始,所以为了让大家看的不晕,minDist数组下标我也从1开始计数,下标0就不使用了,这样下标和节点标号就可以对应上了,避免大家搞混**) + + +### 1 初始状态 + +minDist数组里的数值初始化为最大数,因为本题节点距离不会超过10000,所以初始化最大数为10001就可以。 + +相信这里录友就要问了,为什么这么做? + +现在还没有最小生成树,默认每个节点距离最小生成树是最大的,这样后面我们在比较的时候,发现更近的距离,才能更新到minDist数组上。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20231215105603.png) + +开始构造最小生成树 + +### 2 + +1、prim三部曲,第一步:选距离生成树最近节点 + +选择距离最小生成树最近的节点,加入到最小生成树,刚开始还没有最小生成树,所以随便选一个节点加入就好(因为每一个节点一定会在最小生成树里,所以随便选一个就好),那我们选择节点1(符合遍历数组的习惯,第一个遍历的也是节点1) + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1已经算最小生成树的节点。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来,我们要更新所有节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102048.png) + + +注意下标0,我们就不管它了,下标1与节点1对应,这样可以避免大家把节点搞混。 + +此时所有非生成树的节点距离最小生成树(节点1)的距离都已经跟新了。 + +* 节点2与节点1的距离为1,比原先的距离值10001小,所以更新minDist[2]。 +* 节点3和节点1的距离为1,比原先的距离值10001小,所以更新minDist[3]。 +* 节点5和节点1的距离为2,比原先的距离值10001小,所以更新minDist[5]。 + +**注意图中我标记了minDist数组里更新的权值**,是哪两个节点之间的权值,例如minDist[2]=1,这个1是节点1与节点2之间的连线,清楚这一点对最后我们记录最小生成树的权值总和很重要。 + +(我在后面依然会不断重复prim三部曲,可能基础好的录友会感觉有点啰嗦,但也是让大家感觉这三部曲求解的过程) + +### 3 + +1、prim三部曲,第一步:选距离生成树最近节点 + +选取一个距离最小生成树(节点1)最近的非生成树里的节点,节点2,3,5距离最小生成树(节点1)最近,选节点2(其实选节点3或者节点2都可以,距离一样的)加入最小生成树。 + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1和节点2,已经算最小生成树的节点。 + + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来,我们要更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102431.png) + +此时所有非生成树的节点距离最小生成树(节点1、节点2)的距离都已经跟新了。 + +* 节点3和节点2的距离为2,和原先的距离值1小,所以不用更新。 +* 节点4和节点2的距离为2,比原先的距离值10001小,所以更新minDist[4]。 +* 节点5和节点2的距离为10001(不连接),所以不用更新。 +* 节点6和节点2的距离为1,比原先的距离值10001小,所以更新minDist[6]。 + +### 4 + +1、prim三部曲,第一步:选距离生成树最近节点 + +选择一个距离最小生成树(节点1、节点2)最近的非生成树里的节点,节点3,6距离最小生成树(节点1、节点2)最近,选节点3(选节点6也可以,距离一样)加入最小生成树。 + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1、节点2、节点3算是最小生成树的节点。 + + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102457.png) + +所有非生成树的节点距离最小生成树(节点1、节点2、节点3)的距离都已经跟新了。 + +* 节点4和节点3的距离为1,和原先的距离值2小,所以更新minDist[4]为1。 + +上面为什么我们只比较节点4和节点3的距离呢? + +因为节点3加入最小生成树后,非生成树节点只有节点4和节点3是链接的,所以需要重新更新一下节点4距离最小生成树的距离,其他节点距离最小生成树的距离都不变。 + +### 5 + +1、prim三部曲,第一步:选距离生成树最近节点 + +继续选择一个距离最小生成树(节点1、节点2、节点3)最近的非生成树里的节点,为了巩固大家对minDist数组的理解,这里我再啰嗦一遍: + +![](https://file1.kamacoder.com/i/algo/20231217213516.png) + +**minDist数组是记录了所有非生成树节点距离生成树的最小距离**,所以从数组里我们能看出来,非生成树节点4和节点6距离生成树最近。 + + +任选一个加入生成树,我们选节点4(选节点6也行)。 + +**注意**,我们根据minDist数组,选取距离生成树最近的节点加入生成树,那么**minDist数组里记录的其实也是最小生成树的边的权值**(我在图中把权值对应的是哪两个节点也标记出来了)。 + +如果大家不理解,可以跟着我们下面的讲解,看minDist数组的变化,minDist数组里记录的权值对应的哪条边。 + +理解这一点很重要,因为最后我们要求最小生成树里所有边的权值和。 + +2、prim三部曲,第二步:最近节点加入生成树 + +此时节点1、节点2、节点3、节点4算是最小生成树的节点。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102618.png) + +minDist数组已经更新了所有非生成树的节点距离最小生成树(节点1、节点2、节点3、节点4)的距离。 + +* 节点5和节点4的距离为1,和原先的距离值2小,所以更新minDist[5]为1。 + +### 6 + +1、prim三部曲,第一步:选距离生成树最近节点 + +继续选距离最小生成树(节点1、节点2、节点3、节点4)最近的非生成树里的节点,只有节点5和节点6。 + + +选节点5(选节点6也可以)加入生成树。 + +2、prim三部曲,第二步:最近节点加入生成树 + +节点1、节点2、节点3、节点4、节点5算是最小生成树的节点。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102646.png) + +minDist数组已经更新了所有非生成树的节点距离最小生成树(节点1、节点2、节点3、节点4、节点5)的距离。 + +* 节点6和节点5距离为2,比原先的距离值1大,所以不更新 +* 节点7和节点5距离为1,比原先的距离值10001小,更新minDist[7] + +### 7 + +1、prim三部曲,第一步:选距离生成树最近节点 + +继续选距离最小生成树(节点1、节点2、节点3、节点4、节点5)最近的非生成树里的节点,只有节点6和节点7。 + +2、prim三部曲,第二步:最近节点加入生成树 + +选节点6(选节点7也行,距离一样的)加入生成树。 + +3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + +节点1、节点2、节点3、节点4、节点5、节点6算是最小生成树的节点,接下来更新节点距离最小生成树的距离,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102732.png) + +这里就不在重复描述了,大家类推,最后,节点7加入生成树,如图: + +![](https://file1.kamacoder.com/i/algo/20231222102820.png) + +### 最后 + +最后我们就生成了一个最小生成树,绿色的边将所有节点链接到一起,并且保证权值是最小的,因为我们在更新minDist数组的时候,都是选距离最小生成树最近的点加入到树中。 + +讲解上面的模拟过程的时候,我已经强调多次minDist数组是记录了所有非生成树节点距离生成树的最小距离。 + +最后,minDist数组也就是记录的是最小生成树所有边的权值。 + +我在图中,特别把每条边的权值对应的是哪两个节点标记出来(例如minDist[7]=1,对应的是节点5和节点7之间的边,而不是节点6和节点7),为了就是让大家清楚,minDist里的每一个值对应的是哪条边。 + +那么我们要求最小生成树里边的权值总和就是把最后的minDist数组累加一起。 + +以下代码,我对prim三部曲,做了重点注释,大家根据这三步,就可以透彻理解prim。 + +```CPP +#include +#include +#include + +using namespace std; +int main() { + int v, e; + int x, y, k; + cin >> v >> e; + // 填一个默认最大值,题目描述val最大为10000 + vector> grid(v + 1, vector(v + 1, 10001)); + while (e--) { + cin >> x >> y >> k; + // 因为是双向图,所以两个方向都要填上 + grid[x][y] = k; + grid[y][x] = k; + + } + // 所有节点到最小生成树的最小距离 + vector minDist(v + 1, 10001); + + // 这个节点是否在树里 + vector isInTree(v + 1, false); + + // 我们只需要循环 n-1次,建立 n - 1条边,就可以把n个节点的图连在一起 + for (int i = 1; i < v; i++) { + + // 1、prim三部曲,第一步:选距离生成树最近节点 + int cur = -1; // 选中哪个节点 加入最小生成树 + int minVal = INT_MAX; + for (int j = 1; j <= v; j++) { // 1 - v,顶点编号,这里下标从1开始 + // 选取最小生成树节点的条件: + // (1)不在最小生成树里 + // (2)距离最小生成树最近的节点 + if (!isInTree[j] && minDist[j] < minVal) { + minVal = minDist[j]; + cur = j; + } + } + // 2、prim三部曲,第二步:最近节点(cur)加入生成树 + isInTree[cur] = true; + + // 3、prim三部曲,第三步:更新非生成树节点到生成树的距离(即更新minDist数组) + // cur节点加入之后, 最小生成树加入了新的节点,那么所有节点到 最小生成树的距离(即minDist数组)需要更新一下 + // 由于cur节点是新加入到最小生成树,那么只需要关心与 cur 相连的 非生成树节点 的距离 是否比 原来 非生成树节点到生成树节点的距离更小了呢 + for (int j = 1; j <= v; j++) { + // 更新的条件: + // (1)节点是 非生成树里的节点 + // (2)与cur相连的某节点的权值 比 该某节点距离最小生成树的距离小 + // 很多录友看到自己 就想不明白什么意思,其实就是 cur 是新加入 最小生成树的节点,那么 所有非生成树的节点距离生成树节点的最近距离 由于 cur的新加入,需要更新一下数据了 + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + } + } + } + // 统计结果 + int result = 0; + for (int i = 2; i <= v; i++) { // 不计第一个顶点,因为统计的是边的权值,v个节点有 v-1条边 + result += minDist[i]; + } + cout << result << endl; + +} + +``` + +时间复杂度为O(n^2),其中n为节点数量。 + +## 拓展 + +上面讲解的是记录了最小生成树所有边的权值,如果让打印出来最小生成树的每条边呢?或者说要把这个最小生成树画出来呢? + + +此时我们就需要把最小生成树里每一条边记录下来。 + +此时有两个问题: + +* 1、用什么结构来记录 +* 2、如何记录 + +如果记录边,其实就是记录两个节点就可以,两个节点连成一条边。 + +如何记录两个节点呢? + +我们使用一维数组就可以记录。parent[节点编号] = 节点编号,这样就把一条边记录下来了。(当然如果节点编号非常大,可以考虑使用map) + +使用一维数组记录是有向边,不过我们这里不需要记录方向,所以只关注两条边是连接的就行。 + +parent数组初始化代码: + +```CPP +vector parent(v + 1, -1); +``` + +接下来就是第二个问题,如何记录? + +我们再来回顾一下prim三部曲, + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +大家先思考一下,我们是在第几步,可以记录最小生成树的边呢? + +在本面上半篇我们讲解过:“我们根据minDist数组,选组距离生成树最近的节点加入生成树,那么**minDist数组里记录的其实也是最小生成树的边的权值**。” + +既然minDist数组记录了最小生成树的边,是不是就是在更新minDist数组的时候,去更新parent数组来记录一下对应的边呢。 + + +所以在prim三部曲中的第三步,更新parent数组,代码如下: + +```CPP +for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + parent[j] = cur; // 记录最小生成树的边 (注意数组指向的顺序很重要) + } +} +``` + +代码中注释中,我强调了数组指向的顺序很重要。因为不少录友在这里会写成这样: `parent[cur] = j` 。 + +这里估计大家会疑惑了,parent[节点编号A] = 节点编号B,就表示A和B相连,我们这里就不用在意方向,代码中为什么只能 `parent[j] = cur` 而不能 `parent[cur] = j` 这么写呢? + +如果写成 `parent[cur] = j`,在for循环中,有多个j满足要求,那么 parent[cur] 就会被反复覆盖,因为cur是一个固定值。 + +举个例子,cur=1,在for循环中,可能就j=2,j=3,j=4都符合条件,那么本来应该记录节点1与节点2、节点3、节点4相连的。 + +如果 `parent[cur] = j` 这么写,最后更新的逻辑是 parent[1] = 2, parent[1] = 3, parent[1] = 4,最后只能记录节点1与节点4相连,其他相连情况都被覆盖了。 + +如果这么写 `parent[j] = cur`,那就是 parent[2] = 1, parent[3] = 1, parent[4] = 1 ,这样才能完整表示出节点1与其他节点都是链接的,才没有被覆盖。 + +主要问题也是我们使用了一维数组来记录。 + +如果是二维数组,来记录两个点链接,例如 parent[节点编号A][节点编号B] = 1 ,parent[节点编号B][节点编号A] = 1,来表示节点A与节点B相连,那就没有上面说的这个注意事项了,当然这么做的话,就是多开辟的内存空间。 + +以下是输出最小生成树边的代码,不算最后输出,就额外添加了两行代码,我都注释标记了: + +```CPP +#include +#include +#include + +using namespace std; +int main() { + int v, e; + int x, y, k; + cin >> v >> e; + vector> grid(v + 1, vector(v + 1, 10001)); + while (e--) { + cin >> x >> y >> k; + grid[x][y] = k; + grid[y][x] = k; + } + + vector minDist(v + 1, 10001); + vector isInTree(v + 1, false); + + //加上初始化 + vector parent(v + 1, -1); + + for (int i = 1; i < v; i++) { + int cur = -1; + int minVal = INT_MAX; + for (int j = 1; j <= v; j++) { + if (!isInTree[j] && minDist[j] < minVal) { + minVal = minDist[j]; + cur = j; + } + } + + isInTree[cur] = true; + for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + + parent[j] = cur; // 记录边 + } + } + } + // 输出 最小生成树边的链接情况 + for (int i = 1; i <= v; i++) { + cout << i << "->" << parent[i] << endl; + } +} + +``` + +按照本题示例,代码输入如下: + +``` +1->-1 +2->1 +3->1 +4->3 +5->4 +6->2 +7->5 +``` + +注意,这里是无向图,我在输出上添加了箭头仅仅是为了方便大家看出是边的意思。 + +大家可以和我们本题最后生成的最小生成树的图去对比一下边的链接情况: + +![](https://file1.kamacoder.com/i/algo/20231229115714.png) + +绿色的边是最小生成树,和我们的输出完全一致。 + +## 总结 + +此时我就把prim算法讲解完毕了,我们再来回顾一下。 + +关于prim算法,我自创了三部曲,来帮助大家理解: + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +大家只要理解这三部曲,prim算法至少是可以写出一个框架出来,然后在慢慢补充细节,这样不至于自己在写prim的时候两眼一抹黑完全凭感觉去写。 +这也为什么很多录友感觉prim算法比较难,而且每次学会来,隔一段时间又不会写了,主要是没有一个纲领。 + +理解这三部曲之后,更重要的就是理解minDist数组。 + +**minDist数组是prim算法的灵魂,它帮助prim算法完成最重要的一步,就是如何找到距离最小生成树最近的点**。 + +再来帮大家回顾minDist数组的含义:记录每一个节点距离最小生成树的最近距离。 + +理解minDist数组,至少大家看prim算法的代码不会懵。 + +也正是因为minDist数组的作用,我们根据minDist数组,选取距离生成树最近的节点加入生成树,那么**minDist数组里记录的其实也是最小生成树的边的权值**。 + +所以我们求最小生成树的权值和就是计算后的minDist数组数值总和。 + +最后我们拓展了如何获得最小生成树的每一条边,其实添加的代码很简单,主要是理解为什么使用parent数组来记录边以及在哪里更新parent数组。 + +同时,因为使用一维数组,数组的下标和数组如何赋值很重要,不要搞反,导致结果被覆盖。 + +好了,以上为总结,录友们学习愉快。 + + + + +## 其他语言版本 + +### Java + +```Java +import java.util.*; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int v = scanner.nextInt(); + int e = scanner.nextInt(); + + // 初始化邻接矩阵,所有值初始化为一个大值,表示无穷大 + int[][] grid = new int[v + 1][v + 1]; + for (int i = 0; i <= v; i++) { + Arrays.fill(grid[i], 10001); + } + + // 读取边的信息并填充邻接矩阵 + for (int i = 0; i < e; i++) { + int x = scanner.nextInt(); + int y = scanner.nextInt(); + int k = scanner.nextInt(); + grid[x][y] = k; + grid[y][x] = k; + } + + // 所有节点到最小生成树的最小距离 + int[] minDist = new int[v + 1]; + Arrays.fill(minDist, 10001); + + // 记录节点是否在树里 + boolean[] isInTree = new boolean[v + 1]; + + // Prim算法主循环 + for (int i = 1; i < v; i++) { + int cur = -1; + int minVal = Integer.MAX_VALUE; + + // 选择距离生成树最近的节点 + for (int j = 1; j <= v; j++) { + if (!isInTree[j] && minDist[j] < minVal) { + minVal = minDist[j]; + cur = j; + } + } + + // 将最近的节点加入生成树 + isInTree[cur] = true; + + // 更新非生成树节点到生成树的距离 + for (int j = 1; j <= v; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j]; + } + } + } + + // 统计结果 + int result = 0; + for (int i = 2; i <= v; i++) { + result += minDist[i]; + } + System.out.println(result); + scanner.close(); + } +} + +``` + +### Python +```python +# 接收输入 +v, e = list(map(int, input().strip().split())) +# 按照常规的邻接矩阵存储图信息,不可达的初始化为10001 +graph = [[10001] * (v+1) for _ in range(v+1)] +for _ in range(e): + x, y, w = list(map(int, input().strip().split())) + graph[x][y] = w + graph[y][x] = w + +# 定义加入生成树的标记数组和未加入生成树的最近距离 +visited = [False] * (v + 1) +minDist = [10001] * (v + 1) + +# 循环 n - 1 次,建立 n - 1 条边 +# 从节点视角来看:每次选中一个节点加入树,更新剩余的节点到树的最短距离, +# 这一步其实蕴含了确定下一条选取的边,计入总路程 ans 的计算 +for _ in range(1, v + 1): + min_val = 10002 + cur = -1 + for j in range(1, v + 1): + if visited[j] == False and minDist[j] < min_val: + cur = j + min_val = minDist[j] + visited[cur] = True + for j in range(1, v + 1): + if visited[j] == False and minDist[j] > graph[cur][j]: + minDist[j] = graph[cur][j] + +ans = 0 +for i in range(2, v + 1): + ans += minDist[i] +print(ans) +``` + +```python +def prim(v, e, edges): + import sys + import heapq + + # 初始化邻接矩阵,所有值初始化为一个大值,表示无穷大 + grid = [[10001] * (v + 1) for _ in range(v + 1)] + + # 读取边的信息并填充邻接矩阵 + for edge in edges: + x, y, k = edge + grid[x][y] = k + grid[y][x] = k + + # 所有节点到最小生成树的最小距离 + minDist = [10001] * (v + 1) + + # 记录节点是否在树里 + isInTree = [False] * (v + 1) + + # Prim算法主循环 + for i in range(1, v): + cur = -1 + minVal = sys.maxsize + + # 选择距离生成树最近的节点 + for j in range(1, v + 1): + if not isInTree[j] and minDist[j] < minVal: + minVal = minDist[j] + cur = j + + # 将最近的节点加入生成树 + isInTree[cur] = True + + # 更新非生成树节点到生成树的距离 + for j in range(1, v + 1): + if not isInTree[j] and grid[cur][j] < minDist[j]: + minDist[j] = grid[cur][j] + + # 统计结果 + result = sum(minDist[2:v+1]) + return result + +if __name__ == "__main__": + import sys + input = sys.stdin.read + data = input().split() + + v = int(data[0]) + e = int(data[1]) + + edges = [] + index = 2 + for _ in range(e): + x = int(data[index]) + y = int(data[index + 1]) + k = int(data[index + 2]) + edges.append((x, y, k)) + index += 3 + + result = prim(v, e, edges) + print(result) + +``` + +### Go + +### Rust + +### JavaScript +```js +function prim(v, edges) { + const grid = Array.from({ length: v + 1 }, () => new Array(v + 1).fill(10001)); // Fixed grid initialization + const minDist = new Array(v + 1).fill(10001) + const isInTree = new Array(v + 1).fill(false) + // 建構鄰接矩陣 + for(const [v1, v2, w] of edges) { + grid[v1][v2] = w + grid[v2][v1] = w + } + // prim 演算法 + for (let i = 1 ; i < v ; i++) { + let cur = -1 + let tempMinDist = Number.MAX_VALUE + // 1. 尋找距離生成樹最近的節點 + for (let j = 1 ; j < v + 1 ; j++) { + if (!isInTree[j] && minDist[j] < tempMinDist) { + tempMinDist = minDist[j] + cur = j + } + } + // 2. 將節點放入生成樹 + isInTree[cur] = true + // 3. 更新非生成樹節點與生成樹的最短距離 + for (let j = 1 ; j < v + 1 ; j++) { + if (!isInTree[j] && grid[cur][j] < minDist[j]) { + minDist[j] = grid[cur][j] + } + } + } + console.log(minDist.slice(2).reduce((acc, cur) => acc + cur, 0)) +} + + +async function main() { + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [v, e] = (await readline()).split(" ").map(Number) + const edges = [] + for (let i = 0 ; i < e ; i++) { + edges.push((await readline()).split(" ").map(Number)) + } + prim(v, edges) +} + + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0054.\346\233\277\346\215\242\346\225\260\345\255\227.md" "b/problems/kamacoder/0054.\346\233\277\346\215\242\346\225\260\345\255\227.md" new file mode 100644 index 0000000000..67d31a5564 --- /dev/null +++ "b/problems/kamacoder/0054.\346\233\277\346\215\242\346\225\260\345\255\227.md" @@ -0,0 +1,435 @@ + +# 替换数字 + +[卡码网题目链接](https://kamacoder.com/problempage.php?pid=1064) + +给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。 + +例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"。 + +对于输入字符串 "a5b",函数应该将其转换为 "anumberb" + +输入:一个字符串 s,s 仅包含小写字母和数字字符。 + +输出:打印一个新的字符串,其中每个数字字符都被替换为了number + +样例输入:a1b2c3 + +样例输出:anumberbnumbercnumber + +数据范围:1 <= s.length < 10000。 + +## 思路 + +如果想把这道题目做到极致,就不要只用额外的辅助空间了! (不过使用Java和Python刷题的录友,一定要使用辅助空间,因为Java和Python里的string不能修改) + +首先扩充数组到每个数字字符替换成 "number" 之后的大小。 + +例如 字符串 "a5b" 的长度为3,那么 将 数字字符变成字符串 "number" 之后的字符串为 "anumberb" 长度为 8。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20231030165201.png) + +然后从后向前替换数字字符,也就是双指针法,过程如下:i指向新长度的末尾,j指向旧长度的末尾。 + +![](https://file1.kamacoder.com/i/algo/20231030173058.png) + +有同学问了,为什么要从后向前填充,从前向后填充不行么? + +从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素整体向后移动。 + +**其实很多数组填充类的问题,其做法都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** + +这么做有两个好处: + +1. 不用申请新数组。 +2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。 + +C++代码如下: + +```CPP +#include +using namespace std; +int main() { + string s; + while (cin >> s) { + int sOldIndex = s.size() - 1; + int count = 0; // 统计数字的个数 + for (int i = 0; i < s.size(); i++) { + if (s[i] >= '0' && s[i] <= '9') { + count++; + } + } + // 扩充字符串s的大小,也就是将每个数字替换成"number"之后的大小 + s.resize(s.size() + count * 5); + int sNewIndex = s.size() - 1; + // 从后往前将数字替换为"number" + while (sOldIndex >= 0) { + if (s[sOldIndex] >= '0' && s[sOldIndex] <= '9') { + s[sNewIndex--] = 'r'; + s[sNewIndex--] = 'e'; + s[sNewIndex--] = 'b'; + s[sNewIndex--] = 'm'; + s[sNewIndex--] = 'u'; + s[sNewIndex--] = 'n'; + } else { + s[sNewIndex--] = s[sOldIndex]; + } + sOldIndex--; + } + cout << s << endl; + } +} + + +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +此时算上本题,我们已经做了七道双指针相关的题目了分别是: + +* [27.移除元素](https://programmercarl.com/0027.移除元素.html) +* [15.三数之和](https://programmercarl.com/0015.三数之和.html) +* [18.四数之和](https://programmercarl.com/0018.四数之和.html) +* [206.翻转链表](https://programmercarl.com/0206.翻转链表.html) +* [142.环形链表II](https://programmercarl.com/0142.环形链表II.html) +* [344.反转字符串](https://programmercarl.com/0344.反转字符串.html) + +## 拓展 + +这里也给大家拓展一下字符串和数组有什么差别, + +字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。 + +在C语言中,把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字符串是否结束的标志。 + +例如这段代码: + +``` +char a[5] = "asd"; +for (int i = 0; a[i] != '\0'; i++) { +} +``` + +在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束。 + +例如这段代码: + +``` +string a = "asd"; +for (int i = 0; i < a.size(); i++) { +} +``` + +那么vector< char > 和 string 又有什么区别呢? + +其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。 + +所以想处理字符串,我们还是会定义一个string类型。 + + +## 其他语言版本 + +### C: + +### Java: +解法一 +```java +import java.util.Scanner; + +public class Main { + + public static String replaceNumber(String s) { + int count = 0; // 统计数字的个数 + int sOldSize = s.length(); + for (int i = 0; i < s.length(); i++) { + if(Character.isDigit(s.charAt(i))){ + count++; + } + } + // 扩充字符串s的大小,也就是每个空格替换成"number"之后的大小 + char[] newS = new char[s.length() + count * 5]; + int sNewSize = newS.length; + // 将旧字符串的内容填入新数组 + System.arraycopy(s.toCharArray(), 0, newS, 0, sOldSize); + // 从后先前将空格替换为"number" + for (int i = sNewSize - 1, j = sOldSize - 1; j < i; j--, i--) { + if (!Character.isDigit(newS[j])) { + newS[i] = newS[j]; + } else { + newS[i] = 'r'; + newS[i - 1] = 'e'; + newS[i - 2] = 'b'; + newS[i - 3] = 'm'; + newS[i - 4] = 'u'; + newS[i - 5] = 'n'; + i -= 5; + } + } + return new String(newS); + }; + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + String s = scanner.next(); + System.out.println(replaceNumber(s)); + scanner.close(); + } +} +``` +解法二 +```java +// 为了还原题目本意,先把原数组复制到扩展长度后的新数组,然后不再使用原数组、原地对新数组进行操作。 +import java.util.*; + +public class Main { + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + String s = sc.next(); + int len = s.length(); + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) >= 0 && s.charAt(i) <= '9') { + len += 5; + } + } + + char[] ret = new char[len]; + for (int i = 0; i < s.length(); i++) { + ret[i] = s.charAt(i); + } + for (int i = s.length() - 1, j = len - 1; i >= 0; i--) { + if ('0' <= ret[i] && ret[i] <= '9') { + ret[j--] = 'r'; + ret[j--] = 'e'; + ret[j--] = 'b'; + ret[j--] = 'm'; + ret[j--] = 'u'; + ret[j--] = 'n'; + } else { + ret[j--] = ret[i]; + } + } + System.out.println(ret); + } +} +``` + +### Python: +```python +class Solution(object): + def subsitute_numbers(self, s): + """ + :type s: str + :rtype: str + """ + + count = sum(1 for char in s if char.isdigit()) # 统计数字的个数 + expand_len = len(s) + (count * 5) # 计算扩充后字符串的大小, x->number, 每有一个数字就要增加五个长度 + res = [''] * expand_len + + new_index = expand_len - 1 # 指向扩充后字符串末尾 + old_index = len(s) - 1 # 指向原字符串末尾 + + while old_index >= 0: # 从后往前, 遇到数字替换成“number” + if s[old_index].isdigit(): + res[new_index-5:new_index+1] = "number" + new_index -= 6 + else: + res[new_index] = s[old_index] + new_index -= 1 + old_index -= 1 + + return "".join(res) + +if __name__ == "__main__": + solution = Solution() + + while True: + try: + s = input() + result = solution.subsitute_numbers(s) + print(result) + except EOFError: + break + +``` + +### Go: +````go +package main + +import "fmt" + +func main(){ + var strByte []byte + + fmt.Scanln(&strByte) + + for i := 0; i < len(strByte); i++{ + if strByte[i] <= '9' && strByte[i] >= '0' { + inserElement := []byte{'n','u','m','b','e','r'} + strByte = append(strByte[:i], append(inserElement, strByte[i+1:]...)...) + i = i + len(inserElement) -1 + } + } + + fmt.Printf(string(strByte)) +} +```` +Go使用双指针解法 +````go +package main + +import "fmt" + +func replaceNumber(strByte []byte) string { + // 查看有多少字符 + numCount, oldSize := 0, len(strByte) + for i := 0; i < len(strByte); i++ { + if (strByte[i] <= '9') && (strByte[i] >= '0') { + numCount ++ + } + } + // 增加长度 + for i := 0; i < numCount; i++ { + strByte = append(strByte, []byte(" ")...) + } + tmpBytes := []byte("number") + // 双指针从后遍历 + leftP, rightP := oldSize-1, len(strByte)-1 + for leftP < rightP { + rightShift := 1 + // 如果是数字则加入number + if (strByte[leftP] <= '9') && (strByte[leftP] >= '0') { + for i, tmpByte := range tmpBytes { + strByte[rightP-len(tmpBytes)+i+1] = tmpByte + } + rightShift = len(tmpBytes) + } else { + strByte[rightP] = strByte[leftP] + } + // 更新指针 + rightP -= rightShift + leftP -= 1 + } + return string(strByte) +} + +func main(){ + var strByte []byte + fmt.Scanln(&strByte) + + newString := replaceNumber(strByte) + + fmt.Println(newString) +} +```` + + + +### JavaScript: +```js +const readline = require("readline"); + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +function main() { + const num0 = "0".charCodeAt(); + const num9 = "9".charCodeAt(); + const a = "a".charCodeAt(); + const z = "z".charCodeAt(); + function isAZ(str) { + return str >= a && str <= z; + } + function isNumber(str) { + return str >= num0 && str <= num9; + } + rl.on("line", (input) => { + let n = 0; + for (let i = 0; i < input.length; i++) { + const val = input[i].charCodeAt(); + if (isNumber(val)) { + n+= 6; + } + if (isAZ(val)) { + n++; + } + } + const ans = new Array(n).fill(0); + let index = input.length - 1; + for (let i = n - 1; i >= 0; i--) { + const val = input[index].charCodeAt(); + if (isAZ(val)) { + ans[i] = input[index]; + } + if (isNumber(val)) { + ans[i] = "r"; + ans[i - 1] = "e"; + ans[i - 2] = "b"; + ans[i - 3] = "m"; + ans[i - 4] = "u"; + ans[i - 5] = "n"; + i -= 5; + } + index--; + } + console.log(ans.join("")); + }) +} + +main(); +``` + +### TypeScript: + + +### Swift: + + +### Scala: + +### PHP: + +```php += 0) { + if (is_numeric($s[$oldLen])) { + $s[$newLen--] = 'r'; + $s[$newLen--] = 'e'; + $s[$newLen--] = 'b'; + $s[$newLen--] = 'm'; + $s[$newLen--] = 'u'; + $s[$newLen--] = 'n'; + } else { + $s[$newLen--] = $s[$oldLen]; + } + $oldLen--; +} + +echo $s; +?> +``` + + + + +### Rust: + +
diff --git "a/problems/kamacoder/0055.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" "b/problems/kamacoder/0055.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 0000000000..be998390c5 --- /dev/null +++ "b/problems/kamacoder/0055.\345\217\263\346\227\213\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,412 @@ + + + +# 右旋字符串 + +[卡码网题目链接](https://kamacoder.com/problempage.php?pid=1065) + +字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。 + +例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。 + +输入:输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。 + +输出:输出共一行,为进行了右旋转操作后的字符串。 + +样例输入: + +``` +2 +abcdefg +``` + +样例输出: + +``` +fgabcde +``` + +数据范围:1 <= k < 10000, 1 <= s.length < 10000; + + +## 思路 + +为了让本题更有意义,提升一下本题难度:**不能申请额外空间,只能在本串上操作**。 (Java不能在字符串上修改,所以使用java一定要开辟新空间) + +不能使用额外空间的话,模拟在本串操作要实现右旋转字符串的功能还是有点困难的。 + +那么我们可以想一下上一题目[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中讲过,使用整体反转+局部反转就可以实现反转单词顺序的目的。 + + +本题中,我们需要将字符串右移n位,字符串相当于分成了两个部分,如果n为2,符串相当于分成了两个部分,如图: (length为字符串长度) + +![](https://file1.kamacoder.com/i/algo/20231106170143.png) + + +右移n位, 就是将第二段放在前面,第一段放在后面,先不考虑里面字符的顺序,是不是整体倒叙不就行了。如图: + +![](https://file1.kamacoder.com/i/algo/20231106171557.png) + +此时第一段和第二段的顺序是我们想要的,但里面的字符位置被我们倒叙,那么此时我们在把 第一段和第二段里面的字符再倒叙一把,这样字符顺序不就正确了。 如果: + +![](https://file1.kamacoder.com/i/algo/20231106172058.png) + +其实,思路就是 通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,**负负得正**,这样就不影响子串里面字符的顺序了。 + +整体代码如下: + +```CPP +// 版本一 +#include +#include +using namespace std; +int main() { + int n; + string s; + cin >> n; + cin >> s; + int len = s.size(); //获取长度 + + reverse(s.begin(), s.end()); // 整体反转 + reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n + reverse(s.begin() + n, s.end()); // 再反转后一段 + + cout << s << endl; + +} +``` + +那么整体反正的操作放在下面,先局部反转行不行? + +可以的,不过,要记得 控制好 局部反转的长度,如果先局部反转,那么先反转的子串长度就是 len - n,如图: + +![](https://file1.kamacoder.com/i/algo/20231106172534.png) + +代码如下: + +```CPP +// 版本二 +#include +#include +using namespace std; +int main() { + int n; + string s; + cin >> n; + cin >> s; + int len = s.size(); //获取长度 + reverse(s.begin(), s.begin() + len - n); // 先反转前一段,长度len-n ,注意这里是和版本一的区别 + reverse(s.begin() + len - n, s.end()); // 再反转后一段 + reverse(s.begin(), s.end()); // 整体反转 + cout << s << endl; + +} +``` + + +## 拓展 + +大家在做剑指offer的时候,会发现 剑指offer的题目是左反转,那么左反转和右反转 有什么区别呢? + +其实思路是一样一样的,就是反转的区间不同而已。如果本题是左旋转n,那么实现代码如下: + +```CPP +#include +#include +using namespace std; +int main() { + int n; + string s; + cin >> n; + cin >> s; + int len = s.size(); //获取长度 + reverse(s.begin(), s.begin() + n); // 反转第一段长度为n + reverse(s.begin() + n, s.end()); // 反转第二段长度为len-n + reverse(s.begin(), s.end()); // 整体反转 + cout << s << endl; + +} +``` + +大家可以感受一下 这份代码和 版本二的区别, 其实就是反转的区间不同而已。 + +那么左旋转的话,可以不可以先整体反转,例如想版本一的那样呢? + +当然可以。 + + + + +## 其他语言版本 + +### Java: +```Java +// 版本一 +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner in = new Scanner(System.in); + int n = Integer.parseInt(in.nextLine()); + String s = in.nextLine(); + + int len = s.length(); //获取字符串长度 + char[] chars = s.toCharArray(); + reverseString(chars, 0, len - 1); //反转整个字符串 + reverseString(chars, 0, n - 1); //反转前一段字符串,此时的字符串首尾尾是0,n - 1 + reverseString(chars, n, len - 1); //反转后一段字符串,此时的字符串首尾尾是n,len - 1 + + System.out.println(chars); + + } + + public static void reverseString(char[] ch, int start, int end) { + //异或法反转字符串,参照题目 344.反转字符串的解释 + while (start < end) { + ch[start] ^= ch[end]; + ch[end] ^= ch[start]; + ch[start] ^= ch[end]; + start++; + end--; + } + } +} + + +// 版本二 +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner in = new Scanner(System.in); + int n = Integer.parseInt(in.nextLine()); + String s = in.nextLine(); + + int len = s.length(); //获取字符串长度 + char[] chars = s.toCharArray(); + reverseString(chars, 0, len - n - 1); //反转前一段字符串,此时的字符串首尾是0,len - n - 1 + reverseString(chars, len - n, len - 1); //反转后一段字符串,此时的字符串首尾是len - n,len - 1 + reverseString(chars, 0, len - 1); //反转整个字符串 + + System.out.println(chars); + + } + + public static void reverseString(char[] ch, int start, int end) { + //异或法反转字符串,参照题目 344.反转字符串的解释 + while (start < end) { + ch[start] ^= ch[end]; + ch[end] ^= ch[start]; + ch[start] ^= ch[end]; + start++; + end--; + } + } +} +``` + +### Python: +```Python +#获取输入的数字k和字符串 +k = int(input()) +s = input() + +#通过切片反转第一段和第二段字符串 +#注意:python中字符串是不可变的,所以也需要额外空间 +s = s[len(s)-k:] + s[:len(s)-k] +print(s) +``` + +```Python 切片法 +k = int(input()) +s = input() + +print(s[-k:] + s[:-k]) +``` + +### Go: +```go +package main +import "fmt" + +func reverse (strByte []byte, l, r int){ + for l < r { + strByte[l], strByte[r] = strByte[r], strByte[l] + l++ + r-- + } +} + + +func main(){ + var str string + var target int + + fmt.Scanln(&target) + fmt.Scanln(&str) + strByte := []byte(str) + + reverse(strByte, 0, len(strByte) - 1) + reverse(strByte, 0, target - 1) + reverse(strByte, target, len(strByte) - 1) + + fmt.Printf(string(strByte)) +} +``` + + +### C: +```C +#include +#include + +void reverse(char *s, int left, int right) +{ + while(left <= right) + { + char c = s[left]; + s[left] = s[right]; + s[right] = c; + left++; + right--; + } +} + +void rightRotate(char *s, int k) +{ + int len = strlen(s); + // 先局部反转再整体反转 + reverse(s, 0, len - k - 1); // 反转前部分 + reverse(s, len - k, len - 1); // 反转后部分:后k位 + reverse(s, 0, len - 1); // 整体反转 +} + +int main() +{ + + int k; + scanf("%d", &k); + char s[10000]; + scanf("%s", s); + + rightRotate(s, k); + printf("%s\n", s); + + return 0; +} +``` + +### JavaScript: +```javascript +// JS中字符串内不可单独修改 + +const readline = require('readline') + +const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout +}) + +const inputs = []; // 存储输入 + +rl.on('line', function(data) { + inputs.push(data); + +}).on('close', function() { + const res = deal(inputs); + // 打印结果 + console.log(res); +}) + +// 对传入的数据进行处理 +function deal(inputs) { + let [k, s] = inputs; + const len = s.length - 1; + k = parseInt(k); + str = s.split(''); + + str = reverseStr(str, 0, len - k) + str = reverseStr(str, len - k + 1, len) + str = reverseStr(str, 0, len) + + return str.join(''); +} + +// 根据提供的范围进行翻转 +function reverseStr(s, start, end) { + + while (start < end) { + [s[start], s[end]] = [s[end], s[start]] + + start++; + end--; + } + + return s; +} +``` + +### TypeScript: + + +### Swift: +```swift +func rotateWords(_ s: String, _ k: Int) -> String { + var chars = Array(s) + // 先反转整体 + reverseWords(&chars, start: 0, end: s.count - 1) + // 反转前半段 + reverseWords(&chars, start: 0, end: k - 1) + // 反转后半段 + reverseWords(&chars, start: k, end: s.count - 1) + return String(chars) +} + +// 反转start...end 的字符数组 +func reverseWords(_ chars: inout [Character], start: Int, end: Int) { + var left = start + var right = end + while left < right, right < chars.count { + (chars[left], chars[right]) = (chars[right], chars[left]) + left += 1 + right -= 1 + } +} +``` + + +### PHP: + +```php + +``` + + +### Scala: + + +### Rust: + +
diff --git "a/problems/kamacoder/0058.\345\214\272\351\227\264\345\222\214.md" "b/problems/kamacoder/0058.\345\214\272\351\227\264\345\222\214.md" new file mode 100644 index 0000000000..894e0383d5 --- /dev/null +++ "b/problems/kamacoder/0058.\345\214\272\351\227\264\345\222\214.md" @@ -0,0 +1,411 @@ + +# 58. 区间和 + +> 本题为代码随想录后续扩充题目,还没有视频讲解,顺便让大家练习一下ACM输入输出模式(笔试面试必备) + +[题目链接](https://kamacoder.com/problempage.php?pid=1070) + +题目描述 + +给定一个整数数组 Array,请计算该数组在每个指定区间内元素的总和。 + +输入描述 + +第一行输入为整数数组 Array 的长度 n,接下来 n 行,每行一个整数,表示数组的元素。随后的输入为需要计算总和的区间,直至文件结束。 + +输出描述 + +输出每个指定区间内元素的总和。 + +输入示例 + +``` +5 +1 +2 +3 +4 +5 +0 1 +1 3 +``` + +输出示例 + +``` +3 +9 +``` + +数据范围: + +0 < n <= 100000 + +## 思路 + +本题我们来讲解 数组 上常用的解题技巧:前缀和 + +首先来看本题,我们最直观的想法是什么? + +那就是给一个区间,然后 把这个区间的和都累加一遍不就得了,是一道简单不能再简单的题目。 + +代码如下: + +```CPP +#include +#include +using namespace std; +int main() { + int n, a, b; + cin >> n; + vector vec(n); + for (int i = 0; i < n; i++) cin >> vec[i]; + while (cin >> a >> b) { + int sum = 0; + // 累加区间 a 到 b 的和 + for (int i = a; i <= b; i++) sum += vec[i]; + cout << sum << endl; + } +} +``` + +代码一提交,发现超时了..... + +我在制作本题的时候,特别制作了大数据量查询,卡的就是这种暴力解法。 + +来举一个极端的例子,如果我查询m次,每次查询的范围都是从0 到 n - 1 + +那么该算法的时间复杂度是 O(n * m) m 是查询的次数 + +如果查询次数非常大的话,这个时间复杂度也是非常大的。 + +接下来我们来引入前缀和,看看前缀和如何解决这个问题。 + +前缀和的思想是重复利用计算过的子数组之和,从而降低区间查询需要累加计算的次数。 + +**前缀和 在涉及计算区间和的问题时非常有用**! + +前缀和的思路其实很简单,我给大家举个例子很容易就懂了。 + +例如,我们要统计 vec[i] 这个数组上的区间和。 + +我们先做累加,即 p[i] 表示 下标 0 到 i 的 vec[i] 累加 之和。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240627110604.png) + +如果,我们想统计,在vec数组上 下标 2 到下标 5 之间的累加和,那是不是就用 p[5] - p[1] 就可以了。 + +为什么呢? + +`p[1] = vec[0] + vec[1];` + +`p[5] = vec[0] + vec[1] + vec[2] + vec[3] + vec[4] + vec[5];` + +`p[5] - p[1] = vec[2] + vec[3] + vec[4] + vec[5];` + +这不就是我们要求的 下标 2 到下标 5 之间的累加和吗。 + +如图所示: + +![](https://file1.kamacoder.com/i/algo/20240627111319.png) + +`p[5] - p[1]` 就是 红色部分的区间和。 + +而 p 数组是我们之前就计算好的累加和,所以后面每次求区间和的之后 我们只需要 O(1) 的操作。 + +**特别注意**: 在使用前缀和求解的时候,要特别注意 求解区间。 + +如上图,如果我们要求 区间下标 [2, 5] 的区间和,那么应该是 p[5] - p[1],而不是 p[5] - p[2]。 + +**很多录友在使用前缀和的时候,分不清前缀和的区间,建议画一画图,模拟一下 思路会更清晰**。 + +本题C++代码如下: + +```CPP +#include +#include +using namespace std; +int main() { + int n, a, b; + cin >> n; + vector vec(n); + vector p(n); + int presum = 0; + for (int i = 0; i < n; i++) { + cin >> vec[i]; + presum += vec[i]; + p[i] = presum; + } + + while (cin >> a >> b) { + int sum; + if (a == 0) sum = p[b]; + else sum = p[b] - p[a - 1]; + cout << sum << endl; + } +} + +``` + +C++ 代码 面对大量数据 读取 输出操作,最好用scanf 和 printf,耗时会小很多: + +```CPP +#include +#include +using namespace std; +int main() { + int n, a, b; + cin >> n; + vector vec(n); + vector p(n); + int presum = 0; + for (int i = 0; i < n; i++) { + scanf("%d", &vec[i]); + presum += vec[i]; + p[i] = presum; + } + + while (~scanf("%d%d", &a, &b)) { + int sum; + if (a == 0) sum = p[b]; + else sum = p[b] - p[a - 1]; + printf("%d\n", sum); + } +} + +``` + +## 其他语言版本 + +### Java + +```Java + +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + int n = scanner.nextInt(); + int[] vec = new int[n]; + int[] p = new int[n]; + + int presum = 0; + for (int i = 0; i < n; i++) { + vec[i] = scanner.nextInt(); + presum += vec[i]; + p[i] = presum; + } + + while (scanner.hasNextInt()) { + int a = scanner.nextInt(); + int b = scanner.nextInt(); + + int sum; + if (a == 0) { + sum = p[b]; + } else { + sum = p[b] - p[a - 1]; + } + System.out.println(sum); + } + + scanner.close(); + } +} + + +``` + +### Python + +```python + +import sys +input = sys.stdin.read + +def main(): + data = input().split() + index = 0 + n = int(data[index]) + index += 1 + vec = [] + for i in range(n): + vec.append(int(data[index + i])) + index += n + + p = [0] * n + presum = 0 + for i in range(n): + presum += vec[i] + p[i] = presum + + results = [] + while index < len(data): + a = int(data[index]) + b = int(data[index + 1]) + index += 2 + + if a == 0: + sum_value = p[b] + else: + sum_value = p[b] - p[a - 1] + + results.append(sum_value) + + for result in results: + print(result) + +if __name__ == "__main__": + main() + +``` + + +### JavaScript + +``` JavaScript + +function prefixSum() { + const readline = require('readline'); + + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout + }); + + let inputLines = []; + rl.on('line', (line) => { + inputLines.push(line.trim()); + }); + + rl.on('close', () => { + // 读取项数 n + const n = parseInt(inputLines[0]); + + // 使用前缀和,复杂度控制在 O(1) + let sum = new Array(n); + sum[0] = parseInt(inputLines[1]); + + // 计算前缀和数组 + for (let i = 1; i < n; i++) { + let value = parseInt(inputLines[i + 1]); + sum[i] = sum[i - 1] + value; + } + + // 处理区间和查询 + for (let i = n + 1; i < inputLines.length; i++) { + let [left, right] = inputLines[i].split(' ').map(Number); + + if (left === 0) { + console.log(sum[right]); + } else { + console.log(sum[right] - sum[left - 1]); + } + } + }); +} + + +``` + + + +### C + +```C +#include +#include + +int main(int argc, char *argv[]) +{ + int num; + // 读取数组长度 + scanf("%d", &num); + + // 使用动态内存分配而不是静态数组,以适应不同的输入大小 + int *a = (int *)malloc((num + 1) * sizeof(int)); + + // 初始化前缀和数组的第一个元素为0 + a[0] = 0; + + // 读取数组元素并计算前缀和 + for (int i = 1; i <= num; i++) + { + int mm; + scanf("%d", &mm); + // 累加前缀和 + a[i] = a[i - 1] + mm; + } + + int m, n; + // 循环读取区间并计算区间和,直到输入结束 + // scanf()返回成功匹配和赋值的个数,到达文件末尾则返回 EOF + while (scanf("%d%d", &m, &n) == 2) + { + // 输出区间和,注意区间是左闭右开,因此a[n+1]是包含n的元素的前缀和 + printf("%d\n", a[n+1] - a[m]); + } + + // 释放之前分配的内存 + free(a); + return 0; +} + +``` + +### Go + +```go +package main + +import ( + "fmt" + "bufio" + "strconv" + "os" +) + +func main() { + // bufio中读取数据的接口,因为数据卡的比较严,导致使用fmt.Scan会超时 + scanner := bufio.NewScanner(os.Stdin) + + // 获取数组大小 + scanner.Scan() + n, _ := strconv.Atoi(scanner.Text()) + + // 获取数组元素的同时计算前缀和,一般建议切片开大一点防止各种越界问题 + arr := make([]int, n + 1) + for i := 0; i < n; i++ { + scanner.Scan() + arr[i], _ = strconv.Atoi(scanner.Text()) + if i != 0 { + arr[i] += arr[i - 1] + } + } + + /* + 区间[l, r]的和可以使用区间[0, r]和[0, l - 1]相减得到, + 在代码中即为arr[r]-arr[l-1]。这里需要注意l-1是否越界 + */ + for { + var l, r int + scanner.Scan() + _, err := fmt.Sscanf(scanner.Text(), "%d %d", &l, &r) + if err != nil { + return + } + + if l > 0 { + fmt.Println(arr[r] - arr[l - 1]) + } else { + fmt.Println(arr[r]) + } + } +} +``` + +
diff --git "a/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I-SPFA.md" "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I-SPFA.md" new file mode 100644 index 0000000000..9d3fbe839e --- /dev/null +++ "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I-SPFA.md" @@ -0,0 +1,539 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# Bellman_ford 队列优化算法(又名SPFA) + +[卡码网:94. 城市间货物运输 I](https://kamacoder.com/problempage.php?pid=1152) + +题目描述 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。 + +权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + + +请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。 + +如果最低运输成本是一个负数,它表示在遵循最优路径的情况下,运输过程中反而能够实现盈利。 + +城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。 + +> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +输入描述 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v(单向图)。 + +输出描述 + +如果能够从城市 1 到连通到城市 n, 请输出一个整数,表示运输成本。如果该整数是负数,则表示实现了盈利。如果从城市 1 没有路径可达城市 n,请输出 "unconnected"。 + +输入示例: + +``` +6 7 +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +## 背景 + +本题我们来系统讲解 Bellman_ford 队列优化算法 ,也叫SPFA算法(Shortest Path Faster Algorithm)。 + +> SPFA的称呼来自 1994年西南交通大学段凡丁的论文,其实Bellman_ford 提出后不久 (20世纪50年代末期) 就有队列优化的版本,国际上不承认这个算法是是国内提出的。 所以国际上一般称呼 该算法为 Bellman_ford 队列优化算法(Queue improved Bellman-Ford) + +大家知道以上来历,知道 SPFA 和 Bellman_ford 队列优化算法 指的都是一个算法就好。 + +如果大家还不够了解 Bellman_ford 算法,强烈建议按照《代码随想录》的顺序学习,否则可能看不懂下面的讲解。 + +大家可以发现 Bellman_ford 算法每次松弛 都是对所有边进行松弛。 + +但真正有效的松弛,是基于已经计算过的节点在做的松弛。 + +给大家举一个例子: + +![](https://file1.kamacoder.com/i/algo/20240328104119.png) + +本图中,对所有边进行松弛,真正有效的松弛,只有松弛 边(节点1->节点2) 和 边(节点1->节点3) 。 + +而松弛 边(节点4->节点6) ,边(节点5->节点3)等等 都是无效的操作,因为 节点4 和 节点 5 都是没有被计算过的节点。 + + +所以 Bellman_ford 算法 每次都是对所有边进行松弛,其实是多做了一些无用功。 + +**只需要对 上一次松弛的时候更新过的节点作为出发节点所连接的边 进行松弛就够了**。 + +基于以上思路,如何记录 上次松弛的时候更新过的节点呢? + +用队列来记录。(其实用栈也行,对元素顺序没有要求) + +## 模拟过程 + +接下来来举例这个队列是如何工作的。 + +以示例给出的所有边为例: + +``` +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +我们依然使用**minDist数组来表达 起点到各个节点的最短距离**,例如minDist[3] = 5 表示起点到达节点3 的最小距离为5 + +初始化,起点为节点1, 起点到起点的最短距离为0,所以minDist[1] 为 0。 将节点1 加入队列 (下次松弛从节点1开始) + +![](https://file1.kamacoder.com/i/algo/20240411115555.png) + +------------ + +从队列里取出节点1,松弛节点1 作为出发点连接的边(节点1 -> 节点2)和边(节点1 -> 节点3) + +边:节点1 -> 节点2,权值为1 ,minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 。 + +边:节点1 -> 节点3,权值为5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5。 + +将节点2、节点3 加入队列,如图: + +![](https://file1.kamacoder.com/i/algo/20240411115544.png) + + +----------------- + + +从队列里取出节点2,松弛节点2 作为出发点连接的边(节点2 -> 节点4)和边(节点2 -> 节点5) + +边:节点2 -> 节点4,权值为1 ,minDist[4] > minDist[2] + (-3) ,更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 。 + +边:节点2 -> 节点5,权值为2 ,minDist[5] > minDist[2] + 2 ,更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 。 + + +将节点4,节点5 加入队列,如图: + +![](https://file1.kamacoder.com/i/algo/20240412110348.png) + + +-------------------- + + +从队列里出去节点3,松弛节点3 作为出发点连接的边。 + +因为没有从节点3作为出发点的边,所以这里就从队列里取出节点3就好,不用做其他操作,如图: + +![](https://file1.kamacoder.com/i/algo/20240412110420.png) + + +------------ + +从队列中取出节点4,松弛节点4作为出发点连接的边(节点4 -> 节点6) + +边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 。 + +将节点6加入队列 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240412110445.png) + + +--------------- + +从队列中取出节点5,松弛节点5作为出发点连接的边(节点5 -> 节点3),边(节点5 -> 节点6) + +边:节点5 -> 节点3,权值为1 ,minDist[3] > minDist[5] + 1 ,更新 minDist[3] = minDist[5] + 1 = 3 + 1 = 4 + +边:节点5 -> 节点6,权值为-2 ,minDist[6] > minDist[5] + (-2) ,更新 minDist[6] = minDist[5] + (-2) = 3 - 2 = 1 + +如图,将节点3加入队列,因为节点6已经在队列里,所以不用重复添加 + +![](https://file1.kamacoder.com/i/algo/20240729161116.png) + +所以我们在加入队列的过程可以有一个优化,**用visited数组记录已经在队列里的元素,已经在队列的元素不用重复加入** + +-------------- + +从队列中取出节点6,松弛节点6 作为出发点连接的边。 + +节点6作为终点,没有可以出发的边。 + +同理从队列中取出节点3,也没有可以出发的边 + +所以直接从队列中取出,如图: + +![](https://file1.kamacoder.com/i/algo/20240411115424.png) + +---------- + +这样我们就完成了基于队列优化的bellman_ford的算法模拟过程。 + +大家可以发现 基于队列优化的算法,要比bellman_ford 算法 减少很多无用的松弛情况,特别是对于边数众多的大图 优化效果明显。 + +了解了大体流程,我们再看代码应该怎么写。 + +在上面模拟过程中,我们每次都要知道 一个节点作为出发点连接了哪些节点。 + +如果想方便知道这些数据,就需要使用邻接表来存储这个图,如果对于邻接表不了解的话,可以看 [kama0047.参会dijkstra堆](./0047.参会dijkstra堆.md) 中 图的存储 部分。 + + +整体代码如下: + +``` +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); + + vector isInQueue(n + 1); // 加入优化,已经在队里里的元素不用重复添加 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + + queue que; + que.push(start); + + while (!que.empty()) { + + int node = que.front(); que.pop(); + isInQueue[node] = false; // 从队列里取出的时候,要取消标记,我们只保证已经在队列里的元素不用重复加入 + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int value = edge.val; + if (minDist[to] > minDist[from] + value) { // 开始松弛 + minDist[to] = minDist[from] + value; + if (isInQueue[to] == false) { // 已经在队列里的元素不用重复添加 + que.push(to); + isInQueue[to] = true; + } + } + } + + } + if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 +} + +``` + +## 效率分析 + +队列优化版Bellman_ford 的时间复杂度 并不稳定,效率高低依赖于图的结构。 + +例如 如果是一个双向图,且每一个节点和所有其他节点都相连的话,那么该算法的时间复杂度就接近于 Bellman_ford 的 O(N * E) N 为节点数量,E为边的数量。 + +在这种图中,每一个节点都会重复加入队列 n - 1次,因为 这种图中 每个节点 都有 n-1 条指向该节点的边,每条边指向该节点,就需要加入一次队列。(如果这里看不懂,可以在重温一下代码逻辑) + +至于为什么 双向图且每一个节点和所有其他节点都相连的话,每个节点 都有 n-1 条指向该节点的边, 我再来举个例子,如图: + +![](https://file1.kamacoder.com/i/algo/20240416104138.png) + +图中 每个节点都与其他所有节点相连,节点数n 为 4,每个节点都有3条指向该节点的边,即入度为3。 + +n为其他数值的时候,也是一样的。 + +当然这种图是比较极端的情况,也是最稠密的图。 + +所以如果图越稠密,则 SPFA的效率越接近与 Bellman_ford。 + +反之,图越稀疏,SPFA的效率就越高。 + +一般来说,SPFA 的时间复杂度为 O(K * N) K 为不定值,因为 节点需要计入几次队列取决于 图的稠密度。 + +如果图是一条线形图且单向的话,每个节点的入度为1,那么只需要加入一次队列,这样时间复杂度就是 O(N)。 + +所以 SPFA 在最坏的情况下是 O(N * E),但 一般情况下 时间复杂度为 O(K * N)。 + +尽管如此,**以上分析都是 理论上的时间复杂度分析**。 + +并没有计算 出队列 和 入队列的时间消耗。 因为这个在不同语言上 时间消耗也是不一定的。 + +以C++为例,以下两段代码理论上,时间复杂度都是 O(n) : + +```CPP +for (long long i = 0; i < n; i++) { + k++; +} + +``` + +```CPP +for (long long i = 0; i < n; i++) { + que.push(i); + que.front(); + que.pop(); +} + +``` + +在 MacBook Pro (13-inch, M1, 2020) 机器上分别测试这两段代码的时间消耗情况: + +* n = 10^4,第一段代码的时间消耗:1ms,第二段代码的时间消耗: 4 ms +* n = 10^5,第一段代码的时间消耗:1ms,第二段代码的时间消耗: 13 ms +* n = 10^6,第一段代码的时间消耗:4ms,第二段代码的时间消耗: 59 ms +* n = 10^7,第一段代码的时间消耗: 24ms,第二段代码的时间消耗: 463 ms +* n = 10^8,第一段代码的时间消耗: 135ms,第二段代码的时间消耗: 4268 ms + +在这里就可以看出 出队列和入队列 其实也是十分耗时的。 + +SPFA(队列优化版Bellman_ford) 在理论上 时间复杂度更胜一筹,但实际上,也要看图的稠密程度,如果 图很大且非常稠密的情况下,虽然 SPFA的时间复杂度接近Bellman_ford,但实际时间消耗 可能是 SPFA耗时更多。 + +针对这种情况,我在后面题目讲解中,会特别加入稠密图的测试用例来给大家讲解。 + + +## 拓展 + +这里可能有录友疑惑,`while (!que.empty())` 队里里 会不会造成死循环? 例如 图中有环,这样一直有元素加入到队列里? + +其实有环的情况,要看它是 正权回路 还是 负权回路。 + +题目描述中,已经说了,本题没有 负权回路 。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240412111849.png) + +正权回路 就是有环,但环的总权值为正数。 + +在有环且只有正权回路的情况下,即使元素重复加入队列,最后,也会因为 所有边都松弛后,节点数值(minDist数组)不在发生变化了 而终止。 + +(而且有重复元素加入队列是正常的,多条路径到达同一个节点,节点必要要选择一个最短的路径,而这个节点就会重复加入队列进行判断,选一个最短的) + +在[0094.城市间货物运输I](./0094.城市间货物运输I.md) 中我们讲过对所有边 最多松弛 n -1 次,就一定可以求出所有起点到所有节点的最小距离即 minDist数组。 + +即使再松弛n次以上, 所有起点到所有节点的最小距离(minDist数组) 不会再变了。 (这里如果不理解,建议认真看[0094.城市间货物运输I](./0094.城市间货物运输I.md)讲解) + +所以本题我们使用队列优化,有元素重复加入队列,也会因为最后 minDist数组 不会在发生变化而终止。 + +节点再加入队列,需要有松弛的行为, 而 每个节点已经都计算出来 起点到该节点的最短路径,那么就不会有 执行这个判断条件`if (minDist[to] > minDist[from] + value)`,从而不会有新的节点加入到队列。 + +但如果本题有 负权回路,那情况就不一样了,我在下一题目讲解中,会重点讲解 负权回路 带来的变化。 + + + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + List> graph = new ArrayList<>(); + + for (int i = 0; i <= n; i++) { + graph.add(new ArrayList<>()); + } + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + graph.get(from).add(new Edge(from, to, val)); + } + + // Declare the minDist array to record the minimum distance form current node to the original node + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[1] = 0; + + // Declare a queue to store the updated nodes instead of traversing all nodes each loop for more efficiency + Queue queue = new LinkedList<>(); + queue.offer(1); + + // Declare a boolean array to record if the current node is in the queue to optimise the processing + boolean[] isInQueue = new boolean[n + 1]; + + while (!queue.isEmpty()) { + int curNode = queue.poll(); + isInQueue[curNode] = false; // Represents the current node is not in the queue after being polled + for (Edge edge : graph.get(curNode)) { + if (minDist[edge.to] > minDist[edge.from] + edge.val) { // Start relaxing the edge + minDist[edge.to] = minDist[edge.from] + edge.val; + if (!isInQueue[edge.to]) { // Don't add the node if it's already in the queue + queue.offer(edge.to); + isInQueue[edge.to] = true; + } + } + } + } + + // Outcome printing + if (minDist[n] == Integer.MAX_VALUE) { + System.out.println("unconnected"); + } else { + System.out.println(minDist[n]); + } + } +} + +``` + +### Python +```Python +import collections + +def main(): + n, m = map(int, input().strip().split()) + edges = [[] for _ in range(n + 1)] + for _ in range(m): + src, dest, weight = map(int, input().strip().split()) + edges[src].append([dest, weight]) + + minDist = [float("inf")] * (n + 1) + minDist[1] = 0 + que = collections.deque([1]) + visited = [False] * (n + 1) + visited[1] = True + + while que: + cur = que.popleft() + visited[cur] = False + for dest, weight in edges[cur]: + if minDist[cur] != float("inf") and minDist[cur] + weight < minDist[dest]: + minDist[dest] = minDist[cur] + weight + if visited[dest] == False: + que.append(dest) + visited[dest] = True + + if minDist[-1] == float("inf"): + return "unconnected" + return minDist[-1] + +if __name__ == "__main__": + print(main()) +``` +### Go + +### Rust + +### JavaScript + +```js +async function main() { + // 輸入 + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [n, m] = (await readline()).split(" ").map(Number) + const grid = {} + for (let i = 0 ; i < m ; i++) { + const [src, desc, w] = (await readline()).split(" ").map(Number) + if (grid.hasOwnProperty(src)) { + grid[src].push([desc, w]) + } else { + grid[src] = [[desc, w]] + } + } + const minDist = Array.from({length: n + 1}, () => Number.MAX_VALUE) + + // 起始點 + minDist[1] = 0 + + const q = [1] + const visited = Array.from({length: n + 1}, () => false) + + while (q.length) { + const src = q.shift() + const neighbors = grid[src] + visited[src] = false + if (neighbors) { + for (const [desc, w] of neighbors) { + if (minDist[src] !== Number.MAX_VALUE + && minDist[src] + w < minDist[desc]) { + minDist[desc] = minDist[src] + w + if (!visited[desc]) { + q.push(desc) + visited[desc] = true + } + + } + } + } + } + + // 輸出 + if (minDist[n] === Number.MAX_VALUE) { + console.log('unconnected') + } else { + console.log(minDist[n]) + } +} + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + + + + + +
diff --git "a/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I.md" "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I.md" new file mode 100644 index 0000000000..63d1be2a30 --- /dev/null +++ "b/problems/kamacoder/0094.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223I.md" @@ -0,0 +1,541 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# Bellman_ford 算法精讲 + +[卡码网:94. 城市间货物运输 I](https://kamacoder.com/problempage.php?pid=1152) + +题目描述 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。 + +权值为正表示扣除了政府补贴后运输货物仍需支付的费用;权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + + +请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。 + +如果最低运输成本是一个负数,它表示在遵循最优路径的情况下,运输过程中反而能够实现盈利。 + +城市 1 到城市 n 之间可能会出现没有路径的情况,同时保证道路网络中不存在任何负权回路。 + +> 负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +输入描述 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v(单向图)。 + +输出描述 + +如果能够从城市 1 到连通到城市 n, 请输出一个整数,表示运输成本。如果该整数是负数,则表示实现了盈利。如果从城市 1 没有路径可达城市 n,请输出 "unconnected"。 + +输入示例: + +``` +6 7 +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +![](https://file1.kamacoder.com/i/algo/20240509200224.png) + +## 思路 + +本题依然是单源最短路问题,求 从 节点1 到节点n 的最小费用。 **但本题不同之处在于 边的权值是有负数了**。 + +从 节点1 到节点n 的最小费用也可以是负数,费用如果是负数 则表示 运输的过程中 政府补贴大于运输成本。 + +在求单源最短路的方法中,使用dijkstra 的话,则要求图中边的权值都为正数。 + +我们在 [dijkstra朴素版](./0047.参会dijkstra朴素.md) 中专门有讲解:为什么有边为负数 使用dijkstra就不行了。 + +**本题是经典的带负权值的单源最短路问题,此时就轮到Bellman_ford登场了**,接下来我们来详细介绍Bellman_ford 算法 如何解决这类问题。 + +> 该算法是由 R.Bellman 和L.Ford 在20世纪50年代末期发明的算法,故称为Bellman_ford算法。 + +**Bellman_ford算法的核心思想是 对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路**。 + +## 什么叫做松弛 + +看到这里,估计大家都比较晕了,为什么是 n-1 次,那“松弛”这两个字究竟是个啥意思? + +我们先来说什么是 “松弛”。 + +《算法四》里面把这个操作叫做 “放松”, 英文版里叫做 “relax the edge” + +所以大家翻译过来,就是 “放松” 或者 “松弛” 。 + +但《算法四》没有具体去讲这个 “放松” 究竟是个啥? 网上很多题解也没有讲题解里的 “松弛这条边,松弛所有边”等等 里面的 “松弛” 究竟是什么意思? + +这里我给大家举一个例子,每条边有起点、终点和边的权值。例如一条边,节点A 到 节点B 权值为value,如图: + +![](https://file1.kamacoder.com/i/algo/20240327102620.png) + +minDist[B] 表示 到达B节点 最小权值,minDist[B] 有哪些状态可以推出来? + +状态一: minDist[A] + value 可以推出 minDist[B] +状态二: minDist[B]本身就有权值 (可能是其他边链接的节点B 例如节点C,以至于 minDist[B]记录了其他边到minDist[B]的权值) + +minDist[B] 应为如何取舍。 + +本题我们要求最小权值,那么 这两个状态我们就取最小的 + +```CPP +if (minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value + +``` + +也就是说,如果 通过 A 到 B 这条边可以获得更短的到达B节点的路径,即如果 `minDist[B] > minDist[A] + value`,那么我们就更新 `minDist[B] = minDist[A] + value` ,**这个过程就叫做 “松弛**” 。 + +以上讲了这么多,其实都是围绕以下这句代码展开: + +``` +if (minDist[B] > minDist[A] + value) minDist[B] = minDist[A] + value + +``` + +**这句代码就是 Bellman_ford算法的核心操作**。 + +以上代码也可以这么写:`minDist[B] = min(minDist[A] + value, minDist[B]) ` + +如果大家看过代码随想录的动态规划章节,会发现 无论是背包问题还是子序列问题,这段代码(递推公式)出现频率非常高的。 + +其实 Bellman_ford算法 也是采用了动态规划的思想,即:将一个问题分解成多个决策阶段,通过状态之间的递归关系最后计算出全局最优解。 + +(如果理解不了动态规划的思想也无所谓,理解我上面讲的松弛操作就好) + +**那么为什么是 n - 1次 松弛呢**? + +这里要给大家模拟一遍 Bellman_ford 的算法才行,接下来我们来看看对所有边松弛 n - 1 次的操作是什么样的。 + +我们依然使用**minDist数组来表达 起点到各个节点的最短距离**,例如minDist[3] = 5 表示起点到达节点3 的最小距离为5 + +### 模拟过程 + +初始化过程。 + +起点为节点1, 起点到起点的距离为0,所以 minDist[1] 初始化为0 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240328104119.png) + +其他节点对应的minDist初始化为max,因为我们要求最小距离,那么还没有计算过的节点 默认是一个最大数,这样才能更新最小距离。 + + +对所有边 进行第一次松弛: (什么是松弛,在上面我已经详细讲过) + +以示例给出的所有边为例: + +``` +5 6 -2 +1 2 1 +5 3 1 +2 5 2 +2 4 -3 +4 6 4 +1 3 5 +``` + +接下来我们来松弛一遍所有的边。 + +边:节点5 -> 节点6,权值为-2 ,minDist[5] 还是默认数值max,所以不能基于 节点5 去更新节点6,如图: + +![](https://file1.kamacoder.com/i/algo/20240329113537.png) + +(在复习一下,minDist[5] 表示起点到节点5的最短距离) + + +边:节点1 -> 节点2,权值为1 ,minDist[2] > minDist[1] + 1 ,更新 minDist[2] = minDist[1] + 1 = 0 + 1 = 1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240329113703.png) + +边:节点5 -> 节点3,权值为1 ,minDist[5] 还是默认数值max,所以不能基于节点5去更新节点3 如图: + +![](https://file1.kamacoder.com/i/algo/20240329113827.png) + + +边:节点2 -> 节点5,权值为2 ,minDist[5] > minDist[2] + 2 (经过上面的计算minDist[2]已经不是默认值,而是 1),更新 minDist[5] = minDist[2] + 2 = 1 + 2 = 3 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240329113927.png) + + +边:节点2 -> 节点4,权值为-3 ,minDist[4] > minDist[2] + (-3),更新 minDist[4] = minDist[2] + (-3) = 1 + (-3) = -2 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240329114036.png) + +边:节点4 -> 节点6,权值为4 ,minDist[6] > minDist[4] + 4,更新 minDist[6] = minDist[4] + 4 = -2 + 4 = 2 + +![](https://file1.kamacoder.com/i/algo/20240329114120.png) + +边:节点1 -> 节点3,权值为5 ,minDist[3] > minDist[1] + 5,更新 minDist[3] = minDist[1] + 5 = 0 + 5 = 5 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240329114324.png) + +-------- + +以上是对所有边进行一次松弛之后的结果。 + +那么需要对所有边松弛几次才能得到 起点(节点1) 到终点(节点6)的最短距离呢? + +**对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**。 + +上面的距离中,我们得到里 起点达到 与起点一条边相邻的节点2 和 节点3 的最短距离,分别是 minDist[2] 和 minDist[3] + +这里有录友疑惑了 minDist[3] = 5,分明不是 起点到达 节点3 的最短距离,节点1 -> 节点2 -> 节点5 -> 节点3 这条路线 距离才是4。 + +注意我上面讲的是 **对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**,这里 说的是 一条边相连的节点。 + +与起点(节点1)一条边相邻的节点,到达节点2 最短距离是 1,到达节点3 最短距离是5。 + +而 节点1 -> 节点2 -> 节点5 -> 节点3 这条路线 是 与起点 三条边相连的路线了。 + +所以对所有边松弛一次 能得到 与起点 一条边相连的节点最短距离。 + +那对所有边松弛两次 可以得到与起点 两条边相连的节点的最短距离。 + +那对所有边松弛三次 可以得到与起点 三条边相连的节点的最短距离,这个时候,我们就能得到到达节点3真正的最短距离,也就是 节点1 -> 节点2 -> 节点5 -> 节点3 这条路线。 + +那么再回归刚刚的问题,**需要对所有边松弛几次才能得到 起点(节点1) 到终点(节点6)的最短距离呢**? + +节点数量为n,那么起点到终点,最多是 n-1 条边相连。 + +那么无论图是什么样的,边是什么样的顺序,我们对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。 + +其实也同时计算出了,起点 到达 所有节点的最短距离,因为所有节点与起点连接的边数最多也就是 n-1 条边。 + +截止到这里,Bellman_ford 的核心算法思路,大家就了解的差不多了。 + +共有两个关键点。 + +* “松弛”究竟是个啥? +* 为什么要对所有边松弛 n - 1 次 (n为节点个数) ? + +那么Bellman_ford的解题解题过程其实就是对所有边松弛 n-1 次,然后得出得到终点的最短路径。 + + +### 代码 + +理解上面讲解的内容,代码就更容易写了,本题代码如下:(详细注释) + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid; + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + for (int i = 1; i < n; i++) { // 对所有边 松弛 n-1 次 + for (vector &side : grid) { // 每一次松弛,都是对所有边进行松弛 + int from = side[0]; // 边的出发点 + int to = side[1]; // 边的到达点 + int price = side[2]; // 边的权值 + // 松弛操作 + // minDist[from] != INT_MAX 防止从未计算过的节点出发 + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) { + minDist[to] = minDist[from] + price; + } + } + } + if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} +``` + +* 时间复杂度: O(N * E) , N为节点数量,E为图中边的数量 +* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间 + +关于空间复杂度,可能有录友疑惑,代码中数组grid不也开辟空间了吗? 为什么只算minDist数组的空间呢? + +grid数组是用来存图的,这是题目描述中必须要使用的空间,而不是我们算法所使用的空间。 + +我们在讲空间复杂度的时候,一般都是说,我们这个算法所用的空间复杂度。 + + +### 拓展 + +有录友可能会想,那我 松弛 n 次,松弛 n + 1次,松弛 2 * n 次会怎么样? + +其实没啥影响,结果不会变的,因为 题目中说了 “同时保证道路网络中不存在任何负权回路” 也就是图中没有 负权回路(在有向图中出现有向环 且环的总权值为负数)。 + +那么我们只要松弛 n - 1次 就一定能得到结果,没必要在松弛更多次了。 + +这里有疑惑的录友,可以加上打印 minDist数组 的日志,尝试一下,看看松弛 n 次会怎么样。 + +你会发现 松弛 大于 n - 1次,minDist数组 就不会变化了。 + +这里我给出打印日志的代码: + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid; + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + for (int i = 1; i < n; i++) { // 对所有边 松弛 n-1 次 + for (vector &side : grid) { // 每一次松弛,都是对所有边进行松弛 + int from = side[0]; // 边的出发点 + int to = side[1]; // 边的到达点 + int price = side[2]; // 边的权值 + // 松弛操作 + // minDist[from] != INT_MAX 防止从未计算过的节点出发 + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) { + minDist[to] = minDist[from] + price; + } + } + cout << "对所有边松弛 " << i << "次" << endl; + for (int k = 1; k <= n; k++) { + cout << minDist[k] << " "; + } + cout << endl; + } + if (minDist[end] == INT_MAX) cout << "unconnected" << endl; // 不能到达终点 + else cout << minDist[end] << endl; // 到达终点最短路径 + +} + +``` + +通过打日志,大家发现,怎么对所有边进行第二次松弛以后结果就 不再变化了,那根本就不用松弛 n - 1 ? + +这是本题的样例的特殊性, 松弛 n-1 次 是保证对任何图 都能最后求得到终点的最小距离。 + +如果还想不明白 我再举一个例子,用以下测试用例再跑一下。 + + +``` +6 5 +5 6 1 +4 5 1 +3 4 1 +2 3 1 +1 2 1 +``` + +打印结果: + +``` +对所有边松弛 1次 +0 1 2147483647 2147483647 2147483647 2147483647 +对所有边松弛 2次 +0 1 2 2147483647 2147483647 2147483647 +对所有边松弛 3次 +0 1 2 3 2147483647 2147483647 +对所有边松弛 4次 +0 1 2 3 4 2147483647 +对所有边松弛 5次 +0 1 2 3 4 5 +``` + +你会发现到 n-1 次 才打印出最后的最短路结果。 + +关于上面的讲解,大家一定要多写代码去实验,验证自己的想法。 + +**至于 负权回路 ,我在下一篇会专门讲解这种情况,大家有个印象就好**。 + + +## 总结 + +Bellman_ford 是可以计算 负权值的单源最短路算法。 + +其算法核心思路是对 所有边进行 n-1 次 松弛。 + +弄清楚 什么是 松弛? 为什么要 n-1 次? 对理解Bellman_ford 非常重要。 + +## 其他语言版本 + +### Java +```Java +public class Main { + + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + List edges = new ArrayList<>(); + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + edges.add(new Edge(from, to, val)); + } + + // Represents the minimum distance from the current node to the original node + int[] minDist = new int[n + 1]; + + // Initialize the minDist array + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[1] = 0; + + // Starts the loop to relax all edges n - 1 times to update minDist array + for (int i = 1; i < n; i++) { + + for (Edge edge : edges) { + // Updates the minDist array + if (minDist[edge.from] != Integer.MAX_VALUE && (minDist[edge.from] + edge.val) < minDist[edge.to]) { + minDist[edge.to] = minDist[edge.from] + edge.val; + } + } + } + + // Outcome printing + if (minDist[n] == Integer.MAX_VALUE) { + System.out.println("unconnected"); + } else { + System.out.println(minDist[n]); + } + } +} + +``` + +### Python +```Python +def main(): + n, m = map(int, input().strip().split()) + edges = [] + for _ in range(m): + src, dest, weight = map(int, input().strip().split()) + edges.append([src, dest, weight]) + + minDist = [float("inf")] * (n + 1) + minDist[1] = 0 # 起点处距离为0 + + for i in range(1, n): + updated = False + for src, dest, weight in edges: + if minDist[src] != float("inf") and minDist[src] + weight < minDist[dest]: + minDist[dest] = minDist[src] + weight + updated = True + if not updated: # 若边不再更新,即停止回圈 + break + + if minDist[-1] == float("inf"): # 返还终点权重 + return "unconnected" + return minDist[-1] + +if __name__ == "__main__": + print(main()) +``` + +### Go + +### Rust + +### JavaScript + +```js +async function main() { + // 輸入 + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const [n, m] = (await readline()).split(" ").map(Number) + const edges = [] + for (let i = 0 ; i < m ; i++) { + edges.push((await readline()).split(" ").map(Number)) + } + const minDist = Array.from({length: n + 1}, () => Number.MAX_VALUE) + // 起始點 + minDist[1] = 0 + + for (let i = 1 ; i < n ; i++) { + let update = false + for (const [src, desc, w] of edges) { + if (minDist[src] !== Number.MAX_VALUE && minDist[src] + w < minDist[desc]) { + minDist[desc] = minDist[src] + w + update = true + } + } + if (!update) { + break; + } + } + + // 輸出 + if (minDist[n] === Number.MAX_VALUE) { + console.log('unconnected') + } else { + console.log(minDist[n]) + } +} + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0095.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223II.md" "b/problems/kamacoder/0095.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223II.md" new file mode 100644 index 0000000000..957b8a80bc --- /dev/null +++ "b/problems/kamacoder/0095.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223II.md" @@ -0,0 +1,459 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# bellman_ford之判断负权回路 + +[卡码网:95. 城市间货物运输 II](https://kamacoder.com/problempage.php?pid=1153) + +【题目描述】 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。权值为正表示扣除了政府补贴后运输货物仍需支付的费用; + +权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + +然而,在评估从城市 1 到城市 n 的所有可能路径中综合政府补贴后的最低运输成本时,存在一种情况:**图中可能出现负权回路**。 + +负权回路是指一系列道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +为了避免货物运输商采用负权回路这种情况无限的赚取政府补贴,算法还需检测这种特殊情况。 + +请找出从城市 1 到城市 n 的所有可能路径中,综合政府补贴后的最低运输成本。同时能够检测并适当处理负权回路的存在。 + +城市 1 到城市 n 之间可能会出现没有路径的情况 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。 + +【输出描述】 + +如果没有发现负权回路,则输出一个整数,表示从城市 1 到城市 n 的最低运输成本(包括政府补贴)。 + +如果该整数是负数,则表示实现了盈利。如果发现了负权回路的存在,则输出 "circle"。如果从城市 1 无法到达城市 n,则输出 "unconnected"。 + + +输入示例 + +``` +4 4 +1 2 -1 +2 3 1 +3 1 -1 +3 4 1 +``` + +输出示例 + +``` +circle +``` + +## 思路 + +本题是 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 延伸题目。 + +本题是要我们判断 负权回路,也就是图中出现环且环上的边总权值为负数。 + +如果在这样的图中求最短路的话, 就会在这个环里无限循环 (也是负数+负数 只会越来越小),无法求出最短路径。 + +所以对于 在有负权值的图中求最短路,都需要先看看这个图里有没有负权回路。 + +接下来我们来看 如何使用 bellman_ford 算法来判断 负权回路。 + +在 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 中 我们讲了 bellman_ford 算法的核心就是一句话:对 所有边 进行 n-1 次松弛。 同时文中的 【拓展】部分, 我们也讲了 松弛n次以上 会怎么样? + +在没有负权回路的图中,松弛 n 次以上 ,结果不会有变化。 + +但本题有 负权回路,如果松弛 n 次,结果就会有变化了,因为 有负权回路 就是可以无限最短路径(一直绕圈,就可以一直得到无限小的最短距离)。 + +那么每松弛一次,都会更新最短路径,所以结果会一直有变化。 + +(如果对于 bellman_ford 不了解的录友,建议详细看这里:[kama94.城市间货物运输I](./0094.城市间货物运输I.md)) + +以上为理论分析,接下来我们再画图举例。 + +我们拿题目中示例来画一个图: + +![](https://file1.kamacoder.com/i/algo/20240705161426.png) + +图中 节点1 到 节点4 的最短路径是多少(题目中的最低运输成本) (注意边可以为负数的) + +节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本为 -1 + 1 + 1 = 1 + +而图中有负权回路: + +![](https://file1.kamacoder.com/i/algo/20240402103712.png) + +那么我们在负权回路中多绕一圈,我们的最短路径 是不是就更小了 (也就是更低的运输成本) + +节点1 -> 节点2 -> 节点3 -> 节点1 -> 节点2 -> 节点3 -> 节点4,这样的路径总成本 (-1) + 1 + (-1) + (-1) + 1 + (-1) + 1 = -1 + +如果在负权回路多绕两圈,三圈,无穷圈,那么我们的总成本就会无限小, 如果要求最小成本的话,你会发现本题就无解了。 + +在 bellman_ford 算法中,松弛 n-1 次所有的边 就可以求得 起点到任何节点的最短路径,松弛 n 次以上,minDist数组(记录起到到其他节点的最短距离)中的结果也不会有改变 (如果对 bellman_ford 算法 不了解,也不知道 minDist 是什么,建议详看上篇讲解[kama94.城市间货物运输I](./0094.城市间货物运输I.md)) + +而本题有负权回路的情况下,一直都会有更短的最短路,所以 松弛 第n次,minDist数组 也会发生改变。 + +那么解决本题的 核心思路,就是在 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 的基础上,再多松弛一次,看minDist数组 是否发生变化。 + +代码和 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 基本是一样的,如下:(关键地方已注释) + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + bool flag = false; + for (int i = 1; i <= n; i++) { // 这里我们松弛n次,最后一次判断负权回路 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + if (i < n) { + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } else { // 多加一次松弛判断负权回路 + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) flag = true; + + } + } + + } + + if (flag) cout << "circle" << endl; + else if (minDist[end] == INT_MAX) { + cout << "unconnected" << endl; + } else { + cout << minDist[end] << endl; + } +} +``` + +* 时间复杂度: O(N * E) , N为节点数量,E为图中边的数量 +* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间 + +## 拓展 + +本题可不可 使用 队列优化版的bellman_ford(SPFA)呢? + +上面的解法中,我们对所有边松弛了n-1次后,在松弛一次,如果出现minDist出现变化就判断有负权回路。 + +如果使用 SPFA 那么节点都是进队列的,那么节点进入队列几次后 足够判断该图是否有负权回路呢? + +在 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA) 中,我们讲过 在极端情况下,即:所有节点都与其他节点相连,每个节点的入度为 n-1 (n为节点数量),所以每个节点最多加入 n-1 次队列。 + +那么如果节点加入队列的次数 超过了 n-1次 ,那么该图就一定有负权回路。 + +所以本题也是可以使用 SPFA 来做的。 代码如下: + +```CPP +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); // 邻接表 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start = 1; // 起点 + int end = n; // 终点 + + vector minDist(n + 1 , INT_MAX); + minDist[start] = 0; + + queue que; + que.push(start); // 队列里放入起点 + + vector count(n+1, 0); // 记录节点加入队列几次 + count[start]++; + + bool flag = false; + while (!que.empty()) { + + int node = que.front(); que.pop(); + + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int value = edge.val; + if (minDist[to] > minDist[from] + value) { // 开始松弛 + minDist[to] = minDist[from] + value; + que.push(to); + count[to]++; + if (count[to] == n) {// 如果加入队列次数超过 n-1次 就说明该图与负权回路 + flag = true; + while (!que.empty()) que.pop(); + break; + } + } + } + } + + if (flag) cout << "circle" << endl; + else if (minDist[end] == INT_MAX) { + cout << "unconnected" << endl; + } else { + cout << minDist[end] << endl; + } + +} + +``` + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + // 基于Bellman_ford-SPFA方法 + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + List> graph = new ArrayList<>(); + + for (int i = 0; i <= n; i++) { + graph.add(new ArrayList<>()); + } + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + graph.get(from).add(new Edge(from, to, val)); + } + + // Declare the minDist array to record the minimum distance form current node to the original node + int[] minDist = new int[n + 1]; + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[1] = 0; + + // Declare a queue to store the updated nodes instead of traversing all nodes each loop for more efficiency + Queue queue = new LinkedList<>(); + queue.offer(1); + + // Declare an array to record the times each node has been offered in the queue + int[] count = new int[n + 1]; + count[1]++; + + // Declare a boolean array to record if the current node is in the queue to optimise the processing + boolean[] isInQueue = new boolean[n + 1]; + + // Declare a boolean value to check if there is a negative weight loop inside the graph + boolean flag = false; + + while (!queue.isEmpty()) { + int curNode = queue.poll(); + isInQueue[curNode] = false; // Represents the current node is not in the queue after being polled + for (Edge edge : graph.get(curNode)) { + if (minDist[edge.to] > minDist[edge.from] + edge.val) { // Start relaxing the edge + minDist[edge.to] = minDist[edge.from] + edge.val; + if (!isInQueue[edge.to]) { // Don't add the node if it's already in the queue + queue.offer(edge.to); + count[edge.to]++; + isInQueue[edge.to] = true; + } + + if (count[edge.to] == n) { // If some node has been offered in the queue more than n-1 times + flag = true; + while (!queue.isEmpty()) queue.poll(); + break; + } + } + } + } + + if (flag) { + System.out.println("circle"); + } else if (minDist[n] == Integer.MAX_VALUE) { + System.out.println("unconnected"); + } else { + System.out.println(minDist[n]); + } + } +} + +``` + +### Python + +Bellman-Ford方法求解含有负回路的最短路问题 + +```python +import sys + +def main(): + input = sys.stdin.read + data = input().split() + index = 0 + + n = int(data[index]) + index += 1 + m = int(data[index]) + index += 1 + + grid = [] + for i in range(m): + p1 = int(data[index]) + index += 1 + p2 = int(data[index]) + index += 1 + val = int(data[index]) + index += 1 + # p1 指向 p2,权值为 val + grid.append([p1, p2, val]) + + start = 1 # 起点 + end = n # 终点 + + minDist = [float('inf')] * (n + 1) + minDist[start] = 0 + flag = False + + for i in range(1, n + 1): # 这里我们松弛n次,最后一次判断负权回路 + for side in grid: + from_node = side[0] + to = side[1] + price = side[2] + if i < n: + if minDist[from_node] != float('inf') and minDist[to] > minDist[from_node] + price: + minDist[to] = minDist[from_node] + price + else: # 多加一次松弛判断负权回路 + if minDist[from_node] != float('inf') and minDist[to] > minDist[from_node] + price: + flag = True + + if flag: + print("circle") + elif minDist[end] == float('inf'): + print("unconnected") + else: + print(minDist[end]) + +if __name__ == "__main__": + main() + +``` + +SPFA方法求解含有负回路的最短路问题 + +```python +from collections import deque +from math import inf + +def main(): + n, m = [int(i) for i in input().split()] + graph = [[] for _ in range(n+1)] + min_dist = [inf for _ in range(n+1)] + count = [0 for _ in range(n+1)] # 记录节点加入队列的次数 + for _ in range(m): + s, t, v = [int(i) for i in input().split()] + graph[s].append([t, v]) + + min_dist[1] = 0 # 初始化 + count[1] = 1 + d = deque([1]) + flag = False + + while d: # 主循环 + cur_node = d.popleft() + for next_node, val in graph[cur_node]: + if min_dist[next_node] > min_dist[cur_node] + val: + min_dist[next_node] = min_dist[cur_node] + val + count[next_node] += 1 + if next_node not in d: + d.append(next_node) + if count[next_node] == n: # 如果某个点松弛了n次,说明有负回路 + flag = True + if flag: + break + + if flag: + print("circle") + else: + if min_dist[-1] == inf: + print("unconnected") + else: + print(min_dist[-1]) + + +if __name__ == "__main__": + main() +``` + +### Go + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0096.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223III.md" "b/problems/kamacoder/0096.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223III.md" new file mode 100644 index 0000000000..0c00ccb668 --- /dev/null +++ "b/problems/kamacoder/0096.\345\237\216\345\270\202\351\227\264\350\264\247\347\211\251\350\277\220\350\276\223III.md" @@ -0,0 +1,925 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# bellman_ford之单源有限最短路 + +[卡码网:96. 城市间货物运输 III](https://kamacoder.com/problempage.php?pid=1154) + +【题目描述】 + +某国为促进城市间经济交流,决定对货物运输提供补贴。共有 n 个编号为 1 到 n 的城市,通过道路网络连接,网络中的道路仅允许从某个城市单向通行到另一个城市,不能反向通行。 + +网络中的道路都有各自的运输成本和政府补贴,道路的权值计算方式为:运输成本 - 政府补贴。 + +权值为正表示扣除了政府补贴后运输货物仍需支付的费用; + +权值为负则表示政府的补贴超过了支出的运输成本,实际表现为运输过程中还能赚取一定的收益。 + +请计算在最多经过 k 个城市的条件下,从城市 src 到城市 dst 的最低运输成本。 + +【输入描述】 + +第一行包含两个正整数,第一个正整数 n 表示该国一共有 n 个城市,第二个整数 m 表示这些城市中共有 m 条道路。 + +接下来为 m 行,每行包括三个整数,s、t 和 v,表示 s 号城市运输货物到达 t 号城市,道路权值为 v。 + +最后一行包含三个正整数,src、dst、和 k,src 和 dst 为城市编号,从 src 到 dst 经过的城市数量限制。 + +【输出描述】 + +输出一个整数,表示从城市 src 到城市 dst 的最低运输成本,如果无法在给定经过城市数量限制下找到从 src 到 dst 的路径,则输出 "unreachable",表示不存在符合条件的运输方案。 + +输入示例: + +``` +6 7 +1 2 1 +2 4 -3 +2 5 2 +1 3 5 +3 5 1 +4 6 4 +5 6 -2 +2 6 1 +``` + +输出示例: + +``` +0 +``` + +## 思路 + +本题为单源有限最短路问题,同样是 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 延伸题目。 + +注意题目中描述是 **最多经过 k 个城市的条件下,而不是一定经过k个城市,也可以经过的城市数量比k小,但要最短的路径**。 + +在 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 中我们讲了:**对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**。 + +节点数量为n,起点到终点,最多是 n-1 条边相连。 那么对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。 + +(如果对以上讲解看不懂,建议详看 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) ) + +本题是最多经过 k 个城市, 那么是 k + 1条边相连的节点。 这里可能有录友想不懂为什么是k + 1,来看这个图: + +![](https://file1.kamacoder.com/i/algo/20240402115614.png) + +图中,节点1 最多已经经过2个节点 到达节点4,那么中间是有多少条边呢,是 3 条边对吧。 + +所以本题就是求:起点最多经过k + 1 条边到达终点的最短距离。 + +对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离,那么对所有边松弛 k + 1次,就是求 起点到达 与起点k + 1条边相连的节点的 最短距离。 + +**注意**: 本题是 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 的拓展题,如果对 bellman_ford 没有深入了解,强烈建议先看 [kama94.城市间货物运输I](./0094.城市间货物运输I.md) 再做本题。 + +理解以上内容,其实本题代码就很容易了,bellman_ford 标准写法是松弛 n-1 次,本题就松弛 k + 1次就好。 + +此时我们可以写出如下代码: + +```CPP +// 版本一 +#include +#include +#include +#include +using namespace std; + +int main() { + int src, dst,k ,p1, p2, val ,m , n; + + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + } + + cin >> src >> dst >> k; + + vector minDist(n + 1 , INT_MAX); + minDist[src] = 0; + for (int i = 1; i <= k + 1; i++) { // 对所有边松弛 k + 1次 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + + } + + if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点 + else cout << minDist[dst] << endl; // 到达终点最短路径 + +} + +``` + +以上代码 标准 bellman_ford 写法,松弛 k + 1次,看上去没什么问题。 + +但大家提交后,居然没通过! + +这是为什么呢? + +接下来我们拿这组数据来举例: + +``` +4 4 +1 2 -1 +2 3 1 +3 1 -1 +3 4 1 +1 4 3 +``` + +(**注意上面的示例是有负权回路的,只有带负权回路的图才能说明问题**) + +> 负权回路是指一条道路的总权值为负,这样的回路使得通过反复经过回路中的道路,理论上可以无限地减少总成本或无限地增加总收益。 + +正常来说,这组数据输出应该是 1,但以上代码输出的是 -2。 + + +在讲解原因的时候,强烈建议大家,先把 minDist数组打印出来,看看minDist数组是不是按照自己的想法变化的,这样更容易理解我接下来的讲解内容。 (**一定要动手,实践出真实,脑洞模拟不靠谱**) + +打印的代码可以是这样: + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int src, dst,k ,p1, p2, val ,m , n; + + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid.push_back({p1, p2, val}); + } + + cin >> src >> dst >> k; + + vector minDist(n + 1 , INT_MAX); + minDist[src] = 0; + for (int i = 1; i <= k + 1; i++) { // 对所有边松弛 k + 1次 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + if (minDist[from] != INT_MAX && minDist[to] > minDist[from] + price) minDist[to] = minDist[from] + price; + } + // 打印 minDist 数组 + for (int j = 1; j <= n; j++) cout << minDist[j] << " "; + cout << endl; + + } + + if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点 + else cout << minDist[dst] << endl; // 到达终点最短路径 + +} + +``` + +接下来,我按照上面的示例带大家 画图举例 对所有边松弛一次 的效果图。 + +起点为节点1, 起点到起点的距离为0,所以 minDist[1] 初始化为0 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240409111940.png) + +其他节点对应的minDist初始化为max,因为我们要求最小距离,那么还没有计算过的节点 默认是一个最大数,这样才能更新最小距离。 + +当我们开始对所有边开始第一次松弛: + +边:节点1 -> 节点2,权值为-1 ,minDist[2] > minDist[1] + (-1),更新 minDist[2] = minDist[1] + (-1) = 0 - 1 = -1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240409111914.png) + + +边:节点2 -> 节点3,权值为1 ,minDist[3] > minDist[2] + 1 ,更新 minDist[3] = minDist[2] + 1 = -1 + 1 = 0 ,如图: +![](https://file1.kamacoder.com/i/algo/20240409111903.png) + + +边:节点3 -> 节点1,权值为-1 ,minDist[1] > minDist[3] + (-1),更新 minDist[1] = 0 + (-1) = -1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240409111849.png) + + +边:节点3 -> 节点4,权值为1 ,minDist[4] > minDist[3] + 1,更新 minDist[4] = 0 + 1 = 1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20241018192042.png) + + +以上是对所有边进行的第一次松弛,最后 minDist数组为 :-1 -1 0 1 ,(从下标1算起) + +后面几次松弛我就不挨个画图了,过程大同小异,我直接给出minDist数组的变化: + +所有边进行的第二次松弛,minDist数组为 : -2 -2 -1 0 +所有边进行的第三次松弛,minDist数组为 : -3 -3 -2 -1 +所有边进行的第四次松弛,minDist数组为 : -4 -4 -3 -2 (本示例中k为3,所以松弛4次) + +最后计算的结果minDist[4] = -2,即 起点到 节点4,最多经过 3 个节点的最短距离是 -2,但 正确的结果应该是 1,即路径:节点1 -> 节点2 -> 节点3 -> 节点4。 + +理论上来说,**对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离**。 + +对所有边松弛两次,相当于计算 起点到达 与起点两条边相连的节点的最短距离。 + +对所有边松弛三次,以此类推。 + +但在对所有边松弛第一次的过程中,大家会发现,不仅仅 与起点一条边相连的节点更新了,所有节点都更新了。 + +而且对所有边的后面几次松弛,同样是更新了所有的节点,说明 至多经过k 个节点 这个限制 根本没有限制住,每个节点的数值都被更新了。 + +这是为什么? + +在上面画图距离中,对所有边进行第一次松弛,在计算 边(节点2 -> 节点3) 的时候,更新了 节点3。 + +![](https://file1.kamacoder.com/i/algo/20240409111903.png) + +理论上来说节点3 应该在对所有边第二次松弛的时候才更新。 这因为当时是基于已经计算好的 节点2(minDist[2])来做计算了。 + +minDist[2]在计算边:(节点1 -> 节点2)的时候刚刚被赋值为 -1。 + +这样就造成了一个情况,即:计算minDist数组的时候,基于了本次松弛的 minDist数值,而不是上一次 松弛时候minDist的数值。 +所以在每次计算 minDist 时候,要基于 对所有边上一次松弛的 minDist 数值才行,所以我们要记录上一次松弛的minDist。 + +代码修改如下: (关键地方已经注释) + +```CPP +// 版本二 +#include +#include +#include +#include +using namespace std; + +int main() { + int src, dst,k ,p1, p2, val ,m , n; + + cin >> n >> m; + + vector> grid; + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid.push_back({p1, p2, val}); + } + + cin >> src >> dst >> k; + + vector minDist(n + 1 , INT_MAX); + minDist[src] = 0; + vector minDist_copy(n + 1); // 用来记录上一次遍历的结果 + for (int i = 1; i <= k + 1; i++) { + minDist_copy = minDist; // 获取上一次计算的结果 + for (vector &side : grid) { + int from = side[0]; + int to = side[1]; + int price = side[2]; + // 注意使用 minDist_copy 来计算 minDist + if (minDist_copy[from] != INT_MAX && minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + } + } + } + if (minDist[dst] == INT_MAX) cout << "unreachable" << endl; // 不能到达终点 + else cout << minDist[dst] << endl; // 到达终点最短路径 + +} + +``` + +* 时间复杂度: O(K * E) , K为至多经过K个节点,E为图中边的数量 +* 空间复杂度: O(N) ,即 minDist 数组所开辟的空间 + +## 拓展一(边的顺序的影响) + +其实边的顺序会影响我们每一次拓展的结果。 + +我来给大家举个例子。 + +我上面讲解中,给出的示例是这样的: +``` +4 4 +1 2 -1 +2 3 1 +3 1 -1 +3 4 1 +1 4 3 +``` + +我将示例中边的顺序改一下,给成: + +``` +4 4 +3 1 -1 +3 4 1 +2 3 1 +1 2 -1 +1 4 3 +``` + +所构成是图是一样的,都是如下的这个图,但给出的边的顺序是不一样的。 + +![](https://file1.kamacoder.com/i/algo/20240410154340.png) + +再用版本一的代码是运行一下,发现结果输出是 1, 是对的。 + +![](https://file1.kamacoder.com/i/algo/20240410154940.png) + +分明刚刚输出的结果是 -2,是错误的,怎么 一样的图,这次输出的结果就对了呢? + +其实这是和示例中给出的边的顺序是有关的, + +我们按照修改后的示例再来模拟 对所有边的第一次拓展情况。 + +初始化: + +![](https://file1.kamacoder.com/i/algo/20240410155545.png) + +边:节点3 -> 节点1,权值为-1 ,节点3还没有被计算过,节点1 不更新。 + +边:节点3 -> 节点4,权值为1 ,节点3还没有被计算过,节点4 不更新。 + +边:节点2 -> 节点3,权值为 1 ,节点2还没有被计算过,节点3 不更新。 + +边:节点1 -> 节点2,权值为 -1 ,minDist[2] > minDist[1] + (-1),更新 minDist[2] = 0 + (-1) = -1 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240410160046.png) + +以上是对所有边 松弛一次的状态。 + +可以发现 同样的图,边的顺序不一样,使用版本一的代码 每次松弛更新的节点也是不一样的。 + +而边的顺序是随机的,是题目给我们的,所以本题我们才需要 记录上一次松弛的minDist,来保障 每一次对所有边松弛的结果。 + + +## 拓展二(本题本质) + +那么前面讲解过的 [94.城市间货物运输I](./0094.城市间货物运输I.md) 和 [95.城市间货物运输II](./0095.城市间货物运输II.md) 也是bellman_ford经典算法,也没使用 minDist_copy,怎么就没问题呢? + +> 如果没看过我上面这两篇讲解的话,建议详细学习上面两篇,再看我下面讲的区别,否则容易看不懂。 + +[94.城市间货物运输I](./0094.城市间货物运输I.md), 是没有 负权回路的,那么 多松弛多少次,对结果都没有影响。 + +求 节点1 到 节点n 的最短路径,松弛n-1 次就够了,松弛 大于 n-1次,结果也不会变。 + +那么在对所有边进行第一次松弛的时候,如果基于 本次计算的 minDist 来计算 minDist (相当于多做松弛了),也是对最终结果没影响。 + +[95.城市间货物运输II](./0095.城市间货物运输II.md) 是判断是否有 负权回路,一旦有负权回路, 对所有边松弛 n-1 次以后,在做松弛 minDist 数值一定会变,根据这一点来判断是否有负权回路。 + +所以,[95.城市间货物运输II](./0095.城市间货物运输II.md) 只需要判断minDist数值变化了就行,而 minDist 的数值对不对,并不是我们关心的。 + +那么本题 为什么计算minDist 一定要基于上次 的 minDist 数值。 + +其关键在于本题的两个因素: + +* 本题可以有负权回路,说明只要多做松弛,结果是会变的。 +* 本题要求最多经过k个节点,对松弛次数是有限制的。 + +## 拓展三(SPFA) + +本题也可以用 SPFA来做,关于 SPFA ,已经在这里 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有详细讲解。 + +使用SPFA算法解决本题的时候,关键在于 如何控制松弛k次。 + +其实实现不难,但有点技巧,可以用一个变量 que_size 记录每一轮松弛入队列的所有节点数量。 + +下一轮松弛的时候,就把队列里 que_size 个节点都弹出来,就是上一轮松弛入队列的节点。 + +代码如下(详细注释) + +```CPP +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); // 邻接表 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start, end, k; + cin >> start >> end >> k; + + k++; + + vector minDist(n + 1 , INT_MAX); + vector minDist_copy(n + 1); // 用来记录每一次遍历的结果 + + minDist[start] = 0; + + queue que; + que.push(start); // 队列里放入起点 + + int que_size; + while (k-- && !que.empty()) { + + minDist_copy = minDist; // 获取上一次计算的结果 + que_size = que.size(); // 记录上次入队列的节点个数 + while (que_size--) { // 上一轮松弛入队列的节点,这次对应的边都要做松弛 + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + que.push(to); + } + } + + } + } + if (minDist[end] == INT_MAX) cout << "unreachable" << endl; + else cout << minDist[end] << endl; + +} + +``` + +时间复杂度: O(K * H) H 为不确定数,取决于 图的稠密度,但H 一定是小于等于 E 的 + +关于 SPFA的是时间复杂度分析,我在[0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有详细讲解 + +但大家会发现,以上代码大家提交后,怎么耗时这么多? + +![](https://file1.kamacoder.com/i/algo/20240418113308.png) + +理论上,SPFA的时间复杂度不是要比 bellman_ford 更优吗? + +怎么耗时多了这么多呢? + +以上代码有一个可以改进的点,每一轮松弛中,重复节点可以不用入队列。 + +因为重复节点入队列,下次从队列里取节点的时候,该节点要取很多次,而且都是重复计算。 + +所以代码可以优化成这样: + +```CPP +#include +#include +#include +#include +#include +using namespace std; + +struct Edge { //邻接表 + int to; // 链接的节点 + int val; // 边的权重 + + Edge(int t, int w): to(t), val(w) {} // 构造函数 +}; + + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1); // 邻接表 + + // 将所有边保存起来 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + // p1 指向 p2,权值为 val + grid[p1].push_back(Edge(p2, val)); + } + int start, end, k; + cin >> start >> end >> k; + + k++; + + vector minDist(n + 1 , INT_MAX); + vector minDist_copy(n + 1); // 用来记录每一次遍历的结果 + + minDist[start] = 0; + + queue que; + que.push(start); // 队列里放入起点 + + int que_size; + while (k-- && !que.empty()) { + + vector visited(n + 1, false); // 每一轮松弛中,控制节点不用重复入队列 + minDist_copy = minDist; + que_size = que.size(); + while (que_size--) { + int node = que.front(); que.pop(); + for (Edge edge : grid[node]) { + int from = node; + int to = edge.to; + int price = edge.val; + if (minDist[to] > minDist_copy[from] + price) { + minDist[to] = minDist_copy[from] + price; + if(visited[to]) continue; // 不用重复放入队列,但需要重复松弛,所以放在这里位置 + visited[to] = true; + que.push(to); + } + } + + } + } + if (minDist[end] == INT_MAX) cout << "unreachable" << endl; + else cout << minDist[end] << endl; +} +``` + +以上代码提交后,耗时情况: + +![](https://file1.kamacoder.com/i/algo/20240418113952.png) + +大家发现 依然远比 bellman_ford 的代码版本 耗时高。 + +这又是为什么呢? + +对于后台数据,我特别制作的一个稠密大图,该图有250个节点和10000条边, 在这种情况下, SPFA 的时间复杂度 是接近与 bellman_ford的。 + +但因为 SPFA 节点的进出队列操作,耗时很大,所以相同的时间复杂度的情况下,SPFA 实际上更耗时了。 + +这一点我在 [0094.城市间货物运输I-SPFA](./0094.城市间货物运输I-SPFA.md) 有分析,感兴趣的录友再回头去看看。 + +## 拓展四(能否用dijkstra) + +本题能否使用 dijkstra 算法呢? + +dijkstra 是贪心的思路 每一次搜索都只会找距离源点最近的非访问过的节点。 + +如果限制最多访问k个节点,那么 dijkstra 未必能在有限次就能到达终点,即使在经过k个节点确实可以到达终点的情况下。 + +这么说大家会感觉有点抽象,我用 [dijkstra朴素版精讲](./0047.参会dijkstra朴素.md) 里的示例在举例说明: (如果没看过我讲的[dijkstra朴素版精讲](./0047.参会dijkstra朴素.md),建议去仔细看一下,否则下面讲解容易看不懂) + + +在以下这个图中,求节点1 到 节点7 最多经过2个节点 的最短路是多少呢? + +![](https://file1.kamacoder.com/i/algo/20240508112249.png) + +最短路显然是: + +![](https://file1.kamacoder.com/i/algo/20240508112416.png) + +最多经过2个节点,也就是3条边相连的路线:节点1 -> 节点2 -> 节点6-> 节点7 + +如果是 dijkstra 求解的话,求解过程是这样的: (下面是dijkstra的模拟过程,我精简了很多,如果看不懂,一定要先看[dijkstra朴素版精讲](./0047.参会dijkstra朴素.md)) + +初始化如图所示: + +![](https://file1.kamacoder.com/i/algo/20240130115306.png) + +找距离源点最近且没有被访问过的节点,先找节点1 + +![](https://file1.kamacoder.com/i/algo/20240130115421.png) + + +距离源点最近且没有被访问过的节点,找节点2: + +![](https://file1.kamacoder.com/i/algo/20240130121240.png) + +距离源点最近且没有被访问过的节点,找到节点3: + +![](https://file1.kamacoder.com/i/algo/20240130120434.png) + +距离源点最近且没有被访问过的节点,找到节点4: + +![](https://file1.kamacoder.com/i/algo/20240201105335.png) + +此时最多经过2个节点的搜索就完毕了,但结果中minDist[7] (即节点7的结果)并没有被更。 + +那么 dijkstra 会告诉我们 节点1 到 节点7 最多经过2个节点的情况下是不可到达的。 + +通过以上模拟过程,大家应该能感受到 dijkstra 贪心的过程,正是因为 贪心,所以 dijkstra 找不到 节点1 -> 节点2 -> 节点6-> 节点7 这条路径。 + +## 总结 + +本题是单源有限最短路问题,也是 bellman_ford的一个拓展问题,如果理解bellman_ford 其实思路比较容易理解,但有很多细节。 + +例如 为什么要用 minDist_copy 来记录上一轮 松弛的结果。 这也是本篇我为什么花了这么大篇幅讲解的关键所在。 + +接下来,还给大家做了四个拓展: + +* 边的顺序的影响 +* 本题的本质 +* SPFA的解法 +* 能否用dijkstra + +学透了以上四个拓展,相信大家会对bellman_ford有更深入的理解。 + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + // 基于Bellman_for一般解法解决单源最短路径问题 + // Define an inner class Edge + static class Edge { + int from; + int to; + int val; + public Edge(int from, int to, int val) { + this.from = from; + this.to = to; + this.val = val; + } + } + + public static void main(String[] args) { + // Input processing + Scanner sc = new Scanner(System.in); + int n = sc.nextInt(); + int m = sc.nextInt(); + + List graph = new ArrayList<>(); + + for (int i = 0; i < m; i++) { + int from = sc.nextInt(); + int to = sc.nextInt(); + int val = sc.nextInt(); + graph.add(new Edge(from, to, val)); + } + + int src = sc.nextInt(); + int dst = sc.nextInt(); + int k = sc.nextInt(); + + int[] minDist = new int[n + 1]; + int[] minDistCopy; + + Arrays.fill(minDist, Integer.MAX_VALUE); + minDist[src] = 0; + + for (int i = 0; i < k + 1; i++) { // Relax all edges k + 1 times + minDistCopy = Arrays.copyOf(minDist, n + 1); + for (Edge edge : graph) { + int from = edge.from; + int to = edge.to; + int val = edge.val; + // Use minDistCopy to calculate minDist + if (minDistCopy[from] != Integer.MAX_VALUE && minDist[to] > minDistCopy[from] + val) { + minDist[to] = minDistCopy[from] + val; + } + } + } + + // Output printing + if (minDist[dst] == Integer.MAX_VALUE) { + System.out.println("unreachable"); + } else { + System.out.println(minDist[dst]); + } + } +} + +``` + +```java +class Edge { + public int u; // 边的端点1 + public int v; // 边的端点2 + public int val; // 边的权值 + + public Edge() { + } + + public Edge(int u, int v) { + this.u = u; + this.v = v; + this.val = 0; + } + + public Edge(int u, int v, int val) { + this.u = u; + this.v = v; + this.val = val; + } +} + +/** + * SPFA算法(版本3):处理含【负权回路】的有向图的最短路径问题 + * bellman_ford(版本3) 的队列优化算法版本 + * 限定起点、终点、至多途径k个节点 + */ +public class SPFAForSSSP { + + /** + * SPFA算法 + * + * @param n 节点个数[1,n] + * @param graph 邻接表 + * @param startIdx 开始节点(源点) + */ + public static int[] spfa(int n, List> graph, int startIdx, int k) { + // 定义最大范围 + int maxVal = Integer.MAX_VALUE; + // minDist[i] 源点到节点i的最短距离 + int[] minDist = new int[n + 1]; // 有效节点编号范围:[1,n] + Arrays.fill(minDist, maxVal); // 初始化为maxVal + minDist[startIdx] = 0; // 设置源点到源点的最短路径为0 + + // 定义queue记录每一次松弛更新的节点 + Queue queue = new LinkedList<>(); + queue.offer(startIdx); // 初始化:源点开始(queue和minDist的更新是同步的) + + + // SPFA算法核心:只对上一次松弛的时候更新过的节点关联的边进行松弛操作 + while (k + 1 > 0 && !queue.isEmpty()) { // 限定松弛 k+1 次 + int curSize = queue.size(); // 记录当前队列节点个数(上一次松弛更新的节点个数,用作分层统计) + while (curSize-- > 0) { //分层控制,限定本次松弛只针对上一次松弛更新的节点,不对新增的节点做处理 + // 记录当前minDist状态,作为本次松弛的基础 + int[] minDist_copy = Arrays.copyOfRange(minDist, 0, minDist.length); + + // 取出节点 + int cur = queue.poll(); + // 获取cur节点关联的边,进行松弛操作 + List relateEdges = graph.get(cur); + for (Edge edge : relateEdges) { + int u = edge.u; // 与`cur`对照 + int v = edge.v; + int weight = edge.val; + if (minDist_copy[u] + weight < minDist[v]) { + minDist[v] = minDist_copy[u] + weight; // 更新 + // 队列同步更新(此处有一个针对队列的优化:就是如果已经存在于队列的元素不需要重复添加) + if (!queue.contains(v)) { + queue.offer(v); // 与minDist[i]同步更新,将本次更新的节点加入队列,用做下一个松弛的参考基础 + } + } + } + } + // 当次松弛结束,次数-1 + k--; + } + + // 返回minDist + return minDist; + } + + public static void main(String[] args) { + // 输入控制 + Scanner sc = new Scanner(System.in); + System.out.println("1.输入N个节点、M条边(u v weight)"); + int n = sc.nextInt(); + int m = sc.nextInt(); + + System.out.println("2.输入M条边"); + List> graph = new ArrayList<>(); // 构建邻接表 + for (int i = 0; i <= n; i++) { + graph.add(new ArrayList<>()); + } + while (m-- > 0) { + int u = sc.nextInt(); + int v = sc.nextInt(); + int weight = sc.nextInt(); + graph.get(u).add(new Edge(u, v, weight)); + } + + System.out.println("3.输入src dst k(起点、终点、至多途径k个点)"); + int src = sc.nextInt(); + int dst = sc.nextInt(); + int k = sc.nextInt(); + + // 调用算法 + int[] minDist = SPFAForSSSP.spfa(n, graph, src, k); + // 校验起点->终点 + if (minDist[dst] == Integer.MAX_VALUE) { + System.out.println("unreachable"); + } else { + System.out.println("最短路径:" + minDist[n]); + } + } +} +``` + + + +### Python + +Bellman-Ford方法求解单源有限最短路 + +```python +def main(): + # 輸入 + n, m = map(int, input().split()) + edges = list() + for _ in range(m): + edges.append(list(map(int, input().split() ))) + + start, end, k = map(int, input().split()) + min_dist = [float('inf') for _ in range(n + 1)] + min_dist[start] = 0 + + # 只能經過k個城市,所以從起始點到中間有(k + 1)個邊連接 + # 需要鬆弛(k + 1)次 + + for _ in range(k + 1): + update = False + min_dist_copy = min_dist.copy() + for src, desc, w in edges: + if (min_dist_copy[src] != float('inf') and + min_dist_copy[src] + w < min_dist[desc]): + min_dist[desc] = min_dist_copy[src] + w + update = True + if not update: + break + # 輸出 + if min_dist[end] == float('inf'): + print('unreachable') + else: + print(min_dist[end]) + + + +if __name__ == "__main__": + main() +``` + +SPFA方法求解单源有限最短路 + +```python +from collections import deque +from math import inf + + +def main(): + n, m = [int(i) for i in input().split()] + graph = [[] for _ in range(n+1)] + for _ in range(m): + v1, v2, val = [int(i) for i in input().split()] + graph[v1].append([v2, val]) + src, dst, k = [int(i) for i in input().split()] + min_dist = [inf for _ in range(n+1)] + min_dist[src] = 0 # 初始化起点的距离 + que = deque([src]) + + while k != -1 and que: + visited = [False for _ in range(n+1)] # 用于保证每次松弛时一个节点最多加入队列一次 + que_size = len(que) + temp_dist = min_dist.copy() # 用于记录上一次遍历的结果 + for _ in range(que_size): + cur_node = que.popleft() + for next_node, val in graph[cur_node]: + if min_dist[next_node] > temp_dist[cur_node] + val: + min_dist[next_node] = temp_dist[cur_node] + val + if not visited[next_node]: + que.append(next_node) + visited[next_node] = True + k -= 1 + + if min_dist[dst] == inf: + print("unreachable") + else: + print(min_dist[dst]) + + +if __name__ == "__main__": + main() +``` + +### Go + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0097.\345\260\217\346\230\216\351\200\233\345\205\254\345\233\255.md" "b/problems/kamacoder/0097.\345\260\217\346\230\216\351\200\233\345\205\254\345\233\255.md" new file mode 100644 index 0000000000..53e66ee46f --- /dev/null +++ "b/problems/kamacoder/0097.\345\260\217\346\230\216\351\200\233\345\205\254\345\233\255.md" @@ -0,0 +1,577 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# Floyd 算法精讲 + +[卡码网:97. 小明逛公园](https://kamacoder.com/problempage.php?pid=1155) + +【题目描述】 + +小明喜欢去公园散步,公园内布置了许多的景点,相互之间通过小路连接,小明希望在观看景点的同时,能够节省体力,走最短的路径。 + + +给定一个公园景点图,图中有 N 个景点(编号为 1 到 N),以及 M 条双向道路连接着这些景点。每条道路上行走的距离都是已知的。 + + +小明有 Q 个观景计划,每个计划都有一个起点 start 和一个终点 end,表示他想从景点 start 前往景点 end。由于小明希望节省体力,他想知道每个观景计划中从起点到终点的最短路径长度。 请你帮助小明计算出每个观景计划的最短路径长度。 + +【输入描述】 + +第一行包含两个整数 N, M, 分别表示景点的数量和道路的数量。 + +接下来的 M 行,每行包含三个整数 u, v, w,表示景点 u 和景点 v 之间有一条长度为 w 的双向道路。 + +接下里的一行包含一个整数 Q,表示观景计划的数量。 + +接下来的 Q 行,每行包含两个整数 start, end,表示一个观景计划的起点和终点。 + +【输出描述】 + +对于每个观景计划,输出一行表示从起点到终点的最短路径长度。如果两个景点之间不存在路径,则输出 -1。 + +【输入示例】 + +7 3 +1 2 4 +2 5 6 +3 6 8 +2 +1 2 +2 3 + +【输出示例】 + +4 +-1 + +【提示信息】 + +从 1 到 2 的路径长度为 4,2 到 3 之间并没有道路。 + +1 <= N, M, Q <= 1000. + +## 思路 + +本题是经典的多源最短路问题。 + +在这之前我们讲解过,dijkstra朴素版、dijkstra堆优化、Bellman算法、Bellman队列优化(SPFA) 都是单源最短路,即只能有一个起点。 + +而本题是多源最短路,即 求多个起点到多个终点的多条最短路径。 + +通过本题,我们来系统讲解一个新的最短路算法-Floyd 算法。 + +**Floyd 算法对边的权值正负没有要求,都可以处理**。 + +Floyd算法核心思想是动态规划。 + +例如我们再求节点1 到 节点9 的最短距离,用二维数组来表示即:grid[1][9],如果最短距离是10 ,那就是 grid[1][9] = 10。 + +那 节点1 到 节点9 的最短距离 是不是可以由 节点1 到节点5的最短距离 + 节点5到节点9的最短距离组成呢? + +即 grid[1][9] = grid[1][5] + grid[5][9] + +节点1 到节点5的最短距离 是不是可以有 节点1 到 节点3的最短距离 + 节点3 到 节点5 的最短距离组成呢? + +即 grid[1][5] = grid[1][3] + grid[3][5] + +以此类推,节点1 到 节点3的最短距离 可以由更小的区间组成。 + +那么这样我们是不是就找到了,子问题推导求出整体最优方案的递归关系呢。 + +节点1 到 节点9 的最短距离 可以由 节点1 到节点5的最短距离 + 节点5到节点9的最短距离组成, 也可以有 节点1 到节点7的最短距离 + 节点7 到节点9的最短距离的距离组成。 + +那么选哪个呢? + +是不是 要选一个最小的,毕竟是求最短路。 + +此时我们已经接近明确递归公式了。 + +之前在讲解动态规划的时候,给出过动规五部曲: + +* 确定dp数组(dp table)以及下标的含义 +* 确定递推公式 +* dp数组如何初始化 +* 确定遍历顺序 +* 举例推导dp数组 + +那么接下来我们还是用这五部来给大家讲解 Floyd。 + +1、确定dp数组(dp table)以及下标的含义 + +这里我们用 grid数组来存图,那就把dp数组命名为 grid。 + +grid[i][j][k] = m,表示 **节点i 到 节点j 以[1...k] 集合中的一个节点为中间节点的最短距离为m**。 + + +可能有录友会想,凭什么就这么定义呢? + +节点i 到 节点j 的最短距离为m,这句话可以理解,但 以[1...k]集合为中间节点就理解不辽了。 + +节点i 到 节点j 的最短路径中 一定是经过很多节点,那么这个集合用[1...k] 来表示。 + +你可以反过来想,节点i 到 节点j 中间一定经过很多节点,那么你能用什么方式来表述中间这么多节点呢? + +所以 这里的k不能单独指某个节点,k 一定要表示一个集合,即[1...k] ,表示节点1 到 节点k 一共k个节点的集合。 + + +2、确定递推公式 + +在上面的分析中我们已经初步感受到了递推的关系。 + +我们分两种情况: + +1. 节点i 到 节点j 的最短路径经过节点k +2. 节点i 到 节点j 的最短路径不经过节点k + +对于第一种情况,`grid[i][j][k] = grid[i][k][k - 1] + grid[k][j][k - 1]` + +节点i 到 节点k 的最短距离 是不经过节点k,中间节点集合为[1...k-1],所以 表示为`grid[i][k][k - 1]` + +节点k 到 节点j 的最短距离 也是不经过节点k,中间节点集合为[1...k-1],所以表示为 `grid[k][j][k - 1]` + +第二种情况,`grid[i][j][k] = grid[i][j][k - 1]` + +如果节点i 到 节点j的最短距离 不经过节点k,那么 中间节点集合[1...k-1],表示为 `grid[i][j][k - 1]` + +因为我们是求最短路,对于这两种情况自然是取最小值。 + +即: `grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])` + + +3、dp数组如何初始化 + +grid[i][j][k] = m,表示 节点i 到 节点j 以[1...k] 集合为中间节点的最短距离为m。 + +刚开始初始化k 是不确定的。 + +例如题目中只是输入边(节点2 -> 节点6,权值为3),那么grid[2][6][k] = 3,k需要填什么呢? + +把k 填成1,那如何上来就知道 节点2 经过节点1 到达节点6的最短距离是多少 呢。 + +所以 只能 把k 赋值为 0,本题 节点0 是无意义的,节点是从1 到 n。 + +这样我们在下一轮计算的时候,就可以根据 grid[i][j][0] 来计算 grid[i][j][1],此时的 grid[i][j][1] 就是 节点i 经过节点1 到达 节点j 的最小距离了。 + + +grid数组是一个三维数组,那么我们初始化的数据在 i 与 j 构成的平层,如图: + +![](https://file1.kamacoder.com/i/algo/20240425104247.png) + +红色的 底部一层是我们初始化好的数据,注意:从三维角度去看初始化的数据很重要,下面我们在聊遍历顺序的时候还会再讲。 + +所以初始化代码: + +```CPP +vector>> grid(n + 1, vector>(n + 1, vector(n + 1, 10005))); // C++定义了一个三位数组,10005是因为边的最大距离是10^4 + +for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2][0] = val; + grid[p2][p1][0] = val; // 注意这里是双向图 +} + +``` + +grid数组中其他元素数值应该初始化多少呢? + +本题求的是最小值,所以输入数据没有涉及到的节点的情况都应该初始为一个最大数。 + +这样才不会影响,每次计算去最小值的时候 初始值对计算结果的影响。 + +所以grid数组的定义可以是: + +```CPP +// C++写法,定义了一个三位数组,10005是因为边的最大距离是10^4 +vector>> grid(n + 1, vector>(n + 1, vector(n + 1, 10005))); + +``` + +4、确定遍历顺序 + +从递推公式:`grid[i][j][k] = min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1])` 可以看出,我们需要三个for循环,分别遍历i,j 和k + +而 k 依赖于 k - 1, i 和j 的到 并不依赖与 i - 1 或者 j - 1 等等。 + +那么这三个for的嵌套顺序应该是什么样的呢? + +我们来看初始化,我们是把 k =0 的 i 和j 对应的数值都初始化了,这样才能去计算 k = 1 的时候 i 和 j 对应的数值。 + +这就好比是一个三维坐标,i 和j 是平层,而k 是 垂直向上 的。 + +遍历的顺序是从底向上 一层一层去遍历。 + +所以遍历k 的for循环一定是在最外面,这样才能一层一层去遍历。如图: + +![](https://file1.kamacoder.com/i/algo/20240424120109.png) + +至于遍历 i 和 j 的话,for 循环的先后顺序无所谓。 + +代码如下: + +```CPP +for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]); + } + } +} +``` + +有录友可能想,难道 遍历k 放在最里层就不行吗? + +k 放在最里层,代码是这样: + +```CPP +for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + for (int k = 1; k <= n; k++) { + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]); + } + } +} +``` + +此时就遍历了 j 与 k 形成一个平面,i 则是纵面,那遍历 就是这样的: + +![](https://file1.kamacoder.com/i/algo/20240424115827.png) + + +而我们初始化的数据 是 k 为0, i 和 j 形成的平面做初始化,如果以 k 和 j 形成的平面去一层一层遍历,就造成了 递推公式 用不上上一轮计算的结果,从而导致结果不对(初始化的部分是 i 与j 形成的平面,在初始部分有讲过)。 + +我再给大家举一个测试用例 + +``` +5 4 +1 2 10 +1 3 1 +3 4 1 +4 2 1 +1 +1 2 +``` + +就是图: + +![](https://file1.kamacoder.com/i/algo/20240424120942.png) + +求节点1 到 节点 2 的最短距离,运行结果是 10 ,但正确的结果很明显是3。 + +为什么呢? + +因为 k 放在最里面,先就把 节点1 和 节点 2 的最短距离就确定了,后面再也不会计算节点 1 和 节点 2的距离,同时也不会基于 初始化或者之前计算过的结果来计算,即:不会考虑 节点1 到 节点3, 节点3 到节点 4,节点4到节点2 的距离。 + + +造成这一原因,是 在三维立体坐标中, 我们初始化的是 i 和 i 在k 为0 所构成的平面,但遍历的时候 是以 j 和 k构成的平面以 i 为垂直方向去层次遍历。 + + +而遍历k 的for循环如果放在中间呢,同样是 j 与k 行程一个平面,i 是纵面,遍历的也是这样: + +![](https://file1.kamacoder.com/i/algo/20240424115827.png) + +同样不能完全用上初始化 和 上一层计算的结果。 + +根据这个情况再举一个例子: + +``` +5 2 +1 2 1 +2 3 10 +1 +1 3 +``` + +图: + +![](https://file1.kamacoder.com/i/algo/20240425112636.png) + +求 节点1 到节点3 的最短距离,如果k循环放中间,程序的运行结果是 -1,也就是不能到达节点3。 + +在计算 grid[i][j][k] 的时候,需要基于 grid[i][k][k-1] 和 grid[k][j][k-1]的数值。 + +也就是 计算 grid[1][3][2] (表示节点1 到 节点3,经过节点2) 的时候,需要基于 grid[1][2][1] 和 grid[2][3][1]的数值,而 我们初始化,只初始化了 k为0 的那一层。 + +造成这一原因 依然是 在三维立体坐标中, 我们初始化的是 i 和 j 在k 为0 所构成的平面,但遍历的时候 是以 j 和 k构成的平面以 i 为垂直方向去层次遍历。 + + +很多录友对于 floyd算法的遍历顺序搞不懂,**其实 是没有从三维的角度去思考**,同时我把三维立体图给大家画出来,遍历顺序标出来,大家就很容易想明白,为什么 k 放在最外层 才能用上 初始化和上一轮计算的结果了。 + +5、举例推导dp数组 + +这里涉及到 三维矩阵,可以一层一层打印出来去分析,例如k=0 的这一层,k = 1的这一层,但一起把三维带数据的图画出来其实不太好画。 + +## 代码如下 + +以上分析完毕,最后代码如下: + + +```CPP +#include +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector>> grid(n + 1, vector>(n + 1, vector(n + 1, 10005))); // 因为边的最大距离是10^4 + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2][0] = val; + grid[p2][p1][0] = val; // 注意这里是双向图 + + } + // 开始 floyd + for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]); + } + } + } + // 输出结果 + int z, start, end; + cin >> z; + while (z--) { + cin >> start >> end; + if (grid[start][end][n] == 10005) cout << -1 << endl; + else cout << grid[start][end][n] << endl; + } +} + +``` + +## 空间优化 + +这里 我们可以做一下 空间上的优化,从滚动数组的角度来看,我们定义一个 grid[n + 1][ n + 1][2] 这么大的数组就可以,因为k 只是依赖于 k-1的状态,并不需要记录k-2,k-3,k-4 等等这些状态。 + +那么我们只需要记录 grid[i][j][1] 和 grid[i][j][0] 就好,之后就是 grid[i][j][1] 和 grid[i][j][0] 交替滚动。 + +在进一步想,如果本层计算(本层计算即k相同,从三维角度来讲) gird[i][j] 用到了 本层中刚计算好的 grid[i][k] 会有什么问题吗? + +如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 小,说明确实有 i 到 k 的更短路径,那么基于 更小的 grid[i][k] 去计算 gird[i][j] 没有问题。 + +如果 本层刚计算好的 grid[i][k] 比上一层 (即k-1层)计算的 grid[i][k] 大, 这不可能,因为这样也不会做更新 grid[i][k]的操作。 + +所以本层计算中,使用了本层计算过的 grid[i][k] 和 grid[k][j] 是没问题的。 + +那么就没必要区分,grid[i][k] 和 grid[k][j] 是 属于 k - 1 层的呢,还是 k 层的。 + +所以递归公式可以为: + +```CPP +grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]); +``` + +基于二维数组的本题代码为: + + +```CPP +#include +#include +using namespace std; + +int main() { + int n, m, p1, p2, val; + cin >> n >> m; + + vector> grid(n + 1, vector(n + 1, 10005)); // 因为边的最大距离是10^4 + + for(int i = 0; i < m; i++){ + cin >> p1 >> p2 >> val; + grid[p1][p2] = val; + grid[p2][p1] = val; // 注意这里是双向图 + + } + // 开始 floyd + for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]); + } + } + } + // 输出结果 + int z, start, end; + cin >> z; + while (z--) { + cin >> start >> end; + if (grid[start][end] == 10005) cout << -1 << endl; + else cout << grid[start][end] << endl; + } +} + +``` + +* 时间复杂度: O(n^3) +* 空间复杂度:O(n^2) + +## 总结 + +本期如果上来只用二维数组来讲的话,其实更容易,但遍历顺序那里用二维数组其实是讲不清楚的,所以我直接用三维数组来讲,目的是将遍历顺序这里讲清楚。 + +理解了遍历顺序才是floyd算法最精髓的地方。 + + +floyd算法的时间复杂度相对较高,适合 稠密图且源点较多的情况。 + +如果是稀疏图,floyd是从节点的角度去计算了,例如 图中节点数量是 1000,就一条边,那 floyd的时间复杂度依然是 O(n^3) 。 + +如果 源点少,其实可以 多次dijsktra 求源点到终点。 + + +## 其他语言版本 + +### Java + +- 基于三维数组的Floyd算法 + +```java +public class FloydBase { + + // public static int MAX_VAL = Integer.MAX_VALUE; + public static int MAX_VAL = 10005; // 边的最大距离是10^4(不选用Integer.MAX_VALUE是为了避免相加导致数值溢出) + + public static void main(String[] args) { + // 输入控制 + Scanner sc = new Scanner(System.in); + System.out.println("1.输入N M"); + int n = sc.nextInt(); + int m = sc.nextInt(); + + System.out.println("2.输入M条边"); + + // ① dp定义(grid[i][j][k] 节点i到节点j 可能经过节点K(k∈[1,n]))的最短路径 + int[][][] grid = new int[n + 1][n + 1][n + 1]; + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + for (int k = 0; k <= n; k++) { + grid[i][j][k] = grid[j][i][k] = MAX_VAL; // 其余设置为最大值 + } + } + } + + // ② dp 推导:grid[i][j][k] = min{grid[i][k][k-1] + grid[k][j][k-1], grid[i][j][k-1]} + while (m-- > 0) { + int u = sc.nextInt(); + int v = sc.nextInt(); + int weight = sc.nextInt(); + grid[u][v][0] = grid[v][u][0] = weight; // 初始化(处理k=0的情况) ③ dp初始化 + } + + // ④ dp推导:floyd 推导 + for (int k = 1; k <= n; k++) { + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= n; j++) { + grid[i][j][k] = Math.min(grid[i][k][k - 1] + grid[k][j][k - 1], grid[i][j][k - 1]); + } + } + } + + System.out.println("3.输入[起点-终点]计划个数"); + int x = sc.nextInt(); + + System.out.println("4.输入每个起点src 终点dst"); + + while (x-- > 0) { + int src = sc.nextInt(); + int dst = sc.nextInt(); + // 根据floyd推导结果输出计划路径的最小距离 + if (grid[src][dst][n] == MAX_VAL) { + System.out.println("-1"); + } else { + System.out.println(grid[src][dst][n]); + } + } + } +} +``` + + + +### Python + +基于三维数组的Floyd + +```python +if __name__ == '__main__': + max_int = 10005 # 设置最大路径,因为边最大距离为10^4 + + n, m = map(int, input().split()) + + grid = [[[max_int] * (n+1) for _ in range(n+1)] for _ in range(n+1)] # 初始化三维dp数组 + + for _ in range(m): + p1, p2, w = map(int, input().split()) + grid[p1][p2][0] = w + grid[p2][p1][0] = w + + # 开始floyd + for k in range(1, n+1): + for i in range(1, n+1): + for j in range(1, n+1): + grid[i][j][k] = min(grid[i][j][k-1], grid[i][k][k-1] + grid[k][j][k-1]) + + # 输出结果 + z = int(input()) + for _ in range(z): + start, end = map(int, input().split()) + if grid[start][end][n] == max_int: + print(-1) + else: + print(grid[start][end][n]) +``` + +基于二维数组的Floyd + +```python +if __name__ == '__main__': + max_int = 10005 # 设置最大路径,因为边最大距离为10^4 + + n, m = map(int, input().split()) + + grid = [[max_int]*(n+1) for _ in range(n+1)] # 初始化二维dp数组 + + for _ in range(m): + p1, p2, val = map(int, input().split()) + grid[p1][p2] = val + grid[p2][p1] = val + + # 开始floyd + for k in range(1, n+1): + for i in range(1, n+1): + for j in range(1, n+1): + grid[i][j] = min(grid[i][j], grid[i][k] + grid[k][j]) + + # 输出结果 + z = int(input()) + for _ in range(z): + start, end = map(int, input().split()) + if grid[start][end] == max_int: + print(-1) + else: + print(grid[start][end]) +``` + +### Go + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0098.\346\211\200\346\234\211\345\217\257\350\276\276\350\267\257\345\276\204.md" "b/problems/kamacoder/0098.\346\211\200\346\234\211\345\217\257\350\276\276\350\267\257\345\276\204.md" new file mode 100644 index 0000000000..c71981996b --- /dev/null +++ "b/problems/kamacoder/0098.\346\211\200\346\234\211\345\217\257\350\276\276\350\267\257\345\276\204.md" @@ -0,0 +1,890 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 98. 所有可达路径 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1170) + +【题目描述】 + +给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个程序,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。 + +【输入描述】 + +第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边 + +后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径 + +【输出描述】 + +输出所有的可达路径,路径中所有节点的后面跟一个空格,每条路径独占一行,存在多条路径,路径输出的顺序可任意。 + +如果不存在任何一条路径,则输出 -1。 + +注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 `1 3 5`,而不是 `1 3 5 `, 5后面没有空格! + +【输入示例】 + +``` +5 5 +1 3 +3 5 +1 2 +2 4 +4 5 +``` + +【输出示例】 + +``` +1 3 5 +1 2 4 5 +``` + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240514103953.png) + +用例解释: + +有五个节点,其中的从 1 到达 5 的路径有两个,分别是 1 -> 3 -> 5 和 1 -> 2 -> 4 -> 5。 + +因为拥有多条路径,所以输出结果为: + +``` +1 3 5 +1 2 4 5 +``` + +或 + +``` +1 2 4 5 +1 3 5 +``` + +都算正确。 + +数据范围: + +* 图中不存在自环 +* 图中不存在平行边 +* 1 <= N <= 100 +* 1 <= M <= 500 + + +## 插曲 + +------------- + +本题和力扣 [797.所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/description/) 是一样的,录友了解深度优先搜索之后,这道题目就是模板题,是送分题。 + +力扣是核心代码模式,把图的存储方式给大家定义好了,只需要写出深搜的核心代码就可以。 + +如果笔试的时候出一道原题 (笔试都是ACM模式,部分面试也是ACM模式),不少熟练刷力扣的录友都难住了,因为不知道图应该怎么存,也不知道自己存的图如何去遍历。 + +所以这也是为什么我要让大家练习 ACM模式,也是我为什么 在代码随想录图论讲解中,不惜自己亲自出题,让大家统一练习ACM模式。 + +-------- + + +这道题目是深度优先搜索,比较好的入门题。 + +如果对深度优先搜索还不够了解,可以先看这里:[深度优先搜索的理论基础](https://programmercarl.com/图论深搜理论基础.html) + +我依然总结了深搜三部曲,如果按照代码随想录刷题的录友,应该刷过 二叉树的递归三部曲,回溯三部曲。 + +**大家可能有疑惑,深搜 和 二叉树和回溯算法 有什么区别呢**? 什么时候用深搜 什么时候用回溯? + +我在讲解[二叉树理论基础](https://programmercarl.com/二叉树理论基础.html)的时候,提到过,**二叉树的前中后序遍历其实就是深搜在二叉树这种数据结构上的应用**。 + +那么回溯算法呢,**其实 回溯算法就是 深搜,只不过针对某一搜索场景 我们给他一个更细分的定义,叫做回溯算法**。 + +那有的录友可能说:那我以后称回溯算法为深搜,是不是没毛病? + +理论上来说,没毛病,但 就像是 二叉树 你不叫它二叉树,叫它数据结构,有问题不? 也没问题对吧。 + +建议是 有细分的场景,还是称其细分场景的名称。 所以回溯算法可以独立出来,但回溯确实就是深搜。 + +## 图的存储 + +在[图论理论基础篇](./图论理论基础.md) 中我们讲到了 两种 图的存储方式:邻接表 和 邻接矩阵。 + +本题我们将带大家分别实现这两个图的存储方式。 + +### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +本题我们会有n 个节点,因为节点标号是从1开始的,为了节点标号和下标对齐,我们申请 n + 1 * n + 1 这么大的二维数组。 + +```CPP +vector> graph(n + 1, vector(n + 1, 0)); +``` + +输入m个边,构造方式如下: + +```CPP +while (m--) { + cin >> s >> t; + // 使用邻接矩阵 ,1 表示 节点s 指向 节点t + graph[s][t] = 1; +} +``` + +### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造相对邻接矩阵难理解一些。 + +我在 [图论理论基础篇](./图论理论基础.md) 举了一个例子: + + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4指向节点1 + +我们需要构造一个数组,数组里的元素是一个链表。 + +C++写法: + +```CPP +// 节点编号从1到n,所以申请 n+1 这么大的数组 +vector> graph(n + 1); // 邻接表,list为C++里的链表 +``` + +输入m个边,构造方式如下: + +```CPP +while (m--) { + cin >> s >> t; + // 使用邻接表 ,表示 s -> t 是相连的 + graph[s].push_back(t); +} +``` + +本题我们使用邻接表 或者 邻接矩阵都可以,因为后台数据并没有对图的大小以及稠密度做很大的区分。 + +以下我们使用邻接矩阵的方式来讲解,文末我也会给出 使用邻接表的整体代码。 + +**注意邻接表 和 邻接矩阵的写法都要掌握**! + +## 深度优先搜索 + +本题是深度优先搜索的基础题目,关于深搜我在[图论深搜理论基础](./图论深搜理论基础.md) 已经有详细的讲解,图文并茂。 + +关于本题我会直接使用深搜三部曲来分析,如果对深搜不够了解,建议先看 [图论深搜理论基础](./图论深搜理论基础.md)。 + +深搜三部曲来分析题目: + +1. 确认递归函数,参数 + +首先我们dfs函数一定要存一个图,用来遍历的,需要存一个目前我们遍历的节点,定义为x。 + +还需要存一个n,表示终点,我们遍历的时候,用来判断当 x==n 时候 标明找到了终点。 + +(其实在递归函数的参数 不容易一开始就确定了,一般是在写函数体的时候发现缺什么,参加就补什么) + +至于 单一路径 和 路径集合 可以放在全局变量,那么代码是这样的: + +```CPP +vector> result; // 收集符合条件的路径 +vector path; // 0节点到终点的路径 +// x:目前遍历的节点 +// graph:存当前的图 +// n:终点 +void dfs (const vector>& graph, int x, int n) { +``` + +2. 确认终止条件 + +什么时候我们就找到一条路径了? + +当目前遍历的节点 为 最后一个节点 n 的时候 就找到了一条 从出发点到终止点的路径。 + +```CPP +// 当前遍历的节点x 到达节点n +if (x == n) { // 找到符合条件的一条路径 + result.push_back(path); + return; +} +``` + +3. 处理目前搜索节点出发的路径 + +接下来是走 当前遍历节点x的下一个节点。 + +首先是要找到 x节点指向了哪些节点呢? 遍历方式是这样的: + +```c++ +for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x指向的节点,就是节点i + } +} +``` + +接下来就是将 选中的x所指向的节点,加入到 单一路径来。 + +```C++ +path.push_back(i); // 遍历到的节点加入到路径中来 + +``` + + +进入下一层递归 + +```CPP +dfs(graph, i, n); // 进入下一层递归 +``` + +最后就是回溯的过程,撤销本次添加节点的操作。 + +为什么要有回溯,我在[图论深搜理论基础](./图论深搜理论基础.md) 也有详细的讲解。 + +该过程整体代码: + +```CPP +for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x链接的节点 + path.push_back(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 + } +} +``` + +## 打印结果 + +ACM格式大家在输出结果的时候,要关注看看格式问题,特别是字符串,有的题目说的是每个元素后面都有空格,有的题目说的是 每个元素间有空格,最后一个元素没有空格。 + +有的题目呢,压根没说,那只能提交去试一试了。 + +很多录友在提交题目的时候发现结果一样,为什么提交就是不对呢。 + +例如示例输出是: + +`1 3 5` 而不是 `1 3 5 ` + +即 5 的后面没有空格! + +这是我们在输出的时候需要注意的点。 + +有录友可能会想,ACM格式就是麻烦,有空格没有空格有什么影响,结果对了不就行了? + +ACM模式相对于核心代码模式(力扣) 更考验大家对代码的掌控能力。 例如工程代码里,输入输出都是要自己控制的。这也是为什么大公司笔试,都是ACM模式。 + +以上代码中,结果都存在了 result数组里(二维数组,每一行是一个结果),最后将其打印出来。(重点看注释) + +```CPP +// 输出结果 +if (result.size() == 0) cout << -1 << endl; +for (const vector &pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { // 这里指打印到倒数第二个 + cout << pa[i] << " "; + } + cout << pa[pa.size() - 1] << endl; // 这里再打印倒数第一个,控制最后一个元素后面没有空格 +} +``` + +## 本题代码 + +### 邻接矩阵写法 + + +```CPP +#include +#include +using namespace std; +vector> result; // 收集符合条件的路径 +vector path; // 1节点到终点的路径 + +void dfs (const vector>& graph, int x, int n) { + // 当前遍历的节点x 到达节点n + if (x == n) { // 找到符合条件的一条路径 + result.push_back(path); + return; + } + for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x链接的节点 + path.push_back(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 + } + } +} + +int main() { + int n, m, s, t; + cin >> n >> m; + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + vector> graph(n + 1, vector(n + 1, 0)); + + while (m--) { + cin >> s >> t; + // 使用邻接矩阵 表示无线图,1 表示 s 与 t 是相连的 + graph[s][t] = 1; + } + + path.push_back(1); // 无论什么路径已经是从0节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.size() == 0) cout << -1 << endl; + for (const vector &pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + cout << pa[i] << " "; + } + cout << pa[pa.size() - 1] << endl; + } +} + +``` + +### 邻接表写法 + +```CPP +#include +#include +#include +using namespace std; + +vector> result; // 收集符合条件的路径 +vector path; // 1节点到终点的路径 + +void dfs (const vector>& graph, int x, int n) { + + if (x == n) { // 找到符合条件的一条路径 + result.push_back(path); + return; + } + for (int i : graph[x]) { // 找到 x指向的节点 + path.push_back(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.pop_back(); // 回溯,撤销本节点 + } +} + +int main() { + int n, m, s, t; + cin >> n >> m; + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + vector> graph(n + 1); // 邻接表 + while (m--) { + cin >> s >> t; + // 使用邻接表 ,表示 s -> t 是相连的 + graph[s].push_back(t); + + } + + path.push_back(1); // 无论什么路径已经是从0节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.size() == 0) cout << -1 << endl; + for (const vector &pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + cout << pa[i] << " "; + } + cout << pa[pa.size() - 1] << endl; + } +} + +``` + +## 总结 + +本题是一道简单的深搜题目,也可以说是模板题,和 [力扣797. 所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/description/) 思路是一样一样的。 + +很多录友做力扣的时候,轻松就把代码写出来了, 但做面试笔试的时候,遇到这样的题就写不出来了。 + +在力扣上刷题不用考虑图的存储方式,也不用考虑输出的格式。 + +而这些都是 ACM 模式题目的知识点(图的存储方式)和细节(输出的格式) + +所以我才会特别制作ACM题目,同样也重点去讲解图的存储和遍历方式,来帮大家去练习。 + +对于这种有向图路径问题,最合适使用深搜,当然本题也可以使用广搜,但广搜相对来说就麻烦了一些,需要记录一下路径。 + +而深搜和广搜都适合解决颜色类的问题,例如岛屿系列,其实都是 遍历+标记,所以使用哪种遍历都是可以的。 + +至于广搜理论基础,我们在下一篇在好好讲解,敬请期待! + + + +## 其他语言版本 + +### Java + +邻接矩阵写法 +```java +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class Main { + static List> result = new ArrayList<>(); // 收集符合条件的路径 + static List path = new ArrayList<>(); // 1节点到终点的路径 + + public static void dfs(int[][] graph, int x, int n) { + // 当前遍历的节点x 到达节点n + if (x == n) { // 找到符合条件的一条路径 + result.add(new ArrayList<>(path)); + return; + } + for (int i = 1; i <= n; i++) { // 遍历节点x链接的所有节点 + if (graph[x][i] == 1) { // 找到 x链接的节点 + path.add(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.remove(path.size() - 1); // 回溯,撤销本节点 + } + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + int[][] graph = new int[n + 1][n + 1]; + + for (int i = 0; i < m; i++) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + // 使用邻接矩阵表示无向图,1 表示 s 与 t 是相连的 + graph[s][t] = 1; + } + + path.add(1); // 无论什么路径已经是从1节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.isEmpty()) System.out.println(-1); + for (List pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + System.out.print(pa.get(i) + " "); + } + System.out.println(pa.get(pa.size() - 1)); + } + } +} +``` + +邻接表写法 +```java +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Scanner; + +public class Main { + static List> result = new ArrayList<>(); // 收集符合条件的路径 + static List path = new ArrayList<>(); // 1节点到终点的路径 + + public static void dfs(List> graph, int x, int n) { + if (x == n) { // 找到符合条件的一条路径 + result.add(new ArrayList<>(path)); + return; + } + for (int i : graph.get(x)) { // 找到 x指向的节点 + path.add(i); // 遍历到的节点加入到路径中来 + dfs(graph, i, n); // 进入下一层递归 + path.remove(path.size() - 1); // 回溯,撤销本节点 + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + List> graph = new ArrayList<>(n + 1); + for (int i = 0; i <= n; i++) { + graph.add(new LinkedList<>()); + } + + while (m-- > 0) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + // 使用邻接表表示 s -> t 是相连的 + graph.get(s).add(t); + } + + path.add(1); // 无论什么路径已经是从1节点出发 + dfs(graph, 1, n); // 开始遍历 + + // 输出结果 + if (result.isEmpty()) System.out.println(-1); + for (List pa : result) { + for (int i = 0; i < pa.size() - 1; i++) { + System.out.print(pa.get(i) + " "); + } + System.out.println(pa.get(pa.size() - 1)); + } + } +} +``` +### Python +邻接矩阵写法 +``` python +def dfs(graph, x, n, path, result): + if x == n: + result.append(path.copy()) + return + for i in range(1, n + 1): + if graph[x][i] == 1: + path.append(i) + dfs(graph, i, n, path, result) + path.pop() + +def main(): + n, m = map(int, input().split()) + graph = [[0] * (n + 1) for _ in range(n + 1)] + + for _ in range(m): + s, t = map(int, input().split()) + graph[s][t] = 1 + + result = [] + dfs(graph, 1, n, [1], result) + + if not result: + print(-1) + else: + for path in result: + print(' '.join(map(str, path))) + +if __name__ == "__main__": + main() +``` + +邻接表写法 +``` python +from collections import defaultdict + +result = [] # 收集符合条件的路径 +path = [] # 1节点到终点的路径 + +def dfs(graph, x, n): + if x == n: # 找到符合条件的一条路径 + result.append(path.copy()) + return + for i in graph[x]: # 找到 x指向的节点 + path.append(i) # 遍历到的节点加入到路径中来 + dfs(graph, i, n) # 进入下一层递归 + path.pop() # 回溯,撤销本节点 + +def main(): + n, m = map(int, input().split()) + + graph = defaultdict(list) # 邻接表 + for _ in range(m): + s, t = map(int, input().split()) + graph[s].append(t) + + path.append(1) # 无论什么路径已经是从1节点出发 + dfs(graph, 1, n) # 开始遍历 + + # 输出结果 + if not result: + print(-1) + for pa in result: + print(' '.join(map(str, pa))) + +if __name__ == "__main__": + main() +``` +### Go + +#### 邻接矩阵写法 +```go +package main + +import ( + "fmt" +) + +var result [][]int // 收集符合条件的路径 +var path []int // 1节点到终点的路径 + +func dfs(graph [][]int, x, n int) { + // 当前遍历的节点x 到达节点n + if x == n { // 找到符合条件的一条路径 + temp := make([]int, len(path)) + copy(temp, path) + result = append(result, temp) + return + } + for i := 1; i <= n; i++ { // 遍历节点x链接的所有节点 + if graph[x][i] == 1 { // 找到 x链接的节点 + path = append(path, i) // 遍历到的节点加入到路径中来 + dfs(graph, i, n) // 进入下一层递归 + path = path[:len(path)-1] // 回溯,撤销本节点 + } + } +} + +func main() { + var n, m int + fmt.Scanf("%d %d", &n, &m) + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + graph := make([][]int, n+1) + for i := range graph { + graph[i] = make([]int, n+1) + } + + for i := 0; i < m; i++ { + var s, t int + fmt.Scanf("%d %d", &s, &t) + // 使用邻接矩阵表示无向图,1 表示 s 与 t 是相连的 + graph[s][t] = 1 + } + + path = append(path, 1) // 无论什么路径已经是从1节点出发 + dfs(graph, 1, n) // 开始遍历 + + // 输出结果 + if len(result) == 0 { + fmt.Println(-1) + } else { + for _, pa := range result { + for i := 0; i < len(pa)-1; i++ { + fmt.Print(pa[i], " ") + } + fmt.Println(pa[len(pa)-1]) + } + } +} +``` + +#### 邻接表写法 +```go +package main + +import ( + "fmt" +) + +var result [][]int +var path []int + +func dfs(graph [][]int, x, n int) { + if x == n { + temp := make([]int, len(path)) + copy(temp, path) + result = append(result, temp) + return + } + for _, i := range graph[x] { + path = append(path, i) + dfs(graph, i, n) + path = path[:len(path)-1] + } +} + +func main() { + var n, m int + fmt.Scanf("%d %d", &n, &m) + + graph := make([][]int, n+1) + for i := 0; i <= n; i++ { + graph[i] = make([]int, 0) + } + + for m > 0 { + var s, t int + fmt.Scanf("%d %d", &s, &t) + graph[s] = append(graph[s], t) + m-- + } + + path = append(path, 1) + dfs(graph, 1, n) + + if len(result) == 0 { + fmt.Println(-1) + } else { + for _, pa := range result { + for i := 0; i < len(pa)-1; i++ { + fmt.Print(pa[i], " ") + } + fmt.Println(pa[len(pa)-1]) + } + } +} +``` + +### Rust + +### JavaScript + +#### 邻接矩阵写法 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async ()=>(await iter.next()).value; + + +let graph; +let N, M; +// 收集符合条件的路径 +let result = []; +// 1节点到终点的路径 +let path = []; + +// 创建邻接矩阵,初始化邻接矩阵 +async function initGraph(){ + let line; + + line = await readline(); + [N, M] = line.split(' ').map(i => parseInt(i)) + graph = new Array(N + 1).fill(0).map(() => new Array(N + 1).fill(0)) + + while(M--){ + line = await readline() + const strArr = line ? line.split(' ').map(i => parseInt(i)) : undefined + strArr ? graph[strArr[0]][strArr[1]] = 1 : null + } +}; + +// 深度搜索 +function dfs(graph, x, n){ + // 当前遍历节点为x, 到达节点为n + if(x == n){ + result.push([...path]) + return + } + for(let i = 1 ; i <= n ; i++){ + if(graph[x][i] == 1){ + path.push(i) + dfs(graph, i, n ) + path.pop(i) + } + } +}; + +(async function(){ + // 创建邻接矩阵,初始化邻接矩阵 + await initGraph(); + + // 从节点1开始深度搜索 + path.push(1); + + // 深度搜索 + dfs(graph, 1, N ); + + // 输出 + if(result.length > 0){ + result.forEach(i => { + console.log(i.join(' ')) + }) + }else{ + console.log(-1) + } + +})(); + +``` + + + +#### 邻接表写法 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph; +let N, M; + +// 收集符合条件的路径 +let result = []; +// 1节点到终点的路径 +let path = []; + +// 创建邻接表,初始化邻接表 +async function initGraph() { + let line; + line = await readline(); + [N, M] = line.split(' ').map(i => parseInt(i)) + graph = new Array(N + 1).fill(0).map(() => new Array()) + + while (line = await readline()) { + const strArr = line.split(' ').map(i => parseInt(i)) + strArr ? graph[strArr[0]].push(strArr[1]) : null + } +}; + +// 深度搜索 +async function dfs(graph, x, n) { + // 当前遍历节点为x, 到达节点为n + if (x == n) { + result.push([...path]) + return + } + + graph[x].forEach(i => { + path.push(i) + dfs(graph, i, n) + path.pop(i) + }) +}; + +(async function () { + // 创建邻接表,初始化邻接表 + await initGraph(); + + // 从节点1开始深度搜索 + path.push(1); + + // 深度搜索 + dfs(graph, 1, N); + + // 输出 + if (result.length > 0) { + result.forEach(i => { + console.log(i.join(' ')) + }) + } else { + console.log(-1) + } +})(); +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + + + + + + + +
diff --git "a/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\345\271\277\346\220\234.md" "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\345\271\277\346\220\234.md" new file mode 100644 index 0000000000..93c1fe41fa --- /dev/null +++ "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\345\271\277\346\220\234.md" @@ -0,0 +1,560 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 99. 岛屿数量 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1171) + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,你需要计算岛屿的数量。岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。 + +后续 N 行,每行包含 M 个数字,数字为 1 或者 0。 + +输出描述: + +输出一个整数,表示岛屿的数量。如果不存在岛屿,则输出 0。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例: + +3 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240516111613.png) + +根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。 + +数据范围: + +* 1 <= N, M <= 50 + + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题题目是 DFS,BFS,并查集,基础题目。 + +本题思路:遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。 + +再遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 + +那么如果把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 + +### 广度优先搜索 + +如果不熟悉广搜,建议先看 [广搜理论基础](./图论广搜理论基础.md)。 + +不少同学用广搜做这道题目的时候,超时了。 这里有一个广搜中很重要的细节: + +根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。 + +很多同学可能感觉这有区别吗? + +如果从队列拿出节点,再去标记这个节点走过,就会发生下图所示的结果,会导致很多节点重复加入队列。 + +![](https://file1.kamacoder.com/i/algo/20250124094043.png) + +超时写法 (从队列中取出节点再标记,注意代码注释的地方) + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + visited[curx][cury] = true; // 从队列中取出在标记走过 + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { + que.push({nextx, nexty}); + } + } + } + +} +``` + + +加入队列 就代表走过,立刻标记,正确写法: (注意代码注释的地方) + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + visited[x][y] = true; // 只要加入队列,立刻标记 + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == '1') { + que.push({nextx, nexty}); + visited[nextx][nexty] = true; // 只要加入队列立刻标记 + } + } + } + +} +``` + +以上两个版本其实,其实只有细微区别,就是 `visited[x][y] = true;` 放在的地方,这取决于我们对 代码中队列的定义,队列中的节点就表示已经走过的节点。 **所以只要加入队列,立即标记该节点走过**。 + +本题完整广搜代码: + +```CPP +#include +#include +#include +using namespace std; + +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(const vector>& grid, vector>& visited, int x, int y) { + queue> que; + que.push({x, y}); + visited[x][y] = true; // 只要加入队列,立刻标记 + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { + que.push({nextx, nexty}); + visited[nextx][nexty] = true; // 只要加入队列立刻标记 + } + } + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + vector> visited(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + result++; // 遇到没访问过的陆地,+1 + bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + + + cout << result << endl; +} + +``` + + +## 其他语言版本 + +### Java + +```java +import java.util.*; + +public class Main { + public static int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};//下右上左逆时针遍历 + + public static void bfs(int[][] grid, boolean[][] visited, int x, int y) { + Queue queue = new LinkedList();//定义坐标队列,没有现成的pair类,在下面自定义了 + queue.add(new pair(x, y)); + visited[x][y] = true;//遇到入队直接标记为优先, + // 否则出队时才标记的话会导致重复访问,比如下方节点会在右下顺序的时候被第二次访问入队 + while (!queue.isEmpty()) { + int curX = queue.peek().first; + int curY = queue.poll().second;//当前横纵坐标 + for (int i = 0; i < 4; i++) { + //顺时针遍历新节点next,下面记录坐标 + int nextX = curX + dir[i][0]; + int nextY = curY + dir[i][1]; + if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) { + continue; + }//去除越界部分 + if (!visited[nextX][nextY] && grid[nextX][nextY] == 1) { + queue.add(new pair(nextX, nextY)); + visited[nextX][nextY] = true;//逻辑同上 + } + } + } + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int m = sc.nextInt(); + int n = sc.nextInt(); + int[][] grid = new int[m][n]; + boolean[][] visited = new boolean[m][n]; + int ans = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + grid[i][j] = sc.nextInt(); + } + } + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + ans++; + bfs(grid, visited, i, j); + } + } + } + System.out.println(ans); + } +} +``` + + +### Python + +```python + +from collections import deque +directions = [[0, 1], [1, 0], [0, -1], [-1, 0]] +def bfs(grid, visited, x, y): + que = deque([]) + que.append([x,y]) + visited[x][y] = True + while que: + cur_x, cur_y = que.popleft() + for i, j in directions: + next_x = cur_x + i + next_y = cur_y + j + if next_y < 0 or next_x < 0 or next_x >= len(grid) or next_y >= len(grid[0]): + continue + if not visited[next_x][next_y] and grid[next_x][next_y] == 1: + visited[next_x][next_y] = True + que.append([next_x, next_y]) + + +def main(): + n, m = map(int, input().split()) + grid = [] + for i in range(n): + grid.append(list(map(int, input().split()))) + visited = [[False] * m for _ in range(n)] + res = 0 + for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + res += 1 + bfs(grid, visited, i, j) + print(res) + +if __name__ == "__main__": + main() + + + +``` + + +### Go + +```go + +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +var dir = [4][2]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} // 四个方向 + +func dfs(grid [][]int, visited [][]bool, x, y int) { + for i := 0; i < 4; i++ { + nextx := x + dir[i][0] + nexty := y + dir[i][1] + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue // 越界了,直接跳过 + } + if !visited[nextx][nexty] && grid[nextx][nexty] == 1 { // 没有访问过的 同时 是陆地的 + visited[nextx][nexty] = true + dfs(grid, visited, nextx, nexty) + } + } +} + +func main() { + reader := bufio.NewReader(os.Stdin) + var n, m int + fmt.Scanf("%d %d", &n, &m) + + grid := make([][]int, n) + for i := 0; i < n; i++ { + grid[i] = make([]int, m) + line, _ := reader.ReadString('\n') + line = strings.TrimSpace(line) + elements := strings.Split(line, " ") + for j := 0; j < m; j++ { + grid[i][j], _ = strconv.Atoi(elements[j]) + } + } + + visited := make([][]bool, n) + for i := 0; i < n; i++ { + visited[i] = make([]bool, m) + } + + result := 0 + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if !visited[i][j] && grid[i][j] == 1 { + visited[i][j] = true + result++ // 遇到没访问过的陆地,+1 + dfs(grid, visited, i, j) // 将与其链接的陆地都标记上 true + } + } + } + + fmt.Println(result) +} + + +``` + + + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph +let N, M +let visited +let result = 0 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x, y)开始广度优先遍历 + * @param {*} graph 地图 + * @param {*} visited 访问过的节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, visited, x, y) => { + let queue = [] + queue.push([x, y]) + visited[x][y] = true //只要加入队列就立刻标记为访问过 + + while (queue.length) { + let [x, y] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if(nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if(!visited[nextx][nexty] && graph[nextx][nexty] === 1){ + queue.push([nextx, nexty]) + visited[nextx][nexty] = true + } + } + } + +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计岛屿数 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { + // 遇到没访问过的陆地,+1 + result++ + + // 广度优先遍历,将相邻陆地标记为已访问 + bfs(graph, visited, i, j) + } + } + } + console.log(result); +})() +``` + + + +### TypeScript + +### PhP + +```PHP + += count($grid) || $nexty < 0 || $nexty >= count($grid[0])) { + continue; // 越界了,直接跳过 + } + if (!$visited[$nextx][$nexty] && $grid[$nextx][$nexty] == 1) { // 没有访问过的 同时 是陆地的 + $visited[$nextx][$nexty] = true; + dfs($grid, $visited, $nextx, $nexty); + } + } +} + +function main() { + fscanf(STDIN, "%d %d", $n, $m); + $grid = []; + for ($i = 0; $i < $n; $i++) { + $grid[$i] = array_map('intval', explode(' ', trim(fgets(STDIN)))); + } + + $visited = array_fill(0, $n, array_fill(0, $m, false)); + + $result = 0; + for ($i = 0; $i < $n; $i++) { + for ($j = 0; $j < $m; $j++) { + if (!$visited[$i][$j] && $grid[$i][$j] == 1) { + $visited[$i][$j] = true; + $result++; // 遇到没访问过的陆地,+1 + dfs($grid, $visited, $i, $j); // 将与其链接的陆地都标记上 true + } + } + } + + echo $result . PHP_EOL; +} + +main(); +?> + + +``` + + +### Swift + +### Scala +```scala +import scala.collection.mutable.Queue +import util.control.Breaks._ + +// Dev on LeetCode: https://leetcode.cn/problems/number-of-islands/description/ +object Solution { + def numIslands(grid: Array[Array[Char]]): Int = { + val row = grid.length + val col = grid(0).length + val dir = List((-1,0), (0,-1), (1,0), (0,1)) // 四个方向 + var visited = Array.fill(row)(Array.fill(col)(false)) + var counter = 0 + var que = Queue.empty[Tuple2[Int, Int]] + + (0 until row).map{ r => + (0 until col).map{ c => + breakable { + if (!visited(r)(c) && grid(r)(c) == '1') { + que.enqueue((r, c)) + visited(r)(c) // 只要加入队列,立刻标记 + } else break // 不是岛屿不进入queue,也不记录 + + while (!que.isEmpty) { + val cur = que.head + que.dequeue() + val x = cur(0) + val y = cur(1) + dir.map{ d => + val nextX = x + d(0) + val nextY = y + d(1) + breakable { + // 越界就跳过 + if (nextX < 0 || nextX >= row || nextY < 0 || nextY >= col) break + if (!visited(nextX)(nextY) && grid(nextX)(nextY) == '1') { + visited(nextX)(nextY) = true // 只要加入队列,立刻标记 + que.enqueue((nextX, nextY)) + } + } + } + } + counter = counter + 1 // 找完一个岛屿后记录一下 + } + } + } + + counter + } +} +``` + +### C# + + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\346\267\261\346\220\234.md" "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\346\267\261\346\220\234.md" new file mode 100644 index 0000000000..3c54278af0 --- /dev/null +++ "b/problems/kamacoder/0099.\345\262\233\345\261\277\347\232\204\346\225\260\351\207\217\346\267\261\346\220\234.md" @@ -0,0 +1,462 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 99. 岛屿数量 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1171) + + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,你需要计算岛屿的数量。岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。 + +后续 N 行,每行包含 M 个数字,数字为 1 或者 0。 + +输出描述: + +输出一个整数,表示岛屿的数量。如果不存在岛屿,则输出 0。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例: + +3 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240516111613.png) + +根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。 + +数据范围: + +* 1 <= N, M <= 50 + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题题目是 DFS,BFS,并查集,基础题目。 + +本题思路,是用遇到一个没有遍历过的节点陆地,计数器就加一,然后把该节点陆地所能遍历到的陆地都标记上。 + +在遇到标记过的陆地节点和海洋节点的时候直接跳过。 这样计数器就是最终岛屿的数量。 + +那么如何把节点陆地所能遍历到的陆地都标记上呢,就可以使用 DFS,BFS或者并查集。 + +### 深度优先搜索 + +以下代码使用dfs实现,如果对dfs不太了解的话,**建议按照代码随想录的讲解顺序学习**。 + +C++代码如下: + +```CPP +// 版本一 +#include +#include +using namespace std; + +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(const vector>& grid, vector>& visited, int x, int y) { + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { // 没有访问过的 同时 是陆地的 + + visited[nextx][nexty] = true; + dfs(grid, visited, nextx, nexty); + } + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + vector> visited(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + visited[i][j] = true; + result++; // 遇到没访问过的陆地,+1 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + + cout << result << endl; +} +``` + +很多录友可能有疑惑,为什么 以上代码中的dfs函数,没有终止条件呢? 感觉递归没有终止很危险。 + +其实终止条件 就写在了 调用dfs的地方,如果遇到不合法的方向,直接不会去调用dfs。 + +当然也可以这么写: + +```CPP +// 版本二 +#include +#include +using namespace std; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(const vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty); + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + vector> visited(n, vector(m, false)); + + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + result++; // 遇到没访问过的陆地,+1 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + } + } + } + cout << result << endl; +} +``` + +这里大家应该能看出区别了,无疑就是版本一中 调用dfs 的条件判断 放在了 版本二 的 终止条件位置上。 + +**版本一的写法**是 :下一个节点是否能合法已经判断完了,传进dfs函数的就是合法节点。 + +**版本二的写法**是:不管节点是否合法,上来就dfs,然后在终止条件的地方进行判断,不合法再return。 + +**理论上来讲,版本一的效率更高一些**,因为避免了 没有意义的递归调用,在调用dfs之前,就做合法性判断。 但从写法来说,可能版本二 更利于理解一些。(不过其实都差不太多) + +很多同学看了同一道题目,都是dfs,写法却不一样,**有时候有终止条件,有时候连终止条件都没有,其实这就是根本原因,两种写法而已**。 + + +## 总结 + +其实本题是 dfs,bfs 模板题,但正是因为是模板题,所以大家或者一些题解把重要的细节都很忽略了,我这里把大家没注意的但以后会踩的坑 都给列出来了。 + +本篇我只给出的dfs的写法,大家发现我写的还是比较细的,那么后面我再单独给出本题的bfs写法,虽然是模板题,但依然有很多注意的点,敬请期待! + + +## 其他语言版本 + +### Java +```java +import java.util.Scanner; + +public class Main { + public static int[][] dir ={{0,1},{1,0},{-1,0},{0,-1}}; + public static void dfs(boolean[][] visited,int x,int y ,int [][]grid) + { + for (int i = 0; i < 4; i++) { + int nextX=x+dir[i][0]; + int nextY=y+dir[i][1]; + if(nextY<0||nextX<0||nextX>= grid.length||nextY>=grid[0].length) + continue; + if(!visited[nextX][nextY]&&grid[nextX][nextY]==1) + { + visited[nextX][nextY]=true; + dfs(visited,nextX,nextY,grid); + } + } + } + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int m= sc.nextInt(); + int n = sc.nextInt(); + int[][] grid = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + grid[i][j]=sc.nextInt(); + } + } + boolean[][]visited =new boolean[m][n]; + int ans = 0; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if(!visited[i][j]&&grid[i][j]==1) + { + ans++; + visited[i][j]=true; + dfs(visited,i,j,grid); + } + } + } + System.out.println(ans); + } +} + +``` +### Python + +版本一 + +```python +direction = [[0, 1], [1, 0], [0, -1], [-1, 0]] # 四个方向:上、右、下、左 + + +def dfs(grid, visited, x, y): + """ + 对一块陆地进行深度优先遍历并标记 + """ + for i, j in direction: + next_x = x + i + next_y = y + j + # 下标越界,跳过 + if next_x < 0 or next_x >= len(grid) or next_y < 0 or next_y >= len(grid[0]): + continue + # 未访问的陆地,标记并调用深度优先搜索 + if not visited[next_x][next_y] and grid[next_x][next_y] == 1: + visited[next_x][next_y] = True + dfs(grid, visited, next_x, next_y) + + +if __name__ == '__main__': + # 版本一 + n, m = map(int, input().split()) + + # 邻接矩阵 + grid = [] + for i in range(n): + grid.append(list(map(int, input().split()))) + + # 访问表 + visited = [[False] * m for _ in range(n)] + + res = 0 + for i in range(n): + for j in range(m): + # 判断:如果当前节点是陆地,res+1并标记访问该节点,使用深度搜索标记相邻陆地。 + if grid[i][j] == 1 and not visited[i][j]: + res += 1 + visited[i][j] = True + dfs(grid, visited, i, j) + + print(res) +``` + +版本二 + +```python +direction = [[0, 1], [1, 0], [0, -1], [-1, 0]] # 四个方向:上、右、下、左 + + +def dfs(grid, visited, x, y): + """ + 对一块陆地进行深度优先遍历并标记 + """ + # 与版本一的差别,在调用前增加判断终止条件 + if visited[x][y] or grid[x][y] == 0: + return + visited[x][y] = True + + for i, j in direction: + next_x = x + i + next_y = y + j + # 下标越界,跳过 + if next_x < 0 or next_x >= len(grid) or next_y < 0 or next_y >= len(grid[0]): + continue + # 由于判断条件放在了方法首部,此处直接调用dfs方法 + dfs(grid, visited, next_x, next_y) + + +if __name__ == '__main__': + # 版本二 + n, m = map(int, input().split()) + + # 邻接矩阵 + grid = [] + for i in range(n): + grid.append(list(map(int, input().split()))) + + # 访问表 + visited = [[False] * m for _ in range(n)] + + res = 0 + for i in range(n): + for j in range(m): + # 判断:如果当前节点是陆地,res+1并标记访问该节点,使用深度搜索标记相邻陆地。 + if grid[i][j] == 1 and not visited[i][j]: + res += 1 + dfs(grid, visited, i, j) + + print(res) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph +let N, M +let visited +let result = 0 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + +/** + * @description: 从节点x,y开始深度优先遍历 + * @param {*} graph 是地图,也就是一个二维数组 + * @param {*} visited 标记访问过的节点,不要重复访问 + * @param {*} x 表示开始搜索节点的下标 + * @param {*} y 表示开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, visited, x, y) => { + for (let i = 0; i < 4; i++) { + const nextx = x + dir[i][0] + const nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if (!visited[nextx][nexty] && graph[nextx][nexty] === 1) { + visited[nextx][nexty] = true + dfs(graph, visited, nextx, nexty) + } + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计岛屿数 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { + // 标记已访问 + visited[i][j] = true + + // 遇到没访问过的陆地,+1 + result++ + + // 深度优先遍历,将相邻陆地标记为已访问 + dfs(graph, visited, i, j) + } + } + } + console.log(result); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala +```scala +import util.control.Breaks._ + +object Solution { + val dir = List((-1,0), (0,-1), (1,0), (0,1)) // 四个方向 + + def dfs(grid: Array[Array[Char]], visited: Array[Array[Boolean]], row: Int, col: Int): Unit = { + (0 until 4).map { x => + val nextR = row + dir(x)(0) + val nextC = col + dir(x)(1) + breakable { + if(nextR < 0 || nextR >= grid.length || nextC < 0 || nextC >= grid(0).length) break + if (!visited(nextR)(nextC) && grid(nextR)(nextC) == '1') { + visited(nextR)(nextC) = true // 经过就记录 + dfs(grid, visited, nextR, nextC) + } + } + } + } + + def numIslands(grid: Array[Array[Char]]): Int = { + val row = grid.length + val col = grid(0).length + var visited = Array.fill(row)(Array.fill(col)(false)) + var counter = 0 + + (0 until row).map{ r => + (0 until col).map{ c => + if (!visited(r)(c) && grid(r)(c) == '1') { + visited(r)(c) = true // 经过就记录 + dfs(grid, visited, r, c) + counter += 1 + } + } + } + + counter + } +} +``` + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0100.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" "b/problems/kamacoder/0100.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" new file mode 100644 index 0000000000..2ae1f452e0 --- /dev/null +++ "b/problems/kamacoder/0100.\345\262\233\345\261\277\347\232\204\346\234\200\345\244\247\351\235\242\347\247\257.md" @@ -0,0 +1,893 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 100. 岛屿的最大面积 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1172) + +题目描述 + +给定一个由 1(陆地)和 0(水)组成的矩阵,计算岛屿的最大面积。岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿由水平方向或垂直方向上相邻的陆地连接而成,并且四周都是水域。你可以假设矩阵外均被水包围。 + +输入描述 + +第一行包含两个整数 N, M,表示矩阵的行数和列数。后续 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。 + +输出描述 + +输出一个整数,表示岛屿的最大面积。如果不存在岛屿,则输出 0。 + +输入示例 + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例 + +4 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240517103410.png) + +样例输入中,岛屿的最大面积为 4。 + +数据范围: + +* 1 <= M, N <= 50。 + + +## 思路 + +注意题目中每座岛屿只能由**水平方向和/或竖直方向上**相邻的陆地连接形成。 + +也就是说斜角度链接是不算了, 例如示例二,是三个岛屿,如图: + +![图一](https://file1.kamacoder.com/i/algo/20220726094200.png) + +这道题目也是 dfs bfs基础类题目,就是搜索每个岛屿上“1”的数量,然后取一个最大的。 + +本题思路上比较简单,难点其实都是 dfs 和 bfs的理论基础,关于理论基础我在这里都有详细讲解 : + +* [DFS理论基础](https://programmercarl.com/kamacoder/图论深搜理论基础.html) +* [BFS理论基础](https://programmercarl.com/kamacoder/图论广搜理论基础.html) + +### DFS + +很多同学写dfs其实也是凭感觉来的,有的时候dfs函数中写终止条件才能过,有的时候 dfs函数不写终止添加也能过! + +这里其实涉及到dfs的两种写法。 + +写法一,dfs处理当前节点的相邻节点,即在主函数遇到岛屿就计数为1,dfs处理接下来的相邻陆地 + +```CPP +// 版本一 +#include +#include +using namespace std; +int count; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y) { + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { // 没有访问过的 同时 是陆地的 + visited[nextx][nexty] = true; + count++; + dfs(grid, visited, nextx, nexty); + } + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + vector> visited(n, vector(m, false)); + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + count = 1; // 因为dfs处理下一个节点,所以这里遇到陆地了就先计数,dfs处理接下来的相邻陆地 + visited[i][j] = true; + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + cout << result << endl; + +} +``` + +写法二,dfs处理当前节点,即在主函数遇到岛屿就计数为0,dfs处理接下来的全部陆地 + +dfs +```CPP +// 版本二 +#include +#include +using namespace std; + +int count; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty); + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + vector> visited = vector>(n, vector(m, false)); + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; // 因为dfs处理当前节点,所以遇到陆地计数为0,进dfs之后在开始从1计数 + dfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + cout << result << endl; +} +``` + +大家通过注释可以发现,两种写法,版本一,在主函数遇到陆地就计数为1,接下来的相邻陆地都在dfs中计算。 + +版本二 在主函数遇到陆地 计数为0,也就是不计数,陆地数量都去dfs里做计算。 + +这也是为什么大家看了很多 dfs的写法 ,发现写法怎么都不一样呢? 其实这就是根本原因。 + + +### BFS + +关于广度优先搜索,如果大家还不了解的话,看这里:[广度优先搜索精讲](./图论广搜理论基础.md) + +本题BFS代码如下: + +```CPP +class Solution { +private: + int count; + int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 + void bfs(vector>& grid, vector>& visited, int x, int y) { + queue que; + que.push(x); + que.push(y); + visited[x][y] = true; // 加入队列就意味节点是陆地可到达的点 + count++; + while(!que.empty()) { + int xx = que.front();que.pop(); + int yy = que.front();que.pop(); + for (int i = 0 ;i < 4; i++) { + int nextx = xx + dir[i][0]; + int nexty = yy + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界 + if (!visited[nextx][nexty] && grid[nextx][nexty] == 1) { // 节点没有被访问过且是陆地 + visited[nextx][nexty] = true; + count++; + que.push(nextx); + que.push(nexty); + } + } + } + } + +public: + int maxAreaOfIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + bfs(grid, visited, i, j); // 将与其链接的陆地都标记上 true + result = max(result, count); + } + } + } + return result; + } +}; + +``` + + +## 其他语言版本 + +### Java + +```java +import java.util.*; +import java.math.*; + +/** + * DFS版 + */ +public class Main{ + + static final int[][] dir={{0,1},{1,0},{0,-1},{-1,0}}; + static int result=0; + static int count=0; + + public static void main(String[] args){ + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] map = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + map[i][j]=scanner.nextInt(); + } + } + boolean[][] visited = new boolean[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if(!visited[i][j]&&map[i][j]==1){ + count=0; + dfs(map,visited,i,j); + result= Math.max(count, result); + } + } + } + System.out.println(result); + } + + static void dfs(int[][] map,boolean[][] visited,int x,int y){ + count++; + visited[x][y]=true; + for (int i = 0; i < 4; i++) { + int nextX=x+dir[i][0]; + int nextY=y+dir[i][1]; + //水或者已经访问过的跳过 + if(nextX<0||nextY<0 + ||nextX>=map.length||nextY>=map[0].length + ||visited[nextX][nextY]||map[nextX][nextY]==0)continue; + + dfs(map,visited,nextX,nextY); + } + } +} +``` + +```java +import java.util.*; +import java.math.*; + +/** + * BFS版 + */ +public class Main { + static class Node { + int x; + int y; + + public Node(int x, int y) { + this.x = x; + this.y = y; + } + } + + static final int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; + static int result = 0; + static int count = 0; + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] map = new int[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + map[i][j] = scanner.nextInt(); + } + } + boolean[][] visited = new boolean[n][m]; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (!visited[i][j] && map[i][j] == 1) { + count = 0; + bfs(map, visited, i, j); + result = Math.max(count, result); + + } + } + } + System.out.println(result); + } + + static void bfs(int[][] map, boolean[][] visited, int x, int y) { + Queue q = new LinkedList<>(); + q.add(new Node(x, y)); + visited[x][y] = true; + count++; + while (!q.isEmpty()) { + Node node = q.remove(); + for (int i = 0; i < 4; i++) { + int nextX = node.x + dir[i][0]; + int nextY = node.y + dir[i][1]; + if (nextX < 0 || nextY < 0 || nextX >= map.length || nextY >= map[0].length || visited[nextX][nextY] || map[nextX][nextY] == 0) + continue; + q.add(new Node(nextX, nextY)); + visited[nextX][nextY] = true; + count++; + } + } + } +} + +``` +### Python + +DFS + +```python +# 四个方向 +position = [[0, 1], [1, 0], [0, -1], [-1, 0]] +count = 0 + + +def dfs(grid, visited, x, y): + """ + 深度优先搜索,对一整块陆地进行标记 + """ + global count # 定义全局变量,便于传递count值 + for i, j in position: + cur_x = x + i + cur_y = y + j + # 下标越界,跳过 + if cur_x < 0 or cur_x >= len(grid) or cur_y < 0 or cur_y >= len(grid[0]): + continue + if not visited[cur_x][cur_y] and grid[cur_x][cur_y] == 1: + visited[cur_x][cur_y] = True + count += 1 + dfs(grid, visited, cur_x, cur_y) + + +n, m = map(int, input().split()) +# 邻接矩阵 +grid = [] +for i in range(n): + grid.append(list(map(int, input().split()))) +# 访问表 +visited = [[False] * m for _ in range(n)] + +result = 0 # 记录最终结果 +for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + count = 1 + visited[i][j] = True + dfs(grid, visited, i, j) + result = max(count, result) + +print(result) +``` + +BFS + +```python +from collections import deque + +position = [[0, 1], [1, 0], [0, -1], [-1, 0]] # 四个方向 +count = 0 + + +def bfs(grid, visited, x, y): + """ + 广度优先搜索对陆地进行标记 + """ + global count # 声明全局变量 + que = deque() + que.append([x, y]) + while que: + cur_x, cur_y = que.popleft() + for i, j in position: + next_x = cur_x + i + next_y = cur_y + j + # 下标越界,跳过 + if next_x < 0 or next_x >= len(grid) or next_y < 0 or next_y >= len(grid[0]): + continue + if grid[next_x][next_y] == 1 and not visited[next_x][next_y]: + visited[next_x][next_y] = True + count += 1 + que.append([next_x, next_y]) + + +n, m = map(int, input().split()) +# 邻接矩阵 +grid = [] +for i in range(n): + grid.append(list(map(int, input().split()))) +visited = [[False] * m for _ in range(n)] # 访问表 + +result = 0 # 记录最终结果 +for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + count = 1 + visited[i][j] = True + bfs(grid, visited, i, j) + res = max(result, count) + +print(result) +``` + +### Go + +``` go + +package main + +import ( + "fmt" +) + +var count int +var dir = [][]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} // 四个方向 + +func dfs(grid [][]int, visited [][]bool, x, y int) { + for i := 0; i < 4; i++ { + nextx := x + dir[i][0] + nexty := y + dir[i][1] + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue // 越界了,直接跳过 + } + if !visited[nextx][nexty] && grid[nextx][nexty] == 1 { // 没有访问过的 同时 是陆地的 + visited[nextx][nexty] = true + count++ + dfs(grid, visited, nextx, nexty) + } + } +} + +func main() { + var n, m int + fmt.Scan(&n, &m) + + grid := make([][]int, n) + for i := 0; i < n; i++ { + grid[i] = make([]int, m) + for j := 0; j < m; j++ { + fmt.Scan(&grid[i][j]) + } + } + + visited := make([][]bool, n) + for i := 0; i < n; i++ { + visited[i] = make([]bool, m) + } + + result := 0 + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if !visited[i][j] && grid[i][j] == 1 { + count = 1 // 因为dfs处理下一个节点,所以这里遇到陆地了就先计数,dfs处理接下来的相邻陆地 + visited[i][j] = true + dfs(grid, visited, i, j) + if count > result { + result = count + } + } + } + } + + fmt.Println(result) +} + + + +``` + + + +### Rust +DFS + +``` rust +use std::io; +use std::cmp; + +// 定义四个方向 +const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + +fn dfs(grid: &Vec>, visited: &mut Vec>, x: usize, y: usize, count: &mut i32) { + if visited[x][y] || grid[x][y] == 0 { + return; // 终止条件:已访问或者遇到海水 + } + visited[x][y] = true; // 标记已访问 + *count += 1; + + for &(dx, dy) in DIRECTIONS.iter() { + let new_x = x as i32 + dx; + let new_y = y as i32 + dy; + + // 检查边界条件 + if new_x >= 0 && new_x < grid.len() as i32 && new_y >= 0 && new_y < grid[0].len() as i32 { + dfs(grid, visited, new_x as usize, new_y as usize, count); + } + } +} + +fn main() { + let mut input = String::new(); + + // 读取 n 和 m + io::stdin().read_line(&mut input); + let dims: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + let (n, m) = (dims[0], dims[1]); + + // 读取 grid + let mut grid = vec![]; + for _ in 0..n { + input.clear(); + io::stdin().read_line(&mut input); + let row: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + grid.push(row); + } + + // 初始化访问记录 + let mut visited = vec![vec![false; m]; n]; + let mut result = 0; + + // 遍历所有格子 + for i in 0..n { + for j in 0..m { + if !visited[i][j] && grid[i][j] == 1 { + let mut count = 0; + dfs(&grid, &mut visited, i, j, &mut count); + result = cmp::max(result, count); + } + } + } + + // 输出结果 + println!("{}", result); +} + +``` +BFS +```rust +use std::io; +use std::collections::VecDeque; + +// 定义四个方向 +const DIRECTIONS: [(i32, i32); 4] = [(0, 1), (1, 0), (-1, 0), (0, -1)]; + +fn bfs(grid: &Vec>, visited: &mut Vec>, x: usize, y: usize) -> i32 { + let mut count = 0; + let mut queue = VecDeque::new(); + queue.push_back((x, y)); + visited[x][y] = true; // 标记已访问 + + while let Some((cur_x, cur_y)) = queue.pop_front() { + count += 1; // 增加计数 + + for &(dx, dy) in DIRECTIONS.iter() { + let new_x = cur_x as i32 + dx; + let new_y = cur_y as i32 + dy; + + // 检查边界条件 + if new_x >= 0 && new_x < grid.len() as i32 && new_y >= 0 && new_y < grid[0].len() as i32 { + let new_x_usize = new_x as usize; + let new_y_usize = new_y as usize; + + // 如果未访问且是陆地,加入队列 + if !visited[new_x_usize][new_y_usize] && grid[new_x_usize][new_y_usize] == 1 { + visited[new_x_usize][new_y_usize] = true; // 标记已访问 + queue.push_back((new_x_usize, new_y_usize)); + } + } + } + } + + count +} + +fn main() { + let mut input = String::new(); + + // 读取 n 和 m + io::stdin().read_line(&mut input).expect("Failed to read line"); + let dims: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + let (n, m) = (dims[0], dims[1]); + + // 读取 grid + let mut grid = vec![]; + for _ in 0..n { + input.clear(); + io::stdin().read_line(&mut input).expect("Failed to read line"); + let row: Vec = input.trim().split_whitespace().map(|s| s.parse().unwrap()).collect(); + grid.push(row); + } + + // 初始化访问记录 + let mut visited = vec![vec![false; m]; n]; + let mut result = 0; + + // 遍历所有格子 + for i in 0..n { + for j in 0..m { + if !visited[i][j] && grid[i][j] == 1 { + let count = bfs(&grid, &mut visited, i, j); + result = result.max(count); + } + } + } + + // 输出结果 + println!("{}", result); +} + +``` + +### JavaScript + +```javascript +// 广搜版 + +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let visited // 访问过的节点 +let result = 0 // 最大岛屿面积 +let count = 0 // 岛屿内节点数 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x, y)开始广度优先遍历 + * @param {*} graph 地图 + * @param {*} visited 访问过的节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, visited, x, y) => { + let queue = [] + queue.push([x, y]) + count++ + visited[x][y] = true //只要加入队列就立刻标记为访问过 + + while (queue.length) { + let [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if(nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if(!visited[nextx][nexty] && graph[nextx][nexty] === 1){ + queue.push([nextx, nexty]) + count++ + visited[nextx][nexty] = true + } + } + } + +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计最大岛屿面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { //遇到没有访问过的陆地 + // 重新计算面积 + count = 0 + + // 广度优先遍历,统计岛屿内节点数,并将岛屿标记为已访问 + bfs(graph, visited, i, j) + + // 更新最大岛屿面积 + result = Math.max(result, count) + } + } + } + console.log(result); +})() +``` + +```javascript + +// 深搜版 + +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let visited // 访问过的节点 +let result = 0 // 最大岛屿面积 +let count = 0 // 岛屿内节点数 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + +/** + * @description: 从(x, y)开始深度优先遍历 + * @param {*} graph 地图 + * @param {*} visited 访问过的节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, visited, x, y) => { + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if(nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if(!visited[nextx][nexty] && graph[nextx][nexty] === 1){ + count++ + visited[nextx][nexty] = true + dfs(graph, visited, nextx, nexty) + } + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 统计最大岛屿面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (!visited[i][j] && graph[i][j] === 1) { //遇到没有访问过的陆地 + // 重新计算面积 + count = 1 + visited[i][j] = true + + // 深度优先遍历,统计岛屿内节点数,并将岛屿标记为已访问 + dfs(graph, visited, i, j) + + // 更新最大岛屿面积 + result = Math.max(result, count) + } + } + } + console.log(result); +})() +``` + +### TypeScript + +### PhP + +``` php + += count($grid) || $nexty < 0 || $nexty >= count($grid[0])) continue; // 越界了,直接跳过 + if (!$visited[$nextx][$nexty] && $grid[$nextx][$nexty] == 1) { // 没有访问过的 同时 是陆地的 + $visited[$nextx][$nexty] = true; + $count++; + dfs($grid, $visited, $nextx, $nexty, $count, $dir); + } + } +} + +// Main function +function main() { + $input = trim(fgets(STDIN)); + list($n, $m) = explode(' ', $input); + + $grid = []; + for ($i = 0; $i < $n; $i++) { + $input = trim(fgets(STDIN)); + $grid[] = array_map('intval', explode(' ', $input)); + } + + $visited = []; + for ($i = 0; $i < $n; $i++) { + $visited[] = array_fill(0, $m, false); + } + + $result = 0; + $count = 0; + $dir = [[0, 1], [1, 0], [-1, 0], [0, -1]]; // 四个方向 + + for ($i = 0; $i < $n; $i++) { + for ($j = 0; $j < $m; $j++) { + if (!$visited[$i][$j] && $grid[$i][$j] == 1) { + $count = 1; // 因为dfs处理下一个节点,所以这里遇到陆地了就先计数,dfs处理接下来的相邻陆地 + $visited[$i][$j] = true; + dfs($grid, $visited, $i, $j, $count, $dir); // 将与其链接的陆地都标记上 true + $result = max($result, $count); + } + } + } + + echo $result . "\n"; +} + +main(); + +?> + + +``` + + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0101.\345\255\244\345\262\233\347\232\204\346\200\273\351\235\242\347\247\257.md" "b/problems/kamacoder/0101.\345\255\244\345\262\233\347\232\204\346\200\273\351\235\242\347\247\257.md" new file mode 100644 index 0000000000..c883100724 --- /dev/null +++ "b/problems/kamacoder/0101.\345\255\244\345\262\233\347\232\204\346\200\273\351\235\242\347\247\257.md" @@ -0,0 +1,703 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 101. 孤岛的总面积 + +[卡码网:101. 孤岛的总面积](https://kamacoder.com/problempage.php?pid=1173) + +题目描述 + +给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。 + + +现在你需要计算所有孤岛的总面积,岛屿面积的计算方式为组成岛屿的陆地的总数。 + +输入描述 + +第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0。 + +输出描述 + +输出一个整数,表示所有孤岛的总面积,如果不存在孤岛,则输出 0。 + +输入示例 + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + + +输出示例: + +1 + +提示信息: + +![](https://file1.kamacoder.com/i/algo/20240517105557.png) + +在矩阵中心部分的岛屿,因为没有任何一个单元格接触到矩阵边缘,所以该岛屿属于孤岛,总面积为 1。 + + +数据范围: + +1 <= M, N <= 50。 + +## 思路 + +本题使用dfs,bfs,并查集都是可以的。 + +本题要求找到不靠边的陆地面积,那么我们只要从周边找到陆地然后 通过 dfs或者bfs 将周边靠陆地且相邻的陆地都变成海洋,然后再去重新遍历地图 统计此时还剩下的陆地就可以了。 + +如图,在遍历地图周围四个边,靠地图四边的陆地,都为绿色, + +![](https://file1.kamacoder.com/i/algo/20220830104632.png) + +在遇到地图周边陆地的时候,将1都变为0,此时地图为这样: + +![](https://file1.kamacoder.com/i/algo/20220830104651.png) + +然后我们再去遍历这个地图,遇到有陆地的地方,去采用深搜或者广搜,边统计所有陆地。 + +如果对深搜或者广搜不够了解,建议先看这里:[深度优先搜索精讲](./图论深搜理论基础.md),[广度优先搜索精讲](./图论广搜理论基础.md)。 + + +采用深度优先搜索的代码如下: + +```CPP +#include +#include +using namespace std; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 +void dfs(vector>& grid, int x, int y) { + grid[x][y] = 0; + for (int i = 0; i < 4; i++) { // 向四个方向遍历 + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + // 超过边界 + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; + // 不符合条件,不继续遍历 + if (grid[nextx][nexty] == 0) continue; + + dfs (grid, nextx, nexty); + } + return; +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) dfs(grid, i, 0); + if (grid[i][m - 1] == 1) dfs(grid, i, m - 1); + } + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) dfs(grid, 0, j); + if (grid[n - 1][j] == 1) dfs(grid, n - 1, j); + } + int count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) count++; + } + } + cout << count << endl; +} +``` + + +采用广度优先搜索的代码如下: + +```CPP +#include +#include +#include +using namespace std; +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void bfs(vector>& grid, int x, int y) { + queue> que; + que.push({x, y}); + grid[x][y] = 0; // 只要加入队列,立刻标记 + while(!que.empty()) { + pair cur = que.front(); que.pop(); + int curx = cur.first; + int cury = cur.second; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + if (grid[nextx][nexty] == 1) { + que.push({nextx, nexty}); + grid[nextx][nexty] = 0; // 只要加入队列立刻标记 + } + } + } +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) bfs(grid, i, 0); + if (grid[i][m - 1] == 1) bfs(grid, i, m - 1); + } + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) bfs(grid, 0, j); + if (grid[n - 1][j] == 1) bfs(grid, n - 1, j); + } + int count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) count++; + } + } + + cout << count << endl; +} + +``` + + +## 其他语言版本 + +### Java + +``` java + +import java.util.*; + +public class Main { + private static int count = 0; + private static final int[][] dir = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}}; // 四个方向 + + private static void bfs(int[][] grid, int x, int y) { + Queue que = new LinkedList<>(); + que.add(new int[]{x, y}); + grid[x][y] = 0; // 只要加入队列,立刻标记 + count++; + while (!que.isEmpty()) { + int[] cur = que.poll(); + int curx = cur[0]; + int cury = cur[1]; + for (int i = 0; i < 4; i++) { + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; + if (nextx < 0 || nextx >= grid.length || nexty < 0 || nexty >= grid[0].length) continue; // 越界了,直接跳过 + if (grid[nextx][nexty] == 1) { + que.add(new int[]{nextx, nexty}); + count++; + grid[nextx][nexty] = 0; // 只要加入队列立刻标记 + } + } + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] grid = new int[n][m]; + + // 读取网格 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + grid[i][j] = scanner.nextInt(); + } + } + + // 从左侧边,和右侧边向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) bfs(grid, i, 0); + if (grid[i][m - 1] == 1) bfs(grid, i, m - 1); + } + + // 从上边和下边向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) bfs(grid, 0, j); + if (grid[n - 1][j] == 1) bfs(grid, n - 1, j); + } + + count = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) bfs(grid, i, j); + } + } + + System.out.println(count); + } +} + + + +``` + + +### Python +#### 深搜版 +```python +position = [[1, 0], [0, 1], [-1, 0], [0, -1]] +count = 0 + +def dfs(grid, x, y): + global count + grid[x][y] = 0 + count += 1 + for i, j in position: + next_x = x + i + next_y = y + j + if next_x < 0 or next_y < 0 or next_x >= len(grid) or next_y >= len(grid[0]): + continue + if grid[next_x][next_y] == 1: + dfs(grid, next_x, next_y) + +n, m = map(int, input().split()) + +# 邻接矩阵 +grid = [] +for i in range(n): + grid.append(list(map(int, input().split()))) + +# 清除边界上的连通分量 +for i in range(n): + if grid[i][0] == 1: + dfs(grid, i, 0) + if grid[i][m - 1] == 1: + dfs(grid, i, m - 1) + +for j in range(m): + if grid[0][j] == 1: + dfs(grid, 0, j) + if grid[n - 1][j] == 1: + dfs(grid, n - 1, j) + +count = 0 # 将count重置为0 +# 统计内部所有剩余的连通分量 +for i in range(n): + for j in range(m): + if grid[i][j] == 1: + dfs(grid, i, j) + +print(count) +``` + +#### 广搜版 +```python +from collections import deque + +# 处理输入 +n, m = list(map(int, input().split())) +g = [] +for _ in range(n): + row = list(map(int, input().split())) + g.append(row) + +# 定义四个方向、孤岛面积(遍历完边缘后会被重置) +directions = [[0,1], [1,0], [-1,0], [0,-1]] +count = 0 + +# 广搜 +def bfs(r, c): + global count + q = deque() + q.append((r, c)) + g[r][c] = 0 + count += 1 + + while q: + r, c = q.popleft() + for di in directions: + next_r = r + di[0] + next_c = c + di[1] + if next_c < 0 or next_c >= m or next_r < 0 or next_r >= n: + continue + if g[next_r][next_c] == 1: + q.append((next_r, next_c)) + g[next_r][next_c] = 0 + count += 1 + + +for i in range(n): + if g[i][0] == 1: + bfs(i, 0) + if g[i][m-1] == 1: + bfs(i, m-1) + +for i in range(m): + if g[0][i] == 1: + bfs(0, i) + if g[n-1][i] == 1: + bfs(n-1, i) + +count = 0 +for i in range(n): + for j in range(m): + if g[i][j] == 1: + bfs(i, j) + +print(count) +``` + +```python +direction = [[1, 0], [-1, 0], [0, 1], [0, -1]] +result = 0 + +# 深度搜尋 +def dfs(grid, y, x): + grid[y][x] = 0 + global result + result += 1 + + for i, j in direction: + next_x = x + j + next_y = y + i + if (next_x < 0 or next_y < 0 or + next_x >= len(grid[0]) or next_y >= len(grid) + ): + continue + if grid[next_y][next_x] == 1 and not visited[next_y][next_x]: + visited[next_y][next_x] = True + dfs(grid, next_y, next_x) + + +# 讀取輸入值 +n, m = map(int, input().split()) +grid = [] +visited = [[False] * m for _ in range(n)] + +for i in range(n): + grid.append(list(map(int, input().split()))) + +# 處理邊界 +for j in range(m): + # 上邊界 + if grid[0][j] == 1 and not visited[0][j]: + visited[0][j] = True + dfs(grid, 0, j) + # 下邊界 + if grid[n - 1][j] == 1 and not visited[n - 1][j]: + visited[n - 1][j] = True + dfs(grid, n - 1, j) + +for i in range(n): + # 左邊界 + if grid[i][0] == 1 and not visited[i][0]: + visited[i][0] = True + dfs(grid, i, 0) + # 右邊界 + if grid[i][m - 1] == 1 and not visited[i][m - 1]: + visited[i][m - 1] = True + dfs(grid, i, m - 1) + +# 計算孤島總面積 +result = 0 # 初始化,避免使用到處理邊界時所產生的累加值 + +for i in range(n): + for j in range(m): + if grid[i][j] == 1 and not visited[i][j]: + visited[i][j] = True + dfs(grid, i, j) + +# 輸出孤島的總面積 +print(result) +``` + +### Go + +``` go + +package main + +import ( + "fmt" +) + +var count int +var dir = [4][2]int{{0, 1}, {1, 0}, {-1, 0}, {0, -1}} // 四个方向 + +func bfs(grid [][]int, x, y int) { + queue := [][2]int{{x, y}} + grid[x][y] = 0 // 只要加入队列,立刻标记 + count++ + + for len(queue) > 0 { + cur := queue[0] + queue = queue[1:] + curx, cury := cur[0], cur[1] + + for i := 0; i < 4; i++ { + nextx := curx + dir[i][0] + nexty := cury + dir[i][1] + + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue // 越界了,直接跳过 + } + + if grid[nextx][nexty] == 1 { + queue = append(queue, [2]int{nextx, nexty}) + count++ + grid[nextx][nexty] = 0 // 只要加入队列立刻标记 + } + } + } +} + +func main() { + var n, m int + fmt.Scan(&n, &m) + + grid := make([][]int, n) + for i := range grid { + grid[i] = make([]int, m) + } + + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + fmt.Scan(&grid[i][j]) + } + } + + // 从左侧边,和右侧边向中间遍历 + for i := 0; i < n; i++ { + if grid[i][0] == 1 { + bfs(grid, i, 0) + } + if grid[i][m-1] == 1 { + bfs(grid, i, m-1) + } + } + + // 从上边和下边向中间遍历 + for j := 0; j < m; j++ { + if grid[0][j] == 1 { + bfs(grid, 0, j) + } + if grid[n-1][j] == 1 { + bfs(grid, n-1, j) + } + } + + // 清空之前的计数 + count = 0 + + // 遍历所有位置 + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if grid[i][j] == 1 { + bfs(grid, i, j) + } + } + } + + fmt.Println(count) +} + + +``` + +### Rust + +### JavaScript + +#### 深搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let count = 0 // 孤岛的总面积 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, x, y) => { + if(graph[x][y] === 0) return + graph[x][y] = 0 // 标记为海洋 + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + dfs(graph, nextx, nexty) + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) dfs(graph, i, 0) + if (graph[i][M - 1] === 1) dfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) dfs(graph, 0, j) + if (graph[N - 1][j] === 1) dfs(graph, N - 1, j) + } + + count = 0 + // 统计孤岛的总面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) count++ + } + } + console.log(count); +})() +``` + + + +#### 广搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let count = 0 // 孤岛的总面积 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, x, y) => { + let queue = [] + queue.push([x, y]) + graph[x][y] = 0 // 只要加入队列,立刻标记 + + while (queue.length) { + let [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if (graph[nextx][nexty] === 1) { + queue.push([nextx, nexty]) + graph[nextx][nexty] = 0 // 只要加入队列,立刻标记 + } + } + } + +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) bfs(graph, i, 0) + if (graph[i][M - 1] === 1) bfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) bfs(graph, 0, j) + if (graph[N - 1][j] === 1) bfs(graph, N - 1, j) + } + + count = 0 + // 统计孤岛的总面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) count++ + } + } + console.log(count); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0102.\346\262\211\346\262\241\345\255\244\345\262\233.md" "b/problems/kamacoder/0102.\346\262\211\346\262\241\345\255\244\345\262\233.md" new file mode 100644 index 0000000000..1b31676277 --- /dev/null +++ "b/problems/kamacoder/0102.\346\262\211\346\262\241\345\255\244\345\262\233.md" @@ -0,0 +1,506 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 102. 沉没孤岛 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1174) + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。 + + +现在你需要将所有孤岛“沉没”,即将孤岛中的所有陆地单元格(1)转变为水域单元格(0)。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。 + +之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。 + +输出描述 + +输出将孤岛“沉没”之后的岛屿矩阵。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例: + +``` +1 1 0 0 0 +1 1 0 0 0 +0 0 0 0 0 +0 0 0 1 1 +``` + +提示信息: + +![](https://file1.kamacoder.com/i/algo/20240517110932.png) + +将孤岛沉没: + +![](https://file1.kamacoder.com/i/algo/20240517110953.png) + +数据范围: + +1 <= M, N <= 50 + +## 思路 + +这道题目和[0101.孤岛的总面积](https://kamacoder.com/problempage.php?pid=1173)正好反过来了,[0101.孤岛的总面积](https://kamacoder.com/problempage.php?pid=1173)是求 地图中间的空格数,而本题是要把地图中间的 1 都改成 0 。 + +那么两题在思路上也是差不多的。 + +思路依然是从地图周边出发,将周边空格相邻的陆地都做上标记,然后在遍历一遍地图,遇到 陆地 且没做过标记的,那么都是地图中间的 陆地 ,全部改成水域就行。 + +有的录友可能想,我再定义一个 visited 二维数组,单独标记周边的陆地,然后遍历地图的时候同时对 地图数组 和 数组visited 进行判断,决定 陆地是否变成水域。 + +这样做其实就有点麻烦了,不用额外定义空间了,标记周边的陆地,可以直接改陆地为其他特殊值作为标记。 + +步骤一:深搜或者广搜将地图周边的 1 (陆地)全部改成 2 (特殊标记) + +步骤二:将水域中间 1 (陆地)全部改成 水域(0) + +步骤三:将之前标记的 2 改为 1 (陆地) + +如图: + +![](https://file1.kamacoder.com/i/algo/20240517113813.png) + +整体C++代码如下,以下使用dfs实现,其实遍历方式dfs,bfs都是可以的。 + +```CPP +#include +#include +using namespace std; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; // 保存四个方向 +void dfs(vector>& grid, int x, int y) { + grid[x][y] = 2; + for (int i = 0; i < 4; i++) { // 向四个方向遍历 + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + // 超过边界 + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; + // 不符合条件,不继续遍历 + if (grid[nextx][nexty] == 0 || grid[nextx][nexty] == 2) continue; + dfs (grid, nextx, nexty); + } + return; +} + +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + + // 步骤一: + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) dfs(grid, i, 0); + if (grid[i][m - 1] == 1) dfs(grid, i, m - 1); + } + + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) dfs(grid, 0, j); + if (grid[n - 1][j] == 1) dfs(grid, n - 1, j); + } + // 步骤二、步骤三 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) grid[i][j] = 0; + if (grid[i][j] == 2) grid[i][j] = 1; + } + } + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cout << grid[i][j] << " "; + } + cout << endl; + } +} +``` + +## 其他语言版本 + +### Java + +```JAVA + +import java.util.Scanner; + +public class Main { + static int[][] dir = { {-1, 0}, {0, -1}, {1, 0}, {0, 1} }; // 保存四个方向 + + public static void dfs(int[][] grid, int x, int y) { + grid[x][y] = 2; + for (int[] d : dir) { + int nextX = x + d[0]; + int nextY = y + d[1]; + // 超过边界 + if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) continue; + // 不符合条件,不继续遍历 + if (grid[nextX][nextY] == 0 || grid[nextX][nextY] == 2) continue; + dfs(grid, nextX, nextY); + } + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + int[][] grid = new int[n][m]; + + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + grid[i][j] = scanner.nextInt(); + } + } + + // 步骤一: + // 从左侧边,和右侧边 向中间遍历 + for (int i = 0; i < n; i++) { + if (grid[i][0] == 1) dfs(grid, i, 0); + if (grid[i][m - 1] == 1) dfs(grid, i, m - 1); + } + + // 从上边和下边 向中间遍历 + for (int j = 0; j < m; j++) { + if (grid[0][j] == 1) dfs(grid, 0, j); + if (grid[n - 1][j] == 1) dfs(grid, n - 1, j); + } + + // 步骤二、步骤三 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) grid[i][j] = 0; + if (grid[i][j] == 2) grid[i][j] = 1; + } + } + + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + System.out.print(grid[i][j] + " "); + } + System.out.println(); + } + + scanner.close(); + } +} + + +``` + + +### Python + + +```python + +def dfs(grid, x, y): + grid[x][y] = 2 + directions = [(-1, 0), (0, -1), (1, 0), (0, 1)] # 四个方向 + for dx, dy in directions: + nextx, nexty = x + dx, y + dy + # 超过边界 + if nextx < 0 or nextx >= len(grid) or nexty < 0 or nexty >= len(grid[0]): + continue + # 不符合条件,不继续遍历 + if grid[nextx][nexty] == 0 or grid[nextx][nexty] == 2: + continue + dfs(grid, nextx, nexty) + +def main(): + n, m = map(int, input().split()) + grid = [[int(x) for x in input().split()] for _ in range(n)] + + # 步骤一: + # 从左侧边,和右侧边 向中间遍历 + for i in range(n): + if grid[i][0] == 1: + dfs(grid, i, 0) + if grid[i][m - 1] == 1: + dfs(grid, i, m - 1) + + # 从上边和下边 向中间遍历 + for j in range(m): + if grid[0][j] == 1: + dfs(grid, 0, j) + if grid[n - 1][j] == 1: + dfs(grid, n - 1, j) + + # 步骤二、步骤三 + for i in range(n): + for j in range(m): + if grid[i][j] == 1: + grid[i][j] = 0 + if grid[i][j] == 2: + grid[i][j] = 1 + + # 打印结果 + for row in grid: + print(' '.join(map(str, row))) + +if __name__ == "__main__": + main() +``` + + + 广搜版 +```Python +from collections import deque + +n, m = list(map(int, input().split())) +g = [] +for _ in range(n): + row = list(map(int,input().split())) + g.append(row) + +directions = [(1,0),(-1,0),(0,1),(0,-1)] +count = 0 + +def bfs(r,c,mode): + global count + q = deque() + q.append((r,c)) + count += 1 + + while q: + r, c = q.popleft() + if mode: + g[r][c] = 2 + + for di in directions: + next_r = r + di[0] + next_c = c + di[1] + if next_c < 0 or next_c >= m or next_r < 0 or next_r >= n: + continue + if g[next_r][next_c] == 1: + q.append((next_r,next_c)) + if mode: + g[r][c] = 2 + + count += 1 + + +for i in range(n): + if g[i][0] == 1: bfs(i,0,True) + if g[i][m-1] == 1: bfs(i, m-1,True) + +for j in range(m): + if g[0][j] == 1: bfs(0,j,1) + if g[n-1][j] == 1: bfs(n-1,j,1) + +for i in range(n): + for j in range(m): + if g[i][j] == 2: + g[i][j] = 1 + else: + g[i][j] = 0 + +for row in g: + print(" ".join(map(str, row))) + +``` + + +### Go + +### Rust + +### JavaScript + +#### 深搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, x, y) => { + if (graph[x][y] !== 1) return + graph[x][y] = 2 // 标记为非孤岛陆地 + + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + dfs(graph, nextx, nexty) + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) dfs(graph, i, 0) + if (graph[i][M - 1] === 1) dfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) dfs(graph, 0, j) + if (graph[N - 1][j] === 1) dfs(graph, N - 1, j) + } + + + // 遍历地图,将孤岛沉没,即将陆地1标记为0;将非孤岛陆地2标记为1 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) graph[i][j] = 0 + else if (graph[i][j] === 2) graph[i][j] = 1 + } + console.log(graph[i].join(' ')); + } +})() +``` + + + +#### 广搜版 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, x, y) => { + let queue = [] + queue.push([x, y]) + graph[x][y] = 2 // 标记为非孤岛陆地 + + while (queue.length) { + let [xx, yy] = queue.shift() + + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue + if (graph[nextx][nexty] === 1) bfs(graph, nextx, nexty) + } + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图左右两边 + for (let i = 0; i < N; i++) { + if (graph[i][0] === 1) bfs(graph, i, 0) + if (graph[i][M - 1] === 1) bfs(graph, i, M - 1) + } + + // 遍历地图上下两边 + for (let j = 0; j < M; j++) { + if (graph[0][j] === 1) bfs(graph, 0, j) + if (graph[N - 1][j] === 1) bfs(graph, N - 1, j) + } + + + // 遍历地图,将孤岛沉没,即将陆地1标记为0;将非孤岛陆地2标记为1 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] === 1) graph[i][j] = 0 + else if (graph[i][j] === 2) graph[i][j] = 1 + } + console.log(graph[i].join(' ')); + } +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0103.\346\260\264\346\265\201\351\227\256\351\242\230.md" "b/problems/kamacoder/0103.\346\260\264\346\265\201\351\227\256\351\242\230.md" new file mode 100644 index 0000000000..bf6cd40f43 --- /dev/null +++ "b/problems/kamacoder/0103.\346\260\264\346\265\201\351\227\256\351\242\230.md" @@ -0,0 +1,857 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 103. 水流问题 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1175) + +题目描述: + +现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。 + + +矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。 + +输入描述: + +第一行包含两个整数 N 和 M,分别表示矩阵的行数和列数。 + +后续 N 行,每行包含 M 个整数,表示矩阵中的每个单元格的高度。 + +输出描述: + +输出共有多行,每行输出两个整数,用一个空格隔开,表示可达第一组边界和第二组边界的单元格的坐标,输出顺序任意。 + +输入示例: + +``` +5 5 +1 3 1 2 4 +1 2 1 3 2 +2 4 7 2 1 +4 5 6 1 1 +1 4 1 2 1 +``` + +输出示例: + +``` +0 4 +1 3 +2 2 +3 0 +3 1 +3 2 +4 0 +4 1 +``` + +提示信息: + +![](https://file1.kamacoder.com/i/algo/20240517115816.png) + +图中的蓝色方块上的雨水既能流向第一组边界,也能流向第二组边界。所以最终答案为所有蓝色方块的坐标。 + + +数据范围: + +1 <= M, N <= 50 + +## 思路 + +一个比较直白的想法,其实就是 遍历每个点,然后看这个点 能不能同时到达第一组边界和第二组边界。 + +至于遍历方式,可以用dfs,也可以用bfs,以下用dfs来举例。 + +那么这种思路的实现代码如下: + +```CPP +#include +#include +using namespace std; +int n, m; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; + +// 从 x,y 出发 把可以走的地方都标记上 +void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y]) return; + + visited[x][y] = true; + + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; + if (grid[x][y] < grid[nextx][nexty]) continue; // 高度不合适 + + dfs (grid, visited, nextx, nexty); + } + return; +} +bool isResult(vector>& grid, int x, int y) { + vector> visited(n, vector(m, false)); + + // 深搜,将x,y出发 能到的节点都标记上。 + dfs(grid, visited, x, y); + bool isFirst = false; + bool isSecond = false; + + // 以下就是判断x,y出发,是否到达第一组边界和第二组边界 + // 第一边界的上边 + for (int j = 0; j < m; j++) { + if (visited[0][j]) { + isFirst = true; + break; + } + } + // 第一边界的左边 + for (int i = 0; i < n; i++) { + if (visited[i][0]) { + isFirst = true; + break; + } + } + // 第二边界下边 + for (int j = 0; j < m; j++) { + if (visited[n - 1][j]) { + isSecond = true; + break; + } + } + // 第二边界右边 + for (int i = 0; i < n; i++) { + if (visited[i][m - 1]) { + isSecond = true; + break; + } + } + if (isFirst && isSecond) return true; + return false; +} + + +int main() { + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + // 遍历每一个点,看是否能同时到达第一组边界和第二组边界 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (isResult(grid, i, j)) { + cout << i << " " << j << endl; + } + } + } +} + +``` + +这种思路很直白,但很明显,以上代码超时了。 来看看时间复杂度。 + +遍历每一个节点,是 m * n,遍历每一个节点的时候,都要做深搜,深搜的时间复杂度是: m * n + +那么整体时间复杂度 就是 O(m^2 * n^2) ,这是一个四次方的时间复杂度。 + +## 优化 + +那么我们可以 反过来想,从第一组边界上的节点 逆流而上,将遍历过的节点都标记上。 + +同样从第二组边界的边上节点 逆流而上,将遍历过的节点也标记上。 + +然后**两方都标记过的节点就是既可以流向第一组边界也可以流向第二组边界的节点**。 + +从第一组边界边上节点出发,如图: (图中并没有把所有遍历的方向都画出来,只画关键部分) + +![](https://file1.kamacoder.com/i/algo/20250304174747.png) + +从第二组边界上节点出发,如图: (图中并没有把所有遍历的方向都画出来,只画关键部分) + +![](https://file1.kamacoder.com/i/algo/20250304174801.png) + +最后,我们得到两个方向交界的这些节点,就是我们最后要求的节点。 + +按照这样的逻辑,就可以写出如下遍历代码:(详细注释) + + +```CPP +#include +#include +using namespace std; +int n, m; +int dir[4][2] = {-1, 0, 0, -1, 1, 0, 0, 1}; +void dfs(vector>& grid, vector>& visited, int x, int y) { + if (visited[x][y]) return; + + visited[x][y] = true; + + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; + if (grid[x][y] > grid[nextx][nexty]) continue; // 注意:这里是从低向高遍历 + + dfs (grid, visited, nextx, nexty); + } + return; +} + + + +int main() { + + cin >> n >> m; + vector> grid(n, vector(m, 0)); + + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + // 标记从第一组边界上的节点出发,可以遍历的节点 + vector> firstBorder(n, vector(m, false)); + + // 标记从第一组边界上的节点出发,可以遍历的节点 + vector> secondBorder(n, vector(m, false)); + + // 从最上和最下行的节点出发,向高处遍历 + for (int i = 0; i < n; i++) { + dfs (grid, firstBorder, i, 0); // 遍历最左列,接触第一组边界 + dfs (grid, secondBorder, i, m - 1); // 遍历最右列,接触第二组边界 + } + + // 从最左和最右列的节点出发,向高处遍历 + for (int j = 0; j < m; j++) { + dfs (grid, firstBorder, 0, j); // 遍历最上行,接触第一组边界 + dfs (grid, secondBorder, n - 1, j); // 遍历最下行,接触第二组边界 + } + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + // 如果这个节点,从第一组边界和第二组边界出发都遍历过,就是结果 + if (firstBorder[i][j] && secondBorder[i][j]) cout << i << " " << j << endl;; + } + } + + +} + +``` + + +时间复杂度分析, 关于dfs函数搜索的过程 时间复杂度是 O(n * m),这个大家比较容易想。 + +关键看主函数,那么每次dfs的时候,上面还是有for循环的。 + +第一个for循环,时间复杂度是:n * (n * m) 。 + +第二个for循环,时间复杂度是:m * (n * m)。 + +所以本题看起来 时间复杂度好像是 : n * (n * m) + m * (n * m) = (m * n) * (m + n) 。 + +其实这是一个误区,大家再自己看 dfs函数的实现,其实 有visited函数记录 走过的节点,而走过的节点是不会再走第二次的。 + +所以 调用dfs函数,**只要参数传入的是 数组 firstBorder,那么地图中 每一个节点其实就遍历一次,无论你调用多少次**。 + +同理,调用dfs函数,只要 参数传入的是 数组 secondBorder,地图中每个节点也只会遍历一次。 + +所以,以下这段代码的时间复杂度是 2 * n * m。 地图用每个节点就遍历了两次,参数传入 firstBorder 的时候遍历一次,参数传入 secondBorder 的时候遍历一次。 + +```CPP +// 从最上和最下行的节点出发,向高处遍历 +for (int i = 0; i < n; i++) { + dfs (grid, firstBorder, i, 0); // 遍历最左列,接触第一组边界 + dfs (grid, secondBorder, i, m - 1); // 遍历最右列,接触第二组边界 +} + +// 从最左和最右列的节点出发,向高处遍历 +for (int j = 0; j < m; j++) { + dfs (grid, firstBorder, 0, j); // 遍历最上行,接触第一组边界 + dfs (grid, secondBorder, n - 1, j); // 遍历最下行,接触第二组边界 +} +``` + +那么本题整体的时间复杂度其实是: 2 * n * m + n * m ,所以最终时间复杂度为 O(n * m) 。 + +空间复杂度为:O(n * m) 这个就不难理解了。开了几个 n * m 的数组。 + + + +## 其他语言版本 + +### Java +```Java +public class Main { + + // 采用 DFS 进行搜索 + public static void dfs(int[][] heights, int x, int y, boolean[][] visited, int preH) { + // 遇到边界或者访问过的点,直接返回 + if (x < 0 || x >= heights.length || y < 0 || y >= heights[0].length || visited[x][y]) return; + // 不满足水流入条件的直接返回 + if (heights[x][y] < preH) return; + // 满足条件,设置为true,表示可以从边界到达此位置 + visited[x][y] = true; + + // 向下一层继续搜索 + dfs(heights, x + 1, y, visited, heights[x][y]); + dfs(heights, x - 1, y, visited, heights[x][y]); + dfs(heights, x, y + 1, visited, heights[x][y]); + dfs(heights, x, y - 1, visited, heights[x][y]); + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int m = sc.nextInt(); + int n = sc.nextInt(); + + int[][] heights = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + heights[i][j] = sc.nextInt(); + } + } + + // 初始化两个二位boolean数组,代表两个边界 + boolean[][] pacific = new boolean[m][n]; + boolean[][] atlantic = new boolean[m][n]; + + // 从左右边界出发进行DFS + for (int i = 0; i < m; i++) { + dfs(heights, i, 0, pacific, Integer.MIN_VALUE); + dfs(heights, i, n - 1, atlantic, Integer.MIN_VALUE); + } + + // 从上下边界出发进行DFS + for (int j = 0; j < n; j++) { + dfs(heights, 0, j, pacific, Integer.MIN_VALUE); + dfs(heights, m - 1, j, atlantic, Integer.MIN_VALUE); + } + + // 当两个边界二维数组在某个位置都为true时,符合题目要求 + List> res = new ArrayList<>(); + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (pacific[i][j] && atlantic[i][j]) { + res.add(Arrays.asList(i, j)); + } + } + } + + // 打印结果 + for (List list : res) { + for (int k = 0; k < list.size(); k++) { + if (k == 0) { + System.out.print(list.get(k) + " "); + } else { + System.out.print(list.get(k)); + } + } + System.out.println(); + } + } +} + +``` + +### Python +```Python +first = set() +second = set() +directions = [[-1, 0], [0, 1], [1, 0], [0, -1]] + +def dfs(i, j, graph, visited, side): + if visited[i][j]: + return + + visited[i][j] = True + side.add((i, j)) + + for x, y in directions: + new_x = i + x + new_y = j + y + if ( + 0 <= new_x < len(graph) + and 0 <= new_y < len(graph[0]) + and int(graph[new_x][new_y]) >= int(graph[i][j]) + ): + dfs(new_x, new_y, graph, visited, side) + +def main(): + global first + global second + + N, M = map(int, input().strip().split()) + graph = [] + for _ in range(N): + row = input().strip().split() + graph.append(row) + + # 是否可到达第一边界 + visited = [[False] * M for _ in range(N)] + for i in range(M): + dfs(0, i, graph, visited, first) + for i in range(N): + dfs(i, 0, graph, visited, first) + + # 是否可到达第二边界 + visited = [[False] * M for _ in range(N)] + for i in range(M): + dfs(N - 1, i, graph, visited, second) + for i in range(N): + dfs(i, M - 1, graph, visited, second) + + # 可到达第一边界和第二边界 + res = first & second + + for x, y in res: + print(f"{x} {y}") + + +if __name__ == "__main__": + main() +``` + +### Go +```go +package main + +import ( + "os" + "fmt" + "strings" + "strconv" + "bufio" +) + +var directions = [][]int{{0, -1}, {0, 1}, {-1, 0}, {1, 0}} // 四个方向的偏移量 + +func main() { + scanner := bufio.NewScanner(os.Stdin) + + scanner.Scan() + lineList := strings.Fields(scanner.Text()) + N, _ := strconv.Atoi(lineList[0]) + M, _ := strconv.Atoi(lineList[1]) + + grid := make([][]int, N) + visited := make([][]bool, N) // 用于标记是否访问过 + for i := 0; i < N; i++ { + grid[i] = make([]int, M) + visited[i] = make([]bool, M) + scanner.Scan() + lineList = strings.Fields(scanner.Text()) + + for j := 0; j < M; j++ { + grid[i][j], _ = strconv.Atoi(lineList[j]) + } + } + + // 遍历每个单元格,使用DFS检查是否可达两组边界 + for i := 0; i < N; i++ { + for j := 0; j < M; j++ { + canReachFirst, canReachSecond := dfs(grid, visited, i, j) + if canReachFirst && canReachSecond { + fmt.Println(strconv.Itoa(i) + " " + strconv.Itoa(j)) + } + } + } +} + +func dfs(grid [][]int, visited [][]bool, startx int, starty int) (bool, bool) { + visited[startx][starty] = true + canReachFirst := startx == 0 || starty == 0 || startx == len(grid)-1 || starty == len(grid[0])-1 + canReachSecond := startx == len(grid)-1 || starty == len(grid[0])-1 || startx == 0 || starty == 0 + + if canReachFirst && canReachSecond { + return true, true + } + + for _, direction := range directions { + nextx := startx + direction[0] + nexty := starty + direction[1] + + if nextx < 0 || nextx >= len(grid) || nexty < 0 || nexty >= len(grid[0]) { + continue + } + + if grid[nextx][nexty] <= grid[startx][starty] && !visited[nextx][nexty] { + hasReachFirst, hasReachSecond := dfs(grid, visited, nextx, nexty) + if !canReachFirst { + canReachFirst = hasReachFirst + } + if !canReachSecond { + canReachSecond = hasReachSecond + } + } + } + return canReachFirst, canReachSecond +} +``` + +### Rust + +### JavaScript + +#### 深搜 + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const dfs = (graph, visited, x, y) => { + if (visited[x][y]) return + visited[x][y] = true // 标记为可访问 + + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界,跳过 + if (graph[x][y] < graph[nextx][nexty]) continue //不能流过.跳过 + dfs(graph, visited, nextx, nexty) + } +} + + +/** + * @description: 判断地图上的(x, y)是否可以到达第一组边界和第二组边界 + * @param {*} x 坐标 + * @param {*} y 坐标 + * @return {*} true可以到达,false不可以到达 + */ +const isResult = (x, y) => { + let visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + let isFirst = false //是否可到达第一边界 + let isSecond = false //是否可到达第二边界 + + // 深搜,将(x, y)可到达的所有节点做标记 + dfs(graph, visited, x, y) + + // 判断能否到第一边界左边 + for (let i = 0; i < N; i++) { + if (visited[i][0]) { + isFirst = true + break + } + } + + // 判断能否到第一边界上边 + for (let j = 0; j < M; j++) { + if (visited[0][j]) { + isFirst = true + break + } + } + + // 判断能否到第二边界右边 + for (let i = 0; i < N; i++) { + if (visited[i][M - 1]) { + isSecond = true + break + } + } + + // 判断能否到第二边界下边 + for (let j = 0; j < M; j++) { + if (visited[N - 1][j]) { + isSecond = true + break + } + } + + return isFirst && isSecond +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图,判断是否能到达第一组边界和第二组边界 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (isResult(i, j)) console.log(i + ' ' + j); + } + } +})() +``` + + + +#### 广搜-解法一 + +```java +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + + +/** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ +const bfs = (graph, visited, x, y) => { + let queue = [] + queue.push([x, y]) + visited[x][y] = true + + while (queue.length) { + const [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + // 可访问或者不能流过, 跳过 (注意这里是graph[xx][yy] < graph[nextx][nexty], 不是graph[x][y] < graph[nextx][nexty]) + if (visited[nextx][nexty] || graph[xx][yy] < graph[nextx][nexty]) continue + + queue.push([nextx, nexty]) + visited[nextx][nexty] = true + + } + } +} + + +/** + * @description: 判断地图上的(x, y)是否可以到达第一组边界和第二组边界 + * @param {*} x 坐标 + * @param {*} y 坐标 + * @return {*} true可以到达,false不可以到达 + */ +const isResult = (x, y) => { + let visited = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + let isFirst = false //是否可到达第一边界 + let isSecond = false //是否可到达第二边界 + + // 深搜,将(x, y)可到达的所有节点做标记 + bfs(graph, visited, x, y) + + // console.log(visited); + + // 判断能否到第一边界左边 + for (let i = 0; i < N; i++) { + if (visited[i][0]) { + isFirst = true + break + } + } + + // 判断能否到第一边界上边 + for (let j = 0; j < M; j++) { + if (visited[0][j]) { + isFirst = true + break + } + } + + // 判断能否到第二边界右边 + for (let i = 0; i < N; i++) { + if (visited[i][M - 1]) { + isSecond = true + break + } + } + + // 判断能否到第二边界下边 + for (let j = 0; j < M; j++) { + if (visited[N - 1][j]) { + isSecond = true + break + } + } + + return isFirst && isSecond +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 遍历地图,判断是否能到达第一组边界和第二组边界 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (isResult(i, j)) console.log(i + ' ' + j); + } + } +})() +``` + + + +#### 广搜-解法二 + +从第一边界和第二边界开始向高处流, 标记可以流到的位置, 两个边界都能到达的位置就是所求结果 + +```javascript + const r1 = require('readline').createInterface({ input: process.stdin }); + // 创建readline接口 + let iter = r1[Symbol.asyncIterator](); + // 创建异步迭代器 + const readline = async () => (await iter.next()).value; + + let graph // 地图 + let N, M // 地图大小 + const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + + + // 读取输入,初始化地图 + const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } + } + + + /** + * @description: 从(x,y)开始广度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @return {*} + */ + const bfs = (graph, visited, x, y) => { + if(visited[x][y]) return + + let queue = [] + queue.push([x, y]) + visited[x][y] = true + + while (queue.length) { + const [xx, yy] = queue.shift() + for (let i = 0; i < 4; i++) { + let nextx = xx + dir[i][0] + let nexty = yy + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + // 可访问或者不能流过, 跳过 (注意因为是从边界往高处流, 所以这里是graph[xx][yy] >= graph[nextx][nexty], 还要注意不是graph[xx][yy] >= graph[nextx][nexty]) + if (visited[nextx][nexty] || graph[xx][yy] >= graph[nextx][nexty]) continue + + queue.push([nextx, nexty]) + visited[nextx][nexty] = true + } + } + } + + (async function () { + + // 读取输入,初始化地图 + await initGraph() + + // 记录第一边界可到达的节点 + let firstBorder = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + // 记录第二边界可到达的节点 + let secondBorder = new Array(N).fill(false).map(() => new Array(M).fill(false)) + + // 第一边界左边和第二边界右边 + for (let i = 0; i < N; i++) { + bfs(graph, firstBorder, i, 0) + bfs(graph, secondBorder, i, M - 1) + } + + // 第一边界上边和第二边界下边 + for (let j = 0; j < M; j++) { + bfs(graph, firstBorder, 0, j) + bfs(graph, secondBorder, N - 1, j) + } + + // 遍历地图,判断是否能到达第一组边界和第二组边界 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (firstBorder[i][j] && secondBorder[i][j]) console.log(i + ' ' + j); + } + } + })() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + + + +
diff --git "a/problems/kamacoder/0104.\345\273\272\351\200\240\346\234\200\345\244\247\345\262\233\345\261\277.md" "b/problems/kamacoder/0104.\345\273\272\351\200\240\346\234\200\345\244\247\345\262\233\345\261\277.md" new file mode 100644 index 0000000000..8c4964a9e4 --- /dev/null +++ "b/problems/kamacoder/0104.\345\273\272\351\200\240\346\234\200\345\244\247\345\262\233\345\261\277.md" @@ -0,0 +1,666 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 104.建造最大岛屿 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1176) + +题目描述: + +给定一个由 1(陆地)和 0(水)组成的矩阵,你最多可以将矩阵中的一格水变为一块陆地,在执行了此操作之后,矩阵中最大的岛屿面积是多少。 + +岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设矩阵外均被水包围。 + +输入描述: + +第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。 + +输出描述: + +输出一个整数,表示最大的岛屿面积。 + +输入示例: + +``` +4 5 +1 1 0 0 0 +1 1 0 0 0 +0 0 1 0 0 +0 0 0 1 1 +``` + +输出示例 + +6 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240522154055.png) + + +对于上面的案例,有两个位置可将 0 变成 1,使得岛屿的面积最大,即 6。 + +![](https://file1.kamacoder.com/i/algo/20240522154110.png) + + +数据范围: + +1 <= M, N <= 50。 + + +## 思路 + +本题的一个暴力想法,应该是遍历地图尝试 将每一个 0 改成1,然后去搜索地图中的最大的岛屿面积。 + +计算地图的最大面积:遍历地图 + 深搜岛屿,时间复杂度为 n * n。 + +(其实使用深搜还是广搜都是可以的,其目的就是遍历岛屿做一个标记,相当于染色,那么使用哪个遍历方式都行,以下我用深搜来讲解) + +每改变一个0的方格,都需要重新计算一个地图的最大面积,所以 整体时间复杂度为:n^4。 + +## 优化思路 + +其实每次深搜遍历计算最大岛屿面积,我们都做了很多重复的工作。 + +只要用一次深搜把每个岛屿的面积记录下来就好。 + +第一步:一次遍历地图,得出各个岛屿的面积,并做编号记录。可以使用map记录,key为岛屿编号,value为岛屿面积 + +第二步:再遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积。 + +拿如下地图的岛屿情况来举例: (1为陆地) + +![](https://file1.kamacoder.com/i/algo/20220829104834.png) + +第一步,则遍历地图,并将岛屿的编号和面积都统计好,过程如图所示: + +![](https://file1.kamacoder.com/i/algo/20220829105644.png) + + +本过程代码如下: + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y, int mark) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty, mark); + } +} + +int largestIsland(vector>& grid) { + int n = grid.size(), m = grid[0].size(); + vector> visited = vector>(n, vector(m, false)); // 标记访问过的点 + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + dfs(grid, visited, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } +} +``` + + +这个过程时间复杂度 n * n 。可能有录友想:分明是两个for循环下面套这一个dfs,时间复杂度怎么回事 n * n呢? + +其实大家可以仔细看一下代码,**n * n这个方格地图中,每个节点我们就遍历一次,并不会重复遍历**。 + +第二步过程如图所示: + +![](https://file1.kamacoder.com/i/algo/20220829105249.png) + +也就是遍历每一个0的方格,并统计其相邻岛屿面积,最后取一个最大值。 + +这个过程的时间复杂度也为 n * n。 + +所以整个解法的时间复杂度,为 n * n + n * n 也就是 n^2。 + +当然这里还有一个优化的点,就是 可以不用 visited数组,因为有mark来标记,所以遍历过的grid[i][j]是不等于1的。 + +代码如下: + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, int x, int y, int mark) { + if (grid[x][y] != 1 || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; // 越界了,直接跳过 + dfs(grid, nextx, nexty, mark); + } +} + +int main() { + cin >> n >> m; + vector> grid(n, vector(m, 0)); + + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (grid[i][j] == 1) { + count = 0; + dfs(grid, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } +``` + +不过为了让各个变量各司其事,代码清晰一些,完整代码还是使用visited数组来标记。 + +最后,整体代码如下: + +```CPP +#include +#include +#include +#include +using namespace std; +int n, m; +int count; + +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 四个方向 +void dfs(vector>& grid, vector>& visited, int x, int y, int mark) { + if (visited[x][y] || grid[x][y] == 0) return; // 终止条件:访问过的节点 或者 遇到海水 + visited[x][y] = true; // 标记访问过 + grid[x][y] = mark; // 给陆地标记新标签 + count++; + for (int i = 0; i < 4; i++) { + int nextx = x + dir[i][0]; + int nexty = y + dir[i][1]; + if (nextx < 0 || nextx >= n || nexty < 0 || nexty >= m) continue; // 越界了,直接跳过 + dfs(grid, visited, nextx, nexty, mark); + } +} + +int main() { + cin >> n >> m; + vector> grid(n, vector(m, 0)); + + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + vector> visited(n, vector(m, false)); // 标记访问过的点 + unordered_map gridNum; + int mark = 2; // 记录每个岛屿的编号 + bool isAllGrid = true; // 标记是否整个地图都是陆地 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 0) isAllGrid = false; + if (!visited[i][j] && grid[i][j] == 1) { + count = 0; + dfs(grid, visited, i, j, mark); // 将与其链接的陆地都标记上 true + gridNum[mark] = count; // 记录每一个岛屿的面积 + mark++; // 记录下一个岛屿编号 + } + } + } + if (isAllGrid) { + cout << n * m << endl; // 如果都是陆地,返回全面积 + return 0; // 结束程序 + } + + // 以下逻辑是根据添加陆地的位置,计算周边岛屿面积之和 + int result = 0; // 记录最后结果 + unordered_set visitedGrid; // 标记访问过的岛屿 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + count = 1; // 记录连接之后的岛屿数量 + visitedGrid.clear(); // 每次使用时,清空 + if (grid[i][j] == 0) { + for (int k = 0; k < 4; k++) { + int neari = i + dir[k][1]; // 计算相邻坐标 + int nearj = j + dir[k][0]; + if (neari < 0 || neari >= n || nearj < 0 || nearj >= m) continue; + if (visitedGrid.count(grid[neari][nearj])) continue; // 添加过的岛屿不要重复添加 + // 把相邻四面的岛屿数量加起来 + count += gridNum[grid[neari][nearj]]; + visitedGrid.insert(grid[neari][nearj]); // 标记该岛屿已经添加过 + } + } + result = max(result, count); + } + } + cout << result << endl; + +} +``` + + +## 其他语言版本 + +### Java +```Java +public class Main { + // 该方法采用 DFS + // 定义全局变量 + // 记录每次每个岛屿的面积 + static int count; + // 对每个岛屿进行标记 + static int mark; + // 定义二维数组表示四个方位 + static int[][] dirs = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}}; + + // DFS 进行搜索,将每个岛屿标记为不同的数字 + public static void dfs(int[][] grid, int x, int y, boolean[][] visited) { + // 当遇到边界,直接return + if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length) return; + // 遇到已经访问过的或者遇到海水,直接返回 + if (visited[x][y] || grid[x][y] == 0) return; + + visited[x][y] = true; + count++; + grid[x][y] = mark; + + // 继续向下层搜索 + dfs(grid, x, y + 1, visited); + dfs(grid, x, y - 1, visited); + dfs(grid, x + 1, y, visited); + dfs(grid, x - 1, y, visited); + } + + public static void main (String[] args) { + // 接收输入 + Scanner sc = new Scanner(System.in); + int m = sc.nextInt(); + int n = sc.nextInt(); + + int[][] grid = new int[m][n]; + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + grid[i][j] = sc.nextInt(); + } + } + + // 初始化mark变量,从2开始(区别于0水,1岛屿) + mark = 2; + + // 定义二位boolean数组记录该位置是否被访问 + boolean[][] visited = new boolean[m][n]; + + // 定义一个HashMap,记录某片岛屿的标记号和面积 + HashMap getSize = new HashMap<>(); + + // 定义一个HashSet,用来判断某一位置水四周是否存在不同标记编号的岛屿 + HashSet set = new HashSet<>(); + + // 定义一个boolean变量,看看DFS之后,是否全是岛屿 + boolean isAllIsland = true; + + // 遍历二维数组进行DFS搜索,标记每片岛屿的编号,记录对应的面积 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) isAllIsland = false; + if (grid[i][j] == 1) { + count = 0; + dfs(grid, i, j, visited); + getSize.put(mark, count); + mark++; + } + } + } + + int result = 0; + if (isAllIsland) result = m * n; + + // 对标记完的grid继续遍历,判断每个水位置四周是否有岛屿,并记录下四周不同相邻岛屿面积之和 + // 每次计算完一个水位置周围可能存在的岛屿面积之和,更新下result变量 + for (int i = 0; i < m; i++) { + for (int j = 0; j < n; j++) { + if (grid[i][j] == 0) { + set.clear(); + // 当前水位置变更为岛屿,所以初始化为1 + int curSize = 1; + + for (int[] dir : dirs) { + int curRow = i + dir[0]; + int curCol = j + dir[1]; + + if (curRow < 0 || curRow >= m || curCol < 0 || curCol >= n) continue; + int curMark = grid[curRow][curCol]; + // 如果当前相邻的岛屿已经遍历过或者HashMap中不存在这个编号,继续搜索 + if (set.contains(curMark) || !getSize.containsKey(curMark)) continue; + set.add(curMark); + curSize += getSize.get(curMark); + } + + result = Math.max(result, curSize); + } + } + } + + // 打印结果 + System.out.println(result); + } +} + +``` + +### Python + + +#### BFS +```Python +from typing import List +from collections import defaultdict + +class Solution: + def __init__(self): + self.direction = [(1,0),(-1,0),(0,1),(0,-1)] + self.res = 0 + self.count = 0 + self.idx = 1 + self.count_area = defaultdict(int) + + def max_area_island(self, grid: List[List[int]]) -> int: + if not grid or len(grid) == 0 or len(grid[0]) == 0: + return 0 + + for i in range(len(grid)): + for j in range(len(grid[0])): + if grid[i][j] == 1: + self.count = 0 + self.idx += 1 + self.dfs(grid,i,j) + # print(grid) + self.check_area(grid) + # print(self.count_area) + + if self.check_largest_connect_island(grid=grid): + return self.res + 1 + return max(self.count_area.values()) + + def dfs(self,grid,row,col): + grid[row][col] = self.idx + self.count += 1 + for dr,dc in self.direction: + _row = dr + row + _col = dc + col + if 0<=_row (await iter.next()).value; + +let graph // 地图 +let N, M // 地图大小 +let visited // 访问过的节点, 标记岛屿 +const dir = [[0, 1], [1, 0], [0, -1], [-1, 0]] //方向 + +let count = 0 // 统计岛屿面积 +let areaMap = new Map() // 存储岛屿面积 + + +// 读取输入,初始化地图 +const initGraph = async () => { + let line = await readline(); + [N, M] = line.split(' ').map(Number); + graph = new Array(N).fill(0).map(() => new Array(M).fill(0)) + visited = new Array(N).fill(0).map(() => new Array(M).fill(0)) + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + for (let j = 0; j < M; j++) { + graph[i][j] = line[j] + } + } +} + +/** + * @description: 从(x,y)开始深度优先遍历地图 + * @param {*} graph 地图 + * @param {*} visited 可访问节点 + * @param {*} x 开始搜索节点的下标 + * @param {*} y 开始搜索节点的下标 + * @param {*} mark 当前岛屿的标记 + * @return {*} + */ +const dfs = (graph, visited, x, y, mark) => { + if (visited[x][y] != 0) return + visited[x][y] = mark + count++ + + for (let i = 0; i < 4; i++) { + let nextx = x + dir[i][0] + let nexty = y + dir[i][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + // 已访问过, 或者是海洋, 跳过 + if (visited[nextx][nexty] != 0 || graph[nextx][nexty] == 0) continue + + dfs(graph, visited, nextx, nexty, mark) + } +} + +(async function () { + + // 读取输入,初始化地图 + await initGraph() + + let isAllLand = true //标记整个地图都是陆地 + + let mark = 2 // 标记岛屿 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (graph[i][j] == 0 && isAllLand) isAllLand = false + if (graph[i][j] === 1 && visited[i][j] === 0) { + count = 0 + dfs(graph, visited, i, j, mark) + areaMap.set(mark, count) + mark++ + } + } + } + + // 如果全是陆地, 直接返回面积 + if (isAllLand) { + console.log(N * M); + return + } + + let result = 0 // 记录最后结果 + let visitedIsland = new Map() //标记访问过的岛屿, 因为海洋四周可能是同一个岛屿, 需要标记避免重复统计面积 + for (let i = 0; i < N; i++) { + for (let j = 0; j < M; j++) { + if (visited[i][j] === 0) { + count = 1 // 记录连接之后的岛屿数量 + visitedIsland.clear() // 每次使用时,清空 + + // 计算海洋周围岛屿面积 + for (let m = 0; m < 4; m++) { + const nextx = i + dir[m][0] + const nexty = j + dir[m][1] + if (nextx < 0 || nextx >= N || nexty < 0 || nexty >= M) continue //越界, 跳过 + + const island = visited[nextx][nexty] + if (island == 0 || visitedIsland.get(island)) continue// 四周是海洋或者访问过的陆地 跳过 + + // 标记为访问, 计算面积 + visitedIsland.set(island, true) + count += areaMap.get(visited[nextx][nexty]) + } + + result = Math.max(result, count) + } + } + } + + console.log(result); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0105.\346\234\211\345\220\221\345\233\276\347\232\204\345\256\214\345\205\250\345\217\257\350\276\276\346\200\247.md" "b/problems/kamacoder/0105.\346\234\211\345\220\221\345\233\276\347\232\204\345\256\214\345\205\250\345\217\257\350\276\276\346\200\247.md" new file mode 100644 index 0000000000..1358f673d5 --- /dev/null +++ "b/problems/kamacoder/0105.\346\234\211\345\220\221\345\233\276\347\232\204\345\256\214\345\205\250\345\217\257\350\276\276\346\200\247.md" @@ -0,0 +1,556 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 105.有向图的完全可达性 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1177) + +【题目描述】 + +给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。 + +【输入描述】 + +第一行包含两个正整数,表示节点数量 N 和边的数量 K。 后续 K 行,每行两个正整数 s 和 t,表示从 s 节点有一条边单向连接到 t 节点。 + +【输出描述】 + +如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。 + +【输入示例】 + +``` +4 4 +1 2 +2 1 +1 3 +2 4 +``` + +【输出示例】 + +1 + +【提示信息】 + +![](https://file1.kamacoder.com/i/algo/20240522174707.png) + +从 1 号节点可以到达任意节点,输出 1。 + +数据范围: + +* 1 <= N <= 100; +* 1 <= K <= 2000。 + +## 思路 + +本题给我们是一个有向图, 意识到这是有向图很重要! + +接下来我们再画一个图,从图里可以直观看出来,节点6 是 不能到达节点1 的 + +![](https://file1.kamacoder.com/i/algo/20240522175451.png) + +这就很容易让我们想起岛屿问题,只要发现独立的岛,就是不可到达的。 + +**但本题是有向图**,在有向图中,即使所有节点都是链接的,但依然不可能从0出发遍历所有边。 + +例如上图中,节点1 可以到达节点2,但节点2是不能到达节点1的。 + +所以本题是一个**有向图搜索全路径的问题**。 只能用深搜(DFS)或者广搜(BFS)来搜。 + +**以下dfs分析 大家一定要仔细看,本题有两种dfs的解法,很多题解没有讲清楚**。 看完之后 相信你对dfs会有更深的理解。 + +深搜三部曲: + +1. 确认递归函数,参数 + +需要传入地图,需要知道当前我们拿到的key,以至于去下一个房间。 + +同时还需要一个数组,用来记录我们都走过了哪些房间,这样好知道最后有没有把所有房间都遍历的,可以定义一个一维数组。 + +所以 递归函数参数如下: + +```C++ +// key 当前得到的可以 +// visited 记录访问过的房间 +void dfs(const vector>& graph, int key, vector& visited) { +``` + +2. 确认终止条件 + +遍历的时候,什么时候终止呢? + +这里有一个很重要的逻辑,就是在递归中,**我们是处理当前访问的节点,还是处理下一个要访问的节点**。 + +这决定 终止条件怎么写。 + +首先明确,本题中什么叫做处理,就是 visited数组来记录访问过的节点,该节点默认 数组里元素都是false,把元素标记为true就是处理 本节点了。 + +如果我们是处理当前访问的节点,当前访问的节点如果是 true ,说明是访问过的节点,那就终止本层递归,如果不是true,我们就把它赋值为true,因为这是我们处理本层递归的节点。 + +代码就是这样: + +```C++ +// 写法一:处理当前访问的节点 +void dfs(const vector>& graph, int key, vector& visited) { + if (visited[key]) { + return; + } + visited[key] = true; + list keys = graph[key]; + for (int key : keys) { + // 深度优先搜索遍历 + dfs(graph, key, visited); + } +} +``` + +如果我们是处理下一层访问的节点,而不是当前层。那么就要在 深搜三部曲中第三步:处理目前搜索节点出发的路径的时候对 节点进行处理。 + +这样的话,就不需要终止条件,而是在 搜索下一个节点的时候,直接判断 下一个节点是否是我们要搜的节点。 + +代码就是这样的: + +```C++ +// 写法二:处理下一个要访问的节点 +void dfs(const vector>& graph, int key, vector& visited) { + list keys = graph[key]; + for (int key : keys) { + if (visited[key] == false) { // 确认下一个是没访问过的节点 + visited[key] = true; + dfs(graph, key, visited); + } + } +} +``` + +可以看出,**如何看待 我们要访问的节点,直接决定了两种不一样的写法**,很多录友对这一块很模糊,可能做过这道题,但没有思考到这个维度上。 + + +3. 处理目前搜索节点出发的路径 + +其实在上面,深搜三部曲 第二部,就已经讲了,因为终止条件的两种写法, 直接决定了两种不一样的递归写法。 + +这里还有细节: + +看上面两个版本的写法中, 好像没有发现回溯的逻辑。 + +我们都知道,有递归就有回溯,回溯就在递归函数的下面, 那么之前我们做的dfs题目,都需要回溯操作,例如:[0098.所有可达路径](./0098.所有可达路径), **为什么本题就没有回溯呢?** + +代码中可以看到dfs函数下面并没有回溯的操作。 + +此时就要在思考本题的要求了,本题是需要判断 1节点 是否能到所有节点,那么我们就没有必要回溯去撤销操作了,只要遍历过的节点一律都标记上。 + +**那什么时候需要回溯操作呢?** + +当我们需要搜索一条可行路径的时候,就需要回溯操作了,因为没有回溯,就没法“调头”, 如果不理解的话,去看我写的 [0098.所有可达路径](./0098.所有可达路径.md) 的题解。 + + +以上分析完毕,DFS整体实现C++代码如下: + +```CPP +// 写法一:dfs 处理当前访问的节点 +#include +#include +#include +using namespace std; + +void dfs(const vector>& graph, int key, vector& visited) { + if (visited[key]) { + return; + } + visited[key] = true; + list keys = graph[key]; + for (int key : keys) { + // 深度优先搜索遍历 + dfs(graph, key, visited); + } +} + +int main() { + int n, m, s, t; + cin >> n >> m; + + // 节点编号从1到n,所以申请 n+1 这么大的数组 + vector> graph(n + 1); // 邻接表 + while (m--) { + cin >> s >> t; + // 使用邻接表 ,表示 s -> t 是相连的 + graph[s].push_back(t); + } + vector visited(n + 1, false); + dfs(graph, 1, visited); + //检查是否都访问到了 + for (int i = 1; i <= n; i++) { + if (visited[i] == false) { + cout << -1 << endl; + return 0; + } + } + cout << 1 << endl; +} + +``` + +**第二种写法注意有注释的地方是和写法一的区别** + +```CPP +写法二:dfs处理下一个要访问的节点 +#include +#include +#include +using namespace std; + +void dfs(const vector>& graph, int key, vector& visited) { + list keys = graph[key]; + for (int key : keys) { + if (visited[key] == false) { // 确认下一个是没访问过的节点 + visited[key] = true; + dfs(graph, key, visited); + } + } +} + +int main() { + int n, m, s, t; + cin >> n >> m; + + vector> graph(n + 1); + while (m--) { + cin >> s >> t; + graph[s].push_back(t); + + } + vector visited(n + 1, false); + + visited[1] = true; // 节点1 预先处理 + dfs(graph, 1, visited); + + for (int i = 1; i <= n; i++) { + if (visited[i] == false) { + cout << -1 << endl; + return 0; + } + } + cout << 1 << endl; +} + +``` + +本题我也给出 BFS C++代码,[BFS理论基础](https://programmercarl.com/kamacoder/%E5%9B%BE%E8%AE%BA%E6%B7%B1%E6%90%9C%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html),代码如下: + +```CPP +#include +#include +#include +#include +using namespace std; + +int main() { + int n, m, s, t; + cin >> n >> m; + + vector> graph(n + 1); + while (m--) { + cin >> s >> t; + graph[s].push_back(t); + + } + vector visited(n + 1, false); + visited[1] = true; // 1 号房间开始 + queue que; + que.push(1); // 1 号房间开始 + + // 广度优先搜索的过程 + while (!que.empty()) { + int key = que.front(); que.pop(); + list keys = graph[key]; + for (int key : keys) { + if (!visited[key]) { + que.push(key); + visited[key] = true; + } + } + } + + for (int i = 1; i <= n; i++) { + if (visited[i] == false) { + cout << -1 << endl; + return 0; + } + } + cout << 1 << endl; +} + +``` + + +## 其他语言版本 + +### Java + +```java + +import java.util.*; + +public class Main { + public static List> adjList = new ArrayList<>(); + + public static void dfs(boolean[] visited, int key) { + if (visited[key]) { + return; + } + visited[key] = true; + List nextKeys = adjList.get(key); + for (int nextKey : nextKeys) { + dfs(visited, nextKey); + } + } + + public static void bfs(boolean[] visited, int key) { + Queue queue = new LinkedList(); + queue.add(key); + visited[key] = true; + while (!queue.isEmpty()) { + int curKey = queue.poll(); + List list = adjList.get(curKey); + for (int nextKey : list) { + if (!visited[nextKey]) { + queue.add(nextKey); + visited[nextKey] = true; + } + } + } + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + int vertices_num = sc.nextInt(); + int line_num = sc.nextInt(); + for (int i = 0; i < vertices_num; i++) { + adjList.add(new LinkedList<>()); + }//Initialization + for (int i = 0; i < line_num; i++) { + int s = sc.nextInt(); + int t = sc.nextInt(); + adjList.get(s - 1).add(t - 1); + }//构造邻接表 + boolean[] visited = new boolean[vertices_num]; + dfs(visited, 0); +// bfs(visited, 0); + + for (int i = 0; i < vertices_num; i++) { + if (!visited[i]) { + System.out.println(-1); + return; + } + } + System.out.println(1); + } +} + +``` + + +### Python +BFS算法 +```Python +import collections + +path = set() # 纪录 BFS 所经过之节点 + +def bfs(root, graph): + global path + + que = collections.deque([root]) + while que: + cur = que.popleft() + path.add(cur) + + for nei in graph[cur]: + que.append(nei) + graph[cur] = [] + return + +def main(): + N, K = map(int, input().strip().split()) + graph = collections.defaultdict(list) + for _ in range(K): + src, dest = map(int, input().strip().split()) + graph[src].append(dest) + + bfs(1, graph) + if path == {i for i in range(1, N + 1)}: + return 1 + return -1 + + +if __name__ == "__main__": + print(main()) + +``` + +``` python + +def dfs(graph, key, visited): + for neighbor in graph[key]: + if not visited[neighbor]: # Check if the next node is not visited + visited[neighbor] = True + dfs(graph, neighbor, visited) + +def main(): + import sys + input = sys.stdin.read + data = input().split() + + n = int(data[0]) + m = int(data[1]) + + graph = [[] for _ in range(n + 1)] + index = 2 + for _ in range(m): + s = int(data[index]) + t = int(data[index + 1]) + graph[s].append(t) + index += 2 + + visited = [False] * (n + 1) + visited[1] = True # Process node 1 beforehand + dfs(graph, 1, visited) + + for i in range(1, n + 1): + if not visited[i]: + print(-1) + return + + print(1) + +if __name__ == "__main__": + main() + + +``` + +### Go + +```go + +package main + +import ( + "bufio" + "fmt" + "os" +) + +func dfs(graph [][]int, key int, visited []bool) { + visited[key] = true + for _, neighbor := range graph[key] { + if !visited[neighbor] { + dfs(graph, neighbor, visited) + } + } +} + +func main() { + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + var n, m int + fmt.Sscanf(scanner.Text(), "%d %d", &n, &m) + + graph := make([][]int, n+1) + for i := 0; i <= n; i++ { + graph[i] = make([]int, 0) + } + + for i := 0; i < m; i++ { + scanner.Scan() + var s, t int + fmt.Sscanf(scanner.Text(), "%d %d", &s, &t) + graph[s] = append(graph[s], t) + } + + visited := make([]bool, n+1) + + dfs(graph, 1, visited) + + for i := 1; i <= n; i++ { + if !visited[i] { + fmt.Println(-1) + return + } + } + fmt.Println(1) +} + + +``` + + +### Rust + +### JavaScript + +```javascript +const rl = require('readline').createInterface({ + input:process.stdin, + output:process.stdout +}) + +let inputLines = [] + +rl.on('line' , (line)=>{ + inputLines.push(line) +}) + +rl.on('close',()=>{ + let [n , edgesCount]= inputLines[0].trim().split(' ').map(Number) + + let graph = Array.from({length:n+1} , ()=>{return[]}) + + for(let i = 1 ; i < inputLines.length ; i++ ){ + let [from , to] = inputLines[i].trim().split(' ').map(Number) + graph[from].push(to) + } + + let visited = new Array(n + 1).fill(false) + + let dfs = (graph , key , visited)=>{ + if(visited[key]){ + return + } + + visited[key] = true + for(let nextKey of graph[key]){ + dfs(graph,nextKey , visited) + } + } + + dfs(graph , 1 , visited) + + for(let i = 1 ; i <= n;i++){ + if(visited[i] === false){ + console.log(-1) + return + } + } + console.log(1) + +}) +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0106.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" "b/problems/kamacoder/0106.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" new file mode 100644 index 0000000000..4492d5cd6d --- /dev/null +++ "b/problems/kamacoder/0106.\345\262\233\345\261\277\347\232\204\345\221\250\351\225\277.md" @@ -0,0 +1,362 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 106. 岛屿的周长 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1178) + +题目描述 + +给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。 + + +你可以假设矩阵外均被水包围。在矩阵中恰好拥有一个岛屿,假设组成岛屿的陆地边长都为 1,请计算岛屿的周长。岛屿内部没有水域。 + +输入描述 + +第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。 + +输出描述 + +输出一个整数,表示岛屿的周长。 + +输入示例 + +``` +5 5 +0 0 0 0 0 +0 1 0 1 0 +0 1 1 1 0 +0 1 1 1 0 +0 0 0 0 0 +``` + +输出示例 + +14 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240524115244.png) + +岛屿的周长为 14。 + +数据范围: + +1 <= M, N <= 50。 + +## 思路 + +岛屿问题最容易让人想到BFS或者DFS,但本题确实还用不上。 + +为了避免大家惯性思维,所以给大家安排了这道题目。 + +### 解法一: + +遍历每一个空格,遇到岛屿则计算其上下左右的空格情况。 + +如果该陆地上下左右的空格是有水域,则说明是一条边,如图: + +![](https://file1.kamacoder.com/i/algo/20240524115933.png) + +陆地的右边空格是水域,则说明找到一条边。 + + +如果该陆地上下左右的空格出界了,则说明是一条边,如图: + +![](https://file1.kamacoder.com/i/algo/20240524120105.png) + +该陆地的下边空格出界了,则说明找到一条边。 + + +C++代码如下:(详细注释) + +```CPP +#include +#include +using namespace std; +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + int direction[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; + int result = 0; + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) { + for (int k = 0; k < 4; k++) { // 上下左右四个方向 + int x = i + direction[k][0]; + int y = j + direction[k][1]; // 计算周边坐标x,y + if (x < 0 // x在边界上 + || x >= grid.size() // x在边界上 + || y < 0 // y在边界上 + || y >= grid[0].size() // y在边界上 + || grid[x][y] == 0) { // x,y位置是水域 + result++; + } + } + } + } + } + cout << result << endl; + +} +``` + +### 解法二: + +计算出总的岛屿数量,总的变数为:岛屿数量 * 4 + +因为有一对相邻两个陆地,边的总数就要减2,如图红线部分,有两个陆地相邻,总边数就要减2 + +![](https://file1.kamacoder.com/i/algo/20240524120855.png) + +那么只需要在计算出相邻岛屿的数量就可以了,相邻岛屿数量为cover。 + +结果 result = 岛屿数量 * 4 - cover * 2; + +C++代码如下:(详细注释) + +```CPP +#include +#include +using namespace std; +int main() { + int n, m; + cin >> n >> m; + vector> grid(n, vector(m, 0)); + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + cin >> grid[i][j]; + } + } + int sum = 0; // 陆地数量 + int cover = 0; // 相邻数量 + for (int i = 0; i < n; i++) { + for (int j = 0; j < m; j++) { + if (grid[i][j] == 1) { + sum++; // 统计总的陆地数量 + // 统计上边相邻陆地 + if(i - 1 >= 0 && grid[i - 1][j] == 1) cover++; + // 统计左边相邻陆地 + if(j - 1 >= 0 && grid[i][j - 1] == 1) cover++; + // 为什么没统计下边和右边? 因为避免重复计算 + } + } + } + + cout << sum * 4 - cover * 2 << endl; + +} +``` + +## 其他语言版本 + +### Java +```Java +import java.util.*; + +public class Main { + // 每次遍历到1,探索其周围4个方向,并记录周长,最终合计 + // 声明全局变量,dirs表示4个方向 + static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; + // 统计每单个1的周长 + static int count; + + // 探索其周围4个方向,并记录周长 + public static void helper(int[][] grid, int x, int y) { + for (int[] dir : dirs) { + int nx = x + dir[0]; + int ny = y + dir[1]; + + // 遇到边界或者水,周长加一 + if (nx < 0 || nx >= grid.length || ny < 0 || ny >= grid[0].length + || grid[nx][ny] == 0) { + count++; + } + } + } + + public static void main(String[] args) { + Scanner sc = new Scanner(System.in); + + // 接收输入 + int M = sc.nextInt(); + int N = sc.nextInt(); + + int[][] grid = new int[M][N]; + for (int i = 0; i < M; i++) { + for (int j = 0; j < N; j++) { + grid[i][j] = sc.nextInt(); + } + } + + int result = 0; // 总周长 + for (int i = 0; i < M; i++) { + for (int j = 0; j < N; j++) { + if (grid[i][j] == 1) { + count = 0; + helper(grid, i, j); + // 更新总周长 + result += count; + } + } + } + + // 打印结果 + System.out.println(result); + } +} +``` + +### Python + +```python + +def main(): + import sys + input = sys.stdin.read + data = input().split() + + # 读取 n 和 m + n = int(data[0]) + m = int(data[1]) + + # 初始化 grid + grid = [] + index = 2 + for i in range(n): + grid.append([int(data[index + j]) for j in range(m)]) + index += m + + sum_land = 0 # 陆地数量 + cover = 0 # 相邻数量 + + for i in range(n): + for j in range(m): + if grid[i][j] == 1: + sum_land += 1 + # 统计上边相邻陆地 + if i - 1 >= 0 and grid[i - 1][j] == 1: + cover += 1 + # 统计左边相邻陆地 + if j - 1 >= 0 and grid[i][j - 1] == 1: + cover += 1 + # 不统计下边和右边,避免重复计算 + + result = sum_land * 4 - cover * 2 + print(result) + +if __name__ == "__main__": + main() + + +``` + +### Go + +```go + +package main + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" +) + +func main() { + scanner := bufio.NewScanner(os.Stdin) + scanner.Scan() + line := scanner.Text() + + n, m := parseInput(line) + + // 初始化 grid + grid := make([][]int, n) + for i := range grid { + grid[i] = make([]int, m) + } + + // 读入 grid 数据 + for i := 0; i < n; i++ { + scanner.Scan() + line := scanner.Text() + values := parseLine(line, m) + for j := 0; j < m; j++ { + grid[i][j] = values[j] + } + } + + sum := 0 // 陆地数量 + cover := 0 // 相邻数量 + + for i := 0; i < n; i++ { + for j := 0; j < m; j++ { + if grid[i][j] == 1 { + sum++ // 统计总的陆地数量 + + // 统计上边相邻陆地 + if i-1 >= 0 && grid[i-1][j] == 1 { + cover++ + } + // 统计左边相邻陆地 + if j-1 >= 0 && grid[i][j-1] == 1 { + cover++ + } + // 为什么没统计下边和右边? 因为避免重复计算 + } + } + } + + fmt.Println(sum*4 - cover*2) +} + +// parseInput 解析 n 和 m +func parseInput(line string) (int, int) { + parts := strings.Split(line, " ") + n, _ := strconv.Atoi(parts[0]) + m, _ := strconv.Atoi(parts[1]) + return n, m +} + +// parseLine 解析一行中的多个值 +func parseLine(line string, count int) []int { + parts := strings.Split(line, " ") + values := make([]int, count) + for i := 0; i < count; i++ { + values[i], _ = strconv.Atoi(parts[i]) + } + return values +} + + +``` + + +### Rust + +### JavaScript + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0107.\345\257\273\346\211\276\345\255\230\345\234\250\347\232\204\350\267\257\345\276\204.md" "b/problems/kamacoder/0107.\345\257\273\346\211\276\345\255\230\345\234\250\347\232\204\350\267\257\345\276\204.md" new file mode 100644 index 0000000000..9ab3388f17 --- /dev/null +++ "b/problems/kamacoder/0107.\345\257\273\346\211\276\345\255\230\345\234\250\347\232\204\350\267\257\345\276\204.md" @@ -0,0 +1,425 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 107. 寻找存在的路径 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1179) + +题目描述 + +给定一个包含 n 个节点的无向图中,节点编号从 1 到 n (含 1 和 n )。 + +你的任务是判断是否有一条从节点 source 出发到节点 destination 的路径存在。 + +输入描述 + +第一行包含两个正整数 N 和 M,N 代表节点的个数,M 代表边的个数。  + +后续 M 行,每行两个正整数 s 和 t,代表从节点 s 与节点 t 之间有一条边。 + +最后一行包含两个正整数,代表起始节点 source 和目标节点 destination。 + +输出描述 + +输出一个整数,代表是否存在从节点 source 到节点 destination 的路径。如果存在,输出 1;否则,输出 0。 + +输入示例 + +``` +5 4 +1 2 +1 3 +2 4 +3 4 +1 4 +``` + +输出示例 + +1 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240527104432.png) + +数据范围: + +1 <= M, N <= 100。 + +## 思路 + +本题是并查集基础题目。 如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/kamacoder/图论并查集理论基础.html) + +并查集可以解决什么问题呢? + +主要就是集合问题,**两个节点在不在一个集合,也可以将两个节点添加到一个集合中**。 + +这里整理出我的并查集模板如下: + +```CPP +int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 +vector father = vector (n, 0); // C++里的一种数组结构 + +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +``` + +以上模板中,只要修改 n 大小就可以。 + +并查集主要有三个功能: + +1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个 +2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上 +3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点 + +简单介绍并查集之后,我们再来看一下这道题目。 + +为什么说这道题目是并查集基础题目,题目中各个点是双向图链接,那么判断 一个顶点到另一个顶点有没有有效路径其实就是看这两个顶点是否在同一个集合里。 + +如何算是同一个集合呢,有边连在一起,就算是一个集合。 + +此时我们就可以直接套用并查集模板。 + +使用 join(int u, int v)将每条边加入到并查集。 + +最后 isSame(int u, int v) 判断是否是同一个根 就可以了。 + +C++代码如下: + +```CPP +#include +#include +using namespace std; + +int n; // 节点数量 +vector father = vector (101, 0); // 按照节点大小定义数组大小 + +// 并查集初始化 +void init() { + for (int i = 1; i <= n; i++) father[i] = i; +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} + +int main() { + int m, s, t, source, destination; + cin >> n >> m; + init(); + while (m--) { + cin >> s >> t; + join(s, t); + } + cin >> source >> destination; + if (isSame(source, destination)) cout << 1 << endl; + else cout << 0 << endl; +} +``` + + +## 其他语言版本 + +### Java + +```Java + +import java.util.*; + +public class Main{ + public static void main(String[] args) { + int N, M; + Scanner scanner = new Scanner(System.in); + N = scanner.nextInt(); + M = scanner.nextInt(); + DisJoint disJoint = new DisJoint(N + 1); + for (int i = 0; i < M; ++i) { + disJoint.join(scanner.nextInt(), scanner.nextInt()); + } + if(disJoint.isSame(scanner.nextInt(), scanner.nextInt())) { + System.out.println("1"); + } else { + System.out.println("0"); + } + } + +} + +//并查集模板 +class DisJoint{ + private int[] father; + + public DisJoint(int N) { + father = new int[N]; + for (int i = 0; i < N; ++i){ + father[i] = i; + } + } + + public int find(int n) { + return n == father[n] ? n : (father[n] = find(father[n])); + } + + public void join (int n, int m) { + n = find(n); + m = find(m); + if (n == m) return; + father[m] = n; + } + + public boolean isSame(int n, int m){ + n = find(n); + m = find(m); + return n == m; + } + +} + +``` + +### Python + +```python + +class UnionFind: + def __init__(self, size): + self.parent = list(range(size + 1)) # 初始化并查集 + + def find(self, u): + if self.parent[u] != u: + self.parent[u] = self.find(self.parent[u]) # 路径压缩 + return self.parent[u] + + def union(self, u, v): + root_u = self.find(u) + root_v = self.find(v) + if root_u != root_v: + self.parent[root_v] = root_u + + def is_same(self, u, v): + return self.find(u) == self.find(v) + + +def main(): + import sys + input = sys.stdin.read + data = input().split() + + index = 0 + n = int(data[index]) + index += 1 + m = int(data[index]) + index += 1 + + uf = UnionFind(n) + + for _ in range(m): + s = int(data[index]) + index += 1 + t = int(data[index]) + index += 1 + uf.union(s, t) + + source = int(data[index]) + index += 1 + destination = int(data[index]) + + if uf.is_same(source, destination): + print(1) + else: + print(0) + +if __name__ == "__main__": + main() + + +``` + + +### Go + +```go + +package main + +import ( + "fmt" +) + +const MaxNodes = 101 + +var n int +var father [MaxNodes]int + +// 初始化并查集 +func initialize() { + for i := 1; i <= n; i++ { + father[i] = i + } +} + +// 并查集里寻根的过程 +func find(u int) int { + if u == father[u] { + return u + } + father[u] = find(father[u]) + return father[u] +} + +// 判断 u 和 v 是否找到同一个根 +func isSame(u, v int) bool { + return find(u) == find(v) +} + +// 将 v->u 这条边加入并查集 +func join(u, v int) { + rootU := find(u) + rootV := find(v) + if rootU != rootV { + father[rootV] = rootU + } +} + +func main() { + var m, s, t, source, destination int + fmt.Scan(&n, &m) + initialize() + for i := 0; i < m; i++ { + fmt.Scan(&s, &t) + join(s, t) + } + fmt.Scan(&source, &destination) + if isSame(source, destination) { + fmt.Println(1) + } else { + fmt.Println(0) + } +} + + +``` + +### Rust + +### JavaScript + +```java +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N, M // 节点数和边数 +let source, destination // 起点 终点 +let father = [] // 并查集 + + +// 并查集初始化 +const init = () => { + for (let i = 1; i <= N; i++) father[i] = i; +} + +// 并查集里寻根的过程 +const find = (u) => { + return u == father[u] ? u : father[u] = find(father[u]) +} + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u) + v = find(v) + if (u == v) return // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u +} + +// 判断 u 和 v是否找到同一个根 +const isSame = (u, v) => { + u = find(u) + v = find(v) + return u == v +} + + +(async function () { + // 读取第一行输入 + let line = await readline(); + [N, M] = line.split(' ').map(Number); + + // 初始化并查集 + father = new Array(N) + init() + + // 读取边信息, 加入并查集 + for (let i = 0; i < M; i++) { + line = await readline() + line = line.split(' ').map(Number) + join(line[0], line[1]) + } + + // 读取起点和终点 + line = await readline(); //JS注意这里的冒号 + [source, destination] = line.split(' ').map(Number) + + if (isSame(source, destination)) return console.log(1); + console.log(0); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0108.\345\206\227\344\275\231\350\277\236\346\216\245.md" "b/problems/kamacoder/0108.\345\206\227\344\275\231\350\277\236\346\216\245.md" new file mode 100644 index 0000000000..de2435073c --- /dev/null +++ "b/problems/kamacoder/0108.\345\206\227\344\275\231\350\277\236\346\216\245.md" @@ -0,0 +1,379 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 108. 冗余连接 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1181) + +题目描述 + +有一个图,它是一棵树,他是拥有 n 个节点(节点编号1到n)和 n - 1 条边的连通无环无向图(其实就是一个线形图),如图: + +![](https://file1.kamacoder.com/i/algo/20240905163122.png) + +现在在这棵树上的基础上,添加一条边(依然是n个节点,但有n条边),使这个图变成了有环图,如图 + +![](https://file1.kamacoder.com/i/algo/20240905164721.png) + +先请你找出冗余边,删除后,使该图可以重新变成一棵树。 + +输入描述 + +第一行包含一个整数 N,表示图的节点个数和边的个数。 + +后续 N 行,每行包含两个整数 s 和 t,表示图中 s 和 t 之间有一条边。 + +输出描述 + +输出一条可以删除的边。如果有多个答案,请删除标准输入中最后出现的那条边。 + +输入示例 + +``` +3 +1 2 +2 3 +1 3 +``` + +输出示例 + +1 3 + +提示信息 + +![](https://file1.kamacoder.com/i/algo/20240527110320.png) + +图中的 1 2,2 3,1 3 等三条边在删除后都能使原图变为一棵合法的树。但是 1 3 由于是标准输入里最后出现的那条边,所以输出结果为 1 3 + +数据范围: + +1 <= N <= 1000. + +## 思路 + +这道题目也是并查集基础题目。 + +这里我依然降调一下,并查集可以解决什么问题:两个节点是否在一个集合,也可以将两个节点添加到一个集合中。 + +如果还不了解并查集,可以看这里:[并查集理论基础](./图论并查集理论基础.md) + +我们再来看一下这道题目。 + +题目说是无向图,返回一条可以删去的边,使得结果图是一个有着N个节点的树(即:只有一个根节点)。 + +如果有多个答案,则返回二维数组中最后出现的边。 + +那么我们就可以从前向后遍历每一条边(因为优先让前面的边连上),边的两个节点如果不在同一个集合,就加入集合(即:同一个根节点)。 + + +如图所示,节点A 和节点 B 不在同一个集合,那么就可以将两个 节点连在一起。 + +![](https://file1.kamacoder.com/i/algo/20230604104720.png) + +如果边的两个节点已经出现在同一个集合里,说明着边的两个节点已经连在一起了,再加入这条边一定就出现环了。 + +如图所示: + +![](https://file1.kamacoder.com/i/algo/20230604104330.png) + +已经判断 节点A 和 节点B 在在同一个集合(同一个根),如果将 节点A 和 节点B 连在一起就一定会出现环。 + +这个思路清晰之后,代码就很好写了。 + +并查集C++代码如下: + +```CPP +#include +#include +using namespace std; +int n; // 节点数量 +vector father(1001, 0); // 按照节点大小范围定义数组 + +// 并查集初始化 +void init() { + for (int i = 0; i <= n; ++i) { + father[i] = i; + } +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} + +int main() { + int s, t; + cin >> n; + init(); + for (int i = 0; i < n; i++) { + cin >> s >> t; + if (isSame(s, t)) { + cout << s << " " << t << endl; + return 0; + } else { + join(s, t); + } + } +} +``` + +可以看出,主函数的代码很少,就判断一下边的两个节点在不在同一个集合就可以了。 + +## 拓展 + +题目要求 “请删除标准输入中最后出现的那条边” ,不少录友疑惑,这代码分明是遇到在同一个根的两个节点立刻就返回了,怎么就求出 最后出现的那条边 了呢。 + +有这种疑惑的录友是 认为发现一条冗余边后,后面还可能会有一条冗余边。 + +其实并不会。 + +题目是在 树的基础上 添加一条边,所以冗余边仅仅是一条。 + +到这一条可能靠前出现,可能靠后出现。 + +例如,题目输入示例: + +输入示例 + +``` +3 +1 2 +2 3 +1 3 +``` + +图: + +![](https://file1.kamacoder.com/i/algo/20240527110320.png) + +输出示例 + +1 3 + +当我们从前向后遍历,优先让前面的边连上,最后判断冗余边就是 1 3。 + +如果我们从后向前便利,优先让后面的边连上,最后判断的冗余边就是 1 2。 + +题目要求“请删除标准输入中最后出现的那条边”,所以 1 3 这条边才是我们要求的。 + + + + +## 其他语言版本 + +### Java + +```java +import java.util.Scanner; + +public class Main { + private static int[] father; + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int pointNum = scanner.nextInt(); + father = new int[pointNum + 1]; + init(); + for (int i = 0; i < pointNum; i++) { + join(scanner.nextInt(), scanner.nextInt()); + } + } + + /** + * 并查集初始化 + */ + private static void init() { + for (int i = 1; i < father.length; i++) { + // 让每个元素指向自己 + father[i] = i; + } + } + + /** + * 并查集寻根 + * + * @param u + * @return + */ + private static int find(int u) { + // 判断 u 是否等于自己,如果是的话,直接返回自己 + // 如果不等于自己,就寻找根,寻找的时候,反复进行路径压缩 + return u == father[u] ? u : (father[u] = find(father[u])); + } + + /** + * 判断 u 和 v 是否同根 + * + * @param u + * @param v + * @return + */ + private static boolean isSame(int u, int v) { + return find(u) == find(v); + } + + /** + * 添加 边 到并查集,v 指向 u + * + * @param u + * @param v + */ + private static void join(int u, int v) { + // --if-- 如果两个点已经同根,说明他们的信息已经存储到并查集中了,直接返回即可 + // 寻找u的根 + int uRoot = find(u); + // 寻找v的根 + int vRoot = find(v); + if (uRoot == vRoot) { + // --if-- 如果u,v的根相同,说明两者已经连接了,直接输出 + System.out.println(u + " " + v); + return; + } + // --if-- 将信息添加到并查集 + father[vRoot] = uRoot; + } + +} +``` + + + +### Python + +```python +father = list() + +def find(u): + if u == father[u]: + return u + else: + father[u] = find(father[u]) + return father[u] + +def is_same(u, v): + u = find(u) + v = find(v) + return u == v + +def join(u, v): + u = find(u) + v = find(v) + if u != v: + father[u] = v + +if __name__ == "__main__": + # 輸入 + n = int(input()) + for i in range(n + 1): + father.append(i) + # 尋找冗余邊 + result = None + for i in range(n): + s, t = map(int, input().split()) + if is_same(s, t): + result = str(s) + ' ' + str(t) + else: + join(s, t) + + # 輸出 + print(result) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N // 节点数和边数 +let father = [] // 并查集 + + +// 并查集初始化 +const init = () => { + for (let i = 1; i <= N; i++) father[i] = i; +} + +// 并查集里寻根的过程 +const find = (u) => { + return u == father[u] ? u : father[u] = find(father[u]) +} + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u) + v = find(v) + if (u == v) return // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u +} + +// 判断 u 和 v是否找到同一个根 +const isSame = (u, v) => { + u = find(u) + v = find(v) + return u == v +} + + +(async function () { + // 读取第一行输入 + let line = await readline(); + N = Number(line); + + // 初始化并查集 + father = new Array(N) + init() + + // 读取边信息, 加入并查集 + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + + if (!isSame(line[0], line[1])) { + join(line[0], line[1]) + }else{ + console.log(line[0], line[1]); + break + } + } +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0109.\345\206\227\344\275\231\350\277\236\346\216\245II.md" "b/problems/kamacoder/0109.\345\206\227\344\275\231\350\277\236\346\216\245II.md" new file mode 100644 index 0000000000..6ad59c4188 --- /dev/null +++ "b/problems/kamacoder/0109.\345\206\227\344\275\231\350\277\236\346\216\245II.md" @@ -0,0 +1,599 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 109. 冗余连接II + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1182) + +题目描述 + +有一种有向树,该树只有一个根节点,所有其他节点都是该根节点的后继。该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点。有向树拥有 n 个节点和 n - 1 条边。如图:  + + + +现在有一个有向图,有向图是在有向树中的两个没有直接链接的节点中间添加一条有向边。如图: + + + +输入一个有向图,该图由一个有着 n 个节点(节点编号 从 1 到 n),n 条边,请返回一条可以删除的边,使得删除该条边之后该有向图可以被当作一颗有向树。 + +输入描述 + +第一行输入一个整数 N,表示有向图中节点和边的个数。 + +后续 N 行,每行输入两个整数 s 和 t,代表这是 s 节点连接并指向 t 节点的单向边 + +输出描述 + +输出一条可以删除的边,若有多条边可以删除,请输出标准输入中最后出现的一条边。 + +输入示例 + +``` +3 +1 2 +1 3 +2 3 +``` + +输出示例 + +2 3 + +提示信息 + + + +在删除 2 3 后有向图可以变为一棵合法的有向树,所以输出 2 3 + +数据范围: + +1 <= N <= 1000. + +## 思路 + +本题与 [108.冗余连接](./0108.冗余连接.md) 类似,但本题是一个有向图,有向图相对要复杂一些。 + +本题的本质是 :有一个有向图,是由一颗有向树 + 一条有向边组成的 (所以此时这个图就不能称之为有向树),现在让我们找到那条边 把这条边删了,让这个图恢复为有向树。 + +还有“**若有多条边可以删除,请输出标准输入中最后出现的一条边**”,这说明在两条边都可以删除的情况下,要删顺序靠后的边! + +我们来想一下 有向树的性质,如果是有向树的话,只有根节点入度为0,其他节点入度都为1(因为该树除了根节点之外的每一个节点都有且只有一个父节点,而根节点没有父节点)。 + +所以情况一:如果我们找到入度为2的点,那么删一条指向该节点的边就行了。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240527115807.png) + +找到了节点3 的入度为2,删 1 -> 3 或者 2 -> 3 。选择删顺序靠后便可。 + +但 入度为2 还有一种情况,情况二,只能删特定的一条边,如图: + +![](https://file1.kamacoder.com/i/algo/20240527151456.png) + +节点3 的入度为 2,但在删除边的时候,只能删 这条边(节点1 -> 节点3),如果删这条边(节点4 -> 节点3),那么删后本图也不是有向树了(因为找不到根节点)。 + +综上,如果发现入度为2的节点,我们需要判断 删除哪一条边,删除后本图能成为有向树。如果是删哪个都可以,优先删顺序靠后的边。 + + +情况三: 如果没有入度为2的点,说明 图中有环了(注意是有向环)。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240527120531.png) + +对于情况三,删掉构成环的边就可以了。 + + +## 写代码 + +把每条边记录下来,并统计节点入度: + +```cpp + int s, t; + vector> edges; + cin >> n; + vector inDegree(n + 1, 0); // 记录节点入度 + for (int i = 0; i < n; i++) { + cin >> s >> t; + inDegree[t]++; + edges.push_back({s, t}); + } + + +``` + +前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条,如果删了一条,判断这个图是一个树,那么这条边就是答案。 + +同时注意要从后向前遍历,因为如果两条边删哪一条都可以成为树,就删最后那一条。 + +代码如下: + +```cpp +vector vec; // 记录入度为2的边(如果有的话就两条边) +// 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边 +for (int i = n - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push_back(i); + } +} +if (vec.size() > 0) { + // 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边 + if (isTreeAfterRemoveEdge(edges, vec[0])) { + cout << edges[vec[0]][0] << " " << edges[vec[0]][1]; + } else { + cout << edges[vec[1]][0] << " " << edges[vec[1]][1]; + } + return 0; +} +``` + +再来看情况三,明确没有入度为2的情况,那么一定有向环,找到构成环的边就是要删除的边。 + +可以定义一个函数,代码如下: + +```cpp +// 在有向图里找到删除的那条边,使其变成树 +void getRemoveEdge(const vector>& edges) +``` + +大家应该知道了,我们要解决本题要实现两个最为关键的函数: + +* `isTreeAfterRemoveEdge()` 判断删一个边之后是不是有向树 +* `getRemoveEdge()` 确定图中一定有了有向环,那么要找到需要删除的那条边 + +此时就用到**并查集**了。 + +如果还不了解并查集,可以看这里:[并查集理论基础](https://programmercarl.com/kamacoder/图论并查集理论基础.html) + +`isTreeAfterRemoveEdge()` 判断删一个边之后是不是有向树: 将所有边的两端节点分别加入并查集,遇到要 要删除的边则跳过,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。 + +如果顺利将所有边的两端节点(除了要删除的边)加入了并查集,则说明 删除该条边 还是一个有向树 + +`getRemoveEdge()`确定图中一定有了有向环,那么要找到需要删除的那条边: 将所有边的两端节点分别加入并查集,如果遇到即将加入并查集的边的两端节点 本来就在并查集了,说明构成了环。 + +本题C++代码如下:(详细注释了) + + +```cpp +#include +#include +using namespace std; +int n; +vector father (1001, 0); +// 并查集初始化 +void init() { + for (int i = 1; i <= n; ++i) { + father[i] = i; + } +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); + v = find(v); + if (u == v) return ; + father[v] = u; +} +// 判断 u 和 v是否找到同一个根 +bool same(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 在有向图里找到删除的那条边,使其变成树 +void getRemoveEdge(const vector>& edges) { + init(); // 初始化并查集 + for (int i = 0; i < n; i++) { // 遍历所有的边 + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 + cout << edges[i][0] << " " << edges[i][1]; + return; + } else { + join(edges[i][0], edges[i][1]); + } + } +} + +// 删一条边之后判断是不是树 +bool isTreeAfterRemoveEdge(const vector>& edges, int deleteEdge) { + init(); // 初始化并查集 + for (int i = 0; i < n; i++) { + if (i == deleteEdge) continue; + if (same(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 + return false; + } + join(edges[i][0], edges[i][1]); + } + return true; +} + +int main() { + int s, t; + vector> edges; + cin >> n; + vector inDegree(n + 1, 0); // 记录节点入度 + for (int i = 0; i < n; i++) { + cin >> s >> t; + inDegree[t]++; + edges.push_back({s, t}); + } + + vector vec; // 记录入度为2的边(如果有的话就两条边) + // 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边 + for (int i = n - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push_back(i); + } + } + // 情况一、情况二 + if (vec.size() > 0) { + // 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边 + if (isTreeAfterRemoveEdge(edges, vec[0])) { + cout << edges[vec[0]][0] << " " << edges[vec[0]][1]; + } else { + cout << edges[vec[1]][0] << " " << edges[vec[1]][1]; + } + return 0; + } + + // 处理情况三 + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + getRemoveEdge(edges); +} +``` + +## 其他语言版本 + +### Java + +```java +import java.util.*; + +/* + * 冗余连接II。主要问题是存在入度为2或者成环,也可能两个问题同时存在。 + * 1.判断入度为2的边 + * 2.判断是否成环(并查集) + */ + +public class Main { + /** + * 并查集模板 + */ + static class Disjoint { + + private final int[] father; + + public Disjoint(int n) { + father = new int[n]; + for (int i = 0; i < n; i++) { + father[i] = i; + } + } + + public void join(int n, int m) { + n = find(n); + m = find(m); + if (n == m) return; + father[n] = m; + } + + public int find(int n) { + return father[n] == n ? n : (father[n] = find(father[n])); + } + + public boolean isSame(int n, int m) { + return find(n) == find(m); + } + } + + static class Edge { + int s; + int t; + + public Edge(int s, int t) { + this.s = s; + this.t = t; + } + } + + static class Node { + int id; + int in; + int out; + } + + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + List edges = new ArrayList<>(); + Node[] nodeMap = new Node[n + 1]; + for (int i = 1; i <= n; i++) { + nodeMap[i] = new Node(); + } + Integer doubleIn = null; + for (int i = 0; i < n; i++) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + //记录入度 + nodeMap[t].in++; + if (!(nodeMap[t].in < 2)) doubleIn = t; + Edge edge = new Edge(s, t); + edges.add(edge); + } + Edge result = null; + //存在入度为2的节点,既要消除入度为2的问题同时解除可能存在的环 + if (doubleIn != null) { + List doubleInEdges = new ArrayList<>(); + for (Edge edge : edges) { + if (edge.t == doubleIn) doubleInEdges.add(edge); + if (doubleInEdges.size() == 2) break; + } + Edge edge = doubleInEdges.get(1); + if (isTreeWithExclude(edges, edge, nodeMap)) { + result = edge; + } else { + result = doubleInEdges.get(0); + } + } else { + //不存在入度为2的节点,则只需要解除环即可 + result = getRemoveEdge(edges, nodeMap); + } + + System.out.println(result.s + " " + result.t); + } + + public static boolean isTreeWithExclude(List edges, Edge exculdEdge, Node[] nodeMap) { + Disjoint disjoint = new Disjoint(nodeMap.length + 1); + for (Edge edge : edges) { + if (edge == exculdEdge) continue; + //成环则不是树 + if (disjoint.isSame(edge.s, edge.t)) { + return false; + } + disjoint.join(edge.s, edge.t); + } + return true; + } + + public static Edge getRemoveEdge(List edges, Node[] nodeMap) { + int length = nodeMap.length; + Disjoint disjoint = new Disjoint(length); + + for (Edge edge : edges) { + if (disjoint.isSame(edge.s, edge.t)) return edge; + disjoint.join(edge.s, edge.t); + } + return null; + } + +} + +``` + +### Python + +```python +from collections import defaultdict + +father = list() + + +def find(u): + if u == father[u]: + return u + else: + father[u] = find(father[u]) + return father[u] + + +def is_same(u, v): + u = find(u) + v = find(v) + return u == v + + +def join(u, v): + u = find(u) + v = find(v) + if u != v: + father[u] = v + + +def is_tree_after_remove_edge(edges, edge, n): + # 初始化并查集 + global father + father = [i for i in range(n + 1)] + + for i in range(len(edges)): + if i == edge: + continue + s, t = edges[i] + if is_same(s, t): # 成環,即不是有向樹 + return False + else: # 將s,t放入集合中 + join(s, t) + return True + + +def get_remove_edge(edges): + # 初始化并查集 + global father + father = [i for i in range(n + 1)] + + for s, t in edges: + if is_same(s, t): + print(s, t) + return + else: + join(s, t) + + +if __name__ == "__main__": + # 輸入 + n = int(input()) + edges = list() + in_degree = defaultdict(int) + + for i in range(n): + s, t = map(int, input().split()) + in_degree[t] += 1 + edges.append([s, t]) + + # 尋找入度為2的邊,並紀錄其下標(index) + vec = list() + for i in range(n - 1, -1, -1): + if in_degree[edges[i][1]] == 2: + vec.append(i) + + # 輸出 + if len(vec) > 0: + # 情況一:刪除輸出順序靠後的邊 + if is_tree_after_remove_edge(edges, vec[0], n): + print(edges[vec[0]][0], edges[vec[0]][1]) + # 情況二:只能刪除特定的邊 + else: + print(edges[vec[1]][0], edges[vec[1]][1]) + else: + # 情況三: 原圖有環 + get_remove_edge(edges) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N // 节点数和边数 +let father = [] // 并查集 +let edges = [] // 边集 +let inDegree = [] // 入度 + + +// 并查集初始化 +const init = () => { + for (let i = 1; i <= N; i++) father[i] = i; +} + +// 并查集里寻根的过程 +const find = (u) => { + return u == father[u] ? u : father[u] = find(father[u]) +} + +// 将v->u 这条边加入并查集 +const join = (u, v) => { + u = find(u) + v = find(v) + if (u == v) return // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u +} + +// 判断 u 和 v是否找到同一个根 +const isSame = (u, v) => { + u = find(u) + v = find(v) + return u == v +} + +// 判断删除一条边后是不是树 +const isTreeAfterRemoveEdge = (edges, edge) => { + // 初始化并查集 + init() + + for (let i = 0; i < N; i++) { + if (i == edge) continue + if (isSame(edges[i][0], edges[i][1])) { // 构成有向环了,一定不是树 + return false + } + join(edges[i][0], edges[i][1]) + } + return true +} + +// 在有向图里找到删除的那条边, 使其成为树 +const getRemoveEdge = (edges) => { + // 初始化并查集 + init() + + for (let i = 0; i < N; i++) { + if (isSame(edges[i][0], edges[i][1])) { // 构成有向环了,就是要删除的边 + console.log(edges[i][0], edges[i][1]); + return + } else { + join(edges[i][0], edges[i][1]) + } + } +} + + +(async function () { + // 读取第一行输入 + let line = await readline(); + N = Number(line); + + // 读取边信息, 统计入度 + for (let i = 0; i < N; i++) { + line = await readline() + line = line.split(' ').map(Number) + + edges.push(line) + + inDegree[line[1]] = (inDegree[line[1]] || 0) + 1 + } + + // 找到入度为2的节点 + let vec = [] // 记录入度为2的边(如果有的话就两条边) + // 找入度为2的节点所对应的边,注意要倒序,因为优先删除最后出现的一条边 + for (let i = N - 1; i >= 0; i--) { + if (inDegree[edges[i][1]] == 2) { + vec.push(i) + } + } + + // 情况一、情况二 + if (vec.length > 0) { + // 放在vec里的边已经按照倒叙放的,所以这里就优先删vec[0]这条边 + if (isTreeAfterRemoveEdge(edges, vec[0])) { + console.log(edges[vec[0]][0], edges[vec[0]][1]); + } else { + console.log(edges[vec[1]][0], edges[vec[1]][1]); + } + return 0 + } + + // 情况三 + // 明确没有入度为2的情况,那么一定有有向环,找到构成环的边返回就可以了 + getRemoveEdge(edges) +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231.md" "b/problems/kamacoder/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231.md" new file mode 100644 index 0000000000..6cb5886d20 --- /dev/null +++ "b/problems/kamacoder/0110.\345\255\227\347\254\246\344\270\262\346\216\245\351\276\231.md" @@ -0,0 +1,363 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 110. 字符串接龙 + +[卡码网题目链接(ACM模式)](https://kamacoder.com/problempage.php?pid=1183) + +题目描述 + +字典 strList 中从字符串 beginStr 和 endStr 的转换序列是一个按下述规格形成的序列: + +1. 序列中第一个字符串是 beginStr。 + +2. 序列中最后一个字符串是 endStr。 + +3. **每次转换只能改变一个位置的字符**(例如 ftr 可以转化 fty ,但 ftr 不能转化 frx)。 + +4. 转换过程中的中间字符串必须是字典 strList 中的字符串。 + +5. beginStr 和 endStr 不在 字典 strList 中 + +6. 字符串中只有小写的26个字母 + +给你两个字符串 beginStr 和 endStr 和一个字典 strList,找到从 beginStr 到 endStr 的最短转换序列中的字符串数目。如果不存在这样的转换序列,返回 0。 + +输入描述 + +第一行包含一个整数 N,表示字典 strList 中的字符串数量。 第二行包含两个字符串,用空格隔开,分别代表 beginStr 和 endStr。 后续 N 行,每行一个字符串,代表 strList 中的字符串。 + +输出描述 + +输出一个整数,代表从 beginStr 转换到 endStr 需要的最短转换序列中的字符串数量。如果不存在这样的转换序列,则输出 0。 + +输入示例 + +``` +6 +abc def +efc +dbc +ebc +dec +dfc +yhn +``` + +输出示例 + +4 + +提示信息 + +从 startStr 到 endStr,在 strList 中最短的路径为 abc -> dbc -> dec -> def,所以输出结果为 4 + +数据范围: + +2 <= N <= 500 + +

+ +

+ + +## 思路 + +以示例1为例,从这个图中可以看出 abc 到 def的路线 不止一条,但最短的一条路径上是4个节点。 + +![](https://file1.kamacoder.com/i/algo/20250317105155.png) + + +本题只需要求出最短路径的长度就可以了,不用找出具体路径。 + +所以这道题要解决两个问题: + +1、图中的线是如何连在一起的 + +在搜索的过程中,我们可以枚举,用26个字母替换当前字符串的每一个字符,在看替换后 是否在 strList里出现过,就可以判断 两个字符串 是否是链接的。 + +2、起点和终点的最短路径长度 + +首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个。 + +所以判断点与点之间的关系,需要判断是不是差一个字符,**如果差一个字符,那就是有链接**。 + +然后就是求起点和终点的最短路径长度,在无权图中,求最短路,用深搜或者广搜就行,没必要用最短路算法。 + +**在无权图中,用广搜求最短路最为合适,广搜只要搜到了终点,那么一定是最短的路径**。因为广搜就是以起点中心向四周扩散的搜索。 + +**本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路**。 + +而广搜只要达到终点,一定是最短路。 + +另外需要有一个注意点: + +* 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环! +* 使用set来检查字符串是否出现在字符串集合里更快一些 + +C++代码如下:(详细注释) + +```CPP +#include +#include +#include +#include +#include +#include +using namespace std; +int main() { + string beginStr, endStr, str; + int n; + cin >> n; + unordered_set strSet; + cin >> beginStr >> endStr; + for (int i = 0; i < n; i++) { + cin >> str; + strSet.insert(str); + } + + // 记录strSet里的字符串是否被访问过,同时记录路径长度 + unordered_map visitMap; // <记录的字符串,路径长度> + + // 初始化队列 + queue que; + que.push(beginStr); + + // 初始化visitMap + visitMap.insert(pair(beginStr, 1)); + + while(!que.empty()) { + string word = que.front(); + que.pop(); + int path = visitMap[word]; // 这个字符串在路径中的长度 + + // 开始在这个str中,挨个字符去替换 + for (int i = 0; i < word.size(); i++) { + string newWord = word; // 用一个新字符串替换str,因为每次要置换一个字符 + + // 遍历26的字母 + for (int j = 0 ; j < 26; j++) { + newWord[i] = j + 'a'; + if (newWord == endStr) { // 发现替换字母后,字符串与终点字符串相同 + cout << path + 1 << endl; // 找到了路径 + return 0; + } + // 字符串集合里出现了newWord,并且newWord没有被访问过 + if (strSet.find(newWord) != strSet.end() + && visitMap.find(newWord) == visitMap.end()) { + // 添加访问信息,并将新字符串放到队列中 + visitMap.insert(pair(newWord, path + 1)); + que.push(newWord); + } + } + } + } + + // 没找到输出0 + cout << 0 << endl; + +} +``` + +当然本题也可以用双向BFS,就是从头尾两端进行搜索,大家感兴趣,可以自己去实现,这里就不再做详细讲解了。 + + +## 其他语言版本 + +### Java + +```Java +import java.util.*; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + scanner.nextLine(); + String beginStr = scanner.next(); + String endStr = scanner.next(); + scanner.nextLine(); + List wordList = new ArrayList<>(); + wordList.add(beginStr); + wordList.add(endStr); + for (int i = 0; i < n; i++) { + wordList.add(scanner.nextLine()); + } + int count = bfs(beginStr, endStr, wordList); + System.out.println(count); + } + + /** + * 广度优先搜索-寻找最短路径 + */ + public static int bfs(String beginStr, String endStr, List wordList) { + int len = 1; + Set set = new HashSet<>(wordList); + Set visited = new HashSet<>(); + Queue q = new LinkedList<>(); + visited.add(beginStr); + q.add(beginStr); + q.add(null); + while (!q.isEmpty()) { + String node = q.remove(); + //上一层结束,若下一层还有节点进入下一层 + if (node == null) { + if (!q.isEmpty()) { + len++; + q.add(null); + } + continue; + } + char[] charArray = node.toCharArray(); + //寻找邻接节点 + for (int i = 0; i < charArray.length; i++) { + //记录旧值,用于回滚修改 + char old = charArray[i]; + for (char j = 'a'; j <= 'z'; j++) { + charArray[i] = j; + String newWord = new String(charArray); + if (set.contains(newWord) && !visited.contains(newWord)) { + q.add(newWord); + visited.add(newWord); + //找到结尾 + if (newWord.equals(endStr)) return len + 1; + } + } + charArray[i] = old; + } + } + return 0; + } +} + +``` + +### Python +```Python +def judge(s1,s2): + count=0 + for i in range(len(s1)): + if s1[i]!=s2[i]: + count+=1 + return count==1 + +if __name__=='__main__': + n=int(input()) + beginstr,endstr=map(str,input().split()) + if beginstr==endstr: + print(0) + exit() + strlist=[] + for i in range(n): + strlist.append(input()) + + # use bfs + visit=[False for i in range(n)] + queue=[[beginstr,1]] + while queue: + str,step=queue.pop(0) + if judge(str,endstr): + print(step+1) + exit() + for i in range(n): + if visit[i]==False and judge(strlist[i],str): + visit[i]=True + queue.append([strlist[i],step+1]) + print(0) +``` + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + +let N //输入字符串个数 +let beginStr //开始字符串 +let endStr //结束字符串 +let strSet = new Set() //字符串集合 +let visitedMap = new Map() //访问过的字符串 + +// 读取输入,初始化地图 +const init = async () => { + let line = await readline(); + line = line.trim() + N = Number(line); + + line = await readline(); + line = line.trim().split(' ') + beginStr = line[0] + endStr = line[1] + + for (let i = 0; i < N; i++) { + line = await readline() + line = line.trim() + strSet.add(line.trim()) + } +} + +(async function () { + + // 读取输入,初始化数据 + await init() + + // 初始化队列 + let queue = [] + queue.push(beginStr) + + // 初始化visitMap + visitedMap.set(beginStr, 1) + + while (queue.length) { + let word = queue.shift() + let path = visitedMap.get(word) + + // 遍历26个字母 + for (let i = 0; i < word.length; i++) { + let newWord = word.split('') // 用一个新字符串替换str,因为每次要置换一个字符 + for (let j = 0; j < 26; j++) { + newWord[i] = String.fromCharCode('a'.charCodeAt() + j) + // 发现替换字母后,字符串与终点字符串相同 + if (newWord.join('') === endStr) { + console.log(path + 1); + return 0; // 找到了路径 + } + + // 字符串集合里出现了newWord,并且newWord没有被访问过 + if (strSet.has(newWord.join('')) && !visitedMap.has(newWord.join(''))) { + // 添加访问信息,并将新字符串放到队列中 + queue.push(newWord.join('')) + visitedMap.set(newWord.join(''), path + 1) + } + } + } + } + + console.log(0); +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0117.\350\275\257\344\273\266\346\236\204\345\273\272.md" "b/problems/kamacoder/0117.\350\275\257\344\273\266\346\236\204\345\273\272.md" new file mode 100644 index 0000000000..c5650d9708 --- /dev/null +++ "b/problems/kamacoder/0117.\350\275\257\344\273\266\346\236\204\345\273\272.md" @@ -0,0 +1,542 @@ + +

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

+ +# 拓扑排序精讲 + +[卡码网:117. 软件构建](https://kamacoder.com/problempage.php?pid=1191) + +题目描述: + +某个大型软件项目的构建系统拥有 N 个文件,文件编号从 0 到 N - 1,在这些文件中,某些文件依赖于其他文件的内容,这意味着如果文件 A 依赖于文件 B,则必须在处理文件 A 之前处理文件 B (0 <= A, B <= N - 1)。请编写一个算法,用于确定文件处理的顺序。 + +输入描述: + +第一行输入两个正整数 N, M。表示 N 个文件之间拥有 M 条依赖关系。 + +后续 M 行,每行两个正整数 S 和 T,表示 T 文件依赖于 S 文件。 + +输出描述: + +输出共一行,如果能处理成功,则输出文件顺序,用空格隔开。 + +如果不能成功处理(相互依赖),则输出 -1。 + +输入示例: + +``` +5 4 +0 1 +0 2 +1 3 +2 4 +``` + +输出示例: + +0 1 2 3 4 + +提示信息: + +文件依赖关系如下: + +![](https://file1.kamacoder.com/i/algo/20240510192157.png) + +所以,文件处理的顺序除了示例中的顺序,还存在 + +0 2 4 1 3 + +0 2 1 3 4 + +等等合法的顺序。 + +数据范围: + +* 0 <= N <= 10 ^ 5 +* 1 <= M <= 10 ^ 9 + + +## 拓扑排序的背景 + +本题是拓扑排序的经典题目。 + +一聊到 拓扑排序,一些录友可能会想这是排序,不会想到这是图论算法。 + +其实拓扑排序是经典的图论问题。 + +先说说 拓扑排序的应用场景。 + +大学排课,例如 先上A课,才能上B课,上了B课才能上C课,上了A课才能上D课,等等一系列这样的依赖顺序。 问给规划出一条 完整的上课顺序。 + +拓扑排序在文件处理上也有应用,我们在做项目安装文件包的时候,经常发现 复杂的文件依赖关系, A依赖B,B依赖C,B依赖D,C依赖E 等等。 + +如果给出一条线性的依赖顺序来下载这些文件呢? + +有录友想上面的例子都很简单啊,我一眼能给排序出来。 + +那如果上面的依赖关系是一百对呢,一千对甚至上万个依赖关系,这些依赖关系中可能还有循环依赖,你如何发现循环依赖呢,又如果排出线性顺序呢。 + +所以 拓扑排序就是专门解决这类问题的。 + +概括来说,**给出一个 有向图,把这个有向图转成线性的排序 就叫拓扑排序**。 + +当然拓扑排序也要检测这个有向图 是否有环,即存在循环依赖的情况,因为这种情况是不能做线性排序的。 + +所以**拓扑排序也是图论中判断有向无环图的常用方法**。 + +------------ + + +## 拓扑排序的思路 + +拓扑排序指的是一种 解决问题的大体思路, 而具体算法,可能是广搜也可能是深搜。 + +大家可能发现 各式各样的解法,纠结哪个是拓扑排序? + +其实只要能在把 有向无环图 进行线性排序 的算法 都可以叫做 拓扑排序。 + +实现拓扑排序的算法有两种:卡恩算法(BFS)和DFS + +> 卡恩1962年提出这种解决拓扑排序的思路 + +一般来说我们只需要掌握 BFS (广度优先搜索)就可以了,清晰易懂,如果还想多了解一些,可以再去学一下 DFS 的思路,但 DFS 不是本篇重点。 + +接下来我们来讲解BFS的实现思路。 + +以题目中示例为例如图: + +![](https://file1.kamacoder.com/i/algo/20240510110836.png) + +做拓扑排序的话,如果肉眼去找开头的节点,一定能找到 节点0 吧,都知道要从节点0 开始。 + +但为什么我们能找到 节点0呢,因为我们肉眼看着 这个图就是从 节点0出发的。 + +作为出发节点,它有什么特征? + +你看节点0 的入度 为0 出度为2, 也就是 没有边指向它,而它有两条边是指出去的。 + +> 节点的入度表示 有多少条边指向它,节点的出度表示有多少条边 从该节点出发。 + +所以当我们做拓扑排序的时候,应该优先找 入度为 0 的节点,只有入度为0,它才是出发节点。 +**理解以上内容很重要**! + +接下来我给出 拓扑排序的过程,其实就两步: + +1. 找到入度为0 的节点,加入结果集 +2. 将该节点从图中移除 + +循环以上两步,直到 所有节点都在图中被移除了。 + +结果集的顺序,就是我们想要的拓扑排序顺序 (结果集里顺序可能不唯一) + +## 模拟过程 + +用本题的示例来模拟这一过程: + + +1、找到入度为0 的节点,加入结果集 + +![](https://file1.kamacoder.com/i/algo/20240510113110.png) + +2、将该节点从图中移除 + +![](https://file1.kamacoder.com/i/algo/20240510113142.png) + +---------------- + +1、找到入度为0 的节点,加入结果集 + +![](https://file1.kamacoder.com/i/algo/20240510113345.png) + +这里大家会发现,节点1 和 节点2 入度都为0, 选哪个呢? + +选哪个都行,所以这也是为什么拓扑排序的结果是不唯一的。 + +2、将该节点从图中移除 + +![](https://file1.kamacoder.com/i/algo/20240510113640.png) + +--------------- + +1、找到入度为0 的节点,加入结果集 + +![](https://file1.kamacoder.com/i/algo/20240510113853.png) + +节点2 和 节点3 入度都为0,选哪个都行,这里选节点2 + +2、将该节点从图中移除 + +![](https://file1.kamacoder.com/i/algo/20240510114004.png) + +-------------- + +后面的过程一样的,节点3 和 节点4,入度都为0,选哪个都行。 + +最后结果集为: 0 1 2 3 4 。当然结果不唯一的。 + +## 判断有环 + +如果有 有向环怎么办呢?例如这个图: + +![](https://file1.kamacoder.com/i/algo/20240510115115.png) + +这个图,我们只能将入度为0 的节点0 接入结果集。 + +之后,节点1、2、3、4 形成了环,找不到入度为0 的节点了,所以此时结果集里只有一个元素。 + +那么如果我们发现结果集元素个数 不等于 图中节点个数,我们就可以认定图中一定有 有向环! + +这也是拓扑排序判断有向环的方法。 + +通过以上过程的模拟大家会发现这个拓扑排序好像不难,还有点简单。 + +## 写代码 + +理解思想后,确实不难,但代码写起来也不容易。 + +为了每次可以找到所有节点的入度信息,我们要在初始化的时候,就把每个节点的入度 和 每个节点的依赖关系做统计。 + +代码如下: + +```CPP +cin >> n >> m; +vector inDegree(n, 0); // 记录每个文件的入度 +vector result; // 记录结果 +unordered_map> umap; // 记录文件依赖关系 + +while (m--) { + // s->t,先有s才能有t + cin >> s >> t; + inDegree[t]++; // t的入度加一 + umap[s].push_back(t); // 记录s指向哪些文件 +} + +``` + +找入度为0 的节点,我们需要用一个队列放存放。 + +因为每次寻找入度为0的节点,不一定只有一个节点,可能很多节点入度都为0,所以要将这些入度为0的节点放到队列里,依次去处理。 + +代码如下: + +```CPP + +queue que; +for (int i = 0; i < n; i++) { + // 入度为0的节点,可以作为开头,先加入队列 + if (inDegree[i] == 0) que.push(i); +} +``` + +开始从队列里遍历入度为0 的节点,将其放入结果集。 + +```CPP + +while (que.size()) { + int cur = que.front(); // 当前选中的节点 + que.pop(); + result.push_back(cur); + // 将该节点从图中移除 + +} +``` + +这里面还有一个很重要的过程,如何把这个入度为0的节点从图中移除呢? + +首先我们为什么要把节点从图中移除? + +为的是将 该节点作为出发点所连接的边删掉。 + +删掉的目的是什么呢? + +要把 该节点作为出发点所连接的节点的 入度 减一。 + +如果这里不理解,看上面的模拟过程第一步: + +![](https://file1.kamacoder.com/i/algo/20240510113110.png) + +这事节点1 和 节点2 的入度为 1。 + +将节点0删除后,图为这样: + +![](https://file1.kamacoder.com/i/algo/20240510113142.png) + +那么 节点0 作为出发点 所连接的节点的入度 就都做了 减一 的操作。 + +此时 节点1 和 节点 2 的入度都为0, 这样才能作为下一轮选取的节点。 + +所以,我们在代码实现的过程中,本质是要将 该节点作为出发点所连接的节点的 入度 减一 就可以了,这样好能根据入度找下一个节点,不用真在图里把这个节点删掉。 + +该过程代码如下: + + +```CPP + +while (que.size()) { + int cur = que.front(); // 当前选中的节点 + que.pop(); + result.push_back(cur); + // 将该节点从图中移除 + vector files = umap[cur]; //获取cur指向的节点 + if (files.size()) { // 如果cur有指向的节点 + for (int i = 0; i < files.size(); i++) { // 遍历cur指向的节点 + inDegree[files[i]] --; // cur指向的节点入度都做减一操作 + // 如果指向的节点减一之后,入度为0,说明是我们要选取的下一个节点,放入队列。 + if(inDegree[files[i]] == 0) que.push(files[i]); + } + } + +} +``` + +最后代码如下: + + +```CPP +#include +#include +#include +#include +using namespace std; +int main() { + int m, n, s, t; + cin >> n >> m; + vector inDegree(n, 0); // 记录每个文件的入度 + + unordered_map> umap;// 记录文件依赖关系 + vector result; // 记录结果 + + while (m--) { + // s->t,先有s才能有t + cin >> s >> t; + inDegree[t]++; // t的入度加一 + umap[s].push_back(t); // 记录s指向哪些文件 + } + queue que; + for (int i = 0; i < n; i++) { + // 入度为0的文件,可以作为开头,先加入队列 + if (inDegree[i] == 0) que.push(i); + //cout << inDegree[i] << endl; + } + // int count = 0; + while (que.size()) { + int cur = que.front(); // 当前选中的文件 + que.pop(); + //count++; + result.push_back(cur); + vector files = umap[cur]; //获取该文件指向的文件 + if (files.size()) { // cur有后续文件 + for (int i = 0; i < files.size(); i++) { + inDegree[files[i]] --; // cur的指向的文件入度-1 + if(inDegree[files[i]] == 0) que.push(files[i]); + } + } + } + if (result.size() == n) { + for (int i = 0; i < n - 1; i++) cout << result[i] << " "; + cout << result[n - 1]; + } else cout << -1 << endl; + + +} +``` + +## 其他语言版本 + +### Java + +```java +import java.util.*; + + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int m = scanner.nextInt(); + + List> umap = new ArrayList<>(); // 记录文件依赖关系 + int[] inDegree = new int[n]; // 记录每个文件的入度 + + for (int i = 0; i < n; i++) + umap.add(new ArrayList<>()); + + for (int i = 0; i < m; i++) { + int s = scanner.nextInt(); + int t = scanner.nextInt(); + umap.get(s).add(t); // 记录s指向哪些文件 + inDegree[t]++; // t的入度加一 + } + + Queue queue = new LinkedList<>(); + for (int i = 0; i < n; i++) { + if (inDegree[i] == 0) { + // 入度为0的文件,可以作为开头,先加入队列 + queue.add(i); + } + } + + List result = new ArrayList<>(); + + // 拓扑排序 + while (!queue.isEmpty()) { + int cur = queue.poll(); // 当前选中的文件 + result.add(cur); + for (int file : umap.get(cur)) { + inDegree[file]--; // cur的指向的文件入度-1 + if (inDegree[file] == 0) { + queue.add(file); + } + } + } + + if (result.size() == n) { + for (int i = 0; i < result.size(); i++) { + System.out.print(result.get(i)); + if (i < result.size() - 1) { + System.out.print(" "); + } + } + } else { + System.out.println(-1); + } + } +} +``` + + + +### Python + +```python +from collections import deque, defaultdict + +def topological_sort(n, edges): + inDegree = [0] * n # inDegree 记录每个文件的入度 + umap = defaultdict(list) # 记录文件依赖关系 + + # 构建图和入度表 + for s, t in edges: + inDegree[t] += 1 + umap[s].append(t) + + # 初始化队列,加入所有入度为0的节点 + queue = deque([i for i in range(n) if inDegree[i] == 0]) + result = [] + + while queue: + cur = queue.popleft() # 当前选中的文件 + result.append(cur) + for file in umap[cur]: # 获取该文件指向的文件 + inDegree[file] -= 1 # cur的指向的文件入度-1 + if inDegree[file] == 0: + queue.append(file) + + if len(result) == n: + print(" ".join(map(str, result))) + else: + print(-1) + + +if __name__ == "__main__": + n, m = map(int, input().split()) + edges = [tuple(map(int, input().split())) for _ in range(m)] + topological_sort(n, edges) +``` + + + +### Go + +### Rust + +### JavaScript + +```javascript +const r1 = require('readline').createInterface({ input: process.stdin }); +// 创建readline接口 +let iter = r1[Symbol.asyncIterator](); +// 创建异步迭代器 +const readline = async () => (await iter.next()).value; + + +let N, M // 节点数和边数 +let inDegrees = [] // 入度 +let umap = new Map() // 记录文件依赖关系 +let result = [] // 结果 + + +// 根据输入, 初始化数据 +const init = async () => { + // 读取第一行输入 + let line = await readline(); + [N, M] = line.split(' ').map(Number) + + inDegrees = new Array(N).fill(0) + + // 读取边集 + while (M--) { + line = await readline(); + let [x, y] = line.split(' ').map(Number) + + // 记录入度 + inDegrees[y]++ + + // 记录x指向哪些文件 + if (!umap.has(x)) { + umap.set(x, [y]) + } else { + umap.get(x).push(y) + } + } +} + +(async function () { + // 根据输入, 初始化数据 + await init() + + let queue = [] // 入度为0的节点 + for (let i = 0; i < N; i++) { + if (inDegrees[i] == 0) { + queue.push(i) + } + } + + while (queue.length) { + let cur = queue.shift() //当前文件 + + result.push(cur) + + let files = umap.get(cur) // 当前文件指向的文件 + + // 当前文件指向的文件入度减1 + if (files && files.length) { + for (let i = 0; i < files.length; i++) { + inDegrees[files[i]]-- + if (inDegrees[files[i]] == 0) queue.push(files[i]) + } + } + } + + // 这里result.length == N 一定要判断, 因为可能存在环 + if (result.length == N) return console.log(result.join(' ')) + console.log(-1) +})() +``` + + + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +
diff --git "a/problems/kamacoder/0126.\351\252\221\345\243\253\347\232\204\346\224\273\345\207\273astar.md" "b/problems/kamacoder/0126.\351\252\221\345\243\253\347\232\204\346\224\273\345\207\273astar.md" new file mode 100644 index 0000000000..6669ce7ba1 --- /dev/null +++ "b/problems/kamacoder/0126.\351\252\221\345\243\253\347\232\204\346\224\273\345\207\273astar.md" @@ -0,0 +1,687 @@ + +# A * 算法精讲 (A star算法) + +[卡码网:126. 骑士的攻击](https://kamacoder.com/problempage.php?pid=1203) + +题目描述 + +在象棋中,马和象的移动规则分别是“马走日”和“象走田”。现给定骑士的起始坐标和目标坐标,要求根据骑士的移动规则,计算从起点到达目标点所需的最短步数。 + +骑士移动规则如图,红色是起始位置,黄色是骑士可以走的地方。 + +![](https://file1.kamacoder.com/i/algo/20240626104833.png) + +棋盘大小 1000 x 1000(棋盘的 x 和 y 坐标均在 [1, 1000] 区间内,包含边界) + +输入描述 + +第一行包含一个整数 n,表示测试用例的数量。 + +接下来的 n 行,每行包含四个整数 a1, a2, b1, b2,分别表示骑士的起始位置 (a1, a2) 和目标位置 (b1, b2)。 + +输出描述 + +输出共 n 行,每行输出一个整数,表示骑士从起点到目标点的最短路径长度。 + +输入示例 + +``` +6 +5 2 5 4 +1 1 2 2 +1 1 8 8 +1 1 8 7 +2 1 3 3 +4 6 4 6 +``` + +输出示例 + +``` +2 +4 +6 +5 +1 +0 +``` + + +## 思路 + +我们看到这道题目的第一个想法就是广搜,这也是最经典的广搜类型题目。 + +这里我直接给出广搜的C++代码: + +```CPP +#include +#include +#include +using namespace std; +int moves[1001][1001]; +int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-1,-2}; +void bfs(int a1,int a2, int b1, int b2) +{ + queue q; + q.push(a1); + q.push(a2); + while(!q.empty()) + { + int m=q.front(); q.pop(); + int n=q.front(); q.pop(); + if(m == b1 && n == b2) + break; + for(int i=0;i<8;i++) + { + int mm=m + dir[i][0]; + int nn=n + dir[i][1]; + if(mm < 1 || mm > 1000 || nn < 1 || nn > 1000) + continue; + if(!moves[mm][nn]) + { + moves[mm][nn]=moves[m][n]+1; + q.push(mm); + q.push(nn); + } + } + } +} + +int main() +{ + int n, a1, a2, b1, b2; + cin >> n; + while (n--) { + cin >> a1 >> a2 >> b1 >> b2; + memset(moves,0,sizeof(moves)); + bfs(a1, a2, b1, b2); + cout << moves[b1][b2] << endl; + } + return 0; +} + +``` + +提交后,大家会发现,超时了。 + +因为本题地图足够大,且 n 也有可能很大,导致有非常多的查询。 + +我们来看一下广搜的搜索过程,如图,红色是起点,绿色是终点,黄色是要遍历的点,最后从 起点 找到 达到终点的最短路径是棕色。 + +![](https://file1.kamacoder.com/i/algo/20240611143712.png) + +可以看出 广搜中,做了很多无用的遍历, 黄色的格子是广搜遍历到的点。 + +这里我们能不能让便利方向,向这终点的方向去遍历呢? + +这样我们就可以避免很多无用遍历。 + + +## Astar + +Astar 是一种 广搜的改良版。 有的是 Astar是 dijkstra 的改良版。 + +其实只是场景不同而已 我们在搜索最短路的时候, 如果是无权图(边的权值都是1) 那就用广搜,代码简洁,时间效率和 dijkstra 差不多 (具体要取决于图的稠密) + +如果是有权图(边有不同的权值),优先考虑 dijkstra。 + +而 Astar 关键在于 启发式函数, 也就是 影响 广搜或者 dijkstra 从 容器(队列)里取元素的优先顺序。 + +以下,我用BFS版本的A * 来进行讲解。 + +在BFS中,我们想搜索,从起点到终点的最短路径,要一层一层去遍历。 + +![](https://file1.kamacoder.com/i/algo/20240611143712.png) + +如果 使用A * 的话,其搜索过程是这样的,如图,图中着色的都是我们要遍历的点。 + +![](https://file1.kamacoder.com/i/algo/20240611195223.png) + + +(上面两图中 最短路长度都是8,只是走的方式不同而已) + +大家可以发现 **BFS 是没有目的性的 一圈一圈去搜索, 而 A * 是有方向性的去搜索**。 + +看出 A * 可以节省很多没有必要的遍历步骤。 + +为了让大家可以明显看到区别,我将 BFS 和 A * 制作成可视化动图,大家可以自己看看动图,效果更好。 + +地址:https://kamacoder.com/tools/knight.html + +那么 A * 为什么可以有方向性的去搜索,它的如何知道方向呢? + +**其关键在于 启发式函数**。 + +那么启发式函数落实到代码处,如果指引搜索的方向? + +在本篇开篇中给出了BFS代码,指引 搜索的方向的关键代码在这里: + +```CPP +int m=q.front();q.pop(); +int n=q.front();q.pop(); +``` + +从队列里取出什么元素,接下来就是从哪里开始搜索。 + +**所以 启发式函数 要影响的就是队列里元素的排序**! + +这是影响BFS搜索方向的关键。 + +对队列里节点进行排序,就需要给每一个节点权值,如何计算权值呢? + +每个节点的权值为F,给出公式为:F = G + H + +G:起点达到目前遍历节点的距离 + +H:目前遍历的节点到达终点的距离 + +起点达到目前遍历节点的距离 + 目前遍历的节点到达终点的距离 就是起点到达终点的距离。 + +本题的图是无权网格状,在计算两点距离通常有如下三种计算方式: + +1. 曼哈顿距离,计算方式: d = abs(x1-x2)+abs(y1-y2) +2. 欧氏距离(欧拉距离) ,计算方式:d = sqrt( (x1-x2)^2 + (y1-y2)^2 ) +3. 切比雪夫距离,计算方式:d = max(abs(x1 - x2), abs(y1 - y2)) + +x1, x2 为起点坐标,y1, y2 为终点坐标 ,abs 为求绝对值,sqrt 为求开根号, + +选择哪一种距离计算方式 也会导致 A * 算法的结果不同。 + +本题,采用欧拉距离才能最大程度体现 点与点之间的距离。 + +所以 使用欧拉距离计算 和 广搜搜出来的最短路的节点数是一样的。 (路径可能不同,但路径上的节点数是相同的) + +我在制作动画演示的过程中,分别给出了曼哈顿、欧拉以及契比雪夫 三种计算方式下,A * 算法的寻路过程,大家可以自己看看看其区别。 + +动画地址:https://kamacoder.com/tools/knight.html + +计算出来 F 之后,按照 F 的 大小,来选去出队列的节点。 + +可以使用 优先级队列 帮我们排好序,每次出队列,就是F最小的节点。 + +实现代码如下:(启发式函数 采用 欧拉距离计算方式) + +```CPP +#include +#include +#include +using namespace std; +int moves[1001][1001]; +int dir[8][2]={-2,-1,-2,1,-1,2,1,2,2,1,2,-1,1,-2,-1,-2}; +int b1, b2; +// F = G + H +// G = 从起点到该节点路径消耗 +// H = 该节点到终点的预估消耗 + +struct Knight{ + int x,y; + int g,h,f; + bool operator < (const Knight & k) const{ // 重载运算符, 从小到大排序 + return k.f < f; + } +}; + +priority_queue que; + +int Heuristic(const Knight& k) { // 欧拉距离 + return (k.x - b1) * (k.x - b1) + (k.y - b2) * (k.y - b2); // 统一不开根号,这样可以提高精度 +} +void astar(const Knight& k) +{ + Knight cur, next; + que.push(k); + while(!que.empty()) + { + cur=que.top(); que.pop(); + if(cur.x == b1 && cur.y == b2) + break; + for(int i = 0; i < 8; i++) + { + next.x = cur.x + dir[i][0]; + next.y = cur.y + dir[i][1]; + if(next.x < 1 || next.x > 1000 || next.y < 1 || next.y > 1000) + continue; + if(!moves[next.x][next.y]) + { + moves[next.x][next.y] = moves[cur.x][cur.y] + 1; + + // 开始计算F + next.g = cur.g + 5; // 统一不开根号,这样可以提高精度,马走日,1 * 1 + 2 * 2 = 5 + next.h = Heuristic(next); + next.f = next.g + next.h; + que.push(next); + } + } + } +} + +int main() +{ + int n, a1, a2; + cin >> n; + while (n--) { + cin >> a1 >> a2 >> b1 >> b2; + memset(moves,0,sizeof(moves)); + Knight start; + start.x = a1; + start.y = a2; + start.g = 0; + start.h = Heuristic(start); + start.f = start.g + start.h; + astar(start); + while(!que.empty()) que.pop(); // 队列清空 + cout << moves[b1][b2] << endl; + } + return 0; +} + +``` + +## 复杂度分析 + +A * 算法的时间复杂度 其实是不好去量化的,因为他取决于 启发式函数怎么写。 + +最坏情况下,A * 退化成广搜,算法的时间复杂度 是 O(n * 2),n 为节点数量。 + +最佳情况,是从起点直接到终点,时间复杂度为 O(dlogd),d 为起点到终点的深度。 + +因为在搜索的过程中也需要堆排序,所以是 O(dlogd)。 + +实际上 A * 的时间复杂度是介于 最优 和最坏 情况之间, 可以 非常粗略的认为 A * 算法的时间复杂度是 O(nlogn) ,n 为节点数量。 + +A * 算法的空间复杂度 O(b ^ d) ,d 为起点到终点的深度,b 是 图中节点间的连接数量,本题因为是无权网格图,所以 节点间连接数量为 4。 + + +## 拓展 + +如果本题大家使用 曼哈顿距离 或者 切比雪夫距离 计算的话,可以提交试一试,有的最短路结果是并不是最短的。 + +原因也是 曼哈顿 和 切比雪夫这两种计算方式在 本题的网格地图中,都没有体现出点到点的真正距离! + +可能有些录友找到类似的题目,例如 [poj 2243](http://poj.org/problem?id=2243),使用 曼哈顿距离 提交也过了, 那是因为题目中的地图太小了,仅仅是一张 8 * 8的地图,根本看不出来 不同启发式函数写法的区别。 + +A * 算法 并不是一个明确的最短路算法,**A * 算法搜的路径如何,完全取决于 启发式函数怎么写**。 + +**A * 算法并不能保证一定是最短路**,因为在设计 启发式函数的时候,要考虑 时间效率与准确度之间的一个权衡。 + +虽然本题中,A * 算法得到是最短路,也是因为本题 启发式函数 和 地图结构都是最简单的。 + +例如在游戏中,在地图很大、不同路径权值不同、有障碍 且多个游戏单位在地图中寻路的情况,如果要计算准确最短路,耗时很大,会给玩家一种卡顿的感觉。 + +而真实玩家在玩游戏的时候,并不要求一定是最短路,次短路也是可以的 (玩家不一定能感受出来,及时感受出来也不是很在意),只要奔着目标走过去 大体就可以接受。 + +所以 在游戏开发设计中,**保证运行效率的情况下,A * 算法中的启发式函数 设计往往不是最短路,而是接近最短路的 次短路设计**。 + +大家如果玩 LOL,或者 王者荣耀 可以回忆一下:如果 从很远的地方点击 让英雄直接跑过去 是 跑的路径是不靠谱的,所以玩家们才会在 距离英雄尽可能近的位置去点击 让英雄跑过去。 + +## A * 的缺点 + +大家看上述 A * 代码的时候,可以看到 我们想 队列里添加了很多节点,但真正从队列里取出来的 仅仅是 靠启发式函数判断 距离终点最近的节点。 + +相对了 普通BFS,A * 算法只从 队列里取出 距离终点最近的节点。 + +那么问题来了,A * 在一次路径搜索中,大量不需要访问的节点都在队列里,会造成空间的过度消耗。 + +IDA * 算法 对这一空间增长问题进行了优化,关于 IDA * 算法,本篇不再做讲解,感兴趣的录友可以自行找资料学习。 + +另外还有一种场景 是 A * 解决不了的。 + +如果题目中,给出 多个可能的目标,然后在这多个目标中 选择最近的目标,这种 A * 就不擅长了, A * 只擅长给出明确的目标 然后找到最短路径。 + +如果是多个目标找最近目标(特别是潜在目标数量很多的时候),可以考虑 Dijkstra ,BFS 或者 Floyd。 + + +## 其他语言版本 + +### Java + +### Python + +```Python +import heapq + +n = int(input()) + +moves = [(1, 2), (2, 1), (-1, 2), (2, -1), (1, -2), (-2, 1), (-1, -2), (-2, -1)] + +def distance(a, b): + return ((a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2) ** 0.5 + +def bfs(start, end): + q = [(distance(start, end), start)] + step = {start: 0} + + while q: + d, cur = heapq.heappop(q) + if cur == end: + return step[cur] + for move in moves: + new = (move[0] + cur[0], move[1] + cur[1]) + if 1 <= new[0] <= 1000 and 1 <= new[1] <= 1000: + step_new = step[cur] + 1 + if step_new < step.get(new, float('inf')): + step[new] = step_new + heapq.heappush(q, (distance(new, end) + step_new, new)) + return False + +for _ in range(n): + a1, a2, b1, b2 = map(int, input().split()) + print(bfs((a1, a2), (b1, b2))) +``` + +### Go + +### Rust + +### JavaScript + +```js +class MinHeap { + constructor() { + this.val = [] + } + push(val) { + this.val.push(val) + if (this.val.length > 1) { + this.bubbleUp() + } + } + bubbleUp() { + let pi = this.val.length - 1 + let pp = Math.floor((pi - 1) / 2) + while (pi > 0 && this.val[pp][0] > this.val[pi][0]) { + ;[this.val[pi], this.val[pp]] = [this.val[pp], this.val[pi]] + pi = pp + pp = Math.floor((pi - 1) / 2) + } + } + pop() { + if (this.val.length > 1) { + let pp = 0 + let pi = this.val.length - 1 + ;[this.val[pi], this.val[pp]] = [this.val[pp], this.val[pi]] + const min = this.val.pop() + if (this.val.length > 1) { + this.sinkDown(0) + } + return min + } else if (this.val.length == 1) { + return this.val.pop() + } + + } + sinkDown(parentIdx) { + let pp = parentIdx + let plc = pp * 2 + 1 + let prc = pp * 2 + 2 + let pt = pp // temp pointer + if (plc < this.val.length && this.val[pp][0] > this.val[plc][0]) { + pt = plc + } + if (prc < this.val.length && this.val[pt][0] > this.val[prc][0]) { + pt = prc + } + if (pt != pp) { + ;[this.val[pp], this.val[pt]] = [this.val[pt], this.val[pp]] + this.sinkDown(pt) + } + } +} + +const moves = [ + [1, 2], + [2, 1], + [-1, -2], + [-2, -1], + [-1, 2], + [-2, 1], + [1, -2], + [2, -1] +] + +function dist(a, b) { + return ((a[0] - b[0])**2 + (a[1] - b[1])**2)**0.5 +} + +function isValid(x, y) { + return x >= 1 && y >= 1 && x < 1001 && y < 1001 +} + +function bfs(start, end) { + const step = new Map() + step.set(start.join(" "), 0) + const q = new MinHeap() + q.push([dist(start, end), start[0], start[1]]) + + while(q.val.length) { + const [d, x, y] = q.pop() + // if x and y correspond to end position output result + if (x == end[0] && y == end[1]) { + console.log(step.get(end.join(" "))) + break; + } + for (const [dx, dy] of moves) { + const nx = dx + x + const ny = dy + y + if (isValid(nx, ny)) { + const newStep = step.get([x, y].join(" ")) + 1 + const newDist = dist([nx, ny], [...end]) + const s = step.get([nx, ny].join(" ")) ? + step.get([nx, ny]) : + Number.MAX_VALUE + if (newStep < s) { + q.push( + [ + newStep + newDist, + nx, + ny + ] + ) + step.set([nx, ny].join(" "), newStep) + } + } + } + } +} + +async function main() { + const rl = require('readline').createInterface({ input: process.stdin }) + const iter = rl[Symbol.asyncIterator]() + const readline = async () => (await iter.next()).value + const n = Number((await readline())) + + // find min step + for (let i = 0 ; i < n ; i++) { + const [s1, s2, t1, t2] = (await readline()).split(" ").map(Number) + bfs([s1, s2], [t1, t2]) + } +} + +main() +``` + +### TypeScript + +### PhP + +### Swift + +### Scala + +### C# + +### Dart + +### C + +```C +#include +#include +#include + +// 定义一个结构体,表示棋盘上骑士的位置和相关的 A* 算法参数 +typedef struct { + int x, y; // 骑士在棋盘上的坐标 + int g; // 从起点到当前节点的实际消耗 + int h; // 从当前节点到目标节点的估计消耗(启发式函数值) + int f; // 总的估计消耗(f = g + h) +} Knight; + +#define MAX_HEAP_SIZE 2000000 // 假设优先队列的最大容量 + +// 定义一个优先队列,使用最小堆来实现 A* 算法中的 Open 列表 +typedef struct { + Knight data[MAX_HEAP_SIZE]; + int size; +} PriorityQueue; + +// 初始化优先队列 +void initQueue(PriorityQueue *pq) { + pq->size = 0; +} + +// 将骑士节点插入优先队列 +void push(PriorityQueue *pq, Knight k) { + if (pq->size >= MAX_HEAP_SIZE) { + // 堆已满,无法插入新节点 + return; + } + int i = pq->size++; + pq->data[i] = k; + // 上滤操作,维护最小堆的性质,使得 f 值最小的节点在堆顶 + while (i > 0) { + int parent = (i - 1) / 2; + if (pq->data[parent].f <= pq->data[i].f) { + break; + } + // 交换父节点和当前节点 + Knight temp = pq->data[parent]; + pq->data[parent] = pq->data[i]; + pq->data[i] = temp; + i = parent; + } +} + +// 从优先队列中弹出 f 值最小的骑士节点 +Knight pop(PriorityQueue *pq) { + Knight min = pq->data[0]; + pq->size--; + pq->data[0] = pq->data[pq->size]; + // 下滤操作,维护最小堆的性质 + int i = 0; + while (1) { + int left = 2 * i + 1; + int right = 2 * i + 2; + int smallest = i; + if (left < pq->size && pq->data[left].f < pq->data[smallest].f) { + smallest = left; + } + if (right < pq->size && pq->data[right].f < pq->data[smallest].f) { + smallest = right; + } + if (smallest == i) { + break; + } + // 交换当前节点与最小子节点 + Knight temp = pq->data[smallest]; + pq->data[smallest] = pq->data[i]; + pq->data[i] = temp; + i = smallest; + } + return min; +} + +// 判断优先队列是否为空 +int isEmpty(PriorityQueue *pq) { + return pq->size == 0; +} + +// 启发式函数:计算从当前位置到目标位置的欧几里得距离的平方(避免开方,提高效率) +int heuristic(int x, int y, int goal_x, int goal_y) { + int dx = x - goal_x; + int dy = y - goal_y; + return dx * dx + dy * dy; // 欧几里得距离的平方 +} + +// 用于记录从起点到棋盘上每个位置的最小移动次数 +int moves[1001][1001]; + +// 骑士在棋盘上的8个可能移动方向 +int dir[8][2] = { + {-2, -1}, {-2, 1}, {-1, 2}, {1, 2}, + {2, 1}, {2, -1}, {1, -2}, {-1, -2} +}; + +// 使用 A* 算法寻找从起点到目标点的最短路径 +int astar(int start_x, int start_y, int goal_x, int goal_y) { + PriorityQueue pq; + initQueue(&pq); + + // 初始化 moves 数组,-1 表示未访问过的位置 + memset(moves, -1, sizeof(moves)); + moves[start_x][start_y] = 0; // 起点位置的移动次数为 0 + + // 初始化起始节点 + Knight start; + start.x = start_x; + start.y = start_y; + start.g = 0; + start.h = heuristic(start_x, start_y, goal_x, goal_y); + start.f = start.g + start.h; // 总的估计消耗 + + push(&pq, start); // 将起始节点加入优先队列 + + while (!isEmpty(&pq)) { + Knight current = pop(&pq); // 取出 f 值最小的节点 + + // 如果已经到达目标位置,返回所需的最小移动次数 + if (current.x == goal_x && current.y == goal_y) { + return moves[current.x][current.y]; + } + + // 遍历当前节点的所有可能移动方向 + for (int i = 0; i < 8; i++) { + int nx = current.x + dir[i][0]; + int ny = current.y + dir[i][1]; + + // 检查新位置是否在棋盘范围内且未被访问过 + if (nx >= 1 && nx <= 1000 && ny >= 1 && ny <= 1000 && moves[nx][ny] == -1) { + moves[nx][ny] = moves[current.x][current.y] + 1; // 更新移动次数 + + // 创建新节点,表示骑士移动到的新位置 + Knight neighbor; + neighbor.x = nx; + neighbor.y = ny; + neighbor.g = current.g + 5; // 每次移动的消耗为 5(骑士移动的距离平方) + neighbor.h = heuristic(nx, ny, goal_x, goal_y); + neighbor.f = neighbor.g + neighbor.h; + + push(&pq, neighbor); // 将新节点加入优先队列 + } + } + } + + return -1; // 如果无法到达目标位置,返回 -1 +} + +int main() { + int n; + scanf("%d", &n); + while (n--) { + int a1, a2, b1, b2; // 起点和目标点的坐标 + scanf("%d %d %d %d", &a1, &a2, &b1, &b2); + + int result = astar(a1, a2, b1, b2); // 使用 A* 算法计算最短路径 + printf("%d\n", result); // 输出最小移动次数 + } + return 0; +} + +``` + + + + + + +
diff --git "a/problems/kamacoder/\345\233\276\350\256\272\344\270\272\344\273\200\344\271\210\347\224\250ACM\346\250\241\345\274\217.md" "b/problems/kamacoder/\345\233\276\350\256\272\344\270\272\344\273\200\344\271\210\347\224\250ACM\346\250\241\345\274\217.md" new file mode 100644 index 0000000000..c5122b1760 --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\344\270\272\344\273\200\344\271\210\347\224\250ACM\346\250\241\345\274\217.md" @@ -0,0 +1,94 @@ + + + +# 图论为什么统一使用ACM模式 + +代码随想录图论章节给大家统一换成ACM输入输出模式。 + +图论是在笔试还有面试中,通常都是以ACM模式来考察大家,而大家习惯在力扣刷题(核心代码模式),核心代码模式对图的存储和输出都隐藏了。 + +而图论题目的输出输出 相对其他类型题目来说是最难处理的。 + +ACM模式是最考察候选人对代码细节把控程度的, 图的构成,图的输出,这些只有ACM输入输出模式才能体现出来。 + +### 输入的细节 + +图论的输入难在 图的存储结构,**如果没有练习过 邻接表和邻接矩阵 ,很多录友是写不出来的**。 + +而力扣上是直接给好现成的 数据结构,可以直接用,所以练习不到图的输入,也练习不到邻接表和邻接矩阵。 + +**如果连邻接表 和 邻接矩阵都不知道或者写不出来的话,可以说 图论没有入门**。 + +举个例子,对于力扣 [797.所有可能的路径](https://leetcode.cn/problems/all-paths-from-source-to-target/description/) ,录友了解深度优先搜索之后,这道题目就是模板题,是送分题。 + +如果面试的时候出一道原题 (笔试都是ACM模式,部分面试也是ACM模式),不少熟练刷力扣的录友都难住了,**因为不知道图应该怎么存,也不知道自己存的图如何去遍历**。 + +即使面试的时候,有的面试官,让你用核心代码模式做题,当你写出代码后,**面试官补充一句:这个图 你是怎么存的**? + +难道和面试官说:我只知道图的算法,但我不知道图怎么存。 + +后面大家在刷 代码随想录图论第一题[98. 所有可达路径](./0098.所有可达路径.md) 的时候,就可以感受到图存储的难点所在。 + +所以这也是为什么我要让大家练习 ACM模式,也是我为什么 在代码随想录图论讲解中,不惜自己亲自出题,让大家统一练习ACM模式。 + +### 输出的细节 + +同样,图论的输出也有细节,例如 求节点1 到节点5的所有路径, 输出可能是: + +``` +1 2 4 5 +1 3 5 +``` + +表示有两条路可以到节点5, 那储存这个结果需要二维数组,最后在一起输出,力扣是直接return数组就好了,但 ACM模式要求我们自己输出,这里有就细节了。 + +就拿 只输出一行数据,输出 `1 2 4 5` 来说, + +很多录友代码可能直接就这么写了: + +```CPP +for (int i = 0 ; i < result.size(); i++) { + cout << result[i] << " "; +} +``` + +这么写输出的结果是 `1 2 4 5 `, 发现结果是对的,一提交,发现OJ返回 格式错误 或者 结果错误。 + +如果没练习过这种输出方式的录友,就开始怀疑了,这结果一样一样的,怎么就不对,我在力扣上提交都是对的! + +**大家要注意,5 后面要不要有空格**! + +上面这段代码输出,5后面是加上了空格了,如果判题机判断 结果的长度,标准答案`1 2 4 5`长度是7,而上面代码输出的长度是 8,很明显就是不对的。 + +所以正确的写法应该是: + +```CPP +for (int i = 0 ; i < result.size() - 1; i++) { + cout << result[i] << " "; +} +cout << result[result.size() - 1]; +``` + +这么写,最后一个元素后面就没有空格了。 + +这是很多录友经常疏忽的,也是大家刷习惯了 力扣(核心代码模式)根本不会注意到的细节。 + +**同样在工程开发中,这些细节都是影响系统稳定运行的因素之一**。 + +**ACM模式 除了考验算法思路,也考验 大家对 代码的把控力度**, 而 核心代码模式 只注重算法的解题思路,所以输入输出这些就省略掉了。 + + +### 其他 + +**大家如果熟练ACM模式,那么核心代码模式没问题,但反过来就不一定了**。 + +而且我在讲解图论的时候,最头疼的就是找题,在力扣上 找题总是找不到符合思路且来完整表达算法精髓的题目。 + +特别是最短路算法相关的题目,例如 Bellman_ford系列 ,Floyd ,A * 等等总是找不到符合思路的题目。 + +索性统一我自己来出题,这其中也是巨大的工作量。为了给大家带来极致的学习体验,我在很多细节上都下了功夫。 + +等大家将图论刷完,就会感受到我的良苦用心。加油 + + +
diff --git "a/problems/kamacoder/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" new file mode 100644 index 0000000000..61d49b5297 --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\345\271\266\346\237\245\351\233\206\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -0,0 +1,457 @@ +# 并查集理论基础 + +接下来我们来讲一下并查集,首先当然是并查集理论基础。 + +## 背景 + +首先要知道并查集可以解决什么问题呢? + +并查集常用来解决连通性问题。 + +大白话就是当我们需要判断两个元素是否在同一个集合里的时候,我们就要想到用并查集。 + +并查集主要有两个功能: + +* 将两个元素添加到一个集合中。 +* 判断两个元素在不在同一个集合 + +接下来围绕并查集的这两个功能来展开讲解。 + +## 原理讲解 + +从代码层面,我们如何将两个元素添加到同一个集合中呢。 + +此时有录友会想到:可以把他放到同一个数组里或者set 或者 map 中,这样就表述两个元素在同一个集合。 + +那么问题来了,对这些元素分门别类,可不止一个集合,可能是很多集合,成百上千,那么要定义这么多个数组吗? + +有录友想,那可以定义一个二维数组。 + +但如果我们要判断两个元素是否在同一个集合里的时候 我们又能怎么办? 只能把而二维数组都遍历一遍。 + +而且每当想添加一个元素到某集合的时候,依然需要把把二维数组都遍历一遍,才知道要放在哪个集合里。 + +这仅仅是一个粗略的思路,如果沿着这个思路去实现代码,非常复杂,因为管理集合还需要很多逻辑。 + +那么我们来换一个思路来看看。 + +我们将三个元素A,B,C (分别是数字)放在同一个集合,其实就是将三个元素连通在一起,如何连通呢。 + +只需要用一个一维数组来表示,即:father[A] = B,father[B] = C 这样就表述 A 与 B 与 C连通了(有向连通图)。 + +代码如下: + +``` CPP +// 将v,u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +``` + +可能有录友想,这样我可以知道 A 连通 B,因为 A 是索引下标,根据 father[A]的数值就知道 A 连通 B。那怎么知道 B 连通 A呢? + +我们的目的是判断这三个元素是否在同一个集合里,知道 A 连通 B 就已经足够了。 + +这里要讲到寻根思路,只要 A ,B,C 在同一个根下就是同一个集合。 + +给出A元素,就可以通过 father[A] = B,father[B] = C,找到根为 C。 + +给出B元素,就可以通过 father[B] = C,找到根也为为 C,说明 A 和 B 是在同一个集合里。 +大家会想第一段代码里find函数是如何实现的呢?其实就是通过数组下标找到数组元素,一层一层寻根过程,代码如下: + +```CPP +// 并查集里寻根的过程 +int find(int u) { + if (u == father[u]) return u; // 如果根就是自己,直接返回 + else return find(father[u]); // 如果根不是自己,就根据数组下标一层一层向下找 +} + +``` + + +如何表示 C 也在同一个元素里呢? 我们需要 father[C] = C,即C的根也为C,这样就方便表示 A,B,C 都在同一个集合里了。 + +所以father数组初始化的时候要 father[i] = i,默认自己指向自己。 + +代码如下: + +```CPP +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} +``` + +最后我们如何判断两个元素是否在同一个集合里,如果通过 find函数 找到 两个元素属于同一个根的话,那么这两个元素就是同一个集合,代码如下: + + +```CPP +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} +``` + +## 路径压缩 + +在实现 find 函数的过程中,我们知道,通过递归的方式,不断获取father数组下标对应的数值,最终找到这个集合的根。 + +搜索过程像是一个多叉树中从叶子到根节点的过程,如图: + +![](https://file1.kamacoder.com/i/algo/20230602102619.png) + +如果这棵多叉树高度很深的话,每次find函数 去寻找根的过程就要递归很多次。 + +我们的目的只需要知道这些节点在同一个根下就可以,所以对这棵多叉树的构造只需要这样就可以了,如图: + +![](https://file1.kamacoder.com/i/algo/20230602103040.png) + +除了根节点其他所有节点都挂载根节点下,这样我们在寻根的时候就很快,只需要一步, + +如果我们想达到这样的效果,就需要 **路径压缩**,将非根节点的所有节点直接指向根节点。 +那么在代码层面如何实现呢? + +我们只需要在递归的过程中,让 father[u] 接住 递归函数 find(father[u]) 的返回结果。 + +因为 find 函数向上寻找根节点,father[u] 表述 u 的父节点,那么让 father[u] 直接获取 find函数 返回的根节点,这样就让节点 u 的父节点 变成根节点。 + +代码如下,注意看注释,路径压缩就一行代码: + +```CPP +// 并查集里寻根的过程 +int find(int u) { + if (u == father[u]) return u; + else return father[u] = find(father[u]); // 路径压缩 +} +``` + +以上代码在C++中,可以用三元表达式来精简一下,代码如下: + +```CPP +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); +} + +``` + +相信不少录友在学习并查集的时候,对上面这三行代码实现的 find函数 很熟悉,但理解上却不够深入,仅仅知道这行代码很好用,不知道这里藏着路径压缩的过程。 + +所以对于算法初学者来说,直接看精简代码学习是不太友好的,往往忽略了很多细节。 + +## 代码模板 + +那么此时并查集的模板就出来了, 整体模板C++代码如下: + +```CPP +int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 +vector father = vector (n, 0); // C++里的一种数组结构 + +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + } +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +``` +通过模板,我们可以知道,并查集主要有三个功能。 + +1. 寻找根节点,函数:find(int u),也就是判断这个节点的祖先节点是哪个 +2. 将两个节点接入到同一个集合,函数:join(int u, int v),将两个节点连在同一个根节点上 +3. 判断两个节点是否在同一个集合,函数:isSame(int u, int v),就是判断两个节点是不是同一个根节点 + +## 常见误区 + +这里估计有录友会想,模板中的 join 函数里的这段代码: + +```CPP +u = find(u); // 寻找u的根 +v = find(v); // 寻找v的根 +if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + +``` + +与 isSame 函数的实现是不是重复了? 如果抽象一下呢,代码如下: + +```CPP +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + if (isSame(u, v)) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +``` + +这样写可以吗? 好像看出去没问题,而且代码更精简了。 + +**其实这么写是有问题的**,在join函数中 我们需要寻找 u 和 v 的根,然后再进行连线在一起,而不是直接 用 u 和 v 连线在一起。 + +举一个例子: + +```CPP +join(1, 2); +join(3, 2); +``` + +此时构成的图是这样的: + +![](https://file1.kamacoder.com/i/algo/20230525111307.png) + +此时问 1,3是否在同一个集合,我们调用 `join(1, 2); join(3, 2);` 很明显本意要表示 1,3是在同一个集合。 + +但我们来看一下代码逻辑,当我们调用 `isSame(1, 3)`的时候,find(1) 返回的是1,find(3)返回的是3。 `return 1 == 3` 返回的是false,代码告诉我们 1 和 3 不在同一个集合,这明显不符合我们的预期,所以问题出在哪里? + +问题出在我们精简的代码上,即 join 函数 一定要先 通过find函数寻根再进行关联。 + +如果find函数是这么实现,再来看一下逻辑过程。 + +```CPP +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + if (u == v) return ; // 如果发现根相同,则说明在一个集合,不用两个节点相连直接返回 + father[v] = u; +} +``` + +分别将 这两对元素加入集合。 + +```CPP +join(1, 2); +join(3, 2); +``` + +当执行`join(3, 2)`的时候,会先通过find函数寻找 3的根为3,2的根为1 (第一个`join(1, 2)`,将2的根设置为1),所以最后是将1 指向 3。 + +构成的图是这样的: + +![](https://file1.kamacoder.com/i/algo/20230525112101.png) + +因为在join函数里,我们有find函数进行寻根的过程,这样就保证元素 1,2,3在这个有向图里是强连通的。 + +此时我们在调用 `isSame(1, 3)`的时候,find(1) 返回的是3,find(3) 返回的也是3,`return 3 == 3` 返回的是true,即告诉我们 元素 1 和 元素3 是 在同一个集合里的。 + + + + +## 模拟过程 + +(**凸显途径合并的过程,每一个join都要画图**) + +不少录友在接触并查集模板之后,用起来很娴熟,因为模板确实相对固定,但是对并查集内部数据组织方式以及如何判断是否是同一个集合的原理很模糊。 + +通过以上讲解之后,我再带大家一步一步去画一下,并查集内部数据连接方式。 + +1、`join(1, 8);` + +![](https://file1.kamacoder.com/i/algo/20231122112727.png) + + +2、`join(3, 8);` + +![](https://file1.kamacoder.com/i/algo/20231122113857.png) + +有录友可能想,`join(3, 8)` 在图中为什么 将 元素1 连向元素 3 而不是将 元素 8 连向 元素 3 呢? + +这一点 我在 「常见误区」标题下已经详细讲解了,因为在`join(int u, int v)`函数里 要分别对 u 和 v 寻根之后再进行关联。 + +3、`join(1, 7);` + +![](https://file1.kamacoder.com/i/algo/20231122114108.png) + + +4、`join(8, 5);` + +![](https://file1.kamacoder.com/i/algo/20231122114847.png) + +这里8的根是3,那么 5 应该指向 8 的根 3,这里的原因,我们在上面「常见误区」已经讲过了。 但 为什么 图中 8 又直接指向了 3 了呢? + +**因为路经压缩了** + +即如下代码在寻找根的过程中,会有路径压缩,减少 下次查询的路径长度。 + +```CPP +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : father[u] = find(father[u]); // 路径压缩 +} +``` + +5、`join(2, 9);` + +![](https://file1.kamacoder.com/i/algo/20231122115000.png) + +6、`join(6, 9);` + +![](https://file1.kamacoder.com/i/algo/20231122115404.png) + +这里为什么是 2 指向了 6,因为 9的根为 2,所以用2指向6。 + + + +大家看懂这个有向图后,相信应该知道如下函数的返回值了。 + +```CPP +cout << isSame(8, 7) << endl; +cout << isSame(7, 2) << endl; +``` + +返回值分别如下,表示,8 和 7 是同一个集合,而 7 和 2 不是同一个集合。 + +``` +true +false +``` + + + +## 拓展 + + +在「路径压缩」讲解中,我们知道如何靠压缩路径来缩短查询根节点的时间。 + +其实还有另一种方法:按秩(rank)合并。 + +rank表示树的高度,即树中结点层次的最大值。 + +例如两个集合(多叉树)需要合并,如图所示: + +![](https://file1.kamacoder.com/i/algo/20230602172250.png) + +树1 rank 为2,树2 rank 为 3。那么合并两个集合,是 树1 合入 树2,还是 树2 合入 树1呢? + +我们来看两个不同方式合入的效果。 + +![](https://file1.kamacoder.com/i/algo/20230602172933.png) + +这里可以看出,树2 合入 树1 会导致整棵树的高度变的更高,而 树1 合入 树2 整棵树的高度 和 树2 保持一致。 + +所以在 join函数中如何合并两棵树呢? + +一定是 rank 小的树合入 到 rank大 的树,这样可以保证最后合成的树rank 最小,降低在树上查询的路径长度。 + +按秩合并的代码如下: + +```CPP +int n = 1005; // n根据题目中节点数量而定,一般比节点数量大一点就好 +vector father = vector (n, 0); // C++里的一种数组结构 +vector rank = vector (n, 1); // 初始每棵树的高度都为1 + +// 并查集初始化 +void init() { + for (int i = 0; i < n; ++i) { + father[i] = i; + rank[i] = 1; // 也可以不写 + } +} +// 并查集里寻根的过程 +int find(int u) { + return u == father[u] ? u : find(father[u]);// 注意这里不做路径压缩 +} + +// 判断 u 和 v是否找到同一个根 +bool isSame(int u, int v) { + u = find(u); + v = find(v); + return u == v; +} + +// 将v->u 这条边加入并查集 +void join(int u, int v) { + u = find(u); // 寻找u的根 + v = find(v); // 寻找v的根 + + if (rank[u] <= rank[v]) father[u] = v; // rank小的树合入到rank大的树 + else father[v] = u; + + if (rank[u] == rank[v] && u != v) rank[v]++; // 如果两棵树高度相同,则v的高度+1,因为上面 if (rank[u] <= rank[v]) father[u] = v; 注意是 <= +} +``` + +可以注意到在上面的模板代码中,我是没有做路径压缩的,因为一旦做路径压缩,rank记录的高度就不准了,根据rank来判断如何合并就没有意义。 + +也可以在 路径压缩的时候,再去实时修生rank的数值,但这样在代码实现上麻烦了不少,关键是收益很小。 + +其实我们在优化并查集查询效率的时候,只用路径压缩的思路就够了,不仅代码实现精简,而且效率足够高。 + +按秩合并的思路并没有将树形结构尽可能的扁平化,所以在整理效率上是没有路径压缩高的。 + +说到这里可能有录友会想,那在路径压缩的代码中,只有查询的过程 即 find 函数的执行过程中会有路径压缩,如果一直没有使用find函数,是不是相当于这棵树就没有路径压缩,导致查询效率依然很低呢? + +大家可以再去回顾使用路径压缩的 并查集模板,在isSame函数 和 join函数中,我们都调用了 find 函数来进行寻根操作。 + +也就是说,无论使用并查集模板里哪一个函数(除了init函数),都会有路径压缩的过程,第二次访问相同节点的时候,这个节点就是直连根节点的,即 第一次访问的时候它的路径就被压缩了。 + +**所以这里推荐大家直接使用路径压缩的并查集模板就好**,但按秩合并的优化思路我依然给大家讲清楚,有助于更深一步理解并查集的优化过程。 + +## 复杂度分析 + + +这里对路径压缩版并查集来做分析。 + +空间复杂度: O(n) ,申请一个father数组。 + +关于时间复杂度,如果想精确表达出来需要繁琐的数学证明,就不在本篇讲解范围内了,大家感兴趣可以自己去深入研究。 + +这里做一个简单的分析思路。 + +路径压缩后的并查集时间复杂度在O(logn)与O(1)之间,且随着查询或者合并操作的增加,时间复杂度会越来越趋于O(1)。 + +了解到这个程度对于求职面试来说就够了。 + +在第一次查询的时候,相当于是n叉树上从叶子节点到根节点的查询过程,时间复杂度是logn,但路径压缩后,后面的查询操作都是O(1),而 join 函数 和 isSame函数 里涉及的查询操作也是一样的过程。 + + +## 总结 + +本篇我们讲解了并查集的背景、原理、两种优化方式(路径压缩,按秩合并),代码模板,常见误区,以及模拟过程。 + +要知道并查集解决什么问题,在什么场景下我们要想到使用并查集。 + + +接下来进一步优化并查集的执行效率,重点介绍了路径压缩的方式,另一种方法:按秩合并,我们在 「拓展」中讲解。 + +通过一步一步的原理讲解,最后给出并查集的模板,所有的并查集题目都在这个模板的基础上进行操作或者适当修改。 + +但只给出模板还是不够的,针对大家学习并查集的常见误区,详细讲解了模板代码的细节。 + +为了让录友们进一步了解并查集的运行过程,我们再通过具体用例模拟一遍代码过程并画出对应的内部数据连接图(有向图)。 + +这里也建议大家去模拟一遍才能对并查集理解的更到位。 + +如果对模板代码还是有点陌生,不用担心,接下来我会讲解对应LeetCode上的并查集题目,通过一系列题目练习,大家就会感受到这套模板有多么的好用! + +敬请期待 并查集题目精讲系列。 + +
diff --git "a/problems/kamacoder/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" new file mode 100644 index 0000000000..718f5484a1 --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\345\271\277\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -0,0 +1,105 @@ +# 广度优先搜索理论基础 + +在[深度优先搜索](./图论深搜理论基础.md)的讲解中,我们就讲过深度优先搜索和广度优先搜索的区别。 + +广搜(bfs)是一圈一圈的搜索过程,和深搜(dfs)是一条路跑到黑然后再回溯。 + +## 广搜的使用场景 + +广搜的搜索方式就适合于解决两个点之间的最短路径问题。 + +因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。 + +当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,**这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行**。 (我们会在具体题目讲解中详细来说) + +## 广搜的过程 + +上面我们提过,BFS是一圈一圈的搜索过程,但具体是怎么一圈一圈来搜呢。 + +我们用一个方格地图,假如每次搜索的方向为 上下左右(不包含斜上方),那么给出一个start起始位置,那么BFS就是从四个方向走出第一步。 + +![图一](https://file1.kamacoder.com/i/algo/20220825104505.png) + +如果加上一个end终止位置,那么使用BFS的搜索过程如图所示: + +![图二](https://file1.kamacoder.com/i/algo/20220825102653.png) + +我们从图中可以看出,从start起点开始,是一圈一圈,向外搜索,方格编号1为第一步遍历的节点,方格编号2为第二步遍历的节点,第四步的时候我们找到终止点end。 + +正是因为BFS一圈一圈的遍历方式,所以一旦遇到终止点,那么一定是一条最短路径。 + +而且地图还可以有障碍,如图所示: + +![图三](https://file1.kamacoder.com/i/algo/20220825103900.png) + +在第五步,第六步 我只把关键的节点染色了,其他方向周边没有去染色,大家只要关注关键地方染色的逻辑就可以。 + +从图中可以看出,如果添加了障碍,我们是第六步才能走到end终点。 + +只要BFS只要搜到终点一定是一条最短路径,大家可以参考上面的图,自己再去模拟一下。 + +## 代码框架 + +大家应该好奇,这一圈一圈的搜索过程是怎么做到的,是放在什么容器里,才能这样去遍历。 + +很多网上的资料都是直接说用队列来实现。 + +其实,我们仅仅需要一个容器,能保存我们要遍历过的元素就可以,**那么用队列,还是用栈,甚至用数组,都是可以的**。 + +**用队列的话,就是保证每一圈都是一个方向去转,例如统一顺时针或者逆时针**。 + +因为队列是先进先出,加入元素和弹出元素的顺序是没有改变的。 + +**如果用栈的话,就是第一圈顺时针遍历,第二圈逆时针遍历,第三圈有顺时针遍历**。 + +因为栈是先进后出,加入元素和弹出元素的顺序改变了。 + +那么广搜需要注意 转圈搜索的顺序吗? 不需要! + +所以用队列,还是用栈都是可以的,但大家都习惯用队列了,**所以下面的讲解用我也用队列来讲,只不过要给大家说清楚,并不是非要用队列,用栈也可以**。 + +下面给出广搜代码模板,该模板针对的就是,上面的四方格的地图: (详细注释) + +```CPP +int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 表示四个方向 +// grid 是地图,也就是一个二维数组 +// visited标记访问过的节点,不要重复访问 +// x,y 表示开始搜索节点的下标 +void bfs(vector>& grid, vector>& visited, int x, int y) { + queue> que; // 定义队列 + que.push({x, y}); // 起始节点加入队列 + visited[x][y] = true; // 只要加入队列,立刻标记为访问过的节点 + while(!que.empty()) { // 开始遍历队列里的元素 + pair cur = que.front(); que.pop(); // 从队列取元素 + int curx = cur.first; + int cury = cur.second; // 当前节点坐标 + for (int i = 0; i < 4; i++) { // 开始想当前节点的四个方向左右上下去遍历 + int nextx = curx + dir[i][0]; + int nexty = cury + dir[i][1]; // 获取周边四个方向的坐标 + if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 坐标越界了,直接跳过 + if (!visited[nextx][nexty]) { // 如果节点没被访问过 + que.push({nextx, nexty}); // 队列添加该节点为下一轮要遍历的节点 + visited[nextx][nexty] = true; // 只要加入队列立刻标记,避免重复访问 + } + } + } + +} +``` + + +## 总结 + +当然广搜还有很多细节需要注意的地方,后面我会针对广搜的题目还做针对性的讲解。 + +**因为在理论篇讲太多细节,可能会让刚学广搜的录友们越看越懵**,所以细节方面针对具体题目在做讲解。 + +本篇我们重点讲解了广搜的使用场景,广搜的过程以及广搜的代码框架。 + +其实在二叉树章节的[层序遍历](https://programmercarl.com/0102.%E4%BA%8C%E5%8F%89%E6%A0%91%E7%9A%84%E5%B1%82%E5%BA%8F%E9%81%8D%E5%8E%86.html)中,我们也讲过一次广搜,相当于是广搜在二叉树这种数据结构上的应用。 + +这次则从图论的角度上再详细讲解一次广度优先遍历。 + +相信看完本篇,大家会对广搜有一个基础性的认识,后面再来做对应的题目就会得心应手一些。 + +
diff --git "a/problems/kamacoder/\345\233\276\350\256\272\346\200\273\347\273\223\347\257\207.md" "b/problems/kamacoder/\345\233\276\350\256\272\346\200\273\347\273\223\347\257\207.md" new file mode 100644 index 0000000000..d7b8da94cb --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,146 @@ + +# 图论总结篇 + +从深搜广搜 到并查集,从最小生成树到拓扑排序, 最后是最短路算法系列。 + +至此算上本篇,一共30篇文章,图论之旅就在此收官了。 + +在[0098.所有可达路径](./0098.所有可达路径.md) ,我们接触了两种图的存储方式,邻接表和邻接矩阵,掌握两种图的存储方式很重要。 + +图的存储方式也是大家习惯在核心代码模式下刷题 经常忽略的 知识点。因为在力扣上刷题不需要掌握图的存储方式。 + +## 深搜与广搜 + +在二叉树章节中,其实我们讲过了 深搜和广搜在二叉树上的搜索过程。 + +在图论章节中,深搜与广搜就是在图这个数据结构上的搜索过程。 + +深搜与广搜是图论里基本的搜索方法,大家需要掌握三点: + +* 搜索方式:深搜是可一个方向搜,不到黄河不回头。 广搜是围绕这起点一圈一圈的去搜。 +* 代码模板:需要熟练掌握深搜和广搜的基本写法。 +* 应用场景:图论题目基本上可以即用深搜也可用广搜,无疑是用哪个方便而已 + +### 注意事项 + +需要注意的是,同样是深搜模板题,会有两种写法。 + +在[0099.岛屿的数量深搜.md](./0099.岛屿的数量深搜.md) 和 [0105.有向图的完全可达性](./0105.有向图的完全可达性.md),涉及到dfs的两种写法。 + +**我们对dfs函数的定义是 是处理当前节点 还是处理下一个节点 很重要**,决定了两种dfs的写法。 + +这也是为什么很多录友看到不同的dfs写法,结果发现提交都能过的原因。 + +而深搜还有细节,有的深搜题目需要用到回溯的过程,有的就不用回溯的过程, + +一般是需要计算路径的问题 需要回溯,如果只是染色问题(岛屿问题系列) 就不需要回溯。 + +例如: [0105.有向图的完全可达性](./0105.有向图的完全可达性.md) 深搜就不需要回溯,而 [0098.所有可达路径](./0098.所有可达路径.md) 中的递归就需要回溯,文章中都有详细讲解 + +注意:以上说的是不需要回溯,不是没有回溯,只要有递归就会有回溯,只是我们是否需要用到回溯这个过程,这是需要考虑的。 + +很多录友写出来的广搜可能超时了, 例如题目:[0099.岛屿的数量广搜](./0099.岛屿的数量广搜.md) + +根本原因是**只要 加入队列就代表 走过,就需要标记,而不是从队列拿出来的时候再去标记走过**。 + +具体原因,我在[0099.岛屿的数量广搜](./0099.岛屿的数量广搜.md) 中详细讲了。 + +在深搜与广搜的讲解中,为了防止惯性思维,我特别加入了题目 [0106.岛屿的周长](./0106.岛屿的周长.md),提醒大家,看到类似的题目,也不要上来就想着深搜和广搜。 + +还有一些图的问题,在题目描述中,是没有图的,需要我们自己构建一个图,例如 [0110.字符串接龙](./0110.字符串接龙.md),题目中连线都没有,需要我们自己去思考 什么样的两个字符串可以连成线。 + +## 并查集 + +并查集相对来说是比较复杂的数据结构,其实他的代码不长,但想彻底学透并查集,需要从多个维度入手, + +我在理论基础篇的时候 讲解如下重点: + +* 为什么要用并查集,怎么不用个二维数据,或者set、map之类的。 +* 并查集能解决那些问题,哪些场景会用到并查集 +* 并查集原理以及代码实现 +* 并查集写法的常见误区 +* 带大家去模拟一遍并查集的过程 +* 路径压缩的过程 +* 时间复杂度分析 + +上面这几个维度 大家都去思考了,并查集基本就学明白了。 + +其实理论基础篇就算是给大家出了一道裸的并查集题目了,所以在后面的题目安排中,会稍稍的拔高一些,重点在于并查集的应用上。 + +例如 并查集可以判断这个图是否是树,因为树的话,只有一个根,符合并查集判断集合的逻辑,题目:[0108.冗余连接](./0108.冗余连接.md)。 + +在[0109.冗余连接II](./0109.冗余连接II.md) 中 对有向树的判断难度更大一些,需要考虑的情况比较多。 + + +## 最小生成树 + +最小生成树是所有节点的最小连通子图, 即:以最小的成本(边的权值)将图中所有节点链接到一起。 + +最小生成树算法,有prim 和 kruskal。 + +**prim 算法是维护节点的集合,而 Kruskal 是维护边的集合**。 + +在 稀疏图中,用Kruskal更优。 在稠密图中,用prim算法更优。 + +> 边数量较少为稀疏图,接近或等于完全图(所有节点皆相连)为稠密图 + +Prim 算法 时间复杂度为 O(n^2),其中 n 为节点数量,它的运行效率和图中边树无关,适用稠密图。 + +Kruskal算法 时间复杂度 为 O(nlogn),其中n 为边的数量,适用稀疏图。 + +关于 prim算法,我自创了三部曲,来帮助大家理解: + +1. 第一步,选距离生成树最近节点 +2. 第二步,最近节点加入生成树 +3. 第三步,更新非生成树节点到生成树的距离(即更新minDist数组) + +大家只要理解这三部曲, prim算法 至少是可以写出一个框架出来,然后在慢慢补充细节,这样不至于 自己在写prim的时候 两眼一抹黑 完全凭感觉去写。 + +**minDist数组 是prim算法的灵魂,它帮助 prim算法完成最重要的一步,就是如何找到 距离最小生成树最近的点**。 + +kruscal的主要思路: + +* 边的权值排序,因为要优先选最小的边加入到生成树里 +* 遍历排序后的边 + * 如果边首尾的两个节点在同一个集合,说明如果连上这条边图中会出现环 + * 如果边首尾的两个节点不在同一个集合,加入到最小生成树,并把两个节点加入同一个集合 + +而判断节点是否在一个集合 以及将两个节点放入同一个集合,正是并查集的擅长所在。 + +所以 Kruskal 是需要用到并查集的。 + +这也是我在代码随想录图论编排上 为什么要先 讲解 并查集 在讲解 最小生成树。 + + +## 拓扑排序 + +拓扑排序 是在图上的一种排序。 + +概括来说,**给出一个 有向图,把这个有向图转成线性的排序 就叫拓扑排序**。 + +同样,拓扑排序也可以检测这个有向图 是否有环,即存在循环依赖的情况。 + +拓扑排序的一些应用场景,例如:大学排课,文件下载依赖 等等。 + +只要记住如下两步拓扑排序的过程,代码就容易写了: + +1. 找到入度为0 的节点,加入结果集 +2. 将该节点从图中移除 + +## 最短路算法 + +最短路算法是图论中,比较复杂的算法,而且不同的最短路算法都有不同的应用场景。 + +我在 [最短路算法总结篇](./最短路问题总结篇.md) 里已经做了一个高度的概括。 + +大家要时常温故而知新,才能透彻理解各个最短路算法。 + + +## 总结 + +到最后,图论终于剧终了,相信这是市面上大家能看到最全最细致的图论讲解教程。 + +图论也是我 《代码随想录》所有章节里 所费精力最大的一个章节。 + +只为了不负录友们的期待。 大家加油💪🏻 +
diff --git "a/problems/kamacoder/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" new file mode 100644 index 0000000000..50df8aa6df --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\346\267\261\346\220\234\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -0,0 +1,197 @@ + +# 深度优先搜索理论基础 + +录友们期待图论内容已久了,为什么鸽了这么久,主要是最近半年开始更新[代码随想录算法公开课](https://www.bilibili.com/video/BV1fA4y1o715/),是开源在B站的算法视频,已经帮助非常多基础不好的录友学习算法。 + +录视频其实是非常累的,也要花很多时间,所以图论这边就没抽出时间来。 + +后面计划先给大家讲图论里大家特别需要的深搜和广搜。 + +以下,开始讲解深度优先搜索理论基础: + +## dfs 与 bfs 区别 + +提到深度优先搜索(dfs),就不得不说和广度优先搜索(bfs)有什么区别 + +先来了解dfs的过程,很多录友可能对dfs(深度优先搜索),bfs(广度优先搜索)分不清。 + +先给大家说一下两者大概的区别: + +* dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。 +* bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。 + +当然以上讲的是,大体可以这么理解,接下来 我们详细讲解dfs,(bfs在用单独一篇文章详细讲解) + +## dfs 搜索过程 + +上面说道dfs是可一个方向搜,不到黄河不回头。 那么我们来举一个例子。 + +如图一,是一个无向图,我们要搜索从节点1到节点6的所有路径。 + +![图一](https://file1.kamacoder.com/i/algo/20220707093643.png) + +那么dfs搜索的第一条路径是这样的: (假设第一次延默认方向,就找到了节点6),图二 + +![图二](https://file1.kamacoder.com/i/algo/20220707093807.png) + +此时我们找到了节点6,(遇到黄河了,是不是应该回头了),那么应该再去搜索其他方向了。 如图三: + +![图三](https://file1.kamacoder.com/i/algo/20220707094011.png) + +路径2撤销了,改变了方向,走路径3(红色线), 接着也找到终点6。 那么撤销路径2,改为路径3,在dfs中其实就是回溯的过程(这一点很重要,很多录友不理解dfs代码中回溯是用来干什么的) + +又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图四中,路径4撤销(回溯的过程),改为路径5。 + +![图四](https://file1.kamacoder.com/i/algo/20220707094322.png) + +又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图五,路径6撤销(回溯的过程),改为路径7,路径8 和 路径7,路径9, 结果发现死路一条,都走到了自己走过的节点。 + +![图五](https://file1.kamacoder.com/i/algo/20220707094813.png) + +那么节点2所连接路径和节点3所链接的路径 都走过了,撤销路径只能向上回退,去选择撤销当初节点4的选择,也就是撤销路径5,改为路径10 。 如图图六: + +![图六](https://file1.kamacoder.com/i/algo/20220707095232.png) + + +上图演示中,其实我并没有把 所有的 从节点1 到节点6的dfs(深度优先搜索)的过程都画出来,那样太冗余了,但 已经把dfs 关键的地方都涉及到了,关键就两点: + +* 搜索方向,是认准一个方向搜,直到碰壁之后再换方向 +* 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。 + +## 代码框架 + +正是因为dfs搜索可一个方向,并需要回溯,所以用递归的方式来实现是最方便的。 + +很多录友对回溯很陌生,建议先看看代码随想录,[回溯算法章节](../回溯算法理论基础.md)。 + +有递归的地方就有回溯,那么回溯在哪里呢? + +就递归函数的下面,例如如下代码: + +```cpp +void dfs(参数) { + 处理节点 + dfs(图,选择的节点); // 递归 + 回溯,撤销处理结果 +} +``` + +可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的。 + +在讲解[二叉树章节](../二叉树理论基础.md)的时候,二叉树的递归法其实就是dfs,而二叉树的迭代法,就是bfs(广度优先搜索) + +所以**dfs,bfs其实是基础搜索算法,也广泛应用与其他数据结构与算法中**。 + +我们在回顾一下[回溯法](../回溯算法理论基础.md)的代码框架: + +```cpp +void backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} + +``` + +回溯算法,其实就是dfs的过程,这里给出dfs的代码框架: + +```cpp +void dfs(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本节点所连接的其他节点) { + 处理节点; + dfs(图,选择的节点); // 递归 + 回溯,撤销处理结果 + } +} + +``` + +可以发现dfs的代码框架和回溯算法的代码框架是差不多的。 + +下面我在用 深搜三部曲,来解读 dfs的代码框架。 + +## 深搜三部曲 + +在 [二叉树递归讲解](../二叉树的递归遍历.md)中,给出了递归三部曲。 + +[回溯算法](../回溯算法理论基础.md)讲解中,给出了 回溯三部曲。 + +其实深搜也是一样的,深搜三部曲如下: + +1. 确认递归函数,参数 + +```cpp +void dfs(参数) +``` + +通常我们递归的时候,我们递归搜索需要了解哪些参数,其实也可以在写递归函数的时候,发现需要什么参数,再去补充就可以。 + +一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局变量,避免让我们的函数参数过多。 + +例如这样: + +```cpp +vector> result; // 保存符合条件的所有路径 +vector path; // 起点到终点的路径 +void dfs (图,目前搜索的节点) +``` + +但这种写法看个人习惯,不强求。 + +2. 确认终止条件 + +终止条件很重要,很多同学写dfs的时候,之所以容易死循环,栈溢出等等这些问题,都是因为终止条件没有想清楚。 + +```cpp +if (终止条件) { + 存放结果; + return; +} +``` + +终止添加不仅是结束本层递归,同时也是我们收获结果的时候。 + +另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 隐藏在下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归。这里如果大家不理解的话,没关系,后面会有具体题目来讲解。 + +3. 处理目前搜索节点出发的路径 + +一般这里就是一个for循环的操作,去遍历 目前搜索节点 所能到的所有节点。 + +```cpp +for (选择:本节点所连接的其他节点) { + 处理节点; + dfs(图,选择的节点); // 递归 + 回溯,撤销处理结果 +} +``` + +不少录友疑惑的地方,都是 dfs代码框架中for循环里分明已经处理节点了,那么 dfs函数下面 为什么还要撤销的呢。 + +如图七所示, 路径2 已经走到了 目的地节点6,那么 路径2 是如何撤销,然后改为 路径3呢? 其实这就是 回溯的过程,撤销路径2,走换下一个方向。 + +![图七](https://file1.kamacoder.com/i/algo/20220708093544.png) + + +## 总结 + +我们讲解了,dfs 和 bfs的大体区别(bfs详细过程下篇来讲),dfs的搜索过程以及代码框架。 + +最后还有 深搜三部曲来解读这份代码框架。 + +以上如果大家都能理解了,其实搜索的代码就很好写,具体题目套用具体场景就可以了。 + +后面我也会给大家安排具体练习的题目,依旧是代码随想录的风格,循序渐进由浅入深! + + +
diff --git "a/problems/kamacoder/\345\233\276\350\256\272\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/kamacoder/\345\233\276\350\256\272\347\220\206\350\256\272\345\237\272\347\241\200.md" new file mode 100644 index 0000000000..fb52c83921 --- /dev/null +++ "b/problems/kamacoder/\345\233\276\350\256\272\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -0,0 +1,249 @@ + +# 图论理论基础 + +这一篇我们正式开始图论! + +代码随想录图论中的算法题目将统一使用ACM模式,[为什么要使用ACM模式](./图论为什么用ACM模式.md) + +## 图的基本概念 + +二维坐标中,两点可以连成线,多个点连成的线就构成了图。 + +当然图也可以就一个节点,甚至没有节点(空图) + +### 图的种类 + +整体上一般分为 有向图 和 无向图。 + +有向图是指 图中边是有方向的: + +![](https://file1.kamacoder.com/i/algo/20240510195737.png) + +无向图是指 图中边没有方向: + +![](https://file1.kamacoder.com/i/algo/20240510195451.png) + +加权有向图,就是图中边是有权值的,例如: + +![](https://file1.kamacoder.com/i/algo/20240510195821.png) + +加权无向图也是同理。 + +### 度 + +无向图中有几条边连接该节点,该节点就有几度。 + +例如,该无向图中,节点4的度为5,节点6的度为3。 + +![](https://file1.kamacoder.com/i/algo/20240511115029.png) + +在有向图中,每个节点有出度和入度。 + +出度:从该节点出发的边的个数。 + +入度:指向该节点边的个数。 + +例如,该有向图中,节点3的入度为2,出度为1,节点1的入度为0,出度为2。 + +![](https://file1.kamacoder.com/i/algo/20240511115235.png) + + +## 连通性 + +在图中表示节点的连通情况,我们称之为连通性。 + +### 连通图 + +在无向图中,任何两个节点都是可以到达的,我们称之为连通图 ,如图: + +![](https://file1.kamacoder.com/i/algo/20240511102351.png) + +如果有节点不能到达其他节点,则为非连通图,如图: + +![](https://file1.kamacoder.com/i/algo/20240511102449.png) + +节点1 不能到达节点4。 + +### 强连通图 + +在有向图中,任何两个节点是可以相互到达的,我们称之为 强连通图。 + +这里有录友可能想,这和无向图中的连通图有什么区别,不是一样的吗? + +我们来看这个有向图: + +![](https://file1.kamacoder.com/i/algo/20240511104531.png) + +这个图是强连通图吗? + +初步一看,好像这节点都连着呢,但这不是强连通图,节点1 可以到节点5,但节点5 不能到 节点1 。 + +强连通图是在有向图中**任何两个节点是可以相互到达** + +下面这个有向图才是强连通图: + +![](https://file1.kamacoder.com/i/algo/20240511113101.png) + + +### 连通分量 + +在无向图中的极大连通子图称之为该图的一个连通分量。 + +只看概念大家可能不理解,我来画个图: + +![](https://file1.kamacoder.com/i/algo/20240511111559.png) + +该无向图中 节点1、节点2、节点5 构成的子图就是 该无向图中的一个连通分量,该子图所有节点都是相互可达到的。 + +同理,节点3、节点4、节点6 构成的子图 也是该无向图中的一个连通分量。 + +那么无向图中 节点3 、节点4 构成的子图 是该无向图的联通分量吗? + +不是! + +因为必须是极大联通子图才能是连通分量,所以 必须是节点3、节点4、节点6 构成的子图才是连通分量。 + +在图论中,连通分量是一个很重要的概念,例如岛屿问题(后面章节会有专门讲解)其实就是求连通分量。 + +### 强连通分量 + +在有向图中极大强连通子图称之为该图的强连通分量。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240511112951.png) + +节点1、节点2、节点3、节点4、节点5 构成的子图是强连通分量,因为这是强连通图,也是极大图。 + +节点6、节点7、节点8 构成的子图 不是强连通分量,因为这不是强连通图,节点8 不能达到节点6。 + +节点1、节点2、节点5 构成的子图 也不是 强连通分量,因为这不是极大图。 + + +## 图的构造 + +我们如何用代码来表示一个图呢? + +一般使用邻接表、邻接矩阵 或者用类来表示。 + +主要是 朴素存储、邻接表和邻接矩阵。 + +关于朴素存储,这是我自创的名字,因为这种存储方式,就是将所有边存下来。 + +例如图: + +![](https://file1.kamacoder.com/i/algo/20240511112951.png) + +图中有8条边,我们就定义 8 * 2的数组,即有n条边就申请n * 2,这么大的数组: + +![](https://file1.kamacoder.com/i/algo/20250110114348.png) + +数组第一行:6 7,就表示节点6 指向 节点7,以此类推。 + +当然可以不用数组,用map,或者用 类 到可以表示出 这种边的关系。 + +这种表示方式的好处就是直观,把节点与节点之间关系很容易展现出来。 + +但如果我们想知道 节点1 和 节点6 是否相连,我们就需要把存储空间都枚举一遍才行。 + +这是明显的缺点,同时,我们在深搜和广搜的时候,都不会使用这种存储方式。 + +因为 搜索中,需要知道 节点与其他节点的链接情况,而这种朴素存储,都需要全部枚举才知道链接情况。 + +在图论章节的后面文章讲解中,我会举例说明的。大家先有个印象。 + +### 邻接矩阵 + +邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。 + +例如: grid[2][5] = 6,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5,边的权值为6。 + +如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20240222110025.png) + +在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间。 + +图中有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6 + +这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。 + +而且在寻找节点连接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。 + +邻接矩阵的优点: + +* 表达方式简单,易于理解 +* 检查任意两个顶点间是否存在边的操作非常快 +* 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。 + +缺点: + +* 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费 + +### 邻接表 + +邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。 + +邻接表的构造如图: + +![](https://file1.kamacoder.com/i/algo/20240223103713.png) + +这里表达的图是: + +* 节点1 指向 节点3 和 节点5 +* 节点2 指向 节点4、节点3、节点5 +* 节点3 指向 节点4 +* 节点4指向节点1 + +有多少边 邻接表才会申请多少个对应的链表节点。 + +从图中可以直观看出 使用 数组 + 链表 来表达 边的连接情况 。 + +邻接表的优点: + +* 对于稀疏图的存储,只需要存储边,空间利用率高 +* 遍历节点连接情况相对容易 + +缺点: + +* 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点连接其他节点的数量。 +* 实现相对复杂,不易理解 + + +**以上大家可能理解比较模糊,没关系**,因为大家还没做过图论的题目,对于图的表达没有概念。 + +这里我先不给出具体的实现代码,大家先有个初步印象,在后面算法题实战中,我还会讲到具体代码实现,等带大家做算法题,写了代码之后,自然就理解了。 + +## 图的遍历方式 + +图的遍历方式基本是两大类: + +* 深度优先搜索(dfs) +* 广度优先搜索(bfs) + +在讲解二叉树章节的时候,其实就已经讲过这两种遍历方式。 + +二叉树的递归遍历,是dfs 在二叉树上的遍历方式。 + +二叉树的层序遍历,是bfs 在二叉树上的遍历方式。 + +dfs 和 bfs 一种搜索算法,可以在不同的数据结构上进行搜索,在二叉树章节里是在二叉树这样的数据结构上搜索。 + +而在图论章节,则是在图(邻接表或邻接矩阵)上进行搜索。 + +## 总结 + +以上知识点 大家先有个印象,上面提到的每个知识点,其实都需要大篇幅才能讲明白的。 + +我这里先给大家做一个概括,后面章节会针对每个知识点都会有对应的算法题和针对性的讲解,大家再去深入学习。 + +图论是非常庞大的知识体系,上面的内容还不足以概括图论内容,仅仅是理论基础而已。 + +在图论章节我会带大家深入讲解 深度优先搜索(DFS)、广度优先搜索(BFS)、并查集、拓扑排序、最小生成树系列、最短路算法系列等等。 + +敬请期待! + + +
diff --git "a/problems/kamacoder/\346\234\200\347\237\255\350\267\257\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" "b/problems/kamacoder/\346\234\200\347\237\255\350\267\257\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" new file mode 100644 index 0000000000..194f1f5ee2 --- /dev/null +++ "b/problems/kamacoder/\346\234\200\347\237\255\350\267\257\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,54 @@ + +# 最短路算法总结篇 + +至此已经讲解了四大最短路算法,分别是Dijkstra、Bellman_ford、SPFA 和 Floyd。 + +针对这四大最短路算法,我用了七篇长文才彻底讲清楚,分别是: + +* dijkstra朴素版 +* dijkstra堆优化版 +* Bellman_ford +* Bellman_ford 队列优化算法(又名SPFA) +* bellman_ford 算法判断负权回路 +* bellman_ford之单源有限最短路 +* Floyd 算法精讲 +* 启发式搜索:A * 算法 + + +最短路算法比较复杂,而且各自有各自的应用场景,我来用一张表把讲过的最短路算法的使用场景都展现出来: + +![](https://file1.kamacoder.com/i/algo/20240508121355.png) + +(因为A * 属于启发式搜索,和上面最短路算法并不是一类,不适合一起对比,所以没有放在一起) + + +可能有同学感觉:这个表太复杂了,我记也记不住。 + +其实记不住的原因还是对 这几个最短路算法没有深刻的理解。 + +这里我给大家一个大体使用场景的分析: + +**如果遇到单源且边为正数,直接Dijkstra**。 + +至于 **使用朴素版还是 堆优化版 还是取决于图的稠密度**, 多少节点多少边算是稠密图,多少算是稀疏图,这个没有量化,如果想量化只能写出两个版本然后做实验去测试,不同的判题机得出的结果还不太一样。 + +一般情况下,可以直接用堆优化版本。 + +**如果遇到单源边可为负数,直接 Bellman-Ford**,同样 SPFA 还是 Bellman-Ford 取决于图的稠密度。 + +一般情况下,直接用 SPFA。 + +**如果有负权回路,优先 Bellman-Ford**, 如果是有限节点最短路 也优先 Bellman-Ford,理由是写代码比较方便。 + +**如果是遇到多源点求最短路,直接 Floyd**。 + +除非 源点特别少,且边都是正数,那可以 多次 Dijkstra 求出最短路径,但这种情况很少,一般出现多个源点了,就是想让你用 Floyd 了。 + +对于A * ,由于其高效性,所以在实际工程应用中使用最为广泛 ,由于其 结果的不唯一性,也就是可能是次短路的特性,一般不适合作为算法题。 + +游戏开发、地图导航、数据包路由等都广泛使用 A * 算法。 + + + + +
diff --git a/problems/qita/acm.md b/problems/qita/acm.md new file mode 100644 index 0000000000..d4d942daca --- /dev/null +++ b/problems/qita/acm.md @@ -0,0 +1,89 @@ + +# 如何练习ACM模式输入输出模式 | 如何准备笔试 | 卡码网 + +卡码网地址:[https://kamacoder.com](https://kamacoder.com) + +## 为什么卡码网 + +录友们在求职的时候会发现,很多公司的笔试题和面试题都是ACM模式, 而大家习惯去力扣刷题,力扣是核心代码模式。 + +当大家在做ACM模式的算法题的时候,需要自己处理数据的输入输出,**如果没有接触过的话,还是挺难的**。 + +[知识星球](https://programmercarl.com/other/kstar.html)里很多录友的日常打卡中,都表示被 ACM模式折磨过: + +
+ +
+ +
+ +
+ +
+ +所以我正式推出:**卡码网**([https://kamacoder.com](https://kamacoder.com)),**专门帮助大家练习ACM模式**。 + +那么之前大家去哪里练习ACM模式呢? + +去牛客做笔试真题,结果发现 ACM模式没练出来,题目倒是巨难,一点思路都没有,代码更没有写,ACM模式无从练起。 + +去洛谷,POJ上练习? 结果发现 题目超多,不知道从哪里开始刷,也没有一个循序渐进的刷题顺序。 + +**而卡码网上有我精选+制作的25道题目**!我还把25题的后台测试数据制作了一遍,保证大家练习的效果。 + +为什么题目不多,只有25道? + +因为大家练习ACM模式不需要那么多题目,有一个循序渐进的练习过程就好了。 + +这25道题目包含了数组、链表、哈希表、字符串、二叉树、动态规划以及图的的题目,常见的输入输出方式都覆盖了。 + +**这是最精华的25道题目**!。 + +## 卡码网长什么样 + +来看看这极简的界面,没有烂七八糟的功能,只有刷题! + +
+ +在「状态」这里可以看到 大家提交的代码和判题记录,目前卡码网([https://kamacoder.com](https://kamacoder.com))几乎无时无刻都有卡友在提交代码。 +看看大家周六晚上都在做什么,刷哪些题目。 + +
+ + +提交代码的界面是这样的,**目前支持所有主流刷题语言**。 + +
+ +## 题解 + +基本大家来卡码网([https://kamacoder.com](https://kamacoder.com))练习ACM模式,都是对输入输出不够了解的,所以想看现成的题解,看看究竟是怎么处理的。 + +所以我用C++把卡码网上25道题目的题解都写了,并发布到Github上: + +[https://github.com/youngyangyang04/kamacoder-solutions](https://github.com/youngyangyang04/kamacoder-solutions) + +
+ +**欢迎去Github上star,欢迎fork,也欢迎来Github仓库贡献其他语言版本,成为contributor**。 + +如果不懂如何和开源项目提交代码,[可以看这里](https://www.programmercarl.com/qita/join.html) + +目前已经有两位录友贡献C和Java版本了。 + +
+ +期待在Github(https://github.com/youngyangyang04/kamacoder-solutions) 的contributors上也出现你的头像。 + +目前题解只有C++代码吗? + +当然不是,大多数题目已经有了 Java、python、C版本。 **其他语言版本,就给录友们成为contributor的机会了**。 + +## 最后 + +卡码网地址:[https://kamacoder.com](https://kamacoder.com) + +快去体验吧,笔试之前最好 把卡码网25道题目都刷完。 + +期待录友们成为最早一批把卡码网刷爆的coder! + diff --git a/problems/qita/acm_backup.md b/problems/qita/acm_backup.md new file mode 100755 index 0000000000..d4d942daca --- /dev/null +++ b/problems/qita/acm_backup.md @@ -0,0 +1,89 @@ + +# 如何练习ACM模式输入输出模式 | 如何准备笔试 | 卡码网 + +卡码网地址:[https://kamacoder.com](https://kamacoder.com) + +## 为什么卡码网 + +录友们在求职的时候会发现,很多公司的笔试题和面试题都是ACM模式, 而大家习惯去力扣刷题,力扣是核心代码模式。 + +当大家在做ACM模式的算法题的时候,需要自己处理数据的输入输出,**如果没有接触过的话,还是挺难的**。 + +[知识星球](https://programmercarl.com/other/kstar.html)里很多录友的日常打卡中,都表示被 ACM模式折磨过: + +
+ +
+ +
+ +
+ +
+ +所以我正式推出:**卡码网**([https://kamacoder.com](https://kamacoder.com)),**专门帮助大家练习ACM模式**。 + +那么之前大家去哪里练习ACM模式呢? + +去牛客做笔试真题,结果发现 ACM模式没练出来,题目倒是巨难,一点思路都没有,代码更没有写,ACM模式无从练起。 + +去洛谷,POJ上练习? 结果发现 题目超多,不知道从哪里开始刷,也没有一个循序渐进的刷题顺序。 + +**而卡码网上有我精选+制作的25道题目**!我还把25题的后台测试数据制作了一遍,保证大家练习的效果。 + +为什么题目不多,只有25道? + +因为大家练习ACM模式不需要那么多题目,有一个循序渐进的练习过程就好了。 + +这25道题目包含了数组、链表、哈希表、字符串、二叉树、动态规划以及图的的题目,常见的输入输出方式都覆盖了。 + +**这是最精华的25道题目**!。 + +## 卡码网长什么样 + +来看看这极简的界面,没有烂七八糟的功能,只有刷题! + +
+ +在「状态」这里可以看到 大家提交的代码和判题记录,目前卡码网([https://kamacoder.com](https://kamacoder.com))几乎无时无刻都有卡友在提交代码。 +看看大家周六晚上都在做什么,刷哪些题目。 + +
+ + +提交代码的界面是这样的,**目前支持所有主流刷题语言**。 + +
+ +## 题解 + +基本大家来卡码网([https://kamacoder.com](https://kamacoder.com))练习ACM模式,都是对输入输出不够了解的,所以想看现成的题解,看看究竟是怎么处理的。 + +所以我用C++把卡码网上25道题目的题解都写了,并发布到Github上: + +[https://github.com/youngyangyang04/kamacoder-solutions](https://github.com/youngyangyang04/kamacoder-solutions) + +
+ +**欢迎去Github上star,欢迎fork,也欢迎来Github仓库贡献其他语言版本,成为contributor**。 + +如果不懂如何和开源项目提交代码,[可以看这里](https://www.programmercarl.com/qita/join.html) + +目前已经有两位录友贡献C和Java版本了。 + +
+ +期待在Github(https://github.com/youngyangyang04/kamacoder-solutions) 的contributors上也出现你的头像。 + +目前题解只有C++代码吗? + +当然不是,大多数题目已经有了 Java、python、C版本。 **其他语言版本,就给录友们成为contributor的机会了**。 + +## 最后 + +卡码网地址:[https://kamacoder.com](https://kamacoder.com) + +快去体验吧,笔试之前最好 把卡码网25道题目都刷完。 + +期待录友们成为最早一批把卡码网刷爆的coder! + diff --git a/problems/qita/algo_pdf.md b/problems/qita/algo_pdf.md new file mode 100755 index 0000000000..76e2d16af1 --- /dev/null +++ b/problems/qita/algo_pdf.md @@ -0,0 +1,70 @@ +# 代码随想录完整版PDF下载 | 合集下载 | 百度云 | + +代码随想录已经是很多学习算法的小伙伴刷题必备的资料,也得到非常多的好评,这是Carl继续创作的动力。 + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +估计绝大多数录友之前也都下载过代码随想录PDF,但是那是我两年前整理的了。 + +![](https://file1.kamacoder.com/i/algo/20230815200530.png) + +如今,很多题目的讲解都改了上十遍,很多图都重画过。 + +之前的PDF一直都没有全集,而且章节也不全,主要是重点章节:二叉树、回溯算法、贪心、动态规划的整理。 + +也有太多录友和我反馈过:由于XXX原因,自己不能上网,看不了网站,pdf有完整版吗? + +其实录友们的需求我都记下来了,就是工作太多,我只能慢慢一项一项去处理。 + +**虽然慢,但我一直在做**! + +现在代码随想录网站最新的内容以及全集整理完毕。 + +
+ +这份PDF整理的非常精细,并把我的[算法公开课](./gongkaike.md)视频,对应题目的链接都放上去了: + +
+ +这份《代码随想录》PDF 和 《代码随想录》纸质版 和 代码随想录网站基本一致,大家选一个合适自己的阅读方式就好。 + +不过这里我依然建议大家尽量看代码随想录网站(programmercarl.com),因为网站一直都是最新的,也是经常更新的。 + +PDF可以作为辅助,例如不能上网的时候。 + +昨天我在[知识星球](./kstar.md)里第一时间公布这份全版代码随想录PDF下载的消息 + +
+ +同时有我企业微信的录友,都接到了这份PDF的推送: + +
+ +
+ +
+ +
+ +
+ +现在我把它免费分享给录友们,大家可以加我的企业微信,备注:简单自我介绍+pdf ,例如:XX大学研二-pdf 或者 XX城市后端开发-pdf ,通过之后,会直接发给大家的。 + +
+ + + diff --git a/problems/qita/ewaishuoming.md b/problems/qita/ewaishuoming.md new file mode 100755 index 0000000000..7c3f690368 --- /dev/null +++ b/problems/qita/ewaishuoming.md @@ -0,0 +1,16 @@ + + +# 本模块说明 + +本模块题目,暂时没有纳入「代码随想录」算法教程体系之中。 + +* 本模块部分题解还不够完善。 +* 本模块部分题目和「代码随想录」中是相似的。 +* 本模块题解并没有体系化 + +很多录友反馈,除了「代码随想录」还有没有其他题目可以练手,最好也有题解,所以我才把这些题解放出来。本模块题目可以作为大家刷题的一个补充。 + +加油💪 + + + diff --git a/problems/qita/gitserver.md b/problems/qita/gitserver.md new file mode 100644 index 0000000000..caf93ec6ec --- /dev/null +++ b/problems/qita/gitserver.md @@ -0,0 +1,312 @@ + +# 一文手把手教你搭建Git私服 + +## 为什么要搭建Git私服 + +很多同学都问文章,文档,资料怎么备份啊,自己电脑和公司电脑怎么随时同步资料啊等等,这里呢我写一个搭建自己的git私服的详细教程 + +为什么要搭建一个Git私服呢,而不是用Github免费的私有仓库,有以下几点: +* Github 私有仓库真的慢,文件一旦多了,或者有图片文件,git pull 的时候半天拉不下来 +* 自己的文档难免有自己个人信息,放在github心里也是担心的 +* 想建几个库就建几个,想几个人合作开发都可以,不香么? + +**网上可以搜到很多git搭建,但是说的模棱两可**,而且有的直接是在本地搭建git服务,既然是备份,搭建在本地哪有备份的意义,一定要有一个远端服务器, 而且自己的电脑和公司的电脑还是同步自己的文章,文档和资料等等。 + + +适合人群: 想通过git私服来备份自己的文章,Markdown,并做版本管理的同学 +最后,写好每篇 Chat 是对我的责任,也是对你的尊重。谢谢大家~ + +正文如下: + +----------------------------- + +## 如何找到可以外网访问服务器 + +有的同学问了,自己的电脑就不能作为服务器么? + +这里要说一下,安装家庭带宽,运营商默认是不会给我们独立分配公网IP的 + +一般情况下是一片区域公用一个公网IP池,所以外网是不能访问到在家里我们使用的电脑的 + +除非我们自己去做映射,这其实非常麻烦而且公网IP池 是不断变化的 + +辛辛苦苦做了映射,运营商给IP一换,我们的努力就白扯了 + +那我们如何才能找到一个外网可以访问的服务器呢,此时云计算拯救了我们。 + +推荐大家选一家云厂商(阿里云,腾讯云,百度云都可以)在上面上买一台云服务器 + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + +云厂商经常做活动,如果从来没有买过云服务器的账号更便宜,低配一年一百块左右的样子,强烈推荐一起买个三年。 + +买云服务器的时候推荐直接安装centos系统。 + +这里要说一下,有了自己的云服务器之后 不仅仅可以用来做git私服 + +**同时还可以做网站,做程序后台,跑程序,做测试**(这样我们自己的电脑就不会因为自己各种搭建环境下载各种包而搞的的烂糟糟),等等等。 + +有自己云服务器和一个公网IP真的是一件非常非常幸福的事情,能体验到自己的服务随时可以部署上去提供给所有人使用的喜悦。 + +外网可以访问的服务器解决了,接下来就要部署git服务了 + +本文将采用centos系统来部署git私服 + +## 服务器端安装Git + +切换至root账户 + +``` +su root +``` + +看一下服务器有没有安装git,如果出现下面信息就说明是有git的 +``` +[root@instance-5fcyjde7 ~]# git +usage: git [--version] [--help] [-c name=value] + [--exec-path[=]] [--html-path] [--man-path] [--info-path] + [-p|--paginate|--no-pager] [--no-replace-objects] [--bare] + [--git-dir=] [--work-tree=] [--namespace=] + [] + +The most commonly used git commands are: + add Add file contents to the index + bisect Find by binary search the change that introduced a bug + branch List, create, or delete branches + checkout Checkout a branch or paths to the working tree + clone Clone a repository into a new directory + commit Record changes to the repository + diff Show changes between commits, commit and working tree, etc + fetch Download objects and refs from another repository + grep Print lines matching a pattern + init Create an empty Git repository or reinitialize an existing one + log Show commit logs + merge Join two or more development histories together + mv Move or rename a file, a directory, or a symlink + pull Fetch from and merge with another repository or a local branch + push Update remote refs along with associated objects + rebase Forward-port local commits to the updated upstream head + reset Reset current HEAD to the specified state + rm Remove files from the working tree and from the index + show Show various types of objects + status Show the working tree status + tag Create, list, delete or verify a tag object signed with GPG + +'git help -a' and 'git help -g' lists available subcommands and some +concept guides. See 'git help ' or 'git help ' +to read about a specific subcommand or concept. +``` + +如果没有git,就安装一下,yum安装的版本默认是 `1.8.3.1` + +``` +yum install git +``` + +安装成功之后,看一下自己安装的版本 + +``` +git --version +``` + +## 服务器端设置Git账户 + +创建一个git的linux账户,这个账户只做git私服的操作,也是为了安全起见 + +如果不新创建一个linux账户,在自己的常用的linux账户下创建的话,哪天手抖 来一个`rm -rf *` 操作 数据可全没了 + +**这里linux git账户的密码设置的尽量复杂一些,我这里为了演示,就设置成为'gitpassword'** +``` +adduser git +passwd gitpassword +``` + +然后就要切换成git账户,进行后面的操作了 +``` +[root@instance-5fcyjde7 ~]# su - git +``` + +看一下自己所在的目录,是不是在git目录下面 + +``` +[git@instance-5fcyjde7 ~]$ pwd +/home/git +``` + +## 服务器端密钥管理 + +创建`.ssh` 目录,如果`.ssh` 已经存在了,可以忽略这一项 + +为啥用配置ssh公钥呢,同学们记不记得我使用github上传代码的时候也要把自己的公钥配置上传到github上 + +这也是方面每次操作git仓库的时候不用再去输入密码 + +``` +cd ~/ +mkdir .ssh +``` + +进入.ssh 文件下,创建一个 `authorized_keys` 文件,这个文件就是后面就是要放我们客户端的公钥 + +``` +cd ~/.ssh +touch authorized_keys +``` + +别忘了`authorized_keys`给设置权限,很多同学发现自己不能免密登陆,都是因为忘记了给`authorized_keys` 设置权限 + +``` +chmod 700 /home/git/.ssh +chmod 600 /home/git/.ssh/authorized_keys +``` + +接下来我们要把客户端的公钥放在git服务器上,我们在回到客户端,创建一个公钥 + +在我们自己的电脑上,有公钥和私钥 两个文件分别是:`id_rsa` 和 `id_rsa.pub` + +如果是`windows`系统公钥私钥的目录在`C:\Users\用户名\.ssh` 下 + +如果是mac 或者 linux, 公钥和私钥的目录这里 `cd ~/.ssh/`, 如果发现自己的电脑上没有公钥私钥,那就自己创建一个 + +创建密钥的命令 + +``` +ssh-keygen -t rsa +``` + +创建密钥的过程中,一路点击回车就可以了。不需要填任何东西 + +把公钥拷贝到git服务器上,将我们刚刚生成的`id_rsa.pub`,拷贝到git服务器的`/home/git/.ssh/`目录 + +在git服务器上,将公钥添加到`authorized_keys` 文件中 + +``` +cd /home/git/.ssh/ +cat id_rsa.pub >> authorized_keys +``` + +如何看我们配置的密钥是否成功呢, 在客户端直接登录git服务器,看看是否是免密登陆 +``` +ssh git@git服务器ip +``` + +例如: + +``` +ssh git@127.0.0.1 +``` + +如果可以免密登录,那就说明服务器端密钥配置成功了 + +## 服务器端部署Git 仓库 + +我们在登陆到git 服务器端,切换为成 git账户 + +如果是root账户切换成git账户 +``` +su - git +``` + +如果是其他账户切换为git账户 +``` +sudo su - git +``` + +进入git目录下 +``` +cd ~/git +``` + +创建我们的第一个Git私服的仓库,我们叫它为world仓库 + +那么首先创建一个文件夹名为: world.git ,然后进入这个目录 + +有同学问,为什么文件夹名字后面要放`.git`, 其实不这样命名也是可以的 + +但是细心的同学可能注意到,我们平时在github上 `git clone` 其他人的仓库的时候,仓库名字后面,都是加上`.git`的 + +例如下面这个例子,其实就是github对仓库名称的一个命名规则,所以我们也遵守github的命名规则。 + +``` +git clone https://github.com/youngyangyang04/NoSQLAttack.git +``` + +所以我们的操作是 +``` +[git@localhost git]# mkdir world.git +[git@localhost git]# cd world.git +``` + +初始化我们的`world`仓库 + +``` +git init --bare + +``` + +**如果我们想创建多个仓库,就在这里创建多个文件夹并初始化就可以了,和world仓库的操作过程是一样一样的** + +现在我们服务端的git仓库就部署完了,接下来就看看客户端,如何使用这个仓库呢 + +## 客户端连接远程仓库 + +我们在自己的电脑上创建一个文件夹 也叫做`world`吧 + +其实这里命名是随意的,但是我们为了和git服务端的仓库名称保持同步。 这样更直观我们操作的是哪一个仓库。 + +``` +mkdir world +cd world +``` + +进入world文件,并初始化操作 + +``` +cd world +git init +``` + +在world目录上创建一个测试文件,并且将其添加到git版本管理中 + +``` +touch test +git add test +git commit -m "add test file" +``` + +将次仓库和远端仓库同步 + +``` +git remote add origin git@git服务器端的ip:world.git +git push -u origin master +``` + +此时这个test测试文件就已经提交到我们的git远端私服上了 + +## Git私服安全问题 + +这里有两点安全问题 + +### linux git的密码不要泄露出去 + +否则,别人可以通过 ssh git@git服务器IP 来登陆到你的git私服服务器上 + +当然了,这里同学们如果买的是云厂商的云服务器的话 + +如果有人恶意想通过 尝试不同密码链接的方式来链接你的服务器,重试三次以上 + +这个客户端的IP就会被封掉,同时邮件通知我们可以IP来自哪里 + +所以大可放心 密码只要我们不泄露出去,基本上不会有人同时不断尝试密码的方式来登上我们的git私服服务器 + +### 私钥文件`id_rsa` 不要给别人 + +如果有人得到了这个私钥,就可以免密码登陆我们的git私服上了,我相信大家也不至于把自己的私钥主动给别人吧 + +## 总结 + +这里就是整个git私服搭建的全过程,安全问题我也给大家列举了出来,接下来好好享受自己的Git私服吧 + +**enjoy!** + diff --git a/problems/qita/gongkaike.md b/problems/qita/gongkaike.md new file mode 100755 index 0000000000..26c874b0c8 --- /dev/null +++ b/problems/qita/gongkaike.md @@ -0,0 +1,162 @@ + +# 代码随想录算法公开课 | 最强算法公开课 + +和录友们汇报一下,**代码随想录算法公开课**已经更新完毕了。 + +由我亲自录制了140期算法视频,覆盖了 [《代码随想录》纸质版](./publish.md)上全部题目的讲解。 + +视频全部免费开放在[B站:代码随想录](https://www.bilibili.com/video/BV1fA4y1o715) + +目录就在视频播放的右边,完全按照代码随想录的顺序讲解,配合 《代码随想录》或者代码随想录网站一起学习,味道更佳! + +
+ +从在22年的5月份开始决定把《代码随想录》上的算法题都由我亲自讲解一波。 + +当时录了第一期算法视频 「二分查找」: + +
+ +别看现在这期视频有10w的播放量,因为都是后序录友们自己找到我的视频来看的,一年后才到10w播放。 + +当时这个视频发出去,播放量就几百。 + +毕竟这种算法视频和普通娱乐或者范技术类视频没法比,平台也不会推荐的。 + +我的视频播放量虽然低,但只要看过视频的录友,评论都很高。随便找了几个最新的评论: + +![](https://file1.kamacoder.com/i/algo/20221121094718.png) + +![](https://file1.kamacoder.com/i/algo/20230222100337.png) + +![](https://file1.kamacoder.com/i/algo/20230222113111.png) + +![](https://file1.kamacoder.com/i/algo/20230222163023.png) + +![](https://file1.kamacoder.com/i/algo/20230223235500.png) + +![](https://file1.kamacoder.com/i/algo/20230323211739.png) + +![](https://file1.kamacoder.com/i/algo/20230324150536.png) + +![](https://file1.kamacoder.com/i/algo/20230325215454.png) + +![](https://file1.kamacoder.com/i/algo/20230327100147.png) + +![](https://file1.kamacoder.com/i/algo/20230329101702.png) + +![](https://file1.kamacoder.com/i/algo/20230404202808.png) + +当初也是看到大家的评论,我才下决心继续更下去,从 去年5月份,每周坚持更新四期算法视频,雷打不动,一直坚持到现在。 + +![](https://file1.kamacoder.com/i/algo/20230303170120.png) + +一晃 ,大半年过去了,足足更新了 140期算法视频,已经覆盖了 [《代码随想录》纸质版](./publish.md)上全部题目的讲解。 + +**我应该为数不多(至少目前我还没看到)的技术书籍作者能亲自把书中全部内容以视频的方式讲解出来并免费分享给大家的** + +大家可以想一想这些年买过哪些技术书籍,有作者亲自给大家把每章每一节都做视频讲解并免费开放的。 + +**那么看 [《代码随想录》](./publish.md)的录友们就有这个待遇**! + +讲课录视频,其实是很费精力的,大家看视频,可能看我讲的行云流水,其实我都是做了十足的功课。 + +**我平时养成了只要有空的时候就模拟一遍某算法运行过程的习惯,板书更是擦了写写了再擦**,反复尝试那种方式能给大家讲清楚的,然后再开拍视频。 + +可能大家会想,都出书了照着书讲不就好了吗? 应该不难吧? + +如果这么简单的话,可能市面上 很多技术书籍作者们就都亲自讲解一波了。 + +写出来和讲出来还不是一个维度。 + +讲出来需要很综合的能力包括表达力,而且大家看我的算法视频会发现:我是脱稿的,我没有提示词,摄像头开了就开讲 一镜到底。 + +**我是做了非常多练习才达到这个程度**。 + +很多人看到摄像头,就会紧张,没有提示词就不知道自己下句该说什么了,瞬间就会:我是谁,我在哪,我在干什么? + +## 算法公开课质量如何 + +目前国内算法视频的讲解风格,一般是 录屏力扣写代码 或者 ppt演示。 这样其实录制视频难度低了很多。 + +但大家上油管的话,会发现 海外经典算法视频的up ,都是一个小白板直接开讲。 + +
+
+
+ +这种讲课方式 容易走两个极端,**一种就是非常好,成为经典系列,一种就是被喷讲的像垃圾一样**。 + +如果是 录屏或者ppt演示,这样至少有稿件照着读,或者提示词,就算差也不会差到哪去。 + +所以呢,我选择这种白板模拟思路直接手撕代码的讲课方式,也是给自己一个挑战,目前的口碑来看,还有走向了好的那个极端。 + + +关于质量究竟如何,学习效果如何,大家可以去B站上去看(B站同名:[代码随想录](https://www.bilibili.com/video/BV1fA4y1o715)),有口皆碑! + + +## 辛苦录视频为了啥 + +再说说这个辛苦录视频,一忙就忙大半年,投入这么多时间和精力,最后为什么还免费开放。 + +书 + 付费视频讲解 或者 免费网站内容 + 付费视频讲解 或者 免费网站内容 + 部分视频免费 + 部分视频付费,**这些都是非常好的盈利模式**,而且还可持续变现的。 + +以代码随想录目前的影响力来说,我这套140集视频教程,不用很贵,定价99元,每年卖出上万份问题不大。 + +这可是一笔非常可观的收入!(真的很香!)而且还是持续收入,后期还不用什么去维护,不像 [知识星球](./kstar.md) 或者 [算法训练营](./xunlianying.md) 我需要花大量时间给录友们答疑。 + +那我为什么不这么做呢? + +**“代码随想录” 这五个字,我是会用一生去经营的**,凡事要看的长远,不是什么赚钱就立刻要去干什么。 + +这套算法公开课视频,不仅造福录友们,也放大了代码随想录的品牌影响,这是双赢的。 + +**免费硬核的算法内容是 代码随想录的立身之本**,也是 大家为什么学算法学编程首选代码随想录的根本所在。 + +如果有点影响力了,就暗插各种付费,这样不持久! **也会伤了很多录友的心**。 + +所以目前 代码随想录网站(programmercarl.com),代码随想录Github(https://github.com/youngyangyang04/leetcode-master),代码随想录算法公开课(B站:代码随想录),**都是完全免费**,也足够大家学习算法所用。 + +**我的免费算法视频内容,要比绝大多数视频上大家付费的 算法视频课、算法训练营质量要高得多**,视频课程基于《代码随想录》的刷题顺序来录制,会让视频内容非常系统,而不是东一块,西一块的。 + +至于《代码随想录》纸质版的内容其实和代码随想录网站是一样的,很的录友买纸质版是因为习惯看纸质版,有的是仅仅是为了留念和收藏。 + +而且以上开源内容,我还会持续优化迭代的,不会做完了就放着不管,如果一年前刷完代码随想录的录友现在在重看一遍 代码随想录网站,**你会发现很多题解了又多了很多图,又优化了很多讲解内容**。 + +这是我的github提交记录:(https://github.com/youngyangyang04) + +
+ +每天或多或少多要优化一点点。 **每天的量可能不多,但每天都在优化**! + +如果感觉代码随想录网站 和 代码随想录算法公开课对大家确实有帮助,不用买书,欢迎去[豆瓣](https://book.douban.com/subject/35680544/)给一个好评就好,感谢录友们的支持。 + + +## 公开课 + +再回头说说目前算法公开课, 其实直到现在 我新发的视频播放也就两三千的播放量。 + +
+ +(大家现在去B站上去看,可能已经上万播放量,但新发的时候 播放量一直都很惨淡) + +但为什么发视频就两三千播放量,就可以在B站聚集10w录友。 + +
+ +对于B站十万粉的号主来说,好像都得有几个百万播放的爆款视频。 + +**但我的视频从来没有爆款过,也没有被平台推荐过,都是非常稳定的几千播放**。 + +很多录友都是主动搜索找过来的就关注了,或者身边的人推荐来的。 + +只要是真正能给大家带来价值的,真正能让大家学明白算法,就会得到认可。 + +所以,**酒香不怕巷子深,真正有价值的内容,不需要蹭热点,想学习的人一定会找到你**。 + +目前《代码随想录》上的算法讲解视频终于更新完了,后面有就有足够的精力去更图论、排序、高级数据结构了。 + +算法公开课全部发布在B站上,链接直达:[《代码随想录》算法公开课](https://www.bilibili.com/video/BV1fA4y1o715) + +最后,**认准代码随想录,学习算法不迷路**。加油💪🏻 + diff --git a/problems/qita/join.md b/problems/qita/join.md new file mode 100644 index 0000000000..232d84ca2f --- /dev/null +++ b/problems/qita/join.md @@ -0,0 +1,249 @@ + + +# 如何在Github上提交PR(pull request) + + +* 如何提交代码 +* 合入不规范 + * 提交信息不规范 + * Markdown 代码格式 + * pull request里的commit数量 + * 代码注释 + * 说明具体是哪种方法 + * 代码规范 + * 代码逻辑 + * 处理冲突 + +以下在 [https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master) 上提交pr为为例 + +## 如何合入代码 + +首先来说一说如何合入代码,不少录友还不太会使用github,所以这里也做一下科普。 + +我特意申请一个新的Github账号,给大家做一个示范。 + +需要强调一下,一个commit就只更新一道题目,不要很多题目一起放在一个commit里,那样就很乱。 + +首先LeetCode-Master每天都有更新,如何保持你fork到自己的仓库是最新的版本呢。 + +点击这里Fetch upstream。 + +
+ +点击之后,这里就会显示最新的信息了 +
+ +注意这时是你的远端仓库为最新版本,本地还不是最新的,本地要git pull一下。 + +基于最新的版本,大家在去提交代码。 + +如何提交代码呢,首先把自己的代码提交到自己的fork的远端仓库中,然后open pull request,如图: + +
+ +点击 open pull request之后,就是如下画面,一个pull request有多个commit。 + +
+ +然后就是给pull request 添加备注,pull request是对本次commit的一个总结。如果一个pull request就一个commit,那么就和commit的备注保持一次。 然后点击 create pull request 就可以了 + +
+ +此时你就提交成功了,我会在项目中的pull requests 处理列表里看到你的请求。 +
+ +然后如果你发现自己的代码没有合入多半是有问题,如果有问题都有会在pull request里给出留言的, + +## 注意事项 + +### 提交信息不规范 + + +大家提交代码的时候有两个地方需要写备注,一个是commit,一个是pull request,pull request包好多个commit。 + +commit 说清楚本文件多了哪些修改,而pull request则是对本次合入的所有commit做一个总结性描述。 + +commit备注,举例:添加Rust python3,那么commit备注就是:添加0001两数之和 Rust python3 版本 + +而pull request 如果只有一个commit,那么就也是:添加0001两数之和 Rust python3 版本 + +如果是多个commit ,则把本次commit都描述一遍。 + +### Markdown 语法 + +关于 Markdown 代码格式,例如 添加C++代码,需要有代码块语法 + +\`\`\`C++ +C++代码 +\`\`\` + +例如这个commit,在添加java代码的时候,就直接添加代码 +
+ +正确的格式应该是这样: +
+ +一般发现问题,我也会在代码中给出评论: + +
+ +这样大家也可以学习一些 提交代码的规范方面的知识 + + +有的录友 是添加的代码块语法,但没有标记是哪种语言,这样的话 代码就不会针对某种语言高亮显示了,也比较影响阅读,例如: + +
+ +提交python代码的话,要注释好,是python2还是python3 + +例如这样: + +
+ +当然python2的话,只这么写就行 + +\`\`\`python +python代码 +\`\`\` + +### pull request里的commit数量 + + +有的录友是一个pull request 里有很多commit (一个commit是一道题目的代码)。 + +有的录友是一个pull request 里只有一个commit。 + +
+ +其实如果大家是平时一天写了两三道题目的话,那么分三个commit,一个pull request提交上来就行。 + +一个pull request 一个commit也可以,这样大家就会麻烦一点。 + +但注意一个pull request也不要放太多的commit,一旦有一道题目代码不合格,我没有合入,就这个pull request里影响其他所有代码的合入了。 + +### 代码注释 + +提交的代码最好要有注释,这样也方便读者理解。 + +例如这位录友,在提交Java代码的时候,按照题解的意思对Java版本的代码进行的注释,这就很棒👍 + +
+ +
+ +当然如果大家感觉 已有的代码 不符合以上要求的话,例如 代码思路不够清晰不够规范,注释不够友好,依然欢迎提交优化代码,要记得详细注释哦。 + +
+ +### 说明具体是哪种方法 + +有的题解有两种甚至三四种解法,在添加代码的时候,注释上也清楚具体是哪一种方法的版本。 + +下面这位录友做的就很好 + +
+ + +
+ +有的题解,是一起给出了多道题目的讲解,例如项目中0102.二叉树的层序遍历.md 中有八道题目,那么大家添加代码的时候 应该在代码注释上,或者 直接写上 是哪个题目的代码。 + + +### 代码规范 + + +大家提交代码要规范,当然代码可以在力扣上运行通过是最基本的。 + +虽然我主张没有绝对正确的代码风格,但既然是给LeetCode-Master提交代码,尽量遵循Google编程规范。 + +经常看我的代码的录友应该都知道,我的代码风格严格按照 Google C++ 编程规范来的,这样看上去会比较整洁。 + +大家提交代码的时候遇到规范性问题,例如哪里应该有空格,哪里没有空格,可以参考我的代码来。 + +有一位录友在提交代码的时候会把之前的代码 做一下规范性的调整,这就很棒。 + +
+ +**代码规范从你我做起!** + + +### 代码逻辑 + +**提交的代码要按照题解思路来写**。 + +虽然大家自己发挥想象空间是好的,但是题解还是要一脉相承,读者看完题解,发现代码和题解不是一个思路的话,那和重新读代码有啥区别了。 + +所以和题解不是一个思路的代码,除非详细注释了自己的思路 或者 写一段自己代码的描述说明思路和优化的地方,否则我就不会通过合入了哈。 + +大家的代码 最好也将关键地方放上注释,这样有助于别人快速理解你的代码。 + + +### 处理冲突 + +在合入的过程中还要处理冲突的代码, 理解大家代码的思路,解决冲突,然后在力扣提交一下,确保是没问题。 + +例如同一道题目, 一位录友提交了, 我还没处理如何,另一位录友也对这道题也提交了代码,这样就会发生冲突 +
+ +大家提交代码的热情太高了,我有时候根本处理不过来,但我必须当天处理完,否则第二天代码冲突会越来越多。 +
+ +一天晚上分别有两位录友提交了 30多道 java代码,全部冲突,解决冲突处理的我脖子疼[哭] + +那么在处理冲突的时候 保留谁的代码,删点谁的代码呢? + +我一定是看谁 代码逻辑和题解一致,代码风格好,注释友好,就保留谁的。 + +所以例如当你想提交Java代码的时候,即使发现该题解已经有Java版本了,只要你的代码写的好,一样可以提交,我评审合格一样可以合入代码库。 + + +### 不要做额外修改 + +确保这种额外文件不要提交。 + +
+ +还有添加不同方法的时候,直接用正文格式写,哪种方法就可以了,不要添加目录 ,例如这样,这样整篇文章目录结构就有影响了。 + +
+ +前面不要加 `## 前序遍历(迭代法)`,直接写`前序遍历(迭代法)`就可以了。 + +当然这里也没有给代码块标记上对应的语言,应该是 + +\`\`\` Go +Go语言代码 +\`\`\` + + +## 对代码保持敬畏 + +有的录友甚至提交的代码并不是本题的代码,虽然我是鼓励大家提交代码的,但是大家贡献代码的时候也要对 自己的代码有敬畏之心,自己的代码是要给很多读者看的。 + +* 代码运行无误 +* 写的够不够简洁 +* 注释清不清晰 +* 备注规不规范 + +这也是培养大家以后协调工作的一种能力。 + +## 优化 + +目前 leetcode-master中大部分题解已经补充了其他语言,但如果你发现了可以优化的地方,依然可以提交PR来优化。 + +甚至发现哪里有语病,也欢迎提交PR来修改,例如下面:就是把【下表】 纠正为【下标】 + +
+ +不用非要写出牛逼的代码才能提交PR,只要发现 文章中有任何问题,或者错别字,都欢迎提交PR,成为contributor。 + +
+ +## 特别注意 + +git add之前,要git diff 查看一下,本次提交所修改的代码是不是 自己修改的,是否 误删,或者误加的文件。 + +提交代码,不要使用git push -f 这种命令,要足够了解 -f 意味着什么。 + + + diff --git a/problems/qita/language.md b/problems/qita/language.md new file mode 100755 index 0000000000..625154e0a5 --- /dev/null +++ b/problems/qita/language.md @@ -0,0 +1,22 @@ + +# 编程语言基础课 + +「代码随想录」的内容是完全免费的。 + +**不过不少录友是编程零基础**,而刷「代码随想录」至少默认你是会一定的编程语言知识的。 + +如果你是编程零基础,又想快速达到刷算法题(或者说刷代码随想录)所需编程语言的水平,推荐 + +* [C++基础课](../ke/cplus.md) +* [Java基础课](../ke/java.md) +* [Python基础课](../ke/python.md) +* [Go基础课](../ke/go.md) +* [Javascript基础课](../ke/js.md) + +如果你有一定数据结构算法知识,想用数据结构做一个小项目的话,推荐: + +* [手写STL(C++)](../ke/stl.md) +* [kv存储-CPP](../ke/kvcplus.md) +* [kv存储-JAVA](../ke/java.md) + + diff --git a/problems/qita/publish.md b/problems/qita/publish.md new file mode 100755 index 0000000000..5a57ec4e4b --- /dev/null +++ b/problems/qita/publish.md @@ -0,0 +1,200 @@ +# 十年所学,终成《代码随想录》 + +**《代码随想录》终于终于正式出版上市了!** (文末附购买链接,直接五折!) + +[B站介绍](https://www.bilibili.com/video/BV13L4y1E7s4/) + +最近这一年不少录友都问我,代码随想录什么时候出版啊? + +**其实我比大家还期待这一刻的到来!** + +先奉上几张书照片:(封面最终选定为梵高的画作,阿姆斯特丹,圣马迪拉莫,1888,海景) + +
+ +
+ +其实在去年,也就是2020年我就已经将这本书的内容写好了,本以为可以很快出版,但我还是严重低估了写书的工作量。 + +因为自己对质量的追求,一直在不断打磨,所以又是一年快过去了。 + +**《代码随想录》总共将近500页,70w字,200多个插图,真的处处都是心血**。 + +出书是一件浩大工程,比写文章难太多了,**真的字字斟酌,大家看书里可能平平淡淡的一句话、一个词语、一个概念,我可能就查阅很多资料,反复推敲:表达是否准确,用词是否到位,生怕辜负了大家的期待**。 + +这是我自己平时书桌的场景: + +
+ +这两年可以说我没有什么娱乐活动,业余生活极其枯燥,都花费在这本书上了,其中艰辛只有自己知道。 + +而此时当大家都能看到《代码随想录》这部作品的时候,其满足感对我来说已经足以。 + +写这本书用了两年,**而真正消化、理解、研究这些算法知识,我用了整整十年**,十年前我就开始写算法文章,妄图闯进算法的大门,这一写就是十年。 + +
+ +**真的是十年所学,两年打磨,终成《代码随想录》!** + +所以当坚持一件事情的时候,一年、两年,甚至三年、五年,不足以看出其效果,但也许坚持十年的时候,才等到真正收获的时刻。 + +## 代码随想录的故事 + +《代码随想录》不是两年憋大招来个横空出世。 + +而是一点一点打磨出来的,其刷题顺序、题解内容、思考深度 都是经过了上10w录友的共同见证。 + +也正是这些内容,把大家汇聚在一起,一起攻克算法的一座又一座高山。 + +与此同时,也几乎每天都会有录友来专门私信我来表达自己的感激: + +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ +这些都是大家与“代码随想录”之间的故事,也欢迎大家在文章留言,说一说自己和 “代码随想录”之间的故事。 + +## 《代码随想录》有何不同? + +大家在学习编程、算法,刷题的时候,**真正的苦恼在于没有一套行之有效的刷题顺序**。 + +从何学起,先学什么,再学什么。力扣(Leetcode)上两千道题目,怎么刷,很多人刷题的效率低,主要体现在如下三点: + +* 找题 +* 找到了不合适现阶段做的题 +* 没有全套的优质题解可以参考 + +而市面上基本找不到真正能解决以上痛点的算法书籍。 + +一些书籍是每个知识点蜻蜓点水,然后就叫大家举一反三。 + +一些书籍是一堆题解堆在一起,让大家学起来感受不到知识的连贯性和系统性。 + +断片式的学习,效率怎么能高呢? + +当初我在学习算法的时候,就深感其中的艰难,当我的题量达到一定数量时候,随着反复的琢磨和深入的思考,我再去回顾这些算法题目,**发现如果要是按照合理的顺序来刷题,那效果一定是 事半功倍!** + +所以我将每一个专题中的**题目按照由易到难的顺序进行编排,每一道题目所涉及的知识,前面都会有相应的题目做知识铺垫,做到环环相扣**。 + +**建议大家按照章节顺序阅读本书**,在阅读的过程中会发现我在题目编排上的良苦用心! + +本书不仅在题目编排上精心设计,而且在针对读者最头痛的算法问题上做了详细且深入的讲解。 + +* 关于动态规划,都知道递推公式的重要性,但dp数组的含义、dp数组的初始化、遍历顺序以及如何打印dp数组来排查Bug,这些都很重要。例如,解决背包问题的时候,遍历顺序才是最关键的,也是最难理解的。 + +* 关于回溯算法,题目要求集合之间不可重复,那么就需要去重,各种资料都说要去重,但没有说清楚是“树层去重”还是“树枝去重”——这是我为了说明去重的过程而创造的两个词汇。 + +* 关于KMP算法,都知道使用前缀表进行回退,可什么是前缀表,为什么一定要用前缀表,根据前缀表回退有几种方式,这些却没有说清楚,导致最后大家看的一头雾水。 + +* 关于二叉树,不同的遍历顺序其递归函数究竟如何安排,递归函数什么时候需要返回值,什么时候不用返回值,什么情况下分别使用前、中、后序遍历,迭代法又要如何实现,这些都决定了对二叉树的理解是否到位。 + +同时我针对每一个专题的特点,整理出其通用的解法套路。 + +例如: + +* 在二叉树专题中,总结了递归“三部曲”来帮助读者掌握二叉树中各种遍历方式的写法。 +* 回溯算法中的回溯“三部曲”可以帮助读者理解回溯算法晦涩难懂的过程。 +* 动态规划中的动规“五部曲”可以帮助读者在一套思考框架下去解决动态规划题目。 + +再来说一说动态规划,在程序员面试中,动态规划是公认的最难掌握的算法,也是出现频率最高的算法。 + +**如果仅仅讲解几道题目,即使再举一反三也远远达不到真正理解动态规划的程度**。 + +**如果把动态规划的题目单纯地堆砌在一起,也只会让人越学越懵,陷入“一看就会,一写就废”的怪圈**。 + +讲清楚一道题容易,讲清楚两道题也容易,但把整个动态规划的各个分支讲清楚,把每道题目讲透彻,并用一套方法论把整个动态规划的题目贯彻始终就有难度了。 + +**而帮助大家解决这个问题,这也是这本书的使命所在**。 + +购买方式,可以扫下方二维码,也可以直接[点击这里,京东直达](https://union-click.jd.com/jdc?e=&p=JF8BAMQJK1olXg8EUVhVCkkWAV8IGV8WVAICU24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYAUF1UDEsQHDZNRwYlGFh6NVkPcRdyHWwMZRlLHlQDUj02eEcbM244GFIXWQYAUV5VOHsXBF9edVsUXAcDVVtdDUgnAl8IHFkdXw4BU1lfCkoRM2gIEmtIFVpKAxVtOHsUM184G2sWbURsUVpcCEMVAjgIHAxBWFYAAVdfXE8QBGkBGQsdCQEFVgttCkoWB2Y4) + +
+ + +## 目录 + +
+ +
+ +这里不少录友会问,书的内容和Github:https://github.com/youngyangyang04/leetcode-master,和网站:programmercarl.com 有什么区别呢? + +其实写文章相对来说是随意一些的,但书一定要非常严谨。 + +正如我本篇开头所说,书的内容其实一年前就写好的,但排版、纠错、打磨、重新画图,又花费了一年,所以书一定是更精细的,更严谨的。 + +**《代码随想录》的排版看起来非常舒服,会让你发现 原来学算法 会上瘾!** + +
+ +《代码随想录》的推荐语,我都是颇为用心,不是随随便便找个人写一写推荐语来凑数的。 + +
+ +哈工大计算机王院长,百度杰出架构师猛哥,腾讯专家工程师强哥,王道论坛创始人风华哥,**他们是在我学习工作的不同阶段里对我影响非常大的顶级巨佬**。 + +他们的学习方法,做事风格,都是值得每一位技术人学习。同时他们也是每一位技术人的榜样。 + +特别感谢巨佬们能在百忙之中阅读了本书的书稿,并给本书写了评语。 + +## 最后 + +我希望这本书,不仅仅是可以帮助大家学习编程,循序渐进的去学习算法,高效刷题,进大公司。 + +**同时 当你把这本书放在自己的书桌前,床头前的时候,它也会给你一种乘风破浪的勇气!** + +正如封面(梵高,阿姆斯特丹,圣马迪拉莫,1888,海景),一只帆船在波涛汹涌的大海里扬帆远航! + +《代码随想录》这就要和大家见面了,其实很多录友已经迫不及待: + +
+ +
+ +
+ +
+ +这本书原价还挺贵的(毕竟比较厚),但这里申请到了京东五折优惠,大家可以速度下手了。 + +点击下方链接直接五折购买,全网最低价格了。海外的录友们可以在等几天,广州有货之后,就可以配送的海外了。 + +《代码随想录》使用的语言是C++,使用其他语言的录友可以看本书的讲解思路,刷题顺序,然后配合看网站:programmercarl.com,网站上都对应的Java,Python,Go,Js,C,Swift版本 基本可以满足大家的学习需求。 + +购买方式,可以扫下方二维码,也可以直接[点击这里,京东直达](https://union-click.jd.com/jdc?e=&p=JF8BAMQJK1olXg8EUVhVCkkWAV8IGV8WVAICU24ZVxNJXF9RXh5UHw0cSgYYXBcIWDoXSQVJQwYAUF1UDEsQHDZNRwYlGFh6NVkPcRdyHWwMZRlLHlQDUj02eEcbM244GFIXWQYAUV5VOHsXBF9edVsUXAcDVVtdDUgnAl8IHFkdXw4BU1lfCkoRM2gIEmtIFVpKAxVtOHsUM184G2sWbURsUVpcCEMVAjgIHAxBWFYAAVdfXE8QBGkBGQsdCQEFVgttCkoWB2Y4) + +
+ +最后也感谢录友们的陪伴,真心希望大家都有一个好的前程! + +正如《代码随想录》正式出版一样,**你所期盼,终将到来! 加油💪** + + diff --git a/problems/qita/say_feel.md b/problems/qita/say_feel.md new file mode 100755 index 0000000000..00df8c0bf2 --- /dev/null +++ b/problems/qita/say_feel.md @@ -0,0 +1,14 @@ +恭喜你,已经把代码随想录通关了,欢迎在[卡码笔记](https://notes.kamacoder.com/question/102144)记录一下自己的收获,写一篇小作文。 + +不过一刷代码随想录,理解的一定是不到位的,建议二刷之后,对各个经典类型的题目就有自己的想法了。 + +大家可以在自己的博客写一篇 代码随想录一刷总结,记录这阶段性进步的一刻。 + +如果感觉代码随想录对你确实有帮助,不用买书,欢迎去[豆瓣](https://book.douban.com/subject/35680544/)给一个好评就好,代码随想录在豆瓣上被人恶意抹黑,希望录友们可以去说一说自己刷代码随想录的真实感受,感谢录友们的支持。 + +希望大家都能梦想成真,有好的前程,加油💪 + + + + + diff --git a/problems/qita/server.md b/problems/qita/server.md new file mode 100644 index 0000000000..890cf8bcd5 --- /dev/null +++ b/problems/qita/server.md @@ -0,0 +1,129 @@ + +# 一台服务器有什么用! + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + +但在组织这场活动的时候,了解到大家都有一个共同的问题: **这个服务器究竟有啥用??** + +这真是一个好问题,而且我一句两句还说不清楚,所以就专门发文来讲一讲。 + +同时我还录制的一期视频,我的视频号,大家可以关注一波。 + + +一说到服务器,可能很多人都说搞分布式,做计算,搞爬虫,做程序后台服务,多人合作等等。 + +其实这些普通人都用不上,我来说一说大家能用上的吧。 + +## 搭建git私服 + +大家平时工作的时候一定有一个自己的工作文件夹,学生的话就是自己的课件,考试,准备面试的资料等等。 + +已经工作的录友,会有一个文件夹放着自己重要的文档,Markdown,图片,简历等等。 + +这么重要的文件夹,而且我们每天都要更新,也担心哪天电脑丢了,或者坏了,突然这些都不见了。 + +所以我们想备份嘛。 + +还有就是我们经常个人电脑和工作电脑要同步一些私人资料,而不是用微信传来传去。 + +这些都是git私服的使用场景,而且很好用。 + +大家也知道 github,gitee也可以搞私人仓库 用来备份,同步文件,但自己的文档可能放着很多重要的信息,包括自己的各种密码,密钥之类的,放到上面未必安全。你就不怕哪些重大bug把你的信息都泄漏了么[机智] + +更关键的是,github 和 gitee都限速的。毕竟人家的功能定位并不是网盘。 + +项目里有大文件(几百M以上),例如pdf,ppt等等 其上传和下载速度会让你窒息。 + +**后面我会发文专门来讲一讲,如何大家git私服!** + +## 搞一个文件存储 + +这个可以用来生成文件的下载链接,也可以把本地文件传到服务器上。 + +相当于自己做一个对象存储,其实云厂商也有对象存储的产品。 + +不过我们自己也可以做一个,不够很多很同学应该都不知道对象存储怎么用吧,其实我们用服务器可以自己做一个类似的公司。 + +我现在就用自己用go写的一个工具,部署在服务器上。 用来和服务器传文件,或者生成一些文件的临时下载链接。 + +这些都是直接命令行操作的, + +操作方式这样,我把命令包 包装成一个shell命令,想传那个文件,直接 uploadtomyserver,然后就返回可以下载的链接,这个文件也同时传到了我的服务器上。 + +![](https://file1.kamacoder.com/i/algo/20211126165643.png) + +我也把我的项目代码放在了github上: + +https://github.com/youngyangyang04/fileHttpServer + +感兴趣的录友可以去学习一波,顺便给个star。 + + +## 网站 + +做网站,例如 大家知道用html 写几行代码,就可以生成一个网页,但怎么给别人展示呢? + +大家如果用自己的电脑做服务器,只能同一个路由器下的设备可以访问你的网站,可能这个设备出了这个屋子 都访问不了你的网站了。 + +因为你的IP不是公网IP。 + +如果有了一台云服务器,都是配公网IP,你的网站就可以让任何人访问了。 + +或者说 你提供的一个服务就可以让任何人使用。 + +例如第二个例子中,我们可以自己开发一个文件存储,这个服务,我只把把命令行给其他人,其他人都可以使用我的服务来生成链接,当然他们的文件也都传到了我的服务器上。 + +再说一个使用场景。 + +我之前在组织免费里服务器的活动的时候,阿里云给我一个excel,让面就是从我这里买服务器录友的名单,我直接把这个名单甩到群里,让大家自己检查,出现在名单里就可以找我返现,这样做是不是也可以。 + +这么做有几个很大的问题: +* 大家都要去下载excel,做对比,会有人改excel的内容然后就说是从你这里买的,我不可能挨个去比较excel有没有改动 +* excel有其他人的个人信息,这是不能暴漏的。 +* 如果每个人自己用excel查询,私信我返现,一个将近两千人找我返现,我微信根本处理不过来,这就变成体力活了。 + +那应该怎么做呢, + +我就简单写一个查询的页面,后端逻辑就是读一个execel表格,大家在查询页面输入自己的阿里云ID,如果在excel里,页面就会返回返现群的二维码,大家就可以自主扫码加群了。 + +这样,我最后就直接在返现群里 发等额红包就好了,是不是极大降低人力成本了 + +当然我是把 17个返现群的二维码都生成好了,按照一定的规则,展现给查询通过的录友。 + +就是这样一个非常普通的查询页面。 + +![](https://file1.kamacoder.com/i/algo/20211126160200.png) + +查询通过之后,就会展现返现群二维码。 + +![](https://file1.kamacoder.com/i/algo/20211127160558.png) + +但要部署在服务器上,因为没有公网IP,别人用不了你的服务。 + + +## 学习linux + +学习linux其实在自己的电脑上搞一台虚拟机,或者安装双系统也可以学习,不过这很考验你的电脑性能如何了。 + +如果你有一个服务器,那就是独立的一台电脑,你怎么霍霍就怎么霍霍,而且一年都不用关机的,可以一直跑你的任务,和你本地电脑也完全隔离。 + +更方便的是,你目前系统假如是CentOS,想做一个实验需要在Ubuntu上,如果是云服务器,更换系统就是在 后台点一下,一键重装,云厂商基本都是支持所有系统一件安装的。 + +我们平时自己玩linux经常是配各种环境,然后这个linux就被自己玩坏了(一般都是毫无节制使用root权限导致的),总之就是环境配不起来了,基本就要重装了。 + +那云服务器重装系统可太方便了。 + +还有就是加入你好不容易配好的环境,如果以后把这个环境玩坏了,你先回退这之前配好的环境而不是重装系统在重新配一遍吧。 + +那么可以用云服务器的镜像保存功能,就是你配好环境的那一刻就可以打一个镜像包,以后如果环境坏了,直接回退到上次镜像包的状态,这是不是就很香了。 + + +## 总结 + +其实云服务器还有很多其他用处,不过我就说一说大家普遍能用的上的。 + + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + diff --git a/problems/qita/shejimoshi.md b/problems/qita/shejimoshi.md new file mode 100644 index 0000000000..959a3fa90a --- /dev/null +++ b/problems/qita/shejimoshi.md @@ -0,0 +1,57 @@ + +# 23种设计模式精讲 | 配套练习题 | 卡码网 + +关于设计模式的学习,大家应该还是看书或者看博客,但却没有一个边学边练的学习环境。 + +学完了一种设计模式 是不是应该去练一练? + +所以卡码网 针对 23种设计,**推出了 23道编程题目,来帮助大家练习设计模式**。 + +
+ +这里的23到编程题目对应了 23种这几模式。 例如第一题,小明的购物车,就是单例模式: + +
+ +区别于网上其他教程,本教程的特点是: + +* **23种设计模式全覆盖**,涵盖了所有Gang of Four设计模式,包括创建型、结构型和行为型设计模式。 +* 通过23道简单而实用的例子,**以刷算法题的形式了解每种设计模式的概念、结构和应用场景**。 +* **为每个设计模式提供清晰的文字解释、结构图和代码演示**,帮助你更好地理解和实践。 +* **难度安排循序渐进**,从基础的、常用的设计模式逐步深入。 + +这样的一个学习体验,要收费吗? + +**免费的**! + +相信录友们可能还没有这种学习设计模式的体验,快去卡码网(kamacoder.com)上体验吧。 + +23道 设计模式的题目给大家出了,那么是不是得安排上对应的讲解? + +**当然安排**! + +针对每道题目,还给大家编写了一套 23种设计模式精讲,已经开源到Github上: + +> https://github.com/youngyangyang04/kama-DesignPattern + +支持Java,Python,Go,C++ 版本,也欢迎大家去Github上提交PR,补充其他语言版本。 + +所以题解也免费开放给录友! + +同时还给全部整理到PDF上,这份PDF,我们写的很用心了,来个大家截个图: + +
+ +
+ +
+ +
+ +关于设计模式的题目,大家现在就可以去 卡码网(kamacoder)去做了。 + +关于这23道题目对应 设计模式精讲 PDF,也免费分享给录友们,大家可以加我的企业微信获取: +
+ +已经有我企业微信的录友,直接发:设计模式,这四个字就好,我会直接发你。 + diff --git a/problems/qita/tulunfabu.md b/problems/qita/tulunfabu.md new file mode 100644 index 0000000000..8987709564 --- /dev/null +++ b/problems/qita/tulunfabu.md @@ -0,0 +1,239 @@ + + +# 图论正式发布! + +录友们! 今天和大家正式宣布:大家期盼已久的「代码随想录图论」正式发布了。 + +**一年多来,日日夜夜,伏案编码、思考、写作,就为了今天给录友们一个交代**! + +我知道录友们在等图论等太久了,其实我比大家都着急。 + +![大家一直都在催](https://file1.kamacoder.com/i/algo/20240613105618.png) + +图论完整版目前已经开放在代码随想录网站:programmercarl.com + +**「代码随想录图论」共分为 五大模块**,共有三十一篇长文讲解: + +* 深搜与广搜 +* 并查集 +* 最小生成树 +* 拓扑排序 +* 最短路算法 + +![](https://file1.kamacoder.com/i/algo/20240613104436.png) + +**耗时一年之久,代码随想录图论 终于面世了**! + +一年前 23年的3月份 我刚刚更新完了 [代码随想录算法公开课](https://mp.weixin.qq.com/s/xsKjrnB4GyWApm4BYxshvg) ,这是我亲自录制的 140期 算法视频讲解,目前口碑极佳。 + +录完公开课之后,我就开始筹划更新图论内容了,无奈图论内容真的很庞大。 + +关于图论章节,**可以说 是代码随想录所有章节里画图数量最多,工程量最大的一个章节**,整个图论 画图就400百多幅。 + +随便截一些图,大家感受一下: + +![](https://file1.kamacoder.com/i/algo/20240613104703.png) + +![](https://file1.kamacoder.com/i/algo/20240613104824.png) + +![](https://file1.kamacoder.com/i/algo/20240613104852.png) + +![](https://file1.kamacoder.com/i/algo/20240613104926.png) + +![](https://file1.kamacoder.com/i/algo/20240613105007.png) + +![](https://file1.kamacoder.com/i/algo/20240613105030.png) + +![](https://file1.kamacoder.com/i/algo/20240613105106.png) + +![](https://file1.kamacoder.com/i/algo/20240613105143.png) + +具体内容,大家可以去代码随想录网站(programmercarl.com)去看看,非常精彩! + +期间有很多录友再催:**卡哥怎么不更新图论呢? 卡哥是不是不打算更新图论了**? 等等 + +每次我都要去解释一波。 + +如果我想 快速出图论章节,可以很快! + +**但我不想做 降低 代码随想录整体质量和口碑的事情**。 + +所以关于现在发布的「代码随想录图论」,我可以很自信的说,**这是市面上大家能看到的,最全、最细致图论算法教程**。 + +**我在写作的时候没有避开任何雷区,完全遇雷排雷,然后让大家舒舒服服的走过去**。 + +什么是雷区? + +很多知识点 ,大家去看资料的时候会发现是 没有讲解的或者一笔带过, 因为这种雷区知识点 很难讲清楚或者需要花费大量的时间去讲明白。 + +一些知识点是这样的:**自己一旦懂了就知道是那么回事,但要写出来,要给别人讲清楚,是很难的一件事**。 + +这些知识点同样是雷区,就是大家在看 教程或者算法讲解的时候,作者避而不谈的部分。 + +例如: 深搜为什么有两种写法,同样的广搜为什么有一种写法超时了,bellman_ford 为什么 要松弛 n - 1次,负权回路对最短路求解的影响 等等。 + +这一点大家在阅读代码随想录图论的时候,**可以感受到 我对细节讲解的把控程度**。 + +## 为什么要出图论 + +图论是很重要的章节,也是 大家求职笔试面试,无论是社招还是校招,都会考察的知识内容。 + +而且图论应用广泛,在大家做项目开发的时候,或多或少都会用到图论相关知识。 + +例如:通信网络(拓扑排序、最短路算法),社交网络(深搜、广搜),路径优化(最短路算法),任务调度(拓扑排序),生物信息学(基因为节点,基因关系为边),游戏开发(A * 算法等)等等 + +为了保质保量更新图论,**市面上所有的算法书籍,我都看过**! + +**反复确认 思路正确性的同时,不仅感叹 市面上的算法书籍 在图论方面的 “缺斤少两**” 。 + +大名鼎鼎的《算法4》 以图论内容详细且图解多 而被大家好评, + +以最短路算法为例,《算法4》,只讲解了 Dijkstra(堆优化)、SPFA (Bellman-Ford算法基于队列) 和 拓扑排序, + +而 dijkstra朴素版、Bellman_ford 朴素版、bellman_ford之单源有限最短路、Floyd 、A * 算法 以及各个最短路算法的优劣,并没有讲解。 + +其他算法书籍 更是对图论的很多知识点一笔带过。 + +而在 代码随想录图论章节,**仅仅是 最短路算法方面,我就详细讲解了如下内容**: + +* dijkstra朴素版 +* dijkstra堆优化版 +* Bellman_ford +* Bellman_ford 队列优化算法(又名SPFA) +* bellman_ford 算法判断负权回路 +* bellman_ford之单源有限最短路 +* Floyd 算法精讲 +* 启发式搜索:A * 算法 + +**常见最短路算法,我都没有落下**。 + +而且大家在看很多算法书籍是没有编程题目配合练习,这样学习效果大打折扣, 一些书籍有编程题目配合练习但知识点严重不全。 + +## 出题 + +我在讲解图论的时候,最头疼的就是找题,在力扣上 找题总是找不到符合思路且来完整表达算法精髓的题目。 + +特别是最短路算法相关的题目,例如 Bellman_ford系列 ,Floyd ,A * 等等总是找不到符合思路的题目。 + +所以我索性就自己出题吧,**这也是 卡码网(kamacoder.com)诞生的一个原因之一**。 + +**为了给大家带来极致的学习体验,我在很多细节上都下了功**夫。 + +卡码网专门给大家准备的ACM输入输出模式,**图论是在笔试还有面试中,通常都是以ACM模式来考察大家**,而大家习惯在力扣刷题(核心代码模式),核心代码模式对图的存储和输出都隐藏了。 + +**图论题目的输出输出相对其他章节的题目来说是最难处理的**。 + +### 输入的细节 + +图论的输入难在 图的存储结构,**如果没有练习过 邻接表和邻接矩阵 ,很多录友是写不出来的**。 + +而力扣上是直接给好现成的 数据结构,可以直接用,所以练习不到图的输入,也练习不到邻接表和邻接矩阵。 + +ACM输入输出模式是最考察候选人对代码细节把控程度。 + +如果熟练ACM模式,那么核心代码模式基本没问题,但反过来就不一定了。 + +### 输出的细节 + +同样,图论的输出也有细节,例如 求节点1 到节点5的所有路径, 输出可能是: + +``` +1 2 4 5 +1 3 5 +``` + +表示有两条路可以到节点5, 那储存这个结果需要二维数组,最后在一起输出,力扣是直接return数组就好了,但 ACM模式要求我们自己输出,这里有就细节了。 + +就拿 只输出一行数据,输出 `1 2 4 5` 来说, + +很多录友代码可能直接就这么写了: + +```CPP +for (int i = 0 ; i < result.size(); i++) { + cout << result[i] << " "; +} +``` + +这么写输出的结果是 `1 2 4 5 `, 发现结果是对的,一提交,发现OJ返回 格式错误 或者 结果错误。 + +如果没练习过这种输出方式的录友,就开始怀疑了,这结果一样一样的,怎么就不对,我在力扣上提交都是对的! + +**大家要注意,5 后面要不要有空格**! + +上面这段代码输出,5后面是加上了空格了,如果判题机判断 结果的长度,标准答案`1 2 4 5`长度是7,而上面代码输出的长度是 8,很明显就是不对的。 + +所以正确的写法应该是: + +```CPP +for (int i = 0 ; i < result.size() - 1; i++) { + cout << result[i] << " "; +} +cout << result[result.size() - 1]; +``` + +这么写,最后一个元素后面就没有空格了。 + +这是很多录友经常疏忽的,也是大家刷习惯了 力扣(核心代码模式)根本不会注意到的细节。 + +**同样在工程开发中,这些细节都是影响系统稳定运行的因素之一**。 + +**ACM模式 除了考验算法思路,也考验 大家对 代码的把控力度**, 而 核心代码模式 只注重算法的解题思路,所以输入输出这些就省略掉了。 + +## 情怀 + +大家可以发现,**现在 用心给大家更新硬核且免费资料的博主 已经不多了**。 + +这一年的空闲时间,如果我用来打磨付费课程或者付费项目,或者干脆把图论做成付费专栏 加上现在的影响力,一定可以 “狠狠赚一笔”。 + +对我来说,有些钱可以赚,有些钱不赚。 + +如果持续关注代码随想录的录友可以发现:代码随想录不仅仅优质题解和视频免费,还有 [ACM模版配套25题](https://mp.weixin.qq.com/s/ai_Br_jSayeV2ELIYvnMYQ)、 [设计模式精讲配套23题](https://mp.weixin.qq.com/s/Wmu8jW4ezCi4CQ0uT9v9iA)、[每周举办大厂笔试真题(制作真题是十分费时的)](https://mp.weixin.qq.com/s/ULTehoK4GbdbQIdauKYt1Q), 这些都是免费优质资源。 + +**在付费与免费之间,我一直都在努力寻找平衡**。 + +很多录友之所以付费加入 [知识星球](https://mp.weixin.qq.com/s/65Vrq6avJkuTqofnz361Rw) 或者 [算法训练营](https://mp.weixin.qq.com/s/vkbcihvdNvBu1W4-bExoXA) ,也是因为看了这些免费资源想支持我一下。 + +“不忘初心”,说出来很容易,**但真正能随着岁月的流淌 坚持初心,是非常非常难的事情**。 + +**诱惑太多!有惰性的诱惑,有利益的诱惑**。 + +正如我之前说的:“代码随想录” 这五个字,我是会用一生去经营。 + +**免费硬核的算法内容是 代码随想录的立身之本**,也是 大家为什么学算法学编程首选代码随想录的根本所在。 + +当大家通过 代码随想录 提升了编程与算法能力,考上研或者找到好工作的时候,于我来说已经是很幸福的事情: + +![对笔试帮助大](https://file1.kamacoder.com/i/algo/20230914172536.png) + +![华为od将近满分](https://file1.kamacoder.com/i/algo/20230914172607.png) + +![研究生复试](https://file1.kamacoder.com/i/algo/20240621103130.png) + +![红包感谢代码随想录366](https://file1.kamacoder.com/i/algo/20231123151310.png) + +![上岸亚马逊](https://file1.kamacoder.com/i/algo/20240206174151.png) + +![](https://file1.kamacoder.com/i/algo/20220718094112.png) + +![](https://file1.kamacoder.com/i/algo/20220718094332.png) + +至此**图论内容 已完全免费开放在代码随想录网站(programmercarl.com),造福广大学习编程的录友们**! + +Github 也已经同步 :https://github.com/youngyangyang04/leetcode-master ,关于其他语言版本,欢迎录友们去仓库提交PR + +## 后序 + +关于图论PDF版本,我后面会整理出来,免费发放给大家。 + +关于图论视频版本,不出意外,应该在年底开始在B站更新,同样免费开放。 + +总之,代码随想录会持续更新下去,无论是文字版还是视频版。 + +希望大家 不用 非要到找工作的时候 或者要考研的时候 才想到代码随想录。 + +**代码是作品,算法更是艺术**,时不时来欣赏一段解决关键问题的优雅代码,也是一种享受。 + +最后,**愿录友们学有所成,归来仍看代码随想录**! + + + diff --git a/problems/qita/tulunshuoming.md b/problems/qita/tulunshuoming.md new file mode 100755 index 0000000000..9d6e761ca8 --- /dev/null +++ b/problems/qita/tulunshuoming.md @@ -0,0 +1,44 @@ + +# 图论模块说明 + + +非常多录友在催更图论,同时大家也反馈面试中深搜广搜也最近常考的类型。 + +其实在代码随想录中的二叉树和回溯算法章节中已经讲过深搜和广搜,二叉树的遍历就是深搜和广搜在二叉树结构上的应用, 而回溯算法本身就是深搜,只不过利用其回溯的过程。 + +那么在图论中,深搜和广搜就是在图上的遍历,图的存储方式一般是 邻接表和邻接矩阵。 + +我已经在更新图论ing,不过还没有跟更新完,**之前计划是把更新完的部分先分享给[训练营](./xunlianying.html)和 [知识星球](./kstar.md) 录友,等全部更新完之后在完整的分享到网站上**。 + +不过其他录友们也很着急,我也算更新了不少了,就先分享出来给大家吧。 + +**我一直坚持给大家打造最硬核的算法教程而且是免费的!这一点一直都不会变!**。 + +(**注意图论章节还没有更新完,还有更精彩的内容在路上**) + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/problems/qita/update.md b/problems/qita/update.md new file mode 100755 index 0000000000..c562fd4b3c --- /dev/null +++ b/problems/qita/update.md @@ -0,0 +1,56 @@ + +## 2021年 8月11日 + +[代码随想录网站正式上线](https://mp.weixin.qq.com/s/-6rd_g7LrVD1fuKBYk2tXQ) + +## 2021年 10月19日 + +更新Java,Python,JS,Go版本题解 + +## 2022年 1月17日 + +添加评论功能和阅读量统计。 + +为了方便大家阅读,使用无需登录的评论插件 valine。 + +但由于本站访问量太大,leancloud的api调用超过3w次,就只能用付费版本了,本站使用2个小时之后就超过了3w次条用,而付费版本一年要上万块。 + +免费的网站实在承担不起,所以仅在部分页面添加的评论区。 + +例如各个专题中的理论基础和本章总结,都添加的评论区。 + +## 2022年 2月22日 + +升级内存和带宽以应对更大的访问量 + +## 2022年 5月12日 + +更新[星球生活](https://programmercarl.com/other/)专栏,题解支持C、TypeScript 语言版本 + +## 2022年 5月19日 + +补充[额外题目](https://programmercarl.com/other/ewaishuoming.html) + +## 2022年 6月10日 + +添加边框,可以方便调节黑暗模式,开始加入Scala 和 C# 语言版本。 + +## 2023年 5月8日 + +题解都配上了《代码随想录》算法公开课视频讲解 + +## 2023年 9月11日 + +更新部分图论内容,深搜广搜和并查集 + +## 2024年 4月7日 + +由于访问量过大,网站访问速度慢一直被很多录友诟病,特别是海外录友访问更卡。 + +这次网站全部上CDN,全球加速,方便全球录友学习。 + +同时添加github评论区,录友可以在每篇文章下打卡了! + +## 更多精彩,敬请期待 + + diff --git a/problems/toolgithub.sh b/problems/toolgithub.sh new file mode 100644 index 0000000000..4a0b0a75f0 --- /dev/null +++ b/problems/toolgithub.sh @@ -0,0 +1,76 @@ +######################################################################### + +# File Name: toolgithub.sh +# Author: 程序员Carl +# mail: programmercarl@163.com +# Created Time: Sat Oct 15 16:36:23 2022 +######################################################################### +#!/bin/bash + +#

+# +# +# +#

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+ +# +#

+# +# +# + +for i in *.md +do + if [[ $i != 'README.md' ]] + then + # 移除开头 + sed -i '' '/align/d;/\"\"><\/a>/d;/<\/p>/d;/<\/a>/d;/20210924105952.png/d;/_blank/d' $i + # 移除结尾 + sed -i '' '/训练营/d;/网站星球宣传海报/d' $i + + + # 添加开头 + # 记得从后向前添加 + # ex -sc '1i|

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们受益!

' -cx $i + # ex -sc '1i|' -cx $i + # ex -sc '1i| ' -cx $i + # ex -sc '1i|' -cx $i + # ex -sc '1i|

' -cx $i + + ex -sc '1i|* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html)' -cx $i + ex -sc '1i|* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html)' -cx $i + ex -sc '1i|* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html)' -cx $i + + + # echo '## 其他语言版本' >> $i + # echo '\n' >> $i + # echo 'Java:' >> $i + # echo '\n' >> $i + # echo 'Python:' >> $i + # echo '\n' >> $i + # echo 'Go:' >> $i + # echo '\n' >> $i + # echo '\n' >> $i + + # 添加结尾 + + # echo '

' >> $i + # echo '' >> $i + # echo ' ' >> $i + # echo '' >> $i + + # echo '-----------------------' >> $i + + # echo '

' >> $i + fi +done + +#

+# +# +# + +#

参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+ + + diff --git "a/problems/\344\270\272\344\272\206\347\273\235\346\235\200\347\274\226\350\276\221\350\267\235\347\246\273\357\274\214\345\215\241\345\260\224\345\201\232\344\272\206\344\270\211\346\255\245\351\223\272\345\236\253.md" "b/problems/\344\270\272\344\272\206\347\273\235\346\235\200\347\274\226\350\276\221\350\267\235\347\246\273\357\274\214\345\215\241\345\260\224\345\201\232\344\272\206\344\270\211\346\255\245\351\223\272\345\236\253.md" new file mode 100755 index 0000000000..69d6aa9c45 --- /dev/null +++ "b/problems/\344\270\272\344\272\206\347\273\235\346\235\200\347\274\226\350\276\221\350\267\235\347\246\273\357\274\214\345\215\241\345\260\224\345\201\232\344\272\206\344\270\211\346\255\245\351\223\272\345\236\253.md" @@ -0,0 +1,196 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 动态规划之编辑距离总结篇 + +本周我们讲了动态规划之终极绝杀:编辑距离,为什么叫做终极绝杀呢? + +细心的录友应该知道,我们在前三篇动态规划的文章就一直为 编辑距离 这道题目做铺垫。 + +## 判断子序列 + +[动态规划:392.判断子序列](https://programmercarl.com/0392.判断子序列.html) 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 + + +这道题目 其实是可以用双指针或者贪心的的,但是我在开篇的时候就说了这是编辑距离的入门题目,因为从题意中我们也可以发现,只需要计算删除的情况,不用考虑增加和替换的情况。 + +* if (s[i - 1] == t[j - 1]) + * t中找到了一个字符在s中也出现了 +* if (s[i - 1] != t[j - 1]) + * 相当于t要删除元素,继续匹配 + +状态转移方程: + +``` +if (s[i - 1] == t[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1; +else dp[i][j] = dp[i][j - 1]; +``` + +## 不同的子序列 + +[动态规划:115.不同的子序列](https://programmercarl.com/0115.不同的子序列.html) 给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 + +本题虽然也只有删除操作,不用考虑替换增加之类的,但相对于[动态规划:392.判断子序列](https://programmercarl.com/0392.判断子序列.html)就有难度了,这道题目双指针法可就做不了。 + + +当s[i - 1] 与 t[j - 1]相等时,dp[i][j]可以有两部分组成。 + +一部分是用s[i - 1]来匹配,那么个数为dp[i - 1][j - 1]。 + +一部分是不用s[i - 1]来匹配,个数为dp[i - 1][j]。 + +这里可能有同学不明白了,为什么还要考虑 不用s[i - 1]来匹配,都相同了指定要匹配啊。 + +例如: s:bagg 和 t:bag ,s[3] 和 t[2]是相同的,但是字符串s也可以不用s[3]来匹配,即用s[0]s[1]s[2]组成的bag。 + +当然也可以用s[3]来匹配,即:s[0]s[1]s[3]组成的bag。 + +所以当s[i - 1] 与 t[j - 1]相等时,dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; + +当s[i - 1] 与 t[j - 1]不相等时,dp[i][j]只有一部分组成,不用s[i - 1]来匹配,即:dp[i - 1][j] + +所以递推公式为:dp[i][j] = dp[i - 1][j]; + + +状态转移方程: +```CPP +if (s[i - 1] == t[j - 1]) { + dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; +} else { + dp[i][j] = dp[i - 1][j]; +} +``` + +## 两个字符串的删除操作 + +[动态规划:583.两个字符串的删除操作](https://programmercarl.com/0583.两个字符串的删除操作.html)给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最少步数,每步可以删除任意一个字符串中的一个字符。 + +本题和[动态规划:115.不同的子序列](https://programmercarl.com/0115.不同的子序列.html)相比,其实就是两个字符串可以都可以删除了,情况虽说复杂一些,但整体思路是不变的。 + + +* 当word1[i - 1] 与 word2[j - 1]相同的时候 +* 当word1[i - 1] 与 word2[j - 1]不相同的时候 + +当word1[i - 1] 与 word2[j - 1]相同的时候,dp[i][j] = dp[i - 1][j - 1]; + +当word1[i - 1] 与 word2[j - 1]不相同的时候,有三种情况: + +情况一:删word1[i - 1],最少操作次数为dp[i - 1][j] + 1 + +情况二:删word2[j - 1],最少操作次数为dp[i][j - 1] + 1 + +情况三:同时删word1[i - 1]和word2[j - 1],操作的最少次数为dp[i - 1][j - 1] + 2 + +那最后当然是取最小值,所以当word1[i - 1] 与 word2[j - 1]不相同的时候,递推公式:dp[i][j] = min({dp[i - 1][j - 1] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1}); + +状态转移方程: +```CPP +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] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1}); +} +``` + + +## 编辑距离 + +[动态规划:72.编辑距离](https://programmercarl.com/0072.编辑距离.html) 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 + + +编辑距离终于来了,**有了前面三道题目的铺垫,应该有思路了**,本题是两个字符串可以增删改,比 [动态规划:判断子序列](https://programmercarl.com/0392.判断子序列.html),[动态规划:不同的子序列](https://programmercarl.com/0115.不同的子序列.html),[动态规划:两个字符串的删除操作](https://programmercarl.com/0583.两个字符串的删除操作.html)都要复杂的多。 + + +在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下: + +* if (word1[i - 1] == word2[j - 1]) + * 不操作 +* if (word1[i - 1] != word2[j - 1]) + * 增 + * 删 + * 换 + +也就是如上四种情况。 + +if (word1[i - 1] == word2[j - 1]) 那么说明不用任何编辑,dp[i][j] 就应该是 dp[i - 1][j - 1],即dp[i][j] = dp[i - 1][j - 1]; + +此时可能有同学有点不明白,为啥要即dp[i][j] = dp[i - 1][j - 1]呢? + +那么就在回顾上面讲过的dp[i][j]的定义,word1[i - 1] 与 word2[j - 1]相等了,那么就不用编辑了,以下标i-2为结尾的字符串word1和以下标j-2为结尾的字符串word2的最近编辑距离dp[i - 1][j - 1] 就是 dp[i][j]了。 + +在下面的讲解中,如果哪里看不懂,就回想一下dp[i][j]的定义,就明白了。 + +**在整个动规的过程中,最为关键就是正确理解dp[i][j]的定义!** + +if (word1[i - 1] != word2[j - 1]),此时就需要编辑了,如何编辑呢? + +操作一:word1增加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-2为结尾的word1 与 i-1为结尾的word2的最近编辑距离 加上一个增加元素的操作。 + +即 dp[i][j] = dp[i - 1][j] + 1; + + +操作二:word2添加一个元素,使其word1[i - 1]与word2[j - 1]相同,那么就是以下标i-1为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个增加元素的操作。 + +即 dp[i][j] = dp[i][j - 1] + 1; + +这里有同学发现了,怎么都是添加元素,删除元素去哪了。 + +**word2添加一个元素,相当于word1删除一个元素**,例如 word1 = "ad" ,word2 = "a",word2添加一个元素d,也就是相当于word1删除一个元素d,操作数是一样! + +操作三:替换元素,word1替换word1[i - 1],使其与word2[j - 1]相同,此时不用增加元素,那么以下标i-2为结尾的word1 与 j-2为结尾的word2的最近编辑距离 加上一个替换元素的操作。 + +即 dp[i][j] = dp[i - 1][j - 1] + 1; + +综上,当 if (word1[i - 1] != word2[j - 1]) 时取最小的,即:dp[i][j] = min({dp[i - 1][j - 1], dp[i - 1][j], dp[i][j - 1]}) + 1; + +递归公式代码如下: + +```CPP +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], dp[i - 1][j], dp[i][j - 1]}) + 1; +} +``` + +## 总结 + +心思的录友应该会发现我用了三道题做铺垫,才最后引出了[动态规划:72.编辑距离](https://programmercarl.com/0072.编辑距离.html) ,Carl的良苦用心呀,你们体会到了嘛! + +## 其他语言版本 + +### Java: + +```java +class Solution { + public int minDistance(String word1, String word2) { + int m = word1.length(); + int n = word2.length(); + int[][] dp = new int[m+1][n+1]; + for(int i = 1; i <= m; i++){ + dp[i][0] = i; + } + for(int i = 1; i <= n; i++){ + dp[0][i] = i; + } + for(int i = 1; i <= m; i++){ + for(int j = 1; j <= n; j++){ + int left = dp[i][j-1]+1; + int mid = dp[i-1][j-1]; + int right = dp[i-1][j]+1; + if(word1.charAt(i-1) != word2.charAt(j-1)){ + mid ++; + } + dp[i][j] = Math.min(left,Math.min(mid,right)); + } + } + return dp[m][n]; + } +} +``` + + + + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\344\270\255\351\200\222\345\275\222\345\270\246\347\235\200\345\233\236\346\272\257.md" "b/problems/\344\272\214\345\217\211\346\240\221\344\270\255\351\200\222\345\275\222\345\270\246\347\235\200\345\233\236\346\272\257.md" new file mode 100755 index 0000000000..7fa2b6ec94 --- /dev/null +++ "b/problems/\344\272\214\345\217\211\346\240\221\344\270\255\351\200\222\345\275\222\345\270\246\347\235\200\345\233\236\346\272\257.md" @@ -0,0 +1,687 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 二叉树:以为使用了递归,其实还隐藏着回溯 + +> 补充一波 + +昨天的总结篇中[还在玩耍的你,该总结啦!(本周小结之二叉树)](https://programmercarl.com/周总结/20201003二叉树周末总结.html),有两处问题需要说明一波。 + +## 求相同的树 + +[还在玩耍的你,该总结啦!(本周小结之二叉树)](https://programmercarl.com/周总结/20201003二叉树周末总结.html)中求100.相同的树的代码中,我笔误贴出了 求对称树的代码了,细心的同学应该都发现了。 + +那么如下我再给出求100. 相同的树 的代码,如下: + +```CPP +class Solution { +public: + bool compare(TreeNode* tree1, TreeNode* tree2) { + if (tree1 == NULL && tree2 != NULL) return false; + else if (tree1 != NULL && tree2 == NULL) return false; + else if (tree1 == NULL && tree2 == NULL) return true; + else if (tree1->val != tree2->val) return false; // 注意这里我没有使用else + + // 此时就是:左右节点都不为空,且数值相同的情况 + // 此时才做递归,做下一层的判断 + bool compareLeft = compare(tree1->left, tree2->left); // 左子树:左、 右子树:左 + bool compareRight = compare(tree1->right, tree2->right); // 左子树:右、 右子树:右 + bool isSame = compareLeft && compareRight; // 左子树:中、 右子树:中(逻辑处理) + return isSame; + + } + bool isSameTree(TreeNode* p, TreeNode* q) { + return compare(p, q); + } +}; +``` + +以上的代码相对于:[二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html) 仅仅修改了变量的名字(为了符合判断相同树的语境)和 遍历的顺序。 + +大家应该会体会到:**认清[判断对称树](https://programmercarl.com/0101.对称二叉树.html)本质之后, 对称树的代码 稍作修改 就可以直接用来AC 100.相同的树。** + +## 递归中隐藏着回溯 + +在[二叉树:找我的所有路径?](https://programmercarl.com/0257.二叉树的所有路径.html)中我强调了本题其实是用到了回溯的,并且给出了第一个版本的代码,把回溯的过程充分的提现了出来。 + +如下的代码充分的体现出回溯:(257. 二叉树的所有路径) + +```CPP +class Solution { +private: + + void traversal(TreeNode* cur, vector& path, vector& result) { + path.push_back(cur->val); + // 这才到了叶子节点 + if (cur->left == NULL && cur->right == NULL) { + string sPath; + for (int i = 0; i < path.size() - 1; i++) { + sPath += to_string(path[i]); + sPath += "->"; + } + sPath += to_string(path[path.size() - 1]); + result.push_back(sPath); + return; + } + if (cur->left) { + traversal(cur->left, path, result); + path.pop_back(); // 回溯 + } + if (cur->right) { + traversal(cur->right, path, result); + path.pop_back(); // 回溯 + } + } + +public: + vector binaryTreePaths(TreeNode* root) { + vector result; + vector path; + if (root == NULL) return result; + traversal(root, path, result); + return result; + } +}; +``` + + +如下为精简之后的递归代码:(257. 二叉树的所有路径) +```CPP +class Solution { +private: + void traversal(TreeNode* cur, string path, vector& result) { + path += to_string(cur->val); // 中 + if (cur->left == NULL && cur->right == NULL) { + result.push_back(path); + return; + } + if (cur->left) traversal(cur->left, path + "->", result); // 左 回溯就隐藏在这里 + if (cur->right) traversal(cur->right, path + "->", result); // 右 回溯就隐藏在这里 + } + +public: + vector binaryTreePaths(TreeNode* root) { + vector result; + string path; + if (root == NULL) return result; + traversal(root, path, result); + return result; + } +}; +``` + +上面的代码,大家貌似感受不到回溯了,其实**回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。** + +为了把这份精简代码的回溯过程展现出来,大家可以试一试把: + +```CPP +if (cur->left) traversal(cur->left, path + "->", result); // 左 回溯就隐藏在这里 +``` + +改成如下代码: + +```CPP +path += "->"; +traversal(cur->left, path, result); // 左 +``` + +即: + +``` CPP +if (cur->left) { + path += "->"; + traversal(cur->left, path, result); // 左 +} +if (cur->right) { + path += "->"; + traversal(cur->right, path, result); // 右 +} +``` + +因为在递归右子树之前需要还原path,所以在左子树递归后必须为了右子树而进行回溯操作。而只右子树自己不添加回溯也可以成功AC。 + +因此,在上面代码的基础上,再加上左右子树的回溯代码,就可以AC了。 + +```CPP +if (cur->left) { + path += "->"; + traversal(cur->left, path, result); // 左 + path.pop_back(); // 回溯,抛掉val + path.pop_back(); // 回溯,抛掉-> +} +if (cur->right) { + path += "->"; + traversal(cur->right, path, result); // 右 + path.pop_back(); // 回溯(非必要) + path.pop_back(); +} +``` + +**大家应该可以感受出来,如果把 `path + "->"`作为函数参数就是可以的,因为并有没有改变path的数值,执行完递归函数之后,path依然是之前的数值(相当于回溯了)** + +如果有点遗忘了,建议把这篇[二叉树:找我的所有路径?](https://programmercarl.com/0257.二叉树的所有路径.html)在仔细看一下,然后再看这里的总结,相信会豁然开朗。 + +这里我尽量把逻辑的每一个细节都抠出来展现了,希望对大家有所帮助! + + +## 其他语言版本 + + +### Java: + 100. 相同的树:递归代码 + ```java + class Solution { + public boolean compare(TreeNode tree1, TreeNode tree2) { + + if(tree1==null && tree2==null)return true; + if(tree1==null || tree2==null)return false; + if(tree1.val!=tree2.val)return false; + // 此时就是:左右节点都不为空,且数值相同的情况 + // 此时才做递归,做下一层的判断 + boolean compareLeft = compare(tree1.left, tree2.left); // 左子树:左、 右子树:左 + boolean compareRight = compare(tree1.right, tree2.right); // 左子树:右、 右子树:右 + boolean isSame = compareLeft && compareRight; // 左子树:中、 右子树:中(逻辑处理) + return isSame; + + } + boolean isSameTree(TreeNode p, TreeNode q) { + return compare(p, q); + } +} + ``` + 257. 二叉树的所有路径: 回溯代码 + ```java + class Solution { + public void traversal(TreeNode cur, List path, List result) { + path.add(cur.val); + // 这才到了叶子节点 + if (cur.left == null && cur.right == null) { + String sPath=""; + for (int i = 0; i < path.size() - 1; i++) { + sPath += ""+path.get(i); + sPath += "->"; + } + sPath += path.get(path.size() - 1); + result.add(sPath); + return; + } + if (cur.left!=null) { + traversal(cur.left, path, result); + path.remove(path.size()-1); // 回溯 + } + if (cur.right!=null) { + traversal(cur.right, path, result); + path.remove(path.size()-1); // 回溯 + } + } + + public List binaryTreePaths(TreeNode root) { + List result = new LinkedList<>(); + List path = new LinkedList<>(); + if (root == null) return result; + traversal(root, path, result); + return result; + } +} + + ``` + 如下为精简之后的递归代码:(257. 二叉树的所有路径) + ```java + class Solution { + public void traversal(TreeNode cur, String path, List result) { + path += cur.val; // 中 + if (cur.left == null && cur.right == null) { + result.add(path); + return; + } + if (cur.left!=null) traversal(cur.left, path + "->", result); // 左 回溯就隐藏在这里 + if (cur.right!=null) traversal(cur.right, path + "->", result); // 右 回溯就隐藏在这里 + } + + public List binaryTreePaths(TreeNode root) { + List result = new LinkedList<>(); + String path = ""; + if (root == null) return result; + traversal(root, path, result); + return result; + } +} + ``` + +### Python: + +100.相同的树 +> 递归法 +```python +class Solution: + def isSameTree(self, p: TreeNode, q: TreeNode) -> bool: + return self.compare(p, q) + + def compare(self, tree1, tree2): + if not tree1 and tree2: + return False + elif tree1 and not tree2: + return False + elif not tree1 and not tree2: + return True + elif tree1.val != tree2.val: #注意这里我没有使用else + return False + + #此时就是:左右节点都不为空,且数值相同的情况 + #此时才做递归,做下一层的判断 + compareLeft = self.compare(tree1.left, tree2.left) #左子树:左、 右子树:左 + compareRight = self.compare(tree1.right, tree2.right) #左子树:右、 右子树:右 + isSame = compareLeft and compareRight #左子树:中、 右子树:中(逻辑处理) + return isSame +``` + +257.二叉的所有路径 +> 递归中隐藏着回溯 +```python +class Solution: + def binaryTreePaths(self, root: TreeNode) -> List[str]: + result = [] + path = [] + if not root: + return result + self.traversal(root, path, result) + return result + + def traversal(self, cur, path, result): + path.append(cur.val) + #这才到了叶子节点 + if not cur.left and not cur.right: + sPath = "" + for i in range(len(path)-1): + sPath += str(path[i]) + sPath += "->" + sPath += str(path[len(path)-1]) + result.append(sPath) + return + if cur.left: + self.traversal(cur.left, path, result) + path.pop() #回溯 + if cur.right: + self.traversal(cur.right, path, result) + path.pop() #回溯 +``` + +> 精简版 +```python +class Solution: + def binaryTreePaths(self, root: TreeNode) -> List[str]: + result = [] + path = "" + if not root: + return result + self.traversal(root, path, result) + return result + + def traversal(self, cur, path, result): + path += str(cur.val) #中 + if not cur.left and not cur.right: + result.append(path) + return + if cur.left: + self.traversal(cur.left, path+"->", result) #左 回溯就隐藏在这里 + if cur.right: + self.traversal(cur.right, path+"->", result) #右 回溯就隐藏在这里 +``` + +### Go: + +100.相同的树 +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func isSameTree(p *TreeNode, q *TreeNode) bool { + switch { + case p == nil && q == nil: + return true + case p == nil || q == nil: + fallthrough + case p.Val != q.Val: + return false + } + return isSameTree(p.Left, q.Left) && isSameTree(p.Right, q.Right) +} +``` + +257.二叉的所有路径 +> 递归法 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func binaryTreePaths(root *TreeNode) []string { + var result []string + traversal(root,&result,"") + return result +} +func traversal(root *TreeNode,result *[]string,pathStr string){ + //判断是否为第一个元素 + if len(pathStr)!=0{ + pathStr=pathStr+"->"+strconv.Itoa(root.Val) + }else{ + pathStr=strconv.Itoa(root.Val) + } + //判断是否为叶子节点 + if root.Left==nil&&root.Right==nil{ + *result=append(*result,pathStr) + return + } + //左右 + if root.Left!=nil{ + traversal(root.Left,result,pathStr) + } + if root.Right!=nil{ + traversal(root.Right,result,pathStr) + } +} +``` + +> 回溯法 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ +func binaryTreePaths(root *TreeNode) []string { + var result []string + var path []int + traversal(root,&result,&path) + return result +} +func traversal(root *TreeNode,result *[]string,path *[]int){ + *path=append(*path,root.Val) + //判断是否为叶子节点 + if root.Left==nil&&root.Right==nil{ + pathStr:=strconv.Itoa((*path)[0]) + for i:=1;i"+strconv.Itoa((*path)[i]) + } + *result=append(*result,pathStr) + return + } + //左右 + if root.Left!=nil{ + traversal(root.Left,result,path) + *path=(*path)[:len(*path)-1]//回溯到上一个节点(因为traversal会加下一个节点值到path中) + } + if root.Right!=nil{ + traversal(root.Right,result,path) + *path=(*path)[:len(*path)-1]//回溯 + } +} +``` + +### JavaScript: + +100.相同的树 +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} p + * @param {TreeNode} q + * @return {boolean} + */ +var isSameTree = function(p, q) { + if (p === null && q === null) { + return true; + } else if (p === null || q === null) { + return false; + } else if (p.val !== q.val) { + return false; + } else { + return isSameTree(p.left, q.left) && isSameTree(p.right, q.right); + } +}; +``` + +257.二叉树的不同路径 + +> 回溯法: + +```javascript +/** + * Definition for a binary tree node. + * function TreeNode(val, left, right) { + * this.val = (val===undefined ? 0 : val) + * this.left = (left===undefined ? null : left) + * this.right = (right===undefined ? null : right) + * } + */ +/** + * @param {TreeNode} root + * @return {string[]} + */ +var binaryTreePaths = function(root) { + const getPath = (root, path, result) => { + path.push(root.val); + if (root.left === null && root.right === null) { + let n = path.length; + let str = ''; + for (let i=0; i'; + } + str += path[n-1]; + result.push(str); + } + + if (root.left !== null) { + getPath(root.left, path, result); + path.pop(); // 回溯 + } + + if (root.right !== null) { + getPath(root.right, path, result); + path.pop(); + } + } + + if (root === null) return []; + let result = []; + let path = []; + getPath(root, path, result); + return result; +}; +``` + + +### TypeScript: + +> 相同的树 + +```typescript +function isSameTree(p: TreeNode | null, q: TreeNode | null): boolean { + if (p === null && q === null) return true; + if (p === null || q === null) return false; + if (p.val !== q.val) return false; + let bool1: boolean, bool2: boolean; + bool1 = isSameTree(p.left, q.left); + bool2 = isSameTree(p.right, q.right); + return bool1 && bool2; +}; +``` + +> 二叉树的不同路径 + +```typescript +function binaryTreePaths(root: TreeNode | null): string[] { + function recur(node: TreeNode, nodeSeqArr: number[], resArr: string[]): void { + nodeSeqArr.push(node.val); + if (node.left === null && node.right === null) { + resArr.push(nodeSeqArr.join('->')); + } + if (node.left !== null) { + recur(node.left, nodeSeqArr, resArr); + nodeSeqArr.pop(); + } + if (node.right !== null) { + recur(node.right, nodeSeqArr, resArr); + nodeSeqArr.pop(); + } + } + let nodeSeqArr: number[] = []; + let resArr: string[] = []; + if (root === null) return resArr; + recur(root, nodeSeqArr, resArr); + return resArr; +}; +``` + + + + +### Swift: +> 100.相同的树 +```swift +// 递归 +func isSameTree(_ p: TreeNode?, _ q: TreeNode?) -> Bool { + return _isSameTree3(p, q) +} +func _isSameTree3(_ p: TreeNode?, _ q: TreeNode?) -> Bool { + if p == nil && q == nil { + return true + } else if p == nil && q != nil { + return false + } else if p != nil && q == nil { + return false + } else if p!.val != q!.val { + return false + } + let leftSide = _isSameTree3(p!.left, q!.left) + let rightSide = _isSameTree3(p!.right, q!.right) + return leftSide && rightSide +} +``` + +> 257.二叉树的不同路径 +```swift +// 递归/回溯 +func binaryTreePaths(_ root: TreeNode?) -> [String] { + var res = [String]() + guard let root = root else { + return res + } + var paths = [Int]() + _binaryTreePaths3(root, res: &res, paths: &paths) + return res +} +func _binaryTreePaths3(_ root: TreeNode, res: inout [String], paths: inout [Int]) { + paths.append(root.val) + if root.left == nil && root.right == nil { + var str = "" + for i in 0 ..< (paths.count - 1) { + str.append("\(paths[i])->") + } + str.append("\(paths.last!)") + res.append(str) + } + if let left = root.left { + _binaryTreePaths3(left, res: &res, paths: &paths) + paths.removeLast() + } + if let right = root.right { + _binaryTreePaths3(right, res: &res, paths: &paths) + paths.removeLast() + } +} +``` + +### Rust + +> 100.相同的树 + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn is_same_tree( + p: Option>>, + q: Option>>, + ) -> bool { + match (p, q) { + (None, None) => true, + (None, Some(_)) => false, + (Some(_), None) => false, + (Some(n1), Some(n2)) => { + if n1.borrow().val == n2.borrow().val { + let right = + Self::is_same_tree(n1.borrow().left.clone(), n2.borrow().left.clone()); + let left = + Self::is_same_tree(n1.borrow().right.clone(), n2.borrow().right.clone()); + right && left + } else { + false + } + } + } + } +} +``` + +> 257.二叉树的不同路径 + +```rust +// 递归 +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn binary_tree_paths(root: Option>>) -> Vec { + let mut res = vec![]; + let mut path = vec![]; + Self::recur(&root, &mut path, &mut res); + res + } + + pub fn recur( + root: &Option>>, + path: &mut Vec, + res: &mut Vec, + ) { + let node = root.as_ref().unwrap().borrow(); + path.push(node.val.to_string()); + if node.left.is_none() && node.right.is_none() { + res.push(path.join("->")); + return; + } + if node.left.is_some() { + Self::recur(&node.left, path, res); + path.pop(); //回溯 + } + if node.right.is_some() { + Self::recur(&node.right, path, res); + path.pop(); //回溯 + } + } +} +``` + + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" "b/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" deleted file mode 100644 index b7b0004f1a..0000000000 --- "a/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223.md" +++ /dev/null @@ -1,164 +0,0 @@ -

- -

-

- - - - - - -

- - -不知不觉二叉树已经和我们度过了**三十三天**,[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)里已经发了**三十三篇二叉树的文章**,详细讲解了**30+二叉树经典题目**,一直坚持下来的录友们一定会二叉树有深刻理解了。 - -在每一道二叉树的题目中,我都使用了**递归三部曲**来分析题目,相信大家以后看到二叉树,看到递归,都会想:返回值、参数是什么?终止条件是什么?单层逻辑是什么? - -而且**几乎每一道题目我都给出对应的迭代法**,可以用来进一步提高自己。 - -下面Carl把分析过的题目分门别类,可以帮助新录友循序渐进学习二叉树,也方便老录友面试前快速复习,看到一个标题,就回想一下对应的解题思路,这样很快就可以系统性的复习一遍二叉树了。 - -公众号的发文顺序,就是循序渐进的,所以如下分类基本就是按照文章发文顺序来的,我再做一个系统性的分类。 - -# 二叉树的理论基础 - -* [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A):二叉树的种类、存储方式、遍历方式、定义方式 - -# 二叉树的遍历方式 - -* 深度优先遍历 - * [二叉树:前中后序递归法](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA):递归三部曲初次亮相 - * [二叉树:前中后序迭代法(一)](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg):通过栈模拟递归 - * [二叉树:前中后序迭代法(二)统一风格](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) -* 广度优先遍历 - * [二叉树的层序遍历](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog):通过队列模拟 - - -# 求二叉树的属性 - -* [二叉树:是否对称](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg) - * 递归:后序,比较的是根节点的左子树与右子树是不是相互翻转 - * 迭代:使用队列/栈将两个节点顺序放入容器中进行比较 -* [二叉树:求最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg) - * 递归:后序,求根节点最大高度就是最大深度,通过递归函数的返回值做计算树的高度 - * 迭代:层序遍历 -* [二叉树:求最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA) - * 递归:后序,求根节点最小高度就是最小深度,注意最小深度的定义 - * 迭代:层序遍历 -* [二叉树:求有多少个节点](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw) - * 递归:后序,通过递归函数的返回值计算节点数量 - * 迭代:层序遍历 -* [二叉树:是否平衡](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww) - * 递归:后序,注意后序求高度和前序求深度,递归过程判断高度差 - * 迭代:效率很低,不推荐 -* [二叉树:找所有路径](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA) - * 递归:前序,方便让父节点指向子节点,涉及回溯处理根节点到叶子的所有路径 - * 迭代:一个栈模拟递归,一个栈来存放对应的遍历路径 -* [二叉树:递归中如何隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) - * 详解[二叉树:找所有路径](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)中递归如何隐藏着回溯 -* [二叉树:求左叶子之和](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) - * 递归:后序,必须三层约束条件,才能判断是否是左叶子。 - * 迭代:直接模拟后序遍历 -* [二叉树:求左下角的值](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw) - * 递归:顺序无所谓,优先左孩子搜索,同时找深度最大的叶子节点。 - * 迭代:层序遍历找最后一行最左边 -* [二叉树:求路径总和](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg) - * 递归:顺序无所谓,递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树。 - * 迭代:栈里元素不仅要记录节点指针,还要记录从头结点到该节点的路径数值总和 - -> **本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** - - -# 二叉树的修改与构造 - -* [翻转二叉树](https://mp.weixin.qq.com/s/6gY1MiXrnm-khAAJiIb5Bg) - * 递归:前序,交换左右孩子 - * 迭代:直接模拟前序遍历 -* [构造二叉树](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) - * 递归:前序,重点在于找分割点,分左右区间构造 - * 迭代:比较复杂,意义不大 -* [构造最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w) - * 递归:前序,分割点为数组最大值,分左右区间构造 - * 迭代:比较复杂,意义不大 -* [合并两个二叉树](https://mp.weixin.qq.com/s/3f5fbjOFaOX_4MXzZ97LsQ) - * 递归:前序,同时操作两个树的节点,注意合并的规则 - * 迭代:使用队列,类似层序遍历 - -# 求二叉搜索树的属性 - -* [二叉搜索树中的搜索](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg) - * 递归:二叉搜索树的递归是有方向的 - * 迭代:因为有方向,所以迭代法很简单 -* [是不是二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q) - * 递归:中序,相当于变成了判断一个序列是不是递增的 - * 迭代:模拟中序,逻辑相同 -* [求二叉搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ) - * 递归:中序,双指针操作 - * 迭代:模拟中序,逻辑相同 -* [求二叉搜索树的众数](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg) - * 递归:中序,清空结果集的技巧,遍历一遍便可求众数集合 - * 迭代:模拟中序,逻辑相同 -* [二叉搜索树转成累加树](https://mp.weixin.qq.com/s/hZtJh4T5lIGBarY-lZJf6Q) - * 递归:中序,双指针操作累加 - * 迭代:模拟中序,逻辑相同 - -# 二叉树公共祖先问题 - -* [二叉树的公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ) - * 递归:后序,回溯,找到左子树出现目标值,右子树节点目标值的节点。 - * 迭代:不适合模拟回溯 -* [二叉搜索树的公共祖先问题](https://mp.weixin.qq.com/s/Ja9dVw2QhBcg_vV-1fkiCg) - * 递归:顺序无所谓,如果节点的数值在目标区间就是最近公共祖先 - * 迭代:按序遍历 - -# 二叉搜索树的修改与构造 - -* [二叉搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA) - * 递归:顺序无所谓,通过递归函数返回值添加节点 - * 迭代:按序遍历,需要记录插入父节点,这样才能做插入操作 -* [二叉搜索树中的删除操作](https://mp.weixin.qq.com/s/-p-Txvch1FFk3ygKLjPAKw) - * 递归:前序,想清楚删除非叶子节点的情况 - * 迭代:有序遍历,较复杂 -* [修剪二叉搜索树](https://mp.weixin.qq.com/s/QzmGfYUMUWGkbRj7-ozHoQ) - * 递归:前序,通过递归函数返回值删除节点 - * 迭代:有序遍历,较复杂 -* [构造二叉搜索树](https://mp.weixin.qq.com/s/sy3ygnouaZVJs8lhFgl9mw) - * 递归:前序,数组中间节点分割 - * 迭代:较复杂,通过三个队列来模拟 - -# 阶段总结 - -大家以上题目都做过了,也一定要看如下阶段小结。 - -**每周小结都会对大家的疑问做统一解答,并且对每周的内容进行拓展和补充,所以一定要看,将细碎知识点一网打尽!** - -* [本周小结!(二叉树系列一)](https://mp.weixin.qq.com/s/JWmTeC7aKbBfGx4TY6uwuQ) -* [本周小结!(二叉树系列二)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg) -* [本周小结!(二叉树系列三)](https://mp.weixin.qq.com/s/JLLpx3a_8jurXcz6ovgxtg) -* [本周小结!(二叉树系列四)](https://mp.weixin.qq.com/s/CbdtOTP0N-HIP7DR203tSg) - -# 最后总结 - -**在二叉树题目选择什么遍历顺序是不少同学头疼的事情,我们做了这么多二叉树的题目了,Carl给大家大体分分类**。 - -* 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。 - -* 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。 - -* 求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。 - -注意在普通二叉树的属性中,我用的是一般为后序,例如单纯求深度就用前序, [二叉树:找所有路径](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)也用了前序,这是为了方便让父节点指向子节点。 - -所以求普通二叉树的属性还是要具体问题具体分析。 - -**最后,二叉树系列就这么完美结束了,估计这应该是最长的系列了,感谢大家33天的坚持与陪伴,接下来我们又要开始新的系列了「回溯算法」!** - - -**录友们打卡的时候也说一说自己的感想吧!哈哈** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)** - - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git "a/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223\347\257\207.md" "b/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223\347\257\207.md" new file mode 100755 index 0000000000..7d25d81876 --- /dev/null +++ "b/problems/\344\272\214\345\217\211\346\240\221\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,159 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 二叉树:总结篇!(需要掌握的二叉树技能都在这里了) + +> 力扣二叉树大总结! + +不知不觉二叉树已经和我们度过了**三十三天**,[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)里已经发了**三十三篇二叉树的文章**,详细讲解了**30+二叉树经典题目**,一直坚持下来的录友们一定会二叉树有深刻理解了。 + +在每一道二叉树的题目中,我都使用了**递归三部曲**来分析题目,相信大家以后看到二叉树,看到递归,都会想:返回值、参数是什么?终止条件是什么?单层逻辑是什么? + +而且**几乎每一道题目我都给出对应的迭代法**,可以用来进一步提高自己。 + +下面Carl把分析过的题目分门别类,可以帮助新录友循序渐进学习二叉树,也方便老录友面试前快速复习,看到一个标题,就回想一下对应的解题思路,这样很快就可以系统性的复习一遍二叉树了。 + +公众号的发文顺序,就是循序渐进的,所以如下分类基本就是按照文章发文顺序来的,我再做一个系统性的分类。 + +## 二叉树的理论基础 + +* [关于二叉树,你该了解这些!](https://programmercarl.com/二叉树理论基础.html):二叉树的种类、存储方式、遍历方式、定义方式 + +## 二叉树的遍历方式 + +* 深度优先遍历 + * [二叉树:前中后序递归法](https://programmercarl.com/二叉树的递归遍历.html):递归三部曲初次亮相 + * [二叉树:前中后序迭代法(一)](https://programmercarl.com/二叉树的迭代遍历.html):通过栈模拟递归 + * [二叉树:前中后序迭代法(二)统一风格](https://programmercarl.com/二叉树的统一迭代法.html) +* 广度优先遍历 + * [二叉树的层序遍历](https://programmercarl.com/0102.二叉树的层序遍历.html):通过队列模拟 + + +## 求二叉树的属性 + +* [二叉树:是否对称](https://programmercarl.com/0101.对称二叉树.html) + * 递归:后序,比较的是根节点的左子树与右子树是不是相互翻转 + * 迭代:使用队列/栈将两个节点顺序放入容器中进行比较 +* [二叉树:求最大深度](https://programmercarl.com/0104.二叉树的最大深度.html) + * 递归:后序,求根节点最大高度就是最大深度,通过递归函数的返回值做计算树的高度 + * 迭代:层序遍历 +* [二叉树:求最小深度](https://programmercarl.com/0111.二叉树的最小深度.html) + * 递归:后序,求根节点最小高度就是最小深度,注意最小深度的定义 + * 迭代:层序遍历 +* [二叉树:求有多少个节点](https://programmercarl.com/0222.完全二叉树的节点个数.html) + * 递归:后序,通过递归函数的返回值计算节点数量 + * 迭代:层序遍历 +* [二叉树:是否平衡](https://programmercarl.com/0110.平衡二叉树.html) + * 递归:后序,注意后序求高度和前序求深度,递归过程判断高度差 + * 迭代:效率很低,不推荐 +* [二叉树:找所有路径](https://programmercarl.com/0257.二叉树的所有路径.html) + * 递归:前序,方便让父节点指向子节点,涉及回溯处理根节点到叶子的所有路径 + * 迭代:一个栈模拟递归,一个栈来存放对应的遍历路径 +* [二叉树:递归中如何隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html) + * 详解[二叉树:找所有路径](https://programmercarl.com/0257.二叉树的所有路径.html)中递归如何隐藏着回溯 +* [二叉树:求左叶子之和](https://programmercarl.com/0404.左叶子之和.html) + * 递归:后序,必须三层约束条件,才能判断是否是左叶子。 + * 迭代:直接模拟后序遍历 +* [二叉树:求左下角的值](https://programmercarl.com/0513.找树左下角的值.html) + * 递归:顺序无所谓,优先左孩子搜索,同时找深度最大的叶子节点。 + * 迭代:层序遍历找最后一行最左边 +* [二叉树:求路径总和](https://programmercarl.com/0112.路径总和.html) + * 递归:顺序无所谓,递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树。 + * 迭代:栈里元素不仅要记录节点指针,还要记录从头结点到该节点的路径数值总和 + + +## 二叉树的修改与构造 + +* [翻转二叉树](https://programmercarl.com/0226.翻转二叉树.html) + * 递归:前序,交换左右孩子 + * 迭代:直接模拟前序遍历 +* [构造二叉树](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) + * 递归:前序,重点在于找分割点,分左右区间构造 + * 迭代:比较复杂,意义不大 +* [构造最大的二叉树](https://programmercarl.com/0654.最大二叉树.html) + * 递归:前序,分割点为数组最大值,分左右区间构造 + * 迭代:比较复杂,意义不大 +* [合并两个二叉树](https://programmercarl.com/0617.合并二叉树.html) + * 递归:前序,同时操作两个树的节点,注意合并的规则 + * 迭代:使用队列,类似层序遍历 + +## 求二叉搜索树的属性 + +* [二叉搜索树中的搜索](https://programmercarl.com/0700.二叉搜索树中的搜索.html) + * 递归:二叉搜索树的递归是有方向的 + * 迭代:因为有方向,所以迭代法很简单 +* [是不是二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html) + * 递归:中序,相当于变成了判断一个序列是不是递增的 + * 迭代:模拟中序,逻辑相同 +* [求二叉搜索树的最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html) + * 递归:中序,双指针操作 + * 迭代:模拟中序,逻辑相同 +* [求二叉搜索树的众数](https://programmercarl.com/0501.二叉搜索树中的众数.html) + + * 递归:中序,清空结果集的技巧,遍历一遍便可求众数集合 +* [二叉搜索树转成累加树](https://programmercarl.com/0538.把二叉搜索树转换为累加树.html) + * 递归:中序,双指针操作累加 + * 迭代:模拟中序,逻辑相同 + +## 二叉树公共祖先问题 + +* [二叉树的公共祖先问题](https://programmercarl.com/0236.二叉树的最近公共祖先.html) + * 递归:后序,回溯,找到左子树出现目标值,右子树节点目标值的节点。 + * 迭代:不适合模拟回溯 +* [二叉搜索树的公共祖先问题](https://programmercarl.com/0235.二叉搜索树的最近公共祖先.html) + * 递归:顺序无所谓,如果节点的数值在目标区间就是最近公共祖先 + * 迭代:按序遍历 + +## 二叉搜索树的修改与构造 + +* [二叉搜索树中的插入操作](https://programmercarl.com/0701.二叉搜索树中的插入操作.html) + * 递归:顺序无所谓,通过递归函数返回值添加节点 + * 迭代:按序遍历,需要记录插入父节点,这样才能做插入操作 +* [二叉搜索树中的删除操作](https://programmercarl.com/0450.删除二叉搜索树中的节点.html) + * 递归:前序,想清楚删除非叶子节点的情况 + * 迭代:有序遍历,较复杂 +* [修剪二叉搜索树](https://programmercarl.com/0669.修剪二叉搜索树.html) + * 递归:前序,通过递归函数返回值删除节点 + * 迭代:有序遍历,较复杂 +* [构造二叉搜索树](https://programmercarl.com/0108.将有序数组转换为二叉搜索树.html) + * 递归:前序,数组中间节点分割 + * 迭代:较复杂,通过三个队列来模拟 + +## 阶段总结 + +大家以上题目都做过了,也一定要看如下阶段小结。 + +**每周小结都会对大家的疑问做统一解答,并且对每周的内容进行拓展和补充,所以一定要看,将细碎知识点一网打尽!** + +* [本周小结!(二叉树系列一)](https://programmercarl.com/周总结/20200927二叉树周末总结.html) +* [本周小结!(二叉树系列二)](https://programmercarl.com/周总结/20201003二叉树周末总结.html) +* [本周小结!(二叉树系列三)](https://programmercarl.com/周总结/20201010二叉树周末总结.html) +* [本周小结!(二叉树系列四)](https://programmercarl.com/周总结/20201017二叉树周末总结.html) + +## 最后总结 + +**在二叉树题目选择什么遍历顺序是不少同学头疼的事情,我们做了这么多二叉树的题目了,Carl给大家大体分分类**。 + +* 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。 + +* 求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。 + +* 求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。 + +注意在普通二叉树的属性中,我用的是一般为后序,例如单纯求深度就用前序,[二叉树:找所有路径](https://programmercarl.com/0257.二叉树的所有路径.html)也用了前序,这是为了方便让父节点指向子节点。 + +所以求普通二叉树的属性还是要具体问题具体分析。 + +二叉树专题汇聚为一张图: + +![](https://file1.kamacoder.com/i/algo/20211030125421.png) + +这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[青](https://wx.zsxq.com/dweb2/index/footprint/185251215558842),所画,总结的非常好,分享给大家。 + +**最后,二叉树系列就这么完美结束了,估计这应该是最长的系列了,感谢大家33天的坚持与陪伴,接下来我们又要开始新的系列了「回溯算法」!** + + + + + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\220\206\350\256\272\345\237\272\347\241\200.md" new file mode 100755 index 0000000000..a68f93a901 --- /dev/null +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -0,0 +1,314 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 二叉树理论基础篇 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[关于二叉树,你该了解这些!](https://www.bilibili.com/video/BV1Hy4y1t7ij),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 题目分类 + + +题目分类大纲如下: + +二叉树大纲 + +说到二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容再啰嗦一遍,所以以下我讲的都是一些比较重点的内容。 + +相信只要耐心看完,都会有所收获。 + +## 二叉树的种类 + +在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。 + +### 满二叉树 + +满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。 + +如图所示: + + + +这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。 + + +### 完全二叉树 + +什么是完全二叉树? + +完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。 + +**大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。** + +我来举一个典型的例子如题: + + + +相信不少同学最后一个二叉树是不是完全二叉树都中招了。 + +**之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。** + +### 二叉搜索树 + +前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,**二叉搜索树是一个有序树**。 + + +* 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; +* 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; +* 它的左、右子树也分别为二叉排序树 + +下面这两棵树都是搜索树 + + + + +### 平衡二叉搜索树 + +平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 + +如图: + + + +最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。 + +**C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树**,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_set底层实现是哈希表。 + +**所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!** + + +## 二叉树的存储方式 + +**二叉树可以链式存储,也可以顺序存储。** + +那么链式存储方式就用指针, 顺序存储的方式就是用数组。 + +顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。 + +链式存储如图: + + + +链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢? + +其实就是用数组来存储二叉树,顺序存储的方式如图: + + + +用数组来存储二叉树如何遍历的呢? + +**如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。** + +但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。 + +**所以大家要了解,用数组依然可以表示二叉树。** + +## 二叉树的遍历方式 + +关于二叉树的遍历方式,要知道二叉树遍历的基本方式都有哪些。 + +一些同学用做了很多二叉树的题目了,可能知道前中后序遍历,可能知道层序遍历,但是却没有框架。 + +我这里把二叉树的几种遍历方式列出来,大家就可以一一串起来了。 + +二叉树主要有两种遍历方式: + +1. 深度优先遍历:先往深走,遇到叶子节点再往回走。 +2. 广度优先遍历:一层一层的去遍历。 + +**这两种遍历是图论中最基本的两种遍历方式**,后面在介绍图论的时候 还会介绍到。 + +那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式: + +* 深度优先遍历 + * 前序遍历(递归法,迭代法) + * 中序遍历(递归法,迭代法) + * 后序遍历(递归法,迭代法) +* 广度优先遍历 + * 层次遍历(迭代法) + + +在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。 + +**这里前中后,其实指的就是中间节点的遍历顺序**,只要大家记住 前中后序指的就是中间节点的位置就可以了。 + +看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式 + +* 前序遍历:中左右 +* 中序遍历:左中右 +* 后序遍历:左右中 + +大家可以对着如下图,看看自己理解的前后中序有没有问题。 + + + +最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。 + +**之前我们讲栈与队列的时候,就说过栈其实就是递归的一种实现结构**,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。 + +而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。 + +**这里其实我们又了解了栈与队列的一个应用场景了。** + +具体的实现我们后面都会讲的,这里大家先要清楚这些理论基础。 + +## 二叉树的定义 + +刚刚我们说过了二叉树有两种存储方式顺序存储,和链式存储,顺序存储就是用数组来存,这个定义没啥可说的,我们来看看链式存储的二叉树节点的定义方式。 + + +C++代码如下: + +```cpp +struct TreeNode { + int val; + TreeNode *left; + TreeNode *right; + TreeNode(int x) : val(x), left(NULL), right(NULL) {} +}; +``` + +大家会发现二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子。 + +这里要提醒大家要注意二叉树节点定义的书写方式。 + +**在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。** + +因为我们在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼! + +## 总结 + +二叉树是一种基础数据结构,在算法面试中都是常客,也是众多数据结构的基石。 + +本篇我们介绍了二叉树的种类、存储方式、遍历方式以及定义,比较全面的介绍了二叉树各个方面的重点,帮助大家扫一遍基础。 + +**说到二叉树,就不得不说递归,很多同学对递归都是又熟悉又陌生,递归的代码一般很简短,但每次都是一看就会,一写就废。** + +## 其他语言版本 + +### Java: + +```java +public class TreeNode { + int val; + TreeNode left; + TreeNode right; + + TreeNode() {} + TreeNode(int val) { this.val = val; } + TreeNode(int val, TreeNode left, TreeNode right) { + this.val = val; + this.left = left; + this.right = right; + } +} +``` + +### Python: + +```python +class TreeNode: + def __init__(self, val, left = None, right = None): + self.val = val + self.left = left + self.right = right +``` + +### Go: + +```go +type TreeNode struct { + Val int + Left *TreeNode + Right *TreeNode +} +``` + +### JavaScript: + +```javascript +function TreeNode(val, left, right) { + this.val = (val===undefined ? 0 : val) + this.left = (left===undefined ? null : left) + this.right = (right===undefined ? null : right) +} +``` + +### TypeScript: + +```typescript +class TreeNode { + public val: number; + public left: TreeNode | null; + public right: TreeNode | null; + constructor(val?: number, left?: TreeNode, right?: TreeNode) { + this.val = val === undefined ? 0 : val; + this.left = left === undefined ? null : left; + this.right = right === undefined ? null : right; + } +} +``` + +### Swift: + +```Swift +class TreeNode { + var value: T + var left: TreeNode? + var right: TreeNode? + init(_ value: T, + left: TreeNode? = nil, + right: TreeNode? = nil) { + self.value = value + self.left = left + self.right = right + } +} +``` + +### Scala: + +```scala +class TreeNode(_value: Int = 0, _left: TreeNode = null, _right: TreeNode = null) { + var value: Int = _value + var left: TreeNode = _left + var right: TreeNode = _right +} +``` + +### Rust: + +```rust +#[derive(Debug, PartialEq, Eq)] +pub struct TreeNode { + pub val: T, + pub left: Option>>>, + pub right: Option>>>, +} + +impl TreeNode { + #[inline] + pub fn new(val: T) -> Self { + TreeNode { + val, + left: None, + right: None, + } + } +} + +``` +```c# +public class TreeNode +{ + public int val; + public TreeNode left; + public TreeNode right; + public TreeNode(int x) { val = x; } +} +``` + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\220\206\350\256\272\345\237\272\347\241\200.md" deleted file mode 100644 index 7309c6933f..0000000000 --- "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ /dev/null @@ -1,188 +0,0 @@ -

- -

-

- - - - - - -

- -# 二叉树理论基础 - -我们要开启新的征程了,大家跟上! - -说道二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容再啰嗦一遍,所以一下我讲的都是一些比较重点的内容。 - -相信只要耐心看完,都会有所收获。 - -# 二叉树的种类 - -在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。 - -## 满二叉树 - -满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。 - -如图所示: - - - -这棵二叉树为满二叉树,也可以说深度为 k,有 $(2^k)-1$ 个节点的二叉树。 - - -## 完全二叉树 - -什么是完全二叉树? - -完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1 ~ $2^{(h-1)}$  个节点。 - -**大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。** - -我来举一个典型的例子如题: - - - -相信不少同学最后一个二叉树是不是完全二叉树都中招了。 - -**之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。** - -## 二叉搜索树 - -前面介绍的书,都没有数值的,而二叉搜索树是有数值的了,**二叉搜索树是一个有序树**。 - - -* 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; -* 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; -* 它的左、右子树也分别为二叉排序树 - -下面这两棵树都是搜索树 - - - -## 平衡二叉搜索树 - -平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 - -如图: - - - -最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。 - -**C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树**,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。 - -**所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!** - - -# 二叉树的存储方式 - -**二叉树可以链式存储,也可以顺序存储。** - -那么链式存储方式就用指针, 顺序存储的方式就是用数组。 - -顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。 - -链式存储如图: - - - -链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢? - -其实就是用数组来存储二叉树,顺序存储的方式如图: - - - -用数组来存储二叉树如何遍历的呢? - -**如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。** - -但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。 - -**所以大家要了解,用数组依然可以表示二叉树。** - -# 二叉树的遍历方式 - -关于二叉树的遍历方式,要知道二叉树遍历的基本方式都有哪些。 - -一些同学用做了很多二叉树的题目了,可能知道前序、中序、后序遍历,可能知道层序遍历,但是却没有框架。 - -我这里把二叉树的几种遍历方式列出来,大家就可以一一串起来了。 - -二叉树主要有两种遍历方式: -1. 深度优先遍历:先往深走,遇到叶子节点再往回走。 -2. 广度优先遍历:一层一层的去遍历。 - -**这两种遍历是图论中最基本的两种遍历方式**,后面在介绍图论的时候 还会介绍到。 - -那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式: - -* 深度优先遍历 - * 前序遍历(递归法,迭代法) - * 中序遍历(递归法,迭代法) - * 后序遍历(递归法,迭代法) -* 广度优先遍历 - * 层次遍历(迭代法) - - -在深度优先遍历中:有三个顺序,前序、中序、后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。 - -**这里前、中、后,其实指的就是中间节点的遍历顺序**,只要大家记住 前序、中序、后序指的就是中间节点的位置就可以了。 - -看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式 - -* 前序遍历:中左右 -* 中序遍历:左中右 -* 后序遍历:左右中 - -大家可以对着如下图,看看自己理解的前后中序有没有问题。 - - - -最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前序、中序、后序遍历,使用递归是比较方便的。 - -**之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构**,也就说前序、中序、后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。 - -而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。 - -**这里其实我们又了解了栈与队列的一个应用场景了。** - -具体的实现我们后面都会讲的,这里大家先要清楚这些理论基础。 - -# 二叉树的定义 - -刚刚我们说过了二叉树有两种存储方式顺序存储,和链式存储,顺序存储就是用数组来存,这个定义没啥可说的,我们来看看链式存储的二叉树节点的定义方式。 - - -C++代码如下: - -``` -struct TreeNode { - int val; - TreeNode *left; - TreeNode *right; - TreeNode(int x) : val(x), left(NULL), right(NULL) {} -}; -``` - -大家会发现二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子. - -这里要提醒大家要注意二叉树节点定义的书写方式。 - -**在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。** - -因为我们在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼! - -# 总结 - -二叉树是一种基础数据结构,在算法面试中都是常客,也是众多数据结构的基石。 - -本篇我们介绍了二叉树的种类、存储方式、遍历方式以及定义,比较全面的介绍了二叉树各个方面的重点,帮助大家扫一遍基础。 - -**说道二叉树,就不得不说递归,很多同学对递归都是又熟悉又陌生,递归的代码一般很简短,但每次都是一看就会,一写就废。** - -**那么请跟住Carl的节奏,不仅彻底掌握二叉树的递归遍历,还有迭代遍历!** - - diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\273\237\344\270\200\350\277\255\344\273\243\346\263\225.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\273\237\344\270\200\350\277\255\344\273\243\346\263\225.md" new file mode 100755 index 0000000000..803b25ae80 --- /dev/null +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\347\273\237\344\270\200\350\277\255\344\273\243\346\263\225.md" @@ -0,0 +1,971 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +> 统一写法是一种什么感觉 + +# 二叉树的统一迭代法 + +## 思路 + +此时我们在[二叉树:一入递归深似海,从此offer是路人](https://programmercarl.com/二叉树的递归遍历.html)中用递归的方式,实现了二叉树前中后序的遍历。 + +在[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)中用栈实现了二叉树前后中序的迭代遍历(非递归)。 + +之后我们发现**迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。** + +实践过的同学,也会发现使用迭代法实现先中后序遍历,很难写出统一的代码,不像是递归法,实现了其中的一种遍历方式,其他两种只要稍稍改一下节点顺序就可以了。 + +其实**针对三种遍历方式,使用迭代法是可以写出统一风格的代码!** + +**重头戏来了,接下来介绍一下统一写法。** + +我们以中序遍历为例,在[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)中提到说使用栈的话,**无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况**。 + +**那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。** + +如何标记呢? + +* 方法一:**就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。** 这种方法可以叫做`空指针标记法`。 + +* 方法二:**加一个 `boolean` 值跟随每个节点,`false` (默认值) 表示需要为该节点和它的左右儿子安排在栈中的位次,`true` 表示该节点的位次之前已经安排过了,可以收割节点了。** +这种方法可以叫做`boolean 标记法`,样例代码见下文`C++ 和 Python 的 boolean 标记法`。 这种方法更容易理解,在面试中更容易写出来。 + +### 迭代法中序遍历 + +> 中序遍历(空指针标记法)代码如下:(详细注释) + +```CPP +class Solution { +public: + vector inorderTraversal(TreeNode* root) { + vector result; + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中 + if (node->right) st.push(node->right); // 添加右节点(空节点不入栈) + + st.push(node); // 添加中节点 + st.push(NULL); // 中节点访问过,但是还没有处理,加入空节点做为标记。 + + if (node->left) st.push(node->left); // 添加左节点(空节点不入栈) + } else { // 只有遇到空节点的时候,才将下一个节点放进结果集 + st.pop(); // 将空节点弹出 + node = st.top(); // 重新取出栈中元素 + st.pop(); + result.push_back(node->val); // 加入到结果集 + } + } + return result; + } +}; +``` + +看代码有点抽象我们来看一下动画(中序遍历): + +![中序遍历迭代(统一写法)](https://file1.kamacoder.com/i/algo/%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%E8%BF%AD%E4%BB%A3%EF%BC%88%E7%BB%9F%E4%B8%80%E5%86%99%E6%B3%95%EF%BC%89.gif) + +动画中,result数组就是最终结果集。 + +可以看出我们将访问的节点直接加入到栈中,但如果是处理的节点则后面放入一个空节点, 这样只有空节点弹出的时候,才将下一个节点放进结果集。 + +> 中序遍历(boolean 标记法): +```c++ +class Solution { +public: + vector inorderTraversal(TreeNode* root) { + vector result; + stack> st; + if (root != nullptr) + st.push(make_pair(root, false)); // 多加一个参数,false 为默认值,含义见下文注释 + + while (!st.empty()) { + auto node = st.top().first; + auto visited = st.top().second; //多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + st.pop(); + + if (visited) { // visited 为 True,表示该节点和两个儿子位次之前已经安排过了,现在可以收割节点了 + result.push_back(node->val); + continue; + } + + // visited 当前为 false, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次”。 + + // 中序遍历是'左中右',右儿子最先入栈,最后出栈。 + if (node->right) + st.push(make_pair(node->right, false)); + + // 把自己加回到栈中,位置居中。 + // 同时,设置 visited 为 true,表示下次再访问本节点时,允许收割。 + st.push(make_pair(node, true)); + + if (node->left) + st.push(make_pair(node->left, false)); // 左儿子最后入栈,最先出栈 + } + + return result; + } +}; +``` + +此时我们再来看前序遍历代码。 + +### 迭代法前序遍历 + +迭代法前序遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) + +```CPP +class Solution { +public: + vector preorderTraversal(TreeNode* root) { + vector result; + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 右 + if (node->left) st.push(node->left); // 左 + st.push(node); // 中 + st.push(NULL); + } else { + st.pop(); + node = st.top(); + st.pop(); + result.push_back(node->val); + } + } + return result; + } +}; +``` + +### 迭代法后序遍历 + +> 后续遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) + +```CPP +class Solution { +public: + vector postorderTraversal(TreeNode* root) { + vector result; + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + st.push(node); // 中 + st.push(NULL); + + if (node->right) st.push(node->right); // 右 + if (node->left) st.push(node->left); // 左 + + } else { + st.pop(); + node = st.top(); + st.pop(); + result.push_back(node->val); + } + } + return result; + } +}; +``` + +> 迭代法后序遍历(boolean 标记法): +```c++ +class Solution { +public: + vector postorderTraversal(TreeNode* root) { + vector result; + stack> st; + if (root != nullptr) + st.push(make_pair(root, false)); // 多加一个参数,false 为默认值,含义见下文 + + while (!st.empty()) { + auto node = st.top().first; + auto visited = st.top().second; //多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + st.pop(); + + if (visited) { // visited 为 True,表示该节点和两个儿子位次之前已经安排过了,现在可以收割节点了 + result.push_back(node->val); + continue; + } + + // visited 当前为 false, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次”。 + // 后序遍历是'左右中',节点自己最先入栈,最后出栈。 + // 同时,设置 visited 为 true,表示下次再访问本节点时,允许收割。 + st.push(make_pair(node, true)); + + if (node->right) + st.push(make_pair(node->right, false)); // 右儿子位置居中 + + if (node->left) + st.push(make_pair(node->left, false)); // 左儿子最后入栈,最先出栈 + } + + return result; + } +}; +``` +## 总结 + +此时我们写出了统一风格的迭代法,不用在纠结于前序写出来了,中序写不出来的情况了。 + +但是统一风格的迭代法并不好理解,而且想在面试直接写出来还有难度的。 + +所以大家根据自己的个人喜好,对于二叉树的前中后序遍历,选择一种自己容易理解的递归和迭代法。 + +## 其他语言版本 + +### Java: +迭代法前序遍历代码如下: + +```java +class Solution { + public List preorderTraversal(TreeNode root) { + List result = new LinkedList<>(); + Stack st = new Stack<>(); + if (root != null) st.push(root); + while (!st.empty()) { + TreeNode node = st.peek(); + if (node != null) { + st.pop(); // 将该节点弹出,避免重复操作,下面再将右左中节点添加到栈中(前序遍历-中左右,入栈顺序右左中) + if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈) + if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈) + st.push(node); // 添加中节点 + st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。 + + } else { // 只有遇到空节点的时候,才将下一个节点放进结果集 + st.pop(); // 将空节点弹出 + node = st.peek(); // 重新取出栈中元素 + st.pop(); + result.add(node.val); // 加入到结果集 + } + } + return result; + } +} +``` + +迭代法中序遍历代码如下: +```java +class Solution { +public List inorderTraversal(TreeNode root) { + List result = new LinkedList<>(); + Stack st = new Stack<>(); + if (root != null) st.push(root); + while (!st.empty()) { + TreeNode node = st.peek(); + if (node != null) { + st.pop(); // 将该节点弹出,避免重复操作,下面再将右中左节点添加到栈中(中序遍历-左中右,入栈顺序右中左) + if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈) + st.push(node); // 添加中节点 + st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。 + if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈) + } else { // 只有遇到空节点的时候,才将下一个节点放进结果集 + st.pop(); // 将空节点弹出 + node = st.peek(); // 重新取出栈中元素 + st.pop(); + result.add(node.val); // 加入到结果集 + } + } + return result; +} +} +``` + +迭代法后序遍历代码如下: +```java +class Solution { + public List postorderTraversal(TreeNode root) { + List result = new LinkedList<>(); + Stack st = new Stack<>(); + if (root != null) st.push(root); + while (!st.empty()) { + TreeNode node = st.peek(); + if (node != null) { + st.pop(); // 将该节点弹出,避免重复操作,下面再将中右左节点添加到栈中(后序遍历-左右中,入栈顺序中右左) + st.push(node); // 添加中节点 + st.push(null); // 中节点访问过,但是还没有处理,加入空节点做为标记。 + if (node.right!=null) st.push(node.right); // 添加右节点(空节点不入栈) + if (node.left!=null) st.push(node.left); // 添加左节点(空节点不入栈) + + } else { // 只有遇到空节点的时候,才将下一个节点放进结果集 + st.pop(); // 将空节点弹出 + node = st.peek(); // 重新取出栈中元素 + st.pop(); + result.add(node.val); // 加入到结果集 + } + } + return result; + } +} +``` + +### Python: + +> 迭代法前序遍历(空指针标记法): +```python +class Solution: + def preorderTraversal(self, root: TreeNode) -> List[int]: + result = [] + st= [] + if root: + st.append(root) + while st: + node = st.pop() + if node != None: + if node.right: #右 + st.append(node.right) + if node.left: #左 + st.append(node.left) + st.append(node) #中 + st.append(None) + else: + node = st.pop() + result.append(node.val) + return result +``` + +> 迭代法中序遍历(空指针标记法): +```python +class Solution: + def inorderTraversal(self, root: TreeNode) -> List[int]: + result = [] + st = [] + if root: + st.append(root) + while st: + node = st.pop() + if node != None: + if node.right: #添加右节点(空节点不入栈) + st.append(node.right) + + st.append(node) #添加中节点 + st.append(None) #中节点访问过,但是还没有处理,加入空节点做为标记。 + + if node.left: #添加左节点(空节点不入栈) + st.append(node.left) + else: #只有遇到空节点的时候,才将下一个节点放进结果集 + node = st.pop() #重新取出栈中元素 + result.append(node.val) #加入到结果集 + return result +``` + +> 迭代法后序遍历(空指针标记法): +```python +class Solution: + def postorderTraversal(self, root: TreeNode) -> List[int]: + result = [] + st = [] + if root: + st.append(root) + while st: + node = st.pop() + if node != None: + st.append(node) #中 + st.append(None) + + if node.right: #右 + st.append(node.right) + if node.left: #左 + st.append(node.left) + else: + node = st.pop() + result.append(node.val) + return result +``` + +> 中序遍历,统一迭代(boolean 标记法): +```python +class Solution: + def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]: + values = [] + stack = [(root, False)] if root else [] # 多加一个参数,False 为默认值,含义见下文 + + while stack: + node, visited = stack.pop() # 多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + + if visited: # visited 为 True,表示该节点和两个儿子的位次之前已经安排过了,现在可以收割节点了 + values.append(node.val) + continue + + # visited 当前为 False, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次”。 + # 中序遍历是'左中右',右儿子最先入栈,最后出栈。 + if node.right: + stack.append((node.right, False)) + + stack.append((node, True)) # 把自己加回到栈中,位置居中。同时,设置 visited 为 True,表示下次再访问本节点时,允许收割 + + if node.left: + stack.append((node.left, False)) # 左儿子最后入栈,最先出栈 + + return values +``` + +> 后序遍历,统一迭代(boolean 标记法): +```python +class Solution: + def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]: + values = [] + stack = [(root, False)] if root else [] # 多加一个参数,False 为默认值,含义见下文 + + while stack: + node, visited = stack.pop() # 多加一个 visited 参数,使“迭代统一写法”成为一件简单的事 + + if visited: # visited 为 True,表示该节点和两个儿子位次之前已经安排过了,现在可以收割节点了 + values.append(node.val) + continue + + # visited 当前为 False, 表示初次访问本节点,此次访问的目的是“把自己和两个儿子在栈中安排好位次” + # 后序遍历是'左右中',节点自己最先入栈,最后出栈。 + # 同时,设置 visited 为 True,表示下次再访问本节点时,允许收割。 + stack.append((node, True)) + + if node.right: + stack.append((node.right, False)) # 右儿子位置居中 + + if node.left: + stack.append((node.left, False)) # 左儿子最后入栈,最先出栈 + + return values +``` + +### Go: + +> 前序遍历统一迭代法 + +```GO + /** + type Element struct { + // 元素保管的值 + Value interface{} + // 内含隐藏或非导出字段 +} + +func (l *List) Back() *Element +前序遍历:中左右 +压栈顺序:右左中 + **/ +func preorderTraversal(root *TreeNode) []int { + if root == nil { + return nil + } + var stack = list.New()//栈 + res:=[]int{}//结果集 + stack.PushBack(root) + var node *TreeNode + for stack.Len()>0{ + e := stack.Back() + stack.Remove(e)//弹出元素 + if e.Value==nil{// 如果为空,则表明是需要处理中间节点 + e=stack.Back()//弹出元素(即中间节点) + stack.Remove(e)//删除中间节点 + node=e.Value.(*TreeNode) + res=append(res,node.Val)//将中间节点加入到结果集中 + continue//继续弹出栈中下一个节点 + } + node = e.Value.(*TreeNode) + //压栈顺序:右左中 + if node.Right!=nil{ + stack.PushBack(node.Right) + } + if node.Left!=nil{ + stack.PushBack(node.Left) + } + stack.PushBack(node)//中间节点压栈后再压入nil作为中间节点的标志符 + stack.PushBack(nil) + } + return res + +} +``` + +> 中序遍历统一迭代法 + +```go +/** + * Definition for a binary tree node. + * type TreeNode struct { + * Val int + * Left *TreeNode + * Right *TreeNode + * } + */ + //中序遍历:左中右 + //压栈顺序:右中左 +func inorderTraversal(root *TreeNode) []int { + if root==nil{ + return nil + } + stack:=list.New()//栈 + res:=[]int{}//结果集 + stack.PushBack(root) + var node *TreeNode + for stack.Len()>0{ + e := stack.Back() + stack.Remove(e) + if e.Value==nil{// 如果为空,则表明是需要处理中间节点 + e=stack.Back()//弹出元素(即中间节点) + stack.Remove(e)//删除中间节点 + node=e.Value.(*TreeNode) + res=append(res,node.Val)//将中间节点加入到结果集中 + continue//继续弹出栈中下一个节点 + } + node = e.Value.(*TreeNode) + //压栈顺序:右中左 + if node.Right!=nil{ + stack.PushBack(node.Right) + } + stack.PushBack(node)//中间节点压栈后再压入nil作为中间节点的标志符 + stack.PushBack(nil) + if node.Left!=nil{ + stack.PushBack(node.Left) + } + } + return res +} +``` + +> 后序遍历统一迭代法 + +```go +//后续遍历:左右中 +//压栈顺序:中右左 +func postorderTraversal(root *TreeNode) []int { + if root == nil { + return nil + } + var stack = list.New()//栈 + res:=[]int{}//结果集 + stack.PushBack(root) + var node *TreeNode + for stack.Len()>0{ + e := stack.Back() + stack.Remove(e) + if e.Value==nil{// 如果为空,则表明是需要处理中间节点 + e=stack.Back()//弹出元素(即中间节点) + stack.Remove(e)//删除中间节点 + node=e.Value.(*TreeNode) + res=append(res,node.Val)//将中间节点加入到结果集中 + continue//继续弹出栈中下一个节点 + } + node = e.Value.(*TreeNode) + //压栈顺序:中右左 + stack.PushBack(node)//中间节点压栈后再压入nil作为中间节点的标志符 + stack.PushBack(nil) + if node.Right!=nil{ + stack.PushBack(node.Right) + } + if node.Left!=nil{ + stack.PushBack(node.Left) + } + } + return res +} +``` + +### JavaScript: + +> 前序遍历统一迭代法 + +```js + +// 前序遍历:中左右 +// 压栈顺序:右左中 + +var preorderTraversal = function(root, res = []) { + const stack = []; + if (root) stack.push(root); + while(stack.length) { + const node = stack.pop(); + if(!node) { + res.push(stack.pop().val); + continue; + } + if (node.right) stack.push(node.right); // 右 + if (node.left) stack.push(node.left); // 左 + stack.push(node); // 中 + stack.push(null); + }; + return res; +}; + +``` + +> 中序遍历统一迭代法 + +```js + +// 中序遍历:左中右 +// 压栈顺序:右中左 + +var inorderTraversal = function(root, res = []) { + const stack = []; + if (root) stack.push(root); + while(stack.length) { + const node = stack.pop(); + if(!node) { + res.push(stack.pop().val); + continue; + } + if (node.right) stack.push(node.right); // 右 + stack.push(node); // 中 + stack.push(null); + if (node.left) stack.push(node.left); // 左 + }; + return res; +}; + +``` + +> 后序遍历统一迭代法 + +```js + +// 后续遍历:左右中 +// 压栈顺序:中右左 + +var postorderTraversal = function(root, res = []) { + const stack = []; + if (root) stack.push(root); + while(stack.length) { + const node = stack.pop(); + if(!node) { + res.push(stack.pop().val); + continue; + } + stack.push(node); // 中 + stack.push(null); + if (node.right) stack.push(node.right); // 右 + if (node.left) stack.push(node.left); // 左 + }; + return res; +}; + +``` + +### TypeScript: + +```typescript +// 前序遍历(迭代法) +function preorderTraversal(root: TreeNode | null): number[] { + let helperStack: (TreeNode | null)[] = []; + let res: number[] = []; + let curNode: TreeNode | null; + if (root === null) return res; + helperStack.push(root); + while (helperStack.length > 0) { + curNode = helperStack.pop()!; + if (curNode !== null) { + if (curNode.right !== null) helperStack.push(curNode.right); + if (curNode.left !== null) helperStack.push(curNode.left); + helperStack.push(curNode); + helperStack.push(null); + } else { + curNode = helperStack.pop()!; + res.push(curNode.val); + } + } + return res; +}; + +// 中序遍历(迭代法) +function inorderTraversal(root: TreeNode | null): number[] { + let helperStack: (TreeNode | null)[] = []; + let res: number[] = []; + let curNode: TreeNode | null; + if (root === null) return res; + helperStack.push(root); + while (helperStack.length > 0) { + curNode = helperStack.pop()!; + if (curNode !== null) { + if (curNode.right !== null) helperStack.push(curNode.right); + helperStack.push(curNode); + helperStack.push(null); + if (curNode.left !== null) helperStack.push(curNode.left); + } else { + curNode = helperStack.pop()!; + res.push(curNode.val); + } + } + return res; +}; + +// 后序遍历(迭代法) +function postorderTraversal(root: TreeNode | null): number[] { + let helperStack: (TreeNode | null)[] = []; + let res: number[] = []; + let curNode: TreeNode | null; + if (root === null) return res; + helperStack.push(root); + while (helperStack.length > 0) { + curNode = helperStack.pop()!; + if (curNode !== null) { + helperStack.push(curNode); + helperStack.push(null); + if (curNode.right !== null) helperStack.push(curNode.right); + if (curNode.left !== null) helperStack.push(curNode.left); + } else { + curNode = helperStack.pop()!; + res.push(curNode.val); + } + } + return res; +}; +``` +### Scala: + +```scala +// 前序遍历 +object Solution { + import scala.collection.mutable + def preorderTraversal(root: TreeNode): List[Int] = { + val res = mutable.ListBuffer[Int]() + val stack = mutable.Stack[TreeNode]() + if (root != null) stack.push(root) + while (!stack.isEmpty) { + var curNode = stack.top + if (curNode != null) { + stack.pop() + if (curNode.right != null) stack.push(curNode.right) + if (curNode.left != null) stack.push(curNode.left) + stack.push(curNode) + stack.push(null) + } else { + stack.pop() + res.append(stack.pop().value) + } + } + res.toList + } +} + +// 中序遍历 +object Solution { + import scala.collection.mutable + def inorderTraversal(root: TreeNode): List[Int] = { + val res = mutable.ListBuffer[Int]() + val stack = mutable.Stack[TreeNode]() + if (root != null) stack.push(root) + while (!stack.isEmpty) { + var curNode = stack.top + if (curNode != null) { + stack.pop() + if (curNode.right != null) stack.push(curNode.right) + stack.push(curNode) + stack.push(null) + if (curNode.left != null) stack.push(curNode.left) + } else { + // 等于空的时候好办,弹出这个元素 + stack.pop() + res.append(stack.pop().value) + } + } + res.toList + } +} + +// 后序遍历 +object Solution { + import scala.collection.mutable + def postorderTraversal(root: TreeNode): List[Int] = { + val res = mutable.ListBuffer[Int]() + val stack = mutable.Stack[TreeNode]() + if (root != null) stack.push(root) + while (!stack.isEmpty) { + var curNode = stack.top + if (curNode != null) { + stack.pop() + stack.push(curNode) + stack.push(null) + if (curNode.right != null) stack.push(curNode.right) + if (curNode.left != null) stack.push(curNode.left) + } else { + stack.pop() + res.append(stack.pop().value) + } + } + res.toList + } +} +``` + +### Rust: + +```rust +impl Solution{ + // 前序 + pub fn preorder_traversal(root: Option>>) -> Vec { + let mut res = vec![]; + let mut stack = vec![]; + if root.is_some(){ + stack.push(root); + } + while !stack.is_empty(){ + if let Some(node) = stack.pop().unwrap(){ + if node.borrow().right.is_some(){ + stack.push(node.borrow().right.clone()); + } + if node.borrow().left.is_some(){ + stack.push(node.borrow().left.clone()); + } + stack.push(Some(node)); + stack.push(None); + }else{ + res.push(stack.pop().unwrap().unwrap().borrow().val); + } + } + res + } + // 中序 + pub fn inorder_traversal(root: Option>>) -> Vec { + let mut res = vec![]; + let mut stack = vec![]; + if root.is_some() { + stack.push(root); + } + while !stack.is_empty() { + if let Some(node) = stack.pop().unwrap() { + if node.borrow().right.is_some() { + stack.push(node.borrow().right.clone()); + } + stack.push(Some(node.clone())); + stack.push(None); + if node.borrow().left.is_some() { + stack.push(node.borrow().left.clone()); + } + } else { + res.push(stack.pop().unwrap().unwrap().borrow().val); + } + } + res + } + // 后序 + pub fn postorder_traversal(root: Option>>) -> Vec { + let mut res = vec![]; + let mut stack = vec![]; + if root.is_some() { + stack.push(root); + } + while !stack.is_empty() { + if let Some(node) = stack.pop().unwrap() { + stack.push(Some(node.clone())); + stack.push(None); + if node.borrow().right.is_some() { + stack.push(node.borrow().right.clone()); + } + if node.borrow().left.is_some() { + stack.push(node.borrow().left.clone()); + } + } else { + res.push(stack.pop().unwrap().unwrap().borrow().val); + } + } + res + } +} +``` +### C# +```csharp +// 前序遍历 +public IList PreorderTraversal(TreeNode root) +{ + var res = new List(); + var st = new Stack(); + if (root == null) return res; + st.Push(root); + while (st.Count != 0) + { + var node = st.Peek(); + if (node == null) + { + st.Pop(); + node = st.Peek(); + st.Pop(); + res.Add(node.val); + } + else + { + st.Pop(); + if (node.right != null) st.Push(node.right); + if (node.left != null) st.Push(node.left); + st.Push(node); + st.Push(null); + } + } + return res; +} +``` +```csharp +// 中序遍历 +public IList InorderTraversal(TreeNode root) +{ + var res = new List(); + var st = new Stack(); + if (root == null) return res; + st.Push(root); + while (st.Count != 0) + { + var node = st.Peek(); + if (node == null) + { + st.Pop(); + node = st.Peek(); + st.Pop(); + res.Add(node.val); + } + else + { + st.Pop(); + if (node.right != null) st.Push(node.right); + st.Push(node); + st.Push(null); + if (node.left != null) st.Push(node.left); + } + } + return res; +} +``` + +```csharp +// 后序遍历 +public IList PostorderTraversal(TreeNode root) +{ + var res = new List(); + var st = new Stack(); + if (root == null) return res; + st.Push(root); + while (st.Count != 0) + { + var node = st.Peek(); + if (node == null) + { + st.Pop(); + node = st.Peek(); + st.Pop(); + res.Add(node.val); + } + else + { + st.Pop(); + if (node.left != null) st.Push(node.left); + if (node.right != null) st.Push(node.right); + st.Push(node); + st.Push(null); + } + } + res.Reverse(0, res.Count); + return res; +} +``` + + + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\350\277\255\344\273\243\351\201\215\345\216\206.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\350\277\255\344\273\243\351\201\215\345\216\206.md" new file mode 100755 index 0000000000..efa07d97d9 --- /dev/null +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\350\277\255\344\273\243\351\201\215\345\216\206.md" @@ -0,0 +1,797 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +> 听说还可以用非递归的方式 + +# 二叉树的迭代遍历 + +## 算法公开课 + +[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html): + +* **[写出二叉树的非递归遍历很难么?(前序和后序)](https://www.bilibili.com/video/BV15f4y1W7i2)** +* **[写出二叉树的非递归遍历很难么?(中序))](https://www.bilibili.com/video/BV1Zf4y1a77g)** +**相信结合视频在看本篇题解,更有助于大家对本题的理解。** + +看完本篇大家可以使用迭代法,再重新解决如下三道leetcode上的题目: + +* [144.二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) +* [94.二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) +* [145.二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) + +## 思路 + +为什么可以用迭代法(非递归的方式)来实现二叉树的前后中序遍历呢? + +我们在[栈与队列:匹配问题都是栈的强项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)中提到了,**递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中**,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。 + +此时大家应该知道我们用栈也可以是实现二叉树的前后中序遍历了。 + +### 前序遍历(迭代法) + +我们先看一下前序遍历。 + +前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。 + +为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。 + +动画如下: + +![二叉树前序遍历(迭代法)](https://file1.kamacoder.com/i/algo/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%89%8D%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif) + +不难写出如下代码: (**注意代码中空节点不入栈**) + +```CPP +class Solution { +public: + vector preorderTraversal(TreeNode* root) { + stack st; + vector result; + if (root == NULL) return result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); // 中 + st.pop(); + result.push_back(node->val); + if (node->right) st.push(node->right); // 右(空节点不入栈) + if (node->left) st.push(node->left); // 左(空节点不入栈) + } + return result; + } +}; +``` + +此时会发现貌似使用迭代法写出前序遍历并不难,确实不难。 + +**此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?** + +其实还真不行! + +但接下来,**再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。** + +### 中序遍历(迭代法) + +为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作: + +1. **处理:将元素放进result数组中** +2. **访问:遍历节点** + +分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,**因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。** + +那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了**处理顺序和访问顺序是不一致的。** + +那么**在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。** + +动画如下: + +![二叉树中序遍历(迭代法)](https://file1.kamacoder.com/i/algo/%E4%BA%8C%E5%8F%89%E6%A0%91%E4%B8%AD%E5%BA%8F%E9%81%8D%E5%8E%86%EF%BC%88%E8%BF%AD%E4%BB%A3%E6%B3%95%EF%BC%89.gif) + +**中序遍历,可以写出如下代码:** + +```CPP +class Solution { +public: + vector inorderTraversal(TreeNode* root) { + vector result; + stack st; + TreeNode* cur = root; + while (cur != NULL || !st.empty()) { + if (cur != NULL) { // 指针来访问节点,访问到最底层 + st.push(cur); // 将访问的节点放进栈 + cur = cur->left; // 左 + } else { + cur = st.top(); // 从栈里弹出的数据,就是要处理的数据(放进result数组里的数据) + st.pop(); + result.push_back(cur->val); // 中 + cur = cur->right; // 右 + } + } + return result; + } +}; + +``` + +### 后序遍历(迭代法) + +再来看后序遍历,先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图: + +![前序到后序](https://file1.kamacoder.com/i/algo/20200808200338924.png) + +**所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:** + +```CPP +class Solution { +public: + vector postorderTraversal(TreeNode* root) { + stack st; + vector result; + if (root == NULL) return result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + st.pop(); + result.push_back(node->val); + if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈) + if (node->right) st.push(node->right); // 空节点不入栈 + } + reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了 + return result; + } +}; + +``` + +## 总结 + +此时我们用迭代法写出了二叉树的前后中序遍历,大家可以看出前序和中序是完全两种代码风格,并不像递归写法那样代码稍做调整,就可以实现前后中序。 + +**这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!** + +上面这句话,可能一些同学不太理解,建议自己亲手用迭代法,先写出来前序,再试试能不能写出中序,就能理解了。 + +**那么问题又来了,难道二叉树前后中序遍历的迭代法实现,就不能风格统一么(即前序遍历改变代码顺序就可以实现中序 和 后序)?** + +当然可以,这种写法,还不是很好理解,我们将在下一篇文章里重点讲解,敬请期待! + + +## 其他语言版本 + +### Java: + +```java +// 前序遍历顺序:中-左-右,入栈顺序:中-右-左 +class Solution { + public List preorderTraversal(TreeNode root) { + List result = new ArrayList<>(); + if (root == null){ + return result; + } + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()){ + TreeNode node = stack.pop(); + result.add(node.val); + if (node.right != null){ + stack.push(node.right); + } + if (node.left != null){ + stack.push(node.left); + } + } + return result; + } +} + +// 中序遍历顺序: 左-中-右 入栈顺序: 左-右 +class Solution { + public List inorderTraversal(TreeNode root) { + List result = new ArrayList<>(); + if (root == null){ + return result; + } + Stack stack = new Stack<>(); + TreeNode cur = root; + while (cur != null || !stack.isEmpty()){ + if (cur != null){ + stack.push(cur); + cur = cur.left; + }else{ + cur = stack.pop(); + result.add(cur.val); + cur = cur.right; + } + } + return result; + } +} + +// 后序遍历顺序 左-右-中 入栈顺序:中-左-右 出栈顺序:中-右-左, 最后翻转结果 +class Solution { + public List postorderTraversal(TreeNode root) { + List result = new ArrayList<>(); + if (root == null){ + return result; + } + Stack stack = new Stack<>(); + stack.push(root); + while (!stack.isEmpty()){ + TreeNode node = stack.pop(); + result.add(node.val); + if (node.left != null){ + stack.push(node.left); + } + if (node.right != null){ + stack.push(node.right); + } + } + Collections.reverse(result); + return result; + } +} +``` + +### Python: + +```python +# 前序遍历-迭代-LC144_二叉树的前序遍历 +class Solution: + def preorderTraversal(self, root: TreeNode) -> List[int]: + # 根节点为空则返回空列表 + if not root: + return [] + stack = [root] + result = [] + while stack: + node = stack.pop() + # 中节点先处理 + result.append(node.val) + # 右孩子先入栈 + if node.right: + stack.append(node.right) + # 左孩子后入栈 + if node.left: + stack.append(node.left) + return result +``` +```python + +# 中序遍历-迭代-LC94_二叉树的中序遍历 +class Solution: + def inorderTraversal(self, root: TreeNode) -> List[int]: + + if not root: + return [] + stack = [] # 不能提前将root节点加入stack中 + + result = [] + cur = root + while cur or stack: + # 先迭代访问最底层的左子树节点 + if cur: + stack.append(cur) + cur = cur.left + # 到达最左节点后处理栈顶节点 + else: + cur = stack.pop() + result.append(cur.val) + # 取栈顶元素右节点 + cur = cur.right + return result +``` +```python + +# 后序遍历-迭代-LC145_二叉树的后序遍历 +class Solution: + def postorderTraversal(self, root: TreeNode) -> List[int]: + if not root: + return [] + stack = [root] + result = [] + while stack: + node = stack.pop() + # 中节点先处理 + result.append(node.val) + # 左孩子先入栈 + if node.left: + stack.append(node.left) + # 右孩子后入栈 + if node.right: + stack.append(node.right) + # 将最终的数组翻转 + return result[::-1] + ``` + +#### Python 后序遍历的迭代新解法: +* 本解法不同于前文介绍的`逆转前序遍历调整后的结果`,而是采用了对每个节点直接处理。这个实现方法在面试中不容易写出来,在下一节,我将改造本代码,奉上代码更简洁、更套路化、更容易实现的统一方法。 + +```python +class Solution: + def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]: + values = [] + stack = [] + popped_nodes = set() # 记录值已经被收割了的 nodes,这是关键,已经被收割的节点还在树中,还会被访问到,但逻辑上已经等同于 null 节点。 + current = root + + while current or stack: + if current: # 一次处理完一个节点和他的左右儿子节点,不处理孙子节点,孙子节点由左右儿子等会分别处理。 + stack.append(current) # 入栈自己 + + if current.right: + stack.append(current.right) # 入栈右儿子 + + if current.left: # 因为栈是后进先出,后序是‘左右中’,所以后加左儿子 + stack.append(current.left) # 入栈左儿子 + + current = None # 会导致后面A处出栈 + continue + + node = stack.pop() # A处,出的是左儿子,如果无左儿子,出的就是右儿子,如果连右儿子也没有,出的就是自己了。 + + # 如果 node 是叶子节点,就可以收割了;如果左右儿子都已经被收割了,也可以收割 + if (node.left is None or node.left in popped_nodes) and \ + (node.right is None or node.right in popped_nodes): + popped_nodes.add(node) + values.append(node.val) + continue + + current = node # 不符合收割条件,说明 node 下还有未入栈的儿子,就去入栈 + + return values +``` + +### Go: + +> 迭代法前序遍历 + +```go +func preorderTraversal(root *TreeNode) []int { + ans := []int{} + + if root == nil { + return ans + } + + st := list.New() + st.PushBack(root) + + for st.Len() > 0 { + node := st.Remove(st.Back()).(*TreeNode) + + ans = append(ans, node.Val) + if node.Right != nil { + st.PushBack(node.Right) + } + if node.Left != nil { + st.PushBack(node.Left) + } + } + return ans +} +``` + +> 迭代法后序遍历 + +```go +func postorderTraversal(root *TreeNode) []int { + ans := []int{} + + if root == nil { + return ans + } + + st := list.New() + st.PushBack(root) + + for st.Len() > 0 { + node := st.Remove(st.Back()).(*TreeNode) + + ans = append(ans, node.Val) + if node.Left != nil { + st.PushBack(node.Left) + } + if node.Right != nil { + st.PushBack(node.Right) + } + } + reverse(ans) + return ans +} + +func reverse(a []int) { + l, r := 0, len(a) - 1 + for l < r { + a[l], a[r] = a[r], a[l] + l, r = l+1, r-1 + } +} +``` + +> 迭代法中序遍历 + +```go +func inorderTraversal(root *TreeNode) []int { + ans := []int{} + if root == nil { + return ans + } + + st := list.New() + cur := root + + for cur != nil || st.Len() > 0 { + if cur != nil { + st.PushBack(cur) + cur = cur.Left + } else { + cur = st.Remove(st.Back()).(*TreeNode) + ans = append(ans, cur.Val) + cur = cur.Right + } + } + + return ans +} +``` + +### JavaScript: + +```js + +前序遍历: + +// 入栈 右 -> 左 +// 出栈 中 -> 左 -> 右 +var preorderTraversal = function(root, res = []) { + if(!root) return res; + const stack = [root]; + let cur = null; + while(stack.length) { + cur = stack.pop(); + res.push(cur.val); + cur.right && stack.push(cur.right); + cur.left && stack.push(cur.left); + } + return res; +}; + +中序遍历: + +// 入栈 左 -> 右 +// 出栈 左 -> 中 -> 右 + +var inorderTraversal = function(root, res = []) { + const stack = []; + let cur = root; + while(stack.length || cur) { + if(cur) { + stack.push(cur); + // 左 + cur = cur.left; + } else { + // --> 弹出 中 + cur = stack.pop(); + res.push(cur.val); + // 右 + cur = cur.right; + } + }; + return res; +}; + +后序遍历: + +// 入栈 左 -> 右 +// 出栈 中 -> 右 -> 左 结果翻转 + +var postorderTraversal = function(root, res = []) { + if (!root) return res; + const stack = [root]; + let cur = null; + do { + cur = stack.pop(); + res.push(cur.val); + cur.left && stack.push(cur.left); + cur.right && stack.push(cur.right); + } while(stack.length); + return res.reverse(); +}; +``` + +### TypeScript: + +```typescript +// 前序遍历(迭代法) +function preorderTraversal(root: TreeNode | null): number[] { + if (root === null) return []; + let res: number[] = []; + let helperStack: TreeNode[] = []; + let curNode: TreeNode = root; + helperStack.push(curNode); + while (helperStack.length > 0) { + curNode = helperStack.pop()!; + res.push(curNode.val); + if (curNode.right !== null) helperStack.push(curNode.right); + if (curNode.left !== null) helperStack.push(curNode.left); + } + return res; +}; + +// 中序遍历(迭代法) +function inorderTraversal(root: TreeNode | null): number[] { + let helperStack: TreeNode[] = []; + let res: number[] = []; + if (root === null) return res; + let curNode: TreeNode | null = root; + while (curNode !== null || helperStack.length > 0) { + if (curNode !== null) { + helperStack.push(curNode); + curNode = curNode.left; + } else { + curNode = helperStack.pop()!; + res.push(curNode.val); + curNode = curNode.right; + } + } + return res; +}; + +// 后序遍历(迭代法) +function postorderTraversal(root: TreeNode | null): number[] { + let helperStack: TreeNode[] = []; + let res: number[] = []; + let curNode: TreeNode; + if (root === null) return res; + helperStack.push(root); + while (helperStack.length > 0) { + curNode = helperStack.pop()!; + res.push(curNode.val); + if (curNode.left !== null) helperStack.push(curNode.left); + if (curNode.right !== null) helperStack.push(curNode.right); + } + return res.reverse(); +}; +``` + +### Swift: + +```swift +// 前序遍历迭代法 +func preorderTraversal(_ root: TreeNode?) -> [Int] { + var result = [Int]() + guard let root = root else { return result } + var stack = [root] + while !stack.isEmpty { + let current = stack.removeLast() + // 先右后左,这样出栈的时候才是左右顺序 + if let node = current.right { // 右 + stack.append(node) + } + if let node = current.left { // 左 + stack.append(node) + } + result.append(current.val) // 中 + } + return result +} + +// 后序遍历迭代法 +func postorderTraversal(_ root: TreeNode?) -> [Int] { + var result = [Int]() + guard let root = root else { return result } + var stack = [root] + while !stack.isEmpty { + let current = stack.removeLast() + // 与前序相反,即中右左,最后结果还需反转才是后序 + if let node = current.left { // 左 + stack.append(node) + } + if let node = current.right { // 右 + stack.append(node) + } + result.append(current.val) // 中 + } + return result.reversed() +} + +// 中序遍历迭代法 +func inorderTraversal(_ root: TreeNode?) -> [Int] { + var result = [Int]() + var stack = [TreeNode]() + var current: TreeNode! = root + while current != nil || !stack.isEmpty { + if current != nil { // 先访问到最左叶子 + stack.append(current) + current = current.left // 左 + } else { + current = stack.removeLast() + result.append(current.val) // 中 + current = current.right // 右 + } + } + return result +} +``` +### Scala: + +```scala +// 前序遍历(迭代法) +object Solution { + import scala.collection.mutable + def preorderTraversal(root: TreeNode): List[Int] = { + val res = mutable.ListBuffer[Int]() + if (root == null) return res.toList + // 声明一个栈,泛型为TreeNode + val stack = mutable.Stack[TreeNode]() + stack.push(root) // 先把根节点压入栈 + while (!stack.isEmpty) { + var curNode = stack.pop() + res.append(curNode.value) // 先把这个值压入栈 + // 如果当前节点的左右节点不为空,则入栈,先放右节点,再放左节点 + if (curNode.right != null) stack.push(curNode.right) + if (curNode.left != null) stack.push(curNode.left) + } + res.toList + } +} + +// 中序遍历(迭代法) +object Solution { + import scala.collection.mutable + def inorderTraversal(root: TreeNode): List[Int] = { + val res = mutable.ArrayBuffer[Int]() + if (root == null) return res.toList + val stack = mutable.Stack[TreeNode]() + var curNode = root + // 将左节点都入栈,当遍历到最左(到空)的时候,再弹出栈顶元素,加入res + // 再把栈顶元素的右节点加进来,继续下一轮遍历 + while (curNode != null || !stack.isEmpty) { + if (curNode != null) { + stack.push(curNode) + curNode = curNode.left + } else { + curNode = stack.pop() + res.append(curNode.value) + curNode = curNode.right + } + } + res.toList + } +} + +// 后序遍历(迭代法) +object Solution { + import scala.collection.mutable + def postorderTraversal(root: TreeNode): List[Int] = { + val res = mutable.ListBuffer[Int]() + if (root == null) return res.toList + val stack = mutable.Stack[TreeNode]() + stack.push(root) + while (!stack.isEmpty) { + val curNode = stack.pop() + res.append(curNode.value) + // 这次左节点先入栈,右节点再入栈 + if(curNode.left != null) stack.push(curNode.left) + if(curNode.right != null) stack.push(curNode.right) + } + // 最后需要翻转List + res.reverse.toList + } +} +``` + +### Rust: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + //前序 + pub fn preorder_traversal(root: Option>>) -> Vec { + let mut res = vec![]; + let mut stack = vec![root]; + while !stack.is_empty() { + if let Some(node) = stack.pop().unwrap() { + res.push(node.borrow().val); + stack.push(node.borrow().right.clone()); + stack.push(node.borrow().left.clone()); + } + } + res + } + //中序 + pub fn inorder_traversal(root: Option>>) -> Vec { + let mut res = vec![]; + let mut stack = vec![]; + let mut node = root; + + while !stack.is_empty() || node.is_some() { + while let Some(n) = node { + node = n.borrow().left.clone(); + stack.push(n); + } + if let Some(n) = stack.pop() { + res.push(n.borrow().val); + node = n.borrow().right.clone(); + } + } + res + } + //后序 + pub fn postorder_traversal(root: Option>>) -> Vec { + let mut res = vec![]; + let mut stack = vec![root]; + while !stack.is_empty() { + if let Some(node) = stack.pop().unwrap() { + res.push(node.borrow().val); + stack.push(node.borrow().left.clone()); + stack.push(node.borrow().right.clone()); + } + } + res.into_iter().rev().collect() + } +} +``` +### C# +```csharp +// 前序遍历 +public IList PreorderTraversal(TreeNode root) +{ + var st = new Stack(); + var res = new List(); + if (root == null) return res; + st.Push(root); + while (st.Count != 0) + { + var node = st.Pop(); + res.Add(node.val); + if (node.right != null) + st.Push(node.right); + if (node.left != null) + st.Push(node.left); + } + return res; +} + +// 中序遍历 +public IList InorderTraversal(TreeNode root) +{ + var st = new Stack(); + var res = new List(); + var cur = root; + while (st.Count != 0 || cur != null) + { + if (cur != null) + { + st.Push(cur); + cur = cur.left; + } + else + { + cur = st.Pop(); + res.Add(cur.val); + cur = cur.right; + } + } + return res; +} +// 后序遍历 +public IList PostorderTraversal(TreeNode root) +{ + var res = new List(); + var st = new Stack(); + if (root == null) return res; + st.Push(root); + while (st.Count != 0) + { + var cur = st.Pop(); + res.Add(cur.val); + if (cur.left != null) st.Push(cur.left); + if (cur.right != null) st.Push(cur.right); + } + res.Reverse(0, res.Count()); + return res; +} +``` + diff --git "a/problems/\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\351\201\215\345\216\206.md" "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\351\201\215\345\216\206.md" new file mode 100755 index 0000000000..ffa3ff6cf8 --- /dev/null +++ "b/problems/\344\272\214\345\217\211\346\240\221\347\232\204\351\200\222\345\275\222\351\201\215\345\216\206.md" @@ -0,0 +1,728 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +> 一看就会,一写就废! + +# 二叉树的递归遍历 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[每次写递归都要靠直觉? 这次带你学透二叉树的递归遍历!](https://www.bilibili.com/video/BV1Wh411S7xt),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +这次我们要好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。 + +主要是对递归不成体系,没有方法论,**每次写递归算法 ,都是靠玄学来写代码**,代码能不能编过都靠运气。 + +**本篇将介绍前后中序的递归写法,一些同学可能会感觉很简单,其实不然,我们要通过简单题目把方法论确定下来,有了方法论,后面才能应付复杂的递归。** + +这里帮助大家确定下来递归算法的三个要素。**每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!** + +1. **确定递归函数的参数和返回值:** +确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。 + +2. **确定终止条件:** +写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。 + +3. **确定单层递归的逻辑:** +确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。 + +好了,我们确认了递归的三要素,接下来就来练练手: + + +**以下以前序遍历为例:** + +1. **确定递归函数的参数和返回值**:因为要打印出前序遍历节点的数值,所以参数里需要传入vector来放节点的数值,除了这一点就不需要再处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: + +```cpp +void traversal(TreeNode* cur, vector& vec) +``` + +2. **确定终止条件**:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下: + +```cpp +if (cur == NULL) return; +``` + +3. **确定单层递归的逻辑**:前序遍历是中左右的顺序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: + +```cpp +vec.push_back(cur->val); // 中 +traversal(cur->left, vec); // 左 +traversal(cur->right, vec); // 右 +``` + +单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,再看一下完整代码: + +前序遍历: + +```CPP +class Solution { +public: + void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + vec.push_back(cur->val); // 中 + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 + } + vector preorderTraversal(TreeNode* root) { + vector result; + traversal(root, result); + return result; + } +}; +``` + +那么前序遍历写出来之后,中序和后序遍历就不难理解了,代码如下: + +中序遍历: + +```CPP +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + vec.push_back(cur->val); // 中 + traversal(cur->right, vec); // 右 +} +``` + +后序遍历: + +```CPP +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 + vec.push_back(cur->val); // 中 +} +``` + +此时大家可以做一做leetcode上三道题目,分别是: + +* [144.二叉树的前序遍历](https://leetcode.cn/problems/binary-tree-preorder-traversal/) +* [145.二叉树的后序遍历](https://leetcode.cn/problems/binary-tree-postorder-traversal/) +* [94.二叉树的中序遍历](https://leetcode.cn/problems/binary-tree-inorder-traversal/) + +可能有同学感觉前后中序遍历的递归太简单了,要打迭代法(非递归),别急,我们明天打迭代法,打个通透! + +## 其他语言版本 + +### Java: + +```Java +// 前序遍历·递归·LC144_二叉树的前序遍历 +class Solution { + public List preorderTraversal(TreeNode root) { + List result = new ArrayList(); + preorder(root, result); + return result; + } + + public void preorder(TreeNode root, List result) { + if (root == null) { + return; + } + result.add(root.val); + preorder(root.left, result); + preorder(root.right, result); + } +} +// 中序遍历·递归·LC94_二叉树的中序遍历 +class Solution { + public List inorderTraversal(TreeNode root) { + List res = new ArrayList<>(); + inorder(root, res); + return res; + } + + void inorder(TreeNode root, List list) { + if (root == null) { + return; + } + inorder(root.left, list); + list.add(root.val); // 注意这一句 + inorder(root.right, list); + } +} +// 后序遍历·递归·LC145_二叉树的后序遍历 +class Solution { + public List postorderTraversal(TreeNode root) { + List res = new ArrayList<>(); + postorder(root, res); + return res; + } + + void postorder(TreeNode root, List list) { + if (root == null) { + return; + } + postorder(root.left, list); + postorder(root.right, list); + list.add(root.val); // 注意这一句 + } +} +``` + +### Python: + +```python +# 前序遍历-递归-LC144_二叉树的前序遍历 +# 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 = [] + + def dfs(node): + if node is None: + return + + res.append(node.val) + dfs(node.left) + dfs(node.right) + dfs(root) + return res + +``` +```python +# 中序遍历-递归-LC94_二叉树的中序遍历 +class Solution: + def inorderTraversal(self, root: TreeNode) -> List[int]: + res = [] + + def dfs(node): + if node is None: + return + + dfs(node.left) + res.append(node.val) + dfs(node.right) + dfs(root) + return res +``` +```python + + +# 后序遍历-递归-LC145_二叉树的后序遍历 +class Solution: + def postorderTraversal(self, root: TreeNode) -> List[int]: + res = [] + + def dfs(node): + if node is None: + return + + dfs(node.left) + dfs(node.right) + res.append(node.val) + + dfs(root) + return res +``` + +### Go: + +前序遍历: +```go +func preorderTraversal(root *TreeNode) (res []int) { + var traversal func(node *TreeNode) + traversal = func(node *TreeNode) { + if node == nil { + return + } + res = append(res,node.Val) + traversal(node.Left) + traversal(node.Right) + } + traversal(root) + return res +} + +``` +中序遍历: + +```go +func inorderTraversal(root *TreeNode) (res []int) { + var traversal func(node *TreeNode) + traversal = func(node *TreeNode) { + if node == nil { + return + } + traversal(node.Left) + res = append(res,node.Val) + traversal(node.Right) + } + traversal(root) + return res +} +``` +后序遍历: + +```go +func postorderTraversal(root *TreeNode) (res []int) { + var traversal func(node *TreeNode) + traversal = func(node *TreeNode) { + if node == nil { + return + } + traversal(node.Left) + traversal(node.Right) + res = append(res,node.Val) + } + traversal(root) + return res +} +``` + +### JavaScript: + +前序遍历: +```Javascript +var preorderTraversal = function(root) { +// 第一种 +// let res=[]; +// const dfs=function(root){ +// if(root===null)return ; +// //先序遍历所以从父节点开始 +// res.push(root.val); +// //递归左子树 +// dfs(root.left); +// //递归右子树 +// dfs(root.right); +// } +// //只使用一个参数 使用闭包进行存储结果 +// dfs(root); +// return res; +// 第二种 + return root + ? [ + // 前序遍历:中左右 + root.val, + // 递归左子树 + ...preorderTraversal(root.left), + // 递归右子树 + ...preorderTraversal(root.right), + ] + : []; +}; +``` +中序遍历 +```javascript +var inorderTraversal = function(root) { +// 第一种 + + // let res=[]; + // const dfs=function(root){ + // if(root===null){ + // return ; + // } + // dfs(root.left); + // res.push(root.val); + // dfs(root.right); + // } + // dfs(root); + // return res; + +// 第二种 + return root + ? [ + // 中序遍历:左中右 + // 递归左子树 + ...inorderTraversal(root.left), + root.val, + // 递归右子树 + ...inorderTraversal(root.right), + ] + : []; +}; +``` + +后序遍历 +```javascript +var postorderTraversal = function(root) { + // 第一种 + // let res=[]; + // const dfs=function(root){ + // if(root===null){ + // return ; + // } + // dfs(root.left); + // dfs(root.right); + // res.push(root.val); + // } + // dfs(root); + // return res; + + // 第二种 + // 后续遍历:左右中 + return root + ? [ + // 递归左子树 + ...postorderTraversal(root.left), + // 递归右子树 + ...postorderTraversal(root.right), + root.val, + ] + : []; +}; +``` + +### TypeScript: + +```typescript +// 前序遍历 +function preorderTraversal(node: TreeNode | null): number[] { + function traverse(node: TreeNode | null, res: number[]): void { + if (node === null) return; + res.push(node.val); + traverse(node.left, res); + traverse(node.right, res); + } + const res: number[] = []; + traverse(node, res); + return res; +} + +// 中序遍历 +function inorderTraversal(node: TreeNode | null): number[] { + function traverse(node: TreeNode | null, res: number[]): void { + if (node === null) return; + traverse(node.left, res); + res.push(node.val); + traverse(node.right, res); + } + const res: number[] = []; + traverse(node, res); + return res; +} + +// 后序遍历 +function postorderTraversal(node: TreeNode | null): number[] { + function traverse(node: TreeNode | null, res: number[]): void { + if (node === null) return; + traverse(node.left, res); + traverse(node.right, res); + res.push(node.val); + } + const res: number[] = []; + traverse(node, res); + return res; +} +``` + +### C: + +```c +//前序遍历: +void preOrder(struct TreeNode* root, int* ret, int* returnSize) { + if(root == NULL) + return; + ret[(*returnSize)++] = root->val; + preOrder(root->left, ret, returnSize); + preOrder(root->right, ret, returnSize); +} + +int* preorderTraversal(struct TreeNode* root, int* returnSize){ + int* ret = (int*)malloc(sizeof(int) * 100); + *returnSize = 0; + preOrder(root, ret, returnSize); + return ret; +} + +//中序遍历: +void inOrder(struct TreeNode* node, int* ret, int* returnSize) { + if(!node) + return; + inOrder(node->left, ret, returnSize); + ret[(*returnSize)++] = node->val; + inOrder(node->right, ret, returnSize); +} + +int* inorderTraversal(struct TreeNode* root, int* returnSize){ + int* ret = (int*)malloc(sizeof(int) * 100); + *returnSize = 0; + inOrder(root, ret, returnSize); + return ret; +} + +//后序遍历: +void postOrder(struct TreeNode* node, int* ret, int* returnSize) { + if(node == NULL) + return; + postOrder(node->left, ret, returnSize); + postOrder(node->right, ret, returnSize); + ret[(*returnSize)++] = node->val; +} + +int* postorderTraversal(struct TreeNode* root, int* returnSize){ + int* ret= (int*)malloc(sizeof(int) * 100); + *returnSize = 0; + postOrder(root, ret, returnSize); + return ret; +} +``` + +### Swift: +前序遍历:(144.二叉树的前序遍历) + +```Swift +func preorderTraversal(_ root: TreeNode?) -> [Int] { + var res = [Int]() + preorder(root, res: &res) + return res +} +func preorder(_ root: TreeNode?, res: inout [Int]) { + if root == nil { + return + } + res.append(root!.val) + preorder(root!.left, res: &res) + preorder(root!.right, res: &res) +} +``` + +中序遍历:(94. 二叉树的中序遍历) +```Swift +func inorderTraversal(_ root: TreeNode?) -> [Int] { + var res = [Int]() + inorder(root, res: &res) + return res +} +func inorder(_ root: TreeNode?, res: inout [Int]) { + if root == nil { + return + } + inorder(root!.left, res: &res) + res.append(root!.val) + inorder(root!.right, res: &res) +} +``` + +后序遍历:(145. 二叉树的后序遍历) +```Swift +func postorderTraversal(_ root: TreeNode?) -> [Int] { + var res = [Int]() + postorder(root, res: &res) + return res +} +func postorder(_ root: TreeNode?, res: inout [Int]) { + if root == nil { + return + } + postorder(root!.left, res: &res) + postorder(root!.right, res: &res) + res.append(root!.val) +} +``` +### Scala: + + 前序遍历:(144.二叉树的前序遍历) + +```scala +object Solution { + import scala.collection.mutable.ListBuffer + def preorderTraversal(root: TreeNode): List[Int] = { + val res = ListBuffer[Int]() + def traversal(curNode: TreeNode): Unit = { + if(curNode == null) return + res.append(curNode.value) + traversal(curNode.left) + traversal(curNode.right) + } + traversal(root) + res.toList + } +} +``` +中序遍历:(94. 二叉树的中序遍历) +```scala +object Solution { + import scala.collection.mutable.ListBuffer + def inorderTraversal(root: TreeNode): List[Int] = { + val res = ListBuffer[Int]() + def traversal(curNode: TreeNode): Unit = { + if(curNode == null) return + traversal(curNode.left) + res.append(curNode.value) + traversal(curNode.right) + } + traversal(root) + res.toList + } +} +``` +后序遍历:(145. 二叉树的后序遍历) +```scala +object Solution { + import scala.collection.mutable.ListBuffer + def postorderTraversal(root: TreeNode): List[Int] = { + val res = ListBuffer[Int]() + def traversal(curNode: TreeNode): Unit = { + if (curNode == null) return + traversal(curNode.left) + traversal(curNode.right) + res.append(curNode.value) + } + traversal(root) + res.toList + } +} +``` + +### Rust: + +```rust +use std::cell::RefCell; +use std::rc::Rc; +impl Solution { + pub fn preorder_traversal(root: Option>>) -> Vec { + let mut res = vec![]; + Self::traverse(&root, &mut res); + res + } + +//前序遍历 + pub fn traverse(root: &Option>>, res: &mut Vec) { + if let Some(node) = root { + res.push(node.borrow().val); + Self::traverse(&node.borrow().left, res); + Self::traverse(&node.borrow().right, res); + } + } +//后序遍历 + pub fn traverse(root: &Option>>, res: &mut Vec) { + if let Some(node) = root { + Self::traverse(&node.borrow().left, res); + Self::traverse(&node.borrow().right, res); + res.push(node.borrow().val); + } + } +//中序遍历 + pub fn traverse(root: &Option>>, res: &mut Vec) { + if let Some(node) = root { + Self::traverse(&node.borrow().left, res); + res.push(node.borrow().val); + Self::traverse(&node.borrow().right, res); + } + } +} +``` + +### C# +```csharp +// 前序遍历 +public IList PreorderTraversal(TreeNode root) +{ + var res = new List(); + if (root == null) return res; + Traversal(root, res); + return res; + +} +public void Traversal(TreeNode cur, IList res) +{ + if (cur == null) return; + res.Add(cur.val); + Traversal(cur.left, res); + Traversal(cur.right, res); +} +``` +```csharp +// 中序遍历 +public IList InorderTraversal(TreeNode root) +{ + var res = new List(); + if (root == null) return res; + Traversal(root, res); + return res; +} +public void Traversal(TreeNode cur, IList res) +{ + if (cur == null) return; + Traversal(cur.left, res); + res.Add(cur.val); + Traversal(cur.right, res); +} +``` +```csharp +// 后序遍历 +public IList PostorderTraversal(TreeNode root) +{ + var res = new List(); + if (root == null) return res; + Traversal(root, res); + return res; +} +public void Traversal(TreeNode cur, IList res) +{ + if (cur == null) return; + Traversal(cur.left, res); + Traversal(cur.right, res); + res.Add(cur.val); +} +``` + +### PHP +```php +// 144.前序遍历 +function preorderTraversal($root) { + $output = []; + $this->traversal($root, $output); + return $output; +} + +function traversal($root, array &$output) { + if ($root->val === null) { + return; + } + + $output[] = $root->val; + $this->traversal($root->left, $output); + $this->traversal($root->right, $output); +} +``` +```php +// 94.中序遍历 +function inorderTraversal($root) { + $output = []; + $this->traversal($root, $output); + return $output; +} + +function traversal($root, array &$output) { + if ($root->val === null) { + return; + } + + $this->traversal($root->left, $output); + $output[] = $root->val; + $this->traversal($root->right, $output); +} +``` +```php +// 145.后序遍历 +function postorderTraversal($root) { + $output = []; + $this->traversal($root, $output); + return $output; +} + +function traversal($root, array &$output) { + if ($root->val === null) { + return; + } + + $this->traversal($root->left, $output); + $this->traversal($root->right, $output); + $output[] = $root->val; +} +``` + + diff --git "a/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217.md" "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217.md" new file mode 100644 index 0000000000..70643b7e49 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217.md" @@ -0,0 +1,60 @@ + +# 什么是核心代码模式,什么又是ACM模式? + +很多录友刷了不少题了,到现在也没有搞清楚什么是 ACM模式,什么是核心代码模式。 + +平时大家在力扣上刷题,就是 核心代码模式,即给你一个函数,直接写函数实现,例如这样: + +![](https://file1.kamacoder.com/i/algo/20231109193631.png) + +而ACM模式,是程序头文件,main函数,数据的输入输出都要自己处理,例如这样: + +![](https://file1.kamacoder.com/i/algo/20231109193743.png) + +大家可以发现 右边代码框什么都没有,程序从头到尾都需要自己实现,本题如果写完代码是这样的: (细心的录友可以发现和力扣上刷题是不一样的) + +![](https://file1.kamacoder.com/i/algo/20231109193931.png) + + +**如果大家从一开始学习算法就一直在力扣上的话,突然切到ACM模式会非常不适应**。 + +知识星球里也有很多录友,因为不熟悉ACM模式在面试的过程中吃了不少亏。 + + +
+ +
+ +
+ +
+ +
+ +## 面试究竟怎么考? + +笔试的话,基本都是 ACM模式。 + +面试的话,看情况,有的面试官会让你写一个函数实现就可以,此时就是核心代码模式。 + +有的面试官会 给你一个编辑器,让你写完代码运行一下看看输出结果,此时就是ACM模式。 + +有的录友想,那我平时在力扣刷题,写的是核心代码模式,我也可以运行,为啥一定要用ACM模式。 + +**大家在力扣刷题刷多了,已经忘了程序是如何运行的了**,力扣上写的代码,脱离力扣环境,那个函数,你怎么运行呢? + +想让程序在本地运行起来,是不是需要补充 库函数,是不是要补充main函数,是不是要补充数据的输入和输出。 那不就是ACM模式了。 + +综合来看,** ACM模式更考察综合代码能力, 核心代码模式是更聚焦算法的实现逻辑**。 + +## 去哪练习ACM模式? + +这里给大家推荐卡码网: [kamacoder.com](https://kamacoder.com/) + +你只要能把卡码网首页的25道题目 都刷了 ,就把所有的ACM输入输出方式都练习到位了,不会有任何盲区。 + +![](https://file1.kamacoder.com/i/algo/20231109195056.png) + +而且你不用担心,题目难度太大,直接给自己劝退,**卡码网的前25道题目都是我精心制作的,难度也是循序渐进的**,大家去刷一下就知道了。 + + diff --git "a/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217\345\246\202\344\275\225\346\236\204\345\273\272\344\272\214\345\217\211\346\240\221.md" "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217\345\246\202\344\275\225\346\236\204\345\273\272\344\272\214\345\217\211\346\240\221.md" new file mode 100644 index 0000000000..2eeb7431bf --- /dev/null +++ "b/problems/\345\211\215\345\272\217/ACM\346\250\241\345\274\217\345\246\202\344\275\225\346\236\204\345\273\272\344\272\214\345\217\211\346\240\221.md" @@ -0,0 +1,422 @@ + +# 力扣上如何自己构造二叉树输入用例? + +**这里给大家推荐ACM模式练习网站**:[kamacoder.com](https://kamacoder.com),把上面的题目刷完,ACM模式就没问题了。 + +经常有录友问,二叉树的题目中输入用例在ACM模式下应该怎么构造呢? + +力扣上的题目,输入用例就给了一个数组,怎么就能构造成二叉树呢? + +这次就给大家好好讲一讲! + +就拿最近公众号上 二叉树的打卡题目来说: + +[538.把二叉搜索树转换为累加树](https://mp.weixin.qq.com/s/rlJUFGCnXsIMX0Lg-fRpIw) + +其输入用例,就是用一个数组来表述 二叉树,如下: + +![](https://file1.kamacoder.com/i/algo/20210914222335.png) + +一直跟着公众号学算法的录友 应该知道,我在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/Dza-fqjTyGrsRw4PWNKdxA),已经讲过,**只有 中序与后序 和 中序和前序 可以确定一棵唯一的二叉树。 前序和后序是不能确定唯一的二叉树的**。 + +那么[538.把二叉搜索树转换为累加树](https://mp.weixin.qq.com/s/rlJUFGCnXsIMX0Lg-fRpIw)的示例中,为什么,一个序列(数组或者是字符串)就可以确定二叉树了呢? + +很明显,是后台直接明确了构造规则。 + +再看一下 这个 输入序列 和 对应的二叉树。 +![](https://file1.kamacoder.com/i/algo/20210914222335.png) + +从二叉树 推导到 序列,大家可以发现这就是层序遍历。 + +但从序列 推导到 二叉树,很多同学就看不懂了,这得怎么转换呢。 + +我在 [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/q_eKfL8vmSbSFcptZ3aeRA)已经详细讲过,二叉树可以有两种存储方式,一种是 链式存储,另一种是顺序存储。 + +链式存储,就是大家熟悉的二叉树,用指针指向左右孩子。 + +顺序存储,就是用一个数组来存二叉树,其方式如图所示: + +![](https://file1.kamacoder.com/i/algo/20210914223147.png) + +那么此时大家是不是应该知道了,数组如何转化成 二叉树了。**如果父节点的数组下标是i,那么它的左孩子下标就是i * 2 + 1,右孩子下标就是 i * 2 + 2**。 + +那么这里又有同学疑惑了,这些我都懂了,但我还是不知道 应该 怎么构造。 + +来,咱上代码。 昨天晚上 速度敲了一遍实现代码。 + +具体过程看注释: + +```CPP +// 根据数组构造二叉树 +TreeNode* construct_binary_tree(const vector& vec) { + vector vecTree (vec.size(), NULL); + TreeNode* root = NULL; + // 把输入数值数组,先转化为二叉树节点数组 + for (int i = 0; i < vec.size(); i++) { + TreeNode* node = NULL; + if (vec[i] != -1) node = new TreeNode(vec[i]); // 用 -1 表示null + vecTree[i] = node; + if (i == 0) root = node; + } + // 遍历一遍,根据规则左右孩子赋值就可以了 + // 注意这里 结束规则是 i * 2 + 1 < vec.size(),避免空指针 + // 为什么结束规则不能是i * 2 + 2 < arr.length呢? + // 如果i * 2 + 2 < arr.length 是结束条件 + // 那么i * 2 + 1这个符合条件的节点就被忽略掉了 + // 例如[2,7,9,-1,1,9,6,-1,-1,10] 这样的一个二叉树,最后的10就会被忽略掉 + // 遍历一遍,根据规则左右孩子赋值就可以了 + + for (int i = 0; i * 2 + 1 < vec.size(); i++) { + if (vecTree[i] != NULL) { + // 线性存储转连式存储关键逻辑 + vecTree[i]->left = vecTree[i * 2 + 1]; + if(i * 2 + 2 < vec.size()) + vecTree[i]->right = vecTree[i * 2 + 2]; + } + } + return root; +} +``` + +这个函数最后返回的 指针就是 根节点的指针, 这就是 传入二叉树的格式了,也就是 力扣上的用例输入格式,如图: + +![](https://file1.kamacoder.com/i/algo/20210914224422.png) + +也有不少同学在做ACM模式的题目,就经常疑惑: + +* 让我传入数值,我会! +* 让我传入数组,我会! +* 让我传入链表,我也会! +* **让我传入二叉树,我懵了,啥? 传入二叉树?二叉树怎么传?** + +其实传入二叉树,就是传入二叉树的根节点的指针,和传入链表都是一个逻辑。 + +这种现象主要就是大家对ACM模式过于陌生,说实话,ACM模式才真正的考察代码能力(注意不是算法能力),而 力扣的核心代码模式 总有一种 不够彻底的感觉。 + +所以,如果大家对ACM模式不够了解,一定要多去练习! + +那么以上的代码,我们根据数组构造二叉树,接来下我们在 把 这个二叉树打印出来,看看是不是 我们输入的二叉树结构,这里就用到了层序遍历,我们在[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/4-bDKi7SdwfBGRm9FYduiA)中讲过。 + + +完整测试代码如下: + +```CPP +#include +#include +#include +using namespace std; + +struct TreeNode { + int val; + TreeNode *left; + TreeNode *right; + TreeNode(int x) : val(x), left(NULL), right(NULL) {} +}; + +// 根据数组构造二叉树 +TreeNode* construct_binary_tree(const vector& vec) { + vector vecTree (vec.size(), NULL); + TreeNode* root = NULL; + for (int i = 0; i < vec.size(); i++) { + TreeNode* node = NULL; + if (vec[i] != -1) node = new TreeNode(vec[i]); + vecTree[i] = node; + if (i == 0) root = node; + } + for (int i = 0; i * 2 + 1 < vec.size(); i++) { + if (vecTree[i] != NULL) { + vecTree[i]->left = vecTree[i * 2 + 1]; + if(i * 2 + 2 < vec.size()) + vecTree[i]->right = vecTree[i * 2 + 2]; + } + } + return root; +} + +// 层序打印打印二叉树 +void print_binary_tree(TreeNode* root) { + queue que; + if (root != NULL) que.push(root); + vector> result; + while (!que.empty()) { + int size = que.size(); + vector vec; + for (int i = 0; i < size; i++) { + TreeNode* node = que.front(); + que.pop(); + if (node != NULL) { + vec.push_back(node->val); + que.push(node->left); + que.push(node->right); + } + // 这里的处理逻辑是为了把null节点打印出来,用-1 表示null + else vec.push_back(-1); + } + result.push_back(vec); + } + for (int i = 0; i < result.size(); i++) { + for (int j = 0; j < result[i].size(); j++) { + cout << result[i][j] << " "; + } + cout << endl; + } +} + +int main() { + // 注意本代码没有考虑输入异常数据的情况 + // 用 -1 来表示null + vector vec = {4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1,-1,8}; + TreeNode* root = construct_binary_tree(vec); + print_binary_tree(root); +} + +``` + +可以看出我们传入的数组是:{4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1,-1,8} , 这里是用 -1 来表示null, + +和 [538.把二叉搜索树转换为累加树](https://mp.weixin.qq.com/s/rlJUFGCnXsIMX0Lg-fRpIw) 中的输入是一样的 + +![](https://file1.kamacoder.com/i/algo/20210914222335.png) + +这里可能又有同学疑惑,你这不一样啊,题目是null,你为啥用-1。 + +用-1 表示null为了方便举例,如果非要和 力扣输入一样一样的,就是简单的字符串处理,把null 替换为 -1 就行了。 + +在来看,测试代码输出的效果: + +![](https://file1.kamacoder.com/i/algo/20210914230045.png) + +可以看出和 题目中输入用例 这个图 是一样一样的。 只不过题目中图没有把 空节点 画出来而已。 + +![](https://file1.kamacoder.com/i/algo/20210914230118.png) + +大家可以拿我的代码去测试一下,跑一跑。 + +**注意:我的测试代码,并没有处理输入异常的情况(例如输入空数组之类的),处理各种输入异常,大家可以自己去练练**。 + + +# 总结 + +大家可以发现,这个问题,其实涉及很多知识点,而这些知识点 其实我在文章里都讲过,而且是详细的讲过,如果大家能把这些知识点串起来,很容易解决心中的疑惑了。 + +但为什么很多录友都没有想到这个程度呢。 + +这也是我反复强调,**代码随想录上的 题目和理论基础,至少要详细刷两遍**。 + +**[知识星球](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ)**里有的录友已经开始三刷: + +![](https://file1.kamacoder.com/i/algo/20210727234031.png) + +只做过一遍,真的就是懂了一点皮毛, 第二遍刷才有真的对各个题目有较为深入的理解,也会明白 我为什么要这样安排刷题的顺序了。 + +**都是卡哥的良苦用心呀!** + + +# 其他语言版本 + + +## Java + +```Java +public class Solution { + // 节点类 + static class TreeNode { + // 节点值 + int val; + + // 左节点 + TreeNode left; + + // 右节点 + TreeNode right; + + // 节点的构造函数(默认左右节点都为null) + public TreeNode(int x) { + this.val = x; + this.left = null; + this.right = null; + } + } + + /** + * 根据数组构建二叉树 + * @param arr 树的数组表示 + * @return 构建成功后树的根节点 + */ + public TreeNode constructBinaryTree(final int[] arr) { + // 构建和原数组相同的树节点列表 + List treeNodeList = arr.length > 0 ? new ArrayList<>(arr.length) : null; + TreeNode root = null; + // 把输入数值数组,先转化为二叉树节点列表 + for (int i = 0; i < arr.length; i++) { + TreeNode node = null; + if (arr[i] != -1) { // 用 -1 表示null + node = new TreeNode(arr[i]); + } + treeNodeList.add(node); + if (i == 0) { + root = node; + } + } + // 遍历一遍,根据规则左右孩子赋值就可以了 + // 注意这里 结束规则是 i * 2 + 1 < arr.length,避免空指针 + // 为什么结束规则不能是i * 2 + 2 < arr.length呢? + // 如果i * 2 + 2 < arr.length 是结束条件 + // 那么i * 2 + 1这个符合条件的节点就被忽略掉了 + // 例如[2,7,9,-1,1,9,6,-1,-1,10] 这样的一个二叉树,最后的10就会被忽略掉 + for (int i = 0; i * 2 + 1 < arr.length; i++) { + TreeNode node = treeNodeList.get(i); + if (node != null) { + // 线性存储转连式存储关键逻辑 + node.left = treeNodeList.get(2 * i + 1); + // 再次判断下 不忽略任何一个节点 + if(i * 2 + 2 < arr.length) + node.right = treeNodeList.get(2 * i + 2); + } + } + return root; + } +} +``` + + +## Python + +```Python +class TreeNode: + def __init__(self, val = 0, left = None, right = None): + self.val = val + self.left = left + self.right = right + + +# 根据数组构建二叉树 + +def construct_binary_tree(nums: []) -> TreeNode: + if not nums: + return None + # 用于存放构建好的节点 + root = TreeNode(-1) + Tree = [] + # 将数组元素全部转化为树节点 + for i in range(len(nums)): + if nums[i]!= -1: + node = TreeNode(nums[i]) + else: + node = None + Tree.append(node) + if i == 0: + root = node + # 直接判断2*i+2 []: + if not root: + return + self.inorder(root.left) + self.T.append(root.val) + self.inorder(root.right) + return self.T + + + +# 验证创建二叉树的有效性,二叉排序树的中序遍历应为顺序排列 + +test_tree = [3, 1, 5, -1, 2, 4 ,6] +root = construct_binary_tree(test_tree) +A = Solution() +print(A.inorder(root)) +``` + + +## Go + +```Go +package main + +import "fmt" + +type TreeNode struct { + Val int + Left *TreeNode + Right *TreeNode +} + +func constructBinaryTree(array []int) *TreeNode { + var root *TreeNode + nodes := make([]*TreeNode, len(array)) + + // 初始化二叉树节点 + for i := 0; i < len(nodes); i++ { + var node *TreeNode + if array[i] != -1 { + node = &TreeNode{Val: array[i]} + } + nodes[i] = node + if i == 0 { + root = node + } + } + // 串联节点 + for i := 0; i*2+2 < len(array); i++ { + if nodes[i] != nil { + nodes[i].Left = nodes[i*2+1] + nodes[i].Right = nodes[i*2+2] + } + } + return root +} + +func printBinaryTree(root *TreeNode, n int) { + var queue []*TreeNode + if root != nil { + queue = append(queue, root) + } + + result := []int{} + for len(queue) > 0 { + for j := 0; j < len(queue); j++ { + node := queue[j] + if node != nil { + result = append(result, node.Val) + queue = append(queue, node.Left) + queue = append(queue, node.Right) + } else { + result = append(result, -1) + } + } + // 清除队列中的本层节点, 进入下一层遍历 + queue = queue[len(queue):] + } + + // 参数n控制输出值数量, 否则二叉树最后一层叶子节点的孩子节点也会被打印(但是这些孩子节点是不存在的). + fmt.Println(result[:n]) +} + +func main() { + array := []int{4, 1, 6, 0, 2, 5, 7, -1, -1, -1, 3, -1, -1, -1, 8} + root := constructBinaryTree(array) + printBinaryTree(root, len(array)) +} + +``` + +## JavaScript + +```JavaScript +``` + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/BAT\347\272\247\345\210\253\346\212\200\346\234\257\351\235\242\350\257\225\346\265\201\347\250\213\345\222\214\346\263\250\346\204\217\344\272\213\351\241\271\351\203\275\345\234\250\350\277\231\351\207\214\344\272\206.md" "b/problems/\345\211\215\345\272\217/BAT\347\272\247\345\210\253\346\212\200\346\234\257\351\235\242\350\257\225\346\265\201\347\250\213\345\222\214\346\263\250\346\204\217\344\272\213\351\241\271\351\203\275\345\234\250\350\277\231\351\207\214\344\272\206.md" new file mode 100644 index 0000000000..7d112a19c8 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/BAT\347\272\247\345\210\253\346\212\200\346\234\257\351\235\242\350\257\225\346\265\201\347\250\213\345\222\214\346\263\250\346\204\217\344\272\213\351\241\271\351\203\275\345\234\250\350\277\231\351\207\214\344\272\206.md" @@ -0,0 +1,214 @@ + +# 大厂技术面试流程和注意事项 + +大型互联网企业一般通过几轮技术面试来考察大家的各项能力,一般流程如下: + +* 一面机试:一般会考选择题和编程题 +* 二面基础算法面:就是基础的算法都是该专栏要讲的 +* 三面综合技术面:会考察编程语言,计算机基础知识 ,以及了解项目经历等等 +* 四面技术boss面:会问一些比较范范的内容,考察大家解决问题和快速学习的能力 +* 最后hr面:主要了解面试者与企业文化相不相符,面试者的职业发展,offer的选择以及介绍一下企业提供的薪资待遇等等 + +并不是说一定是这五轮面试,不同的公司情况都不一样,甚至同一个公司不同事业群面试的流程都是不一样的 + +可能 一面和二面放到一起,可能三面和四面放到一起,这里尽量将各个维度拆开,让同学们了解 技术面试需要做哪方面的准备。 + +我们来逐一展开分析各个面试环节面试官是从哪些维度来考察大家的 + + +## 一面 机试 + +一面的话通常是 选择题 + 编程题,还有些公司机试都是编程题。 + +* 选择题:计算机基础知识涉及计算机网络,操作系统,数据库,编程语言等等 +* 编程题:一般是代码量比较大的题目 + +一面机试,**通常校招生的话,BAT的级别的企业 都会提前发笔试题,发到邮箱里然后指定时间内做完,一定要慎重对待,机试没有过,后面就没有面试机会了** + +机试通常是 **选择题 + 编程题,还有些公司机试都是编程题** + +选择题则是计算机基础知识涉及计算机网络,操作系统,数据库,编程语言等等,这里如果有些同学对计算机基础心里没有底的话,可以去牛客网上找一找 历年各大公司的机试题目找找感觉。 + +编程题则一般是代码量比较大的题目,图、复杂数据结构或者一些模拟类的题目,编程题目都是我们这门课程会讲述的重点 + +所以也给同学们也推荐一个编程学习的网站,也就是leetcode + +leetcode是专门针对算法练习的题库,leetcode现在也推出了中文网站,所以更方面中国的算法爱好者在上面刷题。 这门课程也是主要在leetcode上选择经典题目。 + +牛客网上涉及到程序员面试的各个环节,有很多国内互联网公司历年面试的题目还是很不错的。 + +**建议学习计算机基础知识可以在牛客网上,刷算法题可以选择leetcode。** + +## 二面 基础算法面 + +### 更注意考察的是思维方式 + +这一块和机试对算法的考察又不一样,机试仅仅就是要一个结果,对了就是对了不对就是不对, + +而二面的算法面试**面试官更想看到同学们的思考过程**,而不仅仅是一个答案。 + +通常一面机试的题目是代码量比较大的题目,而二面而是一些基础算法 + +面试官会让面试者在白纸上写代码或者给面试者一台电脑来写代码, + +**一般面试官倾向于使用白纸,这样更好看到同学们的思考方式** + +### 应该用什么语言写算法题呢? + +应该用什么语言写算法题呢? **用自己最熟悉什么语言,但最好是JAVA或者C++** + +如果不会JAVA或C++的话,那更建议通过做算法题,顺便学习一下。 + +如果想在编程的路上走得更远,掌握一门重语言是十分重要的,学好了C++或者Java在学脚本语言会非常的快,相当于降维打击 + +反而如果只会脚本语言,工作之后在学习高级语言会很困难,很多东西回不理解。 + +所以这里建议特别是应届生,大家有时间就要打好语言的基础, 不要太迷信用10行代码调用一个包解决100行代码的事, + +因为这样也不会清楚省略掉的90行做了哪些工作。 + +这里建议大家 **在打基础的时候 最好不要上来就走捷径。** + +**简单代码一定要可以手写出来,不要过于依赖IDE的自动补全 。** + +例如写一个翻转二叉树的函数, 很多同学在刷了很多leetcode 上面的题目 + +但是leetcode上一般都把二叉树的结构已经定义好了,所以可以上来直接写函数的实现 + +但是面试的时候要在白纸上写代码,一些同学一下子不知道二叉树的定义应该如何写,不是结构体定义的不对,就是忘了如何写指针。 + +总之,错漏百出。 **所以基本结构的定义以及代码一定要训练在白纸上写出来** + +后面我在讲解各个知识点的时候 会在给同学们在强调一遍哪些代码是一定要手写出来的 + +## 三面 综合技术面 + +综合技术面 一般从如下三点考察大家。 + +### 编程语言 + +编程语言,这里是面试官**考察编程语言掌握程度**,如果是C++的话, 会问STL,继承,多态,指针等等 这里还可以问很多问题。 + +### 计算机基础知识 + +**考察计算机方面的综合知识**,这里不同方向考察的侧重点不一样,如果是后台开发,Linux , TCP, 进程线程这些是一定要问的。 + +### 项目经验 + +在项目经验中 面试官想考察什么呢 + +项目经验主要从这三方面进行考察 **技术原理、 技术深度、应变能力** + +考察技术原理, 做了一个项目,是不是仅仅调一调接口就完事,之后接口背后做了些什么么? 这些还是要了解的 + +考察技术深度,如果是后台开发的话,可以从系统的扩容、缓存、数据存储等多方面进行考察 + +考察应变能力,如果面试官针对项目问同学们一个场景,**最为忌讳的回答是什么?“我没考虑过这种情况”。** 这会让面试官对同学们的印象大打折扣。 + +这个时候,面试官最欣赏的候选人,就是尽管没考虑过,但也会思考出一个方案,然后跟面试官进行讨论。 + +最终讨论出一个可行的方案,这个会让面试官对同学们的好感倍增。 + +通常应届生没有什么项目经验,特备是本科生,其实可以自己做一些的小项目。 + +例如做一个 可以联机的五子棋游戏,这里就涉及到了网络知识,可以结合着自己网络知识来介绍自己的项目。 + +已经工作的人,就要找出自己工作项目的亮点,其实一个项目不是每一个人都有机会参与核心的开发。 + +也不是每个人都有解决难题的机会,这也是我们在工作中 遇到难点,要勇往直前的动力,因为这个就是自己项目经验最值钱的一部分。 + + +## 四面 boss面 + +技术leader面试主要考察面试者两个能力, **解决问题的能力和快速学习的能力** + +### 考察解决问题的能力 + +面试官最喜欢问的相关问题: +* **在项目中遇到的最大的技术挑战是什么,而你是如果解决的** +* **给出一个项目问题来让面试者分析** + +如果你是学生,就会问在你学习中遇到哪些挑战, 这些都是面试官经常问的问题。 + +面试官可能还会给出一个具体的项目场景,问同学们如何去解决。 + +例如微信朋友圈的后台设计,如果是你应该怎么设计,这种问题大家也不必惊慌 + +因为面试官也知道你没有设计过,所以大家只要大胆说出自己的设计方案就好 + +面试官会在进一步指引你的方案可能那里有问题,最终讨论出一个看似合理的结果。 + +**这里面试官考察的主要是针对项目问题,同学们是如何思考的,如何解决的。** + +### 考察快速学习的能力 + +面试官最喜欢问的相关问题: +* **快速学习的能力 如果快速学习一门新的技术或者语言?** +* **读研之后发现自己和本科毕业有什么差别?** + +在具体一点 面试官会问,如果有个项目这两天就要启动,而这个项目使用了你没有用过的语言或者技术,你将怎么完成这个项目? + +换句话说,面试官会问:你如果快速学习一门新的编程语言或技术,这里同学们就要好好总结一下自己学习的技巧 + +如果你是研究生,面试官还喜欢问: 读研之后发现自己和本科毕业有什么差别? + +**这里要体现出自己思维方式和学习方法上的进步,而不是用了两三年的时间有多学了那些技术,因为互联网是不断变化的。** + +面试官更喜欢考察是同学们的快速学习的能力。 + +## 五面 hr面 + +终于到了HR面了,大家是不是感觉完事万事大吉了,这里万万不可大意,否则到手的offer就飞掉了。 + +要知道HR那里如果有十个名额,技术面留给通常留给HR的人数是大于十个的,也就是HR有选择权,HR会选择符合公司文化的价值观的候选人。 + +这里呢给大家列举一些关键问题 + +### 为什么选择我们公司? + +这个大家一定要有所准备,不能被问到了之后一脸茫然,然后说 就是想找个工作,那基本就没戏了 + +要从技术氛围,职业发展,公司潜力等等方面来说自己为什么选择这家公司 + +### 有没有职业规划? + +其实如果刚刚毕业并没有明确的职业规划,这里建议大家不要说 自己想工作几年想做项目经理,工作几年想做产品经理的 + +这样会被HR认为 职业规划不清晰,尽量从技术的角度规划自己。 + +### 是否接受加班? + +虽然大家都不喜欢加班,但是这个问题 我还是建议如果手头没有offer的话,大家尽量选择接受了 + +除非是超级大牛手头N多高新offer,可以直接说不接受,然后起身潇洒离去 + +### 坚持最长的一件事情是什么? + +这里大家最好之前就想好,有一些同学可能印象里自己没有坚持很长的事情,也没有好好想过这个问题,在HR面的时候被问到的时候,一脸茫然 + +憋了半天说出一个不痛不痒的事情。这就是一个减分项了 + +### 如果校招,直接会问:期望薪资XXX是否接受? + +这里大家如果感觉自己表现的很好 给面试官留下的很好的印象,**可以在这里争取 special offer,或者ssp offer** + +这都是可以的,但是要真的对自己信心十足。 + +### 如果社招,则会了解前一家目前公司薪水多少 ? + +**这里大家切记不要虚报工资,因为入职前是要查流水的,这个是比较严肃的问题。** + +其实HR也不会只聊很严肃的话题, 也会聊一聊家常之类的,问一问 家在哪里?在介绍一下公司薪酬福利待遇,这些就比较放松了 + +## 总结 + +这里面试流程就是这样了, 还是那句话 不是所有公司都按照这个流程来面试,但是如果是一线互联网公司,一般都会从我说的这几方面来考察大家 +大家加油! + + + + + + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/Java\345\244\204\347\220\206\350\276\223\345\205\245\350\276\223\345\207\272.md" "b/problems/\345\211\215\345\272\217/Java\345\244\204\347\220\206\350\276\223\345\205\245\350\276\223\345\207\272.md" new file mode 100644 index 0000000000..9ddec96dd4 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/Java\345\244\204\347\220\206\350\276\223\345\205\245\350\276\223\345\207\272.md" @@ -0,0 +1,5 @@ +在面试中,我们常常会遇到面试官让我们用某种编程语言做题,并要求能够在本地编译运行。 +这种模式也被称做ACM模式。 + +本文将介绍如何用Java处理输入输出。 + diff --git "a/problems/\345\211\215\345\272\217/gitserver.md" "b/problems/\345\211\215\345\272\217/gitserver.md" new file mode 100755 index 0000000000..caf93ec6ec --- /dev/null +++ "b/problems/\345\211\215\345\272\217/gitserver.md" @@ -0,0 +1,312 @@ + +# 一文手把手教你搭建Git私服 + +## 为什么要搭建Git私服 + +很多同学都问文章,文档,资料怎么备份啊,自己电脑和公司电脑怎么随时同步资料啊等等,这里呢我写一个搭建自己的git私服的详细教程 + +为什么要搭建一个Git私服呢,而不是用Github免费的私有仓库,有以下几点: +* Github 私有仓库真的慢,文件一旦多了,或者有图片文件,git pull 的时候半天拉不下来 +* 自己的文档难免有自己个人信息,放在github心里也是担心的 +* 想建几个库就建几个,想几个人合作开发都可以,不香么? + +**网上可以搜到很多git搭建,但是说的模棱两可**,而且有的直接是在本地搭建git服务,既然是备份,搭建在本地哪有备份的意义,一定要有一个远端服务器, 而且自己的电脑和公司的电脑还是同步自己的文章,文档和资料等等。 + + +适合人群: 想通过git私服来备份自己的文章,Markdown,并做版本管理的同学 +最后,写好每篇 Chat 是对我的责任,也是对你的尊重。谢谢大家~ + +正文如下: + +----------------------------- + +## 如何找到可以外网访问服务器 + +有的同学问了,自己的电脑就不能作为服务器么? + +这里要说一下,安装家庭带宽,运营商默认是不会给我们独立分配公网IP的 + +一般情况下是一片区域公用一个公网IP池,所以外网是不能访问到在家里我们使用的电脑的 + +除非我们自己去做映射,这其实非常麻烦而且公网IP池 是不断变化的 + +辛辛苦苦做了映射,运营商给IP一换,我们的努力就白扯了 + +那我们如何才能找到一个外网可以访问的服务器呢,此时云计算拯救了我们。 + +推荐大家选一家云厂商(阿里云,腾讯云,百度云都可以)在上面上买一台云服务器 + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + +云厂商经常做活动,如果从来没有买过云服务器的账号更便宜,低配一年一百块左右的样子,强烈推荐一起买个三年。 + +买云服务器的时候推荐直接安装centos系统。 + +这里要说一下,有了自己的云服务器之后 不仅仅可以用来做git私服 + +**同时还可以做网站,做程序后台,跑程序,做测试**(这样我们自己的电脑就不会因为自己各种搭建环境下载各种包而搞的的烂糟糟),等等等。 + +有自己云服务器和一个公网IP真的是一件非常非常幸福的事情,能体验到自己的服务随时可以部署上去提供给所有人使用的喜悦。 + +外网可以访问的服务器解决了,接下来就要部署git服务了 + +本文将采用centos系统来部署git私服 + +## 服务器端安装Git + +切换至root账户 + +``` +su root +``` + +看一下服务器有没有安装git,如果出现下面信息就说明是有git的 +``` +[root@instance-5fcyjde7 ~]# git +usage: git [--version] [--help] [-c name=value] + [--exec-path[=]] [--html-path] [--man-path] [--info-path] + [-p|--paginate|--no-pager] [--no-replace-objects] [--bare] + [--git-dir=] [--work-tree=] [--namespace=] + [] + +The most commonly used git commands are: + add Add file contents to the index + bisect Find by binary search the change that introduced a bug + branch List, create, or delete branches + checkout Checkout a branch or paths to the working tree + clone Clone a repository into a new directory + commit Record changes to the repository + diff Show changes between commits, commit and working tree, etc + fetch Download objects and refs from another repository + grep Print lines matching a pattern + init Create an empty Git repository or reinitialize an existing one + log Show commit logs + merge Join two or more development histories together + mv Move or rename a file, a directory, or a symlink + pull Fetch from and merge with another repository or a local branch + push Update remote refs along with associated objects + rebase Forward-port local commits to the updated upstream head + reset Reset current HEAD to the specified state + rm Remove files from the working tree and from the index + show Show various types of objects + status Show the working tree status + tag Create, list, delete or verify a tag object signed with GPG + +'git help -a' and 'git help -g' lists available subcommands and some +concept guides. See 'git help ' or 'git help ' +to read about a specific subcommand or concept. +``` + +如果没有git,就安装一下,yum安装的版本默认是 `1.8.3.1` + +``` +yum install git +``` + +安装成功之后,看一下自己安装的版本 + +``` +git --version +``` + +## 服务器端设置Git账户 + +创建一个git的linux账户,这个账户只做git私服的操作,也是为了安全起见 + +如果不新创建一个linux账户,在自己的常用的linux账户下创建的话,哪天手抖 来一个`rm -rf *` 操作 数据可全没了 + +**这里linux git账户的密码设置的尽量复杂一些,我这里为了演示,就设置成为'gitpassword'** +``` +adduser git +passwd gitpassword +``` + +然后就要切换成git账户,进行后面的操作了 +``` +[root@instance-5fcyjde7 ~]# su - git +``` + +看一下自己所在的目录,是不是在git目录下面 + +``` +[git@instance-5fcyjde7 ~]$ pwd +/home/git +``` + +## 服务器端密钥管理 + +创建`.ssh` 目录,如果`.ssh` 已经存在了,可以忽略这一项 + +为啥用配置ssh公钥呢,同学们记不记得我使用github上传代码的时候也要把自己的公钥配置上传到github上 + +这也是方面每次操作git仓库的时候不用再去输入密码 + +``` +cd ~/ +mkdir .ssh +``` + +进入.ssh 文件下,创建一个 `authorized_keys` 文件,这个文件就是后面就是要放我们客户端的公钥 + +``` +cd ~/.ssh +touch authorized_keys +``` + +别忘了`authorized_keys`给设置权限,很多同学发现自己不能免密登陆,都是因为忘记了给`authorized_keys` 设置权限 + +``` +chmod 700 /home/git/.ssh +chmod 600 /home/git/.ssh/authorized_keys +``` + +接下来我们要把客户端的公钥放在git服务器上,我们在回到客户端,创建一个公钥 + +在我们自己的电脑上,有公钥和私钥 两个文件分别是:`id_rsa` 和 `id_rsa.pub` + +如果是`windows`系统公钥私钥的目录在`C:\Users\用户名\.ssh` 下 + +如果是mac 或者 linux, 公钥和私钥的目录这里 `cd ~/.ssh/`, 如果发现自己的电脑上没有公钥私钥,那就自己创建一个 + +创建密钥的命令 + +``` +ssh-keygen -t rsa +``` + +创建密钥的过程中,一路点击回车就可以了。不需要填任何东西 + +把公钥拷贝到git服务器上,将我们刚刚生成的`id_rsa.pub`,拷贝到git服务器的`/home/git/.ssh/`目录 + +在git服务器上,将公钥添加到`authorized_keys` 文件中 + +``` +cd /home/git/.ssh/ +cat id_rsa.pub >> authorized_keys +``` + +如何看我们配置的密钥是否成功呢, 在客户端直接登录git服务器,看看是否是免密登陆 +``` +ssh git@git服务器ip +``` + +例如: + +``` +ssh git@127.0.0.1 +``` + +如果可以免密登录,那就说明服务器端密钥配置成功了 + +## 服务器端部署Git 仓库 + +我们在登陆到git 服务器端,切换为成 git账户 + +如果是root账户切换成git账户 +``` +su - git +``` + +如果是其他账户切换为git账户 +``` +sudo su - git +``` + +进入git目录下 +``` +cd ~/git +``` + +创建我们的第一个Git私服的仓库,我们叫它为world仓库 + +那么首先创建一个文件夹名为: world.git ,然后进入这个目录 + +有同学问,为什么文件夹名字后面要放`.git`, 其实不这样命名也是可以的 + +但是细心的同学可能注意到,我们平时在github上 `git clone` 其他人的仓库的时候,仓库名字后面,都是加上`.git`的 + +例如下面这个例子,其实就是github对仓库名称的一个命名规则,所以我们也遵守github的命名规则。 + +``` +git clone https://github.com/youngyangyang04/NoSQLAttack.git +``` + +所以我们的操作是 +``` +[git@localhost git]# mkdir world.git +[git@localhost git]# cd world.git +``` + +初始化我们的`world`仓库 + +``` +git init --bare + +``` + +**如果我们想创建多个仓库,就在这里创建多个文件夹并初始化就可以了,和world仓库的操作过程是一样一样的** + +现在我们服务端的git仓库就部署完了,接下来就看看客户端,如何使用这个仓库呢 + +## 客户端连接远程仓库 + +我们在自己的电脑上创建一个文件夹 也叫做`world`吧 + +其实这里命名是随意的,但是我们为了和git服务端的仓库名称保持同步。 这样更直观我们操作的是哪一个仓库。 + +``` +mkdir world +cd world +``` + +进入world文件,并初始化操作 + +``` +cd world +git init +``` + +在world目录上创建一个测试文件,并且将其添加到git版本管理中 + +``` +touch test +git add test +git commit -m "add test file" +``` + +将次仓库和远端仓库同步 + +``` +git remote add origin git@git服务器端的ip:world.git +git push -u origin master +``` + +此时这个test测试文件就已经提交到我们的git远端私服上了 + +## Git私服安全问题 + +这里有两点安全问题 + +### linux git的密码不要泄露出去 + +否则,别人可以通过 ssh git@git服务器IP 来登陆到你的git私服服务器上 + +当然了,这里同学们如果买的是云厂商的云服务器的话 + +如果有人恶意想通过 尝试不同密码链接的方式来链接你的服务器,重试三次以上 + +这个客户端的IP就会被封掉,同时邮件通知我们可以IP来自哪里 + +所以大可放心 密码只要我们不泄露出去,基本上不会有人同时不断尝试密码的方式来登上我们的git私服服务器 + +### 私钥文件`id_rsa` 不要给别人 + +如果有人得到了这个私钥,就可以免密码登陆我们的git私服上了,我相信大家也不至于把自己的私钥主动给别人吧 + +## 总结 + +这里就是整个git私服搭建的全过程,安全问题我也给大家列举了出来,接下来好好享受自己的Git私服吧 + +**enjoy!** + diff --git "a/problems/\345\211\215\345\272\217/kvstore.md" "b/problems/\345\211\215\345\272\217/kvstore.md" new file mode 100755 index 0000000000..268fc018c7 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/kvstore.md" @@ -0,0 +1,124 @@ + +# 手把手带你实现存储引擎 + + +之前在 [刷题攻略登上榜首](https://mp.weixin.qq.com/s/wZRTrA9Rbvgq1yEkSw4vfQ)这篇文章中说过,Carl不仅写了刷题攻略,还写了很多优秀的开源项目。 + +在星球里也有很多小伙伴问我关于一些,项目的选择,**相信如果是C++后台开发路线的话,基本都会去做WebServer 服务器**。 + +我在[知识星球](https://programmercarl.com/other/kstar.html)给小伙伴答疑,包括看了这么多简历,**发现WebServer这个项目是真的多,有点烂大街了**。 + +所以今天我把自己曾经开发的 KV存储引擎 给大家介绍一波,大家可以拿去当做自己的项目经验。 + +**相信只要是搞后端的同学应该都要熟悉非关系型数据库redis吧,那么应该知道redis的存储引擎是跳表实现的**。 + +现在很多云厂商提供的云数据库,其底层都是用了Facebook开源的rocksdb,而rocksdb的底层是Google开源的Levedb,**而Levedb的核心实现也是跳表**。 + +所以大家应该知道跳表的应用有多么的广泛了。 + +那么为什么这个项目非常合适大家用来做自己的项目经验呢? + +如果你是后端开发的话,你在简历上一定会写熟悉或者了解redis吧,那么可以进一步介绍一下自己的项目用跳表实现了redis核心引擎。 + +面试官一定会非常感兴趣的,然后你就可以和面试官侃侃而谈你是如何用跳表实现的这个KV存储引擎的。 + +**瞬间逼格就高了,有木有!** + +我在18年的时候,用跳表实现了一个轻量级KV存储引擎,代码也写的非常规范,熟悉我的录友应该知道,我的代码严格按照Google C++ style来的。 + +因为当时我是想把这个项目国际化的,注释和readme都是英文的,但最近我把这个项目又汉化回来了,方便大家理解。 + +给大家先随意看一段代码,我在注释中其实就已经在讲解跳表的运行原理了。代码使用了C++模板编程,这样接口支持任意类型的数据(包括自己自定义的类) + +![](https://file1.kamacoder.com/i/algo/20221104121454.png) + +项目地址:**https://github.com/youngyangyang04/Skiplist-CPP** + +这个项目中的代码质量是非常高的,如果无论是C++特性的运用,还是代码风格都是绝对拿得出手的! + +好了,牛逼吹完,然后给大家正式介绍一下这个项目 + +## KV存储引擎 + +本项目就是基于跳表实现的轻量级键值型存储引擎,使用C++实现。插入数据、删除数据、查询数据、数据展示、数据落盘、文件加载数据,以及数据库大小显示。 + +在随机写读情况下,该项目每秒可处理啊请求数(QPS): 24.39w,每秒可处理读请求数(QPS): 18.41w + +## 项目展示 + +![](https://file1.kamacoder.com/i/algo/20221104121509.png) + +文件功能: + +* main.cpp 包含skiplist.h使用跳表进行数据操作 +* skiplist.h 跳表核心实现 +* README.md 中文介绍 +* README-en.md 英文介绍 +* bin 生成可执行文件目录 +* makefile 编译脚本 +* store 数据落盘的文件存放在这个文件夹 +* stress_test_start.sh 压力测试脚本 +* LICENSE 使用协议 + + +## 提供接口 + +* insertElement(插入数据) +* deleteElement(删除数据) +* searchElement(查询数据) +* displayList(展示已存数据) +* dumpFile(数据落盘) +* loadFile(文件加载数据) +* size(返回数据规模) + +## 存储引擎数据表现 + +### 插入操作 + +跳表树高:18 + +采用随机插入数据测试: + + +|插入数据规模(万条) |耗时(秒) | +|---|---| +|10 |0.316763 | +|50 |1.86778 | +|100 |4.10648 | + + +每秒可处理写请求数(QPS): 24.39w + +### 取数据操作 + +|取数据规模(万条) |耗时(秒) | +|---|---| +|10|0.47148 |10| +|50|2.56373 |50| +|100|5.43204 |100| + +每秒可处理读请求数(QPS): 18.41w + +## 项目运行方式 + +``` +make // complie demo main.cpp +./bin/main // run +``` + +运行截图:(其中展示了插入数据,删除数据,展示数据等等功能) + +![](https://file1.kamacoder.com/i/algo/20221104121525.png) + +如果想自己写程序使用这个kv存储引擎,只需要在你的CPP文件中include skiplist.h 就可以了。 + +可以运行如下脚本测试kv存储引擎的性能(当然你可以根据自己的需求进行修改) + +``` +sh stress_test_start.sh +``` + +项目地址:**https://github.com/youngyangyang04/Skiplist-CPP** + +**大家白嫖的同时,别忘了给个star,fork,支持一波!** 录友如果最后拿到offer了,也别忘了和我道个喜哦。 + diff --git "a/problems/\345\211\215\345\272\217/server.md" "b/problems/\345\211\215\345\272\217/server.md" new file mode 100755 index 0000000000..890cf8bcd5 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/server.md" @@ -0,0 +1,129 @@ + +# 一台服务器有什么用! + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + +但在组织这场活动的时候,了解到大家都有一个共同的问题: **这个服务器究竟有啥用??** + +这真是一个好问题,而且我一句两句还说不清楚,所以就专门发文来讲一讲。 + +同时我还录制的一期视频,我的视频号,大家可以关注一波。 + + +一说到服务器,可能很多人都说搞分布式,做计算,搞爬虫,做程序后台服务,多人合作等等。 + +其实这些普通人都用不上,我来说一说大家能用上的吧。 + +## 搭建git私服 + +大家平时工作的时候一定有一个自己的工作文件夹,学生的话就是自己的课件,考试,准备面试的资料等等。 + +已经工作的录友,会有一个文件夹放着自己重要的文档,Markdown,图片,简历等等。 + +这么重要的文件夹,而且我们每天都要更新,也担心哪天电脑丢了,或者坏了,突然这些都不见了。 + +所以我们想备份嘛。 + +还有就是我们经常个人电脑和工作电脑要同步一些私人资料,而不是用微信传来传去。 + +这些都是git私服的使用场景,而且很好用。 + +大家也知道 github,gitee也可以搞私人仓库 用来备份,同步文件,但自己的文档可能放着很多重要的信息,包括自己的各种密码,密钥之类的,放到上面未必安全。你就不怕哪些重大bug把你的信息都泄漏了么[机智] + +更关键的是,github 和 gitee都限速的。毕竟人家的功能定位并不是网盘。 + +项目里有大文件(几百M以上),例如pdf,ppt等等 其上传和下载速度会让你窒息。 + +**后面我会发文专门来讲一讲,如何大家git私服!** + +## 搞一个文件存储 + +这个可以用来生成文件的下载链接,也可以把本地文件传到服务器上。 + +相当于自己做一个对象存储,其实云厂商也有对象存储的产品。 + +不过我们自己也可以做一个,不够很多很同学应该都不知道对象存储怎么用吧,其实我们用服务器可以自己做一个类似的公司。 + +我现在就用自己用go写的一个工具,部署在服务器上。 用来和服务器传文件,或者生成一些文件的临时下载链接。 + +这些都是直接命令行操作的, + +操作方式这样,我把命令包 包装成一个shell命令,想传那个文件,直接 uploadtomyserver,然后就返回可以下载的链接,这个文件也同时传到了我的服务器上。 + +![](https://file1.kamacoder.com/i/algo/20211126165643.png) + +我也把我的项目代码放在了github上: + +https://github.com/youngyangyang04/fileHttpServer + +感兴趣的录友可以去学习一波,顺便给个star。 + + +## 网站 + +做网站,例如 大家知道用html 写几行代码,就可以生成一个网页,但怎么给别人展示呢? + +大家如果用自己的电脑做服务器,只能同一个路由器下的设备可以访问你的网站,可能这个设备出了这个屋子 都访问不了你的网站了。 + +因为你的IP不是公网IP。 + +如果有了一台云服务器,都是配公网IP,你的网站就可以让任何人访问了。 + +或者说 你提供的一个服务就可以让任何人使用。 + +例如第二个例子中,我们可以自己开发一个文件存储,这个服务,我只把把命令行给其他人,其他人都可以使用我的服务来生成链接,当然他们的文件也都传到了我的服务器上。 + +再说一个使用场景。 + +我之前在组织免费里服务器的活动的时候,阿里云给我一个excel,让面就是从我这里买服务器录友的名单,我直接把这个名单甩到群里,让大家自己检查,出现在名单里就可以找我返现,这样做是不是也可以。 + +这么做有几个很大的问题: +* 大家都要去下载excel,做对比,会有人改excel的内容然后就说是从你这里买的,我不可能挨个去比较excel有没有改动 +* excel有其他人的个人信息,这是不能暴漏的。 +* 如果每个人自己用excel查询,私信我返现,一个将近两千人找我返现,我微信根本处理不过来,这就变成体力活了。 + +那应该怎么做呢, + +我就简单写一个查询的页面,后端逻辑就是读一个execel表格,大家在查询页面输入自己的阿里云ID,如果在excel里,页面就会返回返现群的二维码,大家就可以自主扫码加群了。 + +这样,我最后就直接在返现群里 发等额红包就好了,是不是极大降低人力成本了 + +当然我是把 17个返现群的二维码都生成好了,按照一定的规则,展现给查询通过的录友。 + +就是这样一个非常普通的查询页面。 + +![](https://file1.kamacoder.com/i/algo/20211126160200.png) + +查询通过之后,就会展现返现群二维码。 + +![](https://file1.kamacoder.com/i/algo/20211127160558.png) + +但要部署在服务器上,因为没有公网IP,别人用不了你的服务。 + + +## 学习linux + +学习linux其实在自己的电脑上搞一台虚拟机,或者安装双系统也可以学习,不过这很考验你的电脑性能如何了。 + +如果你有一个服务器,那就是独立的一台电脑,你怎么霍霍就怎么霍霍,而且一年都不用关机的,可以一直跑你的任务,和你本地电脑也完全隔离。 + +更方便的是,你目前系统假如是CentOS,想做一个实验需要在Ubuntu上,如果是云服务器,更换系统就是在 后台点一下,一键重装,云厂商基本都是支持所有系统一件安装的。 + +我们平时自己玩linux经常是配各种环境,然后这个linux就被自己玩坏了(一般都是毫无节制使用root权限导致的),总之就是环境配不起来了,基本就要重装了。 + +那云服务器重装系统可太方便了。 + +还有就是加入你好不容易配好的环境,如果以后把这个环境玩坏了,你先回退这之前配好的环境而不是重装系统在重新配一遍吧。 + +那么可以用云服务器的镜像保存功能,就是你配好环境的那一刻就可以打一个镜像包,以后如果环境坏了,直接回退到上次镜像包的状态,这是不是就很香了。 + + +## 总结 + +其实云服务器还有很多其他用处,不过我就说一说大家普遍能用的上的。 + + +* [阿里云活动期间服务器购买](https://www.aliyun.com/minisite/goods?taskCode=shareNew2205&recordId=3641992&userCode=roof0wob) +* [腾讯云活动期间服务器购买](https://curl.qcloud.com/EiaMXllu) + diff --git "a/problems/\345\211\215\345\272\217/vim.md" "b/problems/\345\211\215\345\272\217/vim.md" new file mode 100644 index 0000000000..3f1daa53aa --- /dev/null +++ "b/problems/\345\211\215\345\272\217/vim.md" @@ -0,0 +1,103 @@ +# 人生苦短,我用VIM!| 最强vim配置 + +> Github地址:[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim) +> Gitee地址:[https://gitee.com/programmercarl/power-vim](https://gitee.com/programmercarl/power-vim) + +熟悉我的录友,应该都知道我是vim流,无论是写代码还是写文档(Markdown),都是vim,都没用IDE。 + +但这里我并不是说IDE不好用,IDE在 代码跟踪,引用跳转等等其实是很给力的,效率比vim高。 + +我用vim的话,如果需要跟踪代码的话,就用ctag去跳转,虽然很不智能(是基于规则匹配,不是语义匹配),但加上我自己的智能就也能用(这里真的要看对代码的把握程度了) + +所以连跟踪代码都不用IDE的话,其他方面那我就更用不上IDE了。 + +## 为什么用VIM + +**至于写代码的效率,VIM完爆IDE**,其他不说,就使用IDE每次还要去碰鼠标,就很让人烦心!(真凸显了程序员的执着) + +这里说一说vim的方便之处吧,搞后端开发的同学,都得玩linux吧,在linux下写代码,如果不会vim的话,会非常难受。 + +日常我们的开发机,线上服务器,预发布服务器,都是远端linux,需要跳板机连上去,进行操作,如果不会vim,每次都把代码拷贝到本地,修改编译,在传到远端服务器,还真的麻烦。 + +使用VIM的话,本地,服务器,开发机,一刀流,无缝切换,爽不。 + +IDE那么很吃内存,打开个IDE卡半天,用VIM就很轻便了,秒开! + +而且在我们日常开发中,工作年头多了,都会发现没有纯粹的C++,Java开发啥的,就是 C++也得写,Java也得写,有时候写Go起个http服务,写Python处理一下数据,写shell搞个自动部署,编译啥的。 **总是就是啥语言就得写,一些以项目需求为导向!** + +写语言还要切换不同的IDE,熟悉不同的操作规则,想想是不是很麻烦。 + +听说好像现在有的IDE可以支持很多语言了,这个我还不太了解,但能确定的是,IDE支持的语言再多,也不会有vim多。 + +**因为vim是编辑器!**,什么都可以写,不同的语言做一下相应的配置就好,写起来都是一样的顺畅。 + +应该不少录友感觉vim上快捷键太多了,根本记不过来,其实这和我看IDE是一样的想法,我看IDE上哪些按钮一排一排的也太多了,我都记不过来,所以索性一套vim流 扫遍所有代码,它不香么。 + +而且IDE集成编译、调试、智能补全、语法高亮、工程管理等等,隐藏了太多细节,使用vim,就都自己配置,想支持什么语言就自己配置,想怎么样就怎么样,需要什么就补什么,这不是很酷么? + +可能有的同学感觉什么都要自己配置,有点恐惧。但一旦配置好的就非常舒服了。 + +**其实工程师就要逢山开路遇水搭桥,这也是最基本的素质!** + +从头打在一个自己的开发利器,再舒服不过了。 + +## PowerVim + +这里给大家介绍一下我的vim配置吧,**这套vim配置我已经打磨了将近四年**,不断调整优化,已经可以完全满足工业级打开的需求了。 + +所以我给它起名为PowerVim。一个真正强大的vim。 + +``` + _____ __ ___ + | __ \ \ \ / (_) + | |__) |____ _____ _ _\ \ / / _ _ __ ___ + | ___/ _ \ \ /\ / / _ \ '__\ \/ / | | '_ ` _ \ + | | | (_) \ V V / __/ | \ / | | | | | | | + |_| \___/ \_/\_/ \___|_| \/ |_|_| |_| |_| +``` + +这个配置我开源在Github上,地址:[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim) + + + +来感受一下PowerVim的使用体验,看起来很酷吧!注意这些操作都不用鼠标的,一波键盘控制流!所以我平时写代码是不碰鼠标的! + +![](https://file1.kamacoder.com/i/algo/vim_overview.gif) + +## 安装 + +PowerVim的安装非常简单,我已经写好了安装脚本,只要执行以下就可以安装,而且不会影响你之前的vim配置,之前的配置都给做了备份,大家看一下脚本就知道备份在哪里了。 + +安装过程非常简单: +```bash +git clone https://github.com/youngyangyang04/PowerVim.git +cd PowerVim +sh install.sh +``` + +## 特性 + +目前PowerVim支持如下功能,这些都是自己配置的: + +* CPP、PHP、JAVA代码补全,如果需要其他语言补全,可自行配置关键字列表在PowerVim/.vim/dictionary目录下 +* 显示文件函数变量列表 +* MiniBuf显示打开过的文件 +* 语法高亮支持C++ (including C++11)、 Go、Java、 Php、 Html、 Json 和 Markdown +* 显示git状态,和主干或分支的添加修改删除的情况 +* 显示项目文件目录,方便快速打开 +* 快速注释,使用gcc注释当前行,gc注释选中的块 +* 项目内搜索关键字和文件夹 +* 漂亮的颜色搭配和状态栏显示 + +## 最后 + +当然 还有很多,我还详细写了PowerVim的快捷键,使用方法,插件,配置,等等,都在Github主页的README上。当时我的Github上写的都是英文README,这次为了方便大家阅读,我又翻译成中文README。 + +![](https://file1.kamacoder.com/i/algo/20211013102249.png) + +Github地址:[https://github.com/youngyangyang04/PowerVim](https://github.com/youngyangyang04/PowerVim) + +Gitee地址:[https://gitee.com/programmercarl/power-vim](https://gitee.com/programmercarl/power-vim) + +最后,因为这个vim配置因为我一直没有宣传,所以star数量很少,录友们去给个star吧,真正的开发利器,值得顶起来! + diff --git "a/problems/\345\211\215\345\272\217/\344\273\243\347\240\201\351\243\216\346\240\274.md" "b/problems/\345\211\215\345\272\217/\344\273\243\347\240\201\351\243\216\346\240\274.md" new file mode 100644 index 0000000000..db54fcb3e7 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\344\273\243\347\240\201\351\243\216\346\240\274.md" @@ -0,0 +1,137 @@ + +# 看了这么多代码,谈一谈代码风格! + +最近看了很多录友在[leetcode-master](https://mp.weixin.qq.com/s/wZRTrA9Rbvgq1yEkSw4vfQ)上提交的代码,发现很多录友的代码其实并不规范,这一点平时在交流群和知识星球里也能看出来。 + +很多录友对代码规范应该了解得不多,代码看起来并不舒服。 + +所以呢,我给大家讲一讲代码规范,我主要以C++代码为例。 + +需要强调一下,代码规范并不是仅仅是让代码看着舒服,这是一个很重要的习惯。 + +## 题外话 + +工作之后,**特别是在大厂,看谁的技术牛不牛逼,不用看谁写出多牛逼的代码,就代码风格扫一眼,立刻就能看出来是正规军还是野生程序员**。 + +很多人甚至不屑于了解代码规范,认为实现功能就行,这种观点其实在上个世纪是很普遍的,因为那时候一般写代码不需要合作,自己一个人撸整个项目,想怎么写就怎么写。 + +现在一些小公司,甚至大公司里的某些技术团队也不注重代码规范,赶进度撸出功能就完事,这种情况就要分两方面看: + +* 第一种情况:这个项目在业务上具有巨大潜力,需要抢占市场,只要先站住市场就能赚到钱,每年年终好几十万,那项目前期还关心啥代码风格,赶进度把功能撸出来,赚钱就完事了,例如12年的微信,15年的王者荣耀。这些项目都是后期再不断优化的。 + +* 第二种情况:这个项目没赚到钱,半死不活的,代码还没有设计也没有规范,这样对技术人员的伤害就非常大了。 + +**而不注重代码风格的团队,99.99%都是第二种情况**,如果你赶上了第一种情况,那就恭喜你了,本文下面的内容可以不用看了。 + +## 代码规范 + +### 变量命名 + +这里我简单说一说规范问题。 + +**权威的C++规范以Google为主**,我给大家下载了一份中文版本,在公众号「代码随想录」后台回复:编程规范,就可以领取。 + +**具体的规范要以自己团队风格为主**,融入团队才是最重要的。 + +我先来说说变量的命名。 + +主流有如下三种变量规则: + +* 小驼峰、大驼峰命名法 +* 下划线命名法 +* 匈牙利命名法 + +小驼峰,第一个单词首字母小写,后面其他单词首字母大写。例如 `int myAge;` + +大驼峰法把第一个单词的首字母也大写了。例如:``int MyAge;`` + +通常来讲 java和go都使用驼峰,C++的函数和结构体命名也是用大驼峰,**大家可以看到题解中我的C++代码风格就是小驼峰,因为leetcode上给出的默认函数的命名就是小驼峰,所以我入乡随俗**。 + +下划线命名法是名称中的每一个逻辑断点都用一个下划线来标记,例如:`int my_age`,**下划线命名法是随着C语言的出现流行起来的,如果大家看过UNIX高级编程或者UNIX网络编程,就会发现大量使用这种命名方式**。 + +匈牙利命名法是:变量名 = 属性 + 类型 + 对象描述,例如:`int iMyAge`,这种命名是一个来此匈牙利的程序员在微软内部推广起来,然后推广给了全世界的Windows开发人员。 + +这种命名方式在没有IDE的时代,可以很好的提醒开发人员遍历的意义,例如看到iMyAge,就知道它是一个int型的变量,而不用找它的定义,缺点是一旦改变变量的属性,那么整个项目里这个变量名字都要改动,所以带来代码维护困难。 + +**目前IDE已经很发达了,都不用标记变量属性了,IDE就会帮我们识别了,所以基本没人用匈牙利命名法了**,虽然我不用IDE,VIM大法好。 + +我做了一下总结如图: + +![编程风格](https://file1.kamacoder.com/i/algo/20201119173039835.png) + +### 水平留白(代码空格) + +经常看到有的同学的代码都堆在一起,看起来都费劲,或者是有的间隔有空格,有的没有空格,很不统一,有的同学甚至为了让代码精简,把所有空格都省略掉了。 + +大家如果注意我题解上的代码风格,我的空格都是有统一规范的。 + +**我所有题解的C++代码,都是严格按照Google C++编程规范来的,这样代码看起来就让人感觉清爽一些**。 + +我举一些例子: + +操作符左右一定有空格,例如 +``` +i = i + 1; +``` + +分隔符(`,` 和`;`)前一位没有空格,后一位保持空格,例如: + +``` +int i, j; +for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) +``` + +大括号和函数保持同一行,并有一个空格例如: + +``` +while (n) { + n--; +} +``` + +控制语句(while,if,for)后都有一个空格,例如: +``` +while (n) { + if (k > 0) return 9; + n--; +} +``` + +以下是我刚写的力扣283.移动零的代码,大家可以看一下整体风格,注意空格的细节! +```CPP +class Solution { +public: + void moveZeroes(vector& nums) { + int slowIndex = 0; + for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { + if (nums[fastIndex] != 0) { + nums[slowIndex++] = nums[fastIndex]; + } + } + for (int i = slowIndex; i < nums.size(); i++) { + nums[i] = 0; + } + } +}; +``` + +这里关于大括号是否要重启一行? + +Google规范是 大括号和 控制语句保持同一行的,我个人也很认可这种写法,因为可以缩短代码的行数,特别是项目中代码行数很多的情况下,这种写法是可以提高阅读代码的效率。 + +当然我并不是说一定要按照Google的规范来,**代码风格其实统一就行,没有严格的说谁对谁错**。 + +## 总结 + +如果还是学生,使用C++的话,可以按照题解中我的代码风格来,还是比较标准的。 + +如果不是C++就自己选一种代码风格坚持下来, + +如果已经工作的录友,就要融入团队的代码风格了,团队怎么写,自己就怎么来,毕竟不是一个人在战斗。 + +就酱,以后我还会陆续分享,关于代码,求职,学习工作之类的内容。 + + +----------------------- + +
diff --git "a/problems/\345\211\215\345\272\217/\345\206\205\345\255\230\346\266\210\350\200\227.md" "b/problems/\345\211\215\345\272\217/\345\206\205\345\255\230\346\266\210\350\200\227.md" new file mode 100644 index 0000000000..2be0bea5c1 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\345\206\205\345\255\230\346\266\210\350\200\227.md" @@ -0,0 +1,148 @@ + +# 刷了这么多题,你了解自己代码的内存消耗么? + +理解代码的内存消耗,最关键是要知道自己所用编程语言的内存管理。 + +## 不同语言的内存管理 + +不同的编程语言各自的内存管理方式。 + +* C/C++这种内存堆空间的申请和释放完全靠自己管理 +* Java 依赖JVM来做内存管理,不了解jvm内存管理的机制,很可能会因一些错误的代码写法而导致内存泄漏或内存溢出 +* Python内存管理是由私有堆空间管理的,所有的python对象和数据结构都存储在私有堆空间中。程序员没有访问堆的权限,只有解释器才能操作。 + +例如Python万物皆对象,并且将内存操作封装的很好,**所以python的基本数据类型所用的内存会要远大于存放纯数据类型所占的内存**,例如,我们都知道存储int型数据需要四个字节,但是使用Python 申请一个对象来存放数据的话,所用空间要远大于四个字节。 + +## C++的内存管理 + +以C++为例来介绍一下编程语言的内存管理。 + +如果我们写C++的程序,就要知道栈和堆的概念,程序运行时所需的内存空间分为 固定部分,和可变部分,如下: + +![C++内存空间](https://file1.kamacoder.com/i/algo/20210309165950660.png) + +固定部分的内存消耗 是不会随着代码运行产生变化的, 可变部分则是会产生变化的 + +更具体一些,一个由C/C++编译的程序占用的内存分为以下几个部分: + +* 栈区(Stack) :由编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈。 +* 堆区(Heap) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS收回 +* 未初始化数据区(Uninitialized Data): 存放未初始化的全局变量和静态变量 +* 初始化数据区(Initialized Data):存放已经初始化的全局变量和静态变量 +* 程序代码区(Text):存放函数体的二进制代码 + +代码区和数据区所占空间都是固定的,而且占用的空间非常小,那么看运行时消耗的内存主要看可变部分。 + +在可变部分中,栈区间的数据在代码块执行结束之后,系统会自动回收,而堆区间数据是需要程序员自己回收,所以也就是造成内存泄漏的发源地。 + +**而Java、Python的话则不需要程序员去考虑内存泄漏的问题,虚拟机都做了这些事情**。 + +## 如何计算程序占用多大内存 + +想要算出自己程序会占用多少内存就一定要了解自己定义的数据类型的大小,如下: + +![C++数据类型的大小](https://file1.kamacoder.com/i/algo/20200804193045440.png) + +注意图中有两个不一样的地方,为什么64位的指针就占用了8个字节,而32位的指针占用4个字节呢? + +1个字节占8个比特,那么4个字节就是32个比特,可存放数据的大小为2^32,也就是4G空间的大小,即:可以寻找4G空间大小的内存地址。 + +大家现在使用的计算机一般都是64位了,所以编译器也都是64位的。 + +安装64位的操作系统的计算机内存都已经超过了4G,也就是指针大小如果还是4个字节的话,就已经不能寻址全部的内存地址,所以64位编译器使用8个字节的指针才能寻找所有的内存地址。 + +注意2^64是一个非常巨大的数,对于寻找地址来说已经足够用了。 + +## 内存对齐 + +再介绍一下内存管理中另一个重要的知识点:**内存对齐**。 + +**不要以为只有C/C++才会有内存对齐,只要可以跨平台的编程语言都需要做内存对齐,Java、Python都是一样的**。 + +而且这是面试中面试官非常喜欢问到的问题,就是:**为什么会有内存对齐?** + +主要是两个原因 + +1. 平台原因:不是所有的硬件平台都能访问任意内存地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。为了同一个程序可以在多平台运行,需要内存对齐。 + +2. 硬件原因:经过内存对齐后,CPU访问内存的速度大大提升。 + +可以看一下这段C++代码输出的各个数据类型大小是多少? + +```CPP +struct node{ + int num; + char cha; +}st; +int main() { + int a[100]; + char b[100]; + cout << sizeof(int) << endl; + cout << sizeof(char) << endl; + cout << sizeof(a) << endl; + cout << sizeof(b) << endl; + cout << sizeof(st) << endl; +} +``` + +看一下和自己想的结果一样么, 我们来逐一分析一下。 + +其输出的结果依次为: + +``` +4 +1 +400 +100 +8 +``` + +此时会发现,和单纯计算字节数的话是有一些误差的。 + +这就是因为内存对齐的原因。 + +来看一下内存对齐和非内存对齐产生的效果区别。 + +CPU读取内存不是一次读取单个字节,而是一块一块的来读取内存,块的大小可以是2,4,8,16个字节,具体取多少个字节取决于硬件。 + +假设CPU把内存划分为4字节大小的块,要读取一个4字节大小的int型数据,来看一下这两种情况下CPU的工作量: + +第一种就是内存对齐的情况,如图: + +![内存对齐](https://file1.kamacoder.com/i/algo/20200804193307347.png) + +一字节的char占用了四个字节,空了三个字节的内存地址,int数据从地址4开始。 + +此时,直接将地址4,5,6,7处的四个字节数据读取到即可。 + +第二种是没有内存对齐的情况如图: + +![非内存对齐](https://file1.kamacoder.com/i/algo/20200804193353926.png) + +char型的数据和int型的数据挨在一起,该int数据从地址1开始,那么CPU想要读这个数据的话来看看需要几步操作: + +1. 因为CPU是四个字节四个字节来寻址,首先CPU读取0,1,2,3处的四个字节数据 +2. CPU读取4,5,6,7处的四个字节数据 +3. 合并地址1,2,3,4处四个字节的数据才是本次操作需要的int数据 + +此时一共需要两次寻址,一次合并的操作。 + +**大家可能会发现内存对齐岂不是浪费的内存资源么?** + +是这样的,但事实上,相对来说计算机内存资源一般都是充足的,我们更希望的是提高运行速度。 + +**编译器一般都会做内存对齐的优化操作,也就是说当考虑程序真正占用的内存大小的时候,也需要认识到内存对齐的影响**。 + + +## 总结 + +不少同学对这方面的知识很欠缺,基本处于盲区,通过这一篇大家可以初步补齐一下这块。 + +之后也可以有意识的去学习自己所用的编程语言是如何管理内存的,这些也是程序员的内功。 + + + + +----------------------- + +
diff --git "a/problems/\345\211\215\345\272\217/\345\210\267\345\212\233\346\211\243\347\224\250\344\270\215\347\224\250\345\272\223\345\207\275\346\225\260.md" "b/problems/\345\211\215\345\272\217/\345\210\267\345\212\233\346\211\243\347\224\250\344\270\215\347\224\250\345\272\223\345\207\275\346\225\260.md" new file mode 100644 index 0000000000..7d0e34757b --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\345\210\267\345\212\233\346\211\243\347\224\250\344\270\215\347\224\250\345\272\223\345\207\275\346\225\260.md" @@ -0,0 +1,28 @@ +# 究竟什么时候用库函数,什么时候要自己实现 + +在[知识星球](https://programmercarl.com/other/kstar.html)里有录友问我,刷题究竟要不要用库函数? 刷题的时候总是禁不住库函数的诱惑,如果都不用库函数一些题目做起来还很麻烦。 + +估计不少录友都有这个困惑,我来说一说对于库函数的使用。 + +一些同学可能比较喜欢看力扣上直接调用库函数的评论和题解,**其实我感觉娱乐一下还是可以的,但千万别当真,别沉迷!** + +例如:[字符串:151. 翻转字符串里的单词](https://programmercarl.com/0151.%E7%BF%BB%E8%BD%AC%E5%AD%97%E7%AC%A6%E4%B8%B2%E9%87%8C%E7%9A%84%E5%8D%95%E8%AF%8D.html)这道题目本身是综合考察同学们对字符串的处理能力,如果 split + reverse的话,那就失去了题目的意义了。 + +有的同学可能不屑于实现这么简单的功能,直接调库函数完事,把字符串分成一个个单词,一想就是那么一回事,多简单。 + +相信我,很多面试题都是一想很简单,实现起来一堆问题。 所以刷力扣本来就是为面试,也为了提高自己的代码能力,扎实一点没坏处。 + +**那么平时写算法题目就全都不用库函数了么?** + +当然也不是,这里我给大家提供一个标准。 + +**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数**。 + +**如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,那么直接用库函数。** + +使用库函数最大的忌讳就是不知道这个库函数怎么实现的,也不知道其时间复杂度,上来就用,这样写出来的算法,时间复杂度自己都掌握不好的。 + +例如for循环里套一个字符串的insert,erase之类的操作,你说时间复杂度是多少呢,很明显是O(n^2)的时间复杂度了。 + +在刷题的时候本着我说的标准来使用库函数,相信对大家会有所帮助! + diff --git "a/problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214.md" "b/problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214.md" new file mode 100644 index 0000000000..5e91198948 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\345\212\233\346\211\243\344\270\212\347\232\204\344\273\243\347\240\201\345\234\250\346\234\254\345\234\260\347\274\226\350\257\221\350\277\220\350\241\214.md" @@ -0,0 +1,64 @@ +# 力扣上的代码想在本地编译运行? + + +很多录友都问过我一个问题,就是力扣上的代码如何在本地编译运行? + +其实在代码随想录刷题群里也经常出现这个场景,就是录友发一段代码上来,问大家这个代码怎么有问题? 如果我看到了一般我的回复:都是把那几个变量或者数组打印一下看看对不对,就知道了。 + +然后录友就问了:如何打日志呢? + +其实在力扣上打日志也挺方便的,我一般调试就是直接在力扣上打日志,偶尔需要把代码粘到本地来运行添加日志debug一下。 + +在力扣上直接打日志,这个就不用讲,C++的话想打啥直接cout啥就可以了。 + +我来说一说力扣代码如何在本地运行。 + +毕竟我们天天用力扣刷题,也应该知道力扣上的代码如何在本地编译运行。 + +其实挺简单的,大家看一遍就会了。 + +我拿我们刚讲过的这道题[动态规划:使用最小花费爬楼梯](https://programmercarl.com/0746.使用最小花费爬楼梯.html)来做示范。 + +力扣746. 使用最小花费爬楼梯,完整的可以在直接本地运行的C++代码如下: + +```CPP +#include +#include +using namespace std; + +class Solution { +public: + int minCostClimbingStairs(vector& cost) { + vector dp(cost.size()); + dp[0] = cost[0]; + dp[1] = cost[1]; + for (int i = 2; i < cost.size(); i++) { + dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; + } + return min(dp[cost.size() - 1], dp[cost.size() - 2]); + } +}; + +int main() { + int a[] = {1, 100, 1, 1, 1, 100, 1, 1, 100, 1}; + vector cost(a, a + sizeof(a) / sizeof(int)); + Solution solution; + cout << solution.minCostClimbingStairs(cost) << endl; +} +``` + +大家可以拿去跑一跑,直接粘到编译器上就行了。 + +我用的是linux下gcc来编译的,估计粘到其他编译器也没问题。 + +代码中可以看出,其实就是定义个main函数,构造个输入用例,然后定义一个solution变量,调用minCostClimbingStairs函数就可以了。 + +此时大家就可以随意构造测试数据,然后想怎么打日志就怎么打日志,没有找不出的bug。 + + + + + + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" "b/problems/\345\211\215\345\272\217/\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" new file mode 100644 index 0000000000..4252fc8779 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" @@ -0,0 +1,167 @@ + +# 关于时间复杂度,你不知道的都在这里! + +相信每一位录友都接触过时间复杂度,但又对时间复杂度的认识处于一种朦胧的状态,所以是时候对时间复杂度来一个深度的剖析了。 + +本篇从如下六点进行分析: + +* 究竟什么是时间复杂度 +* 什么是大O +* 不同数据规模的差异 +* 复杂表达式的化简 +* O(log n)中的log是以什么为底? +* 举一个例子 + + +这可能是你见过对时间复杂度分析最通透的一篇文章。 + +## 究竟什么是时间复杂度 + +**时间复杂度是一个函数,它定性描述该算法的运行时间**。 + +我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的大体时间。 + +那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认CPU的每个单元运行消耗的时间都是相同的。 + +假设算法的问题规模为n,那么操作单元数量便用函数f(n)来表示,随着数据规模n的增大,算法执行时间的增长率和f(n)的增长率相同,这称作为算法的渐近时间复杂度,简称时间复杂度,记为 O(f(n))。 + +## 什么是大O + +这里的大O是指什么呢,说到时间复杂度,**大家都知道O(n),O(n^2),却说不清什么是大O**。 + +算法导论给出的解释:**大O用来表示上界的**,当用它作为算法的最坏情况运行时间的上界,就是对任意数据输入的运行时间的上界。 + +同样算法导论给出了例子:拿插入排序来说,插入排序的时间复杂度我们都说是O(n^2) 。 + +输入数据的形式对程序运算时间是有很大影响的,在数据本来有序的情况下时间复杂度是O(n),但如果数据是逆序的话,插入排序的时间复杂度就是O(n^2),也就对于所有输入情况来说,最坏是O(n^2) 的时间复杂度,所以称插入排序的时间复杂度为O(n^2)。 + +同样的同理再看一下快速排序,都知道快速排序是O(nlogn),但是当数据已经有序情况下,快速排序的时间复杂度是O(n^2) 的,**所以严格从大O的定义来讲,快速排序的时间复杂度应该是O(n^2)**。 + +**但是我们依然说快速排序是O(nlogn)的时间复杂度,这个就是业内的一个默认规定,这里说的O代表的就是一般情况,而不是严格的上界**。如图所示: +![时间复杂度4,一般情况下的时间复杂度](https://file1.kamacoder.com/i/algo/20200728185745611-20230310123844306.png) + +我们主要关心的还是一般情况下的数据形式。 + +**面试中说的算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 + + +## 不同数据规模的差异 + +如下图中可以看出不同算法的时间复杂度在不同数据输入规模下的差异。 + +![时间复杂度,不同数据规模的差异](https://file1.kamacoder.com/i/algo/20200728191447384-20230310124015324.png) + +在决定使用哪些算法的时候,不是时间复杂越低的越好(因为简化后的时间复杂度忽略了常数项等等),要考虑数据规模,如果数据规模很小甚至可以用O(n^2)的算法比O(n)的更合适(在有常数项的时候)。 + +就像上图中 O(5n^2) 和 O(100n) 在n为20之前 很明显 O(5n^2)是更优的,所花费的时间也是最少的。 + +那为什么在计算时间复杂度的时候要忽略常数项系数呢,也就说O(100n) 就是O(n)的时间复杂度,O(5n^2) 就是O(n^2)的时间复杂度,而且要默认O(n) 优于O(n^2) 呢 ? + +这里就又涉及到大O的定义,**因为大O就是数据量级突破一个点且数据量级非常大的情况下所表现出的时间复杂度,这个数据量也就是常数项系数已经不起决定性作用的数据量**。 + +例如上图中20就是那个点,n只要大于20 常数项系数已经不起决定性作用了。 + +**所以我们说的时间复杂度都是省略常数项系数的,是因为一般情况下都是默认数据规模足够的大,基于这样的事实,给出的算法时间复杂度的一个排行如下所示**: + +O(1)常数阶 < O(logn)对数阶 < O(n)线性阶 < O(nlogn)线性对数阶 < O(n^2)平方阶 < O(n^3)立方阶 < O(2^n)指数阶 + +但是也要注意大常数,如果这个常数非常大,例如10^7 ,10^9 ,那么常数就是不得不考虑的因素了。 + +## 复杂表达式的化简 + +有时候我们去计算时间复杂度的时候发现不是一个简单的O(n) 或者O(n^2), 而是一个复杂的表达式,例如: + +``` +O(2*n^2 + 10*n + 1000) +``` + +那这里如何描述这个算法的时间复杂度呢,一种方法就是简化法。 + +去掉运行时间中的加法常数项 (因为常数项并不会因为n的增大而增加计算机的操作次数)。 + +``` +O(2*n^2 + 10*n) +``` + +去掉常数系数(上文中已经详细讲过为什么可以去掉常数项的原因)。 + +``` +O(n^2 + n) +``` + +只保留保留最高项,去掉数量级小一级的n (因为n^2 的数据规模远大于n),最终简化为: + +``` +O(n^2) +``` + +如果这一步理解有困难,那也可以做提取n的操作,变成O(n(n+1)) ,省略加法常数项后也就别变成了: + +``` +O(n^2) +``` + +所以最后我们说:这个算法的算法时间复杂度是O(n^2) 。 + + +也可以用另一种简化的思路,其实当n大于40的时候, 这个复杂度会恒小于O(3 × n^2), +O(2 × n^2 + 10 × n + 1000) < O(3 × n^2),所以说最后省略掉常数项系数最终时间复杂度也是O(n^2)。 + +## O(logn)中的log是以什么为底? + +平时说这个算法的时间复杂度是logn的,那么一定是log 以2为底n的对数么? + +其实不然,也可以是以10为底n的对数,也可以是以20为底n的对数,**但我们统一说 logn,也就是忽略底数的描述**。 + +为什么可以这么做呢?如下图所示: + +![时间复杂度1.png](https://file1.kamacoder.com/i/algo/20200728191447349-20230310124032001.png) + + +假如有两个算法的时间复杂度,分别是log以2为底n的对数和log以10为底n的对数,那么这里如果还记得高中数学的话,应该不难理解`以2为底n的对数 = 以2为底10的对数 * 以10为底n的对数`。 + +而以2为底10的对数是一个常数,在上文已经讲述了我们计算时间复杂度是忽略常数项系数的。 + +抽象一下就是在时间复杂度的计算过程中,log以i为底n的对数等于log 以j为底n的对数,所以忽略了i,直接说是logn。 + +这样就应该不难理解为什么忽略底数了。 + +## 举一个例子 + +通过这道面试题目,来分析一下时间复杂度。题目描述:找出n个字符串中相同的两个字符串(假设这里只有两个相同的字符串)。 + +如果是暴力枚举的话,时间复杂度是多少呢,是O(n^2)么? + +这里一些同学会忽略了字符串比较的时间消耗,这里并不像int 型数字做比较那么简单,除了n^2 次的遍历次数外,字符串比较依然要消耗m次操作(m也就是字母串的长度),所以时间复杂度是O(m × n × n)。 + +接下来再想一下其他解题思路。 + +先排对n个字符串按字典序来排序,排序后n个字符串就是有序的,意味着两个相同的字符串就是挨在一起,然后在遍历一遍n个字符串,这样就找到两个相同的字符串了。 + +那看看这种算法的时间复杂度,快速排序时间复杂度为O(nlogn),依然要考虑字符串的长度是m,那么快速排序每次的比较都要有m次的字符比较的操作,就是O(m × n × log n) 。 + +之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 O(m × n × logn + n × m)。 + +我们对O(m × n × log n + n × m) 进行简化操作,把m × n提取出来变成 O(m × n × (logn + 1)),再省略常数项最后的时间复杂度是 O(m × n × log n)。 + +最后很明显O(m × n × logn) 要优于O(m × n × n)! + +所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。 + +这就是我们通过分析两种算法的时间复杂度得来的。 + +**当然这不是这道题目的最优解,我仅仅是用这道题目来讲解一下时间复杂度**。 + +## 总结 + +本篇讲解了什么是时间复杂度,复杂度是用来干什么,以及数据规模对时间复杂度的影响。 + +还讲解了被大多数同学忽略的大O的定义以及log究竟是以谁为底的问题。 + +再分析了如何简化复杂的时间复杂度,最后举一个具体的例子,把本篇的内容串起来。 + +相信看完本篇,大家对时间复杂度的认识会深刻很多! + + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\345\206\231\346\226\207\346\241\243\345\267\245\345\205\267.md" "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\345\206\231\346\226\207\346\241\243\345\267\245\345\205\267.md" new file mode 100644 index 0000000000..c991be1542 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\345\206\231\346\226\207\346\241\243\345\267\245\345\205\267.md" @@ -0,0 +1,133 @@ + +# 程序员应该用什么用具来写文档? + +

欢迎大家参与本项目,贡献其他语言版本的代码,拥抱开源,让更多学习算法的小伙伴们收益!

+ + + +Carl平时写东西,都是统一使用markdown,包括题解啊,笔记啊,所以这里给大家安利一波markdown对程序员的重要性! + +程序员为什么要学习markdown呢? + +**一个让你难以拒绝的理由:markdown可以让你养成了记录的习惯**。 + +自从使用了markdown之后,就喜欢了写文档无法自拔,包括记录工作日志,记录周会,记录季度计划,记录学习目标,写各种设计文档。 + +有一种写代码一样的舒爽,markdown 和vim 一起用,简直绝配! + +那来说一说markdown的好处。 + +## 为什么需要markdown + +大家可能想为什么要使用markdown来写文档,而不用各种可以点击鼠标点点的那种所见即所得的工具来记笔记,例如word,云笔记之类的。 + +首先有如下几点: + +1. Markdown可以在任何地方使用 + +**可以使用它来创建网站,笔记,电子书,演讲稿,邮件信息和各种技术文档** + +2. Markdown很轻便 + +事实上,**包含Markdown格式文本的文件可以被任何一个应用打开**。 + +如果感觉不喜欢当前使用的Markdown渲染应用,可以使用其他渲染应用来打开。 + +而鲜明对比的就是Microsoft Word,必须要使用特定的软件才能打开 .doc 或者 .docx的文档 而且可能还是乱码或者格式乱位。 + +3. Markdown是独立的平台 + +**你可以创建Markdown格式文本的文件在任何一个可以运行的操作系统上** + +4. Markdown已经无处不在 + +**程序员的世界到处都是Markdown**,像简书,GitChat, GitHub,csdn等等都支持Markdown文档,正宗的官方技术文档都是使用Markdown来写的。 + +使用Markdown不仅可以非常方便的记录笔记,而且可以直接导出对应的网站内容,导出可打印的文档 + +至于markdown的语法,真的非常简单,不需要花费很长的时间掌握! + +而且一旦你掌握了它,你就可以在任何地方任何平台使用Markdown来记录笔记,文档甚至写书。 + +很多人使用Markdown来创建网站的内容,但是Markdown更加擅长于格式化的文本内容,**使用Markdown 根部不用担心格式问题,兼容问题**。 + +很多后台开发程序员的工作环境是linux,linux下写文档最佳选择也是markdown。 + +**我平时写代码,写文档都习惯在linux系统下进行(包括我的mac),所以我更喜欢vim + markdown**。 + +关于vim的话,后面我也可以单独介绍一波! + +## Markdown常用语法 + +我这里就简单列举一些最基本的语法。 + +### 标题 + +使用'#' 可以展现1-6级别的标题 + +``` +# 一级标题 +## 二级标题 +### 三级标题 +``` + +### 列表 + +使用 `*` 或者 `+` 或者 `-` 或者 `1. ` `2. ` 来表示列表 + +例如: + +``` +* 列表1 +* 列表2 +* 列表3 +``` + +效果: +* 列表1 +* 列表2 +* 列表3 + +### 链接 + +使用 `[名字](url)` 表示连接,例如`[Github地址](https://github.com/youngyangyang04/Markdown-Resume-Template)` + + +### 添加图片 + +添加图片`![名字](图片地址)` 例如`![Minion](https://octodex.github.com/images/minion.png)` + +### html 标签 + +Markdown支持部分html,例如这样 + +``` +

XXX

+``` + +## Markdown 渲染 + +有如下几种方式渲染Markdown文档 + +* 使用github来渲染,也就是把自己的 .md 文件传到github上,就是有可视化的展现,大家会发现github上每个项目都有一个README.md +* 使用谷歌浏览器安装MarkDown Preview Plus插件,也可以打开markdown文件,但是渲染效果不太好 +* mac下建议使用macdown来打开 markdown文件,然后就可以直接导出pdf来打印了 +* window下可以使用Typora来打开markdown文件,同样也可以直接导出pdf来打印 + +## Markdown学习资料 + +我这里仅仅是介绍了几个常用的语法,刚开始学习Markdown的时候语法难免会忘。 + +所以建议把这个markdown demo:https://markdown-it.github.io/ 收藏一下,平时用到哪里了忘了就看一看。 + +就酱,后面我还会陆续给大家安利一些编程利器。 + +## 总结 + +如果还没有掌握markdown的你还在等啥,赶紧使用markdown记录起来吧 + + + + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\347\256\200\345\216\206.md" "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\347\256\200\345\216\206.md" new file mode 100644 index 0000000000..762b55f400 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\347\250\213\345\272\217\345\221\230\347\256\200\345\216\206.md" @@ -0,0 +1,122 @@ +# 程序员的简历应该这么写!!(附简历模板) + +Carl校招社招都拿过大厂的offer,同时也看过很多应聘者的简历,这里把自己总结的简历技巧以及常见问题给大家梳理一下。 + +## 简历篇幅 + +首先程序员的简历力求简洁明了,不用设计上要过于复杂。 + +对于校招生,一页简历就够了,社招的话两页简历便可。 + +有的校招生说自己的经历太多了,简历要写出两三页,实际上基本是无关内容太多或者描述太啰唆,例如多过的校园活动,学生会经历等等。 + +既然是面试技术岗位,其他的方面一笔带过就好。 + + +## 谨慎使用“精通”两字 + +应届生或者刚毕业的程序员在写简历的时候 **切记不要写精通某某语言**,如果真的学的很好,**推荐写“熟悉”或者“掌握”**。 + +但是有的同学可能仅仅使用一些语言例如go或者python写了一些小东西,或者了解一些语言的语法,就直接写上熟悉C++、JAVA、GO、PYTHON ,这也是大忌,如果C++更了解的话,建议写熟悉C++,了解JAVA、GO、PYTHON。 + +**词语的强烈程度:精通 > 熟悉(推荐使用)> 掌握(推荐使用)> 了解(推荐使用)** + +还有做好心理准备,一旦我们写了熟悉某某语言,这门语言就一定是面试中重点考察的一个点。 + +例如写了熟悉C++, 那么继承、多态、封装、虚函数、C++11的一些特性、STL就一定会被问道。 + +**所以简历上写着熟悉哪一门语言,在准备面试的时候重点准备,其他语言几乎可以不用看了,面试官在面试中通常只会考察一门编程语言**。 + + +## 拿不准的绝对不要写在简历上 + +**不要为了简历上看上去很丰富,就写很多内容上去,内容越多,面试中考点就越多**。 + +简历中突出自己技能的几个点,而不是面面俱到。 + +想想看,面试官一定是拿着你的简历开始问问题的,**如果因为仅仅想展示自己多会一点点的东西就都写在简历上,等于给自己挖了一个“大坑”**。 + +例如仅仅部署过nginx服务器,就在简历上写熟悉nginx,那面试官可能上来就围绕着nginx问很多问题,同学们如果招架不住,然后说:“我仅仅部署过,底层实现我都不了解。这样就是让面试官有些失望”。 + +**同时尽量不要写代码行数10万+ 在简历上**,这就相当于提高了面试官的期望。 + +首先就是代码行数10W+ 无从考证,而且这无疑大大提高的面试官的期望和面试官问问题的范围,这相当于告诉面试官“我写代码没问题,你就尽管问吧”。 + +如果简历上再没有侧重点的话,面试官就开始铺天盖地问起来,恐怕大家回答的效果也不会太好。 + +## 项目经验应该如何写 + +**项目经验中要突出自己的贡献**,不要描述一遍项目就完事,要突出自己的贡献,是添加了哪些功能,还是优化了那些性能指数,最后再说说受益怎么样。 + +例如这个功能被多少人使用,例如性能提升了多少倍。 + +其实很多同学的一个通病就是在面试中说不出自己项目的难点,项目经历写了一大堆,各种框架数据库的使用都写上了,却答不出自己项目中的难点。 + +有的同学可能心里会想:“自己的项目没有什么难点,就是按照功能来做,遇到不会配置的不会调节的,就百度一下”。 + +其实大多数人做项目的时候都是这样的,不是每个项目都有什么难点,可是为什么一样的项目经验,别人就可以在难点上说出一二三来呢? + +这里还是有一些技巧的,首先是**做项目的时候时刻保持着对难点的敏感程度**,很多我们费尽周折解决了一个问题,然后自己也不做记录,就忘掉了,**此时如果及时将自己的思考过程记录下来,就是面试中的重要素材,养成这样的习惯非常重要**。 + +很多同学埋怨自己的项目没难点,其实不然,**找到项目中的一点,深挖下去就会遇到难点,解决它,这种经历就可以拿来在面试中来说了**。 + +例如使用java完成的项目,在深挖一下Java内存管理,看看是不是可以减少一些虚拟机上内存的压力。 + +所以很多时候 **不是自己的项目没有难点,而是自己准备的不充分**。 + +项目经验是面试官一定会问的,那么不是每一个面试都是主动问项目中有哪些亮点或者难点,这时候就需要我们自己主动去说自己项目中的难点。 + +## 变被动为主动 + +再说一个面试中如何变被动为主动的技巧,例如自己的项目是一套分布式系统,我们在介绍项目的时候主动说:“项目中的难点就是分布式数据一致性的问题。”。 + +**此时就应该知道面试官定会问:“你是如何解决数据一致性的?”**。 + +如果你对数据一致性协议的使用和原理足够的了解的话,就可以和面试官侃侃而谈了。 + +我们在简历中突出项目的难点在于数据一致性,并且**我们之前就精心准备一致性协议,数据一致性相关的知识,就等着面试官来问**,这样准备面试更有效率,这些写出来的简历也才是好的简历,而不是简历上泛泛而谈什么都说一些,最后都不太了解。 + +面试一共就三十分钟或者一个小时,说两个两个项目中的难点,既凸显出自己技术上的深度,同时项目中的难点是最好被我们自己掌控的,**因为这块是面试官必问的,就是我们可以变被动为主动的关键**。 + +**真正好的简历是 当同学们把自己的简历递给面试官的时候,基本都知道面试官看着简历都会问什么问题**,然后将面试官的引导到自己最熟悉的领域,这样大家才会占有主动权。 + + +## 博客的重要性 + +简历上可以放上自己的博客地址、Github地址甚至微博(如果发了很多关于技术的内容),**通过博客和github 面试官就可以快速判断同学们对技术的热情,以及学习的态度**,可以让面试官快速的了解同学们的技术水平。 + +如果有很多高质量博客和漂亮的github的话,即使面试现场发挥的不好,面试官通过博客也会知道这位同学基础还是很扎实,只是发挥的不好而已。 + +可以看出记录和总结的重要性。 + +写博客,不一定非要是技术大牛才写博客,大家都可以写博客来记录自己的收获,每一个知识点大家都可以写一篇技术博客,这方面要切忌懒惰! + +**我是欢迎录友们参考我的文章写博客来记录自己收获的,但一定要注明来自公众号「代码随想录」呀!** + +同时大家对github不要畏惧,可以很容易找到一些小的项目来练手。 + +这里贴出我的Github,上面有一些我自己写的小项目,大家可以参考:https://github.com/youngyangyang04 + +面试只有短短的30分钟或者一个小时,如何把自己掌握的技术更好的展现给面试官呢,博客、github都是很好的选择,如果把这些放在简历上,面试官一定会看的,这都是加分项。 + +## 领取方式 + +最后福利,把我的简历模板贡献出来!如下图所示。 + +![简历模板](https://file1.kamacoder.com/i/algo/20200803175538158.png) + +这里是简历模板中Markdown的代码:[https://github.com/youngyangyang04/Markdown-Resume-Template](https://github.com/youngyangyang04/Markdown-Resume-Template) ,可以fork到自己Github仓库上,按照这个模板来修改自己的简历。 + +**Word版本的简历,添加如下企业微信,通过之后就会发你word版本**。 + +
+ +如果已经有我的企业微信,直接回复:简历模板,就可以了。 + +## 总结 + +**好的简历是敲门砖,同时也不要在简历上花费过多的精力,好的简历以及面试技巧都是锦上添花**,真的求得心得的offer靠的还是真才实学。 + + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246.md" "b/problems/\345\211\215\345\272\217/\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246.md" new file mode 100644 index 0000000000..da2caa24b6 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246.md" @@ -0,0 +1,68 @@ + + +# 空间复杂度分析 + +* [关于时间复杂度,你不知道的都在这里!](https://programmercarl.com/前序/关于时间复杂度,你不知道的都在这里!.html) +* [O(n)的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html) +* [通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html) + +那么一直还没有讲空间复杂度,所以打算陆续来补上,内容不难,大家可以读一遍文章就有整体的了解了。 + +什么是空间复杂度呢? + +是对一个算法在运行过程中占用内存空间大小的量度,记做S(n)=O(f(n)。 + +空间复杂度(Space Complexity)记作S(n) 依然使用大O来表示。利用程序的空间复杂度,可以对程序运行中需要多少内存有个预先估计。 + +关注空间复杂度有两个常见的相关问题 + +1. 空间复杂度是考虑程序(可执行文件)的大小么? + +很多同学都会混淆程序运行时内存大小和程序本身的大小。这里强调一下**空间复杂度是考虑程序运行时占用内存的大小,而不是可执行文件的大小。** + +2. 空间复杂度是准确算出程序运行时所占用的内存么? + +不要以为空间复杂度就已经精准的掌握了程序的内存使用大小,很多因素会影响程序真正内存使用大小,例如编译器的内存对齐,编程语言容器的底层实现等等这些都会影响到程序内存的开销。 + +所以空间复杂度是预先大体评估程序内存使用的大小。 + +说到空间复杂度,我想同学们在OJ(online judge)上应该遇到过这种错误,就是超出内存限制,一般OJ对程序运行时的所消耗的内存都有一个限制。 + +为了避免内存超出限制,这也需要我们对算法占用多大的内存有一个大体的预估。 + +同样在工程实践中,计算机的内存空间也不是无限的,需要工程师对软件运行时所使用的内存有一个大体评估,这都需要用到算法空间复杂度的分析。 + +来看一下例子,什么时候的空间复杂度是 $O(1)$ 呢,C++代码如下: + +```CPP +int j = 0; +for (int i = 0; i < n; i++) { + j++; +} + +``` +第一段代码可以看出,随着n的变化,所需开辟的内存空间并不会随着n的变化而变化。即此算法空间复杂度为一个常量,所以表示为大O(1)。 + +什么时候的空间复杂度是O(n)? + +当消耗空间和输入参数n保持线性增长,这样的空间复杂度为O(n),来看一下这段C++代码 +```CPP +int* a = new int(n); +for (int i = 0; i < n; i++) { + a[i] = i; +} +``` + +我们定义了一个数组出来,这个数组占用的大小为n,虽然有一个for循环,但没有再分配新的空间,因此,这段代码的空间复杂度主要看第一行即可,随着n的增大,开辟的内存大小呈线性增长,即 O(n)。 + +其他的 O(n^2), O(n^3) 我想大家应该都可以以此例举出来了,**那么思考一下 什么时候空间复杂度是 O(logn)呢?** + +空间复杂度是logn的情况确实有些特殊,其实是在**递归的时候,会出现空间复杂度为logn的情况**。 + +至于如何求递归的空间复杂度,我会在专门写一篇文章来介绍的,敬请期待! + + + + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/\347\256\227\346\263\225\350\266\205\346\227\266.md" "b/problems/\345\211\215\345\272\217/\347\256\227\346\263\225\350\266\205\346\227\266.md" new file mode 100644 index 0000000000..5603412717 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\347\256\227\346\263\225\350\266\205\346\227\266.md" @@ -0,0 +1,281 @@ + +# On的算法居然超时了,此时的n究竟是多大? + + +一些同学可能对计算机运行的速度还没有概念,就是感觉计算机运行速度应该会很快,那么在leetcode上做算法题目的时候为什么会超时呢? + +计算机究竟1s可以执行多少次操作呢? 接下来探讨一下这个问题。 + +## 超时是怎么回事 + +![程序超时](https://file1.kamacoder.com/i/algo/20200729112716117-20230310124308704.png) + +大家在leetcode上练习算法的时候应该都遇到过一种错误是“超时”。 + +也就是说程序运行的时间超过了规定的时间,一般OJ(online judge)的超时时间就是1s,也就是用例数据输入后最多要1s内得到结果,暂时还不清楚leetcode的判题规则,下文为了方便讲解,暂定超时时间就是1s。 + +如果写出了一个 $O(n)$ 的算法 ,其实可以估算出来n是多大的时候算法的执行时间就会超过1s了。 + +如果n的规模已经足够让 $O(n)$ 的算法运行时间超过了1s,就应该考虑log(n)的解法了。 + +## 从硬件配置看计算机的性能 + +计算机的运算速度主要看CPU的配置,以2015年MacPro为例,CPU配置:2.7 GHz Dual-Core Intel Core i5 。 + +也就是 2.7 GHz 奔腾双核,i5处理器,GHz是指什么呢,1Hz = 1/s,1Hz 是CPU的一次脉冲(可以理解为一次改变状态,也叫时钟周期),称之为为赫兹,那么1GHz等于多少赫兹呢 + +* 1GHz(兆赫)= 1000MHz(兆赫) +* 1MHz(兆赫)= 1百万赫兹 + +所以 1GHz = 10亿Hz,表示CPU可以一秒脉冲10亿次(有10亿个时钟周期),这里不要简单理解一个时钟周期就是一次CPU运算。 + +例如1 + 2 = 3,cpu要执行四次才能完整这个操作,步骤一:把1放入寄存器,步骤二:把2放入寄存器,步骤三:做加法,步骤四:保存3。 + + +而且计算机的cpu也不会只运行我们自己写的程序上,同时cpu也要执行计算机的各种进程任务等等,我们的程序仅仅是其中的一个进程而已。 + + +所以我们的程序在计算机上究竟1s真正能执行多少次操作呢? + +## 做个测试实验 + +在写测试程序测1s内处理多大数量级数据的时候,有三点需要注意: + +* CPU执行每条指令所需的时间实际上并不相同,例如CPU执行加法和乘法操作的耗时实际上都是不一样的。 +* 现在大多计算机系统的内存管理都有缓存技术,所以频繁访问相同地址的数据和访问不相邻元素所需的时间也是不同的。 +* 计算机同时运行多个程序,每个程序里还有不同的进程线程在抢占资源。 + +尽管有很多因素影响,但是还是可以对自己程序的运行时间有一个大体的评估的。 + +引用算法4里面的一段话: + +* 火箭科学家需要大致知道一枚试射火箭的着陆点是在大海里还是在城市中; +* 医学研究者需要知道一次药物测试是会杀死还是会治愈实验对象; + +所以**任何开发计算机程序的软件工程师都应该能够估计这个程序的运行时间是一秒钟还是一年**。 + +这个是最基本的,所以以上误差就不算事了。 + +以下以C++代码为例: + +测试硬件:2015年MacPro,CPU配置:2.7 GHz Dual-Core Intel Core i5 + +实现三个函数,时间复杂度分别是 $O(n)$ , $O(n^2)$ , $O(n\log n)$ ,使用加法运算来统一测试。 + +```CPP +// O(n) +void function1(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + k++; + } +} + +``` + +```CPP +// O(n^2) +void function2(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long j = 0; j < n; j++) { + k++; + } + } + +} +``` + +```CPP +// O(nlogn) +void function3(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long long j = 1; j < n; j = j*2) { // 注意这里j=1 + k++; + } + } +} + +``` + +来看一下这三个函数随着n的规模变化,耗时会产生多大的变化,先测function1 ,就把 function2 和 function3 注释掉 + +```CPP +int main() { + long long n; // 数据规模 + while (1) { + cout << "输入n:"; + cin >> n; + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + function1(n); +// function2(n); +// function3(n); + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << "耗时:" << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +``` + +来看一下运行的效果,如下图: + +![程序超时2](https://file1.kamacoder.com/i/algo/20200729200018460-20230310124315093.png) + +O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下 $O(n^2)$ 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。 + +![程序超时3](https://file1.kamacoder.com/i/algo/2020072919590970-20230310124318532.png) + +O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。 + +在推测一下 $O(n\log n)$ 的话, 1s可以处理的数据规模是什么呢? + +理论上应该是比 $O(n)$ 少一个数量级,因为 $\log n$ 的复杂度 其实是很快,看一下实验数据。 + +![程序超时4](https://file1.kamacoder.com/i/algo/20200729195729407-20230310124322232.png) + +$O(n\log n)$ 的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 + +这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。 + +**整体测试数据整理如下:** + +![程序超时1](https://file1.kamacoder.com/i/algo/20201208231559175-20230310124325152.png) + +至于 $O(\log n)$ 和 $O(n^3)$ 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。 + +## 完整测试代码 + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +// O(n) +void function1(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + k++; + } +} + +// O(n^2) +void function2(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long j = 0; j < n; j++) { + k++; + } + } + +} +// O(nlogn) +void function3(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + for (long long j = 1; j < n; j = j*2) { // 注意这里j=1 + k++; + } + } +} +int main() { + long long n; // 数据规模 + while (1) { + cout << "输入n:"; + cin >> n; + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + function1(n); +// function2(n); +// function3(n); + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << "耗时:" << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} + +``` + + +Java版本 + +```Java +import java.util.Scanner; + +public class TimeComplexity { + // o(n) + public static void function1(long n) { + System.out.println("o(n)算法"); + long k = 0; + for (long i = 0; i < n; i++) { + k++; + } + } + + // o(n^2) + public static void function2(long n) { + System.out.println("o(n^2)算法"); + long k = 0; + for (long i = 0; i < n; i++) { + for (long j = 0; j < n; j++) { + k++; + } + } + } + + // o(nlogn) + public static void function3(long n) { + System.out.println("o(nlogn)算法"); + long k = 0; + for (long i = 0; i < n; i++) { + for (long j = 1; j < n; j = j * 2) { // 注意这里j=1 + k++; + } + } + } + + public static void main(String[] args) { + while(true) { + Scanner in = new Scanner(System.in); + System.out.print("输入n: "); + int n = in.nextInt(); + long startTime = System.currentTimeMillis(); + + function1(n); + // function2(n); + // function3(n); + + long endTime = System.currentTimeMillis(); + long costTime = endTime - startTime; + System.out.println("算法耗时 == " + costTime + "ms"); + } + } +} +``` + +## 总结 + +本文详细分析了在leetcode上做题程序为什么会有超时,以及从硬件配置上大体知道CPU的执行速度,然后亲自做一个实验来看看 $O(n)$ 的算法,跑一秒钟,这个n究竟是做大,最后给出不同时间复杂度,一秒内可以运算出来的n的大小。 + +建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。 + +这样,大家应该对程序超时时候的数据规模有一个整体的认识了。 + +就酱,如果感觉「代码随想录」很干货,就帮忙宣传一波吧,很多录友发现这里之后都感觉相见恨晚! + + + + + + +----------------------- + +
diff --git "a/problems/\345\211\215\345\272\217/\347\274\226\347\250\213\347\264\240\345\205\273\351\203\250\345\210\206\347\232\204\345\220\271\346\257\233\346\261\202\347\226\265.md" "b/problems/\345\211\215\345\272\217/\347\274\226\347\250\213\347\264\240\345\205\273\351\203\250\345\210\206\347\232\204\345\220\271\346\257\233\346\261\202\347\226\265.md" new file mode 100644 index 0000000000..edb62bc8b5 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\347\274\226\347\250\213\347\264\240\345\205\273\351\203\250\345\210\206\347\232\204\345\220\271\346\257\233\346\261\202\347\226\265.md" @@ -0,0 +1,31 @@ +## 代码风格 + +- `不甚了解`是不能更了解的意思,这个地方应该使用存疑。 +- `后期在不断优化`,'在'应为'再'。 +- `googlec++编程规范`,Google拼写错误 + +## 代码本地编译 + +- `粘到本例来运行`存疑,应为本地 +- `本题运行`存疑,应为本地 + +## ACM二叉树 + +- 左孩子和右孩子的下标不太好理解。我给出证明过程: + + 如果父节点在第k层,第$m,m \in [0,2^k]$个节点,则其左孩子所在的位置必然为$k+1$层,第$2*(m-1)+1$个节点。 + + - 计算父节点在数组中的索引: + $$ + index_{father}=(\sum_{i=0}^{i=k-1}2^i)+m-1=2^k-1+m-1 + $$ + + - 计算左子节点在数组的索引: + $$ + index_{left}=(\sum_{i=0}^{i=k}2^i)+2*m-1-1=2^{k+1}+2m-3 + $$ + + - 故左孩子的下标为$index_{left}=index_{father}\times2+1$,同理可得到右子孩子的索引关系。也可以直接在左子孩子的基础上`+1`。 + + + diff --git "a/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" new file mode 100644 index 0000000000..01c07a5c00 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" @@ -0,0 +1,267 @@ + +# 递归算法的时间与空间复杂度分析! + +之前在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)中详细讲解了递归算法的时间复杂度,但没有讲空间复杂度。 + +本篇讲通过求斐波那契数列和二分法再来深入分析一波递归算法的时间和空间复杂度,细心看完,会刷新对递归的认知! + + +## 递归求斐波那契数列的性能分析 + +先来看一下求斐波那契数的递归写法。 + +```CPP +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i-1) + fibonacci(i-2); +} +``` + +对于递归算法来说,代码一般都比较简短,从算法逻辑上看,所用的存储空间也非常少,但运行时需要内存可不见得会少。 + +### 时间复杂度分析 + +来看看这个求斐波那契的递归算法的时间复杂度是多少呢? + +在讲解递归时间复杂度的时候,我们提到了递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归的时间复杂度**。 + +可以看出上面的代码每次递归都是O(1)的操作。再来看递归了多少次,这里将i为5作为输入的递归过程 抽象成一棵递归树,如图: + + +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305093200104.png) + +从图中,可以看出f(5)是由f(4)和f(3)相加而来,那么f(4)是由f(3)和f(2)相加而来 以此类推。 + +在这棵二叉树中每一个节点都是一次递归,那么这棵树有多少个节点呢? + +我们之前也有说到,一棵深度(按根节点深度为1)为k的二叉树最多可以有 2^k - 1 个节点。 + +所以该递归算法的时间复杂度为O(2^n),这个复杂度是非常大的,随着n的增大,耗时是指数上升的。 + +来做一个实验,大家可以有一个直观的感受。 + +以下为C++代码,来测一下,让我们输入n的时候,这段递归求斐波那契代码的耗时。 + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i - 1) + fibonacci(i - 2); +} +void time_consumption() { + int n; + while (cin >> n) { + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + + fibonacci(n); + + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +int main() +{ + time_consumption(); + return 0; +} +``` + +根据以上代码,给出几组实验数据: + +测试电脑以2015版MacPro为例,CPU配置:`2.7 GHz Dual-Core Intel Core i5` + +测试数据如下: + +* n = 40,耗时:837 ms +* n = 50,耗时:110306 ms + +可以看出,O(2^n)这种指数级别的复杂度是非常大的。 + +所以这种求斐波那契数的算法看似简洁,其实时间复杂度非常高,一般不推荐这样来实现斐波那契。 + +其实罪魁祸首就是这里的两次递归,导致了时间复杂度以指数上升。 + +```CPP +return fibonacci(i-1) + fibonacci(i-2); +``` + +可不可以优化一下这个递归算法呢。 主要是减少递归的调用次数。 + +来看一下如下代码: + +```CPP +// 版本二 +int fibonacci(int first, int second, int n) { + if (n <= 0) { + return 0; + } + if (n < 3) { + return 1; + } + else if (n == 3) { + return first + second; + } + else { + return fibonacci(second, first + second, n - 1); + } +} +``` + +这里相当于用first和second来记录当前相加的两个数值,此时就不用两次递归了。 + +因为每次递归的时候n减1,即只是递归了n次,所以时间复杂度是 O(n)。 + +同理递归的深度依然是n,每次递归所需的空间也是常数,所以空间复杂度依然是O(n)。 + +代码(版本二)的复杂度如下: + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +此时再来测一下耗时情况验证一下: + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +int fibonacci_3(int first, int second, int n) { + if (n <= 0) { + return 0; + } + if (n < 3) { + return 1; + } + else if (n == 3) { + return first + second; + } + else { + return fibonacci_3(second, first + second, n - 1); + } +} + +void time_consumption() { + int n; + while (cin >> n) { + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + + fibonacci_3(1, 1, n); + + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +int main() +{ + time_consumption(); + return 0; +} + +``` + +测试数据如下: + +* n = 40,耗时:0 ms +* n = 50,耗时:0 ms + +大家此时应该可以看出差距了!! + +### 空间复杂度分析 + +说完了这段递归代码的时间复杂度,再看看如何求其空间复杂度呢,这里给大家提供一个公式:**递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度** + +为什么要求递归的深度呢? + +因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。 + +此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是 $O(1)$ 。 + +在看递归的深度是多少呢?如图所示: + + +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305094749554.png) + +递归第n个斐波那契数的话,递归调用栈的深度就是n。 + +那么每次递归的空间复杂度是O(1), 调用栈深度为n,所以这段递归代码的空间复杂度就是O(n)。 + +```CPP +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i-1) + fibonacci(i-2); +} +``` + + +最后对各种求斐波那契数列方法的性能做一下分析,如题: + + +![递归的空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305095227356.png) + +可以看出,求斐波那契数的时候,使用递归算法并不一定是在性能上是最优的,但递归确实简化的代码层面的复杂度。 + +### 二分法(递归实现)的性能分析 + +带大家再分析一段二分查找的递归实现。 + +```CPP +int binary_search( int arr[], int l, int r, int x) { + if (r >= l) { + int mid = l + (r - l) / 2; + if (arr[mid] == x) + return mid; + if (arr[mid] > x) + return binary_search(arr, l, mid - 1, x); + return binary_search(arr, mid + 1, r, x); + } + return -1; +} +``` + +都知道二分查找的时间复杂度是O(logn),那么递归二分查找的空间复杂度是多少呢? + +我们依然看 **每次递归的空间复杂度和递归的深度** + +每次递归的空间复杂度可以看出主要就是参数里传入的这个arr数组,但需要注意的是在C/C++中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入的数组首元素地址。 + +**也就是说每一层递归都是公用一块数组地址空间的**,所以 每次递归的空间复杂度是常数即:O(1)。 + +再来看递归的深度,二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * logn = O(logn)。 + +大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)。 + + +## 总结 + +本章我们详细分析了递归实现的求斐波那契和二分法的空间复杂度,同时也对时间复杂度做了分析。 + +特别是两种递归实现的求斐波那契数列,其时间复杂度截然不容,我们还做了实验,验证了时间复杂度为O(2^n)是非常耗时的。 + +通过本篇大家应该对递归算法的时间复杂度和空间复杂度有更加深刻的理解了。 + + + + + + + +----------------------- +
diff --git "a/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" new file mode 100644 index 0000000000..a02c37f2c9 --- /dev/null +++ "b/problems/\345\211\215\345\272\217/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\345\244\215\346\235\202\345\272\246.md" @@ -0,0 +1,143 @@ + +# 通过一道面试题目,讲一讲递归算法的时间复杂度! + +> 本篇通过一道面试题,一个面试场景,来好好分析一下如何求递归算法的时间复杂度。 + +相信很多同学对递归算法的时间复杂度都很模糊,那么这篇来给大家通透的讲一讲。 + +**同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码**。 + +这是为什么呢? + +如果对递归的时间复杂度理解的不够深入的话,就会这样! + +那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了O(n)的代码。 + +面试题:求x的n次方 + +想一下这么简单的一道题目,代码应该如何写呢。最直观的方式应该就是,一个for循环求出结果,代码如下: + +```CPP +int function1(int x, int n) { + int result = 1; // 注意 任何数的0次方等于1 + for (int i = 0; i < n; i++) { + result = result * x; + } + return result; +} +``` +时间复杂度为O(n),此时面试官会说,有没有效率更好的算法呢。 + +**如果此时没有思路,不要说:我不会,我不知道了等等**。 + +可以和面试官探讨一下,询问:“可不可以给点提示”。面试官提示:“考虑一下递归算法”。 + +那么就可以写出了如下这样的一个递归的算法,使用递归解决了这个问题。 + +```CPP +int function2(int x, int n) { + if (n == 0) { + return 1; // return 1 同样是因为0次方是等于1的 + } + return function2(x, n - 1) * x; +} +``` +面试官问:“那么这个代码的时间复杂度是多少?”。 + +一些同学可能一看到递归就想到了O(log n),其实并不是这样,递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归中的操作次数**。 + +那再来看代码,这里递归了几次呢? + +每次n-1,递归了n次时间复杂度是O(n),每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项O(1),所以这份代码的时间复杂度是 n × 1 = O(n)。 + +这个时间复杂度就没有达到面试官的预期。于是又写出了如下的递归算法的代码: + +```CPP +int function3(int x, int n) { + if (n == 0) return 1; + if (n == 1) return x; + + if (n % 2 == 1) { + return function3(x, n / 2) * function3(x, n / 2)*x; + } + return function3(x, n / 2) * function3(x, n / 2); +} + +``` + +面试官看到后微微一笑,问:“这份代码的时间复杂度又是多少呢?” 此刻有些同学可能要陷入了沉思了。 + +我们来分析一下,首先看递归了多少次呢,可以把递归抽象出一棵满二叉树。刚刚同学写的这个算法,可以用一棵满二叉树来表示(为了方便表示,选择n为偶数16),如图: + +![递归算法的时间复杂度](https://file1.kamacoder.com/i/algo/20201209193909426.png) + +当前这棵二叉树就是求x的n次方,n为16的情况,n为16的时候,进行了多少次乘法运算呢? + +这棵树上每一个节点就代表着一次递归并进行了一次相乘操作,所以进行了多少次递归的话,就是看这棵树上有多少个节点。 + +熟悉二叉树话应该知道如何求满二叉树节点数量,这棵满二叉树的节点数量就是`2^3 + 2^2 + 2^1 + 2^0 = 15`,可以发现:**这其实是等比数列的求和公式,这个结论在二叉树相关的面试题里也经常出现**。 + +这么如果是求x的n次方,这个递归树有多少个节点呢,如下图所示:(m为深度,从0开始) + +![递归求时间复杂度](https://file1.kamacoder.com/i/algo/20200728195531892.png) + +**时间复杂度忽略掉常数项`-1`之后,这个递归算法的时间复杂度依然是O(n)**。对,你没看错,依然是O(n)的时间复杂度! + +此时面试官就会说:“这个递归的算法依然还是O(n)啊”, 很明显没有达到面试官的预期。 + +那么O(logn)的递归算法应该怎么写呢? + +想一想刚刚给出的那份递归算法的代码,是不是有哪里比较冗余呢,其实有重复计算的部分。 + +于是又写出如下递归算法的代码: + +```CPP +int function4(int x, int n) { + if (n == 0) return 1; + if (n == 1) return x; + int t = function4(x, n / 2);// 这里相对于function3,是把这个递归操作抽取出来 + if (n % 2 == 1) { + return t * t * x; + } + return t * t; +} +``` + +再来看一下现在这份代码时间复杂度是多少呢? + +依然还是看他递归了多少次,可以看到这里仅仅有一个递归调用,且每次都是n/2 ,所以这里我们一共调用了log以2为底n的对数次。 + +**每次递归了做都是一次乘法操作,这也是一个常数项的操作,那么这个递归算法的时间复杂度才是真正的O(logn)**。 + +此时大家最后写出了这样的代码并且将时间复杂度分析的非常清晰,相信面试官是比较满意的。 + +# 总结 + +对于递归的时间复杂度,毕竟初学者有时候会迷糊,刷过很多题的老手依然迷糊。 + +**本篇我用一道非常简单的面试题目:求x的n次方,来逐步分析递归算法的时间复杂度,注意不要一看到递归就想到了O(logn)!** + +同样使用递归,有的同学可以写出O(logn)的代码,有的同学还可以写出O(n)的代码。 + +对于function3 这样的递归实现,很容易让人感觉这是O(log n)的时间复杂度,其实这是O(n)的算法! + +```CPP +int function3(int x, int n) { + if (n == 0) return 1; + if (n == 1) return x; + if (n % 2 == 1) { + return function3(x, n / 2) * function3(x, n / 2)*x; + } + return function3(x, n / 2) * function3(x, n / 2); +} +``` +可以看出这道题目非常简单,但是又很考究算法的功底,特别是对递归的理解,这也是我面试别人的时候用过的一道题,所以整个情景我才写的如此逼真。 + +大厂面试的时候最喜欢用“简单题”来考察候选人的算法功底,注意这里的“简单题”可并不一定真的简单哦! + +如果认真读完本篇,相信大家对递归算法的有一个新的认识的,同一道题目,同样是递归,效率可是不一样的! + + + +----------------------- +
diff --git "a/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" "b/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" new file mode 100755 index 0000000000..547cae8a30 --- /dev/null +++ "b/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" @@ -0,0 +1,171 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 替换数字 + +力扣已经将剑指offer题目下架,所以我在卡码网上给大家提供类似的题目来练习 + +[卡码网题目链接](https://kamacoder.com/problempage.php?pid=1064) + +给定一个字符串 s,它包含小写字母和数字字符,请编写一个函数,将字符串中的字母字符保持不变,而将每个数字字符替换为number。 + +例如,对于输入字符串 "a1b2c3",函数应该将其转换为 "anumberbnumbercnumber"。 + +对于输入字符串 "a5b",函数应该将其转换为 "anumberb" + +输入:一个字符串 s,s 仅包含小写字母和数字字符。 + +输出:打印一个新的字符串,其中每个数字字符都被替换为了number + +样例输入:a1b2c3 + +样例输出:anumberbnumbercnumber + +数据范围:1 <= s.length < 10000。 + +## 思路 + +如果想把这道题目做到极致,就不要只用额外的辅助空间了! (不过使用Java刷题的录友,一定要使用辅助空间,因为Java里的string不能修改) + +首先扩充数组到每个数字字符替换成 "number" 之后的大小。 + +例如 字符串 "a5b" 的长度为3,那么 将 数字字符变成字符串 "number" 之后的字符串为 "anumberb" 长度为 8。 + +如图: + +![](https://file1.kamacoder.com/i/algo/20231030165201.png) + +然后从后向前替换数字字符,也就是双指针法,过程如下:i指向新长度的末尾,j指向旧长度的末尾。 + +![](https://file1.kamacoder.com/i/algo/20231030173058.png) + +有同学问了,为什么要从后向前填充,从前向后填充不行么? + +从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素整体向后移动。 + +**其实很多数组填充类的问题,其做饭都是先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** + +这么做有两个好处: + +1. 不用申请新数组。 +2. 从后向前填充元素,避免了从前向后填充元素时,每次添加元素都要将添加元素之后的所有元素向后移动的问题。 + +C++代码如下: + +```CPP +#include +using namespace std; +int main() { + string s; + while (cin >> s) { + int count = 0; // 统计数字的个数 + int sOldSize = s.size(); + for (int i = 0; i < s.size(); i++) { + if (s[i] >= '0' && s[i] <= '9') { + count++; + } + } + // 扩充字符串s的大小,也就是每个空格替换成"number"之后的大小 + s.resize(s.size() + count * 5); + int sNewSize = s.size(); + // 从后先前将空格替换为"number" + for (int i = sNewSize - 1, j = sOldSize - 1; j < i; i--, j--) { + if (s[j] > '9' || s[j] < '0') { + s[i] = s[j]; + } else { + s[i] = 'r'; + s[i - 1] = 'e'; + s[i - 2] = 'b'; + s[i - 3] = 'm'; + s[i - 4] = 'u'; + s[i - 5] = 'n'; + i -= 5; + } + } + cout << s << endl; + } +} + + +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +此时算上本题,我们已经做了七道双指针相关的题目了分别是: + +* [27.移除元素](https://programmercarl.com/0027.移除元素.html) +* [15.三数之和](https://programmercarl.com/0015.三数之和.html) +* [18.四数之和](https://programmercarl.com/0018.四数之和.html) +* [206.翻转链表](https://programmercarl.com/0206.翻转链表.html) +* [142.环形链表II](https://programmercarl.com/0142.环形链表II.html) +* [344.反转字符串](https://programmercarl.com/0344.反转字符串.html) + +## 拓展 + +这里也给大家拓展一下字符串和数组有什么差别, + +字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。 + +在C语言中,把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字符串是否结束的标志。 + +例如这段代码: + +``` +char a[5] = "asd"; +for (int i = 0; a[i] != '\0'; i++) { +} +``` + +在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束。 + +例如这段代码: + +``` +string a = "asd"; +for (int i = 0; i < a.size(); i++) { +} +``` + +那么vector< char > 和 string 又有什么区别呢? + +其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。 + +所以想处理字符串,我们还是会定义一个string类型。 + + +## 其他语言版本 + +### C: + +### Java: + + +### Go: + + + +### python: + +### JavaScript: + + +### TypeScript: + + +### Swift: + + +### Scala: + + +### PHP: + + +### Rust: + + + + diff --git "a/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" "b/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" new file mode 100755 index 0000000000..025073220a --- /dev/null +++ "b/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,176 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 右旋字符串 + +力扣已经将剑指offer题目下架,所以在卡码网上给大家提供类似的题目来练习 + +[卡码网题目链接](https://kamacoder.com/problempage.php?pid=1065) + +字符串的右旋转操作是把字符串尾部的若干个字符转移到字符串的前面。给定一个字符串 s 和一个正整数 k,请编写一个函数,将字符串中的后面 k 个字符移到字符串的前面,实现字符串的右旋转操作。 + +例如,对于输入字符串 "abcdefg" 和整数 2,函数应该将其转换为 "fgabcde"。 + +输入:输入共包含两行,第一行为一个正整数 k,代表右旋转的位数。第二行为字符串 s,代表需要旋转的字符串。 + +输出:输出共一行,为进行了右旋转操作后的字符串。 + +样例输入: + +``` +2 +abcdefg +``` + +样例输出: + +``` +fgabcde +``` + +数据范围:1 <= k < 10000, 1 <= s.length < 10000; + + +## 思路 + +为了让本题更有意义,提升一下本题难度:**不能申请额外空间,只能在本串上操作**。 (Java不能在字符串上修改,所以使用java一定要开辟新空间) + +不能使用额外空间的话,模拟在本串操作要实现右旋转字符串的功能还是有点困难的。 + +那么我们可以想一下上一题目[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中讲过,使用整体反转+局部反转就可以实现反转单词顺序的目的。 + + +本题中,我们需要将字符串右移n位,字符串相当于分成了两个部分,如果n为2,符串相当于分成了两个部分,如图: (length为字符串长度) + +![](https://file1.kamacoder.com/i/algo/20231106170143.png) + + +右移n位, 就是将第二段放在前面,第一段放在后面,先不考虑里面字符的顺序,是不是整体倒叙不就行了。如图: + +![](https://file1.kamacoder.com/i/algo/20231106171557.png) + +此时第一段和第二段的顺序是我们想要的,但里面的字符位置被我们倒叙,那么此时我们在把 第一段和第二段里面的字符再倒叙一把,这样字符顺序不就正确了。 如果: + +![](https://file1.kamacoder.com/i/algo/20231106172058.png) + +其实,思路就是 通过 整体倒叙,把两段子串顺序颠倒,两个段子串里的的字符在倒叙一把,**负负得正**,这样就不影响子串里面字符的顺序了。 + +整体代码如下: + +```CPP +// 版本一 +#include +#include +using namespace std; +int main() { + int n; + string s; + cin >> n; + cin >> s; + int len = s.size(); //获取长度 + + reverse(s.begin(), s.end()); // 整体反转 + reverse(s.begin(), s.begin() + n); // 先反转前一段,长度n + reverse(s.begin() + n, s.end()); // 再反转后一段 + + cout << s << endl; + +} +``` + +那么整体反正的操作放在下面,先局部反转行不行? + +可以的,不过,要记得 控制好 局部反转的长度,如果先局部反转,那么先反转的子串长度就是 len - n,如图: + +![](https://file1.kamacoder.com/i/algo/20231106172534.png) + +代码如下: + +```CPP +// 版本二 +#include +#include +using namespace std; +int main() { + int n; + string s; + cin >> n; + cin >> s; + int len = s.size(); //获取长度 + reverse(s.begin(), s.begin() + len - n); // 先反转前一段,长度len-n ,注意这里是和版本一的区别 + reverse(s.begin() + len - n, s.end()); // 再反转后一段 + reverse(s.begin(), s.end()); // 整体反转 + cout << s << endl; + +} +``` + + +## 拓展 + +大家在做剑指offer的时候,会发现 剑指offer的题目是左反转,那么左反转和右反转 有什么区别呢? + +其实思路是一样一样的,就是反转的区间不同而已。如果本题是左旋转n,那么实现代码如下: + +```CPP +#include +#include +using namespace std; +int main() { + int n; + string s; + cin >> n; + cin >> s; + int len = s.size(); //获取长度 + reverse(s.begin(), s.begin() + n); // 反转第一段长度为n + reverse(s.begin() + n, s.end()); // 反转第二段长度为len-n + reverse(s.begin(), s.end()); // 整体反转 + cout << s << endl; + +} +``` + +大家可以感受一下 这份代码和 版本二的区别, 其实就是反转的区间不同而已。 + +那么左旋转的话,可以不可以先整体反转,例如想版本一的那样呢? + +当然可以。 + + + + +## 其他语言版本 + +### Java: + + + +### Python: + + +### Go: + + +### JavaScript: + + +### TypeScript: + + +### Swift: + + + +### PHP: + + +### Scala: + + +### Rust: + + + + diff --git "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222-\350\202\241\347\245\250\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222-\350\202\241\347\245\250\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" new file mode 100755 index 0000000000..ff73cd9606 --- /dev/null +++ "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222-\350\202\241\347\245\250\351\227\256\351\242\230\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,474 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# Leetcode股票问题总结篇! + +之前我们已经把力扣上股票系列的题目都讲过的,但没有来一篇股票总结,来帮大家高屋建瓴,所以总结篇这就来了! + +![股票问题总结](https://file1.kamacoder.com/i/algo/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98%E6%80%BB%E7%BB%93.jpg) + +* [动态规划:121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) +* [动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html) +* [动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html) +* [动态规划:188.买卖股票的最佳时机IV](https://programmercarl.com/0188.买卖股票的最佳时机IV.html) +* [动态规划:309.最佳买卖股票时机含冷冻期](https://programmercarl.com/0309.最佳买卖股票时机含冷冻期.html) +* [动态规划:714.买卖股票的最佳时机含手续费](https://programmercarl.com/0714.买卖股票的最佳时机含手续费(动态规划).html) + +## 卖股票的最佳时机 + +[动态规划:121.买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html),**股票只能买卖一次,问最大利润**。 + +【贪心解法】 + +取最左最小值,取最右最大值,那么得到的差值就是最大利润,代码如下: +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int low = INT_MAX; + int result = 0; + for (int i = 0; i < prices.size(); i++) { + low = min(low, prices[i]); // 取最左最小价格 + result = max(result, prices[i] - low); // 直接取最大区间利润 + } + return result; + } +}; +``` + +【动态规划】 + +* dp[i][0] 表示第i天持有股票所得现金。 +* dp[i][1] 表示第i天不持有股票所得现金。 + +如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 +* 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] +* 第i天买入股票,所得现金就是买入今天的股票后所得现金即:-prices[i] +所以dp[i][0] = max(dp[i - 1][0], -prices[i]); + +如果第i天不持有股票即dp[i][1], 也可以由两个状态推出来 +* 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] +* 第i天卖出股票,所得现金就是按照今天股票佳价格卖出后所得现金即:prices[i] + dp[i - 1][0] +所以dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); + +代码如下: + +```CPP +// 版本一 +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + if (len == 0) return 0; + vector> dp(len, vector(2)); + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i][0] = max(dp[i - 1][0], -prices[i]); + dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); + } + return dp[len - 1][1]; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +使用滚动数组,代码如下: + +```CPP +// 版本二 +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + vector> dp(2, vector(2)); // 注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]); + dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]); + } + return dp[(len - 1) % 2][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + + +## 买卖股票的最佳时机II + +[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html)可以多次买卖股票,问最大收益。 + + +【贪心解法】 + +收集每天的正利润便可,代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int result = 0; + for (int i = 1; i < prices.size(); i++) { + result += max(prices[i] - prices[i - 1], 0); + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + + +【动态规划】 + +dp数组定义: + +* dp[i][0] 表示第i天持有股票所得现金 +* dp[i][1] 表示第i天不持有股票所得最多现金 + + +如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 +* 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] +* 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i] + +**注意这里和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况**。 + +在[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。 + +而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。 + +代码如下:(注意代码中的注释,标记了和121.买卖股票的最佳时机唯一不同的地方) + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + vector> dp(len, vector(2, 0)); + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); // 注意这里是和121. 买卖股票的最佳时机唯一不同的地方。 + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i]); + } + return dp[len - 1][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + + +## 买卖股票的最佳时机III + +[动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html)最多买卖两次,问最大收益。 + +【动态规划】 + +一天一共就有五个状态, + +0. 没有操作 +1. 第一次买入 +2. 第一次卖出 +3. 第二次买入 +4. 第二次卖出 + +dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金。 + + +达到dp[i][1]状态,有两个具体操作: + +* 操作一:第i天买入股票了,那么dp[i][1] = dp[i-1][0] - prices[i] +* 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1] + +dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]); + +同理dp[i][2]也有两个操作: + +* 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i] +* 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2] + +所以dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]) + +同理可推出剩下状态部分: + +dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + +dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + +代码如下: + +```CPP +// 版本一 +class Solution { +public: + int maxProfit(vector& prices) { + if (prices.size() == 0) return 0; + vector> dp(prices.size(), vector(5, 0)); + dp[0][1] = -prices[0]; + dp[0][3] = -prices[0]; + for (int i = 1; i < prices.size(); i++) { + dp[i][0] = dp[i - 1][0]; + dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i]); + dp[i][2] = max(dp[i - 1][2], dp[i - 1][1] + prices[i]); + dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); + dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); + } + return dp[prices.size() - 1][4]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n × 5) + +当然,大家可以看到力扣官方题解里的一种优化空间写法,我这里给出对应的C++版本: + +```CPP +// 版本二 +class Solution { +public: + int maxProfit(vector& prices) { + if (prices.size() == 0) return 0; + vector dp(5, 0); + dp[1] = -prices[0]; + dp[3] = -prices[0]; + for (int i = 1; i < prices.size(); i++) { + dp[1] = max(dp[1], dp[0] - prices[i]); + dp[2] = max(dp[2], dp[1] + prices[i]); + dp[3] = max(dp[3], dp[2] - prices[i]); + dp[4] = max(dp[4], dp[3] + prices[i]); + } + return dp[4]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +**这种写法看上去简单,其实思路很绕,不建议大家这么写,这么思考,很容易把自己绕进去!** 对于本题,把版本一的写法研究明白,足以! + +## 买卖股票的最佳时机IV + +[动态规划:188.买卖股票的最佳时机IV](https://programmercarl.com/0188.买卖股票的最佳时机IV.html) 最多买卖k笔交易,问最大收益。 + +使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j] + +j的状态表示为: + +* 0 表示不操作 +* 1 第一次买入 +* 2 第一次卖出 +* 3 第二次买入 +* 4 第二次卖出 +* ..... + +**除了0以外,偶数就是卖出,奇数就是买入**。 + + +2. 确定递推公式 + +达到dp[i][1]状态,有两个具体操作: + +* 操作一:第i天买入股票了,那么dp[i][1] = dp[i - 1][0] - prices[i] +* 操作二:第i天没有操作,而是沿用前一天买入的状态,即:dp[i][1] = dp[i - 1][1] + +dp[i][1] = max(dp[i - 1][0] - prices[i], dp[i - 1][1]); + +同理dp[i][2]也有两个操作: + +* 操作一:第i天卖出股票了,那么dp[i][2] = dp[i - 1][1] + prices[i] +* 操作二:第i天没有操作,沿用前一天卖出股票的状态,即:dp[i][2] = dp[i - 1][2] + +dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]) + +同理可以类比剩下的状态,代码如下: + +```CPP +for (int j = 0; j < 2 * k - 1; j += 2) { + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); +} +``` + +整体代码如下: + +```CPP +class Solution { +public: + int maxProfit(int k, vector& prices) { + + if (prices.size() == 0) return 0; + vector> dp(prices.size(), vector(2 * k + 1, 0)); + for (int j = 1; j < 2 * k; j += 2) { + dp[0][j] = -prices[0]; + } + for (int i = 1;i < prices.size(); i++) { + for (int j = 0; j < 2 * k - 1; j += 2) { + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); + } + } + return dp[prices.size() - 1][2 * k]; + } +}; +``` + +当然有的解法是定义一个三维数组dp[i][j][k],第i天,第j次买卖,k表示买还是卖的状态,从定义上来讲是比较直观。但感觉三维数组操作起来有些麻烦,直接用二维数组来模拟三维数组的情况,代码看起来也清爽一些。 + +## 最佳买卖股票时机含冷冻期 + +[动态规划:309.最佳买卖股票时机含冷冻期](https://programmercarl.com/0309.最佳买卖股票时机含冷冻期.html)可以多次买卖但每次卖出有冷冻期1天。 + +相对于[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html),本题加上了一个冷冻期。 + + +在[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html) 中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。本题则可以花费为四个状态 + +dp[i][j]:第i天状态为j,所剩的最多现金为dp[i][j]。 + +具体可以区分出如下四个状态: + +* 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作) +* 卖出股票状态,这里就有两种卖出股票状态 + * 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态 + * 状态三:今天卖出了股票 +* 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天! + + +达到买入股票状态(状态一)即:dp[i][0],有两个具体操作: + +* 操作一:前一天就是持有股票状态(状态一),dp[i][0] = dp[i - 1][0] +* 操作二:今天买入了,有两种情况 + * 前一天是冷冻期(状态四),dp[i - 1][3] - prices[i] + * 前一天是保持卖出股票状态(状态二),dp[i - 1][1] - prices[i] + +所以操作二取最大值,即:max(dp[i - 1][3], dp[i - 1][1]) - prices[i] + +那么dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); + +达到保持卖出股票状态(状态二)即:dp[i][1],有两个具体操作: + +* 操作一:前一天就是状态二 +* 操作二:前一天是冷冻期(状态四) + +dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); + +达到今天就卖出股票状态(状态三),即:dp[i][2] ,只有一个操作: + +* 操作一:昨天一定是买入股票状态(状态一),今天卖出 + +即:dp[i][2] = dp[i - 1][0] + prices[i]; + +达到冷冻期状态(状态四),即:dp[i][3],只有一个操作: + +* 操作一:昨天卖出了股票(状态三) + +p[i][3] = dp[i - 1][2]; + +综上分析,递推代码如下: + +```CPP +dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3]- prices[i], dp[i - 1][1]) - prices[i]; +dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); +dp[i][2] = dp[i - 1][0] + prices[i]; +dp[i][3] = dp[i - 1][2]; +``` + +整体代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int n = prices.size(); + if (n == 0) return 0; + vector> dp(n, vector(4, 0)); + dp[0][0] -= prices[0]; // 持股票 + for (int i = 1; i < n; i++) { + dp[i][0] = max(dp[i - 1][0], max(dp[i - 1][3], dp[i - 1][1]) - prices[i]); + dp[i][1] = max(dp[i - 1][1], dp[i - 1][3]); + dp[i][2] = dp[i - 1][0] + prices[i]; + dp[i][3] = dp[i - 1][2]; + } + return max(dp[n - 1][3],max(dp[n - 1][1], dp[n - 1][2])); + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +## 买卖股票的最佳时机含手续费 + +[动态规划:714.买卖股票的最佳时机含手续费](https://programmercarl.com/0714.买卖股票的最佳时机含手续费(动态规划).html) 可以多次买卖,但每次有手续费。 + + +相对于[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html),本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 + +唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。 + +这里重申一下dp数组的含义: + +dp[i][0] 表示第i天持有股票所省最多现金。 +dp[i][1] 表示第i天不持有股票所得最多现金 + + +如果第i天持有股票即dp[i][0], 那么可以由两个状态推出来 +* 第i-1天就持有股票,那么就保持现状,所得现金就是昨天持有股票的所得现金 即:dp[i - 1][0] +* 第i天买入股票,所得现金就是昨天不持有股票的所得现金减去 今天的股票价格 即:dp[i - 1][1] - prices[i] + + +所以:dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); + + +在来看看如果第i天不持有股票即dp[i][1]的情况, 依然可以由两个状态推出来 +* 第i-1天就不持有股票,那么就保持现状,所得现金就是昨天不持有股票的所得现金 即:dp[i - 1][1] +* 第i天卖出股票,所得现金就是按照今天股票价格卖出后所得现金,**注意这里需要有手续费了**即:dp[i - 1][0] + prices[i] - fee + +所以:dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] + prices[i] - fee); + +**本题和[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html)的区别就是这里需要多一个减去手续费的操作**。 + +以上分析完毕,代码如下: + +```CPP +class Solution { +public: + int maxProfit(vector& prices, int fee) { + int n = prices.size(); + vector> dp(n, vector(2, 0)); + dp[0][0] -= prices[0]; // 持股票 + for (int i = 1; i < n; i++) { + 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 max(dp[n - 1][0], dp[n - 1][1]); + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + + +## 总结 + +至此,股票系列正式剧终,全部讲解完毕! + +从买卖一次到买卖多次,从最多买卖两次到最多买卖k次,从冷冻期再到手续费,最后再来一个股票大总结,可以说对股票系列完美收官了。 + +「代码随想录」值得推荐给身边每一位学习算法的朋友同学们,关注后都会发现相见恨晚! + + + + + diff --git "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222\346\200\273\347\273\223\347\257\207.md" "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\346\200\273\347\273\223\347\257\207.md" new file mode 100755 index 0000000000..32df8af41c --- /dev/null +++ "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,132 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 动态规划最强总结篇! + +如今动态规划已经讲解了42道经典题目,共50篇文章,是时候做一篇总结了。 + +关于动态规划,在专题第一篇[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)就说了动规五部曲,**而且强调了五部对解动规题目至关重要!** + +这是Carl做过一百多道动规题目总结出来的经验结晶啊,如果大家跟着「代码随想录」刷过动规专题,一定会对这动规五部曲的作用感受极其深刻。 + +动规五部曲分别为: + +1. 确定dp数组(dp table)以及下标的含义 +2. 确定递推公式 +3. dp数组如何初始化 +4. 确定遍历顺序 +5. 举例推导dp数组 + +动规专题刚开始的时候,讲的题目比较简单,不少录友和我反应:这么简单的题目 讲的复杂了,不用那么多步骤分析,想出递推公式直接就AC这道题目了。 + +**Carl的观点一直都是 简单题是用来 巩固方法论的**。 简单题目是可以靠感觉,但后面稍稍难一点的题目,估计感觉就不好使了。 + +在动规专题讲解中,也充分体现出,这动规五部曲的重要性。 + +还有不少录友对动规的理解是:递推公式是才是最难最重要的,只要想出递归公式,其他都好办。 + +**其实这么想的同学基本对动规理解的不到位的**。 + +动规五部曲里,哪一部没想清楚,这道题目基本就做不出来,即使做出来了也没有想清楚,而是朦朦胧胧的就把题目过了。 + +* 如果想不清楚dp数组的具体含义,递归公式从何谈起,甚至初始化的时候就写错了。 +* 例如[动态规划:不同路径还不够,要有障碍!](https://programmercarl.com/0063.不同路径II.html) 在这道题目中,初始化才是重头戏 +* 如果看过背包系列,特别是完全背包,那么两层for循环先后顺序绝对可以搞懵很多人,反而递归公式是简单的。 +* 至于推导dp数组的重要性,动规专题里几乎每篇Carl都反复强调,当程序结果不对的时候,一定要自己推导公式,看看和程序打印的日志是否一样。 + +好啦,我们再一起回顾一下,动态规划专题中我们都讲了哪些内容。 + +## 动态规划基础 + +* [关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html) +* [动态规划:斐波那契数](https://programmercarl.com/0509.斐波那契数.html) +* [动态规划:爬楼梯](https://programmercarl.com/0070.爬楼梯.html) +* [动态规划:使用最小花费爬楼梯](https://programmercarl.com/0746.使用最小花费爬楼梯.html) +* [动态规划:不同路径](https://programmercarl.com/0062.不同路径.html) +* [动态规划:不同路径还不够,要有障碍!](https://programmercarl.com/0063.不同路径II.html) +* [动态规划:整数拆分,你要怎么拆?](https://programmercarl.com/0343.整数拆分.html) +* [动态规划:不同的二叉搜索树](https://programmercarl.com/0096.不同的二叉搜索树.html) + +## 背包问题系列 + +背包问题大纲 + +* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) +* [动态规划:分割等和子集可以用01背包!](https://programmercarl.com/0416.分割等和子集.html) +* [动态规划:最后一块石头的重量 II](https://programmercarl.com/1049.最后一块石头的重量II.html) +* [动态规划:目标和!](https://programmercarl.com/0494.目标和.html) +* [动态规划:一和零!](https://programmercarl.com/0474.一和零.html) +* [动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html) +* [动态规划:给你一些零钱,你要怎么凑?](https://programmercarl.com/0518.零钱兑换II.html) +* [动态规划:Carl称它为排列总和!](https://programmercarl.com/0377.组合总和Ⅳ.html) +* [动态规划:以前我没得选,现在我选择再爬一次!](https://programmercarl.com/0070.爬楼梯完全背包版本.html) +* [动态规划: 给我个机会,我再兑换一次零钱](https://programmercarl.com/0322.零钱兑换.html) +* [动态规划:一样的套路,再求一次完全平方数](https://programmercarl.com/0279.完全平方数.html) +* [动态规划:单词拆分](https://programmercarl.com/0139.单词拆分.html) +* [动态规划:关于多重背包,你该了解这些!](https://programmercarl.com/背包问题理论基础多重背包.html) +* [听说背包问题很难? 这篇总结篇来拯救你了](https://programmercarl.com/背包总结篇.html) + +## 打家劫舍系列 + +* [动态规划:开始打家劫舍!](https://programmercarl.com/0198.打家劫舍.html) +* [动态规划:继续打家劫舍!](https://programmercarl.com/0213.打家劫舍II.html) +* [动态规划:还要打家劫舍!](https://programmercarl.com/0337.打家劫舍III.html) + +## 股票系列 + +股票问题总结 + +* [动态规划:买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) +* [动态规划:本周我们都讲了这些(系列六)](https://programmercarl.com/周总结/20210225动规周末总结.html) +* [动态规划:买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html) +* [动态规划:买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html) +* [动态规划:买卖股票的最佳时机IV](https://programmercarl.com/0188.买卖股票的最佳时机IV.html) +* [动态规划:最佳买卖股票时机含冷冻期](https://programmercarl.com/0309.最佳买卖股票时机含冷冻期.html) +* [动态规划:本周我们都讲了这些(系列七)](https://programmercarl.com/周总结/20210304动规周末总结.html) +* [动态规划:买卖股票的最佳时机含手续费](https://programmercarl.com/0714.买卖股票的最佳时机含手续费(动态规划).html) +* [动态规划:股票系列总结篇](https://programmercarl.com/动态规划-股票问题总结篇.html) + +## 子序列系列 + + + +* [动态规划:最长递增子序列](https://programmercarl.com/0300.最长上升子序列.html) +* [动态规划:最长连续递增序列](https://programmercarl.com/0674.最长连续递增序列.html) +* [动态规划:最长重复子数组](https://programmercarl.com/0718.最长重复子数组.html) +* [动态规划:最长公共子序列](https://programmercarl.com/1143.最长公共子序列.html) +* [动态规划:不相交的线](https://programmercarl.com/1035.不相交的线.html) +* [动态规划:最大子序和](https://programmercarl.com/0053.最大子序和(动态规划).html) +* [动态规划:判断子序列](https://programmercarl.com/0392.判断子序列.html) +* [动态规划:不同的子序列](https://programmercarl.com/0115.不同的子序列.html) +* [动态规划:两个字符串的删除操作](https://programmercarl.com/0583.两个字符串的删除操作.html) +* [动态规划:编辑距离](https://programmercarl.com/0072.编辑距离.html) +* [为了绝杀编辑距离,我做了三步铺垫,你都知道么?](https://programmercarl.com/为了绝杀编辑距离,卡尔做了三步铺垫.html) +* [动态规划:回文子串](https://programmercarl.com/0647.回文子串.html) +* [动态规划:最长回文子序列](https://programmercarl.com/0516.最长回文子序列.html) + + +## 动规结束语 + +关于动规,还有 树形DP(打家劫舍系列里有一道),数位DP,区间DP ,概率型DP,博弈型DP,状态压缩dp等等等,这些我就不去做讲解了,面试中出现的概率非常低。 + +能把本篇中列举的题目都研究通透的话,你的动规水平就已经非常高了。 对付面试已经足够! + + +![](https://kstar-1253855093.cos.ap-nanjing.myqcloud.com/baguwenpdf/_%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE_%E9%9D%92.png) + +这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[青](https://wx.zsxq.com/dweb2/index/footprint/185251215558842),所画,总结的非常好,分享给大家。 + +这应该是全网对动规最深刻的讲解系列了。 + +**其实大家去网上搜一搜也可以发现,能把动态规划讲清楚的资料挺少的,因为动规确实很难!要给别人讲清楚更难!** + +《剑指offer》上 动规的题目很少,经典的算法书籍《算法4》 没有讲 动规,而《算法导论》讲的动规基本属于劝退级别的。 + +讲清楚一道题容易,讲清楚两道题也容易,但把整个动态规划的各个分支讲清楚,每道题目讲通透,并用一套方法论把整个动规贯彻始终就非常难了。 + +所以Carl花费的这么大精力,把自己对动规算法理解 一五一十的全部分享给了录友们,帮助大家少走弯路,加油! + + diff --git "a/problems/\345\212\250\346\200\201\350\247\204\345\210\222\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\347\220\206\350\256\272\345\237\272\347\241\200.md" new file mode 100755 index 0000000000..63f059798e --- /dev/null +++ "b/problems/\345\212\250\346\200\201\350\247\204\345\210\222\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -0,0 +1,132 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 动态规划理论基础 + +动态规划刷题大纲 + + + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[动态规划理论基础](https://www.bilibili.com/video/BV13Q4y197Wg),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 什么是动态规划 + +动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。 + +所以动态规划中每一个状态一定是由上一个状态推导出来的,**这一点就区分于贪心**,贪心没有状态推导,而是从局部直接选最优的, + +在[关于贪心算法,你该了解这些!](https://programmercarl.com/%E8%B4%AA%E5%BF%83%E7%AE%97%E6%B3%95%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%80.html)中我举了一个背包问题的例子。 + +例如:有N件物品和一个最多能背重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。 + +动态规划中dp[j]是由dp[j-weight[i]]推导出来的,然后取max(dp[j], dp[j - weight[i]] + value[i])。 + +但如果是贪心呢,每次拿物品选一个最大的或者最小的就完事了,和上一个状态没有关系。 + +所以贪心解决不了动态规划的问题。 + +**其实大家也不用死扣动规和贪心的理论区别,后面做做题目自然就知道了**。 + +而且很多讲解动态规划的文章都会讲最优子结构啊和重叠子问题啊这些,这些东西都是教科书的上定义,晦涩难懂而且不实用。 + +大家知道动规是由前一个状态推导出来的,而贪心是局部直接选最优的,对于刷题来说就够用了。 + +上述提到的背包问题,后序会详细讲解。 + +## 动态规划的解题步骤 + +做动规题目的时候,很多同学会陷入一个误区,就是以为把状态转移公式背下来,照葫芦画瓢改改,就开始写代码,甚至把题目AC之后,都不太清楚dp[i]表示的是什么。 + +**这就是一种朦胧的状态,然后就把题给过了,遇到稍稍难一点的,可能直接就不会了,然后看题解,然后继续照葫芦画瓢陷入这种恶性循环中**。 + +状态转移公式(递推公式)是很重要,但动规不仅仅只有递推公式。 + +**对于动态规划问题,我将拆解为如下五步曲,这五步都搞清楚了,才能说把动态规划真的掌握了!** + +1. 确定dp数组(dp table)以及下标的含义 +2. 确定递推公式 +3. dp数组如何初始化 +4. 确定遍历顺序 +5. 举例推导dp数组 + +一些同学可能想为什么要先确定递推公式,然后在考虑初始化呢? + +**因为一些情况是递推公式决定了dp数组要如何初始化!** + +后面的讲解中我都是围绕着这五点来进行讲解。 + +可能刷过动态规划题目的同学可能都知道递推公式的重要性,感觉确定了递推公式这道题目就解出来了。 + +其实 确定递推公式 仅仅是解题里的一步而已! + +一些同学知道递推公式,但搞不清楚dp数组应该如何初始化,或者正确的遍历顺序,以至于记下来公式,但写的程序怎么改都通过不了。 + +后序的讲解的大家就会慢慢感受到这五步的重要性了。 + +## 动态规划应该如何debug + + +相信动规的题目,很大部分同学都是这样做的。 + +看一下题解,感觉看懂了,然后照葫芦画瓢,如果能正好画对了,万事大吉,一旦要是没通过,就怎么改都通过不了,对 dp数组的初始化,递推公式,遍历顺序,处于一种黑盒的理解状态。 + +写动规题目,代码出问题很正常! + +**找问题的最好方式就是把dp数组打印出来,看看究竟是不是按照自己思路推导的!** + +一些同学对于dp的学习是黑盒的状态,就是不清楚dp数组的含义,不懂为什么这么初始化,递推公式背下来了,遍历顺序靠习惯就是这么写的,然后一鼓作气写出代码,如果代码能通过万事大吉,通过不了的话就凭感觉改一改。 + +这是一个很不好的习惯! + +**做动规的题目,写代码之前一定要把状态转移在dp数组的上具体情况模拟一遍,心中有数,确定最后推出的是想要的结果**。 + +然后再写代码,如果代码没通过就打印dp数组,看看是不是和自己预先推导的哪里不一样。 + +如果打印出来和自己预先模拟推导是一样的,那么就是自己的递归公式、初始化或者遍历顺序有问题了。 + +如果和自己预先模拟推导的不一样,那么就是代码实现细节有问题。 + +**这样才是一个完整的思考过程,而不是一旦代码出问题,就毫无头绪的东改改西改改,最后过不了,或者说是稀里糊涂的过了**。 + +这也是我为什么在动规五步曲里强调推导dp数组的重要性。 + +举个例子哈:在「代码随想录」刷题小分队微信群里,一些录友可能代码通过不了,会把代码抛到讨论群里问:我这里代码都已经和题解一模一样了,为什么通过不了呢? + +发出这样的问题之前,其实可以自己先思考这三个问题: + +* 这道题目我举例推导状态转移公式了么? +* 我打印dp数组的日志了么? +* 打印出来了dp数组和我想的一样么? + +**如果这灵魂三问自己都做到了,基本上这道题目也就解决了**,或者更清晰的知道自己究竟是哪一点不明白,是状态转移不明白,还是实现代码不知道该怎么写,还是不理解遍历dp数组的顺序。 + +然后再问问题,目的性就很强了,群里的小伙伴也可以快速知道提问者的疑惑了。 + +**注意这里不是说不让大家问问题哈, 而是说问问题之前要有自己的思考,问题要问到点子上!** + +**大家工作之后就会发现,特别是大厂,问问题是一个专业活,是的,问问题也要体现出专业!** + +如果问同事很不专业的问题,同事们会懒的回答,领导也会认为你缺乏思考能力,这对职场发展是很不利的。 + +所以大家在刷题的时候,就锻炼自己养成专业提问的好习惯。 + +## 总结 + +这一篇是动态规划的整体概述,讲解了什么是动态规划,动态规划的解题步骤,以及如何debug。 + +动态规划是一个很大的领域,今天这一篇讲解的内容是整个动态规划系列中都会使用到的一些理论基础。 + +在后序讲解中针对某一具体问题,还会讲解其对应的理论基础,例如背包问题中的01背包,leetcode上的题目都是01背包的应用,而没有纯01背包的问题,那么就需要在把对应的理论知识讲解一下。 + +大家会发现,我讲解的理论基础并不是教科书上各种动态规划的定义,错综复杂的公式。 + +这里理论基础篇已经是非常偏实用的了,每个知识点都是在解题实战中非常有用的内容,大家要重视起来哈。 + +今天我们开始新的征程了,你准备好了么? + + + diff --git "a/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" "b/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index c98eefb3af..9c92e3d6c3 --- "a/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" +++ "b/problems/\345\217\214\346\214\207\351\222\210\346\200\273\347\273\223.md" @@ -1,22 +1,15 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 又是一波总结 相信大家已经对双指针法很熟悉了,但是双指针法并不隶属于某一种数据结构,我们在讲解数组,链表,字符串都用到了双指针法,所有有必要针对双指针法做一个总结。 -# 数组篇 +# 双指针总结篇 +## 数组篇 -在[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA)中,原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。 +在[数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html)中,原地移除数组上的元素,我们说到了数组上的元素,不能真正的删除,只能覆盖。 一些同学可能会写出如下代码(伪代码): @@ -32,13 +25,13 @@ for (int i = 0; i < array.size(); i++) { 所以此时使用双指针法才展现出效率的优势:**通过两个指针在一个for循环下完成两个for循环的工作。** -# 字符串篇 +## 字符串篇 -在[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)中讲解了反转字符串,注意这里强调要原地反转,要不然就失去了题目的意义。 +在[字符串:这道题目,使用库函数一行代码搞定](https://programmercarl.com/0344.反转字符串.html)中讲解了反转字符串,注意这里强调要原地反转,要不然就失去了题目的意义。 -使用双指针法,**定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。**,时间复杂度是O(n)。 +使用双指针法,**定义两个指针(也可以说是索引下标),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。**,时间复杂度是O(n)。 -在[替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) 中介绍使用双指针填充字符串的方法,如果想把这道题目做到极致,就不要只用额外的辅助空间了! +在[替换空格](https://programmercarl.com/剑指Offer05.替换空格.html) 中介绍使用双指针填充字符串的方法,如果想把这道题目做到极致,就不要只用额外的辅助空间了! 思路就是**首先扩充数组到每个空格替换成"%20"之后的大小。然后双指针从后向前替换空格。** @@ -48,33 +41,33 @@ for (int i = 0; i < array.size(); i++) { **其实很多数组(字符串)填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** -那么在[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中,我们使用双指针法,用O(n)的时间复杂度完成字符串删除类的操作,因为题目要产出冗余空格。 +那么在[字符串:花式反转还不够!](https://programmercarl.com/0151.翻转字符串里的单词.html)中,我们使用双指针法,用O(n)的时间复杂度完成字符串删除类的操作,因为题目要删除冗余空格。 **在删除冗余空格的过程中,如果不注意代码效率,很容易写成了O(n^2)的时间复杂度。其实使用双指针法O(n)就可以搞定。** **主要还是大家用erase用的比较随意,一定要注意for循环下用erase的情况,一般可以用双指针写效率更高!** -# 链表篇 +## 链表篇 翻转链表是现场面试,白纸写代码的好题,考察了候选者对链表以及指针的熟悉程度,而且代码也不长,适合在白纸上写。 -在[链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg)中,讲如何使用双指针法来翻转链表,**只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。** +在[链表:听说过两天反转链表又写不出来了?](https://programmercarl.com/0206.翻转链表.html)中,讲如何使用双指针法来翻转链表,**只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表。** 思路还是很简单的,代码也不长,但是想在白纸上一次性写出bugfree的代码,并不是容易的事情。 -在链表中求环,应该是双指针在链表里最经典的应用,在[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)中讲解了如何通过双指针判断是否有环,而且还要找到环的入口。 +在链表中求环,应该是双指针在链表里最经典的应用,在[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中讲解了如何通过双指针判断是否有环,而且还要找到环的入口。 **使用快慢指针(双指针法),分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。** -那么找到环的入口,其实需要点简单的数学推理,我在文章中把找环的入口清清楚楚的推理的一遍,如果对找环入口不够清楚的同学建议自己看一看[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)。 +那么找到环的入口,其实需要点简单的数学推理,我在文章中把找环的入口清清楚楚的推理的一遍,如果对找环入口不够清楚的同学建议自己看一看[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)。 -# N数之和篇 +## N数之和篇 -在[哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)中,讲到使用哈希法可以解决1.两数之和的问题 +在[哈希表:解决了两数之和,那么能解决三数之和么?](https://programmercarl.com/0015.三数之和.html)中,讲到使用哈希法可以解决1.两数之和的问题 其实使用双指针也可以解决1.两数之和的问题,只不过1.两数之和求的是两个元素的下标,没法用双指针,如果改成求具体两个元素的数值就可以了,大家可以尝试用双指针做一个leetcode上两数之和的题目,就可以体会到我说的意思了。 -使用了哈希法解决了两数之和,但是哈希法并不使用于三数之和! +使用了哈希法解决了两数之和,但是哈希法并不适用于三数之和! 使用哈希法的过程中要把符合条件的三元组放进vector中,然后在去去重,这样是非常费时的,很容易超时,也是三数之和通过率如此之低的根源所在。 @@ -82,19 +75,21 @@ for (int i = 0; i < array.size(); i++) { 时间复杂度可以做到O(n^2),但还是比较费时的,因为不好做剪枝操作。 -所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,**通过前后两个指针不算向中间逼近,在一个for循环下完成两个for循环的工作。** +所以这道题目使用双指针法才是最为合适的,用双指针做这道题目才能就能真正体会到,**通过前后两个指针不断向中间逼近,在一个for循环下完成两个for循环的工作。** 只用双指针法时间复杂度为O(n^2),但比哈希法的O(n^2)效率高得多,哈希法在使用两层for循环的时候,能做的剪枝操作很有限。 -在[双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)中,讲到了四数之和,其实思路是一样的,**在三数之和的基础上再套一层for循环,依然是使用双指针法。** +在[双指针法:一样的道理,能解决四数之和](https://programmercarl.com/0018.四数之和.html)中,讲到了四数之和,其实思路是一样的,**在三数之和的基础上再套一层for循环,依然是使用双指针法。** 对于三数之和使用双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。 同样的道理,五数之和,n数之和都是在这个基础上累加。 -# 总结 +## 总结 -本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为O(n)。 +本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为 $O(n)$ 。 建议大家可以把文中涉及到的题目在好好做一做,琢磨琢磨,基本对双指针法就不在话下了。 + + diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20200927\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20200927\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..11dd298292 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20200927\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,206 @@ + +# 本周小结!(二叉树) + +**周日我做一个针对本周的打卡留言疑问以及在刷题群里的讨论内容做一下梳理吧。**,这样也有助于大家补一补本周的内容,消化消化。 + +**注意这个周末总结和系列总结还是不一样的(二叉树还远没有结束),这个总结是针对留言疑问以及刷题群里讨论内容的归纳。** + +## 周一 + +本周我们开始讲解了二叉树,在[关于二叉树,你该了解这些!](https://programmercarl.com/二叉树理论基础.html)中讲解了二叉树的理论基础。 + +有同学会把红黑树和二叉平衡搜索树弄分开了,其实红黑树就是一种二叉平衡搜索树,这两个树不是独立的,所以C++中map、multimap、set、multiset的底层实现机制是二叉平衡搜索树,再具体一点是红黑树。 + +对于二叉树节点的定义,C++代码如下: + +```CPP +struct TreeNode { + int val; + TreeNode *left; + TreeNode *right; + TreeNode(int x) : val(x), left(NULL), right(NULL) {} +}; +``` +对于这个定义中`TreeNode(int x) : val(x), left(NULL), right(NULL) {}` 有同学不清楚干什么的。 + +这是构造函数,这么说吧C语言中的结构体是C++中类的祖先,所以C++结构体也可以有构造函数。 + +构造函数也可以不写,但是new一个新的节点的时候就比较麻烦。 + +例如有构造函数,定义初始值为9的节点: + +``` +TreeNode* a = new TreeNode(9); +``` + +没有构造函数的话就要这么写: + +```CPP +TreeNode* a = new TreeNode(); +a->val = 9; +a->left = NULL; +a->right = NULL; +``` + +在介绍前中后序遍历的时候,有递归和迭代(非递归),还有一种牛逼的遍历方式:morris遍历。 + +morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为O(1),感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。 + +## 周二 + +在[二叉树:一入递归深似海,从此offer是路人](https://programmercarl.com/二叉树的递归遍历.html)中讲到了递归三要素,以及前中后序的递归写法。 + +文章中我给出了leetcode上三道二叉树的前中后序题目,但是看完[二叉树:一入递归深似海,从此offer是路人](https://programmercarl.com/二叉树的递归遍历.html),依然可以解决n叉树的前后序遍历,在leetcode上分别是 +* [589. N叉树的前序遍历](https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/) +* [590. N叉树的后序遍历](https://leetcode-cn.com/problems/n-ary-tree-postorder-traversal/) + +大家可以再去把这两道题目做了。 + +## 周三 + +在[二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html)中我们开始用栈来实现递归的写法,也就是所谓的迭代法。 + +细心的同学发现文中前后序遍历空节点是否入栈写法是不同的 + +其实空节点入不入栈都差不多,但感觉空节点不入栈确实清晰一些,符合文中动画的演示。 + +拿前序遍历来举例,空节点入栈: + +```CPP +class Solution { +public: + vector preorderTraversal(TreeNode* root) { + stack st; + vector result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); // 中 + st.pop(); + if (node != NULL) result.push_back(node->val); + else continue; + st.push(node->right); // 右 + st.push(node->left); // 左 + } + return result; + } +}; +``` + +前序遍历空节点不入栈的代码:(注意注释部分和上文的区别) + +```CPP +class Solution { +public: + vector preorderTraversal(TreeNode* root) { + stack st; + vector result; + if (root == NULL) return result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); // 中 + st.pop(); + result.push_back(node->val); + if (node->right) st.push(node->right); // 右(空节点不入栈) + if (node->left) st.push(node->left); // 左(空节点不入栈) + } + return result; + } +}; +``` + + +在实现迭代法的过程中,有同学问了:递归与迭代究竟谁优谁劣呢? + +从时间复杂度上其实迭代法和递归法差不多(在不考虑函数调用开销和函数调用产生的堆栈开销),但是空间复杂度上,递归开销会大一些,因为递归需要系统堆栈存参数返回值等等。 + +递归更容易让程序员理解,但收敛不好,容易栈溢出。 + +这么说吧,递归是方便了程序员,难为了机器(各种保存参数,各种进栈出栈)。 + +**在实际项目开发的过程中我们是要尽量避免递归!因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出。** + +## 周四 + +在[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://programmercarl.com/二叉树的统一迭代法.html)中我们使用空节点作为标记,给出了统一的前中后序迭代法。 + +此时又多了一种前中后序的迭代写法,那么有同学问了:前中后序迭代法是不是一定要统一来写,这样才算是规范。 + +其实没必要,还是自己感觉哪一种更好记就用哪种。 + +但是**一定要掌握前中后序一种迭代的写法,并不因为某种场景的题目一定要用迭代,而是现场面试的时候,面试官看你顺畅的写出了递归,一般会进一步考察能不能写出相应的迭代。** + +## 周五 + +在[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html)中我们介绍了二叉树的另一种遍历方式(图论中广度优先搜索在二叉树上的应用)即:层序遍历。 + +看完这篇文章,去leetcode上怒刷五题,文章中 编号107题目的样例图放错了(原谅我匆忙之间总是手抖),但不影响大家理解。 + +只有同学发现leetcode上“515. 在每个树行中找最大值”,也是层序遍历的应用,依然可以分分钟解决,所以就是一鼓作气解决六道了。 + +**层序遍历遍历相对容易一些,只要掌握基本写法(也就是框架模板),剩下的就是在二叉树每一行遍历的时候做做逻辑修改。** + +## 周六 + +在[二叉树:你真的会翻转二叉树么?](https://programmercarl.com/0226.翻转二叉树.html)中我们把翻转二叉树这么一道简单又经典的问题,充分的剖析了一波,相信就算做过这道题目的同学,看完本篇之后依然有所收获! + + +**文中我指的是递归的中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。** + +如果非要使用递归中序的方式写,也可以,如下代码就可以避免节点左右孩子翻转两次的情况: + +``` +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + if (root == NULL) return root; + invertTree(root->left); // 左 + swap(root->left, root->right); // 中 + invertTree(root->left); // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了 + return root; + } +}; +``` + +代码虽然可以,但这毕竟不是真正的递归中序遍历了。 + +但使用迭代方式统一写法的中序是可以的。 + +代码如下: + +``` +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 右 + st.push(node); // 中 + st.push(NULL); + if (node->left) st.push(node->left); // 左 + + } else { + st.pop(); + node = st.top(); + st.pop(); + swap(node->left, node->right); // 节点处理逻辑 + } + } + return root; + } +}; + + +``` + +为什么这个中序就是可以的呢,因为这是用栈来遍历,而不是靠指针来遍历,避免了递归法中翻转了两次的情况,大家可以画图理解一下,这里有点意思的。 + +## 总结 + +**本周我们都是讲解了二叉树,从理论基础到遍历方式,从递归到迭代,从深度遍历到广度遍历,最后再用了一个翻转二叉树的题目把我们之前讲过的遍历方式都串了起来。** + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201003\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201003\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..5b25e9c076 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201003\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,258 @@ +# 本周小结!(二叉树系列二) + +本周赶上了十一国庆,估计大家已经对本周末没什么概念了,但是我们该做总结还是要做总结的。 + +本周的主题其实是**简单但并不简单**,本周所选的题目大多是看一下就会的题目,但是大家看完本周的文章估计也发现了,二叉树的简单题目其实里面都藏了很多细节。 这些细节我都给大家展现了出来。 + + +## 周一 + +本周刚开始我们讲解了判断二叉树是否对称的写法, [二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)。 + +这道题目的本质是要比较两个树(这两个树是根节点的左右子树),遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。 + +而本题的迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,认识到这一点之后就发现:用队列,用栈,甚至用数组,都是可以的。 + +那么做完本题之后,再看如下两个题目。 +* [100.相同的树](https://leetcode.cn/problems/same-tree/description/) +* [572.另一个树的子树](https://leetcode.cn/problems/subtree-of-another-tree/) + +**[二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)中的递归法和迭代法只需要稍作修改其中一个树的遍历顺序,便可刷了100.相同的树。** + +100.相同的树的递归代码如下: + +```CPP +class Solution { +public: + bool compare(TreeNode* left, TreeNode* right) { + // 首先排除空节点的情况 + if (left == NULL && right != NULL) return false; + else if (left != NULL && right == NULL) return false; + else if (left == NULL && right == NULL) return true; + // 排除了空节点,再排除数值不相同的情况 + else if (left->val != right->val) return false; + + // 此时就是:左右节点都不为空,且数值相同的情况 + // 此时才做递归,做下一层的判断 + bool outside = compare(left->left, right->left); // 左子树:左、 右子树:左 (相对于求对称二叉树,只需改一下这里的顺序) + bool inside = compare(left->right, right->right); // 左子树:右、 右子树:右 + bool isSame = outside && inside; // 左子树:中、 右子树:中 (逻辑处理) + return isSame; + + } + bool isSymmetric(TreeNode* p, TreeNode* q) { + return compare(p, q); + } +}; +``` + +100.相同的树,精简之后代码如下: + +```CPP +class Solution { +public: + bool compare(TreeNode* left, TreeNode* right) { + if (left == NULL && right != NULL) return false; + else if (left != NULL && right == NULL) return false; + else if (left == NULL && right == NULL) return true; + else if (left->val != right->val) return false; + else return compare(left->left, right->left) && compare(left->right, right->right); + + } + bool isSameTree(TreeNode* p, TreeNode* q) { + return compare(p, q); + } +}; +``` + +100.相同的树,迭代法代码如下: + +```CPP +class Solution { +public: + + bool isSameTree(TreeNode* p, TreeNode* q) { + if (p == NULL && q == NULL) return true; + if (p == NULL || q == NULL) return false; + queue que; + que.push(p); + que.push(q); + while (!que.empty()) { + TreeNode* leftNode = que.front(); que.pop(); + TreeNode* rightNode = que.front(); que.pop(); + if (!leftNode && !rightNode) { + continue; + } + if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { + return false; + } + // 相对于求对称二叉树,这里两个树都要保持一样的遍历顺序 + que.push(leftNode->left); + que.push(rightNode->left); + que.push(leftNode->right); + que.push(rightNode->right); + } + return true; + } +}; + +``` + +而572.另一个树的子树,则和 100.相同的树几乎一样的了,大家可以直接AC了。 + +## 周二 + +在[二叉树:看看这些树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)中,我们讲解了如何求二叉树的最大深度。 + +本题可以使用前序,也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序呢求的是高度。 + +**而根节点的高度就是二叉树的最大深度**,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度,所以[二叉树:看看这些树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)中使用的是后序遍历。 + +本题当然也可以使用前序,代码如下:(**充分表现出求深度回溯的过程**) +```CPP +class Solution { +public: + int result; + void getDepth(TreeNode* node, int depth) { + result = depth > result ? depth : result; // 中 + + if (node->left == NULL && node->right == NULL) return ; + + if (node->left) { // 左 + depth++; // 深度+1 + getDepth(node->left, depth); + depth--; // 回溯,深度-1 + } + if (node->right) { // 右 + depth++; // 深度+1 + getDepth(node->right, depth); + depth--; // 回溯,深度-1 + } + return ; + } + int maxDepth(TreeNode* root) { + result = 0; + if (root == 0) return result; + getDepth(root, 1); + return result; + } +}; +``` + +**可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!** + +注意以上代码是为了把细节体现出来,简化一下代码如下: + +```CPP +class Solution { +public: + int result; + void getDepth(TreeNode* node, int depth) { + result = depth > result ? depth : result; // 中 + if (node->left == NULL && node->right == NULL) return ; + if (node->left) { // 左 + getDepth(node->left, depth + 1); + } + if (node->right) { // 右 + getDepth(node->right, depth + 1); + } + return ; + } + int maxDepth(TreeNode* root) { + result = 0; + if (root == 0) return result; + getDepth(root, 1); + return result; + } +}; +``` + +## 周三 + +在[二叉树:看看这些树的最小深度](https://programmercarl.com/0111.二叉树的最小深度.html)中,我们讲解如何求二叉树的最小深度, 这道题目要是稍不留心很容易犯错。 + +**注意这里最小深度是从根节点到最近叶子节点的最短路径上的节点数量。注意是叶子节点。** + +什么是叶子节点,左右孩子都为空的节点才是叶子节点! + +**求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。** + +注意到这一点之后 递归法和迭代法 都可以参照[二叉树:看看这些树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html)写出来。 + +## 周四 + +我们在[二叉树:我有多少个节点?](https://programmercarl.com/0222.完全二叉树的节点个数.html)中,讲解了如何求二叉树的节点数量。 + +这一天是十一长假的第一天,又是双节,所以简单一些,只要把之前两篇[二叉树:看看这些树的最大深度](https://programmercarl.com/0104.二叉树的最大深度.html), [二叉树:看看这些树的最小深度](https://programmercarl.com/0111.二叉树的最小深度.html)都认真看了的话,这道题目可以分分钟刷掉了。 + +估计此时大家对这一类求二叉树节点数量以及求深度应该非常熟练了。 + +## 周五 + +在[二叉树:我平衡么?](https://programmercarl.com/0110.平衡二叉树.html)中讲解了如何判断二叉树是否是平衡二叉树 + +今天讲解一道判断平衡二叉树的题目,其实 方法上我们之前讲解深度的时候都讲过了,但是这次我们通过这道题目彻底搞清楚二叉树高度与深度的问题,以及对应的遍历方式。 + +二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。 +二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。 + +**但leetcode中强调的深度和高度很明显是按照节点来计算的**。 + +关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。 + +当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。 + +虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。 + +**例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!** + +讲了这么多二叉树题目的迭代法,有的同学会疑惑,迭代法中究竟什么时候用队列,什么时候用栈? + +**如果是模拟前中后序遍历就用栈,如果是适合层序遍历就用队列,当然还是其他情况,那么就是 先用队列试试行不行,不行就用栈。** + +## 周六 + +在[二叉树:找我的所有路径?](https://programmercarl.com/0257.二叉树的所有路径.html)中正式涉及到了回溯,很多同学过了这道题目,可能都不知道自己使用了回溯,其实回溯和递归都是相伴相生的。最后我依然给出了迭代法的版本。 + +我在题解中第一个版本的代码会把回溯的过程充分体现出来,如果大家直接看简洁的代码版本,很可能就会忽略的回溯的存在。 + +我在文中也强调了这一点。 + +有的同学还不理解 ,文中精简之后的递归代码,回溯究竟隐藏在哪里了。 + +文中我明确的说了:**回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。** + +如果还不理解的话,可以把 +```CPP +traversal(cur->left, path + "->", result); +``` + +改成 +```CPP +string tmp = path + "->"; +traversal(cur->left, tmp, result); +``` +看看还行不行了,答案是这么写就不行了,因为没有回溯了。 + +## 总结 + +二叉树的题目,我都是使用了递归三部曲一步一步的把整个过程分析出来,而不是上来就给出简洁的代码。 + +一些同学可能上来就能写出代码,大体上也知道是为啥,可以自圆其说,但往细节一扣,就不知道了。 + +所以刚接触二叉树的同学,建议按照文章分析的步骤一步一步来,不要上来就照着精简的代码写(那样写完了也很容易忘的,知其然不知其所以然)。 + +**简短的代码看不出遍历的顺序,也看不出分析的逻辑,还会把必要的回溯的逻辑隐藏了,所以尽量按照原理分析一步一步来,写出来之后,再去优化代码。** + +大家加个油!! + + +> **相信很多小伙伴刷题的时候面对力扣上近两千道题目,感觉无从下手,我花费半年时间整理了Github项目:「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解,常用算法模板总结,以及难点视频讲解,按照list一道一道刷就可以了!star支持一波吧!** + +* 公众号:[代码随想录](https://img-blog.csdnimg.cn/20201210231711160.png) +* B站:[代码随想录](https://space.bilibili.com/525438321) +* Github:[leetcode-master](https://github.com/youngyangyang04/leetcode-master) +* 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64) + +![](https://file1.kamacoder.com/i/algo/2021013018121150.png) +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201010\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201010\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..94d95efd38 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201010\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,90 @@ + +# 本周小结!(二叉树系列三) + + +## 周一 + +在[二叉树:以为使用了递归,其实还隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html)中,通过leetcode [257.二叉树的所有路径这道题目](https://programmercarl.com/0257.二叉树的所有路径.html),讲解了递归如何隐藏着回溯,一些代码会把回溯的过程都隐藏了起来了,甚至刷过这道题的同学可能都不知道自己用了回溯。 + +文章中第一版代码把每一个细节都展示了输出来了,大家可以清晰的看到回溯的过程。 + +然后给出了第二版优化后的代码,分析了其回溯隐藏在了哪里,如果要把这个回溯扣出来的话,在第二版的基础上应该怎么改。 + +主要需要理解:**回溯隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。** + + +## 周二 + +在文章[二叉树:做了这么多题目了,我的左叶子之和是多少?](https://programmercarl.com/0404.左叶子之和.html) 中提供了另一个判断节点属性的思路,平时我们习惯了使用通过节点的左右孩子判断本节点的属性,但发现使用这个思路无法判断左叶子。 + +此时需要相连的三层之间构成的约束条件,也就是要通过节点的父节点以及孩子节点来判断本节点的属性。 + +这道题目可以扩展大家对二叉树的解题思路。 + + +## 周三 + +在[二叉树:我的左下角的值是多少?](https://programmercarl.com/0513.找树左下角的值.html)中的题目如果使用递归的写法还是有点难度的,层次遍历反而很简单。 + +题目其实就是要在树的**最后一行**找到**最左边的值**。 + +**如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。** + +在这篇文章中,我们使用递归算法实实在在的求了一次深度,然后使用靠左的遍历,保证求得靠左的最大深度,而且又一次使用了回溯。 + +如果对二叉树的高度与深度又有点模糊了,在看这里[二叉树:我平衡么?](https://programmercarl.com/0110.平衡二叉树.html),回忆一下吧。 + +[二叉树:我的左下角的值是多少?](https://programmercarl.com/0513.找树左下角的值.html)中把我们之前讲过的内容都过了一遍,此外,还用前序遍历的技巧求得了靠左的最大深度。 + +**求二叉树的各种最值,就想应该采用什么样的遍历顺序,确定了遍历循序,其实就和数组求最值一样容易了。** + + +## 周四 + +在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html)中通过两道题目,彻底说清楚递归函数的返回值问题。 + +一般情况下:**如果需要搜索整棵二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。** + +特别是有些时候 递归函数的返回值是bool类型,一些同学会疑惑为啥要加这个,其实就是为了找到一条边立刻返回。 + +其实还有一种就是后序遍历需要根据左右递归的返回值推出中间节点的状态,这种需要有返回值,例如[222.完全二叉树](https://programmercarl.com/0222.完全二叉树的节点个数.html),[110.平衡二叉树](https://programmercarl.com/0110.平衡二叉树.html),这几道我们之前也讲过。 + +## 周五 + +之前都是讲解遍历二叉树,这次该构造二叉树了,在[二叉树:构造二叉树登场!](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html)中,我们通过前序和中序,后序和中序,构造了唯一的一棵二叉树。 + +**构造二叉树有三个注意的点:** + +* 分割时候,坚持区间不变量原则,左闭右开,或者左闭右闭。 +* 分割的时候,注意后序 或者 前序已经有一个节点作为中间节点了,不能继续使用了。 +* 如何使用切割后的后序数组来切合中序数组?利用中序数组大小一定是和后序数组的大小相同这一特点来进行切割。 + +这道题目代码实现并不简单,大家啃下来之后,二叉树的构造应该不是问题了。 + +**最后我还给出了为什么前序和后序不能唯一构成一棵二叉树,因为没有中序遍历就无法确定左右部分,也就无法分割。** + +## 周六 + +知道了如何构造二叉树,那么使用一个套路就可以解决文章[二叉树:构造一棵最大的二叉树](https://programmercarl.com/0654.最大二叉树.html)中的问题。 + +**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下标索引直接在原数组上操作,这样可以节约时间和空间上的开销。** + +文章中我还给出了递归函数什么时候加if,什么时候不加if,其实就是控制空节点(空指针)是否进入递归,是不同的代码实现方式,都是可以的。 + +**一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。** + +## 总结 + +本周我们深度讲解了如下知识点: + +1. [递归中如何隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html) +2. [如何通过三层关系确定左叶子](https://programmercarl.com/0404.左叶子之和.html) +3. [如何通过二叉树深度来判断左下角的值](https://programmercarl.com/0513.找树左下角的值.html) +4. [递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://programmercarl.com/0112.路径总和.html) +5. [前序和中序,后序和中序构造唯一二叉树](https://programmercarl.com/0106.从中序与后序遍历序列构造二叉树.html) +6. [使用数组构造某一特性的二叉树](https://programmercarl.com/0654.最大二叉树.html) + +**如果大家一路跟下来,一定收获满满,如果周末不做这个总结,大家可能都不知道自己收获满满,啊哈!** + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201017\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201017\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..5276cdde6b --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201017\344\272\214\345\217\211\346\240\221\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,119 @@ + + +# 本周小结!(二叉树系列四) + +> 这已经是二叉树的第四周总结了,二叉树是非常重要的数据结构,也是面试中的常客,所以有必要一步一步帮助大家彻底掌握二叉树! + +## 周一 + +在[二叉树:合并两个二叉树](https://programmercarl.com/0617.合并二叉树.html)中讲解了如何合并两个二叉树,平时我们都习惯了操作一个二叉树,一起操作两个树可能还有点陌生。 + +其实套路是一样,只不过一起操作两个树的指针,我们之前讲过求 [二叉树:我对称么?](https://programmercarl.com/0101.对称二叉树.html)的时候,已经初步涉及到了 一起遍历两棵二叉树了。 + +**迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。** + +## 周二 + +周二开始讲解一个新的树,二叉搜索树,开始要换一个思路了,如果没有利用好二叉搜索树的特性,就容易把简单题做成了难题了。 + +学习[二叉搜索树的特性](https://programmercarl.com/0700.二叉搜索树中的搜索.html),还是比较容易的。 + +大多是二叉搜索树的题目,其实都离不开中序遍历,因为这样就是有序的。 + +至于迭代法,相信大家看到文章中如此简单的迭代法的时候,都会感动的痛哭流涕。 + +## 周三 + +了解了二搜索树的特性之后, 开始验证[一棵二叉树是不是二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html)。 + +首先在此强调一下二叉搜索树的特性: + +* 节点的左子树只包含小于当前节点的数。 +* 节点的右子树只包含大于当前节点的数。 +* 所有左子树和右子树自身必须也是二叉搜索树。 + +那么我们在验证二叉搜索树的时候,有两个陷阱: + +* 陷阱一 + +**不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**,而是左子树都小于中间节点,右子树都大于中间节点。 + +* 陷阱二 + +在一个有序序列求最值的时候,不要定义一个全局变量,然后遍历序列更新全局变量求最值。因为最值可能就是int 或者 longlong的最小值。 + +推荐要通过前一个数值(pre)和后一个数值比较(cur),得出最值。 + +**在二叉树中通过两个前后指针作比较,会经常用到**。 + +本文[二叉树:我是不是一棵二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html)中迭代法中为什么没有周一那篇那么简洁了呢,因为本篇是验证二叉搜索树,前提默认它是一棵普通二叉树,所以还是要回归之前老办法。 + +## 周四 + +了解了[二叉搜索树](https://programmercarl.com/0700.二叉搜索树中的搜索.html),并且知道[如何判断二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html),本篇就很简单了。 + +**要知道二叉搜索树和中序遍历是好朋友!** + +在[二叉树:搜索树的最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html)中强调了要利用搜索树的特性,把这道题目想象成在一个有序数组上求两个数最小差值,这就是一道送分题了。 + +**需要明确:在有序数组求任意两数最小值差等价于相邻两数的最小值差**。 + +同样本题也需要用pre节点记录cur节点的前一个节点。(这种写法一定要掌握) + +## 周五 + +此时大家应该知道遇到二叉搜索树,就想是有序数组,那么在二叉搜索树中求二叉搜索树众数就很简单了。 + +在[二叉树:我的众数是多少?](https://programmercarl.com/0501.二叉搜索树中的众数.html)中我给出了如果是普通二叉树,应该如何求众数的集合,然后进一步讲解了二叉搜索树应该如何求众数集合。 + +在求众数集合的时候有一个技巧,因为题目中众数是可以有多个的,所以一般的方法需要遍历两遍才能求出众数的集合。 + +**但可以遍历一遍就可以求众数集合,使用了适时清空结果集的方法**,这个方法还是很巧妙的。相信仔细读了文章的同学会惊呼其巧妙! + +**所以大家不要看题目简单了,就不动手做了,我选的题目,一般不会简单到不用动手的程度**。 + +## 周六 + +在[二叉树:公共祖先问题](https://programmercarl.com/0236.二叉树的最近公共祖先.html)中,我们开始讲解如何在二叉树中求公共祖先的问题,本来是打算和二叉搜索树一起讲的,但发现篇幅过长,所以先讲二叉树的公共祖先问题。 + +**如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。** + +这道题目的看代码比较简单,而且好像也挺好理解的,但是如果把每一个细节理解到位,还是不容易的。 + +主要思考如下几点: + +* 如何从底向上遍历? +* 遍历整棵树,还是遍历局部树? +* 如何把结果传到根节点的? + +这些问题都需要弄清楚,上来直接看代码的话,是可能想不到这些细节的。 + +公共祖先问题,还是有难度的,初学者还是需要慢慢消化! + +## 总结 + +本周我们讲了[如何合并两个二叉树](https://programmercarl.com/0617.合并二叉树.html),了解了如何操作两个二叉树。 + +然后开始另一种树:二叉搜索树,了解[二叉搜索树的特性](https://programmercarl.com/0700.二叉搜索树中的搜索.html),然后[判断一棵二叉树是不是二叉搜索树](https://programmercarl.com/0098.验证二叉搜索树.html)。 + +了解以上知识之后,就开始利用其特性,做一些二叉搜索树上的题目,[求最小绝对差](https://programmercarl.com/0530.二叉搜索树的最小绝对差.html),[求众数集合](https://programmercarl.com/0501.二叉搜索树中的众数.html)。 + +接下来,开始求二叉树与二叉搜索树的公共祖先问题,单篇篇幅原因,先单独介绍[普通二叉树如何求最近公共祖先](https://programmercarl.com/0236.二叉树的最近公共祖先.html)。 + +现在已经讲过了几种二叉树了,二叉树,二叉平衡树,完全二叉树,二叉搜索树,后面还会有平衡二叉搜索树。 那么一些同学难免会有混乱了,我针对如下三个问题,帮大家在捋顺一遍: + +1. 平衡二叉搜索树是不是二叉搜索树和平衡二叉树的结合? + +是的,是二叉搜索树和平衡二叉树的结合。 + +2. 平衡二叉树与完全二叉树的区别在于底层节点的位置? + +是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。 + +3. 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树? + +堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 **但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树**。 + +大家如果每天坚持跟下来,会发现又是充实的一周![机智] + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201030\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201030\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..e8e2948791 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201030\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,117 @@ + + +

+ + + + +

+ +-------------------------- + +# 本周小结!(回溯算法系列一) + +## 周一 + +本周我们正式开始了回溯算法系列,那么首先当然是概述。 + +在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中介绍了什么是回溯,回溯法的效率,回溯法解决的问题以及回溯法模板。 + +**回溯是递归的副产品,只要有递归就会有回溯**。 + +回溯法就是暴力搜索,并不是什么高效的算法,最多在剪枝一下。 + +回溯算法能解决如下问题: + +* 组合问题:N个数里面按一定规则找出k个数的集合 +* 排列问题:N个数按一定规则全排列,有几种排列方式 +* 切割问题:一个字符串按一定规则有几种切割方式 +* 子集问题:一个N个数的集合里有多少符合条件的子集 +* 棋盘问题:N皇后,解数独等等 + +是不是感觉回溯算法有点厉害了。 + +回溯法确实不好理解,所以需要把回溯法抽象为一个图形来理解就容易多了,每一道回溯法的题目都可以抽象为树形结构。 + +针对很多同学都写不好回溯,我在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)用回溯三部曲,分析了回溯算法,并给出了回溯法的模板。 + +这个模板会伴随整个回溯法系列! + +## 周二 + + +在[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)中,我们开始用回溯法解决第一道题目,组合问题。 + +我在文中开始的时候给大家列举k层for循环例子,进而得出都是同样是暴力解法,为什么要用回溯法。 + +**此时大家应该深有体会回溯法的魅力,用递归控制for循环嵌套的数量!** + +本题我把回溯问题抽象为树形结构,可以直观的看出其搜索的过程:**for循环横向遍历,递归纵向遍历,回溯不断调整结果集**。 + +## 周三 + +针对[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)还可以做剪枝的操作。 + +在[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中把回溯法代码做了剪枝优化,在文中我依然把问题抽象为一个树形结构,大家可以一目了然剪的究竟是哪里。 + +**剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够 题目要求的k个元素了,就没有必要搜索了**。 + +## 周四 + +在[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中,相当于 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)加了一个元素总和的限制。 + +整体思路还是一样的,本题的剪枝会好想一些,即:**已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉**。 + +在本题中,依然还可以有一个剪枝,就是[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中提到的,对for循环选择的起始范围的剪枝。 + +所以,剪枝的代码,可以把for循环,加上 `i <= 9 - (k - path.size()) + 1` 的限制! + +组合总和问题还有一些花样,下周还会介绍到。 + +## 周五 + +在[回溯算法:电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html)中,开始用多个集合来求组合,还是熟悉的模板题目,但是有一些细节。 + +例如这里for循环,可不像是在 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)和[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中从startIndex开始遍历的。 + +**因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)和[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)都是是求同一个集合中的组合!** + +如果大家在现场面试的时候,一定要注意各种输入异常的情况,例如本题输入1 * #按键。 + +其实本题不算难,但也处处是细节,还是要反复琢磨。 + +## 周六 + +因为之前链表系列没有写总结,虽然链表系列已经是两个月前的事情,但还是有必要补一下。 + +所以给出[链表:总结篇!](https://programmercarl.com/链表总结篇.html),这里对之前链表理论基础和经典题目进行了总结。 + +同时对[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中求环入口的问题又进行了补充证明,可以说把环形链表的方方面面都讲的很通透了,大家如果没有做过环形链表的题目一定要去做一做。 + +## 总结 + +相信通过这一周对回溯法的学习,大家已经掌握其题本套路了,也不会对回溯法那么畏惧了。 + +回溯法抽象为树形结构后,其遍历过程就是:**for循环横向遍历,递归纵向遍历,回溯不断调整结果集**。 + +这个是我做了很多回溯的题目,不断摸索其规律才总结出来的。 + +对于回溯法的整体框架,网上搜的文章这块一般都说不清楚,按照天上掉下来的代码对着讲解,不知道究竟是怎么来的,也不知道为什么要这么写。 + +所以,录友们刚开始学回溯法,起跑姿势就很标准了。 + +下周依然是回溯法,难度又要上升一个台阶了。 + +最后祝录友们周末愉快! + +**如果感觉「代码随想录」不错,就分享给身边的同学朋友吧,一起来学习算法!** + + + +------------------------ + +* 微信:[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw) +* B站:[代码随想录](https://space.bilibili.com/525438321) +* 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ) + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201107\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201107\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..9da360c2c6 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201107\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,169 @@ + +# 本周小结!(回溯算法系列二) + +> 例行每周小结 + +## 周一 + +在[回溯算法:求组合总和(二)](https://programmercarl.com/0039.组合总和.html)中讲解的组合总和问题,和以前的组合问题还都不一样。 + +本题和[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html),[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)和区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。 + +不少录友都是看到可以重复选择,就义无反顾的把startIndex去掉了。 + +**本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?** + +我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html),[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)。 + +如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[回溯算法:电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html) + +**注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路,后面我在讲解排列的时候会重点介绍**。 + +最后还给出了本题的剪枝优化,如下: + +``` +for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) +``` + +这个优化如果是初学者的话并不容易想到。 + +**在求和问题中,排序之后加剪枝是常见的套路!** + +在[回溯算法:求组合总和(二)](https://programmercarl.com/0039.组合总和.html)第一个树形结构没有画出startIndex的作用,**这里这里纠正一下,准确的树形结构如图所示:** + +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367.png) + +## 周二 + +在[回溯算法:求组合总和(三)](https://programmercarl.com/0040.组合总和II.html)中依旧讲解组合总和问题,本题集合元素会有重复,但要求解集不能包含重复的组合。 + +**所以难就难在去重问题上了**。 + +这个去重问题,相信做过的录友都知道有多么的晦涩难懂。网上的题解一般就说“去掉重复”,但说不清怎么个去重,代码一甩就完事了。 + +为了讲解这个去重问题,**我自创了两个词汇,“树枝去重”和“树层去重”**。 + +都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因**。 + +![40.组合总和II1](https://file1.kamacoder.com/i/algo/20201123202817973.png) + +我在图中将used的变化用橘黄色标注上,可以看出在candidates[i] == candidates[i - 1]相同的情况下: + +* used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 +* used[i - 1] == false,说明同一树层candidates[i - 1]使用过 + +**这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!** + +对于去重,其实排列问题也是一样的道理,后面我会讲到。 + + +## 周三 + +在[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html)中,我们开始讲解切割问题,虽然最后代码看起来好像是一道模板题,但是从分析到学会套用这个模板,是比较难的。 + +我列出如下几个难点: + +* 切割问题其实类似组合问题 +* 如何模拟那些切割线 +* 切割问题中递归如何终止 +* 在递归循环中如何截取子串 +* 如何判断回文 + +如果想到了**用求解组合问题的思路来解决 切割问题本题就成功一大半了**,接下来就可以对着模板照葫芦画瓢。 + +**但后序如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了**。 + +除了这些难点,**本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1**。 + +所以本题应该是一道hard题目了。 + +**本题的树形结构中,和代码的逻辑有一个小出入,已经判断不是回文的子串就不会进入递归了,纠正如下:** + +![131.分割回文串](https://file1.kamacoder.com/i/algo/20201123203228309.png) + + +## 周四 + +如果没有做过[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html)的话,[回溯算法:复原IP地址](https://programmercarl.com/0093.复原IP地址.html)这道题目应该是比较难的。 + +复原IP照[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html)就多了一些限制,例如只能分四段,而且还是更改字符串,插入逗点。 + +树形图如下: + +![93.复原IP地址](https://file1.kamacoder.com/i/algo/20201123203735933-20230310133532452.png) + +在本文的树形结构图中,我已经把详细的分析思路都画了出来,相信大家看了之后一定会思路清晰不少! + +本题还可以有一个剪枝,合法ip长度为12,如果s的长度超过了12就不是有效IP地址,直接返回! + +代码如下: + +``` +if (s.size() > 12) return result; // 剪枝 + +``` + +我之前给出的C++代码没有加这个限制,也没有超时,因为在第四段超过长度之后,就会截止了,所以就算给出特别长的字符串,搜索的范围也是有限的(递归只会到第三层),及时就会返回了。 + + +## 周五 + +在[回溯算法:求子集问题!](https://programmercarl.com/0078.子集.html)中讲解了子集问题,**在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果**。 + +如图: + +![78.子集](https://file1.kamacoder.com/i/algo/202011232041348.png) + + +认清这个本质之后,今天的题目就是一道模板题了。 + +其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了,本来我们就要遍历整棵树。 + +有的同学可能担心不写终止条件会不会无限递归? + +并不会,因为每次递归的下一层就是从i+1开始的。 + +如果要写终止条件,注意:`result.push_back(path);`要放在终止条件的上面,如下: + +``` +result.push_back(path); // 收集子集,要放在终止添加的上面,否则会漏掉自己 +if (startIndex >= nums.size()) { // 终止条件可以不加 + return; +} +``` + +## 周六 + +早起的哈希表系列没有总结,所以[哈希表:总结篇!(每逢总结必经典)](https://programmercarl.com/哈希表总结.html)如约而至。 + +可能之前大家做过很多哈希表的题目,但是没有串成线,总结篇来帮你串成线,捋顺哈希表的整个脉络。 + +大家对什么时候各种set与map比较疑惑,想深入了解红黑树,哈希之类的。 + +**如果真的只是想清楚什么时候使用各种set与map,不用看那么多,把[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)看了就够了**。 + +## 总结 + +本周我们依次介绍了组合问题,分割问题以及子集问题,子集问题还没有讲完,下周还会继续。 + +**我讲解每一种问题,都会和其他问题作对比,做分析,所以只要跟着细心琢磨相信对回溯又有新的认识**。 + +最近这两天题目有点难度,刚刚开始学回溯算法的话,按照现在这个每天一题的速度来,确实有点快,学起来吃力非常正常,这些题目都是我当初学了好几个月才整明白的。 + +**所以大家能跟上的话,已经很优秀了!** + +还有一些录友会很关心leetcode上的耗时统计。 + +这个是很不准确的,相同的代码多提交几次,大家就知道怎么回事了。 + +leetcode上的计时应该是以4ms为单位,有的多提交几次,多个4ms就多击败50%,所以比较夸张,如果程序运行是几百ms的级别,可以看看leetcode上的耗时,因为它的误差10几ms对最终影响不大。 + +**所以我的题解基本不会写击败百分之多少多少,没啥意义,时间复杂度分析清楚了就可以了**,至于回溯算法不用分析时间复杂度了,都是一样的爆搜,就看谁剪枝厉害了。 + +一些录友表示最近回溯算法看的实在是有点懵,回溯算法确实是晦涩难懂,可能视频的话更直观一些,我最近应该会在B站(同名:「代码随想录」)出回溯算法的视频,大家也可以看视频在回顾一波。 + +**就酱,又是充实的一周,做好本周总结,迎接下一周,冲!** + + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201112\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201112\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..031ddc0250 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201112\345\233\236\346\272\257\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,100 @@ + +# 本周小结!(回溯算法系列三) + +## 周一 + +在[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)中,开始针对子集问题进行去重。 + +本题就是[回溯算法:求子集问题!](https://programmercarl.com/0078.子集.html)的基础上加上了去重,去重我们在[回溯算法:求组合总和(三)](https://programmercarl.com/0040.组合总和II.html)也讲过了。 + +所以本题对大家应该并不难。 + +树形结构如下: + +![90.子集II](https://file1.kamacoder.com/i/algo/2020111217110449-20230310133150714.png) + +## 周二 + +在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中,处处都能看到子集的身影,但处处是陷阱,值得好好琢磨琢磨! + +树形结构如下: +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201112170832333-20230310133155209.png) + +[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)留言区大家有很多疑问,主要还是和[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)混合在了一起。 + +详细在[本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag)中给出了介绍! + +## 周三 + +我们已经分析了组合问题,分割问题,子集问题,那么[回溯算法:排列问题!](https://programmercarl.com/0046.全排列.html) 又不一样了。 + +排列是有序的,也就是说[1,2] 和[2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。 + +可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。 + +如图: +![46.全排列](https://file1.kamacoder.com/i/algo/20201112170304979-20230310133201250.png) + +**大家此时可以感受出排列问题的不同:** + +* 每层都是从0开始搜索而不是startIndex +* 需要used数组记录path里都放了哪些元素了 + +## 周四 + +排列问题也要去重了,在[回溯算法:排列问题(二)](https://programmercarl.com/0047.全排列II.html)中又一次强调了“树层去重”和“树枝去重”。 + +树形结构如下: + +![47.全排列II1](https://file1.kamacoder.com/i/algo/20201112171930470-20230310133206398.png) + +**这道题目神奇的地方就是used[i - 1] == false也可以,used[i - 1] == true也可以!** + +我就用输入: [1,1,1] 来举一个例子。 + +树层上去重(used[i - 1] == false),的树形结构如下: + +![47.全排列II2.png](https://file1.kamacoder.com/i/algo/20201112172230434-20230310133211392.png) + +树枝上去重(used[i - 1] == true)的树型结构如下: + +![47.全排列II3](https://file1.kamacoder.com/i/algo/20201112172327967-20230310133216389.png) + +**可以清晰的看到使用(used[i - 1] == false),即树层去重,效率更高!** + +## 性能分析 + +之前并没有分析各个问题的时间复杂度和空间复杂度,这次来说一说。 + +这块网上的资料鱼龙混杂,一些所谓的经典面试书籍根本不讲回溯算法,算法书籍对这块也避而不谈,感觉就像是算法里模糊的边界。 + +**所以这块就说一说我个人理解,对内容持开放态度,集思广益,欢迎大家来讨论!** + +子集问题分析: + +* 时间复杂度:$O(n × 2^n)$,因为每一个元素的状态无外乎取与不取,所以时间复杂度为$O(2^n)$,构造每一组子集都需要填进数组,又有需要$O(n)$,最终时间复杂度:$O(n × 2^n)$。 +* 空间复杂度:$O(n)$,递归深度为n,所以系统栈所用空间为$O(n)$,每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为$O(n)$。 + +排列问题分析: + +* 时间复杂度:$O(n!)$,这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。每个叶子节点都会有一个构造全排列填进数组的操作(对应的代码:`result.push_back(path)`),该操作的复杂度为$O(n)$。所以,最终时间复杂度为:n * n!,简化为$O(n!)$。 +* 空间复杂度:$O(n)$,和子集问题同理。 + +组合问题分析: + +* 时间复杂度:$O(n × 2^n)$,组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。 +* 空间复杂度:$O(n)$,和子集问题同理。 + +**一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!** + +## 总结 + +本周我们对[子集问题进行了去重](https://programmercarl.com/0090.子集II.html),然后介绍了和子集问题非常像的[递增子序列](https://programmercarl.com/0491.递增子序列.html),如果还保持惯性思维,这道题就可以掉坑里。 + +接着介绍了[排列问题!](https://programmercarl.com/0046.全排列.html),以及对[排列问题如何进行去重](https://programmercarl.com/0047.全排列II.html)。 + +最后我补充了子集问题,排列问题和组合问题的性能分析,给大家提供了回溯算法复杂度的分析思路。 + + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201126\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201126\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..d934270a27 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201126\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,115 @@ + +# 本周小结!(贪心算法系列一) + +## 周一 + +本周正式开始了贪心算法,在[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html)中,我们介绍了什么是贪心以及贪心的套路。 + +**贪心的本质是选择每一阶段的局部最优,从而达到全局最优。** + +有没有啥套路呢? + +**不好意思,贪心没套路,就刷题而言,如果感觉好像局部最优可以推出全局最优,然后想不到反例,那就试一试贪心吧!** + +而严格的数据证明一般有如下两种: + +* 数学归纳法 +* 反证法 + +数学就不在讲解范围内了,感兴趣的同学可以自己去查一查资料。 + +正是因为贪心算法有时候会感觉这是常识,本就应该这么做! 所以大家经常看到网上有人说这是一道贪心题目,有人说这不是。 + +这里说一下我的依据:**如果找到局部最优,然后推出整体最优,那么就是贪心**,大家可以参考哈。 + +## 周二 + + +在[贪心算法:分发饼干](https://programmercarl.com/0455.分发饼干.html)中讲解了贪心算法的第一道题目。 + +这道题目很明显能看出来是用贪心,也是入门好题。 + +我在文中给出**局部最优:大饼干喂给胃口大的,充分利用饼干尺寸喂饱一个,全局最优:喂饱尽可能多的小孩**。 + +很多录友都是用小饼干优先先喂饱小胃口的。 + +后来我想一想,虽然结果是一样的,但是大家的这个思考方式更好一些。 + +**因为用小饼干优先喂饱小胃口的 这样可以尽量保证最后省下来的是大饼干(虽然题目没有这个要求)!** + +所以还是小饼干优先先喂饱小胃口更好一些,也比较直观。 + +一些录友不清楚[贪心算法:分发饼干](https://programmercarl.com/0455.分发饼干.html)中时间复杂度是怎么来的? + +就是快排O(nlog n),遍历O(n),加一起就是还是O(nlogn)。 + +## 周三 + +接下来就要上一点难度了,要不然大家会误以为贪心算法就是常识判断一下就行了。 + +在[贪心算法:摆动序列](https://programmercarl.com/0376.摆动序列.html)中,需要计算最长摇摆序列。 + +其实就是让序列有尽可能多的局部峰值。 + +局部最优:删除单调坡度上的节点(不包括单调坡度两端的节点),那么这个坡度就可以有两个局部峰值。 + +整体最优:整个序列有最多的局部峰值,从而达到最长摆动序列。 + +在计算峰值的时候,还是有一些代码技巧的,例如序列两端的峰值如何处理。 + +这些技巧,其实还是要多看多用才会掌握。 + + +## 周四 + +在[贪心算法:最大子序和](https://programmercarl.com/0053.最大子序和.html)中,详细讲解了用贪心的方式来求最大子序列和,其实这道题目是一道动态规划的题目。 + +**贪心的思路为局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。从而推出全局最优:选取最大“连续和”** + +代码很简单,但是思路却比较难。还需要反复琢磨。 + +针对[贪心算法:最大子序和](https://programmercarl.com/0053.最大子序和.html)文章中给出的贪心代码如下; +```cpp +class Solution { +public: + int maxSubArray(vector& nums) { + int result = INT32_MIN; + int count = 0; + for (int i = 0; i < nums.size(); i++) { + count += nums[i]; + if (count > result) { // 取区间累计的最大值(相当于不断确定最大子序终止位置) + result = count; + } + if (count <= 0) count = 0; // 相当于重置最大子序起始位置,因为遇到负数一定是拉低总和 + } + return result; + } +}; +``` +不少同学都来问,如果数组全是负数这个代码就有问题了,如果数组里有int最小值这个代码就有问题了。 + +大家不要脑洞模拟哈,可以亲自构造一些测试数据试一试,就发现其实没有问题。 + +数组都为负数,result记录的就是最大的负数,如果数组里有int最小值,那么最终result就是int最小值。 + + +## 总结 + +本周我们讲解了[贪心算法的理论基础](https://programmercarl.com/贪心算法理论基础.html),了解了贪心本质:局部最优推出全局最优。 + +然后讲解了第一道题目[分发饼干](https://programmercarl.com/0455.分发饼干.html),还是比较基础的,可能会给大家一种贪心算法比较简单的错觉,因为贪心有时候接近于常识。 + +其实我还准备一些简单的贪心题目,甚至网上很多都质疑这些题目是不是贪心算法。这些题目我没有立刻发出来,因为真的会让大家感觉贪心过于简单,而忽略了贪心的本质:局部最优和全局最优两个关键点。 + +**所以我在贪心系列难度会有所交替,难的题目在于拓展思路,简单的题目在于分析清楚其贪心的本质,后续我还会发一些简单的题目来做贪心的分析。** + +在[摆动序列](https://programmercarl.com/0376.摆动序列.html)中大家就初步感受到贪心没那么简单了。 + +本周最后是[最大子序和](https://programmercarl.com/0053.最大子序和.html),这道题目要用贪心的方式做出来,就比较有难度,都知道负数加上正数之后会变小,但是这道题目依然会让很多人搞混淆,其关键在于:**不能让“连续和”为负数的时候加上下一个元素,而不是 不让“连续和”加上一个负数**。这块真的需要仔细体会! + + + + + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201203\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201203\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..70081e82e2 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201203\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,95 @@ + +# 本周小结!(贪心算法系列二) + +## 周一 + +一说到股票问题,一般都会想到动态规划,其实有时候贪心更有效! + +在[贪心算法:买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html)中,讲到只能多次买卖一支股票,如何获取最大利润。 + +**这道题目理解利润拆分是关键点!** 不要整块的去看,而是把整体利润拆为每天的利润,就很容易想到贪心了。 + +**局部最优:只收集每天的正利润,全局最优:得到最大利润**。 + +如果正利润连续上了,相当于连续持有股票,而本题并不需要计算具体的区间。 + +如图: + +![122.买卖股票的最佳时机II](https://file1.kamacoder.com/i/algo/2020112917480858.png) + +## 周二 + +在[贪心算法:跳跃游戏](https://programmercarl.com/0055.跳跃游戏.html)中是给你一个数组看能否跳到终点。 + +本题贪心的关键是:**不用拘泥于每次究竟跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的**。 + +**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!** + +贪心算法局部最优解:移动下标每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点 + +如果覆盖范围覆盖到了终点,就表示一定可以跳过去。 + +如图: + +![55.跳跃游戏](https://file1.kamacoder.com/i/algo/20201124154758229.png) + + +## 周三 + +这道题目:[贪心算法:跳跃游戏II](https://programmercarl.com/0045.跳跃游戏II.html)可就有点难了。 + +本题解题关键在于:**以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点**。 + +那么局部最优:求当前这步的最大覆盖,那么尽可能多走,到达覆盖范围的终点,只需要一步。整体最优:达到终点,步数最少。 + +如图: + +![45.跳跃游戏II](https://file1.kamacoder.com/i/algo/20201201232309103-20230310133110942.png) + +注意:**图中的移动下标是到当前这步覆盖的最远距离(下标2的位置),此时没有到终点,只能增加第二步来扩大覆盖范围**。 + +在[贪心算法:跳跃游戏II](https://programmercarl.com/0045.跳跃游戏II.html)中我给出了两个版本的代码。 + +其实本质都是超过当前覆盖范围,步数就加一,但版本一需要考虑当前覆盖最远距离下标是不是数组终点的情况。 + +而版本二就比较统一的,超过范围,步数就加一,但在移动下标的范围了做了文章。 + +即如果覆盖最远距离下标是倒数第二点:直接加一就行,默认一定可以到终点。如图: +![45.跳跃游戏II2](https://file1.kamacoder.com/i/algo/20201201232445286-20230310133115650.png) + +如果覆盖最远距离下标不是倒数第二点,说明本次覆盖已经到终点了。如图: +![45.跳跃游戏II1](https://file1.kamacoder.com/i/algo/20201201232338693-20230310133120115.png) + +有的录友认为版本一好理解,有的录友认为版本二好理解,其实掌握一种就可以了,也不用非要比拼一下代码的简洁性,简洁程度都差不多了。 + +我个人倾向于版本一的写法,思路清晰一点,版本二会有点绕。 + +## 周四 + +这道题目:[贪心算法:K次取反后最大化的数组和](https://programmercarl.com/1005.K次取反后最大化的数组和.html)就比较简单了,用简单题来讲一讲贪心的思想。 + +**这里其实用了两次贪心!** + +第一次贪心:局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。 + +处理之后,如果K依然大于0,此时的问题是一个有序正整数序列,如何转变K次正负,让 数组和 达到最大。 + +第二次贪心:局部最优:只找数值最小的正整数进行反转,当前数值可以达到最大(例如正整数数组{5, 3, 1},反转1 得到-1 比 反转5得到的-5 大多了),全局最优:整个 数组和 达到最大。 + +例外一位录友留言给出一个很好的建议,因为文中是使用快排,仔细看题,**题目中限定了数据范围是正负一百,所以可以使用桶排序**,这样时间复杂度就可以优化为$O(n)$了。但可能代码要复杂一些了。 + + +## 总结 + +大家会发现本周的代码其实都简单,但思路却很巧妙,并不容易写出来。 + +如果是第一次接触的话,其实很难想出来,就是接触过之后就会了,所以大家不用感觉自己想不出来而烦躁。 + +相信此时大家现在对贪心算法又有一个新的认识了,加油💪 + + + + + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201210\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201210\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..dec7511c21 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201210\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,123 @@ + + + +

+ + + + +

+ + +正好也给「算法汇总」添加一个新专题-算法性能分析,以后如果有空余时间还会陆续更新这个模块,大家如果经常看「算法汇总」的话,就会发现,「算法汇总」里已经更新的三个模块「编程素养」「求职」「算法性能分析」,内容越来越丰满了,大家现在就可以去看看哈。 + +后面在算法题目之余,我还会继续更新这几个模块的! + +# 周一 + +在[程序员的简历应该这么写!!(附简历模板)](https://programmercarl.com/前序/程序员简历.html)中以我自己的总结经验为例讲一讲大家应该如何写简历。 + +主要有如下几点: + +* 简历篇幅不要过长 +* 谨慎使用“精通” +* 拿不准的绝对不要写在简历上 +* 项目经验中要突出自己的贡献 +* 面试中如何变被动为主动 +* 博客的重要性 + +最后还给出我自己的简历模板。 + +每一个点我都在文章中详细讲解了应该怎么写,平时应该如何积累,以及面前如何准备。 + +如果大家把以上几点都注意到了,那就是一份优秀的简历了,至少简历上就没啥毛病,剩下的就看自己的技术功底和临场发挥了。 + +一些录友会问我学校不好怎么办,没有项目经验怎么办之类的问题。 + +其实这就不在简历技巧的范围内了。 + +对于学校的话,某些公司可能有硬性要求,但如果能力特别出众,机会也是很大的。 不过说实话,大家都是普通人,真正技术能力出众的选手毕竟是少数。 + +**而且面试其实挺看缘分的**,相信大家应该都遇到过这种情景:同一家公司面别人的时候问题贼简单,然后人家就顺利拿offer,一到自己面的时候难题就上来了。 + +至于项目经验,没有项目,就要自己找找项目来做。 + +我的Github上有一些我曾经写过的一些小项目,大家可以去看看:https://github.com/youngyangyang04 + +**最后就是要端正写简历的心态,写简历是在自己真实背景和水平下,把自己各个方面包装到极致!** + + +# 周二 + +在[关于时间复杂度,你不知道的都在这里!](https://programmercarl.com/前序/关于时间复杂度,你不知道的都在这里!.html)中详细讲解了时间复杂度,很多被大家忽略的内容,在文中都做了详细的解释。 + +文中涉及如下问题: + +* 究竟什么是大O?大O表示什么意思?严格按照大O的定义来说,快排应该是$O(n^2)$的算法! +* $O(n^2)$ 的算法为什么有时候比 $O(n)$ 的算法更优? +* 什么时间复杂度为什么可以忽略常数项? +* 如何简化复杂的时间复杂度表达式,原理是什么? +* $O(\log n)$ 中的log究竟是以谁为底? + +这些问题大家可能懵懵懂懂的了解一些,但一细问又答不上来。 + +相信看完本篇[关于时间复杂度,你不知道的都在这里!](https://programmercarl.com/前序/关于时间复杂度,你不知道的都在这里!.html),以上问题大家就理解的清晰多了。 + +文中最后还运用以上知识通过一道简单的题目具体分析了一下其时间复杂度,给出两种方法究竟谁最优。 + +可以说从理论到实战将时间复杂度讲的明明白白。 + + +# 周三 + +在[O(n)的算法居然超时了,此时的n究竟是多大?](https://programmercarl.com/前序/On的算法居然超时了,此时的n究竟是多大?.html)中介绍了大家在leetcode上提交代码经常遇到的一个问题-超时! + +估计很多录友知道算法超时了,但没有注意过 O(n)的算法,如果1s内出结果,这个n究竟是多大? + +文中从计算机硬件出发,分析计算机的计算性能,然后亲自做实验,整理出数据如下: + + +![程序超时1](https://file1.kamacoder.com/i/algo/20201208231559175-20230310133304038.png) + +**大家有一个数量级上的概念就可以了!** + +正如文中说到的,**作为一名合格的程序员,至少要知道我们的程序是1s后出结果还是一年后出结果**。 + + +# 周四 + +在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)中,讲一讲如果计算递归算法的时间复杂度。 + +递归的时间复杂度等于**递归的次数 * 每次递归中的操作次数**。 + +所以了解究竟递归了多少次就是重点。 + +文中通过一道简单的面试题:求x的n次方(**注意:这道面试题大厂面试官经常用!**),还原面试场景,来带大家深入了解一下递归的时间复杂度。 + +文中给出了四个版本的代码实现,并逐一分析了其时间复杂度。 + +此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了 $O(\log n)$ 的代码。 + +其本质是要对递归的时间复杂度有清晰的认识,才能运用递归来有效的解决问题! + +相信看了本篇之后,对递归的时间复杂度分析就已经有深刻的理解了。 + + +# 总结 + +本周讲解的内容都是经常被大家忽略的知识点,而通常这种知识点,才最能发现一位候选人的编程功底。 + +因为之前一直都是在持续更新算法题目的文章,这周说一说算法性能分析,感觉也是换了换口味。 + +同时大家也会发现,**大厂面试官最喜欢用“简单题”(就是看起来很简单,其实非常考验技术功底的题目),而不是要手撕红黑树之类的**。 + +所以基础很重要,本周我介绍的内容其实都不难,看过的话都懂了,都是基础内容,但很多同学都把这些内容忽略掉了。 + +这其实也正常,咱们上学的时候教科书上基本没有实用的重点,而一般求职算法书也不讲这些,所以这方面内容可以靠看「代码随想录」的文章,当然更要靠自己多琢磨,多专研,多实践! + +**下周开始恢复贪心题目系列**,后序有空我还会陆续讲一讲类似本周的基础内容,在「算法汇总」的那几个模块都会持续更新的。 + +就酱,「代码随想录」是技术公众号里的一抹清流,值得推荐给身边的朋友同学们! + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201217\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201217\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..cface1c9d3 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201217\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,101 @@ + + +# 本周小结!(贪心算法系列三) + +对于贪心,大多数同学都会感觉,不就是常识嘛,这算啥算法,那么本周的题目就可以带大家初步领略一下贪心的巧妙,贪心算法往往妙的出其不意。 + +## 周一 + +在[贪心算法:加油站](https://programmercarl.com/0134.加油站.html)中给出每一个加油站的汽油和开到这个加油站的消耗,问汽车能不能开一圈。 + +这道题目咋眼一看,感觉是一道模拟题,模拟一下汽车从每一个节点出发看看能不能开一圈,时间复杂度是O(n^2)。 + +即使用模拟这种情况,也挺考察代码技巧的。 + +**for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,对于本题的场景要善于使用while!** + +如果代码功力不到位,就模拟这种情况,可能写的也会很费劲。 + +本题的贪心解法,我给出两种解法。 + +对于解法一,其实我并不认为这是贪心,因为没有找出局部最优,而是直接从全局最优的角度上思考问题,但思路很巧妙,值得学习一下。 + +对于解法二,贪心的局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置。 + +这里是可以从局部最优推出全局最优的,想不出反例,那就试试贪心。 + +**解法二就体现出贪心的精髓,同时大家也会发现,虽然贪心是常识,有些常识并不容易,甚至很难!** + +## 周二 + +在[贪心算法:分发糖果](https://programmercarl.com/0135.分发糖果.html)中我们第一次接触了需要考虑两个维度的情况。 + +例如这道题,是先考虑左边呢,还是考虑右边呢? + +**先考虑哪一边都可以! 就别两边一起考虑,那样就把自己陷进去了**。 + +先贪心一边,局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果 + +如图: + +![135.分发糖果](https://file1.kamacoder.com/i/algo/20201117114916878-20230310133332759.png) + + +接着在贪心另一边,左孩子大于右孩子,左孩子的糖果就要比右孩子多。 + +此时candyVec[i](第i个小孩的糖果数量,左孩子)就有两个选择了,一个是candyVec[i + 1] + 1(从右孩子这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。 + +那么第二次贪心的局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。 + +局部最优可以推出全局最优。 + +如图: +![135.分发糖果1](https://file1.kamacoder.com/i/algo/20201117115658791-20230310133346127.png) + + +## 周三 + +在[贪心算法:柠檬水找零](https://programmercarl.com/0860.柠檬水找零.html)中我们模拟了买柠檬水找零的过程。 + +这道题目刚一看,可能会有点懵,这要怎么找零才能保证完整全部账单的找零呢? + +**但仔细一琢磨就会发现,可供我们做判断的空间非常少!** + +美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能! + +局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。 + +局部最优可以推出全局最优。 + +所以把能遇到的情况分析一下,只要分析到具体情况了,一下子就豁然开朗了。 + +这道题目其实是一道简单题,但如果一开始就想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。 + +## 周四 + +在[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中,我们再一次遇到了需要考虑两个维度的情况。 + +之前我们已经做过一道类似的了就是[贪心算法:分发糖果](https://programmercarl.com/0135.分发糖果.html),但本题比分发糖果难不少! + +[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中依然是要确定一边,然后在考虑另一边,两边一起考虑一定会蒙圈。 + +那么本题先确定k还是先确定h呢,也就是究竟先按h排序呢,还先按照k排序呢? + +这里其实很考察大家的思考过程,如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。 + +**所以先从大到小按照h排个序,再来贪心k**。 + +此时局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性。全局最优:最后都做完插入操作,整个队列满足题目队列属性。 + +局部最优可以推出全局最优,找不出反例,那么就来贪心。 + +## 总结 + +「代码随想录」里已经讲了十一道贪心题目了,大家可以发现在每一道题目的讲解中,我都是把什么是局部最优,和什么是全局最优说清楚。 + +虽然有时候感觉贪心就是常识,但如果真正是常识性的题目,其实是模拟题,就不是贪心算法了!例如[贪心算法:加油站](https://programmercarl.com/0134.加油站.html)中的贪心方法一,其实我就认为不是贪心算法,而是直接从全局最优的角度上来模拟,因为方法里没有体现局部最优的过程。 + +而且大家也会发现,贪心并没有想象中的那么简单,贪心往往妙的出其不意,触不及防! + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20201224\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20201224\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..71abb155c6 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20201224\350\264\252\345\277\203\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,105 @@ + +# 本周小结!(贪心算法系列四) + +## 周一 + +在[贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)中,我们开始讲解了重叠区间问题,用最少的弓箭射爆所有气球,其本质就是找到最大的重叠区间。 + +按照左边界进行排序后,如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭 + +如图: + +![452.用最少数量的箭引爆气球](https://file1.kamacoder.com/i/algo/20201123101929791-20230310133845522.png) + +模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了,从前向后遍历重复的只要跳过就可以的。 + +## 周二 + +在[贪心算法:无重叠区间](https://programmercarl.com/0435.无重叠区间.html)中要去掉最少的区间,来让所有区间没有重叠。 + +我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。 + +如图: + +![435.无重叠区间](https://file1.kamacoder.com/i/algo/20201221201553618.png) + +细心的同学就发现了,此题和 [贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)非常像。 + +弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。 + +把[贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html)代码稍做修改,就可以AC本题。 + +修改后的C++代码如下: + +```CPP +class Solution { +public: + // 按照区间左边界从大到小排序 + static bool cmp (const vector& a, const vector& b) { + return a[0] < b[0]; + } + int eraseOverlapIntervals(vector>& intervals) { + if (intervals.size() == 0) return 0; + sort(intervals.begin(), intervals.end(), cmp); + + int result = 1; + for (int i = 1; i < intervals.size(); i++) { + if (intervals[i][0] >= intervals[i - 1][1]) { // 需要要把> 改成 >= 就可以了 + result++; // 需要一支箭 + } + else { + intervals[i][1] = min(intervals[i - 1][1], intervals[i][1]); // 更新重叠气球最小右边界 + } + } + return intervals.size() - result; + } +}; +``` + +## 周三 + +[贪心算法:划分字母区间](https://programmercarl.com/0763.划分字母区间.html)中我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。 + +这道题目leetcode上标的是贪心,其实我不认为是贪心,因为没感受到局部最优和全局最优的关系。 + +但不影响这是一道好题,思路很不错,**通过字符出现最远距离取并集的方法,把出现过的字符都圈到一个区间里**。 + +解题过程分如下两步: + +* 统计每一个字符最后出现的位置 +* 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点 + +如图: + +![763.划分字母区间](https://file1.kamacoder.com/i/algo/20201222191924417-20230310133855435.png) + + +## 周四 + +[贪心算法:合并区间](https://programmercarl.com/0056.合并区间.html)中要合并所有重叠的区间。 + +相信如果录友们前几天区间问题的题目认真练习了,今天题目就应该算简单一些了。 + +按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。 + +具体操作:按照左边界从小到大排序之后,如果 intervals[i][0] < intervals[i - 1][1] 即intervals[i]左边界 < intervals[i - 1]右边界,则一定有重复,因为intervals[i]的左边界一定是大于等于intervals[i - 1]的左边界。 + +如图: + +![56.合并区间](https://file1.kamacoder.com/i/algo/20201223200632791-20230310133859587.png) + + +## 总结 + +本周的主题就是用贪心算法来解决区间问题,经过本周的学习,大家应该对区间的各种合并分割有一定程度的了解了。 + +其实很多区间的合并操作看起来都是常识,其实贪心算法有时候就是常识,但也别小看了贪心算法。 + +在[贪心算法:合并区间](https://programmercarl.com/0056.合并区间.html)中就说过,对于贪心算法,很多同学都是:「如果能凭常识直接做出来,就会感觉不到自己用了贪心, 一旦第一直觉想不出来, 可能就一直想不出来了」。 + +所以还是要多看多做多练习! + +**「代码随想录」里总结的都是经典题目,大家跟着练就节省了不少选择题目的时间了**。 + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210107\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210107\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..e74907013c --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210107\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,153 @@ +# 本周小结!(动态规划系列一) + +这周我们正式开始动态规划的学习! + +## 周一 + +在[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)中我们讲解了动态规划的基础知识。 + +首先讲一下动规和贪心的区别,其实大家不用太强调理论上的区别,做做题,就感受出来了。 + +然后我们讲了动规的五部曲: + +1. 确定dp数组(dp table)以及下标的含义 +2. 确定递推公式 +3. dp数组如何初始化 +4. 确定遍历顺序 +5. 举例推导dp数组 + +后序我们在讲解动规的题目时候,都离不开这五步! + +本周都是简单题目,大家可能会感觉 按照这五部来好麻烦,凭感觉随手一写,直接就过,越到后面越会感觉,凭感觉这个事还是不靠谱的。 + +最后我们讲了动态规划题目应该如何debug,相信一些录友做动规的题目,一旦报错也是凭感觉来改。 + +其实只要把dp数组打印出来,哪里有问题一目了然! + +**如果代码写出来了,一直AC不了,灵魂三问:** + +1. 这道题目我举例推导状态转移公式了么? +2. 我打印dp数组的日志了么? +3. 打印出来了dp数组和我想的一样么? + +专治各种代码写出来了但AC不了的疑难杂症。 + +## 周二 + +这道题目[动态规划:斐波那契数](https://programmercarl.com/0509.斐波那契数.html)是当之无愧的动规入门题。 + +简单题,我们就是用来了解方法论的,用动规五部曲走一遍,题目其实已经把递推公式,和dp数组如何初始化都给我们了。 + +## 周三 + +[动态规划:爬楼梯](https://programmercarl.com/0070.爬楼梯.html) 这道题目其实就是斐波那契数列。 + +但正常思考过程应该是推导完递推公式之后,发现这是斐波那契,而不是上来就知道这是斐波那契。 + +在这道题目的第三步,确认dp数组如何初始化,其实就可以看出来,对dp[i]定义理解的深度。 + +dp[0]其实就是一个无意义的存在,不用去初始化dp[0]。 + +有的题解是把dp[0]初始化为1,然后遍历的时候i从2开始遍历,这样是可以解题的,然后强行解释一波dp[0]应该等于1的含义。 + +一个严谨的思考过程,应该是初始化dp[1] = 1,dp[2] = 2,然后i从3开始遍历,代码如下: + +```CPP +dp[1] = 1; +dp[2] = 2; +for (int i = 3; i <= n; i++) { // 注意i是从3开始的 + dp[i] = dp[i - 1] + dp[i - 2]; +} +``` + +这个可以是面试的一个小问题,考察候选人对dp[i]定义的理解程度。 + +这道题目还可以继续深化,就是一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。 + +这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,所以后续我在讲解背包问题的时候,今天这道题还会拿从背包问题的角度上来再讲一遍。 + +这里我先给出我的实现代码: + +```CPP +class Solution { +public: + int climbStairs(int n) { + vector dp(n + 1, 0); + dp[0] = 1; + for (int i = 1; i <= n; i++) { + for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题 + if (i - j >= 0) dp[i] += dp[i - j]; + } + } + return dp[n]; + } +}; +``` + +代码中m表示最多可以爬m个台阶。 + +**以上代码不能运行哈,我主要是为了体现只要把m换成2,粘过去,就可以AC爬楼梯这道题,不信你就粘一下试试**。 + + +**此时我就发现一个绝佳的大厂面试题**,第一道题就是单纯的爬楼梯,然后看候选人的代码实现,如果把dp[0]的定义成1了,就可以发难了,为什么dp[0]一定要初始化为1,此时可能候选人就要强行给dp[0]应该是1找各种理由。那这就是一个考察点了,对dp[i]的定义理解的不深入。 + +然后可以继续发难,如果一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。这道题目leetcode上并没有原题,绝对是考察候选人算法能力的绝佳好题。 + +这一连套问下来,候选人算法能力如何,面试官心里就有数了。 + +**其实大厂面试最喜欢问题的就是这种简单题,然后慢慢变化,在小细节上考察候选人**。 + +这道绝佳的面试题我没有用过,如果录友们有面试别人的需求,就把这个套路拿去吧。 + +我在[通过一道面试题目,讲一讲递归算法的时间复杂度!](../前序/递归算法的时间复杂度.md)中,以我自己面试别人的真实经历,通过求x的n次方 这么简单的题目,就可以考察候选人对算法性能以及递归的理解深度,录友们可以看看,绝对有收获! + +## 周四 + +这道题目[动态规划:使用最小花费爬楼梯](https://programmercarl.com/0746.使用最小花费爬楼梯.html)就是在爬台阶的基础上加了一个花费, + +这道题描述也确实有点魔幻。 + +题目描述为:每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。 + +示例1: + +输入:cost = [10, 15, 20] +输出:15 + + +**从题目描述可以看出:要不是第一步不需要花费体力,要不就是第最后一步不需要花费体力,我个人理解:题意说的其实是第一步是要支付费用的!**。因为是当你爬上一个台阶就要花费对应的体力值! + +所以我定义的dp[i]意思是也是第一步是要花费体力的,最后一步不用花费体力了,因为已经支付了。 + +之后一些录友在留言区说 可以定义dp[i]为:第一步是不花费体力,最后一步是花费体力的。 + +所以代码也可以这么写: + +```CPP +class Solution { +public: + int minCostClimbingStairs(vector& cost) { + vector dp(cost.size() + 1); + dp[0] = 0; // 默认第一步都是不花费体力的 + dp[1] = 0; + for (int i = 2; i <= cost.size(); i++) { + dp[i] = min(dp[i - 1] + cost[i - 1], dp[i - 2] + cost[i - 2]); + } + return dp[cost.size()]; + } +}; +``` + +这么写看上去比较顺,但是就是感觉和题目描述的不太符。也没有必要这么细扣题意了,大家只要知道,题目的意思反正就是要不是第一步不花费,要不是最后一步不花费,都可以。 + +## 总结 + +本周题目简单一些,也非常合适初学者来练练手。 + +下周开始上难度了哈,然后大下周就开始讲解背包问题,好戏还在后面,录友们跟上哈。 + +学算法,认准「代码随想录」就够了,Carl带你打怪升级! + + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210114\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210114\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..7cedf63919 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210114\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,156 @@ +# 本周小结!(动态规划系列二) + +## 周一 + +[动态规划:不同路径](https://programmercarl.com/0062.不同路径.html)中求从出发点到终点有几种路径,只能向下或者向右移动一步。 + +我们提供了三种方法,但重点讲解的还是动规,也是需要重点掌握的。 + +**dp[i][j]定义 :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径**。 + +本题在初始化的时候需要点思考了,即: + +dp[i][0]一定都是1,因为从(0, 0)的位置到(i, 0)的路径只有一条,那么dp[0][j]也同理。 + +所以初始化为: + +``` +for (int i = 0; i < m; i++) dp[i][0] = 1; +for (int j = 0; j < n; j++) dp[0][j] = 1; +``` + +这里已经不像之前做过的题目,随便赋个0就行的。 + +遍历顺序以及递推公式: + +``` +for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } +} +``` + +![62.不同路径1](https://file1.kamacoder.com/i/algo/20201209113631392-20230310133703294.png) + + +## 周二 + +[动态规划:不同路径还不够,要有障碍!](https://programmercarl.com/0063.不同路径II.html)相对于[动态规划:不同路径](https://programmercarl.com/0062.不同路径.html)添加了障碍。 + +dp[i][j]定义依然是:表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。 + + +本题难点在于初始化,如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。 + +如图: + +![63.不同路径II](https://file1.kamacoder.com/i/algo/20210104114513928-20230310133707783.png) + + +这里难住了不少同学,代码如下: + +``` +vector> dp(m, vector(n, 0)); +for (int i = 0; i < m && obstacleGrid[i][0] == 0; i++) dp[i][0] = 1; +for (int j = 0; j < n && obstacleGrid[0][j] == 0; j++) dp[0][j] = 1; +``` + + +递推公式只要考虑一下障碍,就不赋值了就可以了,如下: + +``` +for (int i = 1; i < m; i++) { + for (int j = 1; j < n; j++) { + if (obstacleGrid[i][j] == 1) continue; + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } +} +``` + +拿示例1来举例如题: + +![63.不同路径II1](https://file1.kamacoder.com/i/algo/20210104114548983-20230310133711888.png) + +对应的dp table 如图: + +![63.不同路径II2](https://file1.kamacoder.com/i/algo/20210104114610256-20230310133715981.png) + + +## 周三 + +[动态规划:整数拆分,你要怎么拆?](https://programmercarl.com/0343.整数拆分.html)给出一个整数,问有多少种拆分的方法。 + +这道题目就有点难度了,题目中dp我也给出了两种方法,但通过两种方法的比较可以看出,对dp数组定义的理解,以及dp数组初始化的重要性。 + + +**dp[i]定义:分拆数字i,可以得到的最大乘积为dp[i]**。 + +本题中dp[i]的初始化其实也很有考究,严格从dp[i]的定义来说,dp[0] dp[1] 就不应该初始化,也就是没有意义的数值。 + +拆分0和拆分1的最大乘积是多少? + +这是无解的。 + +所以题解里我只初始化dp[2] = 1,从dp[i]的定义来说,拆分数字2,得到的最大乘积是1,这个没有任何异议! + +``` +vector dp(n + 1); +dp[2] = 1; +``` + +遍历顺序以及递推公式: + +``` +for (int i = 3; i <= n ; i++) { + for (int j = 1; j < i - 1; j++) { + dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j)); + } +} +``` + +举例当n为10 的时候,dp数组里的数值,如下: + +![343.整数拆分](https://file1.kamacoder.com/i/algo/20210104173021581-20230310133720552.png) + + + +一些录友可能对为什么没有拆分j没有想清楚。 + +其实可以模拟一下哈,拆分j的情况,在遍历j的过程中dp[i - j]其实都计算过了。 + +例如 i= 10,j = 5,i-j = 5,如果把j拆分为 2 和 3,其实在j = 2 的时候,i-j= 8 ,拆分i-j的时候就可以拆出来一个3了。 + +**或者也可以理解j是拆分i的第一个整数**。 + +[动态规划:整数拆分,你要怎么拆?](https://programmercarl.com/0343.整数拆分.html)总结里,我也给出了递推公式dp[i] = max(dp[i], dp[i - j] * dp[j])这种写法。 + +对于这种写法,一位录友总结的很好,意思就是:如果递推公式是dp[i-j] * dp[j],这样就相当于强制把一个数至少拆分成四份。 + +dp[i-j]至少是两个数的乘积,dp[j]又至少是两个数的乘积,但其实3以下的数,数的本身比任何它的拆分乘积都要大了,所以文章中初始化的时候才要特殊处理。 + +## 周四 + +[动态规划:不同的二叉搜索树](https://programmercarl.com/0096.不同的二叉搜索树.html)给出n个不同的节点求能组成多少个不同二叉搜索树。 + +这道题目还是比较难的,想到用动态规划的方法就很不容易了! + +**dp[i]定义 :1到i为节点组成的二叉搜索树的个数为dp[i]**。 + +递推公式:dp[i] += dp[j - 1] * dp[i - j]; ,j-1 为j为头结点左子树节点数量,i-j 为以j为头结点右子树节点数量 + +dp数组如何初始化:只需要初始化dp[0]就可以了,推导的基础,都是dp[0]。 + +n为5时候的dp数组状态如图: + +![96.不同的二叉搜索树3](https://file1.kamacoder.com/i/algo/20210107093253987-20230310133724531.png) + +## 总结 + +本周题目已经开始点难度了,特别是[动态规划:不同的二叉搜索树](https://programmercarl.com/0096.不同的二叉搜索树.html)这道题目,明显感觉阅读量很低,可能是因为确实有点难吧。 + +我现在也陷入了纠结,题目一简单,就会有录友和我反馈说题目太简单了,题目一难,阅读量就特别低。 + +**但我还会坚持规划好的路线,难度循序渐进,并以面试经典题目为准,该简单的时候就是简单,同时也不会因为阅读量低就放弃有难度的题目!**。 + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210121\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210121\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..8ae7882e61 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210121\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,163 @@ + +# 本周小结!(动态规划系列三) + +本周我们正式开始讲解背包问题,也是动规里非常重要的一类问题。 + +背包问题其实有很多细节,如果了解个大概,然后也能一气呵成把代码写出来,但稍稍变变花样可能会陷入迷茫了。 + +开始回顾一下本周的内容吧! + +## 周一 + +[动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html)中,我们开始介绍了背包问题。 + +首先对于背包的所有问题中,01背包是最最基础的,其他背包也是在01背包的基础上稍作变化。 + +所以我才花费这么大精力去讲解01背包。 + +关于其他几种常用的背包,大家看这张图就了然于胸了: + +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20210117171307407-20230310133624872.png) + +本文用动规五部曲详细讲解了01背包的二维dp数组的实现方法,大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在确定初始化和遍历顺序上。 + +1. 确定dp数组以及下标的含义 + +dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 + +2. 确定递推公式 + +dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + +3. dp数组如何初始化 + +```CPP +// 初始化 dp +vector> dp(weight.size() + 1, vector(bagWeight + 1, 0)); +for (int j = bagWeight; j >= weight[0]; j--) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; +} +``` + +4. 确定遍历顺序 + +**01背包二维dp数组在遍历顺序上,外层遍历物品 ,内层遍历背包容量 和 外层遍历背包容量 ,内层遍历物品 都是可以的!** + +但是先遍历物品更好理解。代码如下: + +```CPP +// weight数组的大小 就是物品个数 +for(int i = 1; i < weight.size(); i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 这个是为了展现dp数组里元素的变化 + else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + + } +} +``` + +5. 举例推导dp数组 + +背包最大重量为4。 + +物品为: + +| | 重量 | 价值 | +| ----- | ---- | ---- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +来看一下对应的dp数组的数值,如图: + +![动态规划-背包问题4](https://file1.kamacoder.com/i/algo/20210118163425129-20230310133630224.jpg) + +最终结果就是dp[2][4]。 + + +## 周二 + +[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中把01背包的一维dp数组(滚动数组)实现详细讲解了一遍。 + +分析一下和二维dp数组有什么区别,在初始化和遍历顺序上又有什么差异? + +最后总结了一道朴实无华的背包面试题。 + +要求候选人先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。 + +然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么? + +这几个问题就可以考察出候选人的算法功底了。 + +01背包一维数组分析如下: + +1. 确定dp数组的定义 + +在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 + +2. 一维dp数组的递推公式 + +``` +dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); +``` + +3. 一维dp数组如何初始化 + +如果物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。 + +4. 一维dp数组遍历顺序 + +代码如下: + +```CPP +for(int i = 0; i < weight.size(); i++) { // 遍历物品 + for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + + } +} +``` + +5. 举例推导dp数组 + +一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下: + +![动态规划-背包问题9](https://file1.kamacoder.com/i/algo/20210110103614769-20230310133634873.png) + + +## 周三 + +[动态规划:416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)中我们开始用01背包来解决问题。 + +只有确定了如下四点,才能把01背包问题套到本题上来。 + +* 背包的体积为sum / 2 +* 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值 +* 背包如何正好装满,说明找到了总和为 sum / 2 的子集。 +* 背包中每一个元素是不可重复放入。 + +接下来就是一个完整的01背包问题,大家应该可以轻松做出了。 + +## 周四 + +[动态规划:1049. 最后一块石头的重量 II](https://programmercarl.com/1049.最后一块石头的重量II.html)这道题目其实和[动态规划:416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)是非常像的。 + +本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。 + +[动态规划:416. 分割等和子集](https://programmercarl.com/0416.分割等和子集.html)相当于是求背包是否正好装满,而本题是求背包最多能装多少。 + +这两道题目是对dp[target]的处理方式不同。这也考验的对dp[i]定义的理解。 + + +## 总结 + +总体来说,本周信息量还是比较大的,特别对于对动态规划还不够了解的同学。 + +但如果坚持下来把,我在文章中列出的每一个问题,都仔细思考,消化为自己的知识,那么进步一定是飞速的。 + +有的同学可能看了看背包递推公式,上来就能撸它几道题目,然后背包问题就这么过去了,其实这样是很不牢固的。 + +就像是我们讲解01背包的时候,花了那么大力气才把每一个细节都讲清楚,这里其实是基础,后面的背包问题怎么变,基础比较牢固自然会有自己的一套思考过程。 + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210128\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210128\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..ff3b771aa8 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210128\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,143 @@ +# 本周小结!(动态规划系列四) + +## 周一 + +[动态规划:目标和!](https://programmercarl.com/0494.目标和.html)要求在数列之间加入+ 或者 -,使其和为S。 + +所有数的总和为sum,假设加法的总和为x,那么可以推出x = (S + sum) / 2。 + +S 和 sum都是固定的,那此时问题就转化为01背包问题(数列中的数只能使用一次): 给你一些物品(数字),装满背包(就是x)有几种方法。 + +1. 确定dp数组以及下标的含义 + +**dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法** + +2. 确定递推公式 + +dp[j] += dp[j - nums[i]] + +**注意:求装满背包有几种方法类似的题目,递推公式基本都是这样的**。 + +3. dp数组如何初始化 + +dp[0] 初始化为1 ,dp[j]其他下标对应的数值应该初始化为0。 + +4. 确定遍历顺序 + +01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。 + + +5. 举例推导dp数组 + +输入:nums: [1, 1, 1, 1, 1], S: 3 + +bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4 + +dp数组状态变化如下: + +![494.目标和](https://file1.kamacoder.com/i/algo/20210125120743274-20230310132918821.jpg) + +## 周二 + +这道题目[动态规划:一和零!](https://programmercarl.com/0474.一和零.html)算有点难度。 + +**不少同学都以为是多重背包,其实这是一道标准的01背包**。 + +这不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。 + +**所以这是一个二维01背包!** + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。** + + +2. 确定递推公式 + +dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1); + +字符串集合中的一个字符串0的数量为zeroNum,1的数量为oneNum。 + +3. dp数组如何初始化 + +因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。 + +4. 确定遍历顺序 + +01背包一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历! + +5. 举例推导dp数组 + +以输入:["10","0001","111001","1","0"],m = 3,n = 3为例 + +最后dp数组的状态如下所示: + + +![474.一和零](https://file1.kamacoder.com/i/algo/20210120111201512-20230310132936011.jpg) + +## 周三 + +此时01背包我们就讲完了,正式开始完全背包。 + +在[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)中我们讲解了完全背包的理论基础。 + +其实完全背包和01背包区别就是完全背包的物品是无限数量。 + +递推公式也是一样的,但难点在于遍历顺序上! + +完全背包的物品是可以添加多次的,所以遍历背包容量要从小到大去遍历,即: + +```CPP +// 先遍历物品,再遍历背包 +for(int i = 0; i < weight.size(); i++) { // 遍历物品 + for(int j = weight[i]; j < bagWeight ; j++) { // 遍历背包容量 + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + + } +} +``` + +基本网上题的题解介绍到这里就到此为止了。 + +**那么为什么要先遍历物品,在遍历背包呢?** (灵魂拷问) + +其实对于纯完全背包,先遍历物品,再遍历背包 与 先遍历背包,再遍历物品都是可以的。我在文中[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)也给出了详细的解释。 + +这个细节是很多同学忽略掉的点,其实也不算细节了,**相信不少同学在写背包的时候,两层for循环的先后循序搞不清楚,靠感觉来的**。 + +所以理解究竟是先遍历啥,后遍历啥非常重要,这也体现出遍历顺序的重要性! + +在文中,我也强调了是对纯完全背包,两个for循环先后循序无所谓,那么题目稍有变化,可就有所谓了。 + +## 周四 + +在[动态规划:给你一些零钱,你要怎么凑?](https://programmercarl.com/0518.零钱兑换II.html)中就是给你一堆零钱(零钱个数无限),为凑成amount的组合数有几种。 + +**注意这里组合数和排列数的区别!** + +看到无限零钱个数就知道是完全背包, + +但本题不是纯完全背包了(求是否能装满背包),而是求装满背包有几种方法。 + +这里在遍历顺序上可就有说法了。 + +* 如果求组合数就是外层for循环遍历物品,内层for遍历背包。 +* 如果求排列数就是外层for遍历背包,内层for循环遍历物品。 + +这里同学们需要理解一波,我在文中也给出了详细的解释,下周我们将介绍求排列数的完全背包题目来加深对这个遍历顺序的理解。 + + +## 总结 + +相信通过本周的学习,大家已经初步感受到遍历顺序的重要性! + +很多对动规理解不深入的同学都会感觉:动规嘛,就是把递推公式推出来其他都easy了。 + +其实这是一种错觉,或者说对动规理解的不够深入! + +我在动规专题开篇介绍[关于动态规划,你该了解这些!](https://programmercarl.com/动态规划理论基础.html)中就强调了 **递推公式仅仅是 动规五部曲里的一小部分, dp数组的定义、初始化、遍历顺序,哪一点没有搞透的话,即使知道递推公式,遇到稍稍难一点的动规题目立刻会感觉写不出来了**。 + +此时相信大家对动规五部曲也有更深的理解了,同样也验证了Carl之前讲过的:**简单题是用来学习方法论的,而遇到难题才体现出方法论的重要性!** + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210204\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210204\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..e1e6baf503 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210204\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,203 @@ +# 本周小结!(动态规划系列五) + +## 周一 + +[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)中给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数(顺序不同的序列被视作不同的组合)。 + +题目面试虽然是组合,但又强调顺序不同的序列被视作不同的组合,其实这道题目求的是排列数! + +递归公式:dp[i] += dp[i - nums[j]]; + +这个和前上周讲的组合问题又不一样,关键就体现在遍历顺序上! + +在[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) 中就已经讲过了。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面! + +所以本题遍历顺序最终遍历顺序:**target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历**。 + +```CPP +class Solution { +public: + int combinationSum4(vector& nums, int target) { + vector dp(target + 1, 0); + dp[0] = 1; + for (int i = 0; i <= target; i++) { // 遍历背包 + for (int j = 0; j < nums.size(); j++) { // 遍历物品 + if (i - nums[j] >= 0 && dp[i] < INT_MAX - dp[i - nums[j]]) { + dp[i] += dp[i - nums[j]]; + } + } + } + return dp[target]; + } +}; +``` + +## 周二 + +爬楼梯之前我们已经做过了,就是斐波那契数列,很好解,但[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html)中我们进阶了一下。 + +改为:每次可以爬 1 、 2、.....、m 个台阶。问有多少种不同的方法可以爬到楼顶呢? + +1阶,2阶,.... m阶就是物品,楼顶就是背包。 + +每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。 + +问跳到楼顶有几种方法其实就是问装满背包有几种方法。 + +**此时大家应该发现这就是一个完全背包问题了!** + + +和昨天的题目[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)基本就是一道题了,遍历顺序也是一样一样的! + +代码如下: +```CPP +class Solution { +public: + int climbStairs(int n) { + vector dp(n + 1, 0); + dp[0] = 1; + for (int i = 1; i <= n; i++) { // 遍历背包 + for (int j = 1; j <= m; j++) { // 遍历物品 + if (i - j >= 0) dp[i] += dp[i - j]; + } + } + return dp[n]; + } +}; + +``` + +代码中m表示最多可以爬m个台阶,代码中把m改成2就是本题70.爬楼梯可以AC的代码了。 + +## 周三 + +[动态规划:322.零钱兑换](https://programmercarl.com/0322.零钱兑换.html)给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数(每种硬币的数量是无限的)。 + +这里我们都知道这是完全背包。 + +递归公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); + +关键看遍历顺序。 + +本题求钱币最小个数,**那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数**。 + +所以本题并不强调集合是组合还是排列。 + +**那么本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!** + + +外层for循环遍历物品,内层for遍历背包: +```CPP +// 版本一 +class Solution { +public: + int coinChange(vector& coins, int amount) { + vector dp(amount + 1, INT_MAX); + dp[0] = 0; + for (int i = 0; i < coins.size(); i++) { // 遍历物品 + for (int j = coins[i]; j <= amount; j++) { // 遍历背包 + if (dp[j - coins[i]] != INT_MAX) { // 如果dp[j - coins[i]]是初始值则跳过 + dp[j] = min(dp[j - coins[i]] + 1, dp[j]); + } + } + } + if (dp[amount] == INT_MAX) return -1; + return dp[amount]; + } +}; +``` + +外层for遍历背包,内层for循环遍历物品: + +```CPP +// 版本二 +class Solution { +public: + int coinChange(vector& coins, int amount) { + vector dp(amount + 1, INT_MAX); + dp[0] = 0; + for (int i = 1; i <= amount; i++) { // 遍历背包 + for (int j = 0; j < coins.size(); j++) { // 遍历物品 + if (i - coins[j] >= 0 && dp[i - coins[j]] != INT_MAX ) { + dp[i] = min(dp[i - coins[j]] + 1, dp[i]); + } + } + } + if (dp[amount] == INT_MAX) return -1; + return dp[amount]; + } +}; +``` + +## 周四 + +[动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html)给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少(平方数可以重复使用)。 + + +如果按顺序把前面的文章都看了,这道题目就是简单题了。 dp[i]的定义,递推公式,初始化,遍历顺序,都是和[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html) 一样一样的。 + +要是没有前面的基础上来做这道题,那这道题目就有点难度了。 + +**这也体现了刷题顺序的重要性**。 + +先遍历背包,再遍历物品: + +```CPP +// 版本一 +class Solution { +public: + int numSquares(int n) { + vector dp(n + 1, INT_MAX); + dp[0] = 0; + for (int i = 0; i <= n; i++) { // 遍历背包 + for (int j = 1; j * j <= i; j++) { // 遍历物品 + dp[i] = min(dp[i - j * j] + 1, dp[i]); + } + } + return dp[n]; + } +}; +``` + +先遍历物品,再遍历背包: + +```CPP +// 版本二 +class Solution { +public: + int numSquares(int n) { + vector dp(n + 1, INT_MAX); + dp[0] = 0; + for (int i = 1; i * i <= n; i++) { // 遍历物品 + for (int j = 1; j <= n; j++) { // 遍历背包 + if (j - i * i >= 0) { + dp[j] = min(dp[j - i * i] + 1, dp[j]); + } + } + } + return dp[n]; + } +}; +``` + + +## 总结 + +本周的主题其实就是背包问题中的遍历顺序! + +我这里做一下总结: + +求组合数:[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) +求排列数:[动态规划:377. 组合总和 Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html) +求最小数:[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)、[动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html) + +此时我们就已经把完全背包的遍历顺序研究的透透的了! + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210225\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210225\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..b17a682972 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210225\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,309 @@ +# 本周小结!(动态规划系列六) + +本周我们主要讲解了打家劫舍系列,这个系列也是dp解决的经典问题,那么来看看我们收获了哪些呢,一起来回顾一下吧。 + +## 周一 + +[动态规划:开始打家劫舍!](https://programmercarl.com/0198.打家劫舍.html)中就是给一个数组相邻之间不能连着偷,如何偷才能得到最大金钱。 + +1. 确定dp数组含义 + +**dp[i]:考虑下标i(包括i)以内的房屋,最多可以偷窃的金额为dp[i]**。 + +2. 确定递推公式 + +dp[i] = max(dp[i - 2] + nums[i], dp[i - 1]); + +3. dp数组如何初始化 + +``` +vector dp(nums.size()); +dp[0] = nums[0]; +dp[1] = max(nums[0], nums[1]); +``` + +4. 确定遍历顺序 + +从前到后遍历 + +5. 举例推导dp数组 + +以示例二,输入[2,7,9,3,1]为例。 + +![198.打家劫舍](https://file1.kamacoder.com/i/algo/20210221170954115-20230310133425353.jpg) + +红框dp[nums.size() - 1]为结果。 + +## 周二 + +[动态规划:继续打家劫舍!](https://programmercarl.com/0213.打家劫舍II.html)就是数组成环了,然后相邻的不能连着偷。 + +这里主要考虑清楚三种情况: + +* 情况一:考虑不包含首尾元素 + +![213.打家劫舍II](https://file1.kamacoder.com/i/algo/20210129160748643.jpg) + +* 情况二:考虑包含首元素,不包含尾元素 + +![213.打家劫舍II1](https://file1.kamacoder.com/i/algo/20210129160821374.jpg) + +* 情况三:考虑包含尾元素,不包含首元素 + +![213.打家劫舍II2](https://file1.kamacoder.com/i/algo/20210129160842491.jpg) + +需要注意的是,**“考虑” 不等于 “偷”**,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素!对于情况三,取nums[1] 和 nums[3]就是最大的。 + +所以情况二 和 情况三 都包含了情况一了,**所以只考虑情况二和情况三就可以了**。 + +成环之后还是难了一些的, 不少题解没有把“考虑房间”和“偷房间”说清楚。 + +这就导致大家会有这样的困惑:“情况三怎么就包含了情况一了呢?本文图中最后一间房不能偷啊,偷了一定不是最优结果”。 + +所以我在本文重点强调了情况一二三是“考虑”的范围,而具体房间偷与不偷交给递推公式去抉择。 + +剩下的就和[动态规划:开始打家劫舍!](https://programmercarl.com/0198.打家劫舍.html)是一个逻辑了。 + +## 周三 + +[动态规划:还要打家劫舍!](https://programmercarl.com/0337.打家劫舍III.html)这次是在一棵二叉树上打家劫舍了,条件还是一样的,相临的不能偷。 + +这道题目是树形DP的入门题目,其实树形DP其实就是在树上进行递推公式的推导,没有什么神秘的。 + +这道题目我给出了暴力的解法: + +```CPP +class Solution { +public: + int rob(TreeNode* root) { + if (root == NULL) return 0; + if (root->left == NULL && root->right == NULL) return root->val; + // 偷父节点 + int val1 = root->val; + if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left,相当于不考虑左孩子了 + if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right,相当于不考虑右孩子了 + // 不偷父节点 + int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子 + return max(val1, val2); + } +}; +``` + +当然超时了,因为我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。 + +那么使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。 + +代码如下: + +```CPP +class Solution { +public: + unordered_map umap; // 记录计算过的结果 + int rob(TreeNode* root) { + if (root == NULL) return 0; + if (root->left == NULL && root->right == NULL) return root->val; + if (umap[root]) return umap[root]; // 如果umap里已经有记录则直接返回 + // 偷父节点 + int val1 = root->val; + if (root->left) val1 += rob(root->left->left) + rob(root->left->right); // 跳过root->left + if (root->right) val1 += rob(root->right->left) + rob(root->right->right); // 跳过root->right + // 不偷父节点 + int val2 = rob(root->left) + rob(root->right); // 考虑root的左右孩子 + umap[root] = max(val1, val2); // umap记录一下结果 + return max(val1, val2); + } +}; +``` + +最后我们还是给出动态规划的解法。 + +因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解。 + +1. 确定递归函数的参数和返回值 + +```CPP +vector robTree(TreeNode* cur) { +``` + +dp数组含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。 + +**所以本题dp数组就是一个长度为2的数组!** + +那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢? + +**别忘了在递归的过程中,系统栈会保存每一层递归的参数**。 + +2. 确定终止条件 + +在遍历的过程中,如果遇到空间点的话,很明显,无论偷还是不偷都是0,所以就返回 + +``` +if (cur == NULL) return vector{0, 0}; +``` + +3. 确定遍历顺序 + +采用后序遍历,代码如下: + +```CPP +// 下标0:不偷,下标1:偷 +vector left = robTree(cur->left); // 左 +vector right = robTree(cur->right); // 右 +// 中 + +``` + +4. 确定单层递归的逻辑 + +如果是偷当前节点,那么左右孩子就不能偷,val1 = cur->val + left[0] + right[0]; + +如果不偷当前节点,那么左右孩子就可以偷,至于到底偷不偷一定是选一个最大的,所以:val2 = max(left[0], left[1]) + max(right[0], right[1]); + +最后当前节点的状态就是{val2, val1}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱} + +代码如下: + +```CPP +vector left = robTree(cur->left); // 左 +vector right = robTree(cur->right); // 右 + +// 偷cur +int val1 = cur->val + left[0] + right[0]; +// 不偷cur +int val2 = max(left[0], left[1]) + max(right[0], right[1]); +return {val2, val1}; +``` + +5. 举例推导dp数组 + +以示例1为例,dp数组状态如下:(**注意用后序遍历的方式推导**) + +![337.打家劫舍III](https://file1.kamacoder.com/i/algo/20210129181331613.jpg) + +**最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱**。 + + +树形DP为什么比较难呢? + +因为平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解! + +大家还记不记得我在讲解贪心专题的时候,讲到这道题目:[贪心算法:我要监控二叉树!](https://programmercarl.com/0968.监控二叉树.html),这也是贪心算法在树上的应用。**那我也可以把这个算法起一个名字,叫做树形贪心** + +“树形贪心”词汇从此诞生,来自「代码随想录」 + + +## 周四 + +[动态规划:买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html) 一段时间,只能买卖一次,问最大收益。 + +这里我给出了三种解法: + +暴力解法代码: + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int result = 0; + for (int i = 0; i < prices.size(); i++) { + for (int j = i + 1; j < prices.size(); j++){ + result = max(result, prices[j] - prices[i]); + } + } + return result; + } +}; +``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(1) + +贪心解法代码如下: + +因为股票就买卖一次,那么贪心的想法很自然就是取最左最小值,取最右最大值,那么得到的差值就是最大利润。 + +```CPP +class Solution { +public: + int maxProfit(vector& prices) { + int low = INT_MAX; + int result = 0; + for (int i = 0; i < prices.size(); i++) { + low = min(low, prices[i]); // 取最左最小价格 + result = max(result, prices[i] - low); // 直接取最大区间利润 + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +动规解法,版本一,代码如下: + +```CPP +// 版本一 +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + vector> dp(len, vector(2)); + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i][0] = max(dp[i - 1][0], -prices[i]); + dp[i][1] = max(dp[i - 1][1], prices[i] + dp[i - 1][0]); + } + return dp[len - 1][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +从递推公式可以看出,dp[i]只是依赖于dp[i - 1]的状态。 + + +那么我们只需要记录 当前天的dp状态和前一天的dp状态就可以了,可以使用滚动数组来节省空间,代码如下: + +```CPP +// 版本二 +class Solution { +public: + int maxProfit(vector& prices) { + int len = prices.size(); + vector> dp(2, vector(2)); // 注意这里只开辟了一个2 * 2大小的二维数组 + dp[0][0] -= prices[0]; + dp[0][1] = 0; + for (int i = 1; i < len; i++) { + dp[i % 2][0] = max(dp[(i - 1) % 2][0], -prices[i]); + dp[i % 2][1] = max(dp[(i - 1) % 2][1], prices[i] + dp[(i - 1) % 2][0]); + } + return dp[(len - 1) % 2][1]; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + + +建议先写出版本一,然后在版本一的基础上优化成版本二,而不是直接就写出版本二。 + + +## 总结 + +刚刚结束了背包问题,本周主要讲解打家劫舍系列。 + +**劫舍系列简单来说就是 数组上连续元素二选一,成环之后连续元素二选一,在树上连续元素二选一,所能得到的最大价值**。 + +那么这里每一种情况 我在文章中都做了详细的介绍。 + +周四我们开始讲解股票系列了,大家应该预测到了,我们下周的主题就是股票!敬请期待吧! + +**代码随想录温馨提醒:投资有风险,入市需谨慎!** + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/20210304\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" "b/problems/\345\221\250\346\200\273\347\273\223/20210304\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" new file mode 100644 index 0000000000..0749becbba --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/20210304\345\212\250\350\247\204\345\221\250\346\234\253\346\200\273\347\273\223.md" @@ -0,0 +1,209 @@ +# 本周小结!(动态规划系列七) + +本周的主题就是股票系列,来一起回顾一下吧 + +## 周一 + +[动态规划:买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html)中股票可以买卖多次了! + +这也是和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)的唯一区别(注意只有一只股票,所以再次购买前要出售掉之前的股票) + +重点在于递推公式的不同。 + +在回顾一下dp数组的含义: + +* dp[i][0] 表示第i天持有股票所得现金。 +* dp[i][1] 表示第i天不持有股票所得最多现金 + + +递推公式: + +``` +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]); +``` + +大家可以发现本题和[121. 买卖股票的最佳时机](https://programmercarl.com/0121.买卖股票的最佳时机.html)的代码几乎一样,唯一的区别在: + +``` +dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]); +``` + +**这正是因为本题的股票可以买卖多次!** 所以买入股票的时候,可能会有之前买卖的利润即:dp[i - 1][1],所以dp[i - 1][1] - prices[i]。 + +## 周二 + +[动态规划:买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html)中最多只能完成两笔交易。 + +**这意味着可以买卖一次,可以买卖两次,也可以不买卖**。 + + +1. 确定dp数组以及下标的含义 + +一天一共就有五个状态, + +0. 没有操作 +1. 第一次买入 +2. 第一次卖出 +3. 第二次买入 +4. 第二次卖出 + +**dp[i][j]中 i表示第i天,j为 [0 - 4] 五个状态,dp[i][j]表示第i天状态j所剩最大现金**。 + +2. 确定递推公式 + +需要注意:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。 + +``` +dp[i][1] = max(dp[i-1][0] - prices[i], dp[i - 1][1]); +dp[i][2] = max(dp[i - 1][1] + prices[i], dp[i - 1][2]); +dp[i][3] = max(dp[i - 1][3], dp[i - 1][2] - prices[i]); +dp[i][4] = max(dp[i - 1][4], dp[i - 1][3] + prices[i]); +``` + +3. dp数组如何初始化 + +dp[0][0] = 0; +dp[0][1] = -prices[0]; +dp[0][2] = 0; +dp[0][3] = -prices[0]; +dp[0][4] = 0; + +4. 确定遍历顺序 + +从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。 + +5. 举例推导dp数组 + +以输入[1,2,3,4,5]为例 + +![123.买卖股票的最佳时机III](https://file1.kamacoder.com/i/algo/20201228181724295.png) + +可以看到红色框为最后两次卖出的状态。 + +现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。 + +所以最终最大利润是dp[4][4] + +## 周三 + +[动态规划:买卖股票的最佳时机IV](https://programmercarl.com/0188.买卖股票的最佳时机IV.html)最多可以完成 k 笔交易。 + +相对于上一道[动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html),本题需要通过前两次的交易,来类比前k次的交易 + + +1. 确定dp数组以及下标的含义 + +使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j] + +j的状态表示为: + +* 0 表示不操作 +* 1 第一次买入 +* 2 第一次卖出 +* 3 第二次买入 +* 4 第二次卖出 +* ..... + +**除了0以外,偶数就是卖出,奇数就是买入**。 + + +2. 确定递推公式 + +还要强调一下:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。 + +```CPP +for (int j = 0; j < 2 * k - 1; j += 2) { + dp[i][j + 1] = max(dp[i - 1][j + 1], dp[i - 1][j] - prices[i]); + dp[i][j + 2] = max(dp[i - 1][j + 2], dp[i - 1][j + 1] + prices[i]); +} +``` + +**本题和[动态规划:123.买卖股票的最佳时机III](https://programmercarl.com/0123.买卖股票的最佳时机III.html)最大的区别就是这里要类比j为奇数是买,偶数是卖的状态**。 + +3. dp数组如何初始化 + +**dp[0][j]当j为奇数的时候都初始化为 -prices[0]** + +代码如下: + +```CPP +for (int j = 1; j < 2 * k; j += 2) { + dp[0][j] = -prices[0]; +} +``` + +**在初始化的地方同样要类比j为奇数是买、偶数是卖的状态**。 + +4. 确定遍历顺序 + +从递归公式其实已经可以看出,一定是从前向后遍历,因为dp[i],依靠dp[i - 1]的数值。 + +5. 举例推导dp数组 + +以输入[1,2,3,4,5],k=2为例。 + + +![188.买卖股票的最佳时机IV](https://file1.kamacoder.com/i/algo/20201229100358221-20230310133805763.png) + +最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解。 + +## 周四 + +[动态规划:最佳买卖股票时机含冷冻期](https://programmercarl.com/0309.最佳买卖股票时机含冷冻期.html)尽可能地完成更多的交易(多次买卖一支股票),但有冷冻期,冷冻期为1天 + +相对于[动态规划:122.买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II(动态规划).html),本题加上了一个冷冻期 + + +**本题则需要第三个状态:不持有股票(冷冻期)的最多现金**。 + +动规五部曲,分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]**。 + +j的状态为: + +* 0:持有股票后的最多现金 +* 1:不持有股票(能购买)的最多现金 +* 2:不持有股票(冷冻期)的最多现金 + +2. 确定递推公式 + +``` +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][2]); +dp[i][2] = dp[i - 1][0] + prices[i]; +``` + +3. dp数组如何初始化 + +可以统一都初始为0了。 + +代码如下: +```CPP +vector> dp(n, vector(3, 0)); +``` + +**初始化其实很有讲究,很多同学可能是稀里糊涂的全都初始化0,反正就可以通过,但没有想清楚,为什么都初始化为0**。 + +4. 确定遍历顺序 + +从递归公式上可以看出,dp[i] 依赖于 dp[i-1],所以是从前向后遍历。 + +5. 举例推导dp数组 + +以 [1,2,3,0,2] 为例,dp数组如下: + + +![309.最佳买卖股票时机含冷冻期](https://file1.kamacoder.com/i/algo/20201229163725348.png) + +最后两个状态 不持有股票(能购买) 和 不持有股票(冷冻期)都有可能最后结果,取最大的。 + +## 总结 + +下周还会有一篇股票系列的文章,**股票系列后面我也会单独写一篇总结,来高度概括一下,这样大家会对股票问题就有一个整体性的理解了**。 + + +
diff --git "a/problems/\345\221\250\346\200\273\347\273\223/\344\272\214\345\217\211\346\240\221\351\230\266\346\256\265\346\200\273\347\273\223\347\263\273\345\210\227\344\270\200.md" "b/problems/\345\221\250\346\200\273\347\273\223/\344\272\214\345\217\211\346\240\221\351\230\266\346\256\265\346\200\273\347\273\223\347\263\273\345\210\227\344\270\200.md" new file mode 100644 index 0000000000..6cbf6e2a77 --- /dev/null +++ "b/problems/\345\221\250\346\200\273\347\273\223/\344\272\214\345\217\211\346\240\221\351\230\266\346\256\265\346\200\273\347\273\223\347\263\273\345\210\227\344\270\200.md" @@ -0,0 +1,209 @@ +# 本周小结!(二叉树) + +**周日我做一个针对本周的打卡留言疑问以及在刷题群里的讨论内容做一下梳理吧。**,这样也有助于大家补一补本周的内容,消化消化。 + +**注意这个周末总结和系列总结还是不一样的(二叉树还远没有结束),这个总结是针对留言疑问以及刷题群里讨论内容的归纳。** + +1. [关于二叉树,你该了解这些!](https://programmercarl.com/二叉树理论基础.html) +2. [二叉树:一入递归深似海,从此offer是路人](https://programmercarl.com/二叉树的递归遍历.html) +3. [二叉树:听说递归能做的,栈也能做!](https://programmercarl.com/二叉树的迭代遍历.html) +4. [二叉树:前中后序迭代方式的写法就不能统一一下么?](https://programmercarl.com/二叉树的统一迭代法.html) +5. [二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html) +6. [二叉树:你真的会翻转二叉树么?](https://programmercarl.com/0226.翻转二叉树.html) + + +## [关于二叉树,你该了解这些!](https://programmercarl.com/二叉树理论基础.html) + +有同学会把红黑树和二叉平衡搜索树弄分开了,其实红黑树就是一种二叉平衡搜索树,这两个树不是独立的,所以C++中map、multimap、set、multiset的底层实现机制是二叉平衡搜索树,再具体一点是红黑树。 + +对于二叉树节点的定义,C++代码如下: + +```CPP +struct TreeNode { + int val; + TreeNode *left; + TreeNode *right; + TreeNode(int x) : val(x), left(NULL), right(NULL) {} +}; +``` +对于这个定义中`TreeNode(int x) : val(x), left(NULL), right(NULL) {}` 有同学不清楚干什么的。 + +这是构造函数,这么说吧C语言中的结构体是C++中类的祖先,所以C++结构体也可以有构造函数。 + +构造函数也可以不写,但是new一个新的节点的时候就比较麻烦。 + +例如有构造函数,定义初始值为9的节点: + +``` +TreeNode* a = new TreeNode(9); +``` + +没有构造函数的话就要这么写: + +```CPP +TreeNode* a = new TreeNode(); +a->val = 9; +a->left = NULL; +a->right = NULL; +``` + +在介绍前中后序遍历的时候,有递归和迭代(非递归),还有一种牛逼的遍历方式:morris遍历。 + +morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为$O(1)$,感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。 + +## [二叉树的递归遍历](https://programmercarl.com/二叉树的递归遍历.html) + +在[二叉树:一入递归深似海,从此offer是路人](https://programmercarl.com/二叉树的递归遍历.html)中讲到了递归三要素,以及前中后序的递归写法。 + +文章中我给出了leetcode上三道二叉树的前中后序题目,但是看完[二叉树:一入递归深似海,从此offer是路人](https://programmercarl.com/二叉树的递归遍历.html),依然可以解决n叉树的前后序遍历,在leetcode上分别是 + +* 589. N叉树的前序遍历 +* 590. N叉树的后序遍历 + +大家可以再去把这两道题目做了。 + +## [二叉树的非递归遍历](https://programmercarl.com/二叉树的迭代遍历.html) + +细心的同学发现文中前后序遍历空节点是入栈的,其实空节点入不入栈都差不多,但感觉空节点不入栈确实清晰一些,符合文中动画的演示。 + +前序遍历空节点不入栈的代码:(注意注释部分,和文章中的区别) + +```CPP +class Solution { +public: + vector preorderTraversal(TreeNode* root) { + stack st; + vector result; + if (root == NULL) return result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); // 中 + st.pop(); + result.push_back(node->val); + if (node->right) st.push(node->right); // 右(空节点不入栈) + if (node->left) st.push(node->left); // 左(空节点不入栈) + } + return result; + } +}; + +``` + +后序遍历空节点不入栈的代码:(注意注释部分,和文章中的区别) + +```CPP +class Solution { +public: + vector postorderTraversal(TreeNode* root) { + stack st; + vector result; + if (root == NULL) return result; + st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + st.pop(); + result.push_back(node->val); + if (node->left) st.push(node->left); // 相对于前序遍历,这更改一下入栈顺序 (空节点不入栈) + if (node->right) st.push(node->right); // 空节点不入栈 + } + reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了 + return result; + } +}; + +``` + +在实现迭代法的过程中,有同学问了:递归与迭代究竟谁优谁劣呢? + +从时间复杂度上其实迭代法和递归法差不多(在不考虑函数调用开销和函数调用产生的堆栈开销),但是空间复杂度上,递归开销会大一些,因为递归需要系统堆栈存参数返回值等等。 + +递归更容易让程序员理解,但收敛不好,容易栈溢出。 + +这么说吧,递归是方便了程序员,难为了机器(各种保存参数,各种进栈出栈)。 + +**在实际项目开发的过程中我们是要尽量避免递归!因为项目代码参数、调用关系都比较复杂,不容易控制递归深度,甚至会栈溢出。** + +## 周四 + +在[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://programmercarl.com/二叉树的统一迭代法.html)中我们使用空节点作为标记,给出了统一的前中后序迭代法。 + +此时又多了一种前中后序的迭代写法,那么有同学问了:前中后序迭代法是不是一定要统一来写,这样才算是规范。 + +其实没必要,还是自己感觉哪一种更好记就用哪种。 + +但是**一定要掌握前中后序一种迭代的写法,并不因为某种场景的题目一定要用迭代,而是现场面试的时候,面试官看你顺畅的写出了递归,一般会进一步考察能不能写出相应的迭代。** + +## 周五 + +在[二叉树:层序遍历登场!](https://programmercarl.com/0102.二叉树的层序遍历.html)中我们介绍了二叉树的另一种遍历方式(图论中广度优先搜索在二叉树上的应用)即:层序遍历。 + +看完这篇文章,去leetcode上怒刷五题,文章中 编号107题目的样例图放错了(原谅我匆忙之间总是手抖),但不影响大家理解。 + +只有同学发现leetcode上“515. 在每个树行中找最大值”,也是层序遍历的应用,依然可以分分钟解决,所以就是一鼓作气解决六道了。 + +**层序遍历遍历相对容易一些,只要掌握基本写法(也就是框架模板),剩下的就是在二叉树每一行遍历的时候做做逻辑修改。** + +## 周六 + +在[二叉树:你真的会翻转二叉树么?](https://programmercarl.com/0226.翻转二叉树.html)中我们把翻转二叉树这么一道简单又经典的问题,充分的剖析了一波,相信就算做过这道题目的同学,看完本篇之后依然有所收获! + + +**文中我指的是递归的中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。** + +如果非要使用递归中序的方式写,也可以,如下代码就可以避免节点左右孩子翻转两次的情况: + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + if (root == NULL) return root; + invertTree(root->left); // 左 + swap(root->left, root->right); // 中 + invertTree(root->left); // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了 + return root; + } +}; +``` + +代码虽然可以,但这毕竟不是真正的递归中序遍历了。 + +但使用迭代方式统一写法的中序是可以的。 + +代码如下: + +```CPP +class Solution { +public: + TreeNode* invertTree(TreeNode* root) { + stack st; + if (root != NULL) st.push(root); + while (!st.empty()) { + TreeNode* node = st.top(); + if (node != NULL) { + st.pop(); + if (node->right) st.push(node->right); // 右 + st.push(node); // 中 + st.push(NULL); + if (node->left) st.push(node->left); // 左 + + } else { + st.pop(); + node = st.top(); + st.pop(); + swap(node->left, node->right); // 节点处理逻辑 + } + } + return root; + } +}; + +``` + +为什么这个中序就是可以的呢,因为这是用栈来遍历,而不是靠指针来遍历,避免了递归法中翻转了两次的情况,大家可以画图理解一下,这里有点意思的。 + +## 总结 + +**本周我们都是讲解了二叉树,从理论基础到遍历方式,从递归到迭代,从深度遍历到广度遍历,最后再用了一个翻转二叉树的题目把我们之前讲过的遍历方式都串了起来。** + + +
diff --git "a/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" "b/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index 597e28308b..55caffe4f0 --- "a/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" +++ "b/problems/\345\223\210\345\270\214\350\241\250\346\200\273\347\273\223.md" @@ -1,27 +1,20 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- -

-

- - - - - - -

> 哈希表总结篇如约而至 -哈希表系列也是早期讲解的时候没有写总结篇,所以选个周末给补上,毕竟「代码随想录」的系列怎么能没有总结篇呢[机智]。 +# 哈希表总结篇 -# 哈希表理论基础 -在[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)中,我们介绍了哈希表的基础理论知识,不同于枯燥的讲解,这里介绍了都是对刷题有帮助的理论知识点。 +## 哈希表理论基础 + +在[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)中,我们介绍了哈希表的基础理论知识,不同于枯燥的讲解,这里介绍了都是对刷题有帮助的理论知识点。 **一般来说哈希表都是用来快速判断一个元素是否出现集合里**。 -对于哈希表,要知道**哈希函数**和**哈希碰撞**在哈希表中的作用. +对于哈希表,要知道**哈希函数**和**哈希碰撞**在哈希表中的作用。 哈希函数是把传入的key映射到符号表的索引上。 @@ -33,34 +26,34 @@ * set(集合) * map(映射) -在C++语言中,set 和 map 都分别提供了三种数据结构,每种数据结构的底层实现和用途都有所不同,在[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)中我给出了详细分析,这一知识点很重要! +在C++语言中,set 和 map 都分别提供了三种数据结构,每种数据结构的底层实现和用途都有所不同,在[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)中我给出了详细分析,这一知识点很重要! 例如什么时候用std::set,什么时候用std::multiset,什么时候用std::unordered_set,都是很有考究的。 **只有对这些数据结构的底层实现很熟悉,才能灵活使用,否则很容易写出效率低下的程序**。 -# 哈希表经典题目 +## 哈希表经典题目 -## 数组作为哈希表 +### 数组作为哈希表 一些应用场景就是为数组量身定做的。 -在[哈希表:有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)中,我们提到了数组就是简单的哈希表,但是数组的大小是受限的! +在[242.有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html)中,我们提到了数组就是简单的哈希表,但是数组的大小是受限的! 这道题目包含小写字母,那么使用数组来做哈希最合适不过。 -在[哈希表:赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ)中同样要求只有小写字母,那么就给我们浓浓的暗示,用数组! +在[383.赎金信](https://programmercarl.com/0383.赎金信.html)中同样要求只有小写字母,那么就给我们浓浓的暗示,用数组! -本题和[哈希表:有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)很像,[哈希表:有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)是求 字符串a 和 字符串b 是否可以相互组成,在[哈希表:赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ)中是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。 +本题和[242.有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html)很像,[242.有效的字母异位词](https://programmercarl.com/0242.有效的字母异位词.html)是求 字符串a 和 字符串b 是否可以相互组成,在[383.赎金信](https://programmercarl.com/0383.赎金信.html)中是求字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。 一些同学可能想,用数组干啥,都用map不就完事了。 **上面两道题目用map确实可以,但使用map的空间消耗要比数组大一些,因为map要维护红黑树或者符号表,而且还要做哈希函数的运算。所以数组更加简单直接有效!** -## set作为哈希表 +### set作为哈希表 -在[哈希表:两个数组的交集](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA)中我们给出了什么时候用数组就不行了,需要用set。 +在[349. 两个数组的交集](https://programmercarl.com/0349.两个数组的交集.html)中我们给出了什么时候用数组就不行了,需要用set。 这道题目没有限制数值的大小,就无法使用数组来做哈希表了。 @@ -71,7 +64,7 @@ 所以此时一样的做映射的话,就可以使用set了。 -关于set,C++ 给提供了如下三种可用的数据结构:(详情请看[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)) +关于set,C++ 给提供了如下三种可用的数据结构:(详情请看[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)) * std::set * std::multiset @@ -79,21 +72,21 @@ std::set和std::multiset底层实现都是红黑树,std::unordered_set的底层实现是哈希, 使用unordered_set 读写效率是最高的,本题并不需要对数据进行排序,而且还不要让数据重复,所以选择unordered_set。 -在[哈希表:快乐数](https://mp.weixin.qq.com/s/G4Q2Zfpfe706gLK7HpZHpA)中,我们再次使用了unordered_set来判断一个数是否重复出现过。 +在[202.快乐数](https://programmercarl.com/0202.快乐数.html)中,我们再次使用了unordered_set来判断一个数是否重复出现过。 -## map作为哈希表 +### map作为哈希表 -在[哈希表:两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ)中map正式登场。 +在[1.两数之和](https://programmercarl.com/0001.两数之和.html)中map正式登场。 来说一说:使用数组和set来做哈希法的局限。 * 数组的大小是受限制的,而且如果元素很少,而哈希值太大会造成内存空间的浪费。 -* set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下表位置,因为要返回x 和 y的下表。所以set 也不能用。 +* set是一个集合,里面放的元素只能是一个key,而两数之和这道题目,不仅要判断y是否存在而且还要记录y的下标位置,因为要返回x 和 y的下标。所以set 也不能用。 -map是一种``的结构,本题可以用key保存数值,用value在保存数值所在的下表。所以使用map最为合适。 +map是一种``的结构,本题可以用key保存数值,用value在保存数值所在的下标。所以使用map最为合适。 -C++提供如下三种map::(详情请看[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)) +C++提供如下三种map:(详情请看[关于哈希表,你该了解这些!](https://programmercarl.com/哈希表理论基础.html)) * std::map * std::multimap @@ -101,23 +94,23 @@ C++提供如下三种map::(详情请看[关于哈希表,你该了解这 std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层实现是红黑树。 -同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),[哈希表:两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ)中并不需要key有序,选择std::unordered_map 效率更高! +同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解),[1.两数之和](https://programmercarl.com/0001.两数之和.html)中并不需要key有序,选择std::unordered_map 效率更高! -在[哈希表:四数相加II](https://mp.weixin.qq.com/s/Ue8pKKU5hw_m-jPgwlHcbA)中我们提到了其实需要哈希的地方都能找到map的身影。 +在[454.四数相加](https://programmercarl.com/0454.四数相加II.html)中我们提到了其实需要哈希的地方都能找到map的身影。 -本题咋眼一看好像和[18. 四数之](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A),[15.三数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)差不多,其实差很多! +本题咋眼一看好像和[18. 四数之和](https://programmercarl.com/0018.四数之和.html),[15.三数之和](https://programmercarl.com/0015.三数之和.html)差不多,其实差很多! -**关键差别是本题为四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑重复问题,而[18. 四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g),[15.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)是一个数组(集合)里找到和为0的组合,可就难很多了!** +**关键差别是本题为四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑重复问题,而[18. 四数之和](https://programmercarl.com/0018.四数之和.html),[15.三数之和](https://programmercarl.com/0015.三数之和.html)是一个数组(集合)里找到和为0的组合,可就难很多了!** 用哈希法解决了两数之和,很多同学会感觉用哈希法也可以解决三数之和,四数之和。 其实是可以解决,但是非常麻烦,需要去重导致代码效率很低。 -在[哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)中我给出了哈希法和双指针两个解法,大家就可以体会到,使用哈希法还是比较麻烦的。 +在[15.三数之和](https://programmercarl.com/0015.三数之和.html)中我给出了哈希法和双指针两个解法,大家就可以体会到,使用哈希法还是比较麻烦的。 -所以[18. 四数之](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A),[15.三数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)都推荐使用双指针法! +所以18. 四数之和,15.三数之和都推荐使用双指针法! -# 总结 +## 总结 对于哈希表的知识相信很多同学都知道,但是没有成体系。 @@ -127,6 +120,7 @@ std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层 相信通过这个总结篇,大家可以对哈希表有一个全面的了解。 -**就酱,如果关注「代码随想录」之后收获满满,就转发给身边的同学朋友吧!** + + diff --git "a/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index 4c8ea1b2ee..e3400ad7f0 --- "a/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\345\223\210\345\270\214\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,108 +1,107 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -# 哈希表 -首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 -> 哈希表是根据关键码的值而直接进行访问的数据结构。 -这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 -哈希表中关键码就是数组的索引下表,然后通过下表直接访问数组中的元素,如下图所示: +# 哈希表理论基础 -![哈希表1](https://img-blog.csdnimg.cn/20210104234805168.png) +## 哈希表 -那么哈希表能解决什么问题呢,**一般哈希表都是用来快速判断一个元素是否出现集合里。** +首先什么是哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 -例如要查询一个名字是否在这所学校里。 +> 哈希表是根据关键码的值而直接进行访问的数据结构。 -要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1) 就可以做到。 +这么官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 + +哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示: + +![哈希表1](https://file1.kamacoder.com/i/algo/20210104234805168.png) + +那么哈希表能解决什么问题呢,**一般哈希表都是用来快速判断一个元素是否出现集合里。** + +例如要查询一个名字是否在这所学校里。 + +要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。 我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。 将学生姓名映射到哈希表上就涉及到了**hash function ,也就是哈希函数**。 -# 哈希函数 +## 哈希函数 -哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下表快速知道这位同学是否在这所学校里了。 +哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下标快速知道这位同学是否在这所学校里了。 哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。 -![哈希表2](https://img-blog.csdnimg.cn/2021010423484818.png) +![哈希表2](https://file1.kamacoder.com/i/algo/2021010423484818.png) -如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢? +如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢? -此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。 +此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,这样我们就保证了学生姓名一定可以映射到哈希表上了。 此时问题又来了,哈希表我们刚刚说过,就是一个数组。 -如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下表的位置。 +如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下标的位置。 接下来**哈希碰撞**登场 -# 哈希碰撞 +### 哈希碰撞 -如图所示,小李和小王都映射到了索引下表 1的位置,**这一现象叫做哈希碰撞**。 +如图所示,小李和小王都映射到了索引下标 1 的位置,**这一现象叫做哈希碰撞**。 -![哈希表3](https://img-blog.csdnimg.cn/2021010423494884.png) +![哈希表3](https://file1.kamacoder.com/i/algo/2021010423494884.png) 一般哈希碰撞有两种解决方法, 拉链法和线性探测法。 -## 拉链法 +### 拉链法 刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了 -![哈希表4](https://img-blog.csdnimg.cn/20210104235015226.png) +![哈希表4](https://file1.kamacoder.com/i/algo/20210104235015226.png) (数据规模是dataSize, 哈希表的大小为tableSize) 其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。 -## 线性探测法 +### 线性探测法 使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。 例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示: -![哈希表5](https://img-blog.csdnimg.cn/20210104235109950.png) +![哈希表5](https://file1.kamacoder.com/i/algo/20210104235109950.png) 其实关于哈希碰撞还有非常多的细节,感兴趣的同学可以再好好研究一下,这里我就不再赘述了。 -# 常见的三种哈希结构 +## 常见的三种哈希结构 当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。 * 数组 * set (集合) -* map(映射) +* map(映射) 这里数组就没啥可说的了,我们来看一下set。 在C++中,set 和 map 分别提供以下三种数据结构,其底层实现以及优劣如下表所示: -|集合 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率| -|---|---| --- |---| --- | --- | ---| -|std::set |红黑树 |有序 |否 |否 | O(logn)|O(logn) | -|std::multiset | 红黑树|有序 |是 | 否| O(logn) |O(logn) | -|std::unordered_set |哈希表 |无序 |否 |否 |O(1) | O(1)| +| 集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 | +| --- | --- | ---- | --- | --- | --- | --- | +| std::set | 红黑树 | 有序 | 否 | 否 | O(log n) | O(log n) | +| std::multiset | 红黑树 | 有序 | 是 | 否 | O(logn) | O(logn) | +| std::unordered_set | 哈希表 | 无序 | 否 | 否 | O(1) | O(1) | std::unordered_set底层实现为哈希表,std::set 和std::multiset 的底层实现是红黑树,红黑树是一种平衡二叉搜索树,所以key值是有序的,但key不可以修改,改动key值会导致整棵树的错乱,所以只能删除和增加。 -|映射 |底层实现 | 是否有序 |数值是否可以重复 | 能否更改数值|查询效率 |增删效率| -|---|---| --- |---| --- | --- | ---| -|std::map |红黑树 |key有序 |key不可重复 |key不可修改 | O(logn)|O(logn) | -|std::multimap | 红黑树|key有序 | key可重复 | key不可修改|O(logn) |O(logn) | -|std::unordered_map |哈希表 | key无序 |key不可重复 |key不可修改 |O(1) | O(1)| +| 映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 | +| --- | --- | --- | --- | --- | --- | --- | +| std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O(logn) | O(logn) | +| std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O(log n) | O(log n) | +| std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O(1) | O(1) | + std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。 @@ -112,15 +111,15 @@ std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底 其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。 -虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 map也是一样的道理。 +虽然std::set和std::multiset 的底层实现基于红黑树而非哈希表,它们通过红黑树来索引和存储数据。不过给我们的使用方式,还是哈希法的使用方式,即依靠键(key)来访问值(value)。所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。std::map也是一样的道理。 这里在说一下,一些C++的经典书籍上 例如STL源码剖析,说到了hash_set hash_map,这个与unordered_set,unordered_map又有什么关系呢? 实际上功能都是一样一样的, 但是unordered_set在C++11的时候被引入标准库了,而hash_set并没有,所以建议还是使用unordered_set比较好,这就好比一个是官方认证的,hash_set,hash_map 是C++11标准之前民间高手自发造的轮子。 -![哈希表6](https://img-blog.csdnimg.cn/20210104235134572.png) +![哈希表6](https://file1.kamacoder.com/i/algo/20210104235134572.png) -# 总结 +## 总结 总结一下,**当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法**。 @@ -128,7 +127,6 @@ std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底 如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法! -预告下篇讲解一波哈希表面试题的解题套路,我们下期见! -都看到这了,还有sei!sei没读懂单独找我! + diff --git "a/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" "b/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index 7090c364ef..7a7929f41a --- "a/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" +++ "b/problems/\345\233\236\346\272\257\346\200\273\347\273\223.md" @@ -1,24 +1,20 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + > 20张树形结构图、14道精选回溯题目,21篇回溯法精讲文章,由浅入深,一气呵成,这是全网最强回溯算法总结! -# 回溯法理论基础 +# 回溯总结篇 + +## 回溯法理论基础 转眼间[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)里已经分享连续讲解了21天的回溯算法,是时候做一个大总结了,本篇高能,需要花费很大的精力来看! 关于回溯算法理论基础,我录了一期B站视频[带你学透回溯算法(理论篇)](https://www.bilibili.com/video/BV1cy4y167mM)如果对回溯算法还不了解的话,可以看一下。 -在[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中我们详细的介绍了回溯算法的理论知识,不同于教科书般的讲解,这里介绍的回溯法的效率,解决的问题以及模板都是在刷题的过程中非常实用! +在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)中我们详细的介绍了回溯算法的理论知识,不同于教科书般的讲解,这里介绍的回溯法的效率,解决的问题以及模板都是在刷题的过程中非常实用! **回溯是递归的副产品,只要有递归就会有回溯**,所以回溯法也经常和二叉树遍历,深度优先搜索混在一起,因为这两种方式都是用了递归。 @@ -36,7 +32,7 @@ 回溯法确实不好理解,所以需要把回溯法抽象为一个图形来理解就容易多了,**在后面的每一道回溯法的题目我都将遍历过程抽象为树形结构方便大家的理解**。 -在[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)还用了回溯三部曲来分析回溯算法,并给出了回溯法的模板: +在[关于回溯算法,你该了解这些!](https://programmercarl.com/回溯算法理论基础.html)还用了回溯三部曲来分析回溯算法,并给出了回溯法的模板: ``` void backtracking(参数) { @@ -55,86 +51,86 @@ void backtracking(参数) { **事实证明这个模板会伴随整个回溯法系列!** -# 组合问题 +## 组合问题 -## 组合问题 +### 组合问题 -在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)中,我们开始用回溯法解决第一道题目:组合问题。 +在[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)中,我们开始用回溯法解决第一道题目:组合问题。 -我在文中开始的时候给大家列举k层for循环例子,进而得出都是同样是暴利解法,为什么要用回溯法! +我在文中开始的时候给大家列举k层for循环例子,进而得出都是同样是暴力解法,为什么要用回溯法! **此时大家应该深有体会回溯法的魅力,用递归控制for循环嵌套的数量!** 本题我把回溯问题抽象为树形结构,如题: -![77.组合1](https://img-blog.csdnimg.cn/20201118152928844.png) +![77.组合1](https://file1.kamacoder.com/i/algo/20201118152928844.png) 可以直观的看出其搜索的过程:**for循环横向遍历,递归纵向遍历,回溯不断调整结果集**,这个理念贯穿整个回溯法系列,也是我做了很多回溯的题目,不断摸索其规律才总结出来的。 -对于回溯法的整体框架,网上搜的文章这块都说不清楚,按照天上掉下来的代码对着讲解,不知道究竟是怎么来的,也不知道为什么要这么写。 +对于回溯法的整体框架,网上搜的文章这块都说不清楚,按照天上掉下来的代码对着讲解,不知道究竟是怎么来的,也不知道为什么要这么写。 **所以,录友们刚开始学回溯法,起跑姿势就很标准了!** -优化回溯算法只有剪枝一种方法,在[回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA)中把回溯法代码做了剪枝优化,树形结构如图: +优化回溯算法只有剪枝一种方法,在[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中把回溯法代码做了剪枝优化,树形结构如图: -![77.组合4](https://img-blog.csdnimg.cn/20201118153133458.png) +![77.组合4](https://file1.kamacoder.com/i/algo/20201118153133458.png) 大家可以一目了然剪的究竟是哪里。 -**[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了**。 +**[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)剪枝精髓是:for循环在寻找起点的时候要有一个范围,如果这个起点到集合终止之间的元素已经不够题目要求的k个元素了,就没有必要搜索了**。 **在for循环上做剪枝操作是回溯法剪枝的常见套路!** 后面的题目还会经常用到。 -## 组合总和 +### 组合总和 -### 组合总和(一) +#### 组合总和(一) -在[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)中,相当于 [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)加了一个元素总和的限制。 +在[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中,相当于 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)加了一个元素总和的限制。 树形结构如图: -![216.组合总和III](https://img-blog.csdnimg.cn/20201118201921245.png) +![216.组合总和III](https://file1.kamacoder.com/i/algo/20201118201921245.png) 整体思路还是一样的,本题的剪枝会好想一些,即:**已选元素总和如果已经大于n(题中要求的和)了,那么往后遍历就没有意义了,直接剪掉**,如图: -![216.组合总和III1](https://img-blog.csdnimg.cn/20201118202038240.png) +![216.组合总和III1](https://file1.kamacoder.com/i/algo/20201118202038240.png) -在本题中,依然还可以有一个剪枝,就是[回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA)中提到的,对for循环选择的起始范围的剪枝。 +在本题中,依然还可以有一个剪枝,就是[回溯算法:组合问题再剪剪枝](https://programmercarl.com/0077.组合优化.html)中提到的,对for循环选择的起始范围的剪枝。 所以剪枝的代码可以在for循环加上 `i <= 9 - (k - path.size()) + 1` 的限制! -### 组合总和(二) +#### 组合总和(二) -在[回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)中讲解的组合总和问题,和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)和区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。 +在[回溯算法:求组合总和(二)](https://programmercarl.com/0039.组合总和.html)中讲解的组合总和问题,和[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html),[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)和区别是:本题没有数量要求,可以无限重复,但是有总和的限制,所以间接的也是有个数的限制。 不少同学都是看到可以重复选择,就义无反顾的把startIndex去掉了。 **本题还需要startIndex来控制for循环的起始位置,对于组合问题,什么时候需要startIndex呢?** -我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)。 +我举过例子,如果是一个集合来求组合的话,就需要startIndex,例如:[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html),[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)。 -如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) +如果是多个集合取组合,各个集合之间相互不影响,那么就不用startIndex,例如:[回溯算法:电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html) **注意以上我只是说求组合的情况,如果是排列问题,又是另一套分析的套路**。 树形结构如下: +![39.组合总和](https://file1.kamacoder.com/i/algo/20201223170730367.png) -![39.组合总和](https://img-blog.csdnimg.cn/20201118152521990.png) 最后还给出了本题的剪枝优化,如下: -``` +```cpp for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) ``` 优化后树形结构如下: -![39.组合总和1](https://img-blog.csdnimg.cn/20201118202115929.png) +![39.组合总和1](https://file1.kamacoder.com/i/algo/20201118202115929.png) -### 组合总和(三) +#### 组合总和(三) -在[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)中集合元素会有重复,但要求解集不能包含重复的组合。 +在[回溯算法:求组合总和(三)](https://programmercarl.com/0040.组合总和II.html)中集合元素会有重复,但要求解集不能包含重复的组合。 **所以难就难在去重问题上了**。 @@ -144,36 +140,36 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 都知道组合问题可以抽象为树形结构,那么“使用过”在这个树形结构上是有两个维度的,一个维度是同一树枝上“使用过”,一个维度是同一树层上“使用过”。**没有理解这两个层面上的“使用过” 是造成大家没有彻底理解去重的根本原因**。 -![40.组合总和II1](https://img-blog.csdnimg.cn/2020111820220675.png) +![40.组合总和II1](https://file1.kamacoder.com/i/algo/2020111820220675.png) 我在图中将used的变化用橘黄色标注上,**可以看出在candidates[i] == candidates[i - 1]相同的情况下:** -* used[i - 1] == true,说明同一树支candidates[i - 1]使用过 +* used[i - 1] == true,说明同一树枝candidates[i - 1]使用过 * used[i - 1] == false,说明同一树层candidates[i - 1]使用过 **这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!** 对于去重,其实排列和子集问题也是一样的道理。 -## 多个集合求组合 +### 多个集合求组合 -在[回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A)中,开始用多个集合来求组合,还是熟悉的模板题目,但是有一些细节。 +在[回溯算法:电话号码的字母组合](https://programmercarl.com/0017.电话号码的字母组合.html)中,开始用多个集合来求组合,还是熟悉的模板题目,但是有一些细节。 -例如这里for循环,可不像是在 [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)中从startIndex开始遍历的。 +例如这里for循环,可不像是在 [回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)和[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)中从startIndex开始遍历的。 -**因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)都是是求同一个集合中的组合!** +**因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而[回溯算法:求组合问题!](https://programmercarl.com/0077.组合.html)和[回溯算法:求组合总和!](https://programmercarl.com/0216.组合总和III.html)都是是求同一个集合中的组合!** 树形结构如下: -![17. 电话号码的字母组合](https://img-blog.csdnimg.cn/20201118202335724.png) +![17. 电话号码的字母组合](https://file1.kamacoder.com/i/algo/20201118202335724.png) 如果大家在现场面试的时候,一定要注意各种输入异常的情况,例如本题输入1 * #按键。 其实本题不算难,但也处处是细节,还是要反复琢磨。 -# 切割问题 +## 切割问题 -在[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)中,我们开始讲解切割问题,虽然最后代码看起来好像是一道模板题,但是从分析到学会套用这个模板,是比较难的。 +在[回溯算法:分割回文串](https://programmercarl.com/0131.分割回文串.html)中,我们开始讲解切割问题,虽然最后代码看起来好像是一道模板题,但是从分析到学会套用这个模板,是比较难的。 我列出如下几个难点: @@ -193,22 +189,22 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 树形结构如下: -![131.分割回文串](https://img-blog.csdnimg.cn/20201118202448642.png) +![131.分割回文串](https://file1.kamacoder.com/i/algo/20201118202448642.png) -# 子集问题 +## 子集问题 -## 子集问题(一) +### 子集问题(一) -在[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)中讲解了子集问题,**在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果**。 +在[回溯算法:求子集问题!](https://programmercarl.com/0078.子集.html)中讲解了子集问题,**在树形结构中子集问题是要收集所有节点的结果,而组合问题是收集叶子节点的结果**。 如图: -![78.子集](https://img-blog.csdnimg.cn/20201118202544339.png) +![78.子集](https://file1.kamacoder.com/i/algo/20201118202544339.png) 认清这个本质之后,今天的题目就是一道模板题了。 -**本题其实可以不需要加终止条件**,因为startIndex >= nums.size(),本层for循环本来也结束了,本来我们就要遍历整颗树。 +**本题其实可以不需要加终止条件**,因为startIndex >= nums.size(),本层for循环本来也结束了,本来我们就要遍历整棵树。 有的同学可能担心不写终止条件会不会无限递归? @@ -223,60 +219,60 @@ if (startIndex >= nums.size()) { // 终止条件可以不加 } ``` -## 子集问题(二) +### 子集问题(二) -在[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中,开始针对子集问题进行去重。 +在[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)中,开始针对子集问题进行去重。 -本题就是[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)的基础上加上了去重,去重我们在[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)也讲过了,一样的套路。 +本题就是[回溯算法:求子集问题!](https://programmercarl.com/0078.子集.html)的基础上加上了去重,去重我们在[回溯算法:求组合总和(三)](https://programmercarl.com/0040.组合总和II.html)也讲过了,一样的套路。 树形结构如下: -![90.子集II](https://img-blog.csdnimg.cn/2020111217110449.png) +![90.子集II](https://file1.kamacoder.com/i/algo/2020111217110449.png) -## 递增子序列 +### 递增子序列 -在[回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)中,处处都能看到子集的身影,但处处是陷阱,值得好好琢磨琢磨! +在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中,处处都能看到子集的身影,但处处是陷阱,值得好好琢磨琢磨! 树形结构如下: -![491. 递增子序列1](https://img-blog.csdnimg.cn/20201112170832333.png) +![491. 递增子序列1](https://file1.kamacoder.com/i/algo/20201112170832333.png) -很多同学都会把这道题目和[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)混在一起。 +很多同学都会把这道题目和[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)混在一起。 -**[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)也可以使用set针对同一父节点本层去重,但子集问题一定要排序,为什么呢?** +**[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)也可以使用set针对同一父节点本层去重,但子集问题一定要排序,为什么呢?** 我用没有排序的集合{2,1,2,2}来举个例子画一个图,如下: -![90.子集II2](https://img-blog.csdnimg.cn/2020111316440479.png) +![90.子集II2](https://file1.kamacoder.com/i/algo/2020111316440479.png) **相信这个图胜过千言万语的解释了**。 -# 排列问题 +## 排列问题 -## 排列问题(一) +### 排列问题(一) -[回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw) 又不一样了。 +[回溯算法:排列问题!](https://programmercarl.com/0046.全排列.html) 又不一样了。 -排列是有序的,也就是说[1,2] 和[2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。 +排列是有序的,也就是说 [1,2] 和 [2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方。 可以看出元素1在[1,2]中已经使用过了,但是在[2,1]中还要在使用一次1,所以处理排列问题就不用使用startIndex了。 如图: -![46.全排列](https://img-blog.csdnimg.cn/20201112170304979.png) +![46.全排列](https://file1.kamacoder.com/i/algo/20201112170304979.png) **大家此时可以感受出排列问题的不同:** * 每层都是从0开始搜索而不是startIndex * 需要used数组记录path里都放了哪些元素了 -## 排列问题(二) +### 排列问题(二) -排列问题也要去重了,在[回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA)中又一次强调了“树层去重”和“树枝去重”。 +排列问题也要去重了,在[回溯算法:排列问题(二)](https://programmercarl.com/0047.全排列II.html)中又一次强调了“树层去重”和“树枝去重”。 树形结构如下: -![47.全排列II1](https://img-blog.csdnimg.cn/20201112171930470.png) +![47.全排列II1](https://file1.kamacoder.com/i/algo/20201112171930470.png) **这道题目神奇的地方就是used[i - 1] == false也可以,used[i - 1] == true也可以!** @@ -284,45 +280,45 @@ if (startIndex >= nums.size()) { // 终止条件可以不加 树层上去重(used[i - 1] == false),的树形结构如下: -![47.全排列II2.png](https://img-blog.csdnimg.cn/20201112172230434.png) +![47.全排列II2.png](https://file1.kamacoder.com/i/algo/20201112172230434.png) 树枝上去重(used[i - 1] == true)的树型结构如下: -![47.全排列II3](https://img-blog.csdnimg.cn/20201112172327967.png) +![47.全排列II3](https://file1.kamacoder.com/i/algo/20201112172327967.png) **可以清晰的看到使用(used[i - 1] == false),即树层去重,效率更高!** 本题used数组即是记录path里都放了哪些元素,同时也用来去重,一举两得。 -# 去重问题 +## 去重问题 以上我都是统一使用used数组来去重的,其实使用set也可以用来去重! -在[本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag)中给出了子集、组合、排列问题使用set来去重的解法以及具体代码,并纠正一些同学的常见错误写法。 +在[本周小结!(回溯算法系列三)续集](https://programmercarl.com/回溯算法去重问题的另一种写法.html)中给出了子集、组合、排列问题使用set来去重的解法以及具体代码,并纠正一些同学的常见错误写法。 同时详细分析了 使用used数组去重 和 使用set去重 两种写法的性能差异: **使用set去重的版本相对于used数组的版本效率都要低很多**,大家在leetcode上提交,能明显发现。 -原因在[回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。 +原因在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。 **而使用used数组在时间复杂度上几乎没有额外负担!** -**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。 +**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。 -那有同学可能疑惑 用used数组也是占用O(n)的空间啊? +那有同学可能疑惑 用used数组也是占用O(n)的空间啊? used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。 -# 重新安排行程(图论额外拓展) +## 重新安排行程(图论额外拓展) 之前说过,有递归的地方就有回溯,深度优先搜索也是用递归来实现的,所以往往伴随着回溯。 -在[回溯算法:重新安排行程](https://mp.weixin.qq.com/s/3kmbS4qDsa6bkyxR92XCTA)其实也算是图论里深搜的题目,但是我用回溯法的套路来讲解这道题目,算是给大家拓展一下思路,原来回溯法还可以这么玩! +在[回溯算法:重新安排行程](https://programmercarl.com/0332.重新安排行程.html)其实也算是图论里深搜的题目,但是我用回溯法的套路来讲解这道题目,算是给大家拓展一下思路,原来回溯法还可以这么玩! 以输入:[["JFK", "KUL"], ["JFK", "NRT"], ["NRT", "JFK"]为例,抽象为树形结构如下: -![](https://img-blog.csdnimg.cn/2020111518065555.png) +![](https://file1.kamacoder.com/i/algo/2020111518065555.png) 本题可以算是一道hard的题目了,关于本题的难点我在文中已经详细列出。 @@ -331,53 +327,53 @@ used数组可是全局变量,每层与每层之间公用一个used数组,所 本题其实是一道深度优先搜索的题目,但是我完全使用回溯法的思路来讲解这道题题目,**算是给大家拓展一下思维方式,其实深搜和回溯也是分不开的,毕竟最终都是用递归**。 -# 棋盘问题 +## 棋盘问题 -## N皇后问题 +### N皇后问题 -在[回溯算法:N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)中终于迎来了传说中的N皇后。 +在[回溯算法:N皇后问题](https://programmercarl.com/0051.N皇后.html)中终于迎来了传说中的N皇后。 -下面我用一个3 * 3 的棋牌,将搜索过程抽象为一颗树,如图: +下面我用一个3 * 3 的棋盘,将搜索过程抽象为一棵树,如图: -![51.N皇后](https://img-blog.csdnimg.cn/20201118225433127.png) +![51.N皇后](https://file1.kamacoder.com/i/algo/20201118225433127.png) -从图中,可以看出,二维矩阵中矩阵的高就是这颗树的高度,矩阵的宽就是树形结构中每一个节点的宽度。 +从图中,可以看出,二维矩阵中矩阵的高就是这棵树的高度,矩阵的宽就是树形结构中每一个节点的宽度。 -那么我们用皇后们的约束条件,来回溯搜索这颗树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。 +那么我们用皇后们的约束条件,来回溯搜索这棵树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。 如果从来没有接触过N皇后问题的同学看着这样的题会感觉无从下手,可能知道要用回溯法,但也不知道该怎么去搜。 **这里我明确给出了棋盘的宽度就是for循环的长度,递归的深度就是棋盘的高度,这样就可以套进回溯法的模板里了**。 -相信看完本篇[回溯算法:N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)也没那么难了,传说已经不是传说了,哈哈。 +相信看完本篇[回溯算法:N皇后问题](https://programmercarl.com/0051.N皇后.html)也没那么难了,传说已经不是传说了。 -## 解数独问题 +### 解数独问题 -在[回溯算法:解数独](https://mp.weixin.qq.com/s/eWE9TapVwm77yW9Q81xSZQ)中要征服回溯法的最后一道山峰。 +在[回溯算法:解数独](https://programmercarl.com/0037.解数独.html)中要征服回溯法的最后一道山峰。 解数独应该是棋盘很难的题目了,比N皇后还要复杂一些,但只要理解 “二维递归”这个过程,其实发现就没那么难了。 -大家已经跟着「代码随想录」刷过了如下回溯法题目,例如:[77.组合(组合问题)](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[131.分割回文串(分割问题)](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q),[78.子集(子集问题)](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA),[46.全排列(排列问题)](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw),以及[51.N皇后(N皇后问题)](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg),其实这些题目都是一维递归。 +大家已经跟着「代码随想录」刷过了如下回溯法题目,例如:[77.组合(组合问题)](https://programmercarl.com/0077.组合.html),[131.分割回文串(分割问题)](https://programmercarl.com/0131.分割回文串.html),[78.子集(子集问题)](https://programmercarl.com/0078.子集.html),[46.全排列(排列问题)](https://programmercarl.com/0046.全排列.html),以及[51.N皇后(N皇后问题)](https://programmercarl.com/0051.N皇后.html),其实这些题目都是一维递归。 -其中[N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg)是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置。 +其中[N皇后问题](https://programmercarl.com/0051.N皇后.html)是因为每一行每一列只放一个皇后,只需要一层for循环遍历一行,递归来遍历列,然后一行一列确定皇后的唯一位置。 本题就不一样了,**本题中棋盘的每一个位置都要放一个数字,并检查数字是否合法,解数独的树形结构要比N皇后更宽更深**。 因为这个树形结构太大了,我抽取一部分,如图所示: -![37.解数独](https://img-blog.csdnimg.cn/2020111720451790.png) +![37.解数独](https://file1.kamacoder.com/i/algo/2020111720451790.png) 解数独可以说是非常难的题目了,如果还一直停留在一维递归的逻辑中,这道题目可以让大家瞬间崩溃。 -**所以我在[回溯算法:解数独](https://mp.weixin.qq.com/s/eWE9TapVwm77yW9Q81xSZQ)中开篇就提到了二维递归,这也是我自创词汇**,希望可以帮助大家理解解数独的搜索过程。 +**所以我在[回溯算法:解数独](https://programmercarl.com/0037.解数独.html)中开篇就提到了二维递归,这也是我自创词汇**,希望可以帮助大家理解解数独的搜索过程。 一波分析之后,在看代码会发现其实也不难,唯一难点就是理解**二维递归**的思维逻辑。 **这样,解数独这么难的问题也被我们攻克了**。 -# 性能分析 +## 性能分析 **关于回溯算法的复杂度分析在网上的资料鱼龙混杂,一些所谓的经典面试书籍不讲回溯算法,算法书籍对这块也避而不谈,感觉就像是算法里模糊的边界**。 @@ -386,22 +382,27 @@ used数组可是全局变量,每层与每层之间公用一个used数组,所 以下在计算空间复杂度的时候我都把系统栈(不是数据结构里的栈)所占空间算进去。 子集问题分析: -* 时间复杂度:O(n * 2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n),构造每一组子集都需要填进数组,又有需要O(n),最终时间复杂度:O(n * 2^n) + +* 时间复杂度:O(2^n),因为每一个元素的状态无外乎取与不取,所以时间复杂度为O(2^n) * 空间复杂度:O(n),递归深度为n,所以系统栈所用空间为O(n),每一层递归所用的空间都是常数级别,注意代码里的result和path都是全局变量,就算是放在参数里,传的也是引用,并不会新申请内存空间,最终空间复杂度为O(n) 排列问题分析: + * 时间复杂度:O(n!),这个可以从排列的树形图中很明显发现,每一层节点为n,第二层每一个分支都延伸了n-1个分支,再往下又是n-2个分支,所以一直到叶子节点一共就是 n * n-1 * n-2 * ..... 1 = n!。 * 空间复杂度:O(n),和子集问题同理。 组合问题分析: -* 时间复杂度:O(n * 2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。 + +* 时间复杂度:O(2^n),组合问题其实就是一种子集的问题,所以组合问题最坏的情况,也不会超过子集问题的时间复杂度。 * 空间复杂度:O(n),和子集问题同理。 -N皇后问题分析: +N皇后问题分析: + * 时间复杂度:O(n!) ,其实如果看树形图的话,直觉上是O(n^n),但皇后之间不能见面所以在搜索的过程中是有剪枝的,最差也就是O(n!),n!表示n * (n-1) * .... * 1。 * 空间复杂度:O(n),和子集问题同理。 解数独问题分析: + * 时间复杂度:O(9^m) , m是'.'的数目。 * 空间复杂度:O(n^2),递归的深度是n^2 @@ -409,7 +410,7 @@ N皇后问题分析: **一般说道回溯算法的复杂度,都说是指数级别的时间复杂度,这也算是一个概括吧!** -# 总结 +## 总结 **[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)历时21天,14道经典题目分析,20张树形图,21篇回溯法精讲文章,从组合到切割,从子集到排列,从棋盘问题到最后的复杂度分析**,至此收尾了。 @@ -418,11 +419,12 @@ N皇后问题分析: 可以说方方面面都详细介绍到了。 例如: + * 如何理解回溯法的搜索过程? -* 什么时候用startIndex,什么时候不用? -* 如何去重?如何理解“树枝去重”与“树层去重”? +* 什么时候用startIndex,什么时候不用? +* 如何去重?如何理解“树枝去重”与“树层去重”? * 去重的几种方法? -* 如何理解二维递归? +* 如何理解二维递归? **这里的每一个问题,网上几乎找不到能讲清楚的文章,这也是直击回溯算法本质的问题**。 @@ -430,11 +432,15 @@ N皇后问题分析: 此时回溯算法系列就要正式告一段落了。 -**录友们可以回顾一下这21天,每天的打卡,每天在交流群里和大家探讨代码,最终换来的都是不知不觉的成长**。 +**录友们可以回顾一下这21天,每天的打卡,每天在交流群里和大家探讨代码,最终换来的都是不知不觉的成长**。 同样也感谢录友们的坚持,这也是我持续写作的动力,**正是因为大家的积极参与,我才知道这件事件是非常有意义的**。 -最后希望大家可以转发这篇文章给身边的朋友们,因为还有很多学习算法的小伙伴依然在浩如烟海的信息中迷茫,而我相信「代码随想录」会让大家少走弯路! +回溯专题汇聚为一张图: + +![](https://file1.kamacoder.com/i/algo/20211030124742.png) + +这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[莫非毛](https://wx.zsxq.com/dweb2/index/footprint/828844212542),所画,总结的非常好,分享给大家。 **回溯算法系列正式结束,新的系列终将开始,录友们准备开启新的征程!** diff --git "a/problems/\345\233\236\346\272\257\347\256\227\346\263\225\345\216\273\351\207\215\351\227\256\351\242\230\347\232\204\345\217\246\344\270\200\347\247\215\345\206\231\346\263\225.md" "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\345\216\273\351\207\215\351\227\256\351\242\230\347\232\204\345\217\246\344\270\200\347\247\215\345\206\231\346\263\225.md" new file mode 100755 index 0000000000..5e2c9345c4 --- /dev/null +++ "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\345\216\273\351\207\215\351\227\256\351\242\230\347\232\204\345\217\246\344\270\200\347\247\215\345\206\231\346\263\225.md" @@ -0,0 +1,709 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 回溯算法去重问题的另一种写法 + +> 在 [本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html) 中一位录友对 整棵树的本层和同一节点的本层有疑问,也让我重新思考了一下,发现这里确实有问题,所以专门写一篇来纠正,感谢录友们的积极交流哈! + +接下来我再把这块再讲一下。 + +在[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)中的去重和 [回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中的去重 都是 同一父节点下本层的去重。 + +[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html)也可以使用set针对同一父节点本层去重,但子集问题一定要排序,为什么呢? + +我用没有排序的集合{2,1,2,2}来举例子画一个图,如图: + + +![90.子集II2](https://file1.kamacoder.com/i/algo/2020111316440479-20230310121930316.png) + +图中,大家就很明显的看到,子集重复了。 + +那么下面我针对[回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html) 给出使用set来对本层去重的代码实现。 + +## 90.子集II + +used数组去重版本: [回溯算法:求子集问题(二)](https://programmercarl.com/0090.子集II.html) + +使用set去重的版本如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& nums, int startIndex, vector& used) { + result.push_back(path); + unordered_set uset; // 定义set对同一节点下的本层去重 + for (int i = startIndex; i < nums.size(); i++) { + if (uset.find(nums[i]) != uset.end()) { // 如果发现出现过就pass + continue; + } + uset.insert(nums[i]); // set跟新元素 + path.push_back(nums[i]); + backtracking(nums, i + 1, used); + path.pop_back(); + } + } + +public: + vector> subsetsWithDup(vector& nums) { + result.clear(); + path.clear(); + vector used(nums.size(), false); + sort(nums.begin(), nums.end()); // 去重需要排序 + backtracking(nums, 0, used); + return result; + } +}; + +``` + +针对留言区录友们的疑问,我再补充一些常见的错误写法, + + +### 错误写法一 + +把uset定义放到类成员位置,然后模拟回溯的样子 insert一次,erase一次。 + +例如: + +```CPP +class Solution { +private: + vector> result; + vector path; + unordered_set uset; // 把uset定义放到类成员位置 + void backtracking(vector& nums, int startIndex, vector& used) { + result.push_back(path); + + for (int i = startIndex; i < nums.size(); i++) { + if (uset.find(nums[i]) != uset.end()) { + continue; + } + uset.insert(nums[i]); // 递归之前insert + path.push_back(nums[i]); + backtracking(nums, i + 1, used); + path.pop_back(); + uset.erase(nums[i]); // 回溯再erase + } + } + +``` + +在树形结构中,**如果把unordered_set uset放在类成员的位置(相当于全局变量),就把树枝的情况都记录了,不是单纯的控制某一节点下的同一层了**。 + +如图: + +![90.子集II1](https://file1.kamacoder.com/i/algo/202011131625054.png) + +可以看出一旦把unordered_set uset放在类成员位置,它控制的就是整棵树,包括树枝。 + +**所以这么写不行!** + +### 错误写法二 + +有同学把 unordered_set uset; 放到类成员位置,然后每次进入单层的时候用uset.clear()。 + +代码如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + unordered_set uset; // 把uset定义放到类成员位置 + void backtracking(vector& nums, int startIndex, vector& used) { + result.push_back(path); + uset.clear(); // 到每一层的时候,清空uset + for (int i = startIndex; i < nums.size(); i++) { + if (uset.find(nums[i]) != uset.end()) { + continue; + } + uset.insert(nums[i]); // set记录元素 + path.push_back(nums[i]); + backtracking(nums, i + 1, used); + path.pop_back(); + } + } +``` +uset已经是全局变量,本层的uset记录了一个元素,然后进入下一层之后这个uset(和上一层是同一个uset)就被清空了,也就是说,层与层之间的uset是同一个,那么就会相互影响。 + +**所以这么写依然不行!** + +**组合问题和排列问题,其实也可以使用set来对同一节点下本层去重,下面我都分别给出实现代码**。 + +## 40. 组合总和 II + +使用used数组去重版本:[回溯算法:求组合总和(三)](https://programmercarl.com/0040.组合总和II.html) + +使用set去重的版本如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking(vector& candidates, int target, int sum, int startIndex) { + if (sum == target) { + result.push_back(path); + return; + } + unordered_set uset; // 控制某一节点下的同一层元素不能重复 + for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) { + if (uset.find(candidates[i]) != uset.end()) { + continue; + } + uset.insert(candidates[i]); // 记录元素 + sum += candidates[i]; + path.push_back(candidates[i]); + backtracking(candidates, target, sum, i + 1); + sum -= candidates[i]; + path.pop_back(); + } + } + +public: + vector> combinationSum2(vector& candidates, int target) { + path.clear(); + result.clear(); + sort(candidates.begin(), candidates.end()); + backtracking(candidates, target, 0, 0); + return result; + } +}; +``` + +## 47. 全排列 II + +使用used数组去重版本:[回溯算法:排列问题(二)](https://programmercarl.com/0047.全排列II.html) + +使用set去重的版本如下: + +```CPP +class Solution { +private: + vector> result; + vector path; + void backtracking (vector& nums, vector& used) { + if (path.size() == nums.size()) { + result.push_back(path); + return; + } + unordered_set uset; // 控制某一节点下的同一层元素不能重复 + for (int i = 0; i < nums.size(); i++) { + if (uset.find(nums[i]) != uset.end()) { + continue; + } + if (used[i] == false) { + uset.insert(nums[i]); // 记录元素 + used[i] = true; + path.push_back(nums[i]); + backtracking(nums, used); + path.pop_back(); + used[i] = false; + } + } + } +public: + vector> permuteUnique(vector& nums) { + result.clear(); + path.clear(); + sort(nums.begin(), nums.end()); // 排序 + vector used(nums.size(), false); + backtracking(nums, used); + return result; + } +}; +``` + +## 两种写法的性能分析 + +需要注意的是:**使用set去重的版本相对于used数组的版本效率都要低很多**,大家在leetcode上提交,能明显发现。 + +原因在[回溯算法:递增子序列](https://programmercarl.com/0491.递增子序列.html)中也分析过,主要是因为程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且insert的时候其底层的符号表也要做相应的扩充,也是费时的。 + +**而使用used数组在时间复杂度上几乎没有额外负担!** + +**使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)中分析过,组合,子集,排列问题的空间复杂度都是O(n),但如果使用set去重,空间复杂度就变成了O(n^2),因为每一层递归都有一个set集合,系统栈空间是n,每一个空间都有set集合。 + +那有同学可能疑惑 用used数组也是占用O(n)的空间啊? + +used数组可是全局变量,每层与每层之间公用一个used数组,所以空间复杂度是O(n + n),最终空间复杂度还是O(n)。 + +## 总结 + +本篇本打算是对[本周小结!(回溯算法系列三)](https://programmercarl.com/周总结/20201112回溯周末总结.html)的一个点做一下纠正,没想到又写出来这么多! + +**这个点都源于一位录友的疑问,然后我思考总结了一下,就写着这一篇,所以还是得多交流啊!** + +如果大家对「代码随想录」文章有什么疑问,尽管打卡留言的时候提出来哈,或者在交流群里提问。 + +**其实这就是相互学习的过程,交流一波之后都对题目理解的更深刻了,我如果发现文中有问题,都会在评论区或者下一篇文章中即时修正,保证不会给大家带跑偏!** + + +## 其他语言版本 + +### Java +**47.全排列II** + + +```java +class Solution { + private List> res = new ArrayList<>(); + private List path = new ArrayList<>(); + private boolean[] used = null; + + public List> permuteUnique(int[] nums) { + used = new boolean[nums.length]; + Arrays.sort(nums); + backtracking(nums); + return res; + } + + public void backtracking(int[] nums) { + if (path.size() == nums.length) { + res.add(new ArrayList<>(path)); + return; + } + HashSet hashSet = new HashSet<>();//层去重 + for (int i = 0; i < nums.length; i++) { + if (hashSet.contains(nums[i])) + continue; + if (used[i] == true)//枝去重 + continue; + hashSet.add(nums[i]);//记录元素 + used[i] = true; + path.add(nums[i]); + backtracking(nums); + path.remove(path.size() - 1); + used[i] = false; + } + } +} + +``` +**90.子集II** +```java +class Solution { + List> reslut = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + + public List> subsetsWithDup(int[] nums) { + if(nums.length == 0){ + reslut.add(path); + return reslut; + } + Arrays.sort(nums); + backtracking(nums,0); + return reslut; + } + + public void backtracking(int[] nums,int startIndex){ + reslut.add(new ArrayList<>(path)); + if(startIndex >= nums.length)return; + HashSet hashSet = new HashSet<>(); + for(int i = startIndex; i < nums.length; i++){ + if(hashSet.contains(nums[i])){ + continue; + } + hashSet.add(nums[i]); + path.add(nums[i]); + backtracking(nums,i+1); + path.removeLast(); + } + } +} +``` +**40.组合总和II** +```java +class Solution { + List> result = new ArrayList<>(); + LinkedList path = new LinkedList<>(); + public List> combinationSum2(int[] candidates, int target) { + Arrays.sort( candidates ); + if( candidates[0] > target ) return result; + backtracking(candidates,target,0,0); + return result; + } + + public void backtracking(int[] candidates,int target,int sum,int startIndex){ + if( sum > target )return; + if( sum == target ){ + result.add( new ArrayList<>(path) ); + } + HashSet hashSet = new HashSet<>(); + for( int i = startIndex; i < candidates.length; i++){ + if( hashSet.contains(candidates[i]) ){ + continue; + } + hashSet.add(candidates[i]); + path.add(candidates[i]); + sum += candidates[i]; + backtracking(candidates,target,sum,i+1); + path.removeLast(); + sum -= candidates[i]; + } + } +} +``` +### Python + +**90.子集II** + +```python +class Solution: + def subsetsWithDup(self, nums): + nums.sort() # 去重需要排序 + result = [] + self.backtracking(nums, 0, [], result) + return result + + def backtracking(self, nums, startIndex, path, result): + result.append(path[:]) + used = set() + for i in range(startIndex, len(nums)): + if nums[i] in used: + continue + used.add(nums[i]) + path.append(nums[i]) + self.backtracking(nums, i + 1, path, result) + path.pop() + +``` + +**40. 组合总和 II** + +```python +class Solution: + def combinationSum2(self, candidates, target): + candidates.sort() + result = [] + self.backtracking(candidates, target, 0, 0, [], result) + return result + + def backtracking(self, candidates, target, sum, startIndex, path, result): + if sum == target: + result.append(path[:]) + return + used = set() + for i in range(startIndex, len(candidates)): + if sum + candidates[i] > target: + break + if candidates[i] in used: + continue + used.add(candidates[i]) + sum += candidates[i] + path.append(candidates[i]) + self.backtracking(candidates, target, sum, i + 1, path, result) + sum -= candidates[i] + path.pop() + +``` + +**47. 全排列 II** + +```python +class Solution: + def permuteUnique(self, nums): + nums.sort() # 排序 + result = [] + self.backtracking(nums, [False] * len(nums), [], result) + return result + + def backtracking(self, nums, used, path, result): + if len(path) == len(nums): + result.append(path[:]) + return + used_set = set() + for i in range(len(nums)): + if nums[i] in used_set: + continue + if not used[i]: + used_set.add(nums[i]) + used[i] = True + path.append(nums[i]) + self.backtracking(nums, used, path, result) + path.pop() + used[i] = False + +``` + +### JavaScript + +**90.子集II** + +```javascript +function subsetsWithDup(nums) { + nums.sort((a, b) => a - b); + const resArr = []; + backTraking(nums, 0, []); + return resArr; + function backTraking(nums, startIndex, route) { + resArr.push([...route]); + const helperSet = new Set(); + for (let i = startIndex, length = nums.length; i < length; i++) { + if (helperSet.has(nums[i])) continue; + helperSet.add(nums[i]); + route.push(nums[i]); + backTraking(nums, i + 1, route); + route.pop(); + } + } +}; +``` + +**40. 组合总和 II** + +```javascript +function combinationSum2(candidates, target) { + candidates.sort((a, b) => a - b); + const resArr = []; + backTracking(candidates, target, 0, 0, []); + return resArr; + function backTracking( candidates, target, curSum, startIndex, route ) { + if (curSum > target) return; + if (curSum === target) { + resArr.push([...route]); + return; + } + const helperSet = new Set(); + for (let i = startIndex, length = candidates.length; i < length; i++) { + let tempVal = candidates[i]; + if (helperSet.has(tempVal)) continue; + helperSet.add(tempVal); + route.push(tempVal); + backTracking(candidates, target, curSum + tempVal, i + 1, route); + route.pop(); + } + } +}; +``` + +**47. 全排列 II** + +```javascript +function permuteUnique(nums) { + const resArr = []; + const usedArr = []; + backTracking(nums, []); + return resArr; + function backTracking(nums, route) { + if (nums.length === route.length) { + resArr.push([...route]); + return; + } + const usedSet = new Set(); + for (let i = 0, length = nums.length; i < length; i++) { + if (usedArr[i] === true || usedSet.has(nums[i])) continue; + usedSet.add(nums[i]); + route.push(nums[i]); + usedArr[i] = true; + backTracking(nums, route); + usedArr[i] = false; + route.pop(); + } + } +}; +``` + +### TypeScript + +**90.子集II** + +```typescript +function subsetsWithDup(nums: number[]): number[][] { + nums.sort((a, b) => a - b); + const resArr: number[][] = []; + backTraking(nums, 0, []); + return resArr; + function backTraking(nums: number[], startIndex: number, route: number[]): void { + resArr.push([...route]); + const helperSet: Set = new Set(); + for (let i = startIndex, length = nums.length; i < length; i++) { + if (helperSet.has(nums[i])) continue; + helperSet.add(nums[i]); + route.push(nums[i]); + backTraking(nums, i + 1, route); + route.pop(); + } + } +}; +``` + +**40. 组合总和 II** + +```typescript +function combinationSum2(candidates: number[], target: number): number[][] { + candidates.sort((a, b) => a - b); + const resArr: number[][] = []; + backTracking(candidates, target, 0, 0, []); + return resArr; + function backTracking( + candidates: number[], target: number, + curSum: number, startIndex: number, route: number[] + ) { + if (curSum > target) return; + if (curSum === target) { + resArr.push([...route]); + return; + } + const helperSet: Set = new Set(); + for (let i = startIndex, length = candidates.length; i < length; i++) { + let tempVal: number = candidates[i]; + if (helperSet.has(tempVal)) continue; + helperSet.add(tempVal); + route.push(tempVal); + backTracking(candidates, target, curSum + tempVal, i + 1, route); + route.pop(); + + } + } +}; +``` + +**47. 全排列 II** + +```typescript +function permuteUnique(nums: number[]): number[][] { + const resArr: number[][] = []; + const usedArr: boolean[] = []; + backTracking(nums, []); + return resArr; + function backTracking(nums: number[], route: number[]): void { + if (nums.length === route.length) { + resArr.push([...route]); + return; + } + const usedSet: Set = new Set(); + for (let i = 0, length = nums.length; i < length; i++) { + if (usedArr[i] === true || usedSet.has(nums[i])) continue; + usedSet.add(nums[i]); + route.push(nums[i]); + usedArr[i] = true; + backTracking(nums, route); + usedArr[i] = false; + route.pop(); + } + } +}; +``` + +### Rust + +**90.子集II**: + +```rust +use std::collections::HashSet; +impl Solution { + pub fn subsets_with_dup(mut nums: Vec) -> Vec> { + let mut res = vec![]; + let mut path = vec![]; + nums.sort(); + Self::backtracking(&nums, &mut path, &mut res, 0); + res + } + + pub fn backtracking( + nums: &Vec, + path: &mut Vec, + res: &mut Vec>, + start_index: usize, + ) { + res.push(path.clone()); + let mut helper_set = HashSet::new(); + for i in start_index..nums.len() { + if helper_set.contains(&nums[i]) { + continue; + } + helper_set.insert(nums[i]); + path.push(nums[i]); + Self::backtracking(nums, path, res, i + 1); + path.pop(); + } + } +} +``` + +**40. 组合总和 II** + +```rust +use std::collections::HashSet; +impl Solution { + pub fn backtracking( + candidates: &Vec, + target: i32, + sum: i32, + path: &mut Vec, + res: &mut Vec>, + start_index: usize, + ) { + if sum > target { + return; + } + if sum == target { + res.push(path.clone()); + } + let mut helper_set = HashSet::new(); + for i in start_index..candidates.len() { + if sum + candidates[i] <= target { + if helper_set.contains(&candidates[i]) { + continue; + } + helper_set.insert(candidates[i]); + path.push(candidates[i]); + Self::backtracking(candidates, target, sum + candidates[i], path, res, i + 1); + path.pop(); + } + } + } + + pub fn combination_sum2(mut candidates: Vec, target: i32) -> Vec> { + let mut res = vec![]; + let mut path = vec![]; + candidates.sort(); + Self::backtracking(&candidates, target, 0, &mut path, &mut res, 0); + res + } +} +``` + +**47. 全排列 II** + +```rust +use std::collections::HashSet; +impl Solution { + pub fn permute_unique(mut nums: Vec) -> Vec> { + let mut res = vec![]; + let mut path = vec![]; + let mut used = vec![false; nums.len()]; + Self::backtracking(&mut res, &mut path, &nums, &mut used); + res + } + pub fn backtracking( + res: &mut Vec>, + path: &mut Vec, + nums: &Vec, + used: &mut Vec, + ) { + if path.len() == nums.len() { + res.push(path.clone()); + return; + } + let mut helper_set = HashSet::new(); + for i in 0..nums.len() { + if used[i] || helper_set.contains(&nums[i]) { + continue; + } + helper_set.insert(nums[i]); + path.push(nums[i]); + used[i] = true; + Self::backtracking(res, path, nums, used); + used[i] = false; + path.pop(); + } + } +} +``` + + diff --git "a/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index f93d29753b..c17e0be3f6 --- "a/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\345\233\236\346\272\257\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,41 +1,44 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) +# 回溯算法理论基础 -> 今天开始正式回溯法的讲解,老规矩,先概述 +## 题目分类 -# 什么是回溯法 +回溯算法大纲 -回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 +## 算法公开课 -在二叉树系列中,我们已经不止一次,提到了回溯,例如[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)。 + + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透回溯算法(理论篇)](https://www.bilibili.com/video/BV1cy4y167mM/),相信结合视频再看本篇题解,更有助于大家对本题的理解。** + +## 理论基础 + +### 什么是回溯法 + +回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 + +在二叉树系列中,我们已经不止一次,提到了回溯,例如[二叉树:以为使用了递归,其实还隐藏着回溯](https://programmercarl.com/二叉树中递归带着回溯.html)。 回溯是递归的副产品,只要有递归就会有回溯。 **所以以下讲解中,回溯函数也就是递归函数,指的都是一个函数**。 -# 回溯法的效率 +### 回溯法的效率 回溯法的性能如何呢,这里要和大家说清楚了,**虽然回溯法很难,很不好理解,但是回溯法并不是什么高效的算法**。 **因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案**,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。 -那么既然回溯法并不高效为什么还要用它呢? +那么既然回溯法并不高效为什么还要用它呢? 因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。 此时大家应该好奇了,都什么问题,这么牛逼,只能暴力搜索。 -# 回溯法解决的问题 +### 回溯法解决的问题 回溯法,一般可以解决如下几种问题: @@ -56,28 +59,28 @@ 记住组合无序,排列有序,就可以了。 -# 如何理解回溯法 +### 如何理解回溯法 **回溯法解决的问题都可以抽象为树形结构**,是的,我指的是所有回溯法的问题都可以抽象为树形结构! -因为回溯法解决的都是在集合中递归查找子集,**集合的大小就构成了树的宽度,递归的深度,都构成的树的深度**。 +因为回溯法解决的都是在集合中递归查找子集,**集合的大小就构成了树的宽度,递归的深度就构成了树的深度**。 -递归就要有终止条件,所以必然是一颗高度有限的树(N叉树)。 +递归就要有终止条件,所以必然是一棵高度有限的树(N叉树)。 这块可能初学者还不太理解,后面的回溯算法解决的所有题目中,我都会强调这一点并画图举相应的例子,现在有一个印象就行。 -# 回溯法模板 +### 回溯法模板 这里给出Carl总结的回溯算法模板。 -在讲[二叉树的递归](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)中我们说了递归三部曲,这里我再给大家列出回溯三部曲。 +在讲[二叉树的递归](https://programmercarl.com/二叉树的递归遍历.html)中我们说了递归三部曲,这里我再给大家列出回溯三部曲。 -* 回溯函数模板返回值以及参数 +* 回溯函数模板返回值以及参数 在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。 -回溯算法中函数返回值一般为void。 +回溯算法中函数返回值一般为void。 再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。 @@ -86,12 +89,12 @@ 回溯函数伪代码如下: ``` -void backtracking(参数) +void backtracking(参数) ``` -* 回溯函数终止条件 +* 回溯函数终止条件 -既然是树形结构,那么我们在讲解[二叉树的递归](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)的时候,就知道遍历树形结构一定要有终止条件。 +既然是树形结构,那么我们在讲解[二叉树的递归](https://programmercarl.com/二叉树的递归遍历.html)的时候,就知道遍历树形结构一定要有终止条件。 所以回溯也有要终止条件。 @@ -105,13 +108,13 @@ if (终止条件) { } ``` -* 回溯搜索的遍历过程 +* 回溯搜索的遍历过程 在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。 -如图: +如图: - +![回溯算法理论基础](https://file1.kamacoder.com/i/algo/20210130173631174.png) 注意图中,我特意举例集合大小和孩子的数量是相等的! @@ -149,11 +152,11 @@ void backtracking(参数) { ``` -**这份模板很重要,后面做回溯法的题目都靠它了!** +**这份模板很重要,后面做回溯法的题目都靠它了!** 如果从来没有学过回溯算法的录友们,看到这里会有点懵,后面开始讲解具体题目的时候就会好一些了,已经做过回溯法题目的录友,看到这里应该会感同身受了。 -# 总结 +## 总结 本篇我们讲解了,什么是回溯算法,知道了回溯和递归是相辅相成的。 @@ -166,7 +169,8 @@ void backtracking(参数) { 今天是回溯算法的第一天,按照惯例Carl都是先概述一波,然后在开始讲解具体题目,没有接触过回溯法的同学刚学起来有点看不懂很正常,后面和具体题目结合起来会好一些。 -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png),也可以在[B站上找到我](https://space.bilibili.com/525438321),本文[leetcode刷题攻略](https://github.com/youngyangyang04/leetcode-master)已收录,更多[精彩算法文章](https://mp.weixin.qq.com/mp/appmsgalbum?__biz=MzUxNjY5NTYxNA==&action=getalbum&album_id=1485825793120387074&scene=173#wechat_redirect)尽在公众号:[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** + + + diff --git "a/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index ba6ca736de..460944c5d2 --- "a/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" +++ "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" @@ -1,14 +1,6 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) # 字符串:总结篇 @@ -17,7 +9,7 @@ 那么这次我们来做一个总结。 -# 什么是字符串 +## 什么是字符串 字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。 @@ -47,9 +39,10 @@ for (int i = 0; i < a.size(); i++) { 所以想处理字符串,我们还是会定义一个string类型。 -# 要不要使用库函数 -在文章[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)中强调了**打基础的时候,不要太迷恋于库函数。** +## 要不要使用库函数 + +在文章[344.反转字符串](https://programmercarl.com/0344.反转字符串.html)中强调了**打基础的时候,不要太迷恋于库函数。** 甚至一些同学习惯于调用substr,split,reverse之类的库函数,却不知道其实现原理,也不知道其时间复杂度,这样实现出来的代码,如果在面试现场,面试官问:“分析其时间复杂度”的话,一定会一脸懵逼! @@ -57,26 +50,26 @@ for (int i = 0; i < a.size(); i++) { **如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。** -# 双指针法 +## 双指针法 -在[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) ,我们使用双指针法实现了反转字符串的操作,**双指针法在数组,链表和字符串中很常用。** +在[344.反转字符串](https://programmercarl.com/0344.反转字符串.html) ,我们使用双指针法实现了反转字符串的操作,**双指针法在数组,链表和字符串中很常用。** -接着在[字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg),同样还是使用双指针法在时间复杂度O(n)的情况下完成替换空格。 +接着在[字符串:替换空格](https://programmercarl.com/剑指Offer05.替换空格.html),同样还是使用双指针法在时间复杂度O(n)的情况下完成替换空格。 **其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** -那么针对数组删除操作的问题,其实在[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA)中就已经提到了使用双指针法进行移除操作。 +那么针对数组删除操作的问题,其实在[27. 移除元素](https://programmercarl.com/0027.移除元素.html)中就已经提到了使用双指针法进行移除操作。 -同样的道理在[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中我们使用O(n)的时间复杂度,完成了删除冗余空格。 +同样的道理在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中我们使用O(n)的时间复杂度,完成了删除冗余空格。 一些同学会使用for循环里调用库函数erase来移除元素,这其实是O(n^2)的操作,因为erase就是O(n)的操作,所以这也是典型的不知道库函数的时间复杂度,上来就用的案例了。 -# 反转系列 +## 反转系列 在反转上还可以在加一些玩法,其实考察的是对代码的掌控能力。 -[字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw)中,一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 +[541. 反转字符串II](https://programmercarl.com/0541.反转字符串II.html)中,一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 其实**当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章**。 @@ -84,38 +77,38 @@ for (int i = 0; i < a.size(); i++) { 因为要找的也就是每2 * k 区间的起点,这样写程序会高效很多。 -在[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。 +在[151.翻转字符串里的单词](https://programmercarl.com/0151.翻转字符串里的单词.html)中要求翻转字符串里的单词,这道题目可以说是综合考察了字符串的多种操作。是考察字符串的好题。 这道题目通过 **先整体反转再局部反转**,实现了反转字符串里的单词。 后来发现反转字符串还有一个牛逼的用处,就是达到左旋的效果。 -在[字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ)中,我们通过**先局部反转再整体反转**达到了左旋的效果。 +在[字符串:反转个字符串还有这个用处?](https://programmercarl.com/剑指Offer58-II.左旋转字符串.html)中,我们通过**先局部反转再整体反转**达到了左旋的效果。 -# KMP +## KMP KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。** -KMP的精髓所在就是前缀表,在[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。 +KMP的精髓所在就是前缀表,在[KMP精讲](https://programmercarl.com/0028.实现strStr.html)中提到了,什么是KMP,什么是前缀表,以及为什么要用前缀表。 -前缀表:起始位置到下表i之前(包括i)的子串中,有多大长度的相同前缀后缀。 +前缀表:起始位置到下标i之前(包括i)的子串中,有多大长度的相同前缀后缀。 那么使用KMP可以解决两类经典问题: -1. 匹配问题:[28. 实现 strStr()](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) -2. 重复子串问题:[459.重复的子字符串](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ) +1. 匹配问题:[28. 实现 strStr()](https://programmercarl.com/0028.实现strStr.html) +2. 重复子串问题:[459.重复的子字符串](https://programmercarl.com/0459.重复的子字符串.html) -在[字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) 强调了什么是前缀,什么是后缀,什么又是最长相等前后缀。 +再一次强调了什么是前缀,什么是后缀,什么又是最长相等前后缀。 前缀:指不包含最后一个字符的所有以第一个字符开头的连续子串。 后缀:指不包含第一个字符的所有以最后一个字符结尾的连续子串。 -然后**针对前缀表到底要不要减一,这其实是不同KMP实现的方式**,我们在[字符串:前缀表不右移,难道就写不出KMP了?](https://mp.weixin.qq.com/s/p3hXynQM2RRROK5c6X7xfw)中针对之前两个问题,分别给出了两个不同版本的的KMP实现。 +然后**针对前缀表到底要不要减一,这其实是不同KMP实现的方式**,我们在[KMP精讲](https://programmercarl.com/0028.实现strStr.html)中针对之前两个问题,分别给出了两个不同版本的的KMP实现。 其中主要**理解j=next[x]这一步最为关键!** -# 总结 +## 总结 字符串类类型的题目,往往想法比较简单,但是实现起来并不容易,复杂的字符串题目非常考验对代码的掌控能力。 @@ -125,3 +118,7 @@ KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易 好了字符串相关的算法知识就介绍到了这里了,明天开始新的征程,大家加油! + + + + diff --git "a/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" "b/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index a8a163d3fe..98ba371fdc --- "a/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" @@ -1,109 +1,102 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) +# 数组总结篇 -# 数组理论基础 +## 数组理论基础 -数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力 +数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力 也就是说,想法很简单,但实现起来 可能就不是那么回事了。 首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题 -**数组是存放在连续内存空间上的相同类型数据的集合。** +**数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下标索引的方式获取到下标下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标对应的数据。 举一个字符数组的例子,如图所示: - + -需要两点注意的是 +需要两点注意的是 * **数组下标都是从0开始的。** -* **数组内存空间的地址是连续的** +* **数组内存空间的地址是连续的** -正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** +正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: - + 而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。 **数组的元素是不能删的,只能覆盖。** -那么二维数组直接上图,大家应该就知道怎么回事了 +那么二维数组直接上图,大家应该就知道怎么回事了 - + -**那么二维数组在内存的空间地址是连续的么?** +**那么二维数组在内存的空间地址是连续的么?** -我们来举一个例子,例如: `int[][] rating = new int[3][4];` , 这个二维数据在内存空间可不是一个 `3*4` 的连续地址空间 +我们来举一个Java的例子,例如: `int[][] rating = new int[3][4];` , 这个二维数组在内存空间可不是一个 `3*4` 的连续地址空间 看了下图,就应该明白了: - + -所以**二维数据在内存中不是 `3*4` 的连续地址空间,而是四条连续的地址空间组成!** +所以**Java的二维数组在内存中不是 `3*4` 的连续地址空间,而是四条连续的地址空间组成!** -# 数组的经典题目 +## 数组的经典题目 在面试中,数组是必考的基础数据结构。 -其实数据的题目在思想上一般比较简单的,但是如果想高效,并不容易。 +其实数组的题目在思想上一般比较简单的,但是如果想高效,并不容易。 我们之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。 -## 二分法 +### 二分法 -[数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) +[数组:每次遇到二分法,都是一看就会,一写就废](https://programmercarl.com/0704.二分查找.html) -这道题目呢,考察的数据的基本操作,思路很简单,但是在通过率在简单题里并不高,不要轻敌。 +这道题目呢,考察数组的基本操作,思路很简单,但是通过率在简单题里并不高,不要轻敌。 -可以使用暴力解法,通过这道题目,如果准求更优的算法,建议试一试用二分法,来解决这道题目 +可以使用暴力解法,通过这道题目,如果追求更优的算法,建议试一试用二分法,来解决这道题目 -暴力解法时间复杂度:O(n) -二分法时间复杂度:O(logn) +* 暴力解法时间复杂度:O(n) +* 二分法时间复杂度:O(logn) 在这道题目中我们讲到了**循环不变量原则**,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。 **二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力**。 -## 双指针法 +### 双指针法 -* [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) +* [数组:就移除个元素很难么?](https://programmercarl.com/0027.移除元素.html) -双指针法(快慢指针法):**通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** +双指针法(快慢指针法):**通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** -暴力解法时间复杂度:O(n^2) -双指针时间复杂度:O(n) +* 暴力解法时间复杂度:O(n^2) +* 双指针时间复杂度:O(n) -这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为一下两点: +这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为以下两点: * 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。 -* C++中vector和array的区别一定要弄清楚,vector的底层实现是array,所以vector展现出友好的一些都是因为经过包装了。 +* C++中vector和array的区别一定要弄清楚,vector的底层实现是array,封装后使用更友好。 双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。 -## 滑动窗口 +### 滑动窗口 -* [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) +* [数组:滑动窗口拯救了你](https://programmercarl.com/0209.长度最小的子数组.html) 本题介绍了数组操作中的另一个重要思想:滑动窗口。 -暴力解法时间复杂度:O(n^2) -滑动窗口时间复杂度:O(n) +* 暴力解法时间复杂度:O(n^2) +* 滑动窗口时间复杂度:O(n) 本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。 @@ -112,25 +105,31 @@ 如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。 -## 模拟行为 +### 模拟行为 -* [数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg) +* [数组:这个循环可以转懵很多人!](https://programmercarl.com/0059.螺旋矩阵II.html) 模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。 在这道题目中,我们再一次介绍到了**循环不变量原则**,其实这也是写程序中的重要原则。 -相信大家又遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,踩了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实**真正解决题目的代码都是简洁的,或者有原则性的**,大家可以在这道题目中体会到这一点。 +相信大家有遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,拆了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实**真正解决题目的代码都是简洁的,或者有原则性的**,大家可以在这道题目中体会到这一点。 +### 前缀和 -# 总结 +> 代码随想录后续补充题目 -从二分法到双指针,从滑动窗口到螺旋矩阵,相信如果大家真的认真做了「代码随想录」每日推荐的题目,定会有所收获。 +* [数组:求取区间和](https://programmercarl.com/kamacoder/0058.区间和.html) + +前缀和的思路其实很简单,但非常实用,如果没接触过的录友,也很难想到这个解法维度,所以 这是开阔思路 而难度又不高的好题。 -推荐的题目即使大家之前做过了,再读一遍的文章,也会帮助你提炼出解题的精髓所在。 +## 总结 -如果感觉有所收获,希望大家多多支持,打卡转发,点赞在看 都是对我最大的鼓励! +![](https://file1.kamacoder.com/i/algo/数组总结.png) -最后,大家周末愉快! +这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画,总结的非常好,分享给大家。 + +从二分法到双指针,从滑动窗口到螺旋矩阵,相信如果大家真的认真做了「代码随想录」每日推荐的题目,定会有所收获。 +推荐的题目即使大家之前做过了,再读一遍文章,也会帮助你提炼出解题的精髓所在。 diff --git "a/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index 084f11561a..49c41f5abb --- "a/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\346\225\260\347\273\204\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,14 +1,8 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + # 数组理论基础 @@ -20,11 +14,12 @@ **数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下标索引的方式获取到下标下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标对应的数据。 举一个字符数组的例子,如图所示: -![算法通关数组](https://img-blog.csdnimg.cn/2020121411152849.png) +![算法通关数组](https://file1.kamacoder.com/i/algo/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%84.png) + 需要两点注意的是 @@ -32,11 +27,12 @@ * **数组下标都是从0开始的。** * **数组内存空间的地址是连续的** -正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** +正是**因为数组在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** 例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: -![算法通关数组1](https://img-blog.csdnimg.cn/2020121411155232.png) +![算法通关数组1](https://file1.kamacoder.com/i/algo/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%841.png) + 而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。 @@ -44,20 +40,80 @@ 那么二维数组直接上图,大家应该就知道怎么回事了 -![算法通关数组2](https://img-blog.csdnimg.cn/20201214111612863.png) +![](https://file1.kamacoder.com/i/algo/20240606105522.png) **那么二维数组在内存的空间地址是连续的么?** -我们来举一个例子,例如: `int[][] rating = new int[3][4];` , 这个二维数据在内存空间可不是一个 `3*4` 的连续地址空间 +不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的。 + +我们来做一个实验,C++测试代码如下: + +```CPP +void test_arr() { + int array[2][3] = { + {0, 1, 2}, + {3, 4, 5} + }; + cout << &array[0][0] << " " << &array[0][1] << " " << &array[0][2] << endl; + cout << &array[1][0] << " " << &array[1][1] << " " << &array[1][2] << endl; +} + +int main() { + test_arr(); +} + +``` + +测试地址为 + +``` +0x7ffee4065820 0x7ffee4065824 0x7ffee4065828 +0x7ffee406582c 0x7ffee4065830 0x7ffee4065834 +``` + +注意地址为16进制,可以看出二维数组地址是连续一条线的。 + +一些录友可能看不懂内存地址,我就简单介绍一下, 0x7ffee4065820 与 0x7ffee4065824 差了一个4,就是4个字节,因为这是一个int型的数组,所以两个相邻数组元素地址差4个字节。 -看了下图,就应该明白了: +0x7ffee4065828 与 0x7ffee406582c 也是差了4个字节,在16进制里8 + 4 = c,c就是12。 -![算法通关数组3](https://img-blog.csdnimg.cn/20201214111631844.png) +如图: -所以**二维数据在内存中不是 `3*4` 的连续地址空间,而是四条连续的地址空间组成!** -很多同学会以为二维数组在内存中是一片连续的地址,其实并不是。 +![数组内存](https://file1.kamacoder.com/i/algo/20210310150641186.png) + +**所以可以看出在C++中二维数组在地址空间上是连续的**。 + +像Java是没有指针的,同时也不对程序员暴露其元素的地址,寻址操作完全交给虚拟机。 + +所以看不到每个元素的地址情况,这里我以Java为例,也做一个实验。 + +```Java +public static void test_arr() { + int[][] arr = {{1, 2, 3}, {3, 4, 5}, {6, 7, 8}, {9,9,9}}; + System.out.println(arr[0]); + System.out.println(arr[1]); + System.out.println(arr[2]); + System.out.println(arr[3]); +} +``` +输出的地址为: + +``` +[I@7852e922 +[I@4e25154f +[I@70dea4e +[I@5c647e05 +``` + +这里的数值也是16进制,这不是真正的地址,而是经过处理过后的数值了,我们也可以看出,二维数组的每一行头结点的地址是没有规则的,更谈不上连续。 + +所以Java的二维数组可能是如下排列的方式: + + +![算法通关数组3](https://file1.kamacoder.com/i/algo/20201214111631844.png) 这里面试中数组相关的理论知识就介绍完了。 -后续我将介绍面试中数组相关的五道经典面试题目,敬请期待! + + diff --git "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" old mode 100644 new mode 100755 index bd2cef42f4..2d09daeb94 --- "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" +++ "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\346\200\273\347\273\223.md" @@ -1,19 +1,12 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) +# 栈与队列总结篇 -# 栈与队列的理论基础 +## 栈与队列的理论基础 -首先我们在[栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ)中讲解了栈和队列的理论基础。 +首先我们在[栈与队列:来看看栈和队列不为人知的一面](https://programmercarl.com/栈与队列理论基础.html)中讲解了栈和队列的理论基础。 里面提到了灵魂四问: @@ -30,24 +23,24 @@ 这个问题有两个陷阱: -* 陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中是不是连续分布。 -* 陷阱2:缺省情况下,默认底层容器是deque,那么deque的在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。 +* 陷阱1:栈是容器适配器,底层容器使用不同的容器,导致栈内数据在内存中不一定是连续分布的。 +* 陷阱2:缺省情况下,默认底层容器是deque,那么deque在内存中的数据分布是什么样的呢? 答案是:不连续的,下文也会提到deque。 所以这就是考察候选者基础知识扎不扎实的好问题。 大家还是要多多重视起来! -了解了栈与队列基础之后,那么可以用[栈与队列:栈实现队列](https://mp.weixin.qq.com/s/P6tupDwRFi6Ay-L7DT4NVg) 和 [栈与队列:队列实现栈](https://mp.weixin.qq.com/s/yzn6ktUlL-vRG3-m5a8_Yw) 来练习一下栈与队列的基本操作。 +了解了栈与队列基础之后,那么可以用[栈与队列:栈实现队列](https://programmercarl.com/0232.用栈实现队列.html) 和 [栈与队列:队列实现栈](https://programmercarl.com/0225.用队列实现栈.html) 来练习一下栈与队列的基本操作。 -值得一提的是,用[栈与队列:用队列实现栈还有点别扭](https://mp.weixin.qq.com/s/yzn6ktUlL-vRG3-m5a8_Yw)中,其实只用一个队列就够了。 +值得一提的是,用[栈与队列:用队列实现栈还有点别扭](https://programmercarl.com/0225.用队列实现栈.html)中,其实只用一个队列就够了。 **一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。** -# 栈经典题目 +## 栈经典题目 -## 栈在系统中的应用 +### 栈在系统中的应用 -如果还记得编译原理的话,编译器在 词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈这种数据结构。 +如果还记得编译原理的话,编译器在词法分析的过程中处理括号、花括号等这个符号的逻辑,就是使用了栈这种数据结构。 再举个例子,linux系统中,cd这个进入目录的命令我们应该再熟悉不过了。 @@ -65,9 +58,9 @@ cd a/b/c/../../ **所以数据结构与算法的应用往往隐藏在我们看不到的地方!** -## 括号匹配问题 +### 括号匹配问题 -在[栈与队列:系统中处处都是栈的应用](https://mp.weixin.qq.com/s/nLlmPMsDCIWSqAtr0jbrpQ)中我们讲解了括号匹配问题。 +在[栈与队列:系统中处处都是栈的应用](https://programmercarl.com/0020.有效的括号.html)中我们讲解了括号匹配问题。 **括号匹配是使用栈解决的经典问题。** @@ -75,31 +68,31 @@ cd a/b/c/../../ 先来分析一下 这里有三种不匹配的情况, -1. 第一种情况,字符串里左方向的括号多余了 ,所以不匹配。 -2. 第二种情况,括号没有多余,但是 括号的类型没有匹配上。 +1. 第一种情况,字符串里左方向的括号多余了,所以不匹配。 +2. 第二种情况,括号没有多余,但是括号的类型没有匹配上。 3. 第三种情况,字符串里右方向的括号多余了,所以不匹配。 这里还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了! -## 字符串去重问题 +### 字符串去重问题 -在[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)中讲解了字符串去重问题。 +在[栈与队列:匹配问题都是栈的强项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)中讲解了字符串去重问题。 1047. 删除字符串中的所有相邻重复项 思路就是可以把字符串顺序放到一个栈中,然后如果相同的话 栈就弹出,这样最后栈里剩下的元素都是相邻不相同的元素了。 -## 逆波兰表达式问题 +### 逆波兰表达式问题 -在[栈与队列:有没有想过计算机是如何处理表达式的?](https://mp.weixin.qq.com/s/hneh2nnLT91rR8ms2fm_kw)中讲解了求逆波兰表达式。 +在[栈与队列:有没有想过计算机是如何处理表达式的?](https://programmercarl.com/0150.逆波兰表达式求值.html)中讲解了求逆波兰表达式。 -本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)中的对对碰游戏是不是就非常像了。** +本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[栈与队列:匹配问题都是栈的强项](https://programmercarl.com/1047.删除字符串中的所有相邻重复项.html)中的对对碰游戏是不是就非常像了。** -# 队列的经典题目 +## 队列的经典题目 -## 滑动窗口最大值问题 +### 滑动窗口最大值问题 -在[栈与队列:滑动窗口里求最大值引出一个重要数据结构](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA)中讲解了一种数据结构:单调队列。 +在[栈与队列:滑动窗口里求最大值引出一个重要数据结构](https://programmercarl.com/0239.滑动窗口最大值.html)中讲解了一种数据结构:单调队列。 这道题目还是比较绕的,如果第一次遇到这种题目,需要反复琢磨琢磨 @@ -112,7 +105,7 @@ cd a/b/c/../../ 设计单调队列的时候,pop,和push操作要保持如下规则: 1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作 -2. push(value):如果push的元素value大于入口元素的数值,那么就将队列出口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 +2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。 @@ -120,14 +113,14 @@ cd a/b/c/../../ **单调队列不是一成不变的,而是不同场景不同写法**,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 -**不要以为本地中的单调队列实现就是固定的写法。** +**不要以为本题中的单调队列实现就是固定的写法。** 我们用deque作为单调队列的底层数据结构,C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。 -## 求前 K 个高频元素 +### 求前 K 个高频元素 -在[栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng)中讲解了求前 K 个高频元素。 +在[栈与队列:求前 K 个高频元素和队列有啥关系?](https://programmercarl.com/0347.前K个高频元素.html)中讲解了求前 K 个高频元素。 通过求前 K 个高频元素,引出另一种队列就是**优先级队列**。 @@ -141,15 +134,15 @@ cd a/b/c/../../ 什么是堆呢? -**堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。** 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 +**堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。** 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。 本题就要**使用优先级队列来对部分频率进行排序。** 注意这里是对部分数据进行排序而不需要对所有数据排序! -所以排序的过程的时间复杂度是O(logk),整个算法的时间复杂度是O(nlogk)。 +所以排序的过程的时间复杂度是 $O(\log k)$ ,整个算法的时间复杂度是 $O(n\log k)$ 。 -# 总结 +## 总结 在栈与队列系列中,我们强调栈与队列的基础,也是很多同学容易忽视的点。 @@ -164,3 +157,4 @@ cd a/b/c/../../ 好了,栈与队列我们就总结到这里了,接下来Carl就要带大家开启新的篇章了,大家加油! + diff --git "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index 7111bbcba1..0d3cc3a0c7 --- "a/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\346\240\210\344\270\216\351\230\237\345\210\227\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,28 +1,22 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) > 来看看栈和队列不为人知的一面 +# 栈与队列理论基础 + 我想栈和队列的原理大家应该很熟悉了,队列是先进先出,栈是先进后出。 如图所示: -![栈与队列理论1](https://img-blog.csdnimg.cn/20210104235346563.png) +![栈与队列理论1](https://file1.kamacoder.com/i/algo/20210104235346563.png) -那么我这里在列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,相信使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 +那么我这里再列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 1. C++中stack 是容器么? -2. 我们使用的stack是属于那个版本的STL? +2. 我们使用的stack是属于哪个版本的STL? 3. 我们使用的STL中stack是如何实现的? 4. stack 提供迭代器来遍历stack空间么? @@ -30,7 +24,7 @@ 有的同学可能仅仅知道有栈和队列这么个数据结构,却不知道底层实现,也不清楚所使用栈和队列和STL是什么关系。 -所以这里我在给大家扫一遍基础知识, +所以这里我再给大家扫一遍基础知识, 首先大家要知道 栈和队列是STL(C++标准库)里面的两个数据结构。 @@ -51,7 +45,8 @@ C++标准库是有多个版本的,要知道我们使用的STL是哪个版本 来说一说栈,栈先进后出,如图所示: -![栈与队列理论2](https://img-blog.csdnimg.cn/20210104235434905.png) + +![栈与队列理论2](https://file1.kamacoder.com/i/algo/20210104235434905.png) 栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。 @@ -63,10 +58,10 @@ C++标准库是有多个版本的,要知道我们使用的STL是哪个版本 从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。 -![栈与队列理论3](https://img-blog.csdnimg.cn/20210104235459376.png) +![栈与队列理论3](https://file1.kamacoder.com/i/algo/20210104235459376.png) -**我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。** +**我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的底层结构。** deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。 @@ -74,7 +69,7 @@ deque是一个双向队列,只要封住一段,只开通另一端就可以实 我们也可以指定vector为栈的底层实现,初始化语句如下: -``` +```cpp std::stack > third; // 使用vector为底层容器的栈 ``` @@ -84,11 +79,15 @@ std::stack > third; // 使用vector为底层容器的栈 也可以指定list 为起底层实现,初始化queue的语句如下: -``` +```cpp std::queue> third; // 定义以list为底层容器的队列 ``` 所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。 -我这里讲的都是(clck)C++ 语言中情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖起内部原理,才能夯实基础。 +我这里讲的都是C++ 语言中的情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖其内部原理,才能夯实基础。 + + + + diff --git "a/problems/\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227\357\274\210vector\345\216\237\347\220\206\350\256\262\350\247\243\357\274\211.md" "b/problems/\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227\357\274\210vector\345\216\237\347\220\206\350\256\262\350\247\243\357\274\211.md" new file mode 100755 index 0000000000..a3566268a1 --- /dev/null +++ "b/problems/\346\240\271\346\215\256\350\272\253\351\253\230\351\207\215\345\273\272\351\230\237\345\210\227\357\274\210vector\345\216\237\347\220\206\350\256\262\350\247\243\357\274\211.md" @@ -0,0 +1,209 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + + +# 贪心算法:根据身高重建队列(续集) + +在讲解[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中,我们提到了使用vector(C++中的动态数组)来进行insert操作是费时的。 + +这里专门写一篇文章来详细说一说这个问题。 + +使用vector的代码如下: + +```CPP +// 版本一,使用vector(动态数组) +class Solution { +public: + static bool cmp(const vector a, const vector b) { + if (a[0] == b[0]) return a[1] < b[1]; + return a[0] > b[0]; + } + vector> reconstructQueue(vector>& people) { + sort (people.begin(), people.end(), cmp); + vector> que; + for (int i = 0; i < people.size(); i++) { + int position = people[i][1]; + que.insert(que.begin() + position, people[i]); + } + return que; + } +}; + +``` + +耗时如下: +![vectorinsert](https://file1.kamacoder.com/i/algo/20201218203611181.png) + +其直观上来看数组的insert操作是O(n)的,整体代码的时间复杂度是O(n^2)。 + +这么一分析好像和版本二链表实现的时间复杂度是一样的啊,为什么提交之后效率会差距这么大呢? + +```CPP +// 版本二,使用list(链表) +class Solution { +public: + // 身高从大到小排(身高相同k小的站前面) + static bool cmp(const vector a, const vector b) { + if (a[0] == b[0]) return a[1] < b[1]; + return a[0] > b[0]; + } + vector> reconstructQueue(vector>& people) { + sort (people.begin(), people.end(), cmp); + list> que; // list底层是链表实现,插入效率比vector高的多 + for (int i = 0; i < people.size(); i++) { + int position = people[i][1]; // 插入到下标为position的位置 + std::list>::iterator it = que.begin(); + while (position--) { // 寻找在插入位置 + it++; + } + que.insert(it, people[i]); + } + return vector>(que.begin(), que.end()); + } +}; +``` + +耗时如下: + +![使用链表](https://file1.kamacoder.com/i/algo/20201218200756257.png) + +大家都知道对于普通数组,一旦定义了大小就不能改变,例如int a[10];,这个数组a至多只能放10个元素,改不了的。 + +对于动态数组,就是可以不用关心初始时候的大小,可以随意往里放数据,那么耗时的原因就在于动态数组的底层实现。 + +动态数组为什么可以不受初始大小的限制,可以随意push_back数据呢? + +**首先vector的底层实现也是普通数组**。 + +vector的大小有两个维度一个是size一个是capicity,size就是我们平时用来遍历vector时候用的,例如: + +``` +for (int i = 0; i < vec.size(); i++) { + +} +``` + +而capicity是vector底层数组(就是普通数组)的大小,capicity可不一定就是size。 + +当insert数据的时候,如果已经大于capicity,capicity会成倍扩容,但对外暴漏的size其实仅仅是+1。 + +那么既然vector底层实现是普通数组,怎么扩容的? + +就是重新申请一个二倍于原数组大小的数组,然后把数据都拷贝过去,并释放原数组内存。(对,就是这么原始粗暴的方法!) + +举一个例子,如图: +![vector原理](https://file1.kamacoder.com/i/algo/20201218185902217.png) + +原vector中的size和capicity相同都是3,初始化为1 2 3,此时要push_back一个元素4。 + +那么底层其实就要申请一个大小为6的普通数组,并且把原元素拷贝过去,释放原数组内存,**注意图中底层数组的内存起始地址已经变了**。 + +**同时也注意此时capicity和size的变化,关键的地方我都标红了**。 + +而在[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中,我们使用vector来做insert的操作,此时大家可会发现,**虽然表面上复杂度是O(n^2),但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是O(n^2 + t × n)级别的,t是底层拷贝的次数**。 + +那么是不是可以直接确定好vector的大小,不让它在动态扩容了,例如在[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html)中已经给出了有people.size这么多的人,可以定义好一个固定大小的vector,这样我们就可以控制vector,不让它底层动态扩容。 + +这种方法需要自己模拟插入的操作,不仅没有直接调用insert接口那么方便,需要手动模拟插入操作,而且效率也不高! + +手动模拟的过程其实不是很简单的,需要很多细节,我粗略写了一个版本,如下: + +```CPP +// 版本三 +// 使用vector,但不让它动态扩容 +class Solution { +public: + static bool cmp(const vector a, const vector b) { + if (a[0] == b[0]) return a[1] < b[1]; + return a[0] > b[0]; + } + vector> reconstructQueue(vector>& people) { + sort (people.begin(), people.end(), cmp); + vector> que(people.size(), vector(2, -1)); + for (int i = 0; i < people.size(); i++) { + int position = people[i][1]; + if (position == que.size() - 1) que[position] = people[i]; + else { // 将插入位置后面的元素整体向后移 + for (int j = que.size() - 2; j >= position; j--) que[j + 1] = que[j]; + que[position] = people[i]; + } + } + return que; + } +}; +``` + +耗时如下: + +![vector手动模拟insert](https://file1.kamacoder.com/i/algo/20201218200626718.png) + +这份代码就是不让vector动态扩容,全程我们自己模拟insert的操作,大家也可以直观的看出是一个O(n^2)的方法了。 + +但这份代码在leetcode上统计的耗时甚至比版本一的还高,我们都不让它动态扩容了,为什么耗时更高了呢? + +一方面是leetcode的耗时统计本来就不太准,忽高忽低的,只能测个大概。 + +另一方面:可能是就算避免的vector的底层扩容,但这个固定大小的数组,每次向后移动元素赋值的次数比方法一中移动赋值的次数要多很多。 + +因为方法一中一开始数组是很小的,插入操作,向后移动元素次数比较少,即使有偶尔的扩容操作。而方法三每次都是按照最大数组规模向后移动元素的。 + +所以对于两种使用数组的方法一和方法三,也不好确定谁优,但一定都没有使用方法二链表的效率高! + +一波分析之后,对于[贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html) ,大家就安心使用链表吧!别折腾了,相当于我替大家折腾了一下。 + +## 总结 + +大家应该发现了,编程语言中一个普通容器的insert,delete的使用,都可能对写出来的算法的有很大影响! + +如果抛开语言谈算法,除非从来不用代码写算法纯分析,**否则的话,语言功底不到位O(n)的算法可以写出O(n^2)的性能**。 + +相信在这里学习算法的录友们,都是想在软件行业长远发展的,都是要从事编程的工作,那么一定要深耕好一门编程语言,这个非常重要! + + +## 其他语言版本 + +### Rust + +```rust +// 版本二,使用list(链表) +use std::collections::LinkedList; +impl Solution{ + pub fn reconstruct_queue(mut people: Vec>) -> Vec> { + let mut queue = LinkedList::new(); + people.sort_by(|a, b| { + if a[0] == b[0] { + return a[1].cmp(&b[1]); + } + b[0].cmp(&a[0]) + }); + queue.push_back(people[0].clone()); + for v in people.iter().skip(1) { + if queue.len() > v[1] as usize { + let mut back_link = queue.split_off(v[1] as usize); + queue.push_back(v.clone()); + queue.append(&mut back_link); + } else { + queue.push_back(v.clone()); + } + } + queue.into_iter().collect() + } +} +``` + +### Go + +Go中slice的`append`操作和C++中vector的扩容机制基本相同。 + +说是基本呢,其实是因为大家平时刷题和工作中遇到的数据不会特别大。 + +具体来说,当当前slice的长度小于**1024**时,执行`append`操作,新slice的capacity会变成当前的2倍;而当slice长度大于等于**1024**时,slice的扩容变成了每次增加当前slice长度的**1/4**。 + +在Go Slice的底层实现中,如果capacity不够时,会做一个reslice的操作,底层数组也会重新被复制到另一块内存区域中,所以`append`一个元素,不一定是O(1), 也可能是O(n)哦。 + + + + diff --git "a/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" "b/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" old mode 100644 new mode 100755 index c1a48f7049..068806d622 --- "a/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" +++ "b/problems/\347\256\227\346\263\225\346\250\241\346\235\277.md" @@ -1,7 +1,13 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) +# 算法模板 -## 二分查找法 +## 算法模板 -``` +### 二分查找法 + +```CPP class Solution { public: int searchInsert(vector& nums, int target) { @@ -24,9 +30,9 @@ public: ``` -## KMP +### KMP -``` +```CPP void kmp(int* next, const string& s){ next[0] = -1; int j = -1; @@ -42,11 +48,11 @@ void kmp(int* next, const string& s){ } ``` -## 二叉树 +### 二叉树 二叉树的定义: -``` +```CPP struct TreeNode { int val; TreeNode *left; @@ -55,10 +61,10 @@ struct TreeNode { }; ``` -### 深度优先遍历(递归) +#### 深度优先遍历(递归) 前序遍历(中左右) -``` +```CPP void traversal(TreeNode* cur, vector& vec) { if (cur == NULL) return; vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 @@ -67,7 +73,7 @@ void traversal(TreeNode* cur, vector& vec) { } ``` 中序遍历(左中右) -``` +```CPP void traversal(TreeNode* cur, vector& vec) { if (cur == NULL) return; traversal(cur->left, vec); // 左 @@ -75,22 +81,22 @@ void traversal(TreeNode* cur, vector& vec) { traversal(cur->right, vec); // 右 } ``` -前序遍历(中左右) -``` +后序遍历(左右中) +```CPP void traversal(TreeNode* cur, vector& vec) { if (cur == NULL) return; - vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 traversal(cur->left, vec); // 左 traversal(cur->right, vec); // 右 + vec.push_back(cur->val); // 中 ,同时也是处理节点逻辑的地方 } ``` -### 深度优先遍历(迭代法) +#### 深度优先遍历(迭代法) 相关题解:[0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md) 前序遍历(中左右) -``` +```CPP vector preorderTraversal(TreeNode* root) { vector result; stack st; @@ -116,7 +122,7 @@ vector preorderTraversal(TreeNode* root) { ``` 中序遍历(左中右) -``` +```CPP vector inorderTraversal(TreeNode* root) { vector result; // 存放中序遍历的元素 stack st; @@ -141,7 +147,7 @@ vector inorderTraversal(TreeNode* root) { ``` 后序遍历(左右中) -``` +```CPP vector postorderTraversal(TreeNode* root) { vector result; stack st; @@ -165,11 +171,11 @@ vector postorderTraversal(TreeNode* root) { return result; } ``` -### 广度优先遍历(队列) +#### 广度优先遍历(队列) -相关题解:[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) +相关题解:[0102.二叉树的层序遍历](https://programmercarl.com/0102.二叉树的层序遍历.html) -``` +```CPP vector> levelOrder(TreeNode* root) { queue que; if (root != NULL) que.push(root); @@ -195,34 +201,34 @@ vector> levelOrder(TreeNode* root) { 可以直接解决如下题目: -* [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) +* [0102.二叉树的层序遍历](https://programmercarl.com/0102.二叉树的层序遍历.html) * [0199.二叉树的右视图](https://github.com/youngyangyang04/leetcode/blob/master/problems/0199.二叉树的右视图.md) * [0637.二叉树的层平均值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0637.二叉树的层平均值.md) -* [0104.二叉树的最大深度 (迭代法)](https://github.com/youngyangyang04/leetcode/blob/master/problems/0104.二叉树的最大深度.md) +* [0104.二叉树的最大深度 (迭代法)](https://programmercarl.com/0104.二叉树的最大深度.html) -* [0111.二叉树的最小深度(迭代法)]((https://github.com/youngyangyang04/leetcode/blob/master/problems/0111.二叉树的最小深度.md)) -* [0222.完全二叉树的节点个数(迭代法)](https://github.com/youngyangyang04/leetcode/blob/master/problems/0222.完全二叉树的节点个数.md) +* [0111.二叉树的最小深度(迭代法)](https://programmercarl.com/0111.二叉树的最小深度.html) +* [0222.完全二叉树的节点个数(迭代法)](https://programmercarl.com/0222.完全二叉树的节点个数.html) -### 二叉树深度 +#### 二叉树深度 -``` +```CPP int getDepth(TreeNode* node) { if (node == NULL) return 0; return 1 + max(getDepth(node->left), getDepth(node->right)); } ``` -### 二叉树节点数量 +#### 二叉树节点数量 -``` +```CPP int countNodes(TreeNode* root) { if (root == NULL) return 0; return 1 + countNodes(root->left) + countNodes(root->right); } ``` -## 回溯算法 -``` +### 回溯算法 +```CPP void backtracking(参数) { if (终止条件) { 存放结果; @@ -238,10 +244,10 @@ void backtracking(参数) { ``` -## 并查集 +### 并查集 -``` - int n = 1005; // 更具题意而定 +```CPP + int n = 1005; // 根据题意而定 int father[1005]; // 并查集初始化 @@ -271,3 +277,577 @@ void backtracking(参数) { (持续补充ing) +## 其他语言版本 + +### JavaScript: + +#### 二分查找法 + +使用左闭右闭区间 + +```javascript +var search = 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) { + right = mid - 1; // 去左面闭区间寻找 + } else if (nums[mid] < target) { + left = mid + 1; // 去右面闭区间寻找 + } else { + return mid; + } + } + return -1; +}; +``` + +使用左闭右开区间 + +```javascript +var search = function (nums, target) { + let left = 0, right = nums.length; + // 使用左闭右开区间 [left, right) + 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 { + return mid; + } + } + return -1; +}; +``` + +#### KMP + +```javascript +var kmp = function (next, s) { + next[0] = -1; + let j = -1; + for(let i = 1; i < s.length; i++){ + while (j >= 0 && s[i] !== s[j + 1]) { + j = next[j]; + } + if (s[i] === s[j + 1]) { + j++; + } + next[i] = j; + } +} +``` + +#### 二叉树 + +##### 深度优先遍历(递归) + +二叉树节点定义: + +```javascript +function TreeNode (val, left, right) { + this.val = (val === undefined ? 0 : val); + this.left = (left === undefined ? null : left); + this.right = (right === undefined ? null : right); +} +``` + +前序遍历(中左右): + +```javascript +var preorder = function (root, list) { + if (root === null) return; + list.push(root.val); // 中 + preorder(root.left, list); // 左 + preorder(root.right, list); // 右 +} +``` + +中序遍历(左中右): + +```javascript +var inorder = function (root, list) { + if (root === null) return; + inorder(root.left, list); // 左 + list.push(root.val); // 中 + inorder(root.right, list); // 右 +} +``` + +后序遍历(左右中): + +```javascript +var postorder = function (root, list) { + if (root === null) return; + postorder(root.left, list); // 左 + postorder(root.right, list); // 右 + list.push(root.val); // 中 +} +``` + +##### 深度优先遍历(迭代) + +前序遍历(中左右): + +```javascript +var preorderTraversal = function (root) { + let res = []; + if (root === null) return res; + let stack = [root], + cur = null; + while (stack.length) { + cur = stack.pop(); + res.push(cur.val); + cur.right && stack.push(cur.right); + cur.left && stack.push(cur.left); + } + return res; +}; +``` + +中序遍历(左中右): + +```javascript +var inorderTraversal = function (root) { + let res = []; + if (root === null) return res; + let stack = []; + let cur = root; + while (stack.length !== 0 || cur !== null) { + if (cur !== null) { + stack.push(cur); + cur = cur.left; + } else { + cur = stack.pop(); + res.push(cur.val); + cur = cur.right; + } + } + return res; +}; +``` + +后序遍历(左右中): + +```javascript +var postorderTraversal = function (root) { + let res = []; + if (root === null) return res; + let stack = [root]; + let cur = null; + while (stack.length) { + cur = stack.pop(); + res.push(cur.val); + cur.left && stack.push(cur.left); + cur.right && stack.push(cur.right); + } + return res.reverse() +}; +``` + +##### 广度优先遍历(队列) + +```javascript +var levelOrder = function (root) { + let res = []; + if (root === null) return res; + let queue = [root]; + while (queue.length) { + let n = queue.length; + let temp = []; + for (let i = 0; i < n; i++) { + let node = queue.shift(); + temp.push(node.val); + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + res.push(temp); + } + return res; +}; +``` + +##### 二叉树深度 + +```javascript +var getDepth = function (node) { + if (node === null) return 0; + return 1 + Math.max(getDepth(node.left), getDepth(node.right)); +} +``` + +##### 二叉树节点数量 + +```javascript +var countNodes = function (root) { + if (root === null) return 0; + return 1 + countNodes(root.left) + countNodes(root.right); +} +``` + +#### 回溯算法 + +```javascript +function backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} + +``` + +#### 并查集 + +```javascript + let n = 1005; // 根据题意而定 + let father = new Array(n).fill(0); + + // 并查集初始化 + function init () { + for (int i = 0; i < n; ++i) { + father[i] = i; + } + } + // 并查集里寻根的过程 + function find (u) { + return u === father[u] ? u : father[u] = find(father[u]); + } + // 将v->u 这条边加入并查集 + function join(u, v) { + u = find(u); + v = find(v); + if (u === v) return ; + father[v] = u; + } + // 判断 u 和 v是否找到同一个根 + function same(u, v) { + u = find(u); + v = find(v); + return u === v; + } +``` + +### TypeScript: + +#### 二分查找法 + +使用左闭右闭区间 + +```typescript +var search = function (nums: number[], target: number): number { + let left: number = 0, right: number = nums.length - 1; + // 使用左闭右闭区间 + while (left <= right) { + let mid: number = left + Math.floor((right - left)/2); + if (nums[mid] > target) { + right = mid - 1; // 去左面闭区间寻找 + } else if (nums[mid] < target) { + left = mid + 1; // 去右面闭区间寻找 + } else { + return mid; + } + } + return -1; +}; +``` + +使用左闭右开区间 + +```typescript +var search = function (nums: number[], target: number): number { + let left: number = 0, right: number = nums.length; + // 使用左闭右开区间 [left, right) + while (left < right) { + let mid: number = left + Math.floor((right - left)/2); + if (nums[mid] > target) { + right = mid; // 去左面闭区间寻找 + } else if (nums[mid] < target) { + left = mid + 1; // 去右面闭区间寻找 + } else { + return mid; + } + } + return -1; +}; +``` + +#### KMP + +```typescript +var kmp = function (next: number[], s: number): void { + next[0] = -1; + let j: number = -1; + for(let i: number = 1; i < s.length; i++){ + while (j >= 0 && s[i] !== s[j + 1]) { + j = next[j]; + } + if (s[i] === s[j + 1]) { + j++; + } + next[i] = j; + } +} +``` + +#### 二叉树 + +##### 深度优先遍历(递归) + +二叉树节点定义: + +```typescript +class TreeNode { + val: number + left: TreeNode | null + right: TreeNode | null + constructor(val?: number, left?: TreeNode | null, right?: TreeNode | null) { + this.val = (val===undefined ? 0 : val) + this.left = (left===undefined ? null : left) + this.right = (right===undefined ? null : right) + } +} +``` + +前序遍历(中左右): + +```typescript +var preorder = function (root: TreeNode | null, list: number[]): void { + if (root === null) return; + list.push(root.val); // 中 + preorder(root.left, list); // 左 + preorder(root.right, list); // 右 +} +``` + +中序遍历(左中右): + +```typescript +var inorder = function (root: TreeNode | null, list: number[]): void { + if (root === null) return; + inorder(root.left, list); // 左 + list.push(root.val); // 中 + inorder(root.right, list); // 右 +} +``` + +后序遍历(左右中): + +```typescript +var postorder = function (root: TreeNode | null, list: number[]): void { + if (root === null) return; + postorder(root.left, list); // 左 + postorder(root.right, list); // 右 + list.push(root.val); // 中 +} +``` + +##### 深度优先遍历(迭代) + +前序遍历(中左右): + +```typescript +var preorderTraversal = function (root: TreeNode | null): number[] { + let res: number[] = []; + if (root === null) return res; + let stack: TreeNode[] = [root], + cur: TreeNode | null = null; + while (stack.length) { + cur = stack.pop(); + res.push(cur.val); + cur.right && stack.push(cur.right); + cur.left && stack.push(cur.left); + } + return res; +}; +``` + +中序遍历(左中右): + +```typescript +var inorderTraversal = function (root: TreeNode | null): number[] { + let res: number[] = []; + if (root === null) return res; + let stack: TreeNode[] = []; + let cur: TreeNode | null = root; + while (stack.length !== 0 || cur !== null) { + if (cur !== null) { + stack.push(cur); + cur = cur.left; + } else { + cur = stack.pop(); + res.push(cur.val); + cur = cur.right; + } + } + return res; +}; +``` + +后序遍历(左右中): + +```typescript +var postorderTraversal = function (root: TreeNode | null): number[] { + let res: number[] = []; + if (root === null) return res; + let stack: TreeNode[] = [root]; + let cur: TreeNode | null = null; + while (stack.length) { + cur = stack.pop(); + res.push(cur.val); + cur.left && stack.push(cur.left); + cur.right && stack.push(cur.right); + } + return res.reverse() +}; +``` + +##### 广度优先遍历(队列) + +```typescript +var levelOrder = function (root: TreeNode | null): number[] { + let res: number[] = []; + if (root === null) return res; + let queue: TreeNode[] = [root]; + while (queue.length) { + let n: number = queue.length; + let temp: number[] = []; + for (let i: number = 0; i < n; i++) { + let node: TreeNode = queue.shift(); + temp.push(node.val); + node.left && queue.push(node.left); + node.right && queue.push(node.right); + } + res.push(temp); + } + return res; +}; +``` + +##### 二叉树深度 + +```typescript +var getDepth = function (node: TreNode | null): number { + if (node === null) return 0; + return 1 + Math.max(getDepth(node.left), getDepth(node.right)); +} +``` + +##### 二叉树节点数量 + +```typescript +var countNodes = function (root: TreeNode | null): number { + if (root === null) return 0; + return 1 + countNodes(root.left) + countNodes(root.right); +} +``` + +#### 回溯算法 + +```typescript +function backtracking(参数) { + if (终止条件) { + 存放结果; + return; + } + + for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) { + 处理节点; + backtracking(路径,选择列表); // 递归 + 回溯,撤销处理结果 + } +} + +``` + +#### 并查集 + +```typescript + let n: number = 1005; // 根据题意而定 + let father: number[] = new Array(n).fill(0); + + // 并查集初始化 + function init () { + for (int i: number = 0; i < n; ++i) { + father[i] = i; + } + } + // 并查集里寻根的过程 + function find (u: number): number { + return u === father[u] ? u : father[u] = find(father[u]); + } + // 将v->u 这条边加入并查集 + function join(u: number, v: number) { + u = find(u); + v = find(v); + if (u === v) return ; + father[v] = u; + } + // 判断 u 和 v是否找到同一个根 + function same(u: number, v: number): boolean { + u = find(u); + v = find(v); + return u === v; + } +``` + +Java: + +### Python: + +#### 二分查找法 +```python +def binarysearch(nums, target): + low = 0 + high = len(nums) - 1 + while (low <= high): + mid = (high + low)//2 + + if (nums[mid] < target): + low = mid + 1 + + if (nums[mid] > target): + high = mid - 1 + + if (nums[mid] == target): + return mid + + return -1 +``` + +#### KMP + +```python +def kmp(self, a, s): + # a: length of the array + # s: string + + next = [0]*a + j = 0 + next[0] = 0 + + for i in range(1, len(s)): + while j > 0 and s[j] != s[i]: + j = next[j - 1] + + if s[j] == s[i]: + j += 1 + next[i] = j + return next +``` + + +Go: + + + + diff --git "a/problems/\350\203\214\345\214\205\346\200\273\347\273\223\347\257\207.md" "b/problems/\350\203\214\345\214\205\346\200\273\347\273\223\347\257\207.md" new file mode 100755 index 0000000000..3f3841e1a2 --- /dev/null +++ "b/problems/\350\203\214\345\214\205\346\200\273\347\273\223\347\257\207.md" @@ -0,0 +1,103 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 听说背包问题很难? 这篇总结篇来拯救你了 + +年前我们已经把背包问题都讲完了,那么现在我们要对背包问题进行总结一番。 + +背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。 + +关于这几种常见的背包,其关系如下: + +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20230310000726.png) + +通过这个图,可以很清晰分清这几种常见背包之间的关系。 + +在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。 + +1. 确定dp数组(dp table)以及下标的含义 +2. 确定递推公式 +3. dp数组如何初始化 +4. 确定遍历顺序 +5. 举例推导dp数组 + +**其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结**。 + +## 背包递推公式 + +问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下: +* [动态规划:416.分割等和子集](https://programmercarl.com/0416.分割等和子集.html) +* [动态规划:1049.最后一块石头的重量 II](https://programmercarl.com/1049.最后一块石头的重量II.html) + +问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下: +* [动态规划:494.目标和](https://programmercarl.com/0494.目标和.html) +* [动态规划:518. 零钱兑换 II](https://programmercarl.com/0518.零钱兑换II.html) +* [动态规划:377.组合总和Ⅳ](https://programmercarl.com/0377.组合总和Ⅳ.html) +* [动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html) + +问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下: +* [动态规划:474.一和零](https://programmercarl.com/0474.一和零.html) + +问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下: +* [动态规划:322.零钱兑换](https://programmercarl.com/0322.零钱兑换.html) +* [动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html) + + +## 遍历顺序 + +### 01背包 + +在[动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。 + +和[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。 + +**一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!** + +### 完全背包 + +说完01背包,再看看完全背包。 + +在[动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。 + +但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +相关题目如下: + +* 求组合数:[动态规划:518.零钱兑换II](https://programmercarl.com/0518.零钱兑换II.html) +* 求排列数:[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://programmercarl.com/0070.爬楼梯完全背包版本.html) + +如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下: + +* 求最小数:[动态规划:322. 零钱兑换](https://programmercarl.com/0322.零钱兑换.html)、[动态规划:279.完全平方数](https://programmercarl.com/0279.完全平方数.html) + + +**对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了**。 + + +## 总结 + + +**这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了**。 + +**而且每一个点,我都给出了对应的力扣题目**。 + +最后如果你想了解多重背包,可以看这篇[动态规划:关于多重背包,你该了解这些!](https://programmercarl.com/背包问题理论基础多重背包.html),力扣上还没有多重背包的题目,也不是面试考察的重点。 + +如果把我本篇总结出来的内容都掌握的话,可以说对背包问题理解的就很深刻了,用来对付面试中的背包问题绰绰有余! + +背包问题总结: + +![](https://file1.kamacoder.com/i/algo/背包问题1.jpeg) + +这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画结的非常好,分享给大家。 + + + + + diff --git "a/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-1.md" "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-1.md" new file mode 100755 index 0000000000..d3258c425e --- /dev/null +++ "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-1.md" @@ -0,0 +1,593 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + + +# 动态规划:01背包理论基础 + + +本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习,题意是一样的。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透0-1背包问题!](https://www.bilibili.com/video/BV1cg411g7Y6/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +正式开始讲解背包问题! + +对于面试的话,其实掌握01背包和完全背包,就够用了,最多可以再来一个多重背包。 + +如果这几种背包,分不清,我这里画了一个图,如下: + +![416.分割等和子集1](https://file1.kamacoder.com/i/algo/20210117171307407.png) + +除此以外其他类型的背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。 + +而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。 + +**所以背包问题的理论基础重中之重是01背包,一定要理解透**! + +leetcode上没有纯01背包的问题,都是01背包应用方面的题目,也就是需要转化为01背包问题。 + +**所以我先通过纯01背包问题,把01背包原理讲清楚,后续再讲解leetcode题目的时候,重点就是讲解如何转化为01背包问题了**。 + +之前可能有些录友已经可以熟练写出背包了,但只要把这个文章仔细看完,相信你会意外收获! + +### 01 背包 + +有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。 + +这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。 + +这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢? + +每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。 + +**所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!** + +在下面的讲解中,我举一个例子: + +背包最大重量为4。 + +物品为: + +| | 重量 | 价值 | +| ----- | ---- | ---- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +问背包能背的物品最大价值是多少? + +以下讲解和图示中出现的数字都是以这个例子为例。 + +(为了方便表述,下面描述 统一用 容量为XX的背包,放下容量(重量)为XX的物品,物品的价值是XX) + +### 二维dp数组01背包 + +依然动规五部曲分析一波。 + +#### 1. 确定dp数组以及下标的含义 + +我们需要使用二维数组,为什么呢? + +因为有两个维度需要分别表示:物品 和 背包容量 + +如图,二维数组为 dp[i][j]。 + +![动态规划-背包问题1](https://file1.kamacoder.com/i/algo/20210110103003361.png) + +那么这里 i 、j、dp[i][j] 分别表示什么呢? + +i 来表示物品、j表示背包容量。 + +(如果想用j 表示物品,i 表示背包容量 行不行? 都可以的,个人习惯而已) + +我们来尝试把上面的 二维表格填写一下。 + +动态规划的思路是根据子问题的求解推导出整体的最优解。 + +我们先看把物品0 放入背包的情况: + +![](https://file1.kamacoder.com/i/algo/20240730113455.png) + +背包容量为0,放不下物品0,此时背包里的价值为0。 + +背包容量为1,可以放下物品0,此时背包里的价值为15. + +背包容量为2,依然可以放下物品0 (注意 01背包里物品只有一个),此时背包里的价值为15。 + +以此类推。 + +再看把物品1 放入背包: + +![](https://file1.kamacoder.com/i/algo/20240730114228.png) + +背包容量为 0,放不下物品0 或者物品1,此时背包里的价值为0。 + +背包容量为 1,只能放下物品0,背包里的价值为15。 + +背包容量为 2,只能放下物品0,背包里的价值为15。 + +背包容量为 3,上一行同一状态,背包只能放物品0,这次也可以选择物品1了,背包可以放物品1 或者 物品0,物品1价值更大,背包里的价值为20。 + +背包容量为 4,上一行同一状态,背包只能放物品0,这次也可以选择物品1了,背包可以放下物品0 和 物品1,背包价值为35。 + +以上举例,是比较容易看懂,我主要是通过这个例子,来帮助大家明确dp数组的含义。 + +上图中,我们看 dp[1][4] 表示什么意思呢。 + +任取 物品0,物品1 放进容量为4的背包里,最大价值是 dp[1][4]。 + +通过这个举例,我们来进一步明确dp数组的含义。 + +即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。 + +**要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的**,如果哪里看懵了,就来回顾一下i代表什么,j又代表什么。 + +#### 2. 确定递推公式 + +这里在把基本信息给出来: + +| | 重量 | 价值 | +| ----- | ---- | ---- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。 + +这里我们dp[1][4]的状态来举例: + +求取 dp[1][4] 有两种情况: + +1. 放物品1 +2. 还是不放物品1 + +如果不放物品1, 那么背包的价值应该是 dp[0][4] 即 容量为4的背包,只放物品0的情况。 + +推导方向如图: + +![](https://file1.kamacoder.com/i/algo/20240730174246.png) + +如果放物品1, **那么背包要先留出物品1的容量**,目前容量是4,物品1 的容量(就是物品1的重量)为3,此时背包剩下容量为1。 + +容量为1,只考虑放物品0 的最大价值是 dp[0][1],这个值我们之前就计算过。 + +所以 放物品1 的情况 = dp[0][1] + 物品1 的价值,推导方向如图: + +![](https://file1.kamacoder.com/i/algo/20240730174436.png) + +两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值) + +`dp[1][4] = max(dp[0][4], dp[0][1] + 物品1 的价值) ` + +以上过程,抽象化如下: + +* **不放物品i**:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。 + +* **放物品i**:背包空出物品i的容量后,背包容量为j - weight[i],dp[i - 1][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i - 1][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值 + +递归公式: `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` + +#### 3. dp数组如何初始化 + +**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。 + +首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图: + +![动态规划-背包问题2](https://file1.kamacoder.com/i/algo/2021011010304192.png) + +在看其他情况。 + +状态转移方程 `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` 可以看出i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。 + +dp[0][j],即:i为0,存放编号0的物品的时候,各个容量的背包所能存放的最大价值。 + +那么很明显当 `j < weight[0]`的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。 + +当`j >= weight[0]`时,dp[0][j] 应该是value[0],因为背包容量放足够放编号0物品。 + +代码初始化如下: + +```CPP +for (int i = 1; i < weight.size(); i++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。 + dp[i][0] = 0; +} +// 正序遍历 +for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; +} +``` + + +此时dp数组初始化情况如图所示: + +![动态规划-背包问题7](https://file1.kamacoder.com/i/algo/20210110103109140.png) + +dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢? + +其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由左上方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。 + +**初始-1,初始-2,初始100,都可以!** + +但只不过一开始就统一把dp数组统一初始为0,更方便一些。 + +如图: + +![动态规划-背包问题10](https://file1.kamacoder.com/i/algo/%E5%8A%A8%E6%80%81%E8%A7%84%E5%88%92-%E8%83%8C%E5%8C%85%E9%97%AE%E9%A2%9810.jpg) + +最后初始化代码如下: + +```CPP +// 初始化 dp +vector> dp(weight.size(), vector(bagweight + 1, 0)); +for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; +} + +``` + +**费了这么大的功夫,才把如何初始化讲清楚,相信不少同学平时初始化dp数组是凭感觉来的,但有时候感觉是不靠谱的**。 + +#### 4. 确定遍历顺序 + +在如下图中,可以看出,有两个遍历的维度:物品与背包重量 + +![动态规划-背包问题3](https://file1.kamacoder.com/i/algo/2021011010314055.png) + +那么问题来了,**先遍历 物品还是先遍历背包重量呢?** + +**其实都可以!! 但是先遍历物品更好理解**。 + +那么我先给出先遍历物品,然后遍历背包重量的代码。 + +```CPP +// weight数组的大小 就是物品个数 +for(int i = 1; i < weight.size(); i++) { // 遍历物品 + for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + + } +} +``` + +**先遍历背包,再遍历物品,也是可以的!(注意我这里使用的二维dp数组)** + +例如这样: + +```CPP +// weight数组的大小 就是物品个数 +for(int j = 0; j <= bagweight; j++) { // 遍历背包容量 + for(int i = 1; i < weight.size(); i++) { // 遍历物品 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } +} +``` + +为什么也是可以的呢? + +**要理解递归的本质和递推的方向**。 + +`dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` 递归公式中可以看出dp[i][j]是靠dp[i-1][j]和dp[i - 1][j - weight[i]]推导出来的。 + +dp[i-1][j]和dp[i - 1][j - weight[i]] 都在dp[i][j]的左上角方向(包括正上方向),那么先遍历物品,再遍历背包的过程如图所示: + +![动态规划-背包问题5](https://file1.kamacoder.com/i/algo/202101101032124.png) + +再来看看先遍历背包,再遍历物品呢,如图: + +![动态规划-背包问题6](https://file1.kamacoder.com/i/algo/20210110103244701.png) + +**大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!** + +但先遍历物品再遍历背包这个顺序更好理解。 + +**其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了**。 + +#### 5. 举例推导dp数组 + +来看一下对应的dp数组的数值,如图: + +![动态规划-背包问题4](https://file1.kamacoder.com/i/algo/20210118163425129.jpg) + +最终结果就是dp[2][4]。 + +建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。 + +**做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!** + +很多同学做dp题目,遇到各种问题,然后凭感觉东改改西改改,怎么改都不对,或者稀里糊涂就改过了。 + +主要就是自己没有动手推导一下dp数组的演变过程,如果推导明白了,代码写出来就算有问题,只要把dp数组打印出来,对比一下和自己推导的有什么差异,很快就可以发现问题了。 + +本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习,题意是一样的,代码如下: + +```CPP +#include +using namespace std; + +int main() { + int n, bagweight;// bagweight代表行李箱空间 + + cin >> n >> bagweight; + + vector weight(n, 0); // 存储每件物品所占空间 + vector value(n, 0); // 存储每件物品价值 + + for(int i = 0; i < n; ++i) { + cin >> weight[i]; + } + for(int j = 0; j < n; ++j) { + cin >> value[j]; + } + // dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值 + vector> dp(weight.size(), vector(bagweight + 1, 0)); + + // 初始化, 因为需要用到dp[i - 1]的值 + // j < weight[0]已在上方被初始化为0 + // j >= weight[0]的值就初始化为value[0] + for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } + + for(int i = 1; i < weight.size(); i++) { // 遍历科研物品 + for(int j = 0; j <= bagweight; j++) { // 遍历行李箱容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; // 如果装不下这个物品,那么就继承dp[i - 1][j]的值 + else { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } + } + cout << dp[n - 1][bagweight] << endl; + + return 0; +} + + +``` + + +## 总结 + +背包问题 是动态规划里的经典类型题目,大家要细细品味。 + +可能有的同学并没有注意到初始化 和 遍历顺序的重要性,我们后面做力扣上背包面试题目的时候,大家就会感受出来了。 + +下一篇 还是理论基础,我们再来讲一维dp数组实现的01背包(滚动数组),分析一下和二维有什么区别,在初始化和遍历顺序上又有什么差异。 + + + +## 其他语言版本 + +### Java + +```Java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int bagweight = scanner.nextInt(); + + int[] weight = new int[n]; + int[] value = new int[n]; + + for (int i = 0; i < n; ++i) { + weight[i] = scanner.nextInt(); + } + for (int j = 0; j < n; ++j) { + value[j] = scanner.nextInt(); + } + + int[][] dp = new int[n][bagweight + 1]; + + for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } + + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } + } + + System.out.println(dp[n - 1][bagweight]); + } +} + +``` + +### Python + +```python +n, bagweight = map(int, input().split()) + +weight = list(map(int, input().split())) +value = list(map(int, input().split())) + +dp = [[0] * (bagweight + 1) for _ in range(n)] + +for j in range(weight[0], bagweight + 1): + dp[0][j] = value[0] + +for i in range(1, n): + for j in range(bagweight + 1): + if j < weight[i]: + dp[i][j] = dp[i - 1][j] + else: + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]) + +print(dp[n - 1][bagweight]) + +``` + +### Go + +```go +package main + +import ( + "fmt" +) + +func main() { + var n, bagweight int // bagweight代表行李箱空间 + fmt.Scan(&n, &bagweight) + + weight := make([]int, n) // 存储每件物品所占空间 + value := make([]int, n) // 存储每件物品价值 + + for i := 0; i < n; i++ { + fmt.Scan(&weight[i]) + } + for j := 0; j < n; j++ { + fmt.Scan(&value[j]) + } + // dp数组, dp[i][j]代表行李箱空间为j的情况下,从下标为[0, i]的物品里面任意取,能达到的最大价值 + dp := make([][]int, n) + for i := range dp { + dp[i] = make([]int, bagweight + 1) + } + + // 初始化, 因为需要用到dp[i - 1]的值 + // j < weight[0]已在上方被初始化为0 + // j >= weight[0]的值就初始化为value[0] + for j := weight[0]; j <= bagweight; j++ { + dp[0][j] = value[0] + } + + for i := 1; i < n; i++ { // 遍历科研物品 + for j := 0; j <= bagweight; j++ { // 遍历行李箱容量 + if j < weight[i] { + dp[i][j] = dp[i-1][j] // 如果装不下这个物品,那么就继承dp[i - 1][j]的值 + } else { + dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i]) + } + } + } + + fmt.Println(dp[n-1][bagweight]) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +``` + +### JavaScript + +```js +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); + +let input = []; + +readline.on('line', (line) => { + input.push(line); +}); + +readline.on('close', () => { + let [n, bagweight] = input[0].split(' ').map(Number); + let weight = input[1].split(' ').map(Number); + let value = input[2].split(' ').map(Number); + + let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0)); + + for (let j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } + + for (let i = 1; i < n; i++) { + for (let j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } + } + + console.log(dp[n - 1][bagweight]); +}); + +``` + + +### C + +```c +#include +#include + +int max(int a, int b) { + return a > b ? a : b; +} + +int main() { + int n, bagweight; + scanf("%d %d", &n, &bagweight); + + int *weight = (int *)malloc(n * sizeof(int)); + int *value = (int *)malloc(n * sizeof(int)); + + for (int i = 0; i < n; ++i) { + scanf("%d", &weight[i]); + } + for (int j = 0; j < n; ++j) { + scanf("%d", &value[j]); + } + + int **dp = (int **)malloc(n * sizeof(int *)); + for (int i = 0; i < n; ++i) { + dp[i] = (int *)malloc((bagweight + 1) * sizeof(int)); + for (int j = 0; j <= bagweight; ++j) { + dp[i][j] = 0; + } + } + + for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } + + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } + } + + printf("%d\n", dp[n - 1][bagweight]); + + for (int i = 0; i < n; ++i) { + free(dp[i]); + } + free(dp); + free(weight); + free(value); + + return 0; +} + +``` + + + diff --git "a/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-2.md" "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-2.md" new file mode 100755 index 0000000000..00dc593417 --- /dev/null +++ "b/problems/\350\203\214\345\214\205\347\220\206\350\256\272\345\237\272\347\241\20001\350\203\214\345\214\205-2.md" @@ -0,0 +1,461 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 动态规划:01背包理论基础(滚动数组) + +本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透0-1背包问题!(滚动数组)](https://www.bilibili.com/video/BV1BU4y177kY/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + +## 思路 + +昨天[动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html)中是用二维dp数组来讲解01背包。 + +今天我们就来说一说滚动数组,其实在前面的题目中我们已经用到过滚动数组了,就是把二维dp降为一维dp,一些录友当时还表示比较困惑。 + +那么我们通过01背包,来彻底讲一讲滚动数组! + +接下来还是用如下这个例子来进行讲解 + +背包最大重量为4。 + +物品为: + +| | 重量 | 价值 | +| --- | --- | --- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +问背包能背的物品最大价值是多少? + +### 一维dp数组(滚动数组) + +对于背包问题其实状态都是可以压缩的。 + +在使用二维数组的时候,递推公式:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + +**其实可以发现如果把dp[i - 1]那一层拷贝到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i]);** + +**与其把dp[i - 1]这一层拷贝到dp[i]上,不如只用一个一维数组了**,只用dp[j](一维数组,也可以理解是一个滚动数组)。 + +这就是滚动数组的由来,需要满足的条件是上一层可以重复利用,直接拷贝到当前层。 + +读到这里估计大家都忘了 dp[i][j]里的i和j表达的是什么了,i是物品,j是背包容量。 + +**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。 + +一定要时刻记住这里i和j的含义,要不然很容易看懵了。 + +动规五部曲分析如下: + +1. 确定dp数组的定义 + +关于dp数组的定义,我在 [01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html) 有详细讲解 + +在一维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 + +2. 一维dp数组的递推公式 + +二维dp数组的递推公式为: `dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);` + +公式是怎么来的 在这里 [01背包理论基础](https://programmercarl.com/背包理论基础01背包-1.html) 有详细讲解。 + +一维dp数组,其实就上上一层 dp[i-1] 这一层 拷贝的 dp[i]来。 + +所以在 上面递推公式的基础上,去掉i这个维度就好。 + +递推公式为:`dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);` + +以下为分析: + +dp[j]为 容量为j的背包所背的最大价值。 + +dp[j]可以通过dp[j - weight[i]]推导出来,dp[j - weight[i]]表示容量为j - weight[i]的背包所背的最大价值。 + +`dp[j - weight[i]] + value[i]` 表示 容量为 [j - 物品i重量] 的背包 加上 物品i的价值。(也就是容量为j的背包,放入物品i了之后的价值即:dp[j]) + +此时dp[j]有两个选择,一个是取自己dp[j] 相当于 二维dp数组中的dp[i-1][j],即不放物品i,一个是取`dp[j - weight[i]] + value[i]`,即放物品i,指定是取最大的,毕竟是求最大价值, + +所以递归公式为: + +``` +dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); +``` + +可以看出相对于二维dp数组的写法,就是把dp[i][j]中i的维度去掉了。 + +3. 一维dp数组如何初始化 + +**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。 + +dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。 + +那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢? + +看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + +dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。 + +**这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了**。 + +那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。 + +4. 一维dp数组遍历顺序 + +代码如下: + +``` +for(int i = 0; i < weight.size(); i++) { // 遍历物品 + for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + + } +} +``` + +**这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!** + +二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。 + +为什么呢? + +**倒序遍历是为了保证物品i只被放入一次!**。但如果一旦正序遍历了,那么物品0就会被重复加入多次! + +举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15 + +如果正序遍历 + +dp[1] = dp[1 - weight[0]] + value[0] = 15 + +dp[2] = dp[2 - weight[0]] + value[0] = 30 + +此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。 + +为什么倒序遍历,就可以保证物品只放入一次呢? + +倒序就是先算dp[2] + +dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0) + +dp[1] = dp[1 - weight[0]] + value[0] = 15 + +所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。 + +**那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢?** + +因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖! + +(如何这里读不懂,大家就要动手试一试了,空想还是不靠谱的,实践出真知!) + +**再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?** + +不可以! + +因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。 + +**所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!**,这一点大家一定要注意。 + +5. 举例推导dp数组 + +一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下: + +![动态规划-背包问题9](https://file1.kamacoder.com/i/algo/20210110103614769.png) + + +本题力扣上没有原题,大家可以去[卡码网第46题](https://kamacoder.com/problempage.php?pid=1046)去练习,题意是一样的,代码如下: + +```CPP +// 一维dp数组实现 +#include +#include +using namespace std; + +int main() { + // 读取 M 和 N + int M, N; + cin >> M >> N; + + vector costs(M); + vector values(M); + + for (int i = 0; i < M; i++) { + cin >> costs[i]; + } + for (int j = 0; j < M; j++) { + cin >> values[j]; + } + + // 创建一个动态规划数组dp,初始值为0 + vector dp(N + 1, 0); + + // 外层循环遍历每个类型的研究材料 + for (int i = 0; i < M; ++i) { + // 内层循环从 N 空间逐渐减少到当前研究材料所占空间 + for (int j = N; j >= costs[i]; --j) { + // 考虑当前研究材料选择和不选择的情况,选择最大值 + dp[j] = max(dp[j], dp[j - costs[i]] + values[i]); + } + } + + // 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值 + cout << dp[N] << endl; + + return 0; +} + +``` + +可以看出,一维dp 的01背包,要比二维简洁的多! 初始化 和 遍历顺序相对简单了。 + +**所以我倾向于使用一维dp数组的写法,比较直观简洁,而且空间复杂度还降了一个数量级!** + +**在后面背包问题的讲解中,我都直接使用一维dp数组来进行推导**。 + +## 总结 + +以上的讲解可以开发一道面试题目(毕竟力扣上没原题)。 + +就是本文中的题目,要求先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。 + +然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么? + +注意以上问题都是在候选人把代码写出来的情况下才问的。 + +就是纯01背包的题目,都不用考01背包应用类的题目就可以看出候选人对算法的理解程度了。 + +**相信大家读完这篇文章,应该对以上问题都有了答案!** + +此时01背包理论基础就讲完了,我用了两篇文章把01背包的dp数组定义、递推公式、初始化、遍历顺序从二维数组到一维数组统统深度剖析了一遍,没有放过任何难点。 + +大家可以发现其实信息量还是挺大的。 + +如果把[动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html)和本篇的内容都理解了,后面我们在做01背包的题目,就会发现非常简单了。 + +不用再凭感觉或者记忆去写背包,而是有自己的思考,了解其本质,代码的方方面面都在自己的掌控之中。 + +即使代码没有通过,也会有自己的逻辑去debug,这样就思维清晰了。 + + +## 其他语言版本 + +### Java + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + // 读取 M 和 N + int M = scanner.nextInt(); // 研究材料的数量 + int N = scanner.nextInt(); // 行李空间的大小 + + int[] costs = new int[M]; // 每种材料的空间占用 + int[] values = new int[M]; // 每种材料的价值 + + // 输入每种材料的空间占用 + for (int i = 0; i < M; i++) { + costs[i] = scanner.nextInt(); + } + + // 输入每种材料的价值 + for (int j = 0; j < M; j++) { + values[j] = scanner.nextInt(); + } + + // 创建一个动态规划数组 dp,初始值为 0 + int[] dp = new int[N + 1]; + + // 外层循环遍历每个类型的研究材料 + for (int i = 0; i < M; i++) { + // 内层循环从 N 空间逐渐减少到当前研究材料所占空间 + for (int j = N; j >= costs[i]; j--) { + // 考虑当前研究材料选择和不选择的情况,选择最大值 + dp[j] = Math.max(dp[j], dp[j - costs[i]] + values[i]); + } + } + + // 输出 dp[N],即在给定 N 行李空间可以携带的研究材料的最大价值 + System.out.println(dp[N]); + + scanner.close(); + } +} + +``` + + + + +### Python + +```python +n, bagweight = map(int, input().split()) +weight = list(map(int, input().split())) +value = list(map(int, input().split())) + +dp = [0] * (bagweight + 1) # 创建一个动态规划数组dp,初始值为0 + +dp[0] = 0 # 初始化dp[0] = 0,背包容量为0,价值最大为0 + +for i in range(n): # 应该先遍历物品,如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品 + for j in range(bagweight, weight[i]-1, -1): # 倒序遍历背包容量是为了保证物品i只被放入一次 + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) + +print(dp[bagweight]) + +``` +### Go +```go +package main + +import ( + "fmt" +) + +func main() { + // 读取 M 和 N + var M, N int + fmt.Scan(&M, &N) + + costs := make([]int, M) + values := make([]int, M) + + for i := 0; i < M; i++ { + fmt.Scan(&costs[i]) + } + for j := 0; j < M; j++ { + fmt.Scan(&values[j]) + } + + // 创建一个动态规划数组dp,初始值为0 + dp := make([]int, N + 1) + + // 外层循环遍历每个类型的研究材料 + for i := 0; i < M; i++ { + // 内层循环从 N 空间逐渐减少到当前研究材料所占空间 + for j := N; j >= costs[i]; j-- { + // 考虑当前研究材料选择和不选择的情况,选择最大值 + dp[j] = max(dp[j], dp[j-costs[i]] + values[i]) + } + } + + // 输出dp[N],即在给定 N 行李空间可以携带的研究材料最大价值 + fmt.Println(dp[N]) +} + +func max(x, y int) int { + if x > y { + return x + } + return y +} + +``` + +### JavaScript + +```js +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); + +let input = []; + +readline.on('line', (line) => { + input.push(line); +}); + +readline.on('close', () => { + let [n, bagweight] = input[0].split(' ').map(Number); + let weight = input[1].split(' ').map(Number); + let value = input[2].split(' ').map(Number); + + let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0)); + + for (let j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } + + for (let i = 1; i < n; i++) { + for (let j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } + } + + console.log(dp[n - 1][bagweight]); +}); + + +``` + +### C +```c +#include +#include + +int max(int a, int b) { + return a > b ? a : b; +} + +int main() { + int n, bagweight; + scanf("%d %d", &n, &bagweight); + + int *weight = (int *)malloc(n * sizeof(int)); + int *value = (int *)malloc(n * sizeof(int)); + + for (int i = 0; i < n; ++i) { + scanf("%d", &weight[i]); + } + for (int j = 0; j < n; ++j) { + scanf("%d", &value[j]); + } + + int **dp = (int **)malloc(n * sizeof(int *)); + for (int i = 0; i < n; ++i) { + dp[i] = (int *)malloc((bagweight + 1) * sizeof(int)); + for (int j = 0; j <= bagweight; ++j) { + dp[i][j] = 0; + } + } + + for (int j = weight[0]; j <= bagweight; j++) { + dp[0][j] = value[0]; + } + + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } + } + + printf("%d\n", dp[n - 1][bagweight]); + + for (int i = 0; i < n; ++i) { + free(dp[i]); + } + free(dp); + free(weight); + free(value); + + return 0; +} + +``` + + diff --git "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\345\256\214\345\205\250\350\203\214\345\214\205\344\270\200\347\273\264.md" "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\345\256\214\345\205\250\350\203\214\345\214\205\344\270\200\347\273\264.md" new file mode 100644 index 0000000000..7dd78302ee --- /dev/null +++ "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\345\256\214\345\205\250\350\203\214\345\214\205\344\270\200\347\273\264.md" @@ -0,0 +1,214 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 完全背包-一维数组 + +本题力扣上没有原题,大家可以去[卡码网第52题](https://kamacoder.com/problempage.php?pid=1052)去练习。 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[带你学透完全背包问题! ](https://www.bilibili.com/video/BV1uK411o7c9/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 + + +## 思路 + +本篇我们不再做五部曲分析,核心内容 在 01背包二维 、01背包一维 和 完全背包二维 的讲解中都讲过了。 + +上一篇我们刚刚讲了完全背包二维DP数组的写法: + +```CPP +for (int i = 1; i < n; i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } +} +``` + +压缩成一维DP数组,也就是将上一层拷贝到当前层。 + +将上一层dp[i-1] 的那一层拷贝到 当前层 dp[i] ,那么 递推公式由:`dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i])` 变成: `dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])` + +这里有录友想,这样拷贝的话, dp[i - 1][j] 的数值会不会 覆盖了 dp[i][j] 的数值呢? + +并不会,因为 当前层 dp[i][j] 是空的,是没有计算过的。 + +变成 `dp[i][j] = max(dp[i][j], dp[i][j - weight[i]] + value[i])` 我们压缩成一维dp数组,去掉 i 层数维度。 + +即:`dp[j] = max(dp[j], dp[j - weight[i]] + value[i])` + + +接下来我们重点讲一下遍历顺序。 + +看过这两篇的话: + +* [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) +* [01背包理论基础(一维数组)](https://programmercarl.com/背包理论基础01背包-2.html) + +就知道了,01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一维dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。 + +**在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序是无所谓的**! + +因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。 + +遍历物品在外层循环,遍历背包容量在内层循环,状态如图: + +![动态规划-完全背包1](https://file1.kamacoder.com/i/algo/20210126104529605.jpg) + +遍历背包容量在外层循环,遍历物品在内层循环,状态如图: + +![动态规划-完全背包2](https://file1.kamacoder.com/i/algo/20210729234011.png) + +看了这两个图,大家就会理解,完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。 + +先遍历背包再遍历物品,代码如下: + +```CPP +for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for(int i = 0; i < weight.size(); i++) { // 遍历物品 + if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } + cout << endl; +} +``` + +先遍历物品再遍历背包: + +```CPP +for(int i = 0; i < weight.size(); i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } +} +``` + +整体代码如下: + +```cpp +#include +#include +using namespace std; + +int main() { + int N, bagWeight; + cin >> N >> bagWeight; + vector weight(N, 0); + vector value(N, 0); + for (int i = 0; i < N; i++) { + int w; + int v; + cin >> w >> v; + weight[i] = w; + value[i] = v; + } + + vector dp(bagWeight + 1, 0); + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for(int i = 0; i < weight.size(); i++) { // 遍历物品 + if (j - weight[i] >= 0) dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } + } + cout << dp[bagWeight] << endl; + + return 0; +} +``` + + + +## 总结 + +细心的同学可能发现,**全文我说的都是对于纯完全背包问题,其for循环的先后循环是可以颠倒的!** + +但如果题目稍稍有点变化,就会体现在遍历顺序上。 + +如果问装满背包有几种方式的话? 那么两个for循环的先后顺序就有很大区别了,而leetcode上的题目都是这种稍有变化的类型。 + +这个区别,我将在后面讲解具体leetcode题目中给大家介绍,因为这块如果不结合具题目,单纯的介绍原理估计很多同学会越看越懵! + +别急,下一篇就是了! + +最后,**又可以出一道面试题了,就是纯完全背包,要求先用二维dp数组实现,然后再用一维dp数组实现,最后再问,两个for循环的先后是否可以颠倒?为什么?** + +这个简单的完全背包问题,估计就可以难住不少候选人了。 + + +## 其他语言版本 + +### Java: + +```java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int N = scanner.nextInt(); + int bagWeight = scanner.nextInt(); + + int[] weight = new int[N]; + int[] value = new int[N]; + for (int i = 0; i < N; i++) { + weight[i] = scanner.nextInt(); + value[i] = scanner.nextInt(); + } + + int[] dp = new int[bagWeight + 1]; + + for (int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for (int i = 0; i < weight.length; i++) { // 遍历物品 + if (j >= weight[i]) { + dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); + } + } + } + + System.out.println(dp[bagWeight]); + scanner.close(); + } +} + +``` + + + +### Python: + +```python +def complete_knapsack(N, bag_weight, weight, value): + dp = [0] * (bag_weight + 1) + + for j in range(bag_weight + 1): # 遍历背包容量 + for i in range(len(weight)): # 遍历物品 + if j >= weight[i]: + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]) + + return dp[bag_weight] + +# 输入 +N, bag_weight = map(int, input().split()) +weight = [] +value = [] +for _ in range(N): + w, v = map(int, input().split()) + weight.append(w) + value.append(v) + +# 输出结果 +print(complete_knapsack(N, bag_weight, weight, value)) + + +``` + + +### Go: + +```go + +``` +### Javascript: + +```Javascript +``` + diff --git "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\244\232\351\207\215\350\203\214\345\214\205.md" "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\244\232\351\207\215\350\203\214\345\214\205.md" new file mode 100755 index 0000000000..39e7ebe378 --- /dev/null +++ "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\244\232\351\207\215\350\203\214\345\214\205.md" @@ -0,0 +1,237 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 动态规划:关于多重背包,你该了解这些! + +本题力扣上没有原题,大家可以去[卡码网第56题](https://kamacoder.com/problempage.php?pid=1066)去练习,题意是一样的。 + +之前我们已经系统的讲解了01背包和完全背包,如果没有看过的录友,建议先把如下三篇文章仔细阅读一波。 + +* [动态规划:关于01背包问题,你该了解这些!](https://programmercarl.com/背包理论基础01背包-1.html) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://programmercarl.com/背包理论基础01背包-2.html) +* [动态规划:关于完全背包,你该了解这些!](https://programmercarl.com/背包问题理论基础完全背包.html) + +这次我们再来说一说多重背包 + +## 多重背包 + +对于多重背包,我在力扣上还没发现对应的题目,所以这里就做一下简单介绍,大家大概了解一下。 + +有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。 + +多重背包和01背包是非常像的, 为什么和01背包像呢? + +每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。 + +例如: + +背包最大重量为10。 + +物品为: + +| | 重量 | 价值 | 数量 | +| --- | --- | --- | --- | +| 物品0 | 1 | 15 | 2 | +| 物品1 | 3 | 20 | 3 | +| 物品2 | 4 | 30 | 2 | + +问背包能背的物品最大价值是多少? + +和如下情况有区别么? + +| | 重量 | 价值 | 数量 | +| --- | --- | --- | --- | +| 物品0 | 1 | 15 | 1 | +| 物品0 | 1 | 15 | 1 | +| 物品1 | 3 | 20 | 1 | +| 物品1 | 3 | 20 | 1 | +| 物品1 | 3 | 20 | 1 | +| 物品2 | 4 | 30 | 1 | +| 物品2 | 4 | 30 | 1 | + +毫无区别,这就转成了一个01背包问题了,且每个物品只用一次。 + + +练习题目:[卡码网第56题,多重背包](https://kamacoder.com/problempage.php?pid=1066) + +代码如下: + + +```CPP +// 超时了 +#include +#include +using namespace std; +int main() { + int bagWeight,n; + cin >> bagWeight >> n; + vector weight(n, 0); + vector value(n, 0); + vector nums(n, 0); + for (int i = 0; i < n; i++) cin >> weight[i]; + for (int i = 0; i < n; i++) cin >> value[i]; + for (int i = 0; i < n; i++) cin >> nums[i]; + + for (int i = 0; i < n; i++) { + while (nums[i] > 1) { // 物品数量不是一的,都展开 + weight.push_back(weight[i]); + value.push_back(value[i]); + nums[i]--; + } + } + + vector dp(bagWeight + 1, 0); + for(int i = 0; i < weight.size(); i++) { // 遍历物品,注意此时的物品数量不是n + for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } + } + cout << dp[bagWeight] << endl; +} +``` + +大家去提交之后,发现这个解法超时了,为什么呢,哪里耗时呢? + +耗时就在 这段代码: + +```CPP +for (int i = 0; i < n; i++) { + while (nums[i] > 1) { // 物品数量不是一的,都展开 + weight.push_back(weight[i]); + value.push_back(value[i]); + nums[i]--; + } +} +``` + +如果物品数量很多的话,C++中,这种操作十分费时,主要消耗在vector的动态底层扩容上。(其实这里也可以优化,先把 所有物品数量都计算好,一起申请vector的空间。 + + +这里也有另一种实现方式,就是把每种商品遍历的个数放在01背包里面在遍历一遍。 + +代码如下:(详看注释) + +```CPP +#include +#include +using namespace std; +int main() { + int bagWeight,n; + cin >> bagWeight >> n; + vector weight(n, 0); + vector value(n, 0); + vector nums(n, 0); + for (int i = 0; i < n; i++) cin >> weight[i]; + for (int i = 0; i < n; i++) cin >> value[i]; + for (int i = 0; i < n; i++) cin >> nums[i]; + + vector dp(bagWeight + 1, 0); + + for(int i = 0; i < n; i++) { // 遍历物品 + for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 + // 以上为01背包,然后加一个遍历个数 + for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { // 遍历个数 + dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]); + } + } + } + + cout << dp[bagWeight] << endl; +} +``` + +时间复杂度:O(m × n × k),m:物品种类个数,n背包容量,k单类物品数量 + +从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。 + +当然还有那种二进制优化的方法,其实就是把每种物品的数量,打包成一个个独立的包。 + +和以上在循环遍历上有所不同,因为是分拆为各个包最后可以组成一个完整背包,具体原理我就不做过多解释了,大家了解一下就行,面试的话基本不会考完这个深度了,感兴趣可以自己深入研究一波。 + +## 总结 + +多重背包在面试中基本不会出现,力扣上也没有对应的题目,大家对多重背包的掌握程度知道它是一种01背包,并能在01背包的基础上写出对应代码就可以了。 + +至于背包九讲里面还有混合背包,二维费用背包,分组背包等等这些,大家感兴趣可以自己去学习学习,这里也不做介绍了,面试也不会考。 + + + + +## 其他语言版本 + +### Java: + +```Java +import java.util.Scanner; +class multi_pack{ + public static void main(String [] args) { + Scanner sc = new Scanner(System.in); + + /** + * bagWeight:背包容量 + * n:物品种类 + */ + int bagWeight, n; + + //获取用户输入数据,中间用空格隔开,回车键换行 + bagWeight = sc.nextInt(); + n = sc.nextInt(); + int[] weight = new int[n]; + int[] value = new int[n]; + int[] nums = new int[n]; + for (int i = 0; i < n; i++) weight[i] = sc.nextInt(); + for (int i = 0; i < n; i++) value[i] = sc.nextInt(); + for (int i = 0; i < n; i++) nums[i] = sc.nextInt(); + + int[] dp = new int[bagWeight + 1]; + + //先遍历物品再遍历背包,作为01背包处理 + for (int i = 0; i < n; i++) { + for (int j = bagWeight; j >= weight[i]; j--) { + //遍历每种物品的个数 + for (int k = 1; k <= nums[i] && (j - k * weight[i]) >= 0; k++) { + dp[j] = Math.max(dp[j], dp[j - k * weight[i]] + k * value[i]); + } + } + } + System.out.println(dp[bagWeight]); + } +} +``` +### Python: + +```python + +C, N = input().split(" ") +C, N = int(C), int(N) + +# value数组需要判断一下非空不然过不了 +weights = [int(x) for x in input().split(" ")] +values = [int(x) for x in input().split(" ") if x] +nums = [int(x) for x in input().split(" ")] + +dp = [0] * (C + 1) +# 遍历背包容量 +for i in range(N): + for j in range(C, weights[i] - 1, -1): + for k in range(1, nums[i] + 1): + # 遍历 k,如果已经大于背包容量直接跳出循环 + if k * weights[i] > j: + break + dp[j] = max(dp[j], dp[j - weights[i] * k] + values[i] * k) +print(dp[-1]) + +``` + +### Go: + + +### TypeScript: + + + + + + diff --git "a/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\256\214\345\205\250\350\203\214\345\214\205.md" "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\256\214\345\205\250\350\203\214\345\214\205.md" new file mode 100755 index 0000000000..02b3cdc32d --- /dev/null +++ "b/problems/\350\203\214\345\214\205\351\227\256\351\242\230\347\220\206\350\256\272\345\237\272\347\241\200\345\256\214\345\205\250\350\203\214\345\214\205.md" @@ -0,0 +1,362 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 完全背包理论基础-二维DP数组 + +本题力扣上没有原题,大家可以去[卡码网第52题](https://kamacoder.com/problempage.php?pid=1052)去练习,题意是一样的。 + +## 完全背包 + +有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品都有无限个(也就是可以放入背包多次)**,求解将哪些物品装入背包里物品价值总和最大。 + +**完全背包和01背包问题唯一不同的地方就是,每种物品有无限件**。 + +同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。 + +在下面的讲解中,我拿下面数据举例子: + +背包最大重量为4,物品为: + +| | 重量 | 价值 | +| ----- | ---- | ---- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +**每件商品都有无限个!** + +问背包能背的物品最大价值是多少? + +**如果没看到之前的01背包讲解,已经要先仔细看如下两篇,01背包是基础,本篇在讲解完全背包,之前的背包基础我将不会重复讲解**。 + +* [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) +* [01背包理论基础(一维数组)](https://programmercarl.com/背包理论基础01背包-2.html) + +动规五部曲分析完全背包,为了从原理上讲清楚,我们先从二维dp数组分析: + +### 1. 确定dp数组以及下标的含义 + +**dp[i][j] 表示从下标为[0-i]的物品,每个物品可以取无限次,放进容量为j的背包,价值总和最大是多少**。 + +很多录友也会疑惑,凭什么上来就定义 dp数组,思考过程是什么样的, 这个思考过程我在 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 “确定dp数组以及下标的含义” 有详细讲解。 + + +### 2. 确定递推公式 + +这里在把基本信息给出来: + +| | 重量 | 价值 | +| ----- | ---- | ---- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +对于递推公式,首先我们要明确有哪些方向可以推导出 dp[i][j]。 + +这里依然拿dp[1][4]的状态来举例: ([01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中也是这个例子,要注意下面的不同之处) + +求取 dp[1][4] 有两种情况: + +1. 放物品1 +2. 还是不放物品1 + +如果不放物品1, 那么背包的价值应该是 dp[0][4] 即 容量为4的背包,只放物品0的情况。 + +推导方向如图: + +![](https://file1.kamacoder.com/i/algo/20241126112952.png) + +如果放物品1, **那么背包要先留出物品1的容量**,目前容量是4,物品1 的容量(就是物品1的重量)为3,此时背包剩下容量为1。 + +容量为1,只考虑放物品0 和物品1 的最大价值是 dp[1][1], **注意 这里和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 有所不同了**! + +在 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中,背包先空留出物品1的容量,此时容量为1,只考虑放物品0的最大价值是 dp[0][1],**因为01背包每个物品只有一个,既然空出物品1,那背包中也不会再有物品1**! + +而在完全背包中,物品是可以放无限个,所以 即使空出物品1空间重量,那背包中也可能还有物品1,所以此时我们依然考虑放 物品0 和 物品1 的最大价值即: **dp[1][1], 而不是 dp[0][1]** + +所以 放物品1 的情况 = dp[1][1] + 物品1 的价值,推导方向如图: + +![](https://file1.kamacoder.com/i/algo/20241126113104.png) + + +(**注意上图和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的区别**,对于理解完全背包很重要) + +两种情况,分别是放物品1 和 不放物品1,我们要取最大值(毕竟求的是最大价值) + +`dp[1][4] = max(dp[0][4], dp[1][1] + 物品1 的价值) ` + +以上过程,抽象化如下: + +* **不放物品i**:背包容量为j,里面不放物品i的最大价值是dp[i - 1][j]。 + +* **放物品i**:背包空出物品i的容量后,背包容量为j - weight[i],dp[i][j - weight[i]] 为背包容量为j - weight[i]且不放物品i的最大价值,那么dp[i][j - weight[i]] + value[i] (物品i的价值),就是背包放物品i得到的最大价值 + +递推公式: `dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);` + +(注意,完全背包二维dp数组 和 01背包二维dp数组 递推公式的区别,01背包中是 `dp[i - 1][j - weight[i]] + value[i])`) + +### 3. dp数组如何初始化 + +**关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱**。 + +首先从dp[i][j]的定义出发,如果背包容量j为0的话,即dp[i][0],无论是选取哪些物品,背包价值总和一定为0。如图: + +![动态规划-背包问题2](https://file1.kamacoder.com/i/algo/2021011010304192.png) + +在看其他情况。 + +状态转移方程 `dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]);` 可以看出有一个方向 i 是由 i-1 推导出来,那么i为0的时候就一定要初始化。 + +dp[0][j],即:存放编号0的物品的时候,各个容量的背包所能存放的最大价值。 + +那么很明显当 `j < weight[0]`的时候,dp[0][j] 应该是 0,因为背包容量比编号0的物品重量还小。 + +当`j >= weight[0]`时,**dp[0][j] 如果能放下weight[0]的话,就一直装,每一种物品有无限个**。 + +代码初始化如下: + +```CPP +for (int i = 1; i < weight.size(); i++) { // 当然这一步,如果把dp数组预先初始化为0了,这一步就可以省略,但很多同学应该没有想清楚这一点。 + dp[i][0] = 0; +} + +// 正序遍历,如果能放下就一直装物品0 +for (int j = weight[0]; j <= bagWeight; j++) + dp[0][j] = dp[0][j - weight[0]] + value[0]; +``` + +(注意上面初始化和 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html)的区别在于物品有无限个) + + +此时dp数组初始化情况如图所示: + +![](https://file1.kamacoder.com/i/algo/20241114161608.png) + +dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢? + +其实从递归公式: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); 可以看出dp[i][j] 是由上方和左方数值推导出来了,那么 其他下标初始为什么数值都可以,因为都会被覆盖。 + +但只不过一开始就统一把dp数组统一初始为0,更方便一些。 + +最后初始化代码如下: + +```CPP +// 初始化 dp +vector> dp(weight.size(), vector(bagweight + 1, 0)); +for (int j = weight[0]; j <= bagWeight; j++) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; +} +``` + + +### 4. 确定遍历顺序 + +[01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中我们讲过,01背包二维DP数组,先遍历物品还是先遍历背包都是可以的。 + +因为两种遍历顺序,对于二维dp数组来说,递推公式所需要的值,二维dp数组里对应的位置都有。 + +详细可以看 [01背包理论基础(二维数组)](https://programmercarl.com/背包理论基础01背包-1.html) 中的 【遍历顺序】的讲解 + +所以既可以 先遍历物品再遍历背包: + +```CPP +for (int i = 1; i < n; i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } +} +``` + +也可以 先遍历背包再遍历物品: + +```CPP +for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + for (int i = 1; i < n; i++) { // 遍历物品 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } +} +``` + +### 5. 举例推导dp数组 + +以本篇举例数据为例,填满了dp二维数组如图: + +![](https://file1.kamacoder.com/i/algo/20241126113752.png) + +因为 物品0 的性价比是最高的,而且 在完全背包中,每一类物品都有无限个,所以有无限个物品0,既然物品0 性价比最高,当然是优先放物品0。 + + +### 本题代码: + + +```CPP +#include +#include +using namespace std; + +int main() { + int n, bagWeight; + int w, v; + cin >> n >> bagWeight; + vector weight(n); + vector value(n); + for (int i = 0; i < n; i++) { + cin >> weight[i] >> value[i]; + } + + vector> dp(n, vector(bagWeight + 1, 0)); + + // 初始化 + for (int j = weight[0]; j <= bagWeight; j++) + dp[0][j] = dp[0][j - weight[0]] + value[0]; + + for (int i = 1; i < n; i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j < weight[i]) dp[i][j] = dp[i - 1][j]; + else dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } + } + + cout << dp[n - 1][bagWeight] << endl; + + return 0; +} + +``` + +关于一维dp数组,大家看这里:[完全背包一维dp数组讲解](./背包问题完全背包一维.md) + +## 其他语言版本 + +### Java + +```Java +import java.util.Scanner; + +public class Main { + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + int n = scanner.nextInt(); + int bagWeight = scanner.nextInt(); + + int[] weight = new int[n]; + int[] value = new int[n]; + + for (int i = 0; i < n; i++) { + weight[i] = scanner.nextInt(); + value[i] = scanner.nextInt(); + } + + int[][] dp = new int[n][bagWeight + 1]; + + // 初始化 + for (int j = weight[0]; j <= bagWeight; j++) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; + } + + // 动态规划 + for (int i = 1; i < n; i++) { + for (int j = 0; j <= bagWeight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } + } + } + + System.out.println(dp[n - 1][bagWeight]); + scanner.close(); + } +} + +``` + +### Go + +### Python + +```python +def knapsack(n, bag_weight, weight, value): + dp = [[0] * (bag_weight + 1) for _ in range(n)] + + # 初始化 + for j in range(weight[0], bag_weight + 1): + dp[0][j] = dp[0][j - weight[0]] + value[0] + + # 动态规划 + for i in range(1, n): + for j in range(bag_weight + 1): + if j < weight[i]: + dp[i][j] = dp[i - 1][j] + else: + dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]) + + return dp[n - 1][bag_weight] + +# 输入 +n, bag_weight = map(int, input().split()) +weight = [] +value = [] +for _ in range(n): + w, v = map(int, input().split()) + weight.append(w) + value.append(v) + +# 输出结果 +print(knapsack(n, bag_weight, weight, value)) + +``` + +### JavaScript + +```js +const readline = require('readline').createInterface({ + input: process.stdin, + output: process.stdout +}); + +let input = []; +readline.on('line', (line) => { + input.push(line.trim()); +}); + +readline.on('close', () => { + // 第一行解析 n 和 v + const [n, bagweight] = input[0].split(' ').map(Number); + + /// 剩余 n 行解析重量和价值 + const weight = []; + const value = []; + for (let i = 1; i <= n; i++) { + const [wi, vi] = input[i].split(' ').map(Number); + weight.push(wi); + value.push(vi); + } + + + let dp = Array.from({ length: n }, () => Array(bagweight + 1).fill(0)); + + for (let j = weight[0]; j <= bagweight; j++) { + dp[0][j] = dp[0][j-weight[0]] + value[0]; + } + + for (let i = 1; i < n; i++) { + for (let j = 0; j <= bagweight; j++) { + if (j < weight[i]) { + dp[i][j] = dp[i - 1][j]; + } else { + dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - weight[i]] + value[i]); + } + } + } + + console.log(dp[n - 1][bagweight]); +}); + +``` + diff --git "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index 87bd1bfd82..7aff85764e --- "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\346\200\273\347\273\223\347\257\207.md" @@ -1,14 +1,9 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + +# 贪心算法总结篇 + 我刚刚开始讲解贪心系列的时候就说了,贪心系列并不打算严格的从简单到困难这么个顺序来讲解。 @@ -20,7 +15,7 @@ 在刚刚讲过的回溯系列中,大家可以发现我是严格按照框架难度顺序循序渐进讲解的,**和贪心又不一样,因为回溯法如果题目顺序没选好,刷题效果会非常差!** -同样回溯系列也不允许简单困难交替着来,因为前后题目都是有因果关系的,**相信跟着刷过回溯系列的录友们都会明白我的良苦用心,哈哈**。 +同样回溯系列也不允许简单困难交替着来,因为前后题目都是有因果关系的,**相信跟着刷过回溯系列的录友们都会明白我的良苦用心**。 **每个系列都有每个系列的特点,我都会根据特点有所调整,大家看我每天的推送的题目,都不是随便找一个到就推送的,都是先有整体规划,然后反复斟酌具体题目的结果**。 @@ -30,7 +25,7 @@ ## 贪心理论基础 -在贪心系列开篇词[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中,我们就讲解了大家对贪心的普遍疑惑。 +在贪心系列开篇词[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html)中,我们就讲解了大家对贪心的普遍疑惑。 1. 贪心很简单,就是常识? @@ -52,42 +47,42 @@ Carl个人认为:如果找出局部最优并可以推出全局最优,就是 就像是 要用一下 1 + 1 = 2,没有必要再证明一下 1 + 1 究竟为什么等于 2。(例子极端了点,但是这个道理) -相信大家读完[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg),就对贪心有了一个基本的认识了。 +相信大家读完[关于贪心算法,你该了解这些!](https://programmercarl.com/贪心算法理论基础.html),就对贪心有了一个基本的认识了。 ## 贪心简单题 以下三道题目就是简单题,大家会发现贪心感觉就是常识。是的,如下三道题目,就是靠常识,但我都具体分析了局部最优是什么,全局最优是什么,贪心也要贪的有理有据! -* [贪心算法:分发饼干](https://mp.weixin.qq.com/s/YSuLIAYyRGlyxbp9BNC1uw) -* [贪心算法:K次取反后最大化的数组和](https://mp.weixin.qq.com/s/dMTzBBVllRm_Z0aaWvYazA) -* [贪心算法:柠檬水找零](https://mp.weixin.qq.com/s/0kT4P-hzY7H6Ae0kjQqnZg) +* [贪心算法:分发饼干](https://programmercarl.com/0455.分发饼干.html) +* [贪心算法:K次取反后最大化的数组和](https://programmercarl.com/1005.K次取反后最大化的数组和.html) +* [贪心算法:柠檬水找零](https://programmercarl.com/0860.柠檬水找零.html) ## 贪心中等题 贪心中等题,靠常识可能就有点想不出来了。开始初现贪心算法的难度与巧妙之处。 -* [贪心算法:摆动序列](https://mp.weixin.qq.com/s/Xytl05kX8LZZ1iWWqjMoHA) -* [贪心算法:单调递增的数字](https://mp.weixin.qq.com/s/TAKO9qPYiv6KdMlqNq_ncg) +* [贪心算法:摆动序列](https://programmercarl.com/0376.摆动序列.html) +* [贪心算法:单调递增的数字](https://programmercarl.com/0738.单调递增的数字.html) ### 贪心解决股票问题 大家都知道股票系列问题是动规的专长,其实用贪心也可以解决,而且还不止就这两道题目,但这两道比较典型,我就拿来单独说一说 -* [贪心算法:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg) -* [贪心算法:买卖股票的最佳时机含手续费](https://mp.weixin.qq.com/s/olWrUuDEYw2Jx5rMeG7XAg) +* [贪心算法:买卖股票的最佳时机II](https://programmercarl.com/0122.买卖股票的最佳时机II.html) +* [贪心算法:买卖股票的最佳时机含手续费](https://programmercarl.com/0714.买卖股票的最佳时机含手续费.html) 本题使用贪心算法比较绕,建议后面学习动态规划章节的时候,理解动规就好 ### 两个维度权衡问题 在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。 -* [贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ) -* [贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw) +* [贪心算法:分发糖果](https://programmercarl.com/0135.分发糖果.html) +* [贪心算法:根据身高重建队列](https://programmercarl.com/0406.根据身高重建队列.html) 在讲解本题的过程中,还强调了编程语言的重要性,模拟插队的时候,使用C++中的list(链表)替代了vector(动态数组),效率会高很多。 -所以在[贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ)详细讲解了,为什么用list(链表)更快! +所以在[贪心算法:根据身高重建队列(续集)](https://programmercarl.com/根据身高重建队列(vector原理讲解).html)详细讲解了,为什么用list(链表)更快! **大家也要掌握自己所用的编程语言,理解其内部实现机制,这样才能写出高效的算法!** @@ -99,38 +94,44 @@ Carl个人认为:如果找出局部最优并可以推出全局最优,就是 关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。 -* [贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA) -* [贪心算法:跳跃游戏II](https://mp.weixin.qq.com/s/kJBcsJ46DKCSjT19pxrNYg) -* [贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw) -* [贪心算法:无重叠区间](https://mp.weixin.qq.com/s/oFOEoW-13Bm4mik-aqAOmw) -* [贪心算法:划分字母区间](https://mp.weixin.qq.com/s/pdX4JwV1AOpc_m90EcO2Hw) -* [贪心算法:合并区间](https://mp.weixin.qq.com/s/royhzEM5tOkUFwUGrNStpw) +* [贪心算法:跳跃游戏](https://programmercarl.com/0055.跳跃游戏.html) +* [贪心算法:跳跃游戏II](https://programmercarl.com/0045.跳跃游戏II.html) +* [贪心算法:用最少数量的箭引爆气球](https://programmercarl.com/0452.用最少数量的箭引爆气球.html) +* [贪心算法:无重叠区间](https://programmercarl.com/0435.无重叠区间.html) +* [贪心算法:划分字母区间](https://programmercarl.com/0763.划分字母区间.html) +* [贪心算法:合并区间](https://programmercarl.com/0056.合并区间.html) ### 其他难题 -[贪心算法:最大子序和](https://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg) 其实是动态规划的题目,但贪心性能更优,很多同学也是第一次发现贪心能比动规更优的题目。 +[贪心算法:最大子序和](https://programmercarl.com/0053.最大子序和.html) 其实是动态规划的题目,但贪心性能更优,很多同学也是第一次发现贪心能比动规更优的题目。 +[贪心算法:加油站](https://programmercarl.com/0134.加油站.html)可能以为是一道模拟题,但就算模拟其实也不简单,需要把while用的很娴熟。但其实是可以使用贪心给时间复杂度降低一个数量级。 -[贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw)可能以为是一道模拟题,但就算模拟其实也不简单,需要把while用的很娴熟。但其实是可以使用贪心给时间复杂度降低一个数量级。 - -最后贪心系列压轴题目[贪心算法:我要监控二叉树!](https://mp.weixin.qq.com/s/kCxlLLjWKaE6nifHC3UL2Q),不仅贪心的思路不好想,而且需要对二叉树的操作特别娴熟,这就是典型的交叉类难题了。 +最后贪心系列压轴题目[贪心算法:我要监控二叉树!](https://programmercarl.com/0968.监控二叉树.html),不仅贪心的思路不好想,而且需要对二叉树的操作特别娴熟,这就是典型的交叉类难题了。 ## 贪心每周总结 周总结里会对每周的题目中大家的疑问、相关难点或者笔误之类的进行复盘和总结。 -如果大家发现文章哪里有问题,那么在周总结里或者文章评论区一定进行了修正,保证不会因为我的笔误或者理解问题而误导大家,哈哈。 +如果大家发现文章哪里有问题,那么在周总结里或者文章评论区一定进行了修正,保证不会因为我的笔误或者理解问题而误导大家。 所以周总结一定要看! -* [本周小结!(贪心算法系列一)](https://mp.weixin.qq.com/s/KQ2caT9GoVXgB1t2ExPncQ) -* [本周小结!(贪心算法系列二)](https://mp.weixin.qq.com/s/RiQri-4rP9abFmq_mlXNiQ) -* [本周小结!(贪心算法系列三)](https://mp.weixin.qq.com/s/JfeuK6KgmifscXdpEyIm-g) -* [本周小结!(贪心算法系列四)](https://mp.weixin.qq.com/s/zAMHT6JfB19ZSJNP713CAQ) +* [本周小结!(贪心算法系列一)](https://programmercarl.com/周总结/20201126贪心周末总结.html) +* [本周小结!(贪心算法系列二)](https://programmercarl.com/周总结/20201203贪心周末总结.html) +* [本周小结!(贪心算法系列三)](https://programmercarl.com/周总结/20201217贪心周末总结.html) +* [本周小结!(贪心算法系列四)](https://programmercarl.com/周总结/20201224贪心周末总结.html) ## 总结 + +贪心专题汇聚为一张图: + +![](https://file1.kamacoder.com/i/algo/贪心总结water.png) + +这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412)所画,总结的非常好,分享给大家。 + 很多没有接触过贪心的同学都会感觉贪心有啥可学的,但只要跟着「代码随想录」坚持下来之后,就会发现,贪心是一种很重要的算法思维而且并不简单,贪心往往妙的出其不意,触不及防! **回想一下我们刚刚开始讲解贪心的时候,大家会发现自己在坚持中进步了很多!** @@ -145,13 +146,6 @@ Carl个人认为:如果找出局部最优并可以推出全局最优,就是 **一个系列的结束,又是一个新系列的开始,我们将在明年第一个工作日正式开始动态规划,来不及解释了,录友们上车别掉队,我们又要开始新的征程!** -> **相信很多小伙伴刷题的时候面对力扣上近两千道题目,感觉无从下手,我花费半年时间整理了Github项目:「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解,常用算法模板总结,以及难点视频讲解,按照list一道一道刷就可以了!star支持一波吧!** - -* 公众号:[代码随想录](https://img-blog.csdnimg.cn/20210210152223466.png) -* B站:[代码随想录](https://space.bilibili.com/525438321) -* Github:[leetcode-master](https://github.com/youngyangyang04/leetcode-master) -* 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64) -![](https://img-blog.csdnimg.cn/20210205113044152.png) diff --git "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index 590850efb5..3bcf307525 --- "a/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\350\264\252\345\277\203\347\256\227\346\263\225\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,20 +1,17 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) -

- -

-

- - - - - - -

# 关于贪心算法,你该了解这些! -> 正式开始新的系列了,贪心算法! -通知:一些录友表示经常看不到每天的文章,现在公众号已经不按照发送时间推荐了,而是根据一些规则乱序推送,所以可能关注了「代码随想录」也一直看不到文章,建议把「代码随想录」设置星标哈,设置星标之后,每天就按发文时间推送了,我每天都是定时8:35发送的,嗷嗷准时,哈哈。 +题目分类大纲如下: + +贪心算法大纲 + +## 算法公开课 + +**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html):[贪心算法理论基础!](https://www.bilibili.com/video/BV1WK4y1R71x/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。 ## 什么是贪心 @@ -30,6 +27,7 @@ 再举一个例子如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。动态规划的问题在下一个系列会详细讲解。 + ## 贪心的套路(什么时候用贪心) 很多同学做贪心的题目的时候,想不出来是贪心,想知道有没有什么套路可以一看就看出来是贪心。 @@ -67,7 +65,7 @@ **那么刷题的时候什么时候真的需要数学推导呢?** -例如这道题目:[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA),这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。 +例如这道题目:[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html),这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。 ## 贪心一般解题步骤 @@ -78,7 +76,10 @@ * 求解每一个子问题的最优解 * 将局部最优解堆叠成全局最优解 -其实这个分的有点细了,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。 +这个四步其实过于理论化了,我们平时在做贪心类的题目时,如果按照这四步去思考,真是有点“鸡肋”。 + +做题的时候,只要想清楚 局部最优 是什么,如果推导出全局最优,其实就够了。 + ## 总结 @@ -88,15 +89,4 @@ 最后给出贪心的一般解题步骤,大家可以发现这个解题步骤也是比较抽象的,不像是二叉树,回溯算法,给出了那么具体的解题套路和模板。 -本篇没有配图,其实可以找一些动漫周边或者搞笑的图配一配(符合大多数公众号文章的作风),但这不是我的风格,所以本篇文字描述足以! - -> **相信很多小伙伴刷题的时候面对力扣上近两千道题目,感觉无从下手,我花费半年时间整理了Github项目:「力扣刷题攻略」[https://github.com/youngyangyang04/leetcode-master](https://github.com/youngyangyang04/leetcode-master)。 里面有100多道经典算法题目刷题顺序、配有40w字的详细图解,常用算法模板总结,以及难点视频讲解,按照list一道一道刷就可以了!star支持一波吧!** - -* 公众号:[代码随想录](https://img-blog.csdnimg.cn/20210210152223466.png) -* B站:[代码随想录](https://space.bilibili.com/525438321) -* Github:[leetcode-master](https://github.com/youngyangyang04/leetcode-master) -* 知乎:[代码随想录](https://www.zhihu.com/people/sun-xiu-yang-64) - -![](https://img-blog.csdnimg.cn/20210205113044152.png) - diff --git "a/problems/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" "b/problems/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" new file mode 100755 index 0000000000..98aa47a865 --- /dev/null +++ "b/problems/\351\200\222\345\275\222\347\256\227\346\263\225\347\232\204\346\227\266\351\227\264\344\270\216\347\251\272\351\227\264\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" @@ -0,0 +1,281 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](./other/kstar.md) +* [刷算法(两个月高强度学算法)](./xunlian/xunlianying.md) +* [背八股(40天挑战高频面试题)](./xunlian/bagu.md) + + + + + + + + + + + + + + +# 递归算法的时间与空间复杂度分析! + +之前在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://programmercarl.com/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.html)中详细讲解了递归算法的时间复杂度,但没有讲空间复杂度。 + +本篇讲通过求斐波那契数列和二分法再来深入分析一波递归算法的时间和空间复杂度,细心看完,会刷新对递归的认知! + + +## 递归求斐波那契数列的性能分析 + +先来看一下求斐波那契数的递归写法。 + +```CPP +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i-1) + fibonacci(i-2); +} +``` + +对于递归算法来说,代码一般都比较简短,从算法逻辑上看,所用的存储空间也非常少,但运行时需要内存可不见得会少。 + +### 时间复杂度分析 + +来看看这个求斐波那契的递归算法的时间复杂度是多少呢? + +在讲解递归时间复杂度的时候,我们提到了递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归的时间复杂度**。 + +可以看出上面的代码每次递归都是O(1)的操作。再来看递归了多少次,这里将i为5作为输入的递归过程 抽象成一棵递归树,如图: + + +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305093200104.png) + +从图中,可以看出f(5)是由f(4)和f(3)相加而来,那么f(4)是由f(3)和f(2)相加而来 以此类推。 + +在这棵二叉树中每一个节点都是一次递归,那么这棵树有多少个节点呢? + +我们之前也有说到,一棵深度(按根节点深度为1)为k的二叉树最多可以有 2^k - 1 个节点。 + +所以该递归算法的时间复杂度为O(2^n),这个复杂度是非常大的,随着n的增大,耗时是指数上升的。 + +来做一个实验,大家可以有一个直观的感受。 + +以下为C++代码,来测一下,让我们输入n的时候,这段递归求斐波那契代码的耗时。 + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i - 1) + fibonacci(i - 2); +} +void time_consumption() { + int n; + while (cin >> n) { + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + + fibonacci(n); + + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +int main() +{ + time_consumption(); + return 0; +} +``` + +根据以上代码,给出几组实验数据: + +测试电脑以2015版MacPro为例,CPU配置:`2.7 GHz Dual-Core Intel Core i5` + +测试数据如下: + +* n = 40,耗时:837 ms +* n = 50,耗时:110306 ms + +可以看出,O(2^n)这种指数级别的复杂度是非常大的。 + +所以这种求斐波那契数的算法看似简洁,其实时间复杂度非常高,一般不推荐这样来实现斐波那契。 + +其实罪魁祸首就是这里的两次递归,导致了时间复杂度以指数上升。 + +```CPP +return fibonacci(i-1) + fibonacci(i-2); +``` + +可不可以优化一下这个递归算法呢。 主要是减少递归的调用次数。 + +来看一下如下代码: + +```CPP +// 版本二 +int fibonacci(int first, int second, int n) { + if (n <= 0) { + return 0; + } + if (n < 3) { + return 1; + } + else if (n == 3) { + return first + second; + } + else { + return fibonacci(second, first + second, n - 1); + } +} +``` + +这里相当于用first和second来记录当前相加的两个数值,此时就不用两次递归了。 + +因为每次递归的时候n减1,即只是递归了n次,所以时间复杂度是 O(n)。 + +同理递归的深度依然是n,每次递归所需的空间也是常数,所以空间复杂度依然是O(n)。 + +代码(版本二)的复杂度如下: + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +此时再来测一下耗时情况验证一下: + +```CPP +#include +#include +#include +using namespace std; +using namespace chrono; +int fibonacci_3(int first, int second, int n) { + if (n <= 0) { + return 0; + } + if (n < 3) { + return 1; + } + else if (n == 3) { + return first + second; + } + else { + return fibonacci_3(second, first + second, n - 1); + } +} + +void time_consumption() { + int n; + while (cin >> n) { + milliseconds start_time = duration_cast( + system_clock::now().time_since_epoch() + ); + + fibonacci_3(1, 1, n); + + milliseconds end_time = duration_cast( + system_clock::now().time_since_epoch() + ); + cout << milliseconds(end_time).count() - milliseconds(start_time).count() + <<" ms"<< endl; + } +} +int main() +{ + time_consumption(); + return 0; +} + +``` + +测试数据如下: + +* n = 40,耗时:0 ms +* n = 50,耗时:0 ms + +大家此时应该可以看出差距了!! + +### 空间复杂度分析 + +说完了这段递归代码的时间复杂度,再看看如何求其空间复杂度呢,这里给大家提供一个公式:**递归算法的空间复杂度 = 每次递归的空间复杂度 * 递归深度** + +为什么要求递归的深度呢? + +因为每次递归所需的空间都被压到调用栈里(这是内存管理里面的数据结构,和算法里的栈原理是一样的),一次递归结束,这个栈就是就是把本次递归的数据弹出去。所以这个栈最大的长度就是递归的深度。 + +此时可以分析这段递归的空间复杂度,从代码中可以看出每次递归所需要的空间大小都是一样的,所以每次递归中需要的空间是一个常量,并不会随着n的变化而变化,每次递归的空间复杂度就是$O(1)$。 + +在看递归的深度是多少呢?如图所示: + + +![递归空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305094749554.png) + +递归第n个斐波那契数的话,递归调用栈的深度就是n。 + +那么每次递归的空间复杂度是O(1), 调用栈深度为n,所以这段递归代码的空间复杂度就是O(n)。 + +```CPP +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i-1) + fibonacci(i-2); +} +``` + + +最后对各种求斐波那契数列方法的性能做一下分析,如题: + + +![递归的空间复杂度分析](https://file1.kamacoder.com/i/algo/20210305095227356.png) + +可以看出,求斐波那契数的时候,使用递归算法并不一定是在性能上是最优的,但递归确实简化的代码层面的复杂度。 + +### 二分法(递归实现)的性能分析 + +带大家再分析一段二分查找的递归实现。 + +```CPP +int binary_search( int arr[], int l, int r, int x) { + if (r >= l) { + int mid = l + (r - l) / 2; + if (arr[mid] == x) + return mid; + if (arr[mid] > x) + return binary_search(arr, l, mid - 1, x); + return binary_search(arr, mid + 1, r, x); + } + return -1; +} +``` + +都知道二分查找的时间复杂度是O(logn),那么递归二分查找的空间复杂度是多少呢? + +我们依然看 **每次递归的空间复杂度和递归的深度** + +每次递归的空间复杂度可以看出主要就是参数里传入的这个arr数组,但需要注意的是在C/C++中函数传递数组参数,不是整个数组拷贝一份传入函数而是传入的数组首元素地址。 + +**也就是说每一层递归都是公用一块数组地址空间的**,所以 每次递归的空间复杂度是常数即:O(1)。 + +再来看递归的深度,二分查找的递归深度是logn ,递归深度就是调用栈的长度,那么这段代码的空间复杂度为 1 * logn = O(logn)。 + +大家要注意自己所用的语言在传递函数参数的时,是拷贝整个数值还是拷贝地址,如果是拷贝整个数值那么该二分法的空间复杂度就是O(nlogn)。 + + +## 总结 + +本章我们详细分析了递归实现的求斐波那契和二分法的空间复杂度,同时也对时间复杂度做了分析。 + +特别是两种递归实现的求斐波那契数列,其时间复杂度截然不容,我们还做了实验,验证了时间复杂度为O(2^n)是非常耗时的。 + +通过本篇大家应该对递归算法的时间复杂度和空间复杂度有更加深刻的理解了。 + + + + + + + diff --git "a/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" "b/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" old mode 100644 new mode 100755 index 8129ed2508..df1747e26a --- "a/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\351\223\276\350\241\250\346\200\273\347\273\223\347\257\207.md" @@ -1,19 +1,13 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) +# 链表总结篇 -# 链表的理论基础 -在这篇文章[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ)中,介绍了如下几点: +## 链表的理论基础 + +在这篇文章[关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html)中,介绍了如下几点: * 链表的种类主要为:单链表,双链表,循环链表 * 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。 @@ -22,21 +16,21 @@ **可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐**。 -# 链表经典题目 +## 链表经典题目 -## 虚拟头结点 +### 虚拟头结点 -在[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)中,我们讲解了链表操作中一个非常总要的技巧:虚拟头节点。 +在[链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html)中,我们讲解了链表操作中一个非常重要的技巧:虚拟头节点。 链表的一大问题就是操作当前节点必须要找前一个节点才能操作。这就造成了,头结点的尴尬,因为头结点没有前一个节点了。 **每次对应头结点的情况都要单独处理,所以使用虚拟头结点的技巧,就可以解决这个问题**。 -在[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)中,我给出了用虚拟头结点和没用虚拟头结点的代码,大家对比一下就会发现,使用虚拟头结点的好处。 +在[链表:听说用虚拟头节点会方便很多?](https://programmercarl.com/0203.移除链表元素.html)中,我给出了用虚拟头结点和没用虚拟头结点的代码,大家对比一下就会发现,使用虚拟头结点的好处。 -## 链表的基本操作 +### 链表的基本操作 -在[链表:一道题目考察了常见的五个操作!](https://mp.weixin.qq.com/s/Cf95Lc6brKL4g2j8YyF3Mg)中,我们通设计链表把链表常见的五个操作练习了一遍。 +在[链表:一道题目考察了常见的五个操作!](https://programmercarl.com/0707.设计链表.html)中,我们通过设计链表把链表常见的五个操作练习了一遍。 这是练习链表基础操作的非常好的一道题目,考察了: @@ -50,93 +44,55 @@ 这里我依然使用了虚拟头结点的技巧,大家复习的时候,可以去看一下代码。 -## 反转链表 +### 反转链表 -在[链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg)中,讲解了如何反转链表。 +在[链表:听说过两天反转链表又写不出来了?](https://programmercarl.com/0206.翻转链表.html)中,讲解了如何反转链表。 因为反转链表的代码相对简单,有的同学可能直接背下来了,但一写还是容易出问题。 反转链表是面试中高频题目,很考察面试者对链表操作的熟练程度。 -我在[文章](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg)中,给出了两种反转的方式,迭代法和递归法。 +我在[文章](https://programmercarl.com/0206.翻转链表.html)中,给出了两种反转的方式,迭代法和递归法。 建议大家先学透迭代法,然后再看递归法,因为递归法比较绕,如果迭代还写不明白,递归基本也写不明白了。 **可以先通过迭代法,彻底弄清楚链表反转的过程!** -## 环形链表 - -在[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)中,讲解了在链表如何找环,以及如何找环的入口位置。 - -这道题目可以说是链表的比较难的题目了。 - -很多同学关注的问题是:为什么一定会相遇,快指针就不能跳过慢指针么? - -可以确定如下两点: - -* fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。 -* fast和slow都进入环里之后,fast相对于slow来说,fast是一个节点一个节点的靠近slow的,**注意是相对运动,所以fast一定可以和slow重合**。 - -如果fast是一次走三个节点,那么可能会跳过slow,因为相对于slow来说,fast是两个节点移动的。 - -确定有否有环比较容易,但是找到环的入口就不太容易了,需要点数学推理。 - -我在[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)中给出了详细的推理,兼顾易懂和简洁了。 - -这是一位录友在评论区有一个疑问,感觉这个问题很不错,但评论区根本说不清楚,我就趁着总结篇,补充一下这个证明。 +### 删除倒数第N个节点 -在推理过程中,**为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?** +在[链表:删除链表倒数第N个节点,怎么删?](https://programmercarl.com/0019.删除链表的倒数第N个节点.html)中我们结合虚拟头结点 和 双指针法来移除链表倒数第N个节点。 -了解这个问题一定要先把文章[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)看了,即文章中如下的地方: - +### 链表相交 +[链表:链表相交](https://programmercarl.com/面试题02.07.链表相交.html)使用双指针来找到两个链表的交点(引用完全相同,即:内存地址完全相同的交点) -首先slow进环的时候,fast一定是先进环来了。 +### 环形链表 -如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子: +在[链表:环找到了,那入口呢?](https://programmercarl.com/0142.环形链表II.html)中,讲解了在链表如何找环,以及如何找环的入口位置。 - +这道题目可以说是链表的比较难的题目了。 但代码却十分简洁,主要在于一些数学证明。 -可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。 +## 总结 -重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图: +![](https://file1.kamacoder.com/i/algo/链表总结.png) - - -那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。 - -因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 - -**也就是说slow一定没有走到环入口3,而fast已经到环入口3了**。 - -这说明什么呢? - -**在slow开始走的那一环已经和fast相遇了**。 - -那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,**fast相对于slow是一次移动一个节点,所以不可能跳过去**。 - -好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)的补充。 - -这次可以说把环形链表这道题目的各个细节,完完整整的证明了一遍,说这是全网最详细讲解不为过吧,哈哈。 - -# 总结 +这个图是 [代码随想录知识星球](https://programmercarl.com/other/kstar.html) 成员:[海螺人](https://wx.zsxq.com/dweb2/index/footprint/844412858822412),所画,总结的非常好,分享给大家。 考察链表的操作其实就是考察指针的操作,是面试中的常见类型。 -链表篇中开头介绍[链表理论知识](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA),然后分别通过经典题目介绍了如下知识点: +链表篇中开头介绍[链表理论知识](https://programmercarl.com/0203.移除链表元素.html),然后分别通过经典题目介绍了如下知识点: + +1. [关于链表,你该了解这些!](https://programmercarl.com/链表理论基础.html) +2. [虚拟头结点的技巧](https://programmercarl.com/0203.移除链表元素.html) +3. [链表的增删改查](https://programmercarl.com/0707.设计链表.html) +4. [反转一个链表](https://programmercarl.com/0206.翻转链表.html) +5. [删除倒数第N个节点](https://programmercarl.com/0019.删除链表的倒数第N个节点.html) +6. [链表相交](https://programmercarl.com/面试题02.07.链表相交.html) +7. [有否环形,以及环的入口](https://programmercarl.com/0142.环形链表II.html) -* [虚拟头结点的技巧](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) -* [链表的增删改查](https://mp.weixin.qq.com/s/Cf95Lc6brKL4g2j8YyF3Mg) -* [反转一个链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) -* [有否环形,以及环的入口](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) -虽然这几篇文章都是几个月前发的了,但在在文章留言区,可以看到很多录友都在从头打卡! -如果希望从基础学起来的同学,也可以从头学起来,从头开始打卡,打卡的同时也总结自己的所学所思,一定进步飞快! -**在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,快去通关学习吧!** -![](https://img-blog.csdnimg.cn/20201030210901823.jpg) -**「代码随想录」这么用心的公众号,不分享给身边的同学朋友啥的,是不是可惜了? 哈哈** diff --git "a/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" old mode 100644 new mode 100755 index 63f0396cfd..c465818739 --- "a/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,54 +1,48 @@ -

- -

-

- - - - - - -

+* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + # 关于链表,你该了解这些! -什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点是又两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。 +什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点由两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。 -链接的入口点称为列表的头结点也就是head。 +链表的入口节点称为链表的头结点也就是head。 如图所示: -![链表1](https://img-blog.csdnimg.cn/20200806194529815.png) +![链表1](https://file1.kamacoder.com/i/algo/20200806194529815.png) -# 链表的类型 +## 链表的类型 接下来说一下链表的几种类型: -## 单链表 +### 单链表 刚刚说的就是单链表。 -## 双链表 +### 双链表 -单链表中的节点只能指向节点的下一个节点。 +单链表中的指针域只能指向节点的下一个节点。 双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。 双链表 既可以向前查询也可以向后查询。 如图所示: -![链表2](https://img-blog.csdnimg.cn/20200806194559317.png) +![链表2](https://file1.kamacoder.com/i/algo/20200806194559317.png) -## 循环链表 +### 循环链表 循环链表,顾名思义,就是链表首尾相连。 循环链表可以用来解决约瑟夫环问题。 -![链表4](https://img-blog.csdnimg.cn/20200806194629603.png) +![链表4](https://file1.kamacoder.com/i/algo/20200806194629603.png) -# 链表的存储方式 +## 链表的存储方式 了解完链表的类型,再来说一说链表在内存中的存储方式。 @@ -60,11 +54,11 @@ 如图所示: -![链表3](https://img-blog.csdnimg.cn/20200806194613920.png) +![链表3](https://file1.kamacoder.com/i/algo/20200806194613920.png) -这个链表起始节点为2, 终止节点为7, 各个节点分布在内存个不同地址空间上,通过指针串联在一起。 +这个链表起始节点为2, 终止节点为7, 各个节点分布在内存的不同地址空间上,通过指针串联在一起。 -# 链表的定义 +## 链表的定义 接下来说一说链表的定义。 @@ -76,7 +70,7 @@ 这里我给出C/C++的定义链表节点方式,如下所示: -``` +```cpp // 单链表 struct ListNode { int val; // 节点上存储的元素 @@ -87,30 +81,30 @@ struct ListNode { 有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。 -但是这个构造函数不会初始化任何成员变化,下面我来举两个例子: +但是这个构造函数不会初始化任何成员变量,下面我来举两个例子: 通过自己定义构造函数初始化节点: -``` +```cpp ListNode* head = new ListNode(5); ``` 使用默认构造函数初始化节点: -``` +```cpp ListNode* head = new ListNode(); head->val = 5; ``` 所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值! -# 链表的操作 +## 链表的操作 -## 删除节点 +### 删除节点 删除D节点,如图所示: -![链表-删除节点](https://img-blog.csdnimg.cn/20200806195114541.png) +![链表-删除节点](https://file1.kamacoder.com/i/algo/20200806195114541-20230310121459257.png) 只要将C节点的next指针 指向E节点就可以了。 @@ -120,21 +114,21 @@ head->val = 5; 其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。 -## 添加节点 +### 添加节点 如图所示: -![链表-添加节点](https://img-blog.csdnimg.cn/20200806195134331.png) +![链表-添加节点](https://file1.kamacoder.com/i/algo/20200806195134331-20230310121503147.png) 可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。 但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。 -# 性能分析 +## 性能分析 再把链表的特性和数组的特性进行一个对比,如图所示: -![链表-链表与数据性能对比](https://img-blog.csdnimg.cn/20200806195200276.png) +![链表-链表与数据性能对比](https://file1.kamacoder.com/i/algo/20200806195200276.png) 数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。 @@ -143,3 +137,139 @@ head->val = 5; 相信大家已经对链表足够的了解,后面我会讲解关于链表的高频面试题目,我们下期见! + + +## 其他语言版本 + +### Java: + +```java +public class ListNode { + // 结点的值 + int val; + + // 下一个结点 + ListNode next; + + // 节点的构造函数(无参) + public ListNode() { + } + + // 节点的构造函数(有一个参数) + public ListNode(int val) { + this.val = val; + } + + // 节点的构造函数(有两个参数) + public ListNode(int val, ListNode next) { + this.val = val; + this.next = next; + } +} +``` + +### JavaScript: + +```javascript +class ListNode { + val; + next = null; + constructor(value) { + this.val = value; + this.next = null; + } +} +``` + +### TypeScript: + +```typescript +class ListNode { + public val: number; + public next: ListNode|null = null; + constructor(value: number) { + this.val = value; + this.next = null; + } +} +``` + +### Python: + +```python +class ListNode: + def __init__(self, val, next=None): + self.val = val + self.next = next +``` + +### Go: + +```go +type ListNode struct { + Val int + Next *ListNode +} +``` + +### Scala: + +```scala +class ListNode(_x: Int = 0, _next: ListNode = null) { + var next: ListNode = _next + var x: Int = _x +} +``` + +### Rust: + +```rust +#[derive(PartialEq, Eq, Clone, Debug)] +pub struct ListNode { + pub val: T, + pub next: Option>>, +} + +impl ListNode { + #[inline] + fn new(val: T, node: Option>>) -> Self { + ListNode { next: node, val } + } +} +``` + + +### C: + +```c +typedef struct ListNodeT { + int val; + struct ListNodeT next; +} ListNode; +``` + +### C# + +```c# +public class Node +{ + // 节点存储的数据 + public T Data { get; set; } + + // 指向下一个节点的引用 + public Node Next { get; set; } + + // 节点的构造函数,用于初始化节点 + public Node(T data) + { + Data = data; + Next = null; // 初始时没有下一个节点,因此设为 null + } +} +``` + + + + + + diff --git "a/problems/\351\235\242\350\257\225\351\242\23002.07.\351\223\276\350\241\250\347\233\270\344\272\244.md" "b/problems/\351\235\242\350\257\225\351\242\23002.07.\351\223\276\350\241\250\347\233\270\344\272\244.md" new file mode 100755 index 0000000000..7e23172093 --- /dev/null +++ "b/problems/\351\235\242\350\257\225\351\242\23002.07.\351\223\276\350\241\250\347\233\270\344\272\244.md" @@ -0,0 +1,575 @@ +* [做项目(多个C++、Java、Go、测开、前端项目)](https://www.programmercarl.com/other/kstar.html) +* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html) +* [背八股(40天挑战高频面试题)](https://www.programmercarl.com/xunlian/bagu.html) + + +# 面试题 02.07. 链表相交 + +同:160.链表相交 + +[力扣题目链接](https://leetcode.cn/problems/intersection-of-two-linked-lists-lcci/) + +给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。 + +图示两个链表在节点 c1 开始相交: + +![](https://file1.kamacoder.com/i/algo/20211219221657.png) + +题目数据 保证 整个链式结构中不存在环。 + +注意,函数返回结果后,链表必须 保持其原始结构 。 + +示例 1: + +![](https://file1.kamacoder.com/i/algo/20211219221723.png) + +示例 2: + +![](https://file1.kamacoder.com/i/algo/20211219221749.png) + +示例 3: + +![](https://file1.kamacoder.com/i/algo/20211219221812.png) + + + +## 思路 + + +简单来说,就是求两个链表交点节点的**指针**。 这里同学们要注意,交点不是数值相等,而是指针相等。 + +为了方便举例,假设节点元素数值相等,则节点指针相等。 + +看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点: + +![面试题02.07.链表相交_1](https://file1.kamacoder.com/i/algo/面试题02.07.链表相交_1.png) + +我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图: + +![面试题02.07.链表相交_2](https://file1.kamacoder.com/i/algo/面试题02.07.链表相交_2.png) + +此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到交点。 + +否则循环退出返回空指针。 + +C++代码如下: + +```CPP +class Solution { +public: + ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { + ListNode* curA = headA; + ListNode* curB = headB; + int lenA = 0, lenB = 0; + while (curA != NULL) { // 求链表A的长度 + lenA++; + curA = curA->next; + } + while (curB != NULL) { // 求链表B的长度 + lenB++; + curB = curB->next; + } + curA = headA; + curB = headB; + // 让curA为最长链表的头,lenA为其长度 + if (lenB > lenA) { + swap (lenA, lenB); + swap (curA, curB); + } + // 求长度差 + int gap = lenA - lenB; + // 让curA和curB在同一起点上(末尾位置对齐) + while (gap--) { + curA = curA->next; + } + // 遍历curA 和 curB,遇到相同则直接返回 + while (curA != NULL) { + if (curA == curB) { + return curA; + } + curA = curA->next; + curB = curB->next; + } + return NULL; + } +}; +``` + +* 时间复杂度:O(n + m) +* 空间复杂度:O(1) + +## 其他语言版本 + +### Java: + +```Java +(版本一)先行移动长链表实现同步移动 +public class Solution { + public ListNode getIntersectionNode(ListNode headA, ListNode headB) { + ListNode curA = headA; + ListNode curB = headB; + int lenA = 0, lenB = 0; + while (curA != null) { // 求链表A的长度 + lenA++; + curA = curA.next; + } + while (curB != null) { // 求链表B的长度 + lenB++; + curB = curB.next; + } + curA = headA; + curB = headB; + // 让curA为最长链表的头,lenA为其长度 + if (lenB > lenA) { + //1. swap (lenA, lenB); + int tmpLen = lenA; + lenA = lenB; + lenB = tmpLen; + //2. swap (curA, curB); + ListNode tmpNode = curA; + curA = curB; + curB = tmpNode; + } + // 求长度差 + int gap = lenA - lenB; + // 让curA和curB在同一起点上(末尾位置对齐) + while (gap-- > 0) { + curA = curA.next; + } + // 遍历curA 和 curB,遇到相同则直接返回 + while (curA != null) { + if (curA == curB) { + return curA; + } + curA = curA.next; + curB = curB.next; + } + return null; + } + +} + +(版本二) 合并链表实现同步移动 +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; + } +} +``` + +### Python: + +```python + +(版本一)求长度,同时出发 + +class Solution: + def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: + lenA, lenB = 0, 0 + cur = headA + while cur: # 求链表A的长度 + cur = cur.next + lenA += 1 + cur = headB + while cur: # 求链表B的长度 + cur = cur.next + lenB += 1 + curA, curB = headA, headB + if lenA > lenB: # 让curB为最长链表的头,lenB为其长度 + curA, curB = curB, curA + lenA, lenB = lenB, lenA + for _ in range(lenB - lenA): # 让curA和curB在同一起点上(末尾位置对齐) + curB = curB.next + while curA: # 遍历curA 和 curB,遇到相同则直接返回 + if curA == curB: + return curA + else: + curA = curA.next + curB = curB.next + return None +``` +```python +(版本二)求长度,同时出发 (代码复用) +class Solution: + def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: + lenA = self.getLength(headA) + lenB = self.getLength(headB) + + # 通过移动较长的链表,使两链表长度相等 + if lenA > lenB: + headA = self.moveForward(headA, lenA - lenB) + else: + headB = self.moveForward(headB, lenB - lenA) + + # 将两个头向前移动,直到它们相交 + while headA and headB: + if headA == headB: + return headA + headA = headA.next + headB = headB.next + + return None + + def getLength(self, head: ListNode) -> int: + length = 0 + while head: + length += 1 + head = head.next + return length + + def moveForward(self, head: ListNode, steps: int) -> ListNode: + while steps > 0: + head = head.next + steps -= 1 + return head +``` +```python +(版本三)求长度,同时出发 (代码复用 + 精简) +class Solution: + def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: + dis = self.getLength(headA) - self.getLength(headB) + + # 通过移动较长的链表,使两链表长度相等 + if dis > 0: + headA = self.moveForward(headA, dis) + else: + headB = self.moveForward(headB, abs(dis)) + + # 将两个头向前移动,直到它们相交 + while headA and headB: + if headA == headB: + return headA + headA = headA.next + headB = headB.next + + return None + + def getLength(self, head: ListNode) -> int: + length = 0 + while head: + length += 1 + head = head.next + return length + + def moveForward(self, head: ListNode, steps: int) -> ListNode: + while steps > 0: + head = head.next + steps -= 1 + return head +``` +```python +(版本四)等比例法 +# Definition for singly-linked list. +# class ListNode: +# def __init__(self, x): +# self.val = x +# self.next = None + + +class Solution: + def getIntersectionNode(self, headA: ListNode, headB: ListNode) -> ListNode: + # 处理边缘情况 + if not headA or not headB: + return None + + # 在每个链表的头部初始化两个指针 + pointerA = headA + pointerB = headB + + # 遍历两个链表直到指针相交 + while pointerA != pointerB: + # 将指针向前移动一个节点 + pointerA = pointerA.next if pointerA else headB + pointerB = pointerB.next if pointerB else headA + + # 如果相交,指针将位于交点节点,如果没有交点,值为None + return pointerA +``` + +### Go: + +```go +func getIntersectionNode(headA, headB *ListNode) *ListNode { + curA := headA + curB := headB + lenA, lenB := 0, 0 + // 求A,B的长度 + for curA != nil { + curA = curA.Next + lenA++ + } + for curB != nil { + curB = curB.Next + lenB++ + } + var step int + var fast, slow *ListNode + // 请求长度差,并且让更长的链表先走相差的长度 + if lenA > lenB { + step = lenA - lenB + fast, slow = headA, headB + } else { + step = lenB - lenA + fast, slow = headB, headA + } + for i:=0; i < step; i++ { + fast = fast.Next + } + // 遍历两个链表遇到相同则跳出遍历 + for fast != slow { + fast = fast.Next + slow = slow.Next + } + return fast +} +``` + +双指针 + +```go +func getIntersectionNode(headA, headB *ListNode) *ListNode { + l1,l2 := headA, headB + for l1 != l2 { + if l1 != nil { + l1 = l1.Next + } else { + l1 = headB + } + + if l2 != nil { + l2 = l2.Next + } else { + l2 = headA + } + } + + return l1 +} +``` + +### JavaScript: + +```js +var getListLen = function(head) { + let len = 0, cur = head; + while(cur) { + len++; + cur = cur.next; + } + return len; +} +var getIntersectionNode = function(headA, headB) { + let curA = headA,curB = headB, + lenA = getListLen(headA), // 求链表A的长度 + lenB = getListLen(headB); + if(lenA < lenB) { // 让curA为最长链表的头,lenA为其长度 + + // 交换变量注意加 “分号” ,两个数组交换变量在同一个作用域下时 + // 如果不加分号,下面两条代码等同于一条代码: [curA, curB] = [lenB, lenA] + + [curA, curB] = [curB, curA]; + [lenA, lenB] = [lenB, lenA]; + } + let i = lenA - lenB; // 求长度差 + while(i-- > 0) { // 让curA和curB在同一起点上(末尾位置对齐) + curA = curA.next; + } + while(curA && curA !== curB) { // 遍历curA 和 curB,遇到相同则直接返回 + curA = curA.next; + curB = curB.next; + } + return curA; +}; +``` + +### TypeScript: + +```typescript +function getIntersectionNode(headA: ListNode | null, headB: ListNode | null): ListNode | null { + let sizeA: number = 0, + sizeB: number = 0; + let curA: ListNode | null = headA, + curB: ListNode | null = headB; + while (curA) { + sizeA++; + curA = curA.next; + } + while (curB) { + sizeB++; + curB = curB.next; + } + curA = headA; + curB = headB; + if (sizeA < sizeB) { + [sizeA, sizeB] = [sizeB, sizeA]; + [curA, curB] = [curB, curA]; + } + let gap = sizeA - sizeB; + while (gap-- && curA) { + curA = curA.next; + } + while (curA && curB) { + if (curA === curB) { + return curA; + } + curA = curA.next; + curB = curB.next; + } + return null; +}; +``` + +### C: + +```c +ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) { + ListNode *l = NULL, *s = NULL; + int lenA = 0, lenB = 0, gap = 0; + // 求出两个链表的长度 + s = headA; + while (s) { + lenA ++; + s = s->next; + } + s = headB; + while (s) { + lenB ++; + s = s->next; + } + + // 求出两个链表长度差 + if (lenA > lenB) { + l = headA, s = headB; + gap = lenA - lenB; + } else { + l = headB, s = headA; + gap = lenB - lenA; + } + + // 尾部对齐 + while (gap--) l = l->next; + // 移动,并检查是否有相同的元素 + while (l) { + if (l == s) return l; + l = l->next, s = s->next; + } + + return NULL; +} +``` + +### Scala: + +```scala +object Solution { + def getIntersectionNode(headA: ListNode, headB: ListNode): ListNode = { + var lenA = 0 // headA链表的长度 + var lenB = 0 // headB链表的长度 + var tmp = headA // 临时变量 + // 统计headA的长度 + while (tmp != null) { + lenA += 1; + tmp = tmp.next + } + // 统计headB的长度 + tmp = headB // 临时变量赋值给headB + while (tmp != null) { + lenB += 1 + tmp = tmp.next + } + // 因为传递过来的参数是不可变量,所以需要重新定义 + var listA = headA + var listB = headB + // 两个链表的长度差 + // 如果gap>0,lenA>lenB,headA(listA)链表往前移动gap步 + // 如果gap<0,lenA 0) { + // 因为不可以i-=1,所以可以使用for + for (i <- 0 until gap) { + listA = listA.next // 链表headA(listA) 移动 + } + } else { + gap = math.abs(gap) // 此刻gap为负值,取绝对值 + for (i <- 0 until gap) { + listB = listB.next + } + } + // 现在两个链表同时往前走,如果相等则返回 + while (listA != null && listB != null) { + if (listA == listB) { + return listA + } + listA = listA.next + listB = listB.next + } + // 如果链表没有相交则返回null,return可以省略 + null + } +} +``` +### C# +```csharp +public ListNode GetIntersectionNode(ListNode headA, ListNode headB) +{ + if (headA == null || headB == null) return null; + ListNode cur1 = headA, cur2 = headB; + while (cur1 != cur2) + { + cur1 = cur1 == null ? headB : cur1.next; + cur2 = cur2 == null ? headA : cur2.next; + } + return cur1; +} +``` + +### Swift: +```swift +func getIntersectionNode(_ headA: ListNode?, _ headB: ListNode?) -> ListNode? { + var lenA = 0 + var lenB = 0 + var nodeA = headA + var nodeB = headB + // 计算链表A和链表B的长度 + while nodeA != nil { + lenA += 1 + nodeA = nodeA?.next + } + while nodeB != nil { + lenB += 1 + nodeB = nodeB?.next + } + // 重置指针 + nodeA = headA + nodeB = headB + // 如果链表A更长,让它先走lenA-lenB步 + if lenA > lenB { + for _ in 0..<(lenA - lenB) { + nodeA = nodeA?.next + } + } else if lenB > lenA { + // 如果链表B更长,让它先走lenB-lenA步 + for _ in 0..<(lenB - lenA) { + nodeB = nodeB?.next + } + } + // 同时遍历两个链表,寻找交点 + while nodeA !== nodeB { + nodeA = nodeA?.next + nodeB = nodeB?.next + } + return nodeA +} +``` + +