diff --git a/README.md b/README.md index 0b3a5a3f..df397457 100644 --- a/README.md +++ b/README.md @@ -1,429 +1,421 @@ -

- -

+## 一些闲话: + +> 1. **介绍**:本项目是一套完整的刷题计划,旨在帮助大家少走弯路,循序渐进学算法,[关注作者](#关于作者) +> 2. **PDF版本** : [「代码随想录」算法精讲 PDF 版本](https://mp.weixin.qq.com/s/RsdcQ9umo09R6cfnwXZlrQ) 。 +> 3. **学习社区** : 一起学习打卡/面试技巧/如何选择offer/大厂内推/职场规则/简历修改/技术分享/程序人生。欢迎加入[「代码随想录」学习社区](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ) 。 +> 4. **转载须知** :以下所有文章皆为我([程序员Carl](https://github.com/youngyangyang04))的原创。引用本项目文章请注明出处,发现恶意抄袭或搬运,会动用法律武器维护自己的权益。让我们一起维护一个良好的技术创作环境! +

- - + + + + +

+ + - -

-目录: -================= - -* [算法面试思维导图](#算法面试思维导图) -* [B站算法视频讲解](#B站算法视频讲解) -* [LeetCode 刷题攻略](#LeetCode-刷题攻略) -* [算法文章精选](#算法文章精选) -* [算法模板](#算法模板) -* [LeetCode 最强题解](#LeetCode-最强题解) -* [关于作者](#关于作者) - -# 算法面试思维导图 - -![算法面试知识大纲](https://img-blog.csdnimg.cn/20200729181420491.png) - -# B站算法视频讲解 - -* [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 刷题攻略 -> 不少同学和我反应LeetCode 刷题攻略这一栏没有了,首先感谢大家对本仓库的关注!因为我发现一些公众号抄袭我的Github,所以我把刷题攻略隐藏了,但是很多同学就看不到刷题顺序了,为了大家可以继续学习,我把算法精选文章的顺序整理了,和刷题攻略顺序一致的,**文章顺序即刷题顺序**,而且比刷题攻略更全!感谢大家的支持,**这个仓库我每天都会更新的**! +## 刷题攻略的背景 -刷题顺序:建议先从同一类型里题目开始刷起,同一类型里再从简单到中等到困难刷起,题型顺序建议:**数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论**。 +很多刚开始刷题的同学都有一个困惑:面对leetcode上近两千道题目,从何刷起。 -目前大家可以按照下面的「算法文章精选」顺序来刷,里面都是各个类型的经典题目而且题目顺序都是精心设计的,**初学者可以按照这个顺序来刷题**,算法老手可以按照这个list查缺补漏! +其实我之前在知乎上回答过这个问题,回答内容大概是按照如下类型来刷数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心->动态规划->图论->高级数据结构,再从简单刷起,做了几个类型题目之后,再慢慢做中等题目、困难题目。 -**同时这份刷题列表也在公众号[「代码随想录」](https://img-blog.csdnimg.cn/20201124161234338.png)左下角的「算法汇总」里,方便大家用手机查看**,用手机看的好处可以看到每篇文章下都有很多录友(代码随想录的朋友们)的留言,录友会总结每篇文章的重点,如果文章有一些笔误的话,留言区也会及时纠正,所以**刷一下文章留言区会对理解知识点非常有帮助**!而且公众号更新要比Github早2-3天。 +但我能设身处地的感受到:即使有这样一个整体规划,对于一位初学者甚至算法老手寻找合适自己的题目也是很困难,时间成本很高,而且题目还不一定就是经典题目。 -**赶紧去公众号[「代码随想录」](https://img-blog.csdnimg.cn/20201124161234338.png)里看看吧,你会发现相见恨晚!** +对于刷题,我们都是想用最短的时间把经典题目都做一篇,这样效率才是最高的! + +所以我整理了leetcode刷题攻略:一个超级详细的刷题顺序,**每道题目都是我精心筛选,都是经典题目高频面试题**,大家只要按照这个顺序刷就可以了,**你没看错,就是题目顺序都排好了,文章顺序就是刷题顺序!挨个刷就可以,不用自己再去题海里选题了!** + +而且每道题目我都写了的详细题解(图文并茂,难点配有视频),力扣上我的题解都是排在对应题目的首页,质量是有目共睹的。 + +**那么现在我把刷题顺序都整理出来,是为了帮助更多的学习算法的同学少走弯路!** + +如果你在刷leetcode,强烈建议先按照本攻略刷题顺序来刷,刷完了你会发现对整个知识体系有一个质的飞跃,不用在题海茫然的寻找方向。 + +
最新文章会首发在公众号「代码随想录」,扫码看看吧,你会发现相见恨晚!
+ +
+ +## 如何使用该刷题攻略 + +电脑端还看不到留言,大家可以在公众号[「代码随想录」](https://img-blog.csdnimg.cn/20201124161234338.png),左下角有「刷题攻略」,这是手机版刷题攻略,看完就会发现有很多录友(代码随想录的朋友们)在文章下留言打卡,这份刷题顺序和题解已经陪伴了上万录友了,同时也说明文章的质量是经过上万人的考验! + +欢迎每一位学习算法的小伙伴加入到这个学习阵营来! + +**目前已经更新了,数组-> 链表-> 哈希表->字符串->栈与队列->树->回溯->贪心,八个专题了,正在讲解动态规划!** + +在刷题指南中,每个专题开始都有理论基础篇,并不像是教科书般的理论介绍,而是从实战中归纳需要的基础知识。每个专题结束都有总结篇,最这个专题的归纳总结。 + +如果你是算法老手,这篇攻略也是复习的最佳资料,如果把每个系列对应的总结篇,快速过一遍,整个算法知识体系以及各种解法就重现脑海了。 + +在按照如下顺序刷题的过程中,每一道题解一定要看对应文章下面的留言(留言目前只能在手机端查看)。 + +如果你有疑问或者发现文章哪里有不对的地方,都可以在留言区都能找到答案,还有很多录友的总结非常赞,看完之后也很有收获。 + +目前「代码随想录」刷题指南更新了:**200多篇文章,精讲了200道经典算法题目,共60w字的详细图解,部分难点题目还搭配了20分钟左右的视频讲解**。 + +准备好了么,刷题攻略开始咯,go go go! + +--------------------------------------------- + +## 前序 + +* [「代码随想录」后序安排](https://mp.weixin.qq.com/s/4eeGJREy6E-v6D7cR_5A4g) +* [「代码随想录」学习社区](https://mp.weixin.qq.com/s/X1XCH-KevURi3LnakJsCkA) -# 算法文章精选 * 编程语言 * [C++面试&C++学习指南知识点整理](https://github.com/youngyangyang04/TechCPP) * 编程素养 - * [看了这么多代码,谈一谈代码风格!](https://mp.weixin.qq.com/s/UR9ztxz3AyL3qdHn_zMbqw) + * [看了这么多代码,谈一谈代码风格!](./problems/前序/代码风格.md) + * [力扣上的代码想在本地编译运行?](./problems/前序/力扣上的代码想在本地编译运行?.md) + * [什么是核心代码模式,什么又是ACM模式?](./problems/前序/什么是核心代码模式,什么又是ACM模式?.md) +* 工具 + * [一站式vim配置](https://github.com/youngyangyang04/PowerVim) + * [保姆级Git入门教程,万字详解](https://mp.weixin.qq.com/s/Q_O0ey4C9tryPZaZeJocbA) + * [程序员应该用什么用具来写文档?](./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/3VJHF2zNohBwDBxARFIn-Q) - * [北京有这些互联网公司,你都知道么?]() - * [上海有这些互联网公司,你都知道么?]() - * [成都有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Y9Qg22WEsBngs8B-K8acqQ) - * [广州有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Ir_hQP0clbnvHrWzDL-qXg) + * [程序员的简历应该这么写!!(附简历模板)](./problems/前序/程序员简历.md) + * [BAT级别技术面试流程和注意事项都在这里了](./problems/前序/BAT级别技术面试流程和注意事项都在这里了.md) + * [北京有这些互联网公司,你都知道么?](./problems/前序/北京互联网公司总结.md) + * [上海有这些互联网公司,你都知道么?](./problems/前序/上海互联网公司总结.md) + * [深圳有这些互联网公司,你都知道么?](./problems/前序/深圳互联网公司总结.md) + * [广州有这些互联网公司,你都知道么?](./problems/前序/广州互联网公司总结.md) + * [成都有这些互联网公司,你都知道么?](./problems/前序/成都互联网公司总结.md) + * [杭州有这些互联网公司,你都知道么?](./problems/前序/杭州互联网公司总结.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) + * [关于时间复杂度,你不知道的都在这里!](./problems/前序/关于时间复杂度,你不知道的都在这里!.md) + * [O(n)的算法居然超时了,此时的n究竟是多大?](./problems/前序/On的算法居然超时了,此时的n究竟是多大?.md) + * [通过一道面试题目,讲一讲递归算法的时间复杂度!](./problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md) + * [本周小结!(算法性能分析系列一)](./problems/周总结/20201210复杂度分析周末总结.md) + * [关于空间复杂度,可能有几个疑问?](./problems/前序/关于空间复杂度,可能有几个疑问?.md) + * [递归算法的时间与空间复杂度分析!](./problems/前序/递归算法的时间与空间复杂度分析.md) + * [刷了这么多题,你了解自己代码的内存消耗么?](./problems/前序/刷了这么多题,你了解自己代码的内存消耗么?.md) -* 数组 - * [必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg) - * [数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) - * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - * [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) - * [数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg) - * [数组:总结篇](https://mp.weixin.qq.com/s/LIfQFRJBH5ENTZpvixHEmg) -* 链表 - * [关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ) - * [链表:听说用虚拟头节点会方便很多?](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://mp.weixin.qq.com/s/vK0JjSTHfpAbs8evz5hH8A) +(持续更新中.....) -* 哈希表 - * [关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA) - * [哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig) - * [哈希表:哈希值太大了,还是得用set](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA) - * [哈希表:今天你快乐了么?](https://mp.weixin.qq.com/s/G4Q2Zfpfe706gLK7HpZHpA) - * [哈希表:map等候多时了](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ) - * [哈希表:其实需要哈希的地方都能找到map的身影](https://mp.weixin.qq.com/s/Ue8pKKU5hw_m-jPgwlHcbA) - * [哈希表:这道题目我做过?](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) - * [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) - * [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - * [哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg) +## 数组 + +1. [数组过于简单,但你该了解这些!](./problems/数组理论基础.md) +2. [数组:每次遇到二分法,都是一看就会,一写就废](./problems/0704.二分查找.md) +3. [数组:就移除个元素很难么?](./problems/0027.移除元素.md) +4. [数组:滑动窗口拯救了你](./problems/0209.长度最小的子数组.md) +5. [数组:这个循环可以转懵很多人!](./problems/0059.螺旋矩阵II.md) +6. [数组:总结篇](./problems/数组总结篇.md) + +## 链表 + +1. [关于链表,你该了解这些!](./problems/链表理论基础.md) +2. [链表:听说用虚拟头节点会方便很多?](./problems/0203.移除链表元素.md) +3. [链表:一道题目考察了常见的五个操作!](./problems/0707.设计链表.md) +4. [链表:听说过两天反转链表又写不出来了?](./problems/0206.翻转链表.md) +5. [链表:删除链表的倒数第 N 个结点](./problems/0019.删除链表的倒数第N个节点.md) +5. [链表:环找到了,那入口呢?](./problems/0142.环形链表II.md) +6. [链表:总结篇!](./problems/链表总结篇.md) + +## 哈希表 + +1. [关于哈希表,你该了解这些!](./problems/哈希表理论基础.md) +2. [哈希表:可以拿数组当哈希表来用,但哈希值不要太大](./problems/0242.有效的字母异位词.md) +3. [哈希表:哈希值太大了,还是得用set](./problems/0349.两个数组的交集.md) +4. [哈希表:用set来判断快乐数](./problems/0202.快乐数.md) +5. [哈希表:map等候多时了](./problems/0001.两数之和.md) +6. [哈希表:其实需要哈希的地方都能找到map的身影](./problems/0454.四数相加II.md) +7. [哈希表:这道题目我做过?](./problems/0383.赎金信.md) +8. [哈希表:解决了两数之和,那么能解决三数之和么?](./problems/0015.三数之和.md) +9. [双指针法:一样的道理,能解决四数之和](./problems/0018.四数之和.md) +10. [哈希表:总结篇!(每逢总结必经典)](./problems/哈希表总结.md) -* 字符串 - * [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) - * [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw) - * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) - * [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) - * [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ) - * [帮你把KMP算法学个通透!(理论篇)B站视频](https://www.bilibili.com/video/BV1PD4y1o7nd) - * [帮你把KMP算法学个通透!(代码篇)B站视频](https://www.bilibili.com/video/BV1M5411j7Xx) - * [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) - * [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ) - * [字符串:前缀表不右移,难道就写不出KMP了?](https://mp.weixin.qq.com/s/p3hXynQM2RRROK5c6X7xfw) - * [字符串:总结篇!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg) +## 字符串 -* 双指针法 - * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - * [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) - * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) - * [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) - * [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) - * [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) - * [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) - * [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - * [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) +1. [字符串:这道题目,使用库函数一行代码搞定](./problems/0344.反转字符串.md) +2. [字符串:简单的反转还不够!](./problems/0541.反转字符串II.md) +3. [字符串:替换空格](./problems/剑指Offer05.替换空格.md) +4. [字符串:花式反转还不够!](./problems/0151.翻转字符串里的单词.md) +5. [字符串:反转个字符串还有这个用处?](./problems/剑指Offer58-II.左旋转字符串.md) +6. [帮你把KMP算法学个通透](./problems/0028.实现strStr.md) +8. [字符串:KMP算法还能干这个!](./problems/0459.重复的子字符串.md) +9. [字符串:总结篇!](./problems/字符串总结.md) -* 栈与队列 - * [栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ) - * [栈与队列:我用栈来实现队列怎么样?](https://mp.weixin.qq.com/s/P6tupDwRFi6Ay-L7DT4NVg) - * [栈与队列:用队列实现栈还有点别扭](https://mp.weixin.qq.com/s/yzn6ktUlL-vRG3-m5a8_Yw) - * [栈与队列:系统中处处都是栈的应用](https://mp.weixin.qq.com/s/nLlmPMsDCIWSqAtr0jbrpQ) - * [栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg) - * [栈与队列:有没有想过计算机是如何处理表达式的?](https://mp.weixin.qq.com/s/hneh2nnLT91rR8ms2fm_kw) - * [栈与队列:滑动窗口里求最大值引出一个重要数据结构](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA) - * [栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng) - * [栈与队列:总结篇!](https://mp.weixin.qq.com/s/xBcHyvHlWq4P13fzxEtkPg) +## 双指针法 -* 二叉树 - * [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A) - * [二叉树:一入递归深似海,从此offer是路人](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/6gY1MiXrnm-khAAJiIb5Bg) - * [本周小结!(二叉树)](https://mp.weixin.qq.com/s/JWmTeC7aKbBfGx4TY6uwuQ) - * [二叉树:我对称么?](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/QMBUTYnoaNfsVHlUADEzKg) - * [二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) - * [二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) - * [二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw) - * [二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg) - * [二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) - * [二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w) - * [本周小结!(二叉树系列三)](https://mp.weixin.qq.com/s/JLLpx3a_8jurXcz6ovgxtg) - * [二叉树:合并两个二叉树](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/n6Rk3nc_X3TSkhXHrVmBTQ) - * [本周小结!(二叉树系列四)](https://mp.weixin.qq.com/s/CbdtOTP0N-HIP7DR203tSg) - * [二叉树:搜索树的公共祖先问题](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/hZtJh4T5lIGBarY-lZJf6Q) - * [二叉树:总结篇!(需要掌握的二叉树技能都在这里了)](https://mp.weixin.qq.com/s/-ZJn3jJVdF683ap90yIj4Q) +双指针法基本都是应用在数组,字符串与链表的题目上 + +1. [数组:就移除个元素很难么?](./problems/0027.移除元素.md) +2. [字符串:这道题目,使用库函数一行代码搞定](./problems/0344.反转字符串.md) +3. [字符串:替换空格](./problems/剑指Offer05.替换空格.md) +4. [字符串:花式反转还不够!](./problems/0151.翻转字符串里的单词.md) +5. [链表:听说过两天反转链表又写不出来了?](./problems/0206.翻转链表.md) +6. [链表:环找到了,那入口呢?](./problems/0142.环形链表II.md) +7. [链表:删除链表的倒数第 N 个结点](./problems/0019.删除链表的倒数第N个节点.md) +8. [哈希表:解决了两数之和,那么能解决三数之和么?](./problems/0015.三数之和.md) +9. [双指针法:一样的道理,能解决四数之和](./problems/0018.四数之和.md) +10. [双指针法:总结篇!](./problems/双指针总结.md) + +## 栈与队列 + +1. [栈与队列:来看看栈和队列不为人知的一面](./problems/栈与队列理论基础.md) +2. [栈与队列:我用栈来实现队列怎么样?](./problems/0232.用栈实现队列.md) +3. [栈与队列:用队列实现栈还有点别扭](./problems/0225.用队列实现栈.md) +4. [栈与队列:系统中处处都是栈的应用](./problems/0020.有效的括号.md) +5. [栈与队列:匹配问题都是栈的强项](./problems/1047.删除字符串中的所有相邻重复项.md) +6. [栈与队列:有没有想过计算机是如何处理表达式的?](./problems/0150.逆波兰表达式求值.md) +7. [栈与队列:滑动窗口里求最大值引出一个重要数据结构](./problems/0239.滑动窗口最大值.md) +8. [栈与队列:求前 K 个高频元素和队列有啥关系?](./problems/0347.前K个高频元素.md) +9. [栈与队列:总结篇!](./problems/栈与队列总结.md) + +## 二叉树 + +题目分类大纲如下: +二叉树大纲 + +1. [关于二叉树,你该了解这些!](./problems/二叉树理论基础.md) +2. [二叉树:一入递归深似海,从此offer是路人](./problems/二叉树的递归遍历.md) +3. [二叉树:听说递归能做的,栈也能做!](./problems/二叉树的迭代遍历.md) +4. [二叉树:前中后序迭代方式的写法就不能统一一下么?](./problems/二叉树的统一迭代法.md) +5. [二叉树:层序遍历登场!](./problems/0102.二叉树的层序遍历.md) +6. [二叉树:你真的会翻转二叉树么?](./problems/0226.翻转二叉树.md) +7. [本周小结!(二叉树)](./problems/周总结/20200927二叉树周末总结.md) +8. [二叉树:我对称么?](./problems/0101.对称二叉树.md) +9. [二叉树:看看这些树的最大深度](./problems/0104.二叉树的最大深度.md) +10. [二叉树:看看这些树的最小深度](./problems/0111.二叉树的最小深度.md) +11. [二叉树:我有多少个节点?](./problems/0222.完全二叉树的节点个数.md) +12. [二叉树:我平衡么?](./problems/0110.平衡二叉树.md) +13. [二叉树:找我的所有路径?](./problems/0257.二叉树的所有路径.md) +14. [本周总结!二叉树系列二](./problems/周总结/20201003二叉树周末总结.md) +15. [二叉树:以为使用了递归,其实还隐藏着回溯](./problems/二叉树中递归带着回溯.md) +16. [二叉树:做了这么多题目了,我的左叶子之和是多少?](./problems/0404.左叶子之和.md) +17. [二叉树:我的左下角的值是多少?](./problems/0513.找树左下角的值.md) +18. [二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](./problems/0112.路径总和.md) +19. [二叉树:构造二叉树登场!](./problems/0106.从中序与后序遍历序列构造二叉树.md) +20. [二叉树:构造一棵最大的二叉树](./problems/0654.最大二叉树.md) +21. [本周小结!(二叉树系列三)](./problems/周总结/20201010二叉树周末总结.md) +22. [二叉树:合并两个二叉树](./problems/0617.合并二叉树.md) +23. [二叉树:二叉搜索树登场!](./problems/0700.二叉搜索树中的搜索.md) +24. [二叉树:我是不是一棵二叉搜索树](./problems/0098.验证二叉搜索树.md) +25. [二叉树:搜索树的最小绝对差](./problems/0530.二叉搜索树的最小绝对差.md) +26. [二叉树:我的众数是多少?](./problems/0501.二叉搜索树中的众数.md) +27. [二叉树:公共祖先问题](./problems/0236.二叉树的最近公共祖先.md) +28. [本周小结!(二叉树系列四)](./problems/周总结/20201017二叉树周末总结.md) +29. [二叉树:搜索树的公共祖先问题](./problems/0235.二叉搜索树的最近公共祖先.md) +30. [二叉树:搜索树中的插入操作](./problems/0701.二叉搜索树中的插入操作.md) +31. [二叉树:搜索树中的删除操作](./problems/0450.删除二叉搜索树中的节点.md) +32. [二叉树:修剪一棵搜索树](./problems/0669.修剪二叉搜索树.md) +33. [二叉树:构造一棵搜索树](./problems/0108.将有序数组转换为二叉搜索树.md) +34. [二叉树:搜索树转成累加树](./problems/0538.把二叉搜索树转换为累加树.md) +35. [二叉树:总结篇!(需要掌握的二叉树技能都在这里了)](./problems/二叉树总结篇.md) -* 回溯算法 - * [关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw) - * [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ) - * [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) - * [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) - * [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) - * [本周小结!(回溯算法系列一)](https://mp.weixin.qq.com/s/m2GnTJdkYhAamustbb6lmw) - * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) - * [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) - * [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) - * [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) - * [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA) - * [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw) - * [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) - * [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ) - * [回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw) - * [回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA) - * [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) - * [本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag) - * [视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) - * [视频来了!!回溯算法:组合问题](https://mp.weixin.qq.com/s/a_r5JR93K_rBKSFplPGNAA) - * [视频来了!!回溯算法:组合问题的剪枝操作](https://mp.weixin.qq.com/s/CK0kj9lq8-rFajxL4amyEg) - * [视频来了!!回溯算法:组合总和](https://mp.weixin.qq.com/s/4M4Cr04uFOWosRMc_5--gg) - * [回溯算法:重新安排行程](https://mp.weixin.qq.com/s/3kmbS4qDsa6bkyxR92XCTA) - * [回溯算法:N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg) - * [回溯算法:解数独](https://mp.weixin.qq.com/s/eWE9TapVwm77yW9Q81xSZQ) - * [一篇总结带你彻底搞透回溯算法!](https://mp.weixin.qq.com/s/r73thpBnK1tXndFDtlsdCQ) +## 回溯算法 -* 贪心算法 - * [关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg) - * [贪心算法:分发饼干](https://mp.weixin.qq.com/s/YSuLIAYyRGlyxbp9BNC1uw) - * [贪心算法:摆动序列](https://mp.weixin.qq.com/s/Xytl05kX8LZZ1iWWqjMoHA) - * [贪心算法:最大子序和](https://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg) - * [本周小结!(贪心算法系列一)](https://mp.weixin.qq.com/s/KQ2caT9GoVXgB1t2ExPncQ) - * [贪心算法:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg) - * [贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA) - * [贪心算法:跳跃游戏II](https://mp.weixin.qq.com/s/kJBcsJ46DKCSjT19pxrNYg) - * [贪心算法:K次取反后最大化的数组和](https://mp.weixin.qq.com/s/dMTzBBVllRm_Z0aaWvYazA) - * [本周小结!(贪心算法系列二)](https://mp.weixin.qq.com/s/RiQri-4rP9abFmq_mlXNiQ) - * [贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw) - * [贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ) - * [贪心算法:柠檬水找零](https://mp.weixin.qq.com/s/0kT4P-hzY7H6Ae0kjQqnZg) +题目分类大纲如下: +回溯算法大纲 -* 动态规划 +1. [关于回溯算法,你该了解这些!](./problems/回溯算法理论基础.md) +2. [回溯算法:组合问题](./problems/0077.组合.md) +3. [回溯算法:组合问题再剪剪枝](./problems/0077.组合优化.md) +4. [回溯算法:求组合总和!](./problems/0216.组合总和III.md) +5. [回溯算法:电话号码的字母组合](./problems/0017.电话号码的字母组合.md) +6. [本周小结!(回溯算法系列一)](./problems/周总结/20201030回溯周末总结.md) +7. [回溯算法:求组合总和(二)](./problems/0039.组合总和.md) +8. [回溯算法:求组合总和(三)](./problems/0040.组合总和II.md) +9. [回溯算法:分割回文串](./problems/0131.分割回文串.md) +10. [回溯算法:复原IP地址](./problems/0093.复原IP地址.md) +11. [回溯算法:求子集问题!](./problems/0078.子集.md) +12. [本周小结!(回溯算法系列二)](./problems/周总结/20201107回溯周末总结.md) +13. [回溯算法:求子集问题(二)](./problems/0090.子集II.md) +14. [回溯算法:递增子序列](./problems/0491.递增子序列.md) +15. [回溯算法:排列问题!](./problems/0046.全排列.md) +16. [回溯算法:排列问题(二)](./problems/0047.全排列II.md) +17. [本周小结!(回溯算法系列三)](./problems/周总结/20201112回溯周末总结.md) +18. [回溯算法去重问题的另一种写法](./problems/回溯算法去重问题的另一种写法.md) +23. [回溯算法:重新安排行程](./problems/0332.重新安排行程.md) +24. [回溯算法:N皇后问题](./problems/0051.N皇后.md) +25. [回溯算法:解数独](./problems/0037.解数独.md) +26. [一篇总结带你彻底搞透回溯算法!](./problems/回溯总结.md) -* 图论 +## 贪心算法 -* 数论 +题目分类大纲如下: + +贪心算法大纲 + +1. [关于贪心算法,你该了解这些!](./problems/贪心算法理论基础.md) +2. [贪心算法:分发饼干](./problems/0455.分发饼干.md) +3. [贪心算法:摆动序列](./problems/0376.摆动序列.md) +4. [贪心算法:最大子序和](./problems/0053.最大子序和.md) +5. [本周小结!(贪心算法系列一)](./problems/周总结/20201126贪心周末总结.md) +6. [贪心算法:买卖股票的最佳时机II](./problems/0122.买卖股票的最佳时机II.md) +7. [贪心算法:跳跃游戏](./problems/0055.跳跃游戏.md) +8. [贪心算法:跳跃游戏II](./problems/0045.跳跃游戏II.md) +9. [贪心算法:K次取反后最大化的数组和](./problems/1005.K次取反后最大化的数组和.md) +10. [本周小结!(贪心算法系列二)](./problems/周总结/20201203贪心周末总结.md) +11. [贪心算法:加油站](./problems/0134.加油站.md) +12. [贪心算法:分发糖果](./problems/0135.分发糖果.md) +13. [贪心算法:柠檬水找零](./problems/0860.柠檬水找零.md) +14. [贪心算法:根据身高重建队列](./problems/0406.根据身高重建队列.md) +15. [本周小结!(贪心算法系列三)](./problems/周总结/20201217贪心周末总结.md) +16. [贪心算法:根据身高重建队列(续集)](./problems/根据身高重建队列(vector原理讲解).md) +17. [贪心算法:用最少数量的箭引爆气球](./problems/0452.用最少数量的箭引爆气球.md) +18. [贪心算法:无重叠区间](./problems/0435.无重叠区间.md) +19. [贪心算法:划分字母区间](./problems/0763.划分字母区间.md) +20. [贪心算法:合并区间](./problems/0056.合并区间.md) +21. [本周小结!(贪心算法系列四)](./problems/周总结/20201224贪心周末总结.md) +22. [贪心算法:单调递增的数字](./problems/0738.单调递增的数字.md) +23. [贪心算法:买卖股票的最佳时机含手续费](./problems/0714.买卖股票的最佳时机含手续费.md) +24. [贪心算法:我要监控二叉树!](./problems/0968.监控二叉树.md) +25. [贪心算法:总结篇!(每逢总结必经典)](./problems/贪心算法总结篇.md) + +## 动态规划 + +动态规划专题已经开始啦,来不及解释了,小伙伴们上车别掉队! + +1. [关于动态规划,你该了解这些!](./problems/动态规划理论基础.md) +2. [动态规划:斐波那契数](./problems/0509.斐波那契数.md) +3. [动态规划:爬楼梯](./problems/0070.爬楼梯.md) +4. [动态规划:使用最小花费爬楼梯](./problems/0746.使用最小花费爬楼梯.md) +5. [本周小结!(动态规划系列一)](./problems/周总结/20210107动规周末总结.md) +6. [动态规划:不同路径](./problems/0062.不同路径.md) +7. [动态规划:不同路径还不够,要有障碍!](./problems/0063.不同路径II.md) +8. [动态规划:整数拆分,你要怎么拆?](./problems/0343.整数拆分.md) +9. [动态规划:不同的二叉搜索树](./problems/0096.不同的二叉搜索树.md) +10. [本周小结!(动态规划系列二)](./problems/周总结/20210114动规周末总结.md) + +背包问题系列: + +背包问题大纲 + +11. [动态规划:关于01背包问题,你该了解这些!](./problems/背包理论基础01背包-1.md) +12. [动态规划:关于01背包问题,你该了解这些!(滚动数组)](./problems/背包理论基础01背包-2.md) +13. [动态规划:分割等和子集可以用01背包!](./problems/0416.分割等和子集.md) +14. [动态规划:最后一块石头的重量 II](./problems/1049.最后一块石头的重量II.md) +15. [本周小结!(动态规划系列三)](./problems/周总结/20210121动规周末总结.md) +16. [动态规划:目标和!](./problems/0494.目标和.md) +17. [动态规划:一和零!](./problems/0474.一和零.md) +18. [动态规划:关于完全背包,你该了解这些!](./problems/背包问题理论基础完全背包.md) +19. [动态规划:给你一些零钱,你要怎么凑?](./problems/0518.零钱兑换II.md) +20. [本周小结!(动态规划系列四)](./problems/周总结/20210128动规周末总结.md) +21. [动态规划:Carl称它为排列总和!](./problems/0377.组合总和Ⅳ.md) +22. [动态规划:以前我没得选,现在我选择再爬一次!](./problems/0070.爬楼梯完全背包版本.md) +23. [动态规划: 给我个机会,我再兑换一次零钱](./problems/0322.零钱兑换.md) +24. [动态规划:一样的套路,再求一次完全平方数](./problems/0279.完全平方数.md) +25. [本周小结!(动态规划系列五)](./problems/周总结/20210204动规周末总结.md) +26. [动态规划:单词拆分](./problems/0139.单词拆分.md) +27. [动态规划:关于多重背包,你该了解这些!](./problems/背包问题理论基础多重背包.md) +28. [听说背包问题很难? 这篇总结篇来拯救你了](./problems/背包总结篇.md) + +打家劫舍系列: + +29. [动态规划:开始打家劫舍!](./problems/0198.打家劫舍.md) +30. [动态规划:继续打家劫舍!](./problems/0213.打家劫舍II.md) +31. [动态规划:还要打家劫舍!](./problems/0337.打家劫舍III.md) + +股票系列: + +股票问题总结 + +32. [动态规划:买卖股票的最佳时机](./problems/0121.买卖股票的最佳时机.md) +33. [动态规划:本周我们都讲了这些(系列六)](./problems/周总结/20210225动规周末总结.md) +33. [动态规划:买卖股票的最佳时机II](./problems/0122.买卖股票的最佳时机II(动态规划).md) +34. [动态规划:买卖股票的最佳时机III](./problems/0123.买卖股票的最佳时机III.md) +35. [动态规划:买卖股票的最佳时机IV](./problems/0188.买卖股票的最佳时机IV.md) +36. [动态规划:最佳买卖股票时机含冷冻期](./problems/0309.最佳买卖股票时机含冷冻期.md) +37. [动态规划:本周我们都讲了这些(系列七)](./problems/周总结/20210304动规周末总结.md) +38. [动态规划:买卖股票的最佳时机含手续费](./problems/0714.买卖股票的最佳时机含手续费(动态规划).md) +39. [动态规划:股票系列总结篇](./problems/动态规划-股票问题总结篇.md) + +子序列系列: + +40. [动态规划:最长递增子序列](./problems/0300.最长上升子序列.md) +41. [动态规划:最长连续递增序列](./problems/0674.最长连续递增序列.md) +42. [动态规划:最长重复子数组](./problems/0718.最长重复子数组.md) +43. [动态规划:最长公共子序列](./problems/1143.最长公共子序列.md) +45. [动态规划:不相交的线](./problems/1035.不相交的线.md) +46. [动态规划:最大子序和](./problems/0053.最大子序和(动态规划).md) +47. [动态规划:判断子序列](./problems/0392.判断子序列.md) +48. [动态规划:不同的子序列](./problems/0115.不同的子序列.md) +49. [动态规划:两个字符串的删除操作](./problems/0583.两个字符串的删除操作.md) +51. [动态规划:编辑距离](./problems/0072.编辑距离.md) +52. [为了绝杀编辑距离,Carl做了三步铺垫,你都知道么?](./problems/为了绝杀编辑距离,卡尔做了三步铺垫.md) +53. [动态规划:回文子串](./problems/0647.回文子串.md) +54. [动态规划:最长回文子序列](./problems/0516.最长回文子序列.md) -* 高级数据结构经典题目 - * 并查集 - * 最小生成树 - * 线段树 - * 树状数组 - * 字典树 -* 海量数据处理 (持续更新中....) +## 图论 + +## 十大排序 + +## 数论 + +## 高级数据结构经典题目 + +* 并查集 +* 最小生成树 +* 线段树 +* 树状数组 +* 字典树 + +## 海量数据处理 # 算法模板 [各类基础算法模板](https://github.com/youngyangyang04/leetcode/blob/master/problems/算法模板.md) -# LeetCode 最强题解: +# 备战秋招 -|题目 | 类型 | 难度 | 解题方法 | -|---|---| ---| --- | -|[0001.两数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0001.两数之和.md) | 数组|简单|**暴力** **哈希**| -|[0015.三数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0015.三数之和.md) | 数组 |中等|**双指针** **哈希**| -|[0017.电话号码的字母组合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0017.电话号码的字母组合.md) | 回溯 |中等|**回溯**| -|[0018.四数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0018.四数之和.md) | 数组 |中等|**双指针**| -|[0019.删除链表的倒数第N个节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0019.删除链表的倒数第N个节点.md) | 链表 |中等|**双指针**| -|[0020.有效的括号](https://github.com/youngyangyang04/leetcode/blob/master/problems/0020.有效的括号.md) | 栈 |简单|**栈**| -|[0021.合并两个有序链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0021.合并两个有序链表.md) |链表 |简单|**模拟** | -|[0024.两两交换链表中的节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0024.两两交换链表中的节点.md) |链表 |中等|**模拟** | -|[0026.删除排序数组中的重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/0026.删除排序数组中的重复项.md) |数组 |简单|**暴力** **快慢指针/快慢指针** | -|[0027.移除元素](https://github.com/youngyangyang04/leetcode/blob/master/problems/0027.移除元素.md) |数组 |简单| **暴力** **双指针/快慢指针/双指针**| -|[0028.实现strStr()](https://github.com/youngyangyang04/leetcode/blob/master/problems/0028.实现strStr().md) |字符串 |简单| **KMP** | -|[0031.下一个排列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0031.下一个排列.md) |数组 |中等| **模拟** 这道题目还是有难度的| -|[0034.在排序数组中查找元素的第一个和最后一个位置](https://github.com/youngyangyang04/leetcode/blob/master/problems/0031.下一个排列.md) |数组 |中等| **二分查找**比35.搜索插入位置难一些| -|[0035.搜索插入位置](https://github.com/youngyangyang04/leetcode/blob/master/problems/0035.搜索插入位置.md) |数组 |简单| **暴力** **二分**| -|[0037.解数独](https://github.com/youngyangyang04/leetcode/blob/master/problems/0037.解数独.md) |回溯 |困难| **回溯**| -|[0039.组合总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0039.组合总和.md) |数组/回溯 |中等| **回溯**| -|[0040.组合总和II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0040.组合总和II.md) |数组/回溯 |中等| **回溯**| -|[0042.接雨水](https://github.com/youngyangyang04/leetcode/blob/master/problems/0042.接雨水.md) |数组/栈/双指针 |困难| **双指针** **单调栈** **动态规划**| -|[0045.跳跃游戏II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0045.跳跃游戏II.md) |贪心 |困难| **贪心**| -|[0046.全排列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0046.全排列.md) |回溯|中等| **回溯**| -|[0047.全排列II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0047.全排列II.md) |回溯|中等| **回溯**| -|[0051.N皇后](https://github.com/youngyangyang04/leetcode/blob/master/problems/0051.N皇后.md) |回溯|困难| **回溯**| -|[0052.N皇后II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0052.N皇后II.md) |回溯|困难| **回溯**| -|[0053.最大子序和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md) |数组 |简单|**暴力** **贪心** 动态规划 分治| -|[0055.跳跃游戏](https://github.com/youngyangyang04/leetcode/blob/master/problems/0053.最大子序和.md) |数组 |中等| **贪心** 经典题目| -|[0056.合并区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0056.合并区间.md) |数组 |中等| **贪心** 以为是模拟题,其实是贪心| -|[0057.插入区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0057.插入区间.md) |数组 |困难| **模拟** 是一道数组难题| -|[0059.螺旋矩阵II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0059.螺旋矩阵II.md) |数组 |中等|**模拟**| -|[0062.不同路径](https://github.com/youngyangyang04/leetcode/blob/master/problems/0062.不同路径.md) |数组、动态规划 |中等|**深搜** **动态规划** **数论**| -|[0070.爬楼梯](https://github.com/youngyangyang04/leetcode/blob/master/problems/0070.爬楼梯.md) |动态规划|简单|**动态规划** dp里求排列| -|[0077.组合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0077.组合.md) |回溯 |中等|**回溯**| -|[0078.子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0078.子集.md) |回溯/数组 |中等|**回溯**| -|[0083.删除排序链表中的重复元素](https://github.com/youngyangyang04/leetcode/blob/master/problems/0083.删除排序链表中的重复元素.md) |链表 |简单|**模拟**| -|[0084.柱状图中最大的矩形](https://github.com/youngyangyang04/leetcode/blob/master/problems/0084.柱状图中最大的矩形.md) |数组 |困难|**单调栈**| -|[0090.子集II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0090.子集II.md) |回溯/数组 |中等|**回溯**| -|[0093.复原IP地址](https://github.com/youngyangyang04/leetcode/blob/master/problems/0093.复原IP地址) |回溯 |中等|**回溯**| -|[0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md) |树 |中等|**递归** **迭代/栈**| -|[0098.验证二叉搜索树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0098.验证二叉搜索树.md) |树 |中等|**递归**| -|[0100.相同的树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0100.相同的树.md) |树 |简单|**递归** | -|[0101.对称二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0101.对称二叉树.md) |树 |简单|**递归** **迭代/队列/栈** 和100. 相同的树 相似| -|[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) |树 |中等|**广度优先搜索/队列**| -|[0104.二叉树的最大深度](https://github.com/youngyangyang04/leetcode/blob/master/problems/0104.二叉树的最大深度.md) |树 |简单|**递归** **迭代/队列/BFS**| -|[0105.从前序与中序遍历序列构造二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0105.从前序与中序遍历序列构造二叉树.md) |二叉树 |中等|**递归**| -|[0106.从中序与后序遍历序列构造二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0106.从中序与后序遍历序列构造二叉树.md) |二叉树 |中等|**递归** 根据数组构造二叉树| -|[0107.二叉树的层次遍历II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0107.二叉树的层次遍历II.md) |树 |简单|**广度优先搜索/队列/BFS**| -|[0108.将有序数组转换为二叉搜索树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0108.将有序数组转换为二叉搜索树.md) |二叉搜索树 |中等|**递归** **迭代** 通过递归函数返回值构造树| -|[0110.平衡二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0110.平衡二叉树.md) |二叉树 |简单|**递归**| -|[0111.二叉树的最小深度](https://github.com/youngyangyang04/leetcode/blob/master/problems/0111.二叉树的最小深度.md) |二叉树 |简单|**递归** **队列/BFS**| -|[0112.路径总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0112.路径总和.md) |二叉树树 |简单|**深度优先搜索/递归** **回溯** **栈** 思考递归函数什么时候需要返回值| -|[0113.路径总和II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0113.路径总和II.md) |二叉树树 |简单|**深度优先搜索/递归** **回溯** **栈**| -|[0116.填充每个节点的下一个右侧节点指针](https://github.com/youngyangyang04/leetcode/blob/master/problems/0116.填充每个节点的下一个右侧节点指针.md) |二叉树 |中等|**递归** **迭代/广度优先搜索**| -|[0117.填充每个节点的下一个右侧节点指针II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0117.填充每个节点的下一个右侧节点指针II.md) |二叉树 |中等|**递归** **迭代/广度优先搜索**| -|[0122.买卖股票的最佳时机II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0122.买卖股票的最佳时机II.md) |贪心 |简单|**贪心** | -|[0127.单词接龙](https://github.com/youngyangyang04/leetcode/blob/master/problems/0127.单词接龙.md) |广度优先搜索 |中等|**广度优先搜索**| -|[0129.求根到叶子节点数字之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0129.求根到叶子节点数字之和.md) |二叉树 |中等|**递归/回溯** 递归里隐藏着回溯,和113.路径总和II类似| -|[0131.分割回文串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0131.分割回文串.md) |回溯 |中等|**回溯**| -|[0135.分发糖果](https://github.com/youngyangyang04/leetcode/blob/master/problems/0135.分发糖果.md) |贪心 |困难|**贪心**好题目| -|[0139.单词拆分](https://github.com/youngyangyang04/leetcode/blob/master/problems/0139.单词拆分.md) |动态规划 |中等|**完全背包** **回溯法**| -|[0141.环形链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0141.环形链表.md) |链表 |简单|**快慢指针/双指针**| -|[0142.环形链表II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0142.环形链表II.md) |链表 |中等|**快慢指针/双指针**| -|[0143.重排链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0143.重排链表.md) |链表 |中等|**快慢指针/双指针** 也可以用数组,双向队列模拟,考察链表综合操作的好题| -|[0144.二叉树的前序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) |树 |中等|**递归** **迭代/栈**| -|[0145.二叉树的后序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0145.二叉树的后序遍历.md) |树 |困难|**递归** **迭代/栈**| -|[0147.对链表进行插入排序](https://github.com/youngyangyang04/leetcode/blob/master/problems/0147.对链表进行插入排序.md) |链表 |中等|**模拟** 考察链表综合操作| -|[0150.逆波兰表达式求值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0150.逆波兰表达式求值.md) |栈 |中等|**栈**| -|[0151.翻转字符串里的单词](https://github.com/youngyangyang04/leetcode/blob/master/problems/0151.翻转字符串里的单词.md) |字符串 |中等|**模拟/双指针**| -|[0155.最小栈](https://github.com/youngyangyang04/leetcode/blob/master/problems/0155.最小栈.md) |栈 |简单|**栈**| -|[0199.二叉树的右视图](https://github.com/youngyangyang04/leetcode/blob/master/problems/0199.二叉树的右视图.md) |二叉树 |中等|**广度优先遍历/队列**| -|[0202.快乐数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0202.快乐数.md) |哈希表 |简单|**哈希**| -|[0203.移除链表元素](https://github.com/youngyangyang04/leetcode/blob/master/problems/0203.移除链表元素.md) |链表 |简单|**模拟** **虚拟头结点**| -|[0205.同构字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0205.同构字符串.md) |哈希表 |简单| **哈希**| -|[0206.翻转链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0206.翻转链表.md) |链表 |简单| **双指针法** **递归**| -|[0209.长度最小的子数组](https://github.com/youngyangyang04/leetcode/blob/master/problems/0209.长度最小的子数组.md) |数组 |中等| **暴力** **滑动窗口**| -|[0216.组合总和III](https://github.com/youngyangyang04/leetcode/blob/master/problems/0216.组合总和III.md) |数组/回溯 |中等| **回溯算法**| -|[0219.存在重复元素II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0219.存在重复元素II.md) | 哈希表 |简单| **哈希** | -|[0222.完全二叉树的节点个数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0222.完全二叉树的节点个数.md) | 树 |简单| **递归** | -|[0225.用队列实现栈](https://github.com/youngyangyang04/leetcode/blob/master/problems/0225.用队列实现栈.md) | 队列 |简单| **队列** | -|[0226.翻转二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0226.翻转二叉树.md) |二叉树 |简单| **递归** **迭代**| -|[0232.用栈实现队列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0232.用栈实现队列.md) | 栈 |简单| **栈** | -|[0235.二叉搜索树的最近公共祖先](https://github.com/youngyangyang04/leetcode/blob/master/problems/0235.二叉搜索树的最近公共祖先.md) | 二叉搜索树 |简单| **递归** **迭代** | -|[0236.二叉树的最近公共祖先](https://github.com/youngyangyang04/leetcode/blob/master/problems/0236.二叉树的最近公共祖先.md) | 二叉树 |中等| **递归/回溯** 与其说是递归,不如说是回溯| -|[0237.删除链表中的节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0237.删除链表中的节点.md) |链表 |简单| **原链表移除** **添加虚拟节点** 递归| -|[0239.滑动窗口最大值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0239.滑动窗口最大值.md) |滑动窗口/队列 |困难| **单调队列**| -|[0242.有效的字母异位词](https://github.com/youngyangyang04/leetcode/blob/master/problems/0242.有效的字母异位词.md) |哈希表 |简单| **哈希**| -|[0257.二叉树的所有路径](https://github.com/youngyangyang04/leetcode/blob/master/problems/0257.二叉树的所有路径.md) |树 |简单| **递归/回溯**| -|[0283.移动零](https://github.com/youngyangyang04/leetcode/blob/master/problems/0283.移动零.md) |数组 |简单| **双指针** 和 27.移除元素 一个套路| -|[0300.最长上升子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0300.最长上升子序列.md) |动态规划 |中等| **动态规划**| -|[0316.去除重复字母](https://github.com/youngyangyang04/leetcode/blob/master/problems/0316.去除重复字母.md) |贪心/字符串 |中等| **单调栈** 这道题目处理的情况比较多,属于单调栈中的难题| -|[0332.重新安排行程](https://github.com/youngyangyang04/leetcode/blob/master/problems/0332.重新安排行程.md) |深度优先搜索/回溯 |中等| **深度优先搜索/回溯算法**| -|[0343.整数拆分](https://github.com/youngyangyang04/leetcode/blob/master/problems/0343.整数拆分.md) |动态规划/贪心 |中等| **动态规划**| -|[0344.反转字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0344.反转字符串.md) |字符串 |简单| **双指针**| -|[0347.前K个高频元素](https://github.com/youngyangyang04/leetcode/blob/master/problems/0347.前K个高频元素.md) |哈希/堆/优先级队列 |中等| **哈希/优先级队列**| -|[0349.两个数组的交集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0349.两个数组的交集.md) |哈希表 |简单|**哈希**| -|[0350.两个数组的交集II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0350.两个数组的交集II.md) |哈希表 |简单|**哈希**| -|[0377.组合总和Ⅳ](https://github.com/youngyangyang04/leetcode/blob/master/problems/0377.组合总和Ⅳ.md) |动态规划 |中等|**完全背包** 求排列| -|[0383.赎金信](https://github.com/youngyangyang04/leetcode/blob/master/problems/0383.赎金信.md) |数组 |简单|**暴力** **字典计数** **哈希**| -|[0404.左叶子之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0404.左叶子之和.md) |树/二叉树 |简单|**递归** **迭代**| -|[0406.根据身高重建队列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0406.根据身高重建队列.md) |树/二叉树 |简单|**递归** **迭代**| -|[0416.分割等和子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0416.分割等和子集.md) |动态规划 |中等|**背包问题/01背包**| -|[0429.N叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0429.N叉树的层序遍历.md) |树 |简单|**队列/广度优先搜索**| -|[0434.字符串中的单词数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0434.字符串中的单词数.md) |字符串 |简单|**模拟**| -|[0435.无重叠区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0435.无重叠区间.md) |贪心 |中等|**贪心** 经典题目,有点难| -|[0450.删除二叉搜索树中的节点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0450.删除二叉搜索树中的节点.md) |树 |中等|**递归**| -|[0452.用最少数量的箭引爆气球](https://github.com/youngyangyang04/leetcode/blob/master/problems/0452.用最少数量的箭引爆气球.md) |贪心/排序 |中等|**贪心** 经典题目| -|[0454.四数相加II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0454.四数相加II.md) |哈希表 |中等| **哈希**| -|[0455.分发饼干](https://github.com/youngyangyang04/leetcode/blob/master/problems/0455.分发饼干.md) |贪心 |简单| **贪心**| -|[0459.重复的子字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0459.重复的子字符串.md) |字符创 |简单| **KMP**| -|[0473.火柴拼正方形](https://github.com/youngyangyang04/leetcode/blob/master/problems/0473.火柴拼正方形.md) |深度优先搜索|中等| **回溯算法** 和698.划分为k个相等的子集差不多| -|[0474.一和零](https://github.com/youngyangyang04/leetcode/blob/master/problems/0474.一和零.md) |动态规划 |中等| **多重背包** 好题目| -|[0486.预测赢家](https://github.com/youngyangyang04/leetcode/blob/master/problems/0486.预测赢家.md) |动态规划 |中等| **递归** **记忆递归** **动态规划**| -|[0491.递增子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/0491.递增子序列.md) |深度优先搜索 |中等|**深度优先搜索/回溯算法** 这个去重有意思| -|[0496.下一个更大元素I](https://github.com/youngyangyang04/leetcode/blob/master/problems/0496.下一个更大元素I.md) |栈 |中等|**单调栈** 入门题目,但是两个数组还是有点绕的| -|[0501.二叉搜索树中的众数](https://github.com/youngyangyang04/leetcode/blob/master/problems/0501.二叉搜索树中的众数.md) |二叉树 |简单|**递归/中序遍历**| -|[0513.找树左下角的值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0513.找树左下角的值.md) |二叉树 |中等|**递归** **迭代**| -|[0515.在每个树行中找最大值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0515.在每个树行中找最大值.md) |二叉树 |简单|**广度优先搜索/队列**| -|[0518.零钱兑换II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0518.零钱兑换II.md) |动态规划 |中等|**动态规划** dp里求组合| -|[0530.二叉搜索树的最小绝对差](https://github.com/youngyangyang04/leetcode/blob/master/problems/0530.二叉搜索树的最小绝对差.md) |二叉树搜索树 |简单|**递归** **迭代**| -|[0538.把二叉搜索树转换为累加树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0538.把二叉搜索树转换为累加树.md) |二叉搜索树 |简单|**递归** **迭代**| -|[0541.反转字符串II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0541.反转字符串II.md) |字符串 |简单| **模拟**| -|[0559.N叉树的最大深度](https://github.com/youngyangyang04/leetcode/blob/master/problems/0559.N叉树的最大深度.md) |N叉树 |简单| **递归**| -|[0572.另一个树的子树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0572.另一个树的子树.md) |二叉树 |简单| **递归**| -|[0575.分糖果](https://github.com/youngyangyang04/leetcode/blob/master/problems/0575.分糖果.md) |哈希表 |简单|**哈希**| -|[0589.N叉树的前序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0589.N叉树的前序遍历.md) |N叉树 |简单|**递归** **栈/迭代**| -|[0590.N叉树的后序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0590.N叉树的后序遍历.md) |N叉树 |简单|**递归** **栈/迭代**| -|[0617.合并二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0617.合并二叉树.md) |树 |简单|**递归** **迭代**| -|[0637.二叉树的层平均值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0637.二叉树的层平均值.md) |树 |简单|**广度优先搜索/队列**| -|[0649.Dota2参议院](https://github.com/youngyangyang04/leetcode/blob/master/problems/0649.Dota2参议院.md) |贪心 |简单|**贪心算法** 简单的贪心策略但代码实现很有技巧| -|[0654.最大二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0654.最大二叉树.md) |树 |中等|**递归**| -|[0685.冗余连接II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0685.冗余连接II.md) | 并查集/树/图 |困难|**并查集**| -|[0669.修剪二叉搜索树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0669.修剪二叉搜索树.md) | 二叉搜索树/二叉树 |简单|**递归** **迭代**| -|[0698.划分为k个相等的子集](https://github.com/youngyangyang04/leetcode/blob/master/problems/0698.划分为k个相等的子集.md) |回溯算法|中等|动态规划 **回溯算法** 这其实是组合问题,使用了两次递归,好题| -|[0700.二叉搜索树中的搜索](https://github.com/youngyangyang04/leetcode/blob/master/problems/0700.二叉搜索树中的搜索.md) |二叉搜索树 |简单|**递归** **迭代**| -|[0701.二叉搜索树中的插入操作](https://github.com/youngyangyang04/leetcode/blob/master/problems/0701.二叉搜索树中的插入操作.md) |二叉搜索树 |简单|**递归** **迭代**| -|[0705.设计哈希集合](https://github.com/youngyangyang04/leetcode/blob/master/problems/0705.设计哈希集合.md) |哈希表 |简单|**模拟**| -|[0707.设计链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0707.设计链表.md) |链表 |中等|**模拟**| -|[0714.买卖股票的最佳时机含手续费](https://github.com/youngyangyang04/leetcode/blob/master/problems/0714.买卖股票的最佳时机含手续费.md) |贪心 动态规划 |中等|**贪心** **动态规划** 和122.买卖股票的最佳时机II类似,贪心的思路很巧妙| -|[0763.划分字母区间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0763.划分字母区间.md) |贪心 |中等|**双指针/贪心** 体现贪心尽可能多的思想| -|[0738.单调递增的数字](https://github.com/youngyangyang04/leetcode/blob/master/problems/0738.单调递增的数字.md) |贪心算法 |中等|**贪心算法** 思路不错,贪心好题| -|[0739.每日温度](https://github.com/youngyangyang04/leetcode/blob/master/problems/0739.每日温度.md) |栈 |中等|**单调栈** 适合单调栈入门| -|[0767.重构字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0767.重构字符串.md) |字符串 |中等|**字符串** + 排序+一点贪心| -|[0841.钥匙和房间](https://github.com/youngyangyang04/leetcode/blob/master/problems/0841.钥匙和房间.md) |孤岛问题 |中等|**bfs** **dfs**| -|[0844.比较含退格的字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0844.比较含退格的字符串.md) |字符串 |简单|**栈** **双指针优化** 使用栈的思路但没有必要使用栈| -|[0860.柠檬水找零](https://github.com/youngyangyang04/leetcode/blob/master/problems/0860.柠檬水找零.md) |贪心算法 |简单|**贪心算法** 基础题目| -|[0925.长按键入](https://github.com/youngyangyang04/leetcode/blob/master/problems/0925.长按键入.md) |字符串 |简单|**双指针/模拟** 是一道模拟类型的题目| -|[0941.有效的山脉数组](https://github.com/youngyangyang04/leetcode/blob/master/problems/0941.有效的山脉数组.md) |数组 |简单|**双指针**| -|[0968.监控二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0968.监控二叉树.md) |二叉树 |困难|**贪心** 贪心与二叉树的结合| -|[0973.最接近原点的K个点](https://github.com/youngyangyang04/leetcode/blob/master/problems/0973.最接近原点的K个点.md) |优先级队列 |中等|**优先级队列**| -|[0977.有序数组的平方](https://github.com/youngyangyang04/leetcode/blob/master/problems/0977.有序数组的平方.md) |数组 |中等|**双指针** 还是比较巧妙的| -|[1002.查找常用字符](https://github.com/youngyangyang04/leetcode/blob/master/problems/1002.查找常用字符.md) |栈 |简单|**栈**| -|[1005.K次取反后最大化的数组和](https://github.com/youngyangyang04/leetcode/blob/master/problems/1005.K次取反后最大化的数组和.md) |贪心/排序 |简单|**贪心算法** 贪心基础题目| -|[1047.删除字符串中的所有相邻重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |哈希表 |简单|**哈希表/数组**| -|[1049.最后一块石头的重量II](https://github.com/youngyangyang04/leetcode/blob/master/problems/1049.最后一块石头的重量II.md) |动态规划 |中等|**01背包**| -|[1207.独一无二的出现次数](https://github.com/youngyangyang04/leetcode/blob/master/problems/1207.独一无二的出现次数.md) |哈希表 |简单|**哈希** 两层哈希| -|[1221.分割平衡字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/1221.分割平衡字符串.md) |贪心 |简单|**贪心算法** 基础题目| -|[1356.根据数字二进制下1的数目排序](https://github.com/youngyangyang04/leetcode/blob/master/problems/1356.根据数字二进制下1的数目排序.md) |位运算 |简单|**位运算** 巧妙的计算二进制中1的数量| -|[1365.有多少小于当前数字的数字](https://github.com/youngyangyang04/leetcode/blob/master/problems/1365.有多少小于当前数字的数字.md) |数组、哈希表 |简单|**哈希** 从后遍历的技巧很不错| -|[1382.将二叉搜索树变平衡](https://github.com/youngyangyang04/leetcode/blob/master/problems/1047.删除字符串中的所有相邻重复项.md) |二叉搜索树 |中等|**递归** **迭代** 98和108的组合题目| -|[1403.非递增顺序的最小子序列](https://github.com/youngyangyang04/leetcode/blob/master/problems/1403.非递增顺序的最小子序列.md) | 贪心算法|简单|**贪心算法** 贪心基础题目| -|[1518.换酒问题](https://github.com/youngyangyang04/leetcode/blob/master/problems/1518.换酒问题.md) | 贪心算法|简单|**贪心算法** 贪心基础题目| -|[剑指Offer05.替换空格](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) |字符串 |简单|**双指针**| -|[ 剑指Offer58-I.翻转单词顺序](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) |字符串 |简单|**模拟/双指针**| -|[剑指Offer58-II.左旋转字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer58-II.左旋转字符串.md) |字符串 |简单|**反转操作**| -|[剑指Offer59-I.滑动窗口的最大值](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer59-I.滑动窗口的最大值.md) |滑动窗口/队列 |困难|**单调队列**| -|[面试题02.07.链表相交](https://github.com/youngyangyang04/leetcode/blob/master/problems/面试题02.07.链表相交.md) |链表 |简单|**模拟**| +1. [技术比较弱,也对技术不感兴趣,如何选择方向?](https://mp.weixin.qq.com/s/ZCzFiAHZHLqHPLJQXNm75g) +2. [刷题就用库函数了,怎么了?](https://mp.weixin.qq.com/s/6K3_OSaudnHGq2Ey8vqYfg) +3. [关于实习,大家可能有点迷茫!](https://mp.weixin.qq.com/s/xcxzi7c78kQGjvZ8hh7taA) +4. [马上秋招了,慌得很!](https://mp.weixin.qq.com/s/7q7W8Cb2-a5U5atZdOnOFA) -持续更新中.... +# B站算法视频讲解 + +以下为[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/) +* [分割回文串(对应力扣题目:131.分割回文串)](https://www.bilibili.com/video/BV1c54y1e7k6) +* [二叉树理论基础](https://www.bilibili.com/video/BV1Hy4y1t7ij) +* [二叉树的递归遍历](https://www.bilibili.com/video/BV1Wh411S7xt) +* [二叉树的非递归遍历(一)](https://www.bilibili.com/video/BV15f4y1W7i2) + +(持续更新中....) # 关于作者 大家好,我是程序员Carl,哈工大师兄,ACM 校赛、黑龙江省赛、东北四省赛金牌、亚洲区域赛铜牌获得者,先后在腾讯和百度从事后端技术研发,CSDN博客专家。对算法和C++后端技术有一定的见解,利用工作之余重新刷leetcode。 -**加我的微信,备注:「个人简单介绍」+「组队刷题」**, 拉你进刷题群,每天一道经典题目分析,而且题目不是孤立的,每一道题目之间都是有关系的,都是由浅入深一脉相承的,所以学习效果最好是每篇连续着看,也许之前你会某些知识点,但是一直没有把知识点串起来,这里每天一篇文章就会帮你把知识点串起来。 +加入刷题微信群,备注:「个人简单介绍」 + 组队刷题 + +也欢迎与我交流,备注:「个人简单介绍」 + 交流,围观朋友圈,做点赞之交(备注没有自我介绍不通过哦) # 我的公众号 -更多精彩文章持续更新,微信搜索:「代码随想录」第一时间围观,关注后回复:「简历模板」「二叉树总结」「回溯算法总结」「栈与队列总结」等关键字就可以获得我整理的学习资料。 +更多精彩文章持续更新,微信搜索:「代码随想录」第一时间围观,关注后回复:「666」可以获得所有算法专题原创PDF。 -**每天8:35准时为你推送一篇经典面试题目,帮你梳理算法知识体系,轻松学习算法!**,并且公众号里有大量学习资源,也有我自己的学习心得和方法总结,更有很多志同道合的好伙伴在这里打卡学习,来看看就你知道了,相信一定会有所收获! +**「代码随想录」每天准时为你推送一篇经典面试题目,帮你梳理算法知识体系,轻松学习算法!**,并且公众号里有大量学习资源,也有我自己的学习心得和方法总结,更有上万录友们在这里打卡学习。 + +**来看看就知道了,你会发现相见恨晚!** - +![](./pics/公众号.png) diff --git a/pics/100.相同的树.png b/pics/100.相同的树.png deleted file mode 100644 index 88da7600..00000000 Binary files a/pics/100.相同的树.png and /dev/null differ diff --git a/pics/1002.查找常用字符.png b/pics/1002.查找常用字符.png deleted file mode 100644 index 23d75057..00000000 Binary files a/pics/1002.查找常用字符.png and /dev/null differ diff --git a/pics/101. 对称二叉树.png b/pics/101. 对称二叉树.png deleted file mode 100644 index 6485026d..00000000 Binary files a/pics/101. 对称二叉树.png and /dev/null differ diff --git a/pics/101. 对称二叉树1.png b/pics/101. 对称二叉树1.png deleted file mode 100644 index 98286136..00000000 Binary files a/pics/101. 对称二叉树1.png and /dev/null differ diff --git a/pics/102.二叉树的层序遍历.png b/pics/102.二叉树的层序遍历.png deleted file mode 100644 index dcfcf972..00000000 Binary files a/pics/102.二叉树的层序遍历.png and /dev/null differ diff --git a/pics/104. 二叉树的最大深度.png b/pics/104. 二叉树的最大深度.png deleted file mode 100644 index 74873c76..00000000 Binary files a/pics/104. 二叉树的最大深度.png and /dev/null differ diff --git a/pics/1047.删除字符串中的所有相邻重复项.png b/pics/1047.删除字符串中的所有相邻重复项.png deleted file mode 100644 index 57587100..00000000 Binary files a/pics/1047.删除字符串中的所有相邻重复项.png and /dev/null differ diff --git a/pics/105. 从前序与中序遍历序列构造二叉树.png b/pics/105. 从前序与中序遍历序列构造二叉树.png deleted file mode 100644 index 7430be27..00000000 Binary files a/pics/105. 从前序与中序遍历序列构造二叉树.png and /dev/null differ diff --git a/pics/106. 从中序与后序遍历序列构造二叉树1.png b/pics/106. 从中序与后序遍历序列构造二叉树1.png deleted file mode 100644 index ab8fa4b9..00000000 Binary files a/pics/106. 从中序与后序遍历序列构造二叉树1.png and /dev/null differ diff --git a/pics/106.从中序与后序遍历序列构造二叉树.png b/pics/106.从中序与后序遍历序列构造二叉树.png deleted file mode 100644 index 081a7813..00000000 Binary files a/pics/106.从中序与后序遍历序列构造二叉树.png and /dev/null differ diff --git a/pics/106.从中序与后序遍历序列构造二叉树2.png b/pics/106.从中序与后序遍历序列构造二叉树2.png deleted file mode 100644 index 8726f4ce..00000000 Binary files a/pics/106.从中序与后序遍历序列构造二叉树2.png and /dev/null differ diff --git a/pics/107.二叉树的层次遍历II.png b/pics/107.二叉树的层次遍历II.png deleted file mode 100644 index b4021c44..00000000 Binary files a/pics/107.二叉树的层次遍历II.png and /dev/null differ diff --git a/pics/108.将有序数组转换为二叉搜索树.png b/pics/108.将有序数组转换为二叉搜索树.png deleted file mode 100644 index 7d25f99d..00000000 Binary files a/pics/108.将有序数组转换为二叉搜索树.png and /dev/null differ diff --git a/pics/110.平衡二叉树.png b/pics/110.平衡二叉树.png deleted file mode 100644 index f227f1ad..00000000 Binary files a/pics/110.平衡二叉树.png and /dev/null differ diff --git a/pics/110.平衡二叉树1.png b/pics/110.平衡二叉树1.png deleted file mode 100644 index 36e0a09c..00000000 Binary files a/pics/110.平衡二叉树1.png and /dev/null differ diff --git a/pics/110.平衡二叉树2.png b/pics/110.平衡二叉树2.png deleted file mode 100644 index 53d46be6..00000000 Binary files a/pics/110.平衡二叉树2.png and /dev/null differ diff --git a/pics/111.二叉树的最小深度.png b/pics/111.二叉树的最小深度.png deleted file mode 100644 index b1980df8..00000000 Binary files a/pics/111.二叉树的最小深度.png and /dev/null differ diff --git a/pics/111.二叉树的最小深度1.png b/pics/111.二叉树的最小深度1.png deleted file mode 100644 index a0ac70cb..00000000 Binary files a/pics/111.二叉树的最小深度1.png and /dev/null differ diff --git a/pics/112.路径总和.png b/pics/112.路径总和.png deleted file mode 100644 index 2a1b5100..00000000 Binary files a/pics/112.路径总和.png and /dev/null differ diff --git a/pics/112.路径总和1.png b/pics/112.路径总和1.png deleted file mode 100644 index 4c6c0f60..00000000 Binary files a/pics/112.路径总和1.png and /dev/null differ diff --git a/pics/113.路径总和II.png b/pics/113.路径总和II.png deleted file mode 100644 index 931c2a9a..00000000 Binary files a/pics/113.路径总和II.png and /dev/null differ diff --git a/pics/113.路径总和II1.png b/pics/113.路径总和II1.png deleted file mode 100644 index e1d5a2d1..00000000 Binary files a/pics/113.路径总和II1.png and /dev/null differ diff --git a/pics/116.填充每个节点的下一个右侧节点指针.png b/pics/116.填充每个节点的下一个右侧节点指针.png deleted file mode 100644 index bec25c0a..00000000 Binary files a/pics/116.填充每个节点的下一个右侧节点指针.png and /dev/null differ diff --git a/pics/1207.独一无二的出现次数.png b/pics/1207.独一无二的出现次数.png deleted file mode 100644 index bbb036e6..00000000 Binary files a/pics/1207.独一无二的出现次数.png and /dev/null differ diff --git a/pics/122.买卖股票的最佳时机II.png b/pics/122.买卖股票的最佳时机II.png deleted file mode 100644 index 9799abfd..00000000 Binary files a/pics/122.买卖股票的最佳时机II.png and /dev/null differ diff --git a/pics/127.单词接龙.png b/pics/127.单词接龙.png deleted file mode 100644 index 581bb558..00000000 Binary files a/pics/127.单词接龙.png and /dev/null differ diff --git a/pics/129.求根到叶子节点数字之和.png b/pics/129.求根到叶子节点数字之和.png deleted file mode 100644 index 58288a9d..00000000 Binary files a/pics/129.求根到叶子节点数字之和.png and /dev/null differ diff --git a/pics/131.分割回文串.png b/pics/131.分割回文串.png deleted file mode 100644 index 0b50823f..00000000 Binary files a/pics/131.分割回文串.png and /dev/null differ diff --git a/pics/134.加油站.png b/pics/134.加油站.png deleted file mode 100644 index 120f714d..00000000 Binary files a/pics/134.加油站.png and /dev/null differ diff --git a/pics/135.分发糖果.png b/pics/135.分发糖果.png deleted file mode 100644 index cacd2cbe..00000000 Binary files a/pics/135.分发糖果.png and /dev/null differ diff --git a/pics/135.分发糖果1.png b/pics/135.分发糖果1.png deleted file mode 100644 index 27209df9..00000000 Binary files a/pics/135.分发糖果1.png and /dev/null differ diff --git a/pics/1356.根据数字二进制下1的数目排序.png b/pics/1356.根据数字二进制下1的数目排序.png deleted file mode 100644 index 0fca9fed..00000000 Binary files a/pics/1356.根据数字二进制下1的数目排序.png and /dev/null differ diff --git a/pics/1365.有多少小于当前数字的数字.png b/pics/1365.有多少小于当前数字的数字.png deleted file mode 100644 index 9d67d07b..00000000 Binary files a/pics/1365.有多少小于当前数字的数字.png and /dev/null differ diff --git a/pics/142环形链表1.png b/pics/142环形链表1.png deleted file mode 100644 index 7814be45..00000000 Binary files a/pics/142环形链表1.png and /dev/null differ diff --git a/pics/142环形链表2.png b/pics/142环形链表2.png deleted file mode 100644 index 86294647..00000000 Binary files a/pics/142环形链表2.png and /dev/null differ diff --git a/pics/142环形链表3.png b/pics/142环形链表3.png deleted file mode 100644 index 9bd96f6b..00000000 Binary files a/pics/142环形链表3.png and /dev/null differ diff --git a/pics/142环形链表4.png b/pics/142环形链表4.png deleted file mode 100644 index e37690d8..00000000 Binary files a/pics/142环形链表4.png and /dev/null differ diff --git a/pics/142环形链表5.png b/pics/142环形链表5.png deleted file mode 100644 index b99a79f1..00000000 Binary files a/pics/142环形链表5.png and /dev/null differ diff --git a/pics/143.重排链表.png b/pics/143.重排链表.png deleted file mode 100644 index 2fc94408..00000000 Binary files a/pics/143.重排链表.png and /dev/null differ diff --git a/pics/147.对链表进行插入排序.png b/pics/147.对链表进行插入排序.png deleted file mode 100644 index 807f66f4..00000000 Binary files a/pics/147.对链表进行插入排序.png and /dev/null differ diff --git a/pics/151_翻转字符串里的单词.png b/pics/151_翻转字符串里的单词.png deleted file mode 100644 index dc844950..00000000 Binary files a/pics/151_翻转字符串里的单词.png and /dev/null differ diff --git a/pics/17. 电话号码的字母组合.jpeg b/pics/17. 电话号码的字母组合.jpeg deleted file mode 100644 index f80ec767..00000000 Binary files a/pics/17. 电话号码的字母组合.jpeg and /dev/null differ diff --git a/pics/17. 电话号码的字母组合.png b/pics/17. 电话号码的字母组合.png deleted file mode 100644 index b11f7114..00000000 Binary files a/pics/17. 电话号码的字母组合.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点.png b/pics/19.删除链表的倒数第N个节点.png deleted file mode 100644 index 6f6aa9c5..00000000 Binary files a/pics/19.删除链表的倒数第N个节点.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点1.png b/pics/19.删除链表的倒数第N个节点1.png deleted file mode 100644 index cca947b4..00000000 Binary files a/pics/19.删除链表的倒数第N个节点1.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点2.png b/pics/19.删除链表的倒数第N个节点2.png deleted file mode 100644 index 0d8144cd..00000000 Binary files a/pics/19.删除链表的倒数第N个节点2.png and /dev/null differ diff --git a/pics/19.删除链表的倒数第N个节点3.png b/pics/19.删除链表的倒数第N个节点3.png deleted file mode 100644 index d15d05e7..00000000 Binary files a/pics/19.删除链表的倒数第N个节点3.png and /dev/null differ diff --git a/pics/199.二叉树的右视图.png b/pics/199.二叉树的右视图.png deleted file mode 100644 index e5244117..00000000 Binary files a/pics/199.二叉树的右视图.png and /dev/null differ diff --git a/pics/203_链表删除元素1.png b/pics/203_链表删除元素1.png deleted file mode 100644 index b32aa50e..00000000 Binary files a/pics/203_链表删除元素1.png and /dev/null differ diff --git a/pics/203_链表删除元素2.png b/pics/203_链表删除元素2.png deleted file mode 100644 index 5519a69d..00000000 Binary files a/pics/203_链表删除元素2.png and /dev/null differ diff --git a/pics/203_链表删除元素3.png b/pics/203_链表删除元素3.png deleted file mode 100644 index cd50ce13..00000000 Binary files a/pics/203_链表删除元素3.png and /dev/null differ diff --git a/pics/203_链表删除元素4.png b/pics/203_链表删除元素4.png deleted file mode 100644 index 02aaf115..00000000 Binary files a/pics/203_链表删除元素4.png and /dev/null differ diff --git a/pics/203_链表删除元素5.png b/pics/203_链表删除元素5.png deleted file mode 100644 index e24ad3d3..00000000 Binary files a/pics/203_链表删除元素5.png and /dev/null differ diff --git a/pics/203_链表删除元素6.png b/pics/203_链表删除元素6.png deleted file mode 100644 index 13f9b6d6..00000000 Binary files a/pics/203_链表删除元素6.png and /dev/null differ diff --git a/pics/206_反转链表.png b/pics/206_反转链表.png deleted file mode 100644 index f26ad08f..00000000 Binary files a/pics/206_反转链表.png and /dev/null differ diff --git a/pics/216.组合总和III.png b/pics/216.组合总和III.png deleted file mode 100644 index 1b71e67c..00000000 Binary files a/pics/216.组合总和III.png and /dev/null differ diff --git a/pics/216.组合总和III1.png b/pics/216.组合总和III1.png deleted file mode 100644 index e495191d..00000000 Binary files a/pics/216.组合总和III1.png and /dev/null differ diff --git a/pics/222.完全二叉树的节点个数.png b/pics/222.完全二叉树的节点个数.png deleted file mode 100644 index 2eb1eb94..00000000 Binary files a/pics/222.完全二叉树的节点个数.png and /dev/null differ diff --git a/pics/222.完全二叉树的节点个数1.png b/pics/222.完全二叉树的节点个数1.png deleted file mode 100644 index 49e1772c..00000000 Binary files a/pics/222.完全二叉树的节点个数1.png and /dev/null differ diff --git a/pics/226.翻转二叉树.png b/pics/226.翻转二叉树.png deleted file mode 100644 index 1e3aec91..00000000 Binary files a/pics/226.翻转二叉树.png and /dev/null differ diff --git a/pics/226.翻转二叉树1.png b/pics/226.翻转二叉树1.png deleted file mode 100644 index 9435a12d..00000000 Binary files a/pics/226.翻转二叉树1.png and /dev/null differ diff --git a/pics/234.回文链表.png b/pics/234.回文链表.png deleted file mode 100644 index 285c73bb..00000000 Binary files a/pics/234.回文链表.png and /dev/null differ diff --git a/pics/235.二叉搜索树的最近公共祖先.png b/pics/235.二叉搜索树的最近公共祖先.png deleted file mode 100644 index 277b8165..00000000 Binary files a/pics/235.二叉搜索树的最近公共祖先.png and /dev/null differ diff --git a/pics/236.二叉树的最近公共祖先.png b/pics/236.二叉树的最近公共祖先.png deleted file mode 100644 index 023dd955..00000000 Binary files a/pics/236.二叉树的最近公共祖先.png and /dev/null differ diff --git a/pics/236.二叉树的最近公共祖先1.png b/pics/236.二叉树的最近公共祖先1.png deleted file mode 100644 index 4542ad5c..00000000 Binary files a/pics/236.二叉树的最近公共祖先1.png and /dev/null differ diff --git a/pics/236.二叉树的最近公共祖先2.png b/pics/236.二叉树的最近公共祖先2.png deleted file mode 100644 index 1681fbcc..00000000 Binary files a/pics/236.二叉树的最近公共祖先2.png and /dev/null differ diff --git a/pics/239.滑动窗口最大值.png b/pics/239.滑动窗口最大值.png deleted file mode 100644 index 49734e5d..00000000 Binary files a/pics/239.滑动窗口最大值.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点.png b/pics/24.两两交换链表中的节点.png deleted file mode 100644 index 3051c5a1..00000000 Binary files a/pics/24.两两交换链表中的节点.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点1.png b/pics/24.两两交换链表中的节点1.png deleted file mode 100644 index e79642c3..00000000 Binary files a/pics/24.两两交换链表中的节点1.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点2.png b/pics/24.两两交换链表中的节点2.png deleted file mode 100644 index e6dba912..00000000 Binary files a/pics/24.两两交换链表中的节点2.png and /dev/null differ diff --git a/pics/24.两两交换链表中的节点3.png b/pics/24.两两交换链表中的节点3.png deleted file mode 100644 index 7d706cd2..00000000 Binary files a/pics/24.两两交换链表中的节点3.png and /dev/null differ diff --git a/pics/257.二叉树的所有路径.png b/pics/257.二叉树的所有路径.png deleted file mode 100644 index 0a0683e0..00000000 Binary files a/pics/257.二叉树的所有路径.png and /dev/null differ diff --git a/pics/257.二叉树的所有路径1.png b/pics/257.二叉树的所有路径1.png deleted file mode 100644 index 97452f51..00000000 Binary files a/pics/257.二叉树的所有路径1.png and /dev/null differ diff --git a/pics/26_封面.png b/pics/26_封面.png deleted file mode 100644 index b8a2f1a1..00000000 Binary files a/pics/26_封面.png and /dev/null differ diff --git a/pics/27_封面.png b/pics/27_封面.png deleted file mode 100644 index a4a2b009..00000000 Binary files a/pics/27_封面.png and /dev/null differ diff --git a/pics/31.下一个排列.png b/pics/31.下一个排列.png deleted file mode 100644 index f88ef392..00000000 Binary files a/pics/31.下一个排列.png and /dev/null differ diff --git a/pics/332.重新安排行程.png b/pics/332.重新安排行程.png deleted file mode 100644 index 56f3ab02..00000000 Binary files a/pics/332.重新安排行程.png and /dev/null differ diff --git a/pics/332.重新安排行程1.png b/pics/332.重新安排行程1.png deleted file mode 100644 index 69d51882..00000000 Binary files a/pics/332.重新安排行程1.png and /dev/null differ diff --git a/pics/347.前K个高频元素.png b/pics/347.前K个高频元素.png deleted file mode 100644 index 54b1417d..00000000 Binary files a/pics/347.前K个高频元素.png and /dev/null differ diff --git a/pics/35_封面.png b/pics/35_封面.png deleted file mode 100644 index dc2971b5..00000000 Binary files a/pics/35_封面.png and /dev/null differ diff --git a/pics/35_搜索插入位置.png b/pics/35_搜索插入位置.png deleted file mode 100644 index 5fa65490..00000000 Binary files a/pics/35_搜索插入位置.png and /dev/null differ diff --git a/pics/35_搜索插入位置2.png b/pics/35_搜索插入位置2.png deleted file mode 100644 index 71fca151..00000000 Binary files a/pics/35_搜索插入位置2.png and /dev/null differ diff --git a/pics/35_搜索插入位置3.png b/pics/35_搜索插入位置3.png deleted file mode 100644 index ec1ce1eb..00000000 Binary files a/pics/35_搜索插入位置3.png and /dev/null differ diff --git a/pics/35_搜索插入位置4.png b/pics/35_搜索插入位置4.png deleted file mode 100644 index efb88e22..00000000 Binary files a/pics/35_搜索插入位置4.png and /dev/null differ diff --git a/pics/35_搜索插入位置5.png b/pics/35_搜索插入位置5.png deleted file mode 100644 index ec021c9d..00000000 Binary files a/pics/35_搜索插入位置5.png and /dev/null differ diff --git a/pics/37.解数独.png b/pics/37.解数独.png deleted file mode 100644 index 46b0a15a..00000000 Binary files a/pics/37.解数独.png and /dev/null differ diff --git a/pics/376.摆动序列.png b/pics/376.摆动序列.png deleted file mode 100644 index c6a06dfe..00000000 Binary files a/pics/376.摆动序列.png and /dev/null differ diff --git a/pics/376.摆动序列1.png b/pics/376.摆动序列1.png deleted file mode 100644 index 212e7722..00000000 Binary files a/pics/376.摆动序列1.png and /dev/null differ diff --git a/pics/39.组合总和.png b/pics/39.组合总和.png deleted file mode 100644 index 960cba69..00000000 Binary files a/pics/39.组合总和.png and /dev/null differ diff --git a/pics/39.组合总和1.png b/pics/39.组合总和1.png deleted file mode 100644 index d3557efc..00000000 Binary files a/pics/39.组合总和1.png and /dev/null differ diff --git a/pics/40.组合总和II.png b/pics/40.组合总和II.png deleted file mode 100644 index 104e5f4e..00000000 Binary files a/pics/40.组合总和II.png and /dev/null differ diff --git a/pics/40.组合总和II1.png b/pics/40.组合总和II1.png deleted file mode 100644 index 75fda6b3..00000000 Binary files a/pics/40.组合总和II1.png and /dev/null differ diff --git a/pics/404.左叶子之和.png b/pics/404.左叶子之和.png deleted file mode 100644 index 81b6d8c2..00000000 Binary files a/pics/404.左叶子之和.png and /dev/null differ diff --git a/pics/404.左叶子之和1.png b/pics/404.左叶子之和1.png deleted file mode 100644 index 8c050e04..00000000 Binary files a/pics/404.左叶子之和1.png and /dev/null differ diff --git a/pics/406.根据身高重建队列.png b/pics/406.根据身高重建队列.png deleted file mode 100644 index 9ea8f0b2..00000000 Binary files a/pics/406.根据身高重建队列.png and /dev/null differ diff --git a/pics/416.分割等和子集.png b/pics/416.分割等和子集.png deleted file mode 100644 index 8d1b9ef6..00000000 Binary files a/pics/416.分割等和子集.png and /dev/null differ diff --git a/pics/416.分割等和子集1.png b/pics/416.分割等和子集1.png deleted file mode 100644 index 6be70dc6..00000000 Binary files a/pics/416.分割等和子集1.png and /dev/null differ diff --git a/pics/42.接雨水1.png b/pics/42.接雨水1.png deleted file mode 100644 index 26624f4b..00000000 Binary files a/pics/42.接雨水1.png and /dev/null differ diff --git a/pics/42.接雨水2.png b/pics/42.接雨水2.png deleted file mode 100644 index 94eda97e..00000000 Binary files a/pics/42.接雨水2.png and /dev/null differ diff --git a/pics/42.接雨水3.png b/pics/42.接雨水3.png deleted file mode 100644 index 1e4b528a..00000000 Binary files a/pics/42.接雨水3.png and /dev/null differ diff --git a/pics/42.接雨水4.png b/pics/42.接雨水4.png deleted file mode 100644 index a18539ce..00000000 Binary files a/pics/42.接雨水4.png and /dev/null differ diff --git a/pics/42.接雨水5.png b/pics/42.接雨水5.png deleted file mode 100644 index 066792b0..00000000 Binary files a/pics/42.接雨水5.png and /dev/null differ diff --git a/pics/429. N叉树的层序遍历.png b/pics/429. N叉树的层序遍历.png deleted file mode 100644 index d28c543c..00000000 Binary files a/pics/429. N叉树的层序遍历.png and /dev/null differ diff --git a/pics/435.无重叠区间.png b/pics/435.无重叠区间.png deleted file mode 100644 index a45913c1..00000000 Binary files a/pics/435.无重叠区间.png and /dev/null differ diff --git a/pics/45.跳跃游戏II.png b/pics/45.跳跃游戏II.png deleted file mode 100644 index 77130cb2..00000000 Binary files a/pics/45.跳跃游戏II.png and /dev/null differ diff --git a/pics/45.跳跃游戏II1.png b/pics/45.跳跃游戏II1.png deleted file mode 100644 index 7850187f..00000000 Binary files a/pics/45.跳跃游戏II1.png and /dev/null differ diff --git a/pics/45.跳跃游戏II2.png b/pics/45.跳跃游戏II2.png deleted file mode 100644 index aa45f60a..00000000 Binary files a/pics/45.跳跃游戏II2.png and /dev/null differ diff --git a/pics/452.用最少数量的箭引爆气球.png b/pics/452.用最少数量的箭引爆气球.png deleted file mode 100644 index 64080914..00000000 Binary files a/pics/452.用最少数量的箭引爆气球.png and /dev/null differ diff --git a/pics/455.分发饼干.png b/pics/455.分发饼干.png deleted file mode 100644 index 17e15e09..00000000 Binary files a/pics/455.分发饼干.png and /dev/null differ diff --git a/pics/459.重复的子字符串_1.png b/pics/459.重复的子字符串_1.png deleted file mode 100644 index 2feddd6b..00000000 Binary files a/pics/459.重复的子字符串_1.png and /dev/null differ diff --git a/pics/46.全排列.png b/pics/46.全排列.png deleted file mode 100644 index d8c484ec..00000000 Binary files a/pics/46.全排列.png and /dev/null differ diff --git a/pics/463.岛屿的周长.png b/pics/463.岛屿的周长.png deleted file mode 100644 index 5b0fa013..00000000 Binary files a/pics/463.岛屿的周长.png and /dev/null differ diff --git a/pics/463.岛屿的周长1.png b/pics/463.岛屿的周长1.png deleted file mode 100644 index bb14dbd6..00000000 Binary files a/pics/463.岛屿的周长1.png and /dev/null differ diff --git a/pics/47.全排列II1.png b/pics/47.全排列II1.png deleted file mode 100644 index 8160dd86..00000000 Binary files a/pics/47.全排列II1.png and /dev/null differ diff --git a/pics/47.全排列II2.png b/pics/47.全排列II2.png deleted file mode 100644 index d2e096b1..00000000 Binary files a/pics/47.全排列II2.png and /dev/null differ diff --git a/pics/47.全排列II3.png b/pics/47.全排列II3.png deleted file mode 100644 index b6f5a862..00000000 Binary files a/pics/47.全排列II3.png and /dev/null differ diff --git a/pics/486.预测赢家.png b/pics/486.预测赢家.png deleted file mode 100644 index 03ddfaa8..00000000 Binary files a/pics/486.预测赢家.png and /dev/null differ diff --git a/pics/486.预测赢家1.png b/pics/486.预测赢家1.png deleted file mode 100644 index dbddef26..00000000 Binary files a/pics/486.预测赢家1.png and /dev/null differ diff --git a/pics/486.预测赢家2.png b/pics/486.预测赢家2.png deleted file mode 100644 index 7d26de9a..00000000 Binary files a/pics/486.预测赢家2.png and /dev/null differ diff --git a/pics/486.预测赢家3.png b/pics/486.预测赢家3.png deleted file mode 100644 index 506778c4..00000000 Binary files a/pics/486.预测赢家3.png and /dev/null differ diff --git a/pics/486.预测赢家4.png b/pics/486.预测赢家4.png deleted file mode 100644 index 8a679b74..00000000 Binary files a/pics/486.预测赢家4.png and /dev/null differ diff --git a/pics/491. 递增子序列1.png b/pics/491. 递增子序列1.png deleted file mode 100644 index 19da907e..00000000 Binary files a/pics/491. 递增子序列1.png and /dev/null differ diff --git a/pics/491. 递增子序列2.png b/pics/491. 递增子序列2.png deleted file mode 100644 index 24988010..00000000 Binary files a/pics/491. 递增子序列2.png and /dev/null differ diff --git a/pics/491. 递增子序列3.png b/pics/491. 递增子序列3.png deleted file mode 100644 index df293dd5..00000000 Binary files a/pics/491. 递增子序列3.png and /dev/null differ diff --git a/pics/491. 递增子序列4.png b/pics/491. 递增子序列4.png deleted file mode 100644 index 395fc515..00000000 Binary files a/pics/491. 递增子序列4.png and /dev/null differ diff --git a/pics/501.二叉搜索树中的众数.png b/pics/501.二叉搜索树中的众数.png deleted file mode 100644 index a7197b08..00000000 Binary files a/pics/501.二叉搜索树中的众数.png and /dev/null differ diff --git a/pics/501.二叉搜索树中的众数1.png b/pics/501.二叉搜索树中的众数1.png deleted file mode 100644 index 200bc2fa..00000000 Binary files a/pics/501.二叉搜索树中的众数1.png and /dev/null differ diff --git a/pics/51.N皇后.png b/pics/51.N皇后.png deleted file mode 100644 index 730b7cb8..00000000 Binary files a/pics/51.N皇后.png and /dev/null differ diff --git a/pics/51.N皇后1.png b/pics/51.N皇后1.png deleted file mode 100644 index e6c9c72d..00000000 Binary files a/pics/51.N皇后1.png and /dev/null differ diff --git a/pics/513.找树左下角的值.png b/pics/513.找树左下角的值.png deleted file mode 100644 index 67bda96f..00000000 Binary files a/pics/513.找树左下角的值.png and /dev/null differ diff --git a/pics/513.找树左下角的值1.png b/pics/513.找树左下角的值1.png deleted file mode 100644 index a5421ad6..00000000 Binary files a/pics/513.找树左下角的值1.png and /dev/null differ diff --git a/pics/515.在每个树行中找最大值.png b/pics/515.在每个树行中找最大值.png deleted file mode 100644 index e8f2f431..00000000 Binary files a/pics/515.在每个树行中找最大值.png and /dev/null differ diff --git a/pics/530.二叉搜索树的最小绝对差.png b/pics/530.二叉搜索树的最小绝对差.png deleted file mode 100644 index 04ca9a65..00000000 Binary files a/pics/530.二叉搜索树的最小绝对差.png and /dev/null differ diff --git a/pics/538.把二叉搜索树转换为累加树.png b/pics/538.把二叉搜索树转换为累加树.png deleted file mode 100644 index 27077f7b..00000000 Binary files a/pics/538.把二叉搜索树转换为累加树.png and /dev/null differ diff --git a/pics/541_反转字符串II.png b/pics/541_反转字符串II.png deleted file mode 100644 index 45f0ce6c..00000000 Binary files a/pics/541_反转字符串II.png and /dev/null differ diff --git a/pics/55.跳跃游戏.png b/pics/55.跳跃游戏.png deleted file mode 100644 index 012bb635..00000000 Binary files a/pics/55.跳跃游戏.png and /dev/null differ diff --git a/pics/559.N叉树的最大深度.png b/pics/559.N叉树的最大深度.png deleted file mode 100644 index d28c543c..00000000 Binary files a/pics/559.N叉树的最大深度.png and /dev/null differ diff --git a/pics/56.合并区间.png b/pics/56.合并区间.png deleted file mode 100644 index ff905c72..00000000 Binary files a/pics/56.合并区间.png and /dev/null differ diff --git a/pics/57.插入区间.png b/pics/57.插入区间.png deleted file mode 100644 index 67290167..00000000 Binary files a/pics/57.插入区间.png and /dev/null differ diff --git a/pics/57.插入区间1.png b/pics/57.插入区间1.png deleted file mode 100644 index 69835dee..00000000 Binary files a/pics/57.插入区间1.png and /dev/null differ diff --git a/pics/59_封面.png b/pics/59_封面.png deleted file mode 100644 index 795e8f04..00000000 Binary files a/pics/59_封面.png and /dev/null differ diff --git a/pics/617.合并二叉树.png b/pics/617.合并二叉树.png deleted file mode 100644 index 182f959c..00000000 Binary files a/pics/617.合并二叉树.png and /dev/null differ diff --git a/pics/62.不同路径.png b/pics/62.不同路径.png deleted file mode 100644 index d82c6ed0..00000000 Binary files a/pics/62.不同路径.png and /dev/null differ diff --git a/pics/62.不同路径1.png b/pics/62.不同路径1.png deleted file mode 100644 index 0d84838d..00000000 Binary files a/pics/62.不同路径1.png and /dev/null differ diff --git a/pics/62.不同路径2.png b/pics/62.不同路径2.png deleted file mode 100644 index 91984901..00000000 Binary files a/pics/62.不同路径2.png and /dev/null differ diff --git a/pics/637.二叉树的层平均值.png b/pics/637.二叉树的层平均值.png deleted file mode 100644 index 57018dab..00000000 Binary files a/pics/637.二叉树的层平均值.png and /dev/null differ diff --git a/pics/654.最大二叉树.png b/pics/654.最大二叉树.png deleted file mode 100644 index e8fc63aa..00000000 Binary files a/pics/654.最大二叉树.png and /dev/null differ diff --git a/pics/657.机器人能否返回原点.png b/pics/657.机器人能否返回原点.png deleted file mode 100644 index 6ea5b69b..00000000 Binary files a/pics/657.机器人能否返回原点.png and /dev/null differ diff --git a/pics/669.修剪二叉搜索树.png b/pics/669.修剪二叉搜索树.png deleted file mode 100644 index 89fe4104..00000000 Binary files a/pics/669.修剪二叉搜索树.png and /dev/null differ diff --git a/pics/669.修剪二叉搜索树1.png b/pics/669.修剪二叉搜索树1.png deleted file mode 100644 index 1b46e8d0..00000000 Binary files a/pics/669.修剪二叉搜索树1.png and /dev/null differ diff --git a/pics/685.冗余连接II1.png b/pics/685.冗余连接II1.png deleted file mode 100644 index ab833087..00000000 Binary files a/pics/685.冗余连接II1.png and /dev/null differ diff --git a/pics/685.冗余连接II2.png b/pics/685.冗余连接II2.png deleted file mode 100644 index 6dbb2ac7..00000000 Binary files a/pics/685.冗余连接II2.png and /dev/null differ diff --git a/pics/700.二叉搜索树中的搜索.png b/pics/700.二叉搜索树中的搜索.png deleted file mode 100644 index 1fd15466..00000000 Binary files a/pics/700.二叉搜索树中的搜索.png and /dev/null differ diff --git a/pics/700.二叉搜索树中的搜索1.png b/pics/700.二叉搜索树中的搜索1.png deleted file mode 100644 index 037627b1..00000000 Binary files a/pics/700.二叉搜索树中的搜索1.png and /dev/null differ diff --git a/pics/763.划分字母区间.png b/pics/763.划分字母区间.png deleted file mode 100644 index 0c1da0f3..00000000 Binary files a/pics/763.划分字母区间.png and /dev/null differ diff --git a/pics/77.组合.png b/pics/77.组合.png deleted file mode 100644 index 17cde493..00000000 Binary files a/pics/77.组合.png and /dev/null differ diff --git a/pics/77.组合1.png b/pics/77.组合1.png deleted file mode 100644 index a6a4a272..00000000 Binary files a/pics/77.组合1.png and /dev/null differ diff --git a/pics/77.组合2.png b/pics/77.组合2.png deleted file mode 100644 index 94e305b6..00000000 Binary files a/pics/77.组合2.png and /dev/null differ diff --git a/pics/77.组合3.png b/pics/77.组合3.png deleted file mode 100644 index 4ba73549..00000000 Binary files a/pics/77.组合3.png and /dev/null differ diff --git a/pics/77.组合4.png b/pics/77.组合4.png deleted file mode 100644 index b2519ccd..00000000 Binary files a/pics/77.组合4.png and /dev/null differ diff --git a/pics/78.子集.png b/pics/78.子集.png deleted file mode 100644 index 1700030f..00000000 Binary files a/pics/78.子集.png and /dev/null differ diff --git a/pics/841.钥匙和房间.png b/pics/841.钥匙和房间.png deleted file mode 100644 index 3bfdeea4..00000000 Binary files a/pics/841.钥匙和房间.png and /dev/null differ diff --git a/pics/90.子集II.png b/pics/90.子集II.png deleted file mode 100644 index 972010f6..00000000 Binary files a/pics/90.子集II.png and /dev/null differ diff --git a/pics/90.子集II1.png b/pics/90.子集II1.png deleted file mode 100644 index 92a18238..00000000 Binary files a/pics/90.子集II1.png and /dev/null differ diff --git a/pics/90.子集II2.png b/pics/90.子集II2.png deleted file mode 100644 index e42a56f7..00000000 Binary files a/pics/90.子集II2.png and /dev/null differ diff --git a/pics/925.长按键入.png b/pics/925.长按键入.png deleted file mode 100644 index c6bee552..00000000 Binary files a/pics/925.长按键入.png and /dev/null differ diff --git a/pics/93.复原IP地址.png b/pics/93.复原IP地址.png deleted file mode 100644 index 63d11f2e..00000000 Binary files a/pics/93.复原IP地址.png and /dev/null differ diff --git a/pics/941.有效的山脉数组.png b/pics/941.有效的山脉数组.png deleted file mode 100644 index e261242c..00000000 Binary files a/pics/941.有效的山脉数组.png and /dev/null differ diff --git a/pics/968.监控二叉树1.png b/pics/968.监控二叉树1.png deleted file mode 100644 index 7e6a75df..00000000 Binary files a/pics/968.监控二叉树1.png and /dev/null differ diff --git a/pics/968.监控二叉树2.png b/pics/968.监控二叉树2.png deleted file mode 100644 index 664a656e..00000000 Binary files a/pics/968.监控二叉树2.png and /dev/null differ diff --git a/pics/968.监控二叉树3.png b/pics/968.监控二叉树3.png deleted file mode 100644 index 0a3a9ea6..00000000 Binary files a/pics/968.监控二叉树3.png and /dev/null differ diff --git a/pics/977.有序数组的平方.png b/pics/977.有序数组的平方.png deleted file mode 100644 index cb9ccf6c..00000000 Binary files a/pics/977.有序数组的平方.png and /dev/null differ diff --git a/pics/98.验证二叉搜索树.png b/pics/98.验证二叉搜索树.png deleted file mode 100644 index 8a164140..00000000 Binary files a/pics/98.验证二叉搜索树.png and /dev/null differ diff --git a/pics/leetcode_209.png b/pics/leetcode_209.png deleted file mode 100644 index 2278be41..00000000 Binary files a/pics/leetcode_209.png and /dev/null differ diff --git a/pics/公众号.png b/pics/公众号.png new file mode 100644 index 00000000..eeec00ad Binary files /dev/null and b/pics/公众号.png differ diff --git a/pics/公众号二维码.jpg b/pics/公众号二维码.jpg new file mode 100644 index 00000000..a91b2494 Binary files /dev/null and b/pics/公众号二维码.jpg differ diff --git a/pics/剑指Offer05.替换空格.png b/pics/剑指Offer05.替换空格.png deleted file mode 100644 index e4a2dfbc..00000000 Binary files a/pics/剑指Offer05.替换空格.png and /dev/null differ diff --git a/pics/剑指Offer58-II.左旋转字符串.png b/pics/剑指Offer58-II.左旋转字符串.png deleted file mode 100644 index 9b41754e..00000000 Binary files a/pics/剑指Offer58-II.左旋转字符串.png and /dev/null differ diff --git a/pics/动态规划-背包问题1.png b/pics/动态规划-背包问题1.png deleted file mode 100644 index dd51c5a9..00000000 Binary files a/pics/动态规划-背包问题1.png and /dev/null differ diff --git a/pics/动态规划-背包问题2.png b/pics/动态规划-背包问题2.png deleted file mode 100644 index 6ea1863c..00000000 Binary files a/pics/动态规划-背包问题2.png and /dev/null differ diff --git a/pics/动态规划-背包问题3.png b/pics/动态规划-背包问题3.png deleted file mode 100644 index 568c7bd9..00000000 Binary files a/pics/动态规划-背包问题3.png and /dev/null differ diff --git a/pics/动态规划-背包问题4.png b/pics/动态规划-背包问题4.png deleted file mode 100644 index 97632993..00000000 Binary files a/pics/动态规划-背包问题4.png and /dev/null differ diff --git a/pics/回溯算法理论基础.png b/pics/回溯算法理论基础.png deleted file mode 100644 index f8f2eb2f..00000000 Binary files a/pics/回溯算法理论基础.png and /dev/null differ diff --git a/pics/微信搜一搜.png b/pics/微信搜一搜.png new file mode 100644 index 00000000..62bee447 Binary files /dev/null and b/pics/微信搜一搜.png differ diff --git a/pics/我要打十个.gif b/pics/我要打十个.gif deleted file mode 100644 index c64eb70a..00000000 Binary files a/pics/我要打十个.gif and /dev/null differ diff --git a/pics/知识星球.png b/pics/知识星球.png new file mode 100644 index 00000000..43a5c6b3 Binary files /dev/null and b/pics/知识星球.png differ diff --git a/pics/算法大纲.png b/pics/算法大纲.png new file mode 100644 index 00000000..602f1d03 Binary files /dev/null and b/pics/算法大纲.png differ diff --git a/pics/螺旋矩阵.png b/pics/螺旋矩阵.png deleted file mode 100644 index 5148db46..00000000 Binary files a/pics/螺旋矩阵.png and /dev/null differ diff --git a/pics/面试题02.07.链表相交_1.png b/pics/面试题02.07.链表相交_1.png deleted file mode 100644 index 678311d1..00000000 Binary files a/pics/面试题02.07.链表相交_1.png and /dev/null differ diff --git a/pics/面试题02.07.链表相交_2.png b/pics/面试题02.07.链表相交_2.png deleted file mode 100644 index 97881bec..00000000 Binary files a/pics/面试题02.07.链表相交_2.png and /dev/null differ diff --git a/problems/0001.两数之和.md b/problems/0001.两数之和.md index b7c9831b..6c9d99dd 100644 --- a/problems/0001.两数之和.md +++ b/problems/0001.两数之和.md @@ -1,9 +1,15 @@ -# 题目地址 -https://leetcode-cn.com/problems/two-sum/ +

+ + + + +

+

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

-> 只用数组和set还是不够的! -# 第1题. 两数之和 +## 1. 两数之和 + +https://leetcode-cn.com/problems/two-sum/ 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那 两个 整数,并返回他们的数组下标。 @@ -18,7 +24,7 @@ https://leetcode-cn.com/problems/two-sum/ 所以返回 [0, 1] -# 思路 +## 思路 很明显暴力的解法是两层for循环查找,时间复杂度是O(n^2)。 @@ -51,11 +57,11 @@ std::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底 解题思路动画如下: - + -# C++代码 +C++代码: -``` +```C++ class Solution { public: vector twoSum(vector& nums, int target) { @@ -72,66 +78,55 @@ public: }; ``` -## 一般解法 -代码: -```C++ + +## 其他语言版本 + + +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]; + if(map.containsKey(temp)){ + res[1] = i; + res[0] = map.get(temp); + } + map.put(nums[i], i); + } + return res; +} ``` -## 优化解法 +Python: -```C++ -class Solution { -public: - vector twoSum(vector& nums, int target) { - for (int i = 0; i < nums.size(); i ++) { - for (int j = i + 1; j < nums.size(); j++) { - 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 {}; } -}; - + return []int{} +} ``` -``` -class Solution { -public: - vector twoSum(vector& nums, int target) { - std::map map; - for(int i = 0; i < nums.size(); i++) { - auto iter = map.find(target - nums[i]); - if(iter != map.end()) { - return {iter->second,i}; - } - map.insert({nums, i}); - } - return {}; - } -}; -``` -``` -class Solution { -public: - vector twoSum(vector& nums, int target) { - std::unordered_map map; - for(int i = 0; i < nums.size(); i++) { - auto iter = map.find(target - nums[i]); - if(iter != map.end()) { - return {iter->second, i}; - break; - } - map.emplace(nums[i], i); - } - return {}; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +----------------------- +* 作者微信:[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw) +* B站视频:[代码随想录](https://space.bilibili.com/525438321) +* 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ) +
\ No newline at end of file diff --git a/problems/0015.三数之和.md b/problems/0015.三数之和.md index c437cadb..55e22887 100644 --- a/problems/0015.三数之和.md +++ b/problems/0015.三数之和.md @@ -1,10 +1,19 @@ -# 题目地址 -https://leetcode-cn.com/problems/3sum/ +

+ + + + +

+

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

+ + > 用哈希表解决了[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ),那么三数之和呢? # 第15题. 三数之和 +https://leetcode-cn.com/problems/3sum/ + 给你一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?请你找出所有满足条件且不重复的三元组。 **注意:** 答案中不可以包含重复的三元组。 @@ -20,7 +29,7 @@ https://leetcode-cn.com/problems/3sum/ ] -# 思路 +# 思路 **注意[0, 0, 0, 0] 这组数据** @@ -36,14 +45,14 @@ https://leetcode-cn.com/problems/3sum/ 大家可以尝试使用哈希法写一写,就知道其困难的程度了。 -## 哈希法C++代码 -``` +哈希法C++代码: +```C++ class Solution { public: vector> threeSum(vector& nums) { vector> result; sort(nums.begin(), nums.end()); - // 找出a + b + c = 0 + // 找出a + b + c = 0 // a = nums[i], b = nums[j], c = -(a + b) for (int i = 0; i < nums.size(); i++) { // 排序之后如果第一个元素已经大于零,那么不可能凑成三元组 @@ -84,22 +93,22 @@ public: 动画效果如下: - +![15.三数之和](https://code-thinking.cdn.bcebos.com/gifs/15.%E4%B8%89%E6%95%B0%E4%B9%8B%E5%92%8C.gif) -拿这个nums数组来举例,首先将数组排序,然后有一层for循环,i从下表0的地方开始,同时定一个下表left 定义在i+1的位置上,定义下表right 在数组结尾的位置上。 +拿这个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下表就应该向左移动,这样才能让三数之和小一些。 +接下来如何移动left 和right呢, 如果nums[i] + nums[left] + nums[right] > 0 就说明 此时三数之和大了,因为数组是排序后了,所以right下表就应该向左移动,这样才能让三数之和小一些。 -如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。 +如果 nums[i] + nums[left] + nums[right] < 0 说明 此时 三数之和小了,left 就向右移动,才能让三数之和大一些,直到left与right相遇为止。 时间复杂度:O(n^2)。 ## 双指针法C++代码 -``` +```C++ class Solution { public: vector> threeSum(vector& nums) { @@ -152,9 +161,9 @@ public: }; ``` -# 思考题 +# 思考题 -既然三数之和可以使用双指针法,我们之前讲过的[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ),可不可以使用双指针法呢? +既然三数之和可以使用双指针法,我们之前讲过的[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ),可不可以使用双指针法呢? 如果不能,题意如何更改就可以使用双指针法呢? **大家留言说出自己的想法吧!** @@ -162,5 +171,62 @@ public: 如果[两数之和](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ)要求返回的是数值的话,就可以使用双指针法了。 -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: +```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; + } + + if (i > 0 && nums[i] == nums[i - 1]) { + 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])); + + while (right > left && nums[right] == nums[right - 1]) right--; + while (right > left && nums[left] == nums[left + 1]) left++; + + right--; + left++; + } + } + } + return result; + } +} +``` + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0017.电话号码的字母组合.md b/problems/0017.电话号码的字母组合.md index e6d28496..6f51a181 100644 --- a/problems/0017.电话号码的字母组合.md +++ b/problems/0017.电话号码的字母组合.md @@ -1,5 +1,11 @@ +

+ + + + +

+

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

-> 多个集合求组合问题。 # 17.电话号码的字母组合 @@ -9,29 +15,29 @@ 给出数字到字母的映射如下(与电话按键相同)。注意 1 不对应任何字母。 -![17.电话号码的字母组合](https://img-blog.csdnimg.cn/2020102916424043.png) +![17.电话号码的字母组合](https://img-blog.csdnimg.cn/2020102916424043.png) -示例: -输入:"23" -输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. +示例: +输入:"23" +输出:["ad", "ae", "af", "bd", "be", "bf", "cd", "ce", "cf"]. 说明:尽管上面的答案是按字典序排列的,但是你可以任意选择答案输出的顺序。 -# 思路 +# 思路 从示例上来说,输入"23",最直接的想法就是两层for循环遍历了吧,正好把组合的情况都输出了。 -如果输入"233"呢,那么就三层for循环,如果"2333"呢,就四层for循环....... +如果输入"233"呢,那么就三层for循环,如果"2333"呢,就四层for循环....... 大家应该感觉出和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)遇到的一样的问题,就是这for循环的层数如何写出来,此时又是回溯法登场的时候了。 理解本题后,要解决如下三个问题: -1. 数字和字母如何映射 -2. 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来 -3. 输入1 * #按键等等异常情况 +1. 数字和字母如何映射 +2. 两个字母就两个for循环,三个字符我就三个for循环,以此类推,然后发现代码根本写不出来 +3. 输入1 * #按键等等异常情况 -## 数字和字母如何映射 +## 数字和字母如何映射 可以使用map或者定义一个二位数组,例如:string letterMap[10],来做映射,我这里定义一个二维数组,代码如下: @@ -63,7 +69,7 @@ const string letterMap[10] = { 回溯三部曲: -* 确定回溯函数参数 +* 确定回溯函数参数 首先需要一个字符串s来收集叶子节点的结果,然后用一个字符串数组result保存起来,这两个变量我依然定义为全局。 @@ -78,10 +84,10 @@ const string letterMap[10] = { ``` vector result; string s; -void backtracking(const string& digits, int index) +void backtracking(const string& digits, int index) ``` -* 确定终止条件 +* 确定终止条件 例如输入用例"23",两个数字,那么根节点往下递归两层就可以了,叶子节点就是要收集的结果集。 @@ -119,14 +125,15 @@ for (int i = 0; i < letters.size(); i++) { **因为本题每一个数字代表的是不同集合,也就是求不同集合之间的组合,而[77. 组合](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[216.组合总和III](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)都是是求同一个集合中的组合!** -## 输入1 * #按键等等异常情况 +注意:输入1 * #按键等等异常情况 代码中最好考虑这些异常情况,但题目的测试数据中应该没有异常情况的数据,所以我就没有加了。 **但是要知道会有这些异常,如果是现场面试中,一定要考虑到!** -# C++代码 +## C++代码 + 关键地方都讲完了,按照[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中的回溯法模板,不难写出如下C++代码: @@ -217,7 +224,7 @@ public: }; ``` -我不建议把回溯藏在递归的参数里这种写法,很不直观,我在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)这篇文章中也深度分析了,回溯隐藏在了哪里。 +我不建议把回溯藏在递归的参数里这种写法,很不直观,我在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)这篇文章中也深度分析了,回溯隐藏在了哪里。 所以大家可以按照版本一来写就可以了。 @@ -227,9 +234,24 @@ public: 其实本题不算难,但也处处是细节,大家还要自己亲自动手写一写。 -**就酱,如果学到了,就帮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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0018.四数之和.md b/problems/0018.四数之和.md index 34430d1c..ff441bf7 100644 --- a/problems/0018.四数之和.md +++ b/problems/0018.四数之和.md @@ -1,11 +1,18 @@ -# 题目地址 -https://leetcode-cn.com/problems/4sum/ +

+ + + + +

+

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

+ > 一样的道理,能解决四数之和 - > 那么五数之和、六数之和、N数之和呢? -# 第18题. 四数之和 +# 第18题. 四数之和 + +https://leetcode-cn.com/problems/4sum/ 题意:给定一个包含 n 个整数的数组 nums 和一个目标值 target,判断 nums 中是否存在四个元素 a,b,c 和 d ,使得 a + b + c + d 的值与 target 相等?找出所有满足条件且不重复的四元组。 @@ -13,26 +20,26 @@ https://leetcode-cn.com/problems/4sum/ 答案中不可以包含重复的四元组。 -示例: -给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 -满足要求的四元组集合为: +示例: +给定数组 nums = [1, 0, -1, 0, -2, 2],和 target = 0。 +满足要求的四元组集合为: [ [-1, 0, 0, 1], [-2, -1, 1, 2], [-2, 0, 0, 2] ] -# 思路 +# 思路 -四数之和,和[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)是一个思路,都是使用双指针法, 基本解法就是在[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) 的基础上再套一层for循环。 +四数之和,和[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)是一个思路,都是使用双指针法, 基本解法就是在[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) 的基础上再套一层for循环。 但是有一些细节需要注意,例如: 不要判断`nums[k] > target` 就返回了,三数之和 可以通过 `nums[i] > 0` 就返回了,因为 0 已经是确定的数了,四数之和这道题目 target是任意值。(大家亲自写代码就能感受出来) [三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)的双指针解法是一层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) 。 +四数之和的双指针解法是两层for循环nums[k] + nums[i]为确定值,依然是循环内有left和right下表作为双指针,找出nums[k] + nums[i] + nums[left] + nums[right] == target的情况,三数之和的时间复杂度是O(n^2),四数之和的时间复杂度是O(n^3) 。 -那么一样的道理,五数之和、六数之和等等都采用这种解法。 +那么一样的道理,五数之和、六数之和等等都采用这种解法。 对于[三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A)双指针法就是将原本暴力O(n^3)的解法,降为O(n^2)的解法,四数之和的双指针解法就是将原本暴力O(n^4)的解法,降为O(n^3)的解法。 @@ -57,15 +64,16 @@ https://leetcode-cn.com/problems/4sum/ 双指针法在数组和链表中还有很多应用,后面还会介绍到。 -# C++代码 -``` +C++代码 + +```C++ class Solution { public: vector> fourSum(vector& nums, int target) { vector> result; sort(nums.begin(), nums.end()); for (int k = 0; k < nums.size(); k++) { - // 这种剪枝是错误的,这道题目target 是任意值 + // 这种剪枝是错误的,这道题目target 是任意值 // if (nums[k] > target) { // return result; // } @@ -104,5 +112,68 @@ public: }; ``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + + +## 其他语言版本 + + +Java: +```Java +class Solution { + public List> fourSum(int[] nums, int target) { + List> result = new ArrayList<>(); + Arrays.sort(nums); + + for (int i = 0; i < nums.length; i++) { + + if (i > 0 && nums[i - 1] == nums[i]) { + continue; + } + + for (int j = i + 1; j < nums.length; j++) { + + if (j > i + 1 && nums[j - 1] == nums[j]) { + continue; + } + + int left = j + 1; + int right = nums.length - 1; + while (right > left) { + int sum = nums[i] + nums[j] + nums[left] + nums[right]; + if (sum > target) { + right--; + } else if (sum < target) { + left++; + } else { + result.add(Arrays.asList(nums[i], nums[j], nums[left], nums[right])); + + while (right > left && nums[right] == nums[right - 1]) right--; + while (right > left && nums[left] == nums[left + 1]) left++; + + left++; + right--; + } + } + } + } + return result; + } +} +``` + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0019.删除链表的倒数第N个节点.md b/problems/0019.删除链表的倒数第N个节点.md index b332c9de..3b6dde1e 100644 --- a/problems/0019.删除链表的倒数第N个节点.md +++ b/problems/0019.删除链表的倒数第N个节点.md @@ -1,6 +1,39 @@ +

+ + + + +

+

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

-## 思路 + + +## 19.删除链表的倒数第N个节点 + +题目链接:https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/ + +给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。 + +进阶:你能尝试使用一趟扫描实现吗? + +示例 1: + +![19.删除链表的倒数第N个节点](https://img-blog.csdnimg.cn/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] + + +## 思路 双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。 @@ -8,25 +41,24 @@ 分为如下几步: -* 首先这里我推荐大家使用虚拟头结点,这样方面处理删除实际头结点的逻辑,如果虚拟头结点不清楚,可以看这篇: [链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) - +* 首先这里我推荐大家使用虚拟头结点,这样方面处理删除实际头结点的逻辑,如果虚拟头结点不清楚,可以看这篇: [链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/L5aanfALdLEwVWGvyXPDqA) * 定义fast指针和slow指针,初始值为虚拟头结点,如图: - + * fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图: - + -* fast和slow同时移动,之道fast指向末尾,如题: - +* fast和slow同时移动,之道fast指向末尾,如题: + * 删除slow指向的下一个节点,如图: - + 此时不难写出如下C++代码: -``` +```C++ class Solution { public: ListNode* removeNthFromEnd(ListNode* head, int n) { @@ -47,3 +79,43 @@ public: } }; ``` + + +## 其他语言版本 + +java: + +```java +class Solution { + public ListNode removeNthFromEnd(ListNode head, int n) { + ListNode dummy = new ListNode(-1); + dummy.next = head; + + ListNode slow = dummy; + ListNode fast = dummy; + while (n-- > 0) { + fast = fast.next; + } + // 记住 待删除节点slow 的上一节点 + ListNode prev = null; + while (fast != null) { + prev = slow; + slow = slow.next; + fast = fast.next; + } + // 上一节点的next指针绕过 待删除节点slow 直接指向slow的下一节点 + prev.next = slow.next; + // 释放 待删除节点slow 的next指针, 这句删掉也能AC + slow.next = null; + + return dummy.next; + } +} +``` + + +----------------------- +* 作者微信:[程序员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/0020.有效的括号.md b/problems/0020.有效的括号.md index 293c53dd..e8784397 100644 --- a/problems/0020.有效的括号.md +++ b/problems/0020.有效的括号.md @@ -1,11 +1,18 @@ +

+ + + + +

+

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

-## 题目地址 -https://leetcode-cn.com/problems/valid-parentheses/ > 数据结构与算法应用往往隐藏在我们看不到的地方 -# 20. 有效的括号 +# 20. 有效的括号 + +https://leetcode-cn.com/problems/valid-parentheses/ 给定一个只包括 '(',')','{','}','[',']' 的字符串,判断字符串是否有效。 @@ -14,27 +21,27 @@ https://leetcode-cn.com/problems/valid-parentheses/ * 左括号必须以正确的顺序闭合。 * 注意空字符串可被认为是有效字符串。 -示例 1: -输入: "()" +示例 1: +输入: "()" 输出: true -示例 2: -输入: "()[]{}" -输出: true +示例 2: +输入: "()[]{}" +输出: true -示例 3: -输入: "(]" -输出: false +示例 3: +输入: "(]" +输出: false -示例 4: -输入: "([)]" -输出: false +示例 4: +输入: "([)]" +输出: false -示例 5: -输入: "{[]}" -输出: true +示例 5: +输入: "{[]}" +输出: true -# 思路 +# 思路 ## 题外话 @@ -52,9 +59,9 @@ cd a/b/c/../../ 这个命令最后进入a目录,系统是如何知道进入了a目录呢 ,这就是栈的应用(其实可以出一道相应的面试题了) -所以栈在计算机领域中应用是非常广泛的。 +所以栈在计算机领域中应用是非常广泛的。 -有的同学经常会想学的这些数据结构有什么用,也开发不了什么软件,大多数同学说的软件应该都是可视化的软件例如APP、网站之类的,那都是非常上层的应用了,底层很多功能的实现都是基础的数据结构和算法。 +有的同学经常会想学的这些数据结构有什么用,也开发不了什么软件,大多数同学说的软件应该都是可视化的软件例如APP、网站之类的,那都是非常上层的应用了,底层很多功能的实现都是基础的数据结构和算法。 **所以数据结构与算法的应用往往隐藏在我们看不到的地方!** @@ -66,9 +73,9 @@ cd a/b/c/../../ 首先要弄清楚,字符串里的括号不匹配有几种情况。 -**一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越远。** +**一些同学,在面试中看到这种题目上来就开始写代码,然后就越写越乱。** -建议要写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。 +建议要写代码之前要分析好有哪几种不匹配的情况,如果不动手之前分析好,写出的代码也会有很多问题。 先来分析一下 这里有三种不匹配的情况, @@ -83,7 +90,7 @@ cd a/b/c/../../ 动画如下: - +![20.有效括号](https://code-thinking.cdn.bcebos.com/gifs/20.%E6%9C%89%E6%95%88%E6%8B%AC%E5%8F%B7.gif) 第一种情况:已经遍历完了字符串,但是栈不为空,说明有相应的左括号没有右括号来匹配,所以return false @@ -98,12 +105,10 @@ cd a/b/c/../../ 但还有一些技巧,在匹配左括号的时候,右括号先入栈,就只需要比较当前元素和栈顶相不相等就可以了,比左括号先入栈代码实现要简单的多了! -实现代码如下: - -## C++代码 +实现C++代码如下: -``` +```C++ class Solution { public: bool isValid(string s) { @@ -124,5 +129,27 @@ public: ``` 技巧性的东西没有固定的学习方法,还是要多看多练,自己总灵活运用了。 -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0021.合并两个有序链表.md b/problems/0021.合并两个有序链表.md deleted file mode 100644 index 0272272a..00000000 --- a/problems/0021.合并两个有序链表.md +++ /dev/null @@ -1,60 +0,0 @@ -> 更多精彩文章持续更新,微信搜索「代码随想录」第一时间围观,本文[https://github.com/youngyangyang04/TechCPP](https://github.com/youngyangyang04/TechCPP) 已经收录,里面有更多干货等着你,欢迎Star! - -## 题目地址 -https://leetcode-cn.com/problems/merge-two-sorted-lists/ - -## 思路 - -链表的基本操作,一下代码中有详细注释 - - -## 代码 - -``` -/** - * Definition for singly-linked list. - * struct ListNode { - * int val; - * ListNode *next; - * ListNode() : val(0), next(nullptr) {} - * ListNode(int x) : val(x), next(nullptr) {} - * ListNode(int x, ListNode *next) : val(x), next(next) {} - * }; - */ -// 内存消耗比较大,尝试一下 使用题目中的链表 -class Solution { -public: -// 题目中我们为什么定义了一个 新的head - ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) { - ListNode* head = new ListNode(); - ListNode* index = head; - while(l1 != nullptr && l2 != nullptr) { // 注意null 和nullptr的区别 - if (l1->val < l2->val) { - index->next = l1; - l1 = l1->next; - } else { - index->next = l2; - l2 = l2->next; - } - index = index->next; - } - if (l1 != nullptr) { - index->next = l1; - l1 = l1->next; - } - if (l2 != nullptr) { - index->next = l2; - l2 = l2->next; - } - ListNode* tmp = head; // 注意清理内存,不清理也没事 - head = head->next; - delete tmp; - return head; - // return head->next - - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0024.两两交换链表中的节点.md b/problems/0024.两两交换链表中的节点.md deleted file mode 100644 index 92f9b294..00000000 --- a/problems/0024.两两交换链表中的节点.md +++ /dev/null @@ -1,70 +0,0 @@ - - -## 题目地址 - -## 思路 - -这道题目正常模拟就可以了。 - -建议使用虚拟头结点,这样会方便很多,要不然每次针对头结点(没有前一个指针指向头结点),还要单独处理。 - -对虚拟头结点的操作,还不熟悉的话,可以看这篇[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)。 - -接下来就是交换相邻两个元素了,**此时一定要画图,不画图,操作多个指针很容易乱,而且要操作的先后顺序** - -初始时,cur指向虚拟头结点,然后进行如下三步: - - - -操作之后,链表如下: - - - - -看这个可能就更直观一些了: - - - - -对应的C++代码实现如下: - -``` -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移动两位,准备下一轮交换 - } - return dummyHead->next; - } -}; -``` -时间复杂度:O(n) -空间复杂度:O(1) - -## 拓展 - -**这里还是说一下,大家不必太在意leetcode上执行用时,打败多少多少用户,这个就是一个玩具,非常不准确。** - -做题的时候自己能分析出来时间复杂度就可以了,至于leetcode上执行用时,大概看一下就行。 - -上面的代码我第一次提交执行用时8ms,打败6.5%的用户,差点吓到我了。 - -心想应该没有更好的方法了吧,也就O(n)的时间复杂度,重复提交几次,这样了: - - - -所以,不必过于在意leetcode上这个统计。 - - - diff --git a/problems/0026.删除排序数组中的重复项.md b/problems/0026.删除排序数组中的重复项.md deleted file mode 100644 index 114f2da2..00000000 --- a/problems/0026.删除排序数组中的重复项.md +++ /dev/null @@ -1,49 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/ - -# 思路 - -此题使用双指针法,O(n)的时间复杂度,拼速度的话,可以剪剪枝。 - -注意题目中:不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。 - -双指针法,动画如下: - - - -其实**双指针法在在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法,可以将时间复杂度O(n^2)的解法优化为 O(n)的解法,例如:** - -* [0015.三数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0015.三数之和.md) -* [0018.四数之和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0018.四数之和.md) -* [0026.删除排序数组中的重复项](https://github.com/youngyangyang04/leetcode/blob/master/problems/0026.删除排序数组中的重复项.md) -* [0206.翻转链表](https://github.com/youngyangyang04/leetcode/blob/master/problems/0206.翻转链表.md) -* [0344.反转字符串](https://github.com/youngyangyang04/leetcode/blob/master/problems/0344.反转字符串.md) -* [剑指Offer05.替换空格](https://github.com/youngyangyang04/leetcode/blob/master/problems/剑指Offer05.替换空格.md) - -**还有链表找环,也用到双指针:** - -* [0142.环形链表II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0142.环形链表II.md) - -大家都可以去做一做,感受一下双指针法的内在逻辑! - - -# C++ 代码 - - -``` -class Solution { -public: - int removeDuplicates(vector& nums) { - if (nums.empty()) return 0; // 别忘记空数组的判断 - int slowIndex = 0; - for (int fastIndex = 0; fastIndex < (nums.size() - 1); fastIndex++){ - if(nums[fastIndex] != nums[fastIndex + 1]) { // 发现和后一个不相同 - nums[++slowIndex] = nums[fastIndex + 1]; //slowIndex = 0 的数据一定是不重复的,所以直接 ++slowIndex - } - } - return slowIndex + 1; //别忘了slowIndex是从0开始的,所以返回slowIndex + 1 - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0027.移除元素.md b/problems/0027.移除元素.md index 637bd5ad..5cfdb68d 100644 --- a/problems/0027.移除元素.md +++ b/problems/0027.移除元素.md @@ -1,18 +1,13 @@ -

- -

- - + + - -

+

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

-> 移除元素想要高效的话,不是很简单! -# 编号:27. 移除元素 +## 27. 移除元素 题目地址:https://leetcode-cn.com/problems/remove-element/ @@ -22,38 +17,38 @@ 元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。 -示例 1: -给定 nums = [3,2,2,3], val = 3, -函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。 -你不需要考虑数组中超出新长度后面的元素。 +示例 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。 +示例 2: +给定 nums = [0,1,2,2,3,0,4,2], val = 2, +函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。 **你不需要考虑数组中超出新长度后面的元素。** -# 思路 +## 思路 有的同学可能说了,多余的元素,删掉不就得了。 -**要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。** +**要知道数组的元素在内存地址中是连续的,不能单独删除数组中的某个元素,只能覆盖。** -数组的基础知识可以看这里[程序员算法面试中,必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg)。 +数组的基础知识可以看这里[程序员算法面试中,必须掌握的数组理论知识](https://mp.weixin.qq.com/s/c2KABb-Qgg66HrGf8z-8Og)。 -# 暴力解法 +### 暴力解法 -这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。 +这个题目暴力的解法就是两层for循环,一个for循环遍历数组元素 ,第二个for循环更新数组。 删除过程如下: - +![27.移除元素-暴力解法](https://tva1.sinaimg.cn/large/008eGmZEly1gntrc7x9tjg30du09m1ky.gif) 很明显暴力解法的时间复杂度是O(n^2),这道题目暴力解法在leetcode上是可以过的。 -# 暴力解法C++代码 +代码如下: -``` +```C++ // 时间复杂度:O(n^2) // 空间复杂度:O(1) class Solution { @@ -75,53 +70,84 @@ public: }; ``` -# 双指针法 +* 时间复杂度:$O(n^2)$ +* 空间复杂度:$O(1)$ + +### 双指针法 双指针法(快慢指针法): **通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** 删除过程如下: - +![27.移除元素-双指针法](https://tva1.sinaimg.cn/large/008eGmZEly1gntrds6r59g30du09mnpd.gif) -**双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。** +**双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组、链表、字符串等操作的面试题,都使用双指针法。** -我们来回顾一下,之前已经讲过有四道题目使用了双指针法。 +后序都会一一介绍到,本题代码如下: -双指针法将时间复杂度O(n^2)的解法优化为 O(n)的解法。也就是降一个数量级,题目如下: - -* [15.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) -* [18.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - -双指针来记录前后指针实现链表反转: - -* [206.反转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) - -使用双指针来确定有环: - -* [142题.环形链表II](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) - -双指针法在数组和链表中还有很多应用,后面还会介绍到。 - -# 双指针法C++代码: -``` +```C++ // 时间复杂度: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]; + int slowIndex = 0; + for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { + if (val != nums[fastIndex]) { + nums[slowIndex++] = nums[fastIndex]; } } return slowIndex; } }; ``` +注意这些实现方法并没有改变元素的相对位置! -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** +* 时间复杂度:$O(n)$ +* 空间复杂度:$O(1)$ -

- -

+旧文链接:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) + +## 相关题目推荐 + +* 26.删除排序数组中的重复项 +* 283.移动零 +* 844.比较含退格的字符串 +* 977.有序数组的平方 + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + +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; +}; +``` + +----------------------- +* 作者微信:[程序员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/0028.实现strStr().md b/problems/0028.实现strStr.md similarity index 69% rename from problems/0028.实现strStr().md rename to problems/0028.实现strStr.md index f6e689d7..f1de00f8 100644 --- a/problems/0028.实现strStr().md +++ b/problems/0028.实现strStr.md @@ -1,29 +1,36 @@ +

+ + + + +

+

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

+ + +> 在一个串中查找是否出现过另一个串,这是KMP的看家本领。 + +# 28. 实现 strStr() -## 题目地址 https://leetcode-cn.com/problems/implement-strstr/ -> 在一个串中查找是否出现过另一个串,这是KMP的看家本领。 - -# 题目:28. 实现 strStr() - 实现 strStr() 函数。 给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。 -示例 1: -输入: haystack = "hello", needle = "ll" -输出: 2 +示例 1: +输入: haystack = "hello", needle = "ll" +输出: 2 -示例 2: -输入: haystack = "aaaaa", needle = "bba" -输出: -1 +示例 2: +输入: haystack = "aaaaa", needle = "bba" +输出: -1 -说明: -当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 -对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。 +说明: +当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 +对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。 -# 思路 +# 思路 本题是KMP 经典题目。 @@ -108,7 +115,7 @@ next数组就是一个前缀表(prefix table)。 如动画所示: - +![KMP详解1](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B21.gif) 动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说道。 @@ -120,22 +127,45 @@ next数组就是一个前缀表(prefix table)。 首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。 -那么什么是前缀表:**记录下表i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** +那么什么是前缀表:**记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** + +# 最长公共前后缀? + +文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串; + +后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。 + +**正确理解什么是前缀什么是后缀很重要。** + +那么网上清一色都说 “kmp 最长公共前后缀” 又是什么回事呢? + + +我查了一遍 算法导论 和 算法4里KMP的章节,都没有提到 “最长公共前后缀”这个词,也不知道从哪里来了,我理解是用“最长相等前后缀” 准确一些。 + +**因为前缀表要求的就是相同前后缀的长度。** + +而最长公共前后缀里面的“公共”,更像是说前缀和后缀公共的长度。这其实并不是前缀表所需要的。 + +所以字符串a的最长相等前后缀为0。 +字符串aa的最长相等前后缀为1。 +字符串aaa的最长相等前后缀为2。 +等等.....。 + # 为什么一定要用前缀表 这就是前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢? -回顾一下,刚刚匹配的过程在下表5的地方遇到不匹配,模式串是指向f,如图: - +回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图: +KMP精讲1 -然后就找到了下表2,指向b,继续匹配:如图: - +然后就找到了下标2,指向b,继续匹配:如图: +KMP精讲2 以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! -**下表5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。** +**下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。** 所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 @@ -147,14 +177,14 @@ next数组就是一个前缀表(prefix table)。 如图: - +KMP精讲5 长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。) - +KMP精讲6 长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。 - +KMP精讲7 长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。 以此类推: @@ -163,13 +193,13 @@ next数组就是一个前缀表(prefix table)。 长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。 那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: - +KMP精讲8 -可以看出模式串与前缀表对应位置的数字表示的就是:**下表i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** +可以看出模式串与前缀表对应位置的数字表示的就是:**下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** 再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示: - +![KMP精讲2](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B22.gif) 找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。 @@ -177,7 +207,7 @@ next数组就是一个前缀表(prefix table)。 所以要看前一位的 前缀表的数值。 -前一个字符的前缀表的数值是2, 所有把下表移动到下表2的位置继续比配。 可以再反复看一下上面的动画。 +前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。 最后就在文本串中找到了和模式串匹配的子串了。 @@ -203,11 +233,10 @@ next数组就可以是前缀表,但是很多实现都是把前缀表统一减 匹配过程动画如下: - +![KMP精讲4](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B24.gif) # 时间复杂度分析 - 其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。 暴力的解法显而易见是O(n * m),所以**KMP在字符串匹配中极大的提高的搜索的效率。** @@ -234,7 +263,7 @@ void getNext(int* next, const string& s) 1. 初始化: -定义两个指针i和j,j指向前缀终止位置(严格来说是终止位置减一的位置),i指向后缀终止位置(与j同理)。 +定义两个指针i和j,j指向前缀起始位置,i指向后缀起始位置。 然后还要对next数组进行初始化赋值,如下: @@ -255,15 +284,15 @@ next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是 因为j初始化为-1,那么i就从1开始,进行s[i] 与 s[j+1]的比较。 -所以遍历模式串s的循环下表i 要从 1开始,代码如下: +所以遍历模式串s的循环下标i 要从 1开始,代码如下: ``` for(int i = 1; i < s.size(); i++) { ``` -如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回溯。 +如果 s[i] 与 s[j+1]不相同,也就是遇到 前后缀末尾不相同的情况,就要向前回退。 -怎么回溯呢? +怎么回退呢? next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度。 @@ -273,7 +302,7 @@ next[j]就是记录着j(包括j)之前的子串的相同前后缀的长度 ``` while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -    j = next[j]; // 向前回溯 +    j = next[j]; // 向前回退 } ``` @@ -292,13 +321,13 @@ next[i] = j; 最后整体构建next数组的函数代码如下: -``` +```C++ 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]; // 向前回溯 +            j = next[j]; // 向前回退         }         if (s[i] == s[j + 1]) { // 找到相同的前后缀             j++; @@ -311,8 +340,7 @@ void getNext(int* next, const string& s){ 代码构造next数组的逻辑流程动画如下: - - +![KMP精讲3](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B23.gif) 得到了next数组之后,就要用这个来做匹配了。 @@ -320,7 +348,7 @@ void getNext(int* next, const string& s){ 在文本串s里 找是否出现过模式串t。 -定义两个下表j 指向模式串起始位置,i指向文本串起始位置。 +定义两个下标j 指向模式串起始位置,i指向文本串起始位置。 那么j初始值依然为-1,为什么呢? **依然因为next数组里记录的起始位置为-1。** @@ -330,7 +358,7 @@ i就从0开始,遍历文本串,代码如下: for (int i = 0; i < s.size(); i++)  ``` -接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 经行比较。 +接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 进行比较。 如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。 @@ -364,7 +392,7 @@ if (j == (t.size() - 1) ) { 那么使用next数组,用模式串匹配文本串的整体代码如下: -``` +```C++ int j = -1; // 因为next数组里记录的起始位置为-1 for (int i = 0; i < s.size(); i++) { // 注意i就从0开始     while(j >= 0 && s[i] != t[j + 1]) { // 不匹配 @@ -383,7 +411,7 @@ for (int i = 0; i < s.size(); i++) { // 注意i就从0开始 # 前缀表统一减一 C++代码实现 -``` +```C++ class Solution { public:     void getNext(int* next, const string& s) { @@ -391,7 +419,7 @@ public:         next[0] = j;         for(int i = 1; i < s.size(); i++) { // 注意i从1开始             while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了 -                j = next[j]; // 向前回溯 +                j = next[j]; // 向前回退             }             if (s[i] == s[j + 1]) { // 找到相同的前后缀                 j++; @@ -410,11 +438,11 @@ public: while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配 j = next[j]; // j 寻找之前匹配的位置 } - if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动 + if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动 j++; // i的增加在for循环里 } if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t - return (i - needle.size() + 1); + return (i - needle.size() + 1); } } return -1; @@ -433,13 +461,13 @@ public: 我给出的getNext的实现为:(前缀表统一减一) -``` +```C++ 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]; // 向前回溯 +            j = next[j]; // 向前回退         }         if (s[i] == s[j + 1]) { // 找到相同的前后缀             j++; @@ -455,12 +483,12 @@ void getNext(int* next, const string& s) { 那么前缀表不减一来构建next数组,代码如下: -``` +```C++ 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作为数组下表的操作 + while (j > 0 && s[i] != s[j]) { // j要保证大于0,因为下面有取j-1作为数组下标的操作 j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了 } if (s[i] == s[j]) { @@ -478,20 +506,20 @@ void getNext(int* next, const string& s) { 实现代码如下: -``` +```C++ 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]; + for(int i = 1; i < s.size(); i++) { + while (j > 0 && s[i] != s[j]) { + j = next[j - 1]; } - if (s[i] == s[j]) { + if (s[i] == s[j]) { j++; } - next[i] = j; + next[i] = j; } } int strStr(string haystack, string needle) { @@ -501,14 +529,14 @@ public: int next[needle.size()]; getNext(next, needle); int j = 0; - for (int i = 0; i < haystack.size(); i++) { - while(j > 0 && haystack[i] != needle[j]) { - j = next[j - 1]; + for (int i = 0; i < haystack.size(); i++) { + while(j > 0 && haystack[i] != needle[j]) { + j = next[j - 1]; } - if (haystack[i] == needle[j]) { + if (haystack[i] == needle[j]) { j++; } - if (j == needle.size() ) { + if (j == needle.size() ) { return (i - needle.size() + 1); } } @@ -532,7 +560,128 @@ public: 可以说把KMP的每一个细微的细节都扣了出来,毫无遮掩的展示给大家了! +## 其他语言版本 -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +Java: +```java +// 方法一 +class Solution { + public void getNext(int[] next, String s){ + int j = -1; + next[0] = j; + for (int i = 1; i=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=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; + } +} +``` + +Python: + +```python +// 方法一 +class Solution: + def strStr(self, haystack: str, needle: str) -> int: + a=len(needle) + b=len(haystack) + if a==0: + return 0 + next=self.getnext(a,needle) + p=-1 + for j in range(b): + while p>=0 and needle[p+1]!=haystack[j]: + p=next[p] + if needle[p+1]==haystack[j]: + p+=1 + if p==a-1: + return j-a+1 + return -1 + + def getnext(self,a,needle): + next=['' for i in range(a)] + k=-1 + next[0]=k + for i in range(1,len(needle)): + while (k>-1 and needle[k+1]!=needle[i]): + k=next[k] + if needle[k+1]==needle[i]: + k+=1 + next[i]=k + return next +``` + +```python +// 方法二 +class Solution: + def strStr(self, haystack: str, needle: str) -> int: + a=len(needle) + b=len(haystack) + if a==0: + return 0 + i=j=0 + next=self.getnext(a,needle) + while(i diff --git a/problems/0031.下一个排列.md b/problems/0031.下一个排列.md deleted file mode 100644 index 68665d6b..00000000 --- a/problems/0031.下一个排列.md +++ /dev/null @@ -1,58 +0,0 @@ - -## 思路 - -一些同学可能手动写排列的顺序,都没有写对,那么写程序的话思路一定是有问题的了,我这里以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++代码如下: - -``` -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]); - sort(nums.begin() + i + 1, nums.end()); - return; - } - } - } - // 到这里了说明整个数组都是倒叙了,反转一下便可 - reverse(nums.begin(), nums.end()); - } -}; -``` diff --git a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md b/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md deleted file mode 100644 index a62261e9..00000000 --- a/problems/0034.在排序数组中查找元素的第一个和最后一个位置.md +++ /dev/null @@ -1,134 +0,0 @@ -> **如果对二分查找比较模糊,建议看这篇:[为什么每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q),这里详细介绍了二分的两种写法,以及循环不变量的重要性,顺便还可以把「leetcode:35.搜索插入位置」题目刷了**。 - -这道题目如果基础不是很好,不建议大家看简短的代码,简短的代码隐藏了太多逻辑,结果就是稀里糊涂把题AC了,但是没有想清楚具体细节! - -下面我来把所有情况都讨论一下。 - -寻找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} - -这三种情况都考虑到,说明就想的很清楚了。 - -接下来,在去寻找左边界,和右边界了。 - -采用二分法来取寻找左右边界,为了让代码清晰,我分别写两个二分来寻找左边界和右边界。 - -**刚刚接触二分搜索的同学不建议上来就像如果用一个二分来查找左右边界,很容易把自己绕进去,建议扎扎实实的写两个二分分别找左边界和右边界** - -## 寻找右边界 - -先来寻找右边界,至于二分查找,如果看过[为什么每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)就会知道,二分查找中什么时候用while (left <= right),有什么时候用while (left < right),其实只要清楚**循环不变量**,很容易区分两种写法。 - -那么这里我采用while (left <= right)的写法,区间定义为[left, right],即左闭又闭的区间(如果这里有点看不懂了,强烈建议把[为什么每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)这篇文章先看了,在把「leetcode:35.搜索插入位置」做了之后在做这道题目就好很多了) - -确定好:计算出来的右边界是不包好target的右边界,左边界同理。 - -可以写出如下代码 - -``` -// 二分查找,寻找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; -} -``` - -## 寻找左边界 - -``` -// 二分查找,寻找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; -} -``` - -## 处理三种情况 - -左右边界计算完之后,看一下主体代码,这里把上面讨论的三种情况,都覆盖了 - -``` -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; - } -}; -``` - -这份代码在简洁性很有大的优化空间,例如把寻找左右区间函数合并一起。 - -但拆开更清晰一些,而且把三种情况以及对应的处理逻辑完整的展现出来了。 - -# 总结 - -初学者建议大家一块一块的去分拆这道题目,正如本题解描述,想清楚三种情况之后,先专注于寻找右区间,然后专注于寻找左区间,左右根据左右区间做最后判断。 - -不要上来就想如果一起寻找左右区间,搞着搞着就会顾此失彼,绕进去拔不出来了。 - - diff --git a/problems/0035.搜索插入位置.md b/problems/0035.搜索插入位置.md index 5c6a1545..5bd3b553 100644 --- a/problems/0035.搜索插入位置.md +++ b/problems/0035.搜索插入位置.md @@ -1,20 +1,15 @@ - -

- -

- - + + - -

+

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

-> 二分查找法是数组里的常用方法,彻底掌握它是十分必要的。 -# 编号35:搜索插入位置 + +# 35.搜索插入位置 题目地址:https://leetcode-cn.com/problems/search-insert-position/ @@ -22,23 +17,23 @@ 你可以假设数组中无重复元素。 -示例 1: -输入: [1,3,5,6], 5 -输出: 2 +示例 1: +输入: [1,3,5,6], 5 +输出: 2 -示例 2: -输入: [1,3,5,6], 2 -输出: 1 +示例 2: +输入: [1,3,5,6], 2 +输出: 1 -示例 3: -输入: [1,3,5,6], 7 +示例 3: +输入: [1,3,5,6], 7 输出: 4 -示例 4: -输入: [1,3,5,6], 0 -输出: 0 +示例 4: +输入: [1,3,5,6], 0 +输出: 0 -# 思路 +# 思路 这道题目不难,但是为什么通过率相对来说并不高呢,我理解是大家对边界处理的判断有所失误导致的。 @@ -68,20 +63,20 @@ public: 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) +时间复杂度:O(n) +时间复杂度:O(1) 效率如下: @@ -89,7 +84,7 @@ public: ## 二分法 -既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。 +既然暴力解法的时间复杂度是O(n),就要尝试一下使用二分查找法。 ![35_搜索插入位置4](https://img-blog.csdnimg.cn/202012162326354.png) @@ -107,9 +102,9 @@ public: 相信很多同学对二分查找法中边界条件处理不好。 -例如到底是 `while(left < right)` 还是 `while(left <= right)`,到底是`right = middle`呢,还是要`right = middle - 1`呢? +例如到底是 `while(left < right)` 还是 `while(left <= right)`,到底是`right = middle`呢,还是要`right = middle - 1`呢? -这里弄不清楚主要是因为**对区间的定义没有想清楚,这就是不变量**。 +这里弄不清楚主要是因为**对区间的定义没有想清楚,这就是不变量**。 要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查)。 @@ -127,7 +122,7 @@ public: int searchInsert(vector& nums, int target) { int n = nums.size(); int left = 0; - int right = n - 1; // 定义target在左闭右闭的区间里,[left, right] + 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) { @@ -147,7 +142,7 @@ public: } }; ``` -时间复杂度:O(logn) +时间复杂度:O(logn) 时间复杂度:O(1) 效率如下: @@ -190,10 +185,10 @@ public: }; ``` -时间复杂度:O(logn) +时间复杂度:O(logn) 时间复杂度:O(1) -# 总结 +# 总结 希望通过这道题目,大家会发现平时写二分法,为什么总写不好,就是因为对区间定义不清楚。 @@ -203,6 +198,27 @@ public: **循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** -

- -

+ + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0037.解数独.md b/problems/0037.解数独.md index 4e3a74e2..75e2d3cd 100644 --- a/problems/0037.解数独.md +++ b/problems/0037.解数独.md @@ -1,35 +1,42 @@ -> 解数独,理解二维递归是关键 +

+ + + + +

+

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

+ 如果对回溯法理论还不清楚的同学,可以先看这个视频[视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) -# 37. 解数独 +## 37. 解数独 题目地址:https://leetcode-cn.com/problems/sudoku-solver/ 编写一个程序,通过填充空格来解决数独问题。 -一个数独的解法需遵循如下规则: -数字 1-9 在每一行只能出现一次。 -数字 1-9 在每一列只能出现一次。 -数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 -空白格用 '.' 表示。 +一个数独的解法需遵循如下规则: +数字 1-9 在每一行只能出现一次。 +数字 1-9 在每一列只能出现一次。 +数字 1-9 在每一个以粗实线分隔的 3x3 宫内只能出现一次。 +空白格用 '.' 表示。 ![解数独](https://img-blog.csdnimg.cn/202011171912586.png) -一个数独。 +一个数独。 -![解数独](https://img-blog.csdnimg.cn/20201117191340669.png) +![解数独](https://img-blog.csdnimg.cn/20201117191340669.png) -答案被标成红色。 +答案被标成红色。 -提示: +提示: * 给定的数独序列只包含数字 1-9 和字符 '.' 。 * 你可以假设给定的数独只有唯一解。 * 给定数独永远是 9x9 形式的。 -# 思路 +## 思路 -棋盘搜索问题可以使用回溯法暴力搜索,只不过这次我们要做的是**二维递归**。 +棋盘搜索问题可以使用回溯法暴力搜索,只不过这次我们要做的是**二维递归**。 怎么做二维递归呢? @@ -46,9 +53,9 @@ ![37.解数独](https://img-blog.csdnimg.cn/2020111720451790.png) -## 回溯三部曲 +## 回溯三部曲 -* 递归函数以及参数 +* 递归函数以及参数 **递归函数的返回值需要是bool类型,为什么呢?** @@ -60,15 +67,15 @@ bool backtracking(vector>& board) ``` -* 递归终止条件 +* 递归终止条件 本题递归不用终止条件,解数独是要遍历整个树形结构寻找可能的叶子节点就立刻返回。 -**不用终止条件会不会死循环?** +**不用终止条件会不会死循环?** 递归的下一层的棋盘一定比上一层的棋盘多一个数,等数填满了棋盘自然就终止(填满当然好了,说明找到结果了),所以不需要终止条件! -**那么有没有永远填不满的情况呢?** +**那么有没有永远填不满的情况呢?** 这个问题我在递归单层搜索逻辑里在来讲! @@ -89,7 +96,7 @@ bool backtracking(vector>& board) { 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)) { + if (isValid(i, j, k, board)) { board[i][j] = k; // 放置k if (backtracking(board)) return true; // 如果找到合适一组立刻返回 board[i][j] = '.'; // 回溯,撤销k @@ -104,16 +111,16 @@ bool backtracking(vector>& board) { **注意这里return false的地方,这里放return false 是有讲究的**。 -因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! +因为如果一行一列确定下来了,这里尝试了9个数都不行,说明这个棋盘找不到解决数独问题的解! -那么会直接返回, **这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!** +那么会直接返回, **这也就是为什么没有终止条件也不会永远填不满棋盘而无限递归下去!** -## 判断棋盘是否合法 +## 判断棋盘是否合法 判断棋盘是否合法有如下三个维度: -* 同行是否重复 -* 同列是否重复 +* 同行是否重复 +* 同列是否重复 * 9宫格里是否重复 代码如下: @@ -145,7 +152,7 @@ bool isValid(int row, int col, char val, vector>& board) { 最后整体代码如下: -# C++代码 +## C++代码 ```C++ class Solution { @@ -155,7 +162,7 @@ bool backtracking(vector>& board) { 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)) { + if (isValid(i, j, k, board)) { board[i][j] = k; // 放置k if (backtracking(board)) return true; // 如果找到合适一组立刻返回 board[i][j] = '.'; // 回溯,撤销k @@ -195,7 +202,7 @@ public: }; ``` -# 总结 +## 总结 解数独可以说是非常难的题目了,如果还一直停留在单层递归的逻辑中,这道题目可以让大家瞬间崩溃。 @@ -207,9 +214,23 @@ public: **恭喜一路上坚持打卡的录友们,回溯算法已经接近尾声了,接下来就是要一波总结了**。 -如果一直跟住「代码随想录」的节奏,你会发现自己进步飞快,从思维方式到刷题习惯,都会有质的飞跃,「代码随想录」绝对值得推荐给身边的同学朋友们! -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** +## 其他语言版本 -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0039.组合总和.md b/problems/0039.组合总和.md index 509ad28f..f983a994 100644 --- a/problems/0039.组合总和.md +++ b/problems/0039.组合总和.md @@ -1,7 +1,13 @@ +

+ + + + +

+

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

-> 看懂很容易,彻底掌握需要下功夫 -# 第39题. 组合总和 +## 39. 组合总和 题目链接:https://leetcode-cn.com/problems/combination-sum/ @@ -9,29 +15,32 @@ candidates 中的数字可以无限制重复被选取。 -说明: +说明: -* 所有数字(包括 target)都是正整数。 +* 所有数字(包括 target)都是正整数。 * 解集不能包含重复的组合。  -示例 1: -输入:candidates = [2,3,6,7], target = 7, -所求解集为: -[ - [7], - [2,2,3] -] +示例 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] -] +示例 2: +输入:candidates = [2,3,5], target = 8, +所求解集为: +[ +  [2,2,2,2], +  [2,3,3], +  [3,5] +] + +## 思路 + +[B站视频讲解-组合总和](https://www.bilibili.com/video/BV1KT4y1M7HJ) -# 思路 题目中的**无限制重复被选取,吓得我赶紧想想 出现0 可咋办**,然后看到下面提示:1 <= candidates[i] <= 200,我就放心了。 @@ -39,15 +48,14 @@ candidates 中的数字可以无限制重复被选取。 本题搜索的过程抽象成树形结构如下: -![39.组合总和](https://img-blog.csdnimg.cn/20201123202227835.png) - +![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png) 注意图中叶子节点的返回条件,因为本题没有组合数量要求,仅仅是总和的限制,所以递归没有层数的限制,只要选取的元素总和超过target,就返回! 而在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) 中都可以知道要递归K层,因为要取k个元素的组合。 ## 回溯三部曲 -* 递归函数参数 +* 递归函数参数 这里依然是定义两个全局变量,二维数组result存放结果集,数组path存放符合条件的结果。(这两个变量可以作为函数参数传入) @@ -65,23 +73,23 @@ candidates 中的数字可以无限制重复被选取。 代码如下: -``` +```C++ vector> result; vector path; -void backtracking(vector& candidates, int target, int sum, int startIndex) +void backtracking(vector& candidates, int target, int sum, int startIndex) ``` * 递归终止条件 在如下树形结构中: -![39.组合总和](https://img-blog.csdnimg.cn/20201123202227835.png) +![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png) 从叶子节点可以清晰看到,终止只有两种情况,sum大于target和sum等于target。 sum等于target的时候,需要收集结果,代码如下: -``` +```C++ if (sum > target) { return; } @@ -91,7 +99,7 @@ if (sum == target) { } ``` -* 单层搜索的逻辑 +* 单层搜索的逻辑 单层for循环依然是从startIndex开始,搜索candidates集合。 @@ -99,7 +107,7 @@ if (sum == target) { 如何重复选取呢,看代码,注释部分: -``` +```C++ for (int i = startIndex; i < candidates.size(); i++) { sum += candidates[i]; path.push_back(candidates[i]); @@ -111,7 +119,7 @@ for (int i = startIndex; i < candidates.size(); i++) { 按照[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)中给出的模板,不难写出如下C++完整代码: -``` +```C++ // 版本一 class Solution { private: @@ -144,11 +152,11 @@ public: }; ``` -## 剪枝优化 +## 剪枝优化 在这个树形结构中: -![39.组合总和](https://img-blog.csdnimg.cn/20201123202227835.png) +![39.组合总和](https://img-blog.csdnimg.cn/20201223170730367.png) 以及上面的版本一的代码大家可以看到,对于sum已经大于target的情况,其实是依然进入了下一层递归,只是下一层递归结束判断的时候,会判断sum > target的话就返回。 @@ -160,18 +168,18 @@ public: 如图: -![39.组合总和1](https://img-blog.csdnimg.cn/20201123202349897.png) +![39.组合总和1](https://img-blog.csdnimg.cn/20201223170809182.png) for循环剪枝代码如下: ``` -for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) +for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) ``` 整体代码如下:(注意注释的部分) -``` +```C++ class Solution { private: vector> result; @@ -203,12 +211,12 @@ public: }; ``` -# 总结 +## 总结 本题和我们之前讲过的[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)、[回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w)有两点不同: -* 组合没有数量要求 -* 元素可无限重复选取 +* 组合没有数量要求 +* 元素可无限重复选取 针对这两个问题,我都做了详细的分析。 @@ -220,9 +228,26 @@ public: 可以看出我写的文章都会大量引用之前的文章,就是要不断作对比,分析其差异,然后给出代码解决的方法,这样才能彻底理解题目的本质与难点。 -**就酱,如果感觉很给力,就帮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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0040.组合总和II.md b/problems/0040.组合总和II.md index 40179710..ffcbe212 100644 --- a/problems/0040.组合总和II.md +++ b/problems/0040.组合总和II.md @@ -1,6 +1,15 @@ +

+ + + + +

+

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

+ + > 这篇可以说是全网把组合问题如何去重,讲的最清晰的了! -# 40.组合总和II +## 40.组合总和II 题目链接:https://leetcode-cn.com/problems/combination-sum-ii/ @@ -8,42 +17,42 @@ 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] -] +示例 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] +] + +## 思路 这道题目和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)如下区别: 1. 本题candidates 中的每个数字在每个组合中只能使用一次。 -2. 本题数组candidates的元素是有重复的,而[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)是无重复元素的数组candidates +2. 本题数组candidates的元素是有重复的,而[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)是无重复元素的数组candidates -最后本题和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)要求一样,解集不能包含重复的组合。 +最后本题和[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)要求一样,解集不能包含重复的组合。 **本题的难点在于区别2中:集合(数组candidates)有重复元素,但还不能有重复的组合**。 一些同学可能想了:我把所有组合求出来,再用set或者map去重,这么做很容易超时! -所以要在搜索的过程中就去掉重复组合。 +所以要在搜索的过程中就去掉重复组合。 很多同学在去重的问题上想不明白,其实很多题解也没有讲清楚,反正代码是能过的,感觉是那么回事,稀里糊涂的先把题目过了。 @@ -68,17 +77,17 @@ candidates 中的每个数字在每个组合中只能使用一次。 可以看到图中,每个节点相对于 [39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)我多加了used数组,这个used数组下面会重点介绍。 -## 回溯三部曲 +## 回溯三部曲 -* **递归函数参数** +* **递归函数参数** -与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。 +与[39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)套路相同,此题还需要加一个bool型数组used,用来记录同一树枝上的元素是否使用过。 这个集合去重的重任就是used来完成的。 代码如下: -``` +```C++ vector> result; // 存放组合集合 vector path; // 符合条件的组合 void backtracking(vector& candidates, int target, int sum, int startIndex, vector& used) { @@ -90,8 +99,8 @@ void backtracking(vector& candidates, int target, int sum, int startIndex, 代码如下: -``` -if (sum > target) { // 这个条件其实可以省略 +```C++ +if (sum > target) { // 这个条件其实可以省略 return; } if (sum == target) { @@ -118,14 +127,14 @@ if (sum == target) { 我在图中将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]使用过 **这块去重的逻辑很抽象,网上搜的题解基本没有能讲清楚的,如果大家之前思考过这个问题或者刷过这道题目,看到这里一定会感觉通透了很多!** 那么单层搜索的逻辑代码如下: -``` +```C++ 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]使用过 @@ -149,7 +158,7 @@ for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; 回溯三部曲分析完了,整体C++代码如下: -``` +```C++ class Solution { private: vector> result; @@ -190,7 +199,47 @@ public: ``` -# 总结 +## 补充 + +这里直接用startIndex来去重也是可以的, 就不用used数组了。 + +```C++ +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://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)难度提升了不少。 @@ -198,9 +247,26 @@ public: 所以Carl有必要把去重的这块彻彻底底的给大家讲清楚,**就连“树层去重”和“树枝去重”都是我自创的词汇,希望对大家理解有帮助!** -**就酱,如果感觉「代码随想录」诚意满满,就帮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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0042.接雨水.md b/problems/0042.接雨水.md deleted file mode 100644 index 05cf9e08..00000000 --- a/problems/0042.接雨水.md +++ /dev/null @@ -1,340 +0,0 @@ -# 题目链接 - -# 思路 - -接雨水问题在面试中还是常见题目的,有必要好好讲一讲。 - -本文深度讲解如下三种方法: -* 双指针法 -* 动态规划 -* 单调栈 - -## 双指针解法 - -这道题目暴力解法并不简单,我们来看一下思路。 - -首先要明确,要按照行来计算,还是按照列来计算。如图所示: - - - - - -一些同学在实现暴力解法的时候,很容易一会按照行来计算一会按照列来计算,这样就会越写越乱。 - -我个人倾向于按照列来计算,比较容易理解,接下来看一下按照列如何计算。 - -首先,**如果按照列来计算的话,宽度一定是1了,我们再把每一列的雨水的高度求出来就可以了。** - -可以看出每一列雨水的高度,取决于,该列 左侧最高的柱子和右侧最高的柱子中最矮的那个柱子的高度。 - -这句话可以有点绕,来举一个理解,例如求列4的雨水高度,如图: - - - -列4 左侧最高的柱子是列3,高度为2(以下用lHeight表示)。 - -列4 右侧最高的柱子是列7,高度为3(以下用rHeight表示)。 - -列4 柱子的高度为1(以下用height表示) - -那么列4的雨水高度为 列3和列7的高度最小值减列4高度,即: min(lHeight, rHeight) - height。 - -列4的雨水高度求出来了,宽度为1,相乘就是列4的雨水体积了。 - -此时求出了列4的雨水体积。 - -一样的方法,只要从头遍历一遍所有的列,然后求出每一列雨水的体积,相加之后就是总雨水的体积了。 - -首先从头遍历所有的列,并且**要注意第一个柱子和最后一个柱子不接雨水**,代码如下: -``` -for (int i = 0; i < height.size(); i++) { - // 第一个柱子和最后一个柱子不接雨水 - if (i == 0 || i == height.size() - 1) continue; -} -``` - -在for循环中求左右两边最高柱子,代码如下: - -``` -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; // 注意只有h大于零的时候,在统计到总和中 -``` - -整体代码如下: - -``` -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]); - -这样就找到递推公式。 - -是不是地推公式还挺简单的,其实动态规划就是这样,只要想到了递推公式,其实就比较简单了。 - -代码如下: - -``` -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; - } -}; -``` - -## 单调栈解法 - -这个解法可以说是最不好理解的了,所以下面我花了大量的篇幅来介绍这种方法。 - -单调栈就是保持栈内元素有序。和[栈与队列:单调队列](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA)一样,需要我们自己维持顺序,没有现成的容器可以用。 - - -### 准备工作 - -那么本题使用单调栈有如下几个问题: - -1. 首先单调栈是按照行方向来计算雨水,如图: - - - -知道这一点,后面的就可以理解了。 - -2. 使用单调栈内元素的顺序 - -从大到小还是从小打到呢? - -要从栈底到栈头(元素从栈头弹出)是从大到小的顺序。 - -因为一旦发现添加的柱子高度大于栈头元素了,此时就出现凹槽了,栈头元素就是凹槽底部的柱子,栈头第二个元素就是凹槽左边的柱子,而添加的元素就是凹槽右边的柱子。 - -如图: - - - - -3. 遇到相同高度的柱子怎么办。 - -遇到相同的元素,更新栈内下表,就是将栈里元素(旧下标)弹出,将新元素(新下标)加入栈中。 - -例如 5 5 1 3 这种情况。如果添加第二个5的时候就应该将第一个5的下标弹出,把第二个5添加到栈中。 - -因为我们要求宽度的时候 如果遇到相同高度的柱子,需要使用最右边的柱子来计算宽度。 - -如图所示: - - - - - -4. 栈里要保存什么数值 - -是用单调栈,其实是通过 长 * 宽 来计算雨水面积的。 - -长就是通过柱子的高度来计算,宽是通过柱子之间的下表来计算, - -那么栈里有没有必要存一个pair类型的元素,保存柱子的高度和下表呢。 - -其实不用,栈里就存放int类型的元素就行了,表示下表,想要知道对应的高度,通过height[stack.top()] 就知道弹出的下表对应的高度了。 - -所以栈的定义如下: - -``` -stack st; // 存着下标,计算的时候用下标对应的柱子高度 -``` - -明确了如上几点,我们再来看处理逻辑。 - -### 单调栈处理逻辑 - -先将下表0的柱子加入到栈中,`st.push(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); -} -``` - -如果当前遍历的元素(柱子)高度大于栈顶元素的高度,此时就出现凹槽了,如图所示: - - - -取栈顶元素,将栈顶元素弹出,这个就是凹槽的底部,也就是中间位置,下表记为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`。 - -求当前凹槽雨水的体积代码如下: - -``` -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; - } -} -``` - -关键部分讲完了,整体代码如下: - -``` -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; - } -}; -``` - -以上代码冗余了一些,但是思路是清晰的,下面我将代码精简一下,如下: - -``` -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; - } -}; -``` - -精简之后的代码,大家就看不出去三种情况的处理了,貌似好像只处理的情况三,其实是把情况一和情况二融合了。 这样的代码不太利于理解。 - diff --git a/problems/0045.跳跃游戏II.md b/problems/0045.跳跃游戏II.md index 2474b6d1..2def83a9 100644 --- a/problems/0045.跳跃游戏II.md +++ b/problems/0045.跳跃游戏II.md @@ -1,31 +1,40 @@ +

+ + + + +

+

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

+ + > 相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不少,做好心里准备! -# 45.跳跃游戏II +## 45.跳跃游戏II 题目地址:https://leetcode-cn.com/problems/jump-game-ii/ -给定一个非负整数数组,你最初位于数组的第一个位置。 +给定一个非负整数数组,你最初位于数组的第一个位置。 -数组中的每个元素代表你在该位置可以跳跃的最大长度。 - -你的目标是使用最少的跳跃次数到达数组的最后一个位置。 +数组中的每个元素代表你在该位置可以跳跃的最大长度。 -示例: -输入: [2,3,1,1,4] -输出: 2 -解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 +你的目标是使用最少的跳跃次数到达数组的最后一个位置。 -说明: -假设你总是可以到达数组的最后一个位置。 +示例: +输入: [2,3,1,1,4] +输出: 2 +解释: 跳到最后一个位置的最小跳跃数是 2。从下标为 0 跳到下标为 1 的位置,跳 1 步,然后跳 3 步到达数组的最后一个位置。 + +说明: +假设你总是可以到达数组的最后一个位置。 -# 思路 +## 思路 本题相对于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)还是难了不少。 但思路是相似的,还是要看最大覆盖范围。 -本题要计算最小步数,那么就要想清楚什么时候步数才一定要加一呢? +本题要计算最小步数,那么就要想清楚什么时候步数才一定要加一呢? 贪心的思路,局部最优:当前可移动距离尽可能多走,如果还没到终点,步数再加一。整体最优:一步尽可能多走,从而达到最小步数。 @@ -50,7 +59,7 @@ 这里还是有个特殊情况需要考虑,当移动下标达到了当前覆盖的最远距离下标时 * 如果当前覆盖最远距离下标不是是集合终点,步数就加一,还需要继续走。 -* 如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了。 +* 如果当前覆盖最远距离下标就是是集合终点,步数不用加一,因为不能再往后走了。 C++代码如下:(详细注释) @@ -76,7 +85,7 @@ public: return ans; } }; -``` +``` ## 方法二 @@ -103,8 +112,8 @@ class Solution { public: int jump(vector& nums) { int curDistance = 0; // 当前覆盖的最远距离下标 - int ans = 0; // 记录走的最大步数 - int nextDistance = 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) { // 遇到当前覆盖的最远距离下标 @@ -117,11 +126,11 @@ public: }; ``` -可以看出版本二的代码相对于版本一简化了不少! +可以看出版本二的代码相对于版本一简化了不少! 其精髓在于控制移动下标i只移动到nums.size() - 2的位置,所以移动下标只要遇到当前覆盖最远距离的下标,直接步数加一,不用考虑别的了。 -# 总结 +## 总结 相信大家可以发现,这道题目相当于[贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA)难了不止一点。 @@ -129,10 +138,23 @@ public: 理解本题的关键在于:**以最小的步数增加最大的覆盖范围,直到覆盖范围覆盖了终点**,这个范围内最小步数一定可以跳到,不用管具体是怎么跳的,不纠结于一步究竟跳一个单位还是两个单位。 -就酱,如果感觉「代码随想录」很不错,就分享给身边的朋友同学吧! + +## 其他语言版本 -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** +Java: -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0046.全排列.md b/problems/0046.全排列.md index 7e1b87a2..5f7b1ac0 100644 --- a/problems/0046.全排列.md +++ b/problems/0046.全排列.md @@ -1,25 +1,31 @@ +

+ + + + +

+

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

-> 开始排列问题 -# 46.全排列 +## 46.全排列 题目链接:https://leetcode-cn.com/problems/permutations/ -给定一个 没有重复 数字的序列,返回其所有可能的全排列。 - -示例: -输入: [1,2,3] -输出: -[ - [1,2,3], - [1,3,2], - [2,1,3], - [2,3,1], - [3,1,2], - [3,2,1] -] +给定一个 没有重复 数字的序列,返回其所有可能的全排列。 -## 思路 +示例: +输入: [1,2,3] +输出: +[ + [1,2,3], + [1,3,2], + [2,1,3], + [2,3,1], + [3,1,2], + [3,2,1] +] + +## 思路 此时我们已经学习了[组合问题](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)、[切割问题](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)和[子集问题](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA),接下来看一看排列问题。 @@ -33,9 +39,9 @@ ![46.全排列](https://img-blog.csdnimg.cn/20201209174225145.png) -## 回溯三部曲 +## 回溯三部曲 -* 递归函数参数 +* 递归函数参数 **首先排列是有序的,也就是说[1,2] 和[2,1] 是两个集合,这和之前分析的子集以及组合所不同的地方**。 @@ -53,13 +59,13 @@ vector path; void backtracking (vector& nums, vector& used) ``` -* 递归终止条件 +* 递归终止条件 ![46.全排列](https://img-blog.csdnimg.cn/20201209174225145.png) 可以看出叶子节点,就是收割结果的地方。 -那么什么时候,算是到达叶子节点呢? +那么什么时候,算是到达叶子节点呢? 当收集元素的数组path的大小达到和nums数组一样大的时候,说明找到了一个全排列,也表示到达了叶子节点。 @@ -73,7 +79,7 @@ if (path.size() == nums.size()) { } ``` -* 单层搜索的逻辑 +* 单层搜索的逻辑 这里和[组合问题](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)、[切割问题](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)和[子集问题](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)最大的不同就是for循环里不用startIndex了。 @@ -96,9 +102,8 @@ for (int i = 0; i < nums.size(); i++) { 整体C++代码如下: -## C++代码 -``` +```C++ class Solution { public: vector> result; @@ -128,18 +133,32 @@ public: }; ``` -# 总结 +## 总结 大家此时可以感受出排列问题的不同: -* 每层都是从0开始搜索而不是startIndex +* 每层都是从0开始搜索而不是startIndex * 需要used数组记录path里都放了哪些元素了 排列问题是回溯算法解决的经典题目,大家可以好好体会体会。 -就酱,如果感觉「代码随想录」诚意满满,就帮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),关注后就会发现和「代码随想录」相见恨晚!** +## 其他语言版本 -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0047.全排列II.md b/problems/0047.全排列II.md index dbe8a2e6..94bb4df1 100644 --- a/problems/0047.全排列II.md +++ b/problems/0047.全排列II.md @@ -1,28 +1,34 @@ +

+ + + + +

+

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

+# 排列问题(二) -> 排列也要去重了 - -# 47.全排列 II +## 47.全排列 II 题目链接:https://leetcode-cn.com/problems/permutations-ii/ 给定一个可包含重复数字的序列 nums ,按任意顺序 返回所有不重复的全排列。 -示例 1: -输入:nums = [1,1,2] -输出: -[[1,1,2], - [1,2,1], - [2,1,1]] +示例 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]] +示例 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://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)的区别在与**给定一个可包含重复数字的序列**,要返回**所有不重复的全排列**。 @@ -91,14 +97,14 @@ public: 大家发现,去重最为关键的代码为: ``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { continue; } ``` **如果改成 `used[i - 1] == true`, 也是正确的!**,去重代码如下: ``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { continue; } ``` @@ -121,17 +127,17 @@ if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { 大家应该很清晰的看到,树层上对前一位去重非常彻底,效率很高,树枝上对前一位去重虽然最后可以得到答案,但是做了很多无用搜索。 -# 总结 +## 总结 这道题其实还是用了我们之前讲过的去重思路,但有意思的是,去重的代码中,这么写: ``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { continue; } ``` 和这么写: ``` -if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { +if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { continue; } ``` @@ -142,9 +148,83 @@ if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == true) { 是不是豁然开朗了!! -就酱,很多录友表示和「代码随想录」相见恨晚,那么大家帮忙多多宣传,让更多的同学知道这里,感谢啦! +## 其他语言版本 -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** +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: List[int]) -> List[List[int]]: + # res用来存放结果 + if not nums: return [] + res = [] + used = [0] * len(nums) + def backtracking(nums, used, path): + # 终止条件 + if len(path) == len(nums): + res.append(path.copy()) + return + for i in range(len(nums)): + if not used[i]: + if i>0 and nums[i] == nums[i-1] and not used[i-1]: + continue + used[i] = 1 + path.append(nums[i]) + backtracking(nums, used, path) + path.pop() + used[i] = 0 + # 记得给nums排序 + backtracking(sorted(nums),used,[]) + return res +``` + + + + +----------------------- +* 作者微信:[程序员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/0051.N皇后.md b/problems/0051.N皇后.md index 7475325e..ac0235c2 100644 --- a/problems/0051.N皇后.md +++ b/problems/0051.N皇后.md @@ -1,13 +1,17 @@ -> 开始棋盘问题,如果对回溯法还不了解的同学可以看这个视频 - -如果对回溯法理论还不清楚的同学,可以先看这个视频[视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) +

+ + + + +

+

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

-# 第51题. N皇后 +## 第51题. N皇后 -题目链接: https://leetcode-cn.com/problems/n-queens/ +题目链接: https://leetcode-cn.com/problems/n-queens/ -n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 +n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 上图为 8 皇后问题的一种解法。 ![51n皇后](https://img-blog.csdnimg.cn/20200821152118456.png) @@ -17,45 +21,45 @@ n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并 每一种解法包含一个明确的 n 皇后问题的棋子放置方案,该方案中 'Q' 和 '.' 分别代表了皇后和空位。 示例: -输入: 4 -输出: [ - [".Q..", // 解法 1 - "...Q", - "Q...", - "..Q."], - - ["..Q.", // 解法 2 - "Q...", - "...Q", - ".Q.."] -] -解释: 4 皇后问题存在两个不同的解法。 - -提示: +输入: 4 +输出: [ + [".Q..", // 解法 1 + "...Q", + "Q...", + "..Q."], + + ["..Q.", // 解法 2 + "Q...", + "...Q", + ".Q.."] +] +解释: 4 皇后问题存在两个不同的解法。 + +提示: > 皇后,是国际象棋中的棋子,意味着国王的妻子。皇后只做一件事,那就是“吃子”。当她遇见可以吃的棋子时,就迅速冲上去吃掉棋子。当然,她横、竖、斜都可走一到七步,可进可退。(引用自 百度百科 - 皇后 ) -# 思路 +## 思路 都知道n皇后问题是回溯算法解决的经典问题,但是用回溯解决多了组合、切割、子集、排列问题之后,遇到这种二位矩阵还会有点不知所措。 首先来看一下皇后们的约束条件: 1. 不能同行 -2. 不能同列 +2. 不能同列 3. 不能同斜线 确定完约束条件,来看看究竟要怎么去搜索皇后们的位置,其实搜索皇后的位置,可以抽象为一棵树。 下面我用一个3 * 3 的棋牌,将搜索过程抽象为一颗树,如图: -![51.N皇后](https://img-blog.csdnimg.cn/20201118225433127.png) +![51.N皇后](https://img-blog.csdnimg.cn/20210130182532303.jpg) 从图中,可以看出,二维矩阵中矩阵的高就是这颗树的高度,矩阵的宽就是树形结构中每一个节点的宽度。 -那么我们用皇后们的约束条件,来回溯搜索这颗树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。 +那么我们用皇后们的约束条件,来回溯搜索这颗树,**只要搜索到了树的叶子节点,说明就找到了皇后们的合理位置了**。 -## 回溯三部曲 +## 回溯三部曲 按照我总结的如下回溯模板,我们来依次分析: @@ -73,11 +77,11 @@ void backtracking(参数) { } ``` -* 递归函数参数 +* 递归函数参数 我依然是定义全局变量二维数组result来记录最终结果。 -参数n是棋牌的大小,然后用row来记录当前遍历到棋盘的第几层了。 +参数n是棋牌的大小,然后用row来记录当前遍历到棋盘的第几层了。 代码如下: @@ -86,10 +90,11 @@ vector> result; void backtracking(int n, int row, vector& chessboard) { ``` -* 递归终止条件 +* 递归终止条件 在如下树形结构中: -![51.N皇后](https://img-blog.csdnimg.cn/20201118225433127.png) +![51.N皇后](https://img-blog.csdnimg.cn/20210130182532303.jpg) + 可以看出,当递归到棋盘最底层(也就是叶子节点)的时候,就可以收集结果并返回了。 @@ -102,9 +107,9 @@ if (row == n) { } ``` -* 单层搜索的逻辑 +* 单层搜索的逻辑 -递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。 +递归深度就是row控制棋盘的行,每一层里for循环的col控制棋盘的列,一行一列,确定了放置皇后的位置。 每次都是要从新的一行的起始位置开始搜,所以都是从0开始。 @@ -125,10 +130,10 @@ for (int col = 0; col < n; col++) { 按照如下标准去重: 1. 不能同行 -2. 不能同列 +2. 不能同列 3. 不能同斜线 (45度和135度角) -代码如下: +代码如下: ``` bool isValid(int row, int col, vector& chessboard, int n) { @@ -155,15 +160,15 @@ bool isValid(int row, int col, vector& chessboard, int n) { } ``` -在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢? +在这份代码中,细心的同学可以发现为什么没有在同行进行检查呢? 因为在单层搜索的过程中,每一层递归,只会选for循环(也就是同一行)里的一个元素,所以不用去重了。 -那么按照这个模板不难写出如下代码: +那么按照这个模板不难写出如下C++代码: ## C++代码 -``` +```C++ class Solution { private: vector> result; @@ -216,7 +221,7 @@ public: 可以看出,除了验证棋盘合法性的代码,省下来部分就是按照回溯法模板来的。 -# 总结 +## 总结 本题是我们解决棋盘问题的第一道题目。 @@ -226,9 +231,144 @@ public: 大家可以在仔细体会体会! -就酱,如果感觉「代码随想录」干货满满,就分享给身边的朋友同学吧,他们可能也需要! -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** +## 其他语言补充 -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +Python: + +```python +class Solution: + def solveNQueens(self, n: int) -> List[List[str]]: + if not n: return [] + board = [['.'] * n for _ in range(n)] + res = [] + def isVaild(board,row, col): + #判断同一列是否冲突 + for i in range(len(board)): + if board[i][col] == 'Q': + return False + # 判断左上角是否冲突 + i = row -1 + j = col -1 + while i>=0 and j>=0: + if board[i][j] == 'Q': + return False + i -= 1 + j -= 1 + # 判断右上角是否冲突 + i = row - 1 + j = col + 1 + while i>=0 and j < len(board): + if board[i][j] == 'Q': + return False + i -= 1 + j += 1 + return True + + def backtracking(board, row, n): + # 如果走到最后一行,说明已经找到一个解 + if row == n: + temp_res = [] + for temp in board: + temp_str = "".join(temp) + temp_res.append(temp_str) + res.append(temp_res) + for col in range(n): + if not isVaild(board, row, col): + continue + board[row][col] = 'Q' + backtracking(board, row+1, n) + board[row][col] = '.' + backtracking(board, 0, n) + return res +``` + +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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0052.N皇后II.md b/problems/0052.N皇后II.md deleted file mode 100644 index 4cf103fb..00000000 --- a/problems/0052.N皇后II.md +++ /dev/null @@ -1,85 +0,0 @@ -# 题目链接 -https://leetcode-cn.com/problems/n-queens-ii/ - -# 第51题. N皇后 - -n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。 - -上图为 8 皇后问题的一种解法。 -![51n皇后](https://img-blog.csdnimg.cn/20200821152118456.png) - -给定一个整数 n,返回 n 皇后不同的解决方案的数量。 - -示例: - -输入: 4 -输出: 2 -解释: 4 皇后问题存在如下两个不同的解法。 -[ - [".Q..",  // 解法 1 -  "...Q", -  "Q...", -  "..Q."], - - ["..Q.",  // 解法 2 -  "Q...", -  "...Q", -  ".Q.."] -] -# 思路 - -这道题目和 51.N皇后 基本没有区别 - -# C++代码 - -``` -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; - - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0053.最大子序和.md b/problems/0053.最大子序和.md index ab83d861..b8a9d748 100644 --- a/problems/0053.最大子序和.md +++ b/problems/0053.最大子序和.md @@ -1,27 +1,31 @@ +

+ + + + +

+

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

-> 从本题开始,贪心题目都比较难了! -通知:一些录友表示经常看不到每天的文章,现在公众号已经不按照发送时间推荐了,而是根据一些规则乱序推送,所以可能关注了「代码随想录」也一直看不到文章,建议把「代码随想录」设置星标哈,设置星标之后,每天就按发文时间推送了,我每天都是定时8:35发送的,嗷嗷准时! -# 53. 最大子序和 +## 53. 最大子序和 题目地址:https://leetcode-cn.com/problems/maximum-subarray/ 给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 -示例: -输入: [-2,1,-3,4,-1,2,1,-5,4] -输出: 6 -解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 +示例: +输入: [-2,1,-3,4,-1,2,1,-5,4] +输出: 6 +解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 -# 思路 -## 暴力解法 +## 暴力解法 -暴力解法的思路,第一层for 就是设置起始位置,第二层for循环遍历数组寻找最大值 +暴力解法的思路,第一层for 就是设置起始位置,第二层for循环遍历数组寻找最大值 -时间复杂度:O(n^2) +时间复杂度:O(n^2) 空间复杂度:O(1) -``` +```C++ class Solution { public: int maxSubArray(vector& nums) { @@ -41,7 +45,7 @@ public: 以上暴力的解法C++勉强可以过,其他语言就不确定了。 -## 贪心解法 +## 贪心解法 **贪心贪的是哪里呢?** @@ -49,17 +53,17 @@ public: 局部最优:当前“连续和”为负数的时候立刻放弃,从下一个元素重新计算“连续和”,因为负数加上下一个元素 “连续和”只会越来越小。 -全局最优:选取最大“连续和” +全局最优:选取最大“连续和” **局部最优的情况下,并记录最大的“连续和”,可以推出全局最优**。 从代码角度上来讲:遍历nums,从头开始用count累积,如果count一旦加上nums[i]变为负数,那么就应该从nums[i+1]开始从0累积count了,因为已经变为负数的count,只会拖累总和。 -**这相当于是暴力解法中的不断调整最大子序和区间的起始位置**。 +**这相当于是暴力解法中的不断调整最大子序和区间的起始位置**。 -**那有同学问了,区间终止位置不用调整么? 如何才能得到最大“连续和”呢?** +**那有同学问了,区间终止位置不用调整么? 如何才能得到最大“连续和”呢?** 区间的终止位置,其实就是如果count取到最大值了,及时记录下来了。例如如下代码: @@ -67,17 +71,17 @@ public: if (count > result) result = count; ``` -**这样相当于是用result记录最大子序和区间和(变相的算是调整了终止位置)**。 +**这样相当于是用result记录最大子序和区间和(变相的算是调整了终止位置)**。 如动画所示: - +![53.最大子序和](https://code-thinking.cdn.bcebos.com/gifs/53.%E6%9C%80%E5%A4%A7%E5%AD%90%E5%BA%8F%E5%92%8C.gif) 红色的起始位置就是贪心每次取count为正数的时候,开始一个区间的统计。 那么不难写出如下C++代码(关键地方已经注释) -``` +```C++ class Solution { public: int maxSubArray(vector& nums) { @@ -94,18 +98,18 @@ public: } }; ``` -时间复杂度:O(n) -空间复杂度:O(1) +时间复杂度:O(n) +空间复杂度:O(1) 当然题目没有说如果数组为空,应该返回什么,所以数组为空的话返回啥都可以了。 -## 动态规划 +## 动态规划 当然本题还可以用动态规划来做,当前[「代码随想录」](https://img-blog.csdnimg.cn/20201124161234338.png)主要讲解贪心系列,后续到动态规划系列的时候会详细讲解本题的dp方法。 那么先给出我的dp代码如下,有时间的录友可以提前做一做: -``` +```C++ class Solution { public: int maxSubArray(vector& nums) { @@ -122,20 +126,31 @@ public: }; ``` -时间复杂度:O(n) -空间复杂度:O(n) +时间复杂度:O(n) +空间复杂度:O(n) -# 总结 +## 总结 本题的贪心思路其实并不好想,这也进一步验证了,别看贪心理论很直白,有时候看似是常识,但贪心的题目一点都不简单! 后续将介绍的贪心题目都挺难的,哈哈,所以贪心很有意思,别小看贪心! -就酱,如果感觉「代码随想录」干货满满,就帮忙转发一波吧,让更多的小伙伴知道这里! +## 其他语言版本 -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +Java: + + +Python: + + +Go: + +----------------------- +* 作者微信:[程序员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/0053.最大子序和(动态规划).md b/problems/0053.最大子序和(动态规划).md new file mode 100644 index 00000000..957d8b6c --- /dev/null +++ b/problems/0053.最大子序和(动态规划).md @@ -0,0 +1,112 @@ +

+ + + + +

+

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

+ +## 53. 最大子序和 + +题目地址:https://leetcode-cn.com/problems/maximum-subarray/ + +给定一个整数数组 nums ,找到一个具有最大和的连续子数组(子数组最少包含一个元素),返回其最大和。 + +示例: +输入: [-2,1,-3,4,-1,2,1,-5,4] +输出: 6 +解释: 连续子数组 [4,-1,2,1] 的和最大,为 6。 + +## 思路 + +这道题之前我们在讲解贪心专题的时候用贪心算法解决过一次,[贪心算法:最大子序和](https://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg)。 + +这次我们用动态规划的思路再来分析一次。 + +动规五部曲如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i]:包括下标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://img-blog.csdnimg.cn/20210303104129101.png) + +**注意最后的结果可不是dp[nums.size() - 1]!** ,而是dp[6]。 + +在回顾一下dp[i]的定义:包括下标i之前的最大连续子序列和为dp[i]。 + +那么我们要找最大的连续子序列,就应该找每一个i为终点的连续最大子序列。 + +所以在递推公式的时候,可以直接选出最大的dp[i]。 + +以上动规五部曲分析完毕,完整代码如下: + +```C++ +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://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg) + +动规的解法还是很直接的。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0055.跳跃游戏.md b/problems/0055.跳跃游戏.md index 2f0ab898..0cad1fa7 100644 --- a/problems/0055.跳跃游戏.md +++ b/problems/0055.跳跃游戏.md @@ -1,7 +1,13 @@ +

+ + + + +

+

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

-> 通知 -# 55. 跳跃游戏 +## 55. 跳跃游戏 题目链接:https://leetcode-cn.com/problems/jump-game/ @@ -11,20 +17,20 @@ 判断你是否能够到达最后一个位置。 -示例 1: -输入: [2,3,1,1,4] -输出: true -解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 +示例 1: +输入: [2,3,1,1,4] +输出: true +解释: 我们可以先跳 1 步,从位置 0 到达 位置 1, 然后再从位置 1 跳 3 步到达最后一个位置。 -示例 2: -输入: [3,2,1,0,4] -输出: false -解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。 +示例 2: +输入: [3,2,1,0,4] +输出: false +解释: 无论怎样,你总会到达索引为 3 的位置。但该位置的最大跳跃长度是 0 , 所以你永远不可能到达最后一个位置。 -## 思路 +## 思路 -刚看到本题一开始可能想:当前位置元素如果是3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢? +刚看到本题一开始可能想:当前位置元素如果是3,我究竟是跳一步呢,还是两步呢,还是三步呢,究竟跳几步才是最优呢? 其实跳几步无所谓,关键在于可跳的覆盖范围! @@ -32,9 +38,9 @@ 这个范围内,别管是怎么跳的,反正一定可以跳过来。 -**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!** +**那么这个问题就转化为跳跃覆盖范围究竟可不可以覆盖到终点!** -每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。 +每次移动取最大跳跃步数(得到最大的覆盖范围),每移动一个单位,就更新最大覆盖范围。 **贪心算法局部最优解:每次取最大跳跃步数(取最大覆盖范围),整体最优解:最后得到整体最大覆盖范围,看是否能到终点**。 @@ -66,7 +72,7 @@ public: } }; ``` -# 总结 +## 总结 这道题目关键点在于:不用拘泥于每次究竟跳跳几步,而是看覆盖范围,覆盖范围内一定是可以跳过来的,不用管是怎么跳的。 @@ -76,4 +82,22 @@ public: **是真的就是没什么联系,因为贪心无套路!**没有个整体的贪心框架解决一些列问题,只能是接触各种类型的题目锻炼自己的贪心思维! -就酱,「代码随想录」值得推荐给身边的朋友同学们! +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0056.合并区间.md b/problems/0056.合并区间.md index 2d891c36..f939325f 100644 --- a/problems/0056.合并区间.md +++ b/problems/0056.合并区间.md @@ -1,33 +1,68 @@ -## 题目链接 -https://leetcode-cn.com/problems/merge-intervals/ +

+ + + + +

+

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

-## 思路 -这道题目看起来就是一道模拟类的题,但其实是一道贪心题目! -按照左区间排序之后,每次合并都取最大的右区间,这样就可以合并更多的区间了。 +## 56. 合并区间 -那有同学问了,这不是废话么? 当然要取最大的右区间啊。 +题目链接:https://leetcode-cn.com/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]. -合并思路:如果 `intervals[i][0] < intervals[i - 1][1]` 即intervals[i]起始位置 < intervals[i - 1]终止位置,则一定有重复,需要合并。 +示例 2: +输入: intervals = [[1,4],[4,5]] +输出: [[1,5]] +解释: 区间 [1,4] 和 [4,5] 可被视为重叠区间。 +注意:输入类型已于2019年4月15日更改。 请重置默认代码定义以获取新方法签名。 -如图所示: +提示: - +* intervals[i][0] <= intervals[i][1] + +## 思路 + +大家应该都感觉到了,此题一定要排序,那么按照左边界排序,还是右边界排序呢? + +都可以! + +那么我按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。 + +局部最优可以推出全局最优,找不出反例,试试贪心。 + +那有同学问了,本来不就应该合并最大右边界么,这和贪心有啥关系? + +有时候贪心就是常识!哈哈 + +按照左边界从小到大排序之后,如果 `intervals[i][0] < intervals[i - 1][1]` 即intervals[i]左边界 < intervals[i - 1]右边界,则一定有重复,因为intervals[i]的左边界一定是大于等于intervals[i - 1]的左边界。 + +即:intervals[i]的左边界在intervals[i - 1]左边界和右边界的范围内,那么一定有重复! + +这么说有点抽象,看图:(**注意图中区间都是按照左边界排序之后了**) + +![56.合并区间](https://img-blog.csdnimg.cn/20201223200632791.png) + +知道如何判断重复之后,剩下的就是合并了,如何去模拟合并区间呢? + +其实就是用合并区间后左边界和右边界,作为一个新的区间,加入到result数组里就可以了。如果没有合并就把原区间加入到result数组。 C++代码如下: -``` +```C++ class Solution { public: - // 按照区间左边界排序 + // 按照区间左边界从小到大排序 static bool cmp (const vector& a, const vector& b) { return a[0] < b[0]; } - vector> merge(vector>& intervals) { vector> result; if (intervals.size() == 0) return result; @@ -36,13 +71,14 @@ public: int length = intervals.size(); for (int i = 1; i < length; i++) { - int start = intervals[i - 1][0]; - int end = intervals[i - 1][1]; + int start = intervals[i - 1][0]; // 初始为i-1区间的左边界 + int end = intervals[i - 1][1]; // 初始i-1区间的右边界 while (i < length && intervals[i][0] <= end) { // 合并区间 - end = max(end, intervals[i][1]); - if (i == length - 1) flag = true; // 最后一个区间也合并了 - i++; + end = max(end, intervals[i][1]); // 不断更新右区间 + if (i == length - 1) flag = true; // 最后一个区间也合并了 + i++; // 继续合并下一个区间 } + // start和end是表示intervals[i - 1]的左边界右边界,所以最优intervals[i]区间是否合并了要标记一下 result.push_back({start, end}); } // 如果最后一个区间没有合并,将其加入result @@ -53,3 +89,66 @@ public: } }; ``` + +当然以上代码有冗余一些,可以优化一下,如下:(思路是一样的) + +```C++ +class Solution { +public: + vector> merge(vector>& intervals) { + vector> result; + if (intervals.size() == 0) return result; + // 排序的参数使用了lamda表达式 + sort(intervals.begin(), intervals.end(), [](const vector& a, const vector& b){return a[0] < b[0];}); + + result.push_back(intervals[0]); + for (int i = 1; i < intervals.size(); i++) { + if (result.back()[1] >= intervals[i][0]) { // 合并区间 + result.back()[1] = max(result.back()[1], intervals[i][1]); + } else { + result.push_back(intervals[i]); + } + } + return result; + } +}; +``` + +* 时间复杂度:O(nlogn) ,有一个快排 +* 空间复杂度:O(1),我没有算result数组(返回值所需容器占的空间) + + +## 总结 + +对于贪心算法,很多同学都是:**如果能凭常识直接做出来,就会感觉不到自己用了贪心, 一旦第一直觉想不出来, 可能就一直想不出来了**。 + +跟着「代码随想录」刷题的录友应该感受过,贪心难起来,真的难。 + +那应该怎么办呢? + +正如我贪心系列开篇词[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中讲解的一样,贪心本来就没有套路,也没有框架,所以各种常规解法需要多接触多练习,自然而然才会想到。 + +「代码随想录」会把贪心常见的经典题目覆盖到,大家只要认真学习打卡就可以了。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0057.插入区间.md b/problems/0057.插入区间.md deleted file mode 100644 index 79dba1c4..00000000 --- a/problems/0057.插入区间.md +++ /dev/null @@ -1,90 +0,0 @@ - -# 链接 -https://leetcode-cn.com/problems/insert-interval/ - -# 思路 -这道题目合并的情况有很多种,想想都让人头疼。 - -我把这道题目化为三步: - -## 步骤一:找到需要合并的区间 - -找到插入区间需要插入或者合并的位置。 - -代码如下: - -``` -int index = 0; // intervals的索引 -while (index < intervals.size() && intervals[index][1] < newInterval[0]) { - result.push_back(intervals[index++]); -} -``` - -此时intervals[index]就需要合并的区间了 - -## 步骤二:合并区间 - -合并区间还有两种情况 - -1. intervals[index]需要合并,如图: - - - -对于这种情况,只要是intervals[index]起始位置 <= newInterval终止位置,就要一直合并下去。 - -代码如下: - -``` -while (index < intervals.size() && intervals[index][0] <= newInterval[1]) { // 注意防止越界 - newInterval[0] = min(intervals[index][0], newInterval[0]); - newInterval[1] = max(intervals[index][1], newInterval[1]); - index++; -} -``` -合并之后,将newInterval放入result就可以了 - -2. intervals[index]不用合并,插入区间直接插入就行,如图: - - - -对于这种情况,就直接把newInterval放入result就可以了 - -## 步骤三:处理合并区间之后的区间 - -合并之后,就应该把合并之后的区间,以此加入result中。 - -代码如下: - -``` -while (index < intervals.size()) { - result.push_back(intervals[index++]); -} -``` - -# 整体C++代码 - -``` -class Solution { -public: - vector> insert(vector>& intervals, vector& newInterval) { - vector> result; - int index = 0; // intervals的索引 - // 步骤一:找到需要合并的区间 - while (index < intervals.size() && intervals[index][1] < newInterval[0]) { - result.push_back(intervals[index++]); - } - // 步骤二:合并区间 - while (index < intervals.size() && intervals[index][0] <= newInterval[1]) { - newInterval[0] = min(intervals[index][0], newInterval[0]); - newInterval[1] = max(intervals[index][1], newInterval[1]); - index++; - } - result.push_back(newInterval); - // 步骤三:处理合并区间之后的区间 - while (index < intervals.size()) { - result.push_back(intervals[index++]); - } - return result; - } -}; -``` diff --git a/problems/0059.螺旋矩阵II.md b/problems/0059.螺旋矩阵II.md index 111be4c4..5e2d48d8 100644 --- a/problems/0059.螺旋矩阵II.md +++ b/problems/0059.螺旋矩阵II.md @@ -1,22 +1,16 @@ - -

- -

- - + + - -

+

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

-> 一进循环深似海,从此offer是路人 -# 题目59.螺旋矩阵II +## 59.螺旋矩阵II -题目地址:https://leetcode-cn.com/problems/spiral-matrix-ii/ +题目地址:https://leetcode-cn.com/problems/spiral-matrix-ii/ 给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。 示例: @@ -29,7 +23,7 @@ [ 7, 6, 5 ] ] -# 思路 +## 思路 这道题目可以说在面试中出现频率较高的题目,**本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。** @@ -39,13 +33,13 @@ 结果运行的时候各种问题,然后开始各种修修补补,最后发现改了这里哪里有问题,改了那里这里又跑不起来了。 -大家还记得我们在这篇文章[数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)中讲解了二分法,提到如果要写出正确的二分法一定要坚持**循环不变量原则**。 +大家还记得我们在这篇文章[数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/4X-8VRgnYRGd5LYGZ33m4w)中讲解了二分法,提到如果要写出正确的二分法一定要坚持**循环不变量原则**。 而求解本题依然是要坚持循环不变量原则。 模拟顺时针画矩阵的过程: -* 填充上行从左到右 +* 填充上行从左到右 * 填充右列从上到下 * 填充下行从右到左 * 填充左列从下到上 @@ -64,13 +58,13 @@ 这也是坚持了每条边左闭右开的原则。 -一些同学做这道题目之所以一直写不好,代码越写越乱。 +一些同学做这道题目之所以一直写不好,代码越写越乱。 就是因为在画每一条边的时候,一会左开又闭,一会左闭右闭,一会又来左闭右开,岂能不乱。 代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。 -# C++代码 +整体C++代码如下: ```C++ class Solution { @@ -122,8 +116,118 @@ public: }; ``` -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** +## 类似题目 -

- -

+* 54.螺旋矩阵 +* 剑指Offer 29.顺时针打印矩阵 + + + + +## 其他语言版本 + +Java: + +```Java +class Solution { + public int[][] generateMatrix(int n) { + int[][] res = new int[n][n]; + + // 循环次数 + int loop = n / 2; + + // 定义每次循环起始位置 + int startX = 0; + int startY = 0; + + // 定义偏移量 + int offset = 1; + + // 定义填充数字 + int count = 1; + + // 定义中间位置 + int mid = n / 2; + + + while (loop > 0) { + int i = startX; + int j = startY; + + // 模拟上侧从左到右 + for (; j startY; j--) { + res[i][j] = count++; + } + + // 模拟左侧从下到上 + for (; i > startX; i--) { + res[i][j] = count++; + } + + loop--; + + startX += 1; + startY += 1; + + offset += 2; + } + + + if (n % 2 == 1) { + res[mid][mid] = count; + } + + return res; + } +} +``` + +python: + +```python +class Solution: + def generateMatrix(self, n: int) -> List[List[int]]: + left, right, up, down = 0, n-1, 0, n-1 + matrix = [ [0]*n for _ in range(n)] + num = 1 + while left<=right and up<=down: + # 填充左到右 + for i in range(left, right+1): + matrix[up][i] = num + num += 1 + up += 1 + # 填充上到下 + for i in range(up, down+1): + matrix[i][right] = num + num += 1 + right -= 1 + # 填充右到左 + for i in range(right, left-1, -1): + matrix[down][i] = num + num += 1 + down -= 1 + # 填充下到上 + for i in range(down, up-1, -1): + matrix[i][left] = num + num += 1 + left += 1 + return matrix +``` + + + +----------------------- +* 作者微信:[程序员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/0062.不同路径.md b/problems/0062.不同路径.md index 7eee77b3..e3a6da8c 100644 --- a/problems/0062.不同路径.md +++ b/problems/0062.不同路径.md @@ -1,7 +1,53 @@ +

+ + + + +

+

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

-# 思路 +## 62.不同路径 -## 深搜 +题目链接:https://leetcode-cn.com/problems/unique-paths/ + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。 + +问总共有多少条不同的路径? + +示例 1: + +![](https://img-blog.csdnimg.cn/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 + +## 思路 + +### 深搜 这道题目,刚一看最直观的想法就是用图论里的深搜,来枚举出来有多少种路径。 @@ -28,27 +74,36 @@ public: }; ``` -大家如果提交了代码就会发现超时了! +**大家如果提交了代码就会发现超时了!** 来分析一下时间复杂度,这个深搜的算法,其实就是要遍历整个二叉树。 这颗树的深度其实就是m+n-1(深度按从1开始计算)。 -那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有把搜索节点都遍历到,只是近似而已) +那二叉树的节点个数就是 2^(m + n - 1) - 1。可以理解深搜的算法就是遍历了整个满二叉树(其实没有遍历整个满二叉树,只是近似而已) 所以上面深搜代码的时间复杂度为O(2^(m + n - 1) - 1),可以看出,这是指数级别的时间复杂度,是非常大的。 -## 动态规划 +### 动态规划 机器人从(0 , 0) 位置触发,到(m - 1, n - 1)终点。 -按照动规三部曲来分析: +按照动规五部曲来分析: -* dp数组表述啥 +1. 确定dp数组(dp table)以及下标的含义 -这里设计一个dp二维数组,dp[i][j] 表示从(0 ,0)出发,到(i, j) 有几条不同的路径。 +dp[i][j] :表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。 -* dp数组的初始化 + +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]也同理。 @@ -59,19 +114,19 @@ 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 - 1][j] + dp[i][j - 1],dp[i][j]都是从其上方和左方推导而来,那么从左到右一层一层遍历就可以了。 -此时在回顾一下 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] = dp[i-1][j] + dp[i][j - 1],因为dp[i][j]只有这两个方向过来。 +5. 举例推导dp数组 如图所示: ![62.不同路径1](https://img-blog.csdnimg.cn/20201209113631392.png) -C++代码如下: +以上动规五部曲分析完毕,C++代码如下: ```C++ class Solution { @@ -89,7 +144,7 @@ public: } }; ``` -* 时间复杂度:O(m * n) +* 时间复杂度:O(m * n) * 空间复杂度:O(m * n) 其实用一个一维数组(也可以理解是滚动数组)就可以了,但是不利于理解,可以优化点空间,建议先理解了二维,在理解一维,C++代码如下: @@ -109,12 +164,12 @@ public: } }; ``` -* 时间复杂度:O(m * n) +* 时间复杂度:O(m * n) * 空间复杂度:O(n) -# 数论方法 +### 数论方法 -在这个图中,可以看出一共 m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。 +在这个图中,可以看出一共m,n的话,无论怎么走,走到终点都需要 m + n - 2 步。 ![62.不同路径](https://img-blog.csdnimg.cn/20201209113602700.png) @@ -130,7 +185,9 @@ public: **求组合的时候,要防止两个int相乘溢出!** 所以不能把算式的分子都算出来,分母都算出来再做除法。 -``` +例如如下代码是不行的。 + +```C++ class Solution { public: int uniquePaths(int m, int n) { @@ -145,9 +202,9 @@ public: ``` -需要在计算分子的时候,不算除以分母,代码如下: +需要在计算分子的时候,不断除以分母,代码如下: -``` +```C++ class Solution { public: int uniquePaths(int m, int n) { @@ -167,10 +224,37 @@ public: }; ``` -计算组合问题的代码还是有难度的,特别是处理溢出的情况! +时间复杂度:O(m) +空间复杂度:O(1) -最后这个代码还有点复杂了,还是可以优化,我就不继续优化了,有空在整理一下,哈哈,就酱! +**计算组合问题的代码还是有难度的,特别是处理溢出的情况!** + +## 总结 + +本文分别给出了深搜,动规,数论三种方法。 + +深搜当然是超时了,顺便分析了一下使用深搜的时间复杂度,就可以看出为什么超时了。 + +然后在给出动规的方法,依然是使用动规五部曲,这次我们就要考虑如何正确的初始化了,初始化和遍历顺序其实也很重要! + +就酱,循序渐进学算法,认准「代码随想录」! + +## 其他语言版本 + + +Java: + + +Python: + + +Go: +----------------------- +* 作者微信:[程序员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/0063.不同路径II.md b/problems/0063.不同路径II.md new file mode 100644 index 00000000..311f712e --- /dev/null +++ b/problems/0063.不同路径II.md @@ -0,0 +1,247 @@ +

+ + + + +

+

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

+ +## 63. 不同路径 II + +题目链接:https://leetcode-cn.com/problems/unique-paths-ii/ + +一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为“Start” )。 + +机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为“Finish”)。 + +现在考虑网格中有障碍物。那么从左上角到右下角将会有多少条不同的路径? + +![](https://img-blog.csdnimg.cn/20210111204901338.png) + +网格中的障碍物和空位置分别用 1 和 0 来表示。 + +示例 1: + +![](https://img-blog.csdnimg.cn/20210111204939971.png) + +输入:obstacleGrid = [[0,0,0],[0,1,0],[0,0,0]] +输出:2 +解释: +3x3 网格的正中间有一个障碍物。 +从左上角到右下角一共有 2 条不同的路径: +1. 向右 -> 向右 -> 向下 -> 向下 +2. 向下 -> 向下 -> 向右 -> 向右 + +示例 2: + +![](https://img-blog.csdnimg.cn/20210111205857918.png) + +输入:obstacleGrid = [[0,1],[0,0]] +输出:1 + +提示: + +* m == obstacleGrid.length +* n == obstacleGrid[i].length +* 1 <= m, n <= 100 +* obstacleGrid[i][j] 为 0 或 1 + + +## 思路 + +这道题相对于[62.不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A) 就是有了障碍。 + +第一次接触这种题目的同学可能会有点懵,这有障碍了,应该怎么算呢? + +[62.不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A)中我们已经详细分析了没有障碍的情况,有障碍的话,其实就是标记对应的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)。 + +所以代码为: + +``` +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://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A)不同路径中我们给出如下的初始化: + +``` +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://img-blog.csdnimg.cn/20210104114513928.png) + +下标(0, j)的初始化情况同理。 + +所以本题初始化代码为: + +```C++ +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]一定是有数值。 + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210104114548983.png) + +对应的dp table 如图: + +![63.不同路径II2](https://img-blog.csdnimg.cn/20210104114610256.png) + +如果这个图看不同,建议在理解一下递归公式,然后照着文章中说的遍历顺序,自己推导一下​!​ + +动规五部分分析完毕,对应C++代码如下: + +```C++ +class Solution { +public: + int uniquePathsWithObstacles(vector>& obstacleGrid) { + int m = obstacleGrid.size(); + int n = obstacleGrid[0].size(); + 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) + +至于能不能优化空间降为一维dp数组,我感觉不太行,因为要考虑障碍,如果把这些障碍压缩到一行,结果一定就不一样了。 + +## 总结 + +本题是[62.不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A)的障碍版,整体思路大体一致。 + +但就算是做过62.不同路径,在做本题也会有感觉遇到障碍无从下手。 + +其实只要考虑到,遇到障碍dp[i][j]保持0就可以了。 + +也有一些小细节,例如:初始化的部分,很容易忽略了障碍之后应该都是0的情况。 + +就酱,「代码随想录」值得推荐给身边学算法的同学朋友们,关注后都会发现相见恨晚! + + +## 其他语言版本 + +Java: + +```java +class Solution { + public int uniquePathsWithObstacles(int[][] obstacleGrid) { + int n = obstacleGrid.length, m = obstacleGrid[0].length; + int[][] dp = new int[n][m]; + dp[0][0] = 1 - obstacleGrid[0][0]; + for (int i = 1; i < m; i++) { + if (obstacleGrid[0][i] == 0 && dp[0][i - 1] == 1) { + dp[0][i] = 1; + } + } + for (int i = 1; i < n; i++) { + if (obstacleGrid[i][0] == 0 && dp[i - 1][0] == 1) { + dp[i][0] = 1; + } + } + for (int i = 1; i < n; i++) { + for (int j = 1; j < m; j++) { + if (obstacleGrid[i][j] == 1) continue; + dp[i][j] = dp[i - 1][j] + dp[i][j - 1]; + } + } + return dp[n - 1][m - 1]; + } +} +``` + + +Python: + +```python +class Solution: + def uniquePathsWithObstacles(self, obstacleGrid: List[List[int]]) -> int: + # 构造一个DP table + row = len(obstacleGrid) + col = len(obstacleGrid[0]) + dp = [[0 for _ in range(col)] for _ in range(row)] + + dp[0][0] = 1 if obstacleGrid[0][0] != 1 else 0 + if dp[0][0] == 0: return 0 # 如果第一个格子就是障碍,return 0 + # 第一行 + for i in range(1, col): + if obstacleGrid[0][i] != 1: + dp[0][i] = dp[0][i-1] + + # 第一列 + for i in range(1, row): + if obstacleGrid[i][0] != 1: + dp[i][0] = dp[i-1][0] + print(dp) + + for i in range(1, row): + for j in range(1, col): + if obstacleGrid[i][j] != 1: + dp[i][j] = dp[i-1][j] + dp[i][j-1] + return dp[-1][-1] +``` + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0070.爬楼梯.md b/problems/0070.爬楼梯.md index 37a536ed..6ae6adc7 100644 --- a/problems/0070.爬楼梯.md +++ b/problems/0070.爬楼梯.md @@ -1,15 +1,175 @@ +

+ + + + +

+

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

-dp里求排列,1 2 步 和 2 1 步都是上三个台阶,但不一样! +## 70. 爬楼梯 -这是求排列 +假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 + +每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? + +注意:给定 n 是一个正整数。 + +示例 1: +输入: 2 +输出: 2 +解释: 有两种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 +2. 2 阶 + +示例 2: +输入: 3 +输出: 3 +解释: 有三种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 + 1 阶 +2. 1 阶 + 2 阶 +3. 2 阶 + 1 阶 + + +## 思路 + +本题大家如果没有接触过的话,会感觉比较难,多举几个例子,就可以发现其规律。 + +爬到第一层楼梯有一种方法,爬到二层楼梯有两种方法。 + +那么第一层楼梯再跨两步就到第三层 ,第二层楼梯再跨一步就到第三层。 + +所以到第三层楼梯的状态可以由第二层楼梯 和 到第一层楼梯状态推导出来,那么就可以想到动态规划了。 + +我们来分析一下,动规五部曲: + +定义一个一维数组来记录不同楼层的状态 + +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://img-blog.csdnimg.cn/20210105202546299.png) + +如果代码出问题了,就把dp table 打印出来,看看究竟是不是和自己推导的一样。 + +**此时大家应该发现了,这不就是斐波那契数列么!** + +唯一的区别是,没有讨论dp[0]应该是什么,因为dp[0]在本题没有意义! + +以上五部分析完之后,C++代码如下: + +```C++ +// 版本一 +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) + +当然依然也可以,优化一下空间复杂度,代码如下: + +```C++ +// 版本二 +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阶楼顶。 + +这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,所以后续我在讲解背包问题的时候,今天这道题还会拿从背包问题的角度上来再讲一遍。 + +这里我先给出我的实现代码: + +```C++ 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 <= 2; j++) { + for (int j = 1; j <= m; j++) { // 把m换成2,就可以AC爬楼梯这道题 if (i - j >= 0) dp[i] += dp[i - j]; } } @@ -17,3 +177,65 @@ public: } }; ``` + +代码中m表示最多可以爬m个台阶。 + +**以上代码不能运行哈,我主要是为了体现只要把m换成2,粘过去,就可以AC爬楼梯这道题,不信你就粘一下试试,哈哈**。 + + +**此时我就发现一个绝佳的大厂面试题**,第一道题就是单纯的爬楼梯,然后看候选人的代码实现,如果把dp[0]的定义成1了,就可以发难了,为什么dp[0]一定要初始化为1,此时可能候选人就要强行给dp[0]应该是1找各种理由。那这就是一个考察点了,对dp[i]的定义理解的不深入。 + +然后可以继续发难,如果一步一个台阶,两个台阶,三个台阶,直到 m个台阶,有多少种方法爬到n阶楼顶。这道题目leetcode上并没有原题,绝对是考察候选人算法能力的绝佳好题。 + +这一连套问下来,候选人算法能力如何,面试官心里就有数了。 + +**其实大厂面试最喜欢问题的就是这种简单题,然后慢慢变化,在小细节上考察候选人**。 + + + +## 总结 + +这道题目和[动态规划:斐波那契数](https://mp.weixin.qq.com/s/ko0zLJplF7n_4TysnPOa_w)题目基本是一样的,但是会发现本题相比[动态规划:斐波那契数](https://mp.weixin.qq.com/s/ko0zLJplF7n_4TysnPOa_w)难多了,为什么呢? + +关键是 [动态规划:斐波那契数](https://mp.weixin.qq.com/s/ko0zLJplF7n_4TysnPOa_w) 题目描述就已经把动规五部曲里的递归公式和如何初始化都给出来了,剩下几部曲也自然而然的推出来了。 + +而本题,就需要逐个分析了,大家现在应该初步感受出[关于动态规划,你该了解这些!](https://leetcode-cn.com/circle/article/tNuNnM/)里给出的动规五部曲了。 + +简单题是用来掌握方法论的,例如昨天斐波那契的题目够简单了吧,但昨天和今天可以使用一套方法分析出来的,这就是方法论! + +所以不要轻视简单题,那种凭感觉就刷过去了,其实和没掌握区别不大,只有掌握方法论并说清一二三,才能触类旁通,举一反三哈! + +就酱,循序渐进学算法,认准「代码随想录」! + + +## 其他语言版本 + + +Java: + + +Python: + +```python +class Solution: + def climbStairs(self, n: int) -> int: + # dp[i]表示爬到第i级楼梯的种数, (1, 2) (2, 1)是两种不同的类型 + dp = [0] * (n + 1) + dp[0] = 1 + for i in range(n+1): + for j in range(1, 3): + if i>=j: + dp[i] += dp[i-j] + return dp[-1] +``` + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0070.爬楼梯完全背包版本.md b/problems/0070.爬楼梯完全背包版本.md new file mode 100644 index 00000000..beda45d5 --- /dev/null +++ b/problems/0070.爬楼梯完全背包版本.md @@ -0,0 +1,144 @@ +

+ + + + +

+

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

+# 动态规划:以前我没得选,现在我选择再爬一次! + +之前讲这道题目的时候,因为还没有讲背包问题,所以就只是讲了一下爬楼梯最直接的动规方法(斐波那契)。 + +**这次终于讲到了背包问题,我选择带录友们再爬一次楼梯!** + +## 70. 爬楼梯 + +链接:https://leetcode-cn.com/problems/climbing-stairs/ + +假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 + +每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢? + +注意:给定 n 是一个正整数。 + +示例 1: +输入: 2 +输出: 2 +解释: 有两种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 +2. 2 阶 + +示例 2: +输入: 3 +输出: 3 +解释: 有三种方法可以爬到楼顶。 +1. 1 阶 + 1 阶 + 1 阶 +2. 1 阶 + 2 阶 +3. 2 阶 + 1 阶 + +## 思路 + +这道题目 我们在[动态规划:爬楼梯](https://mp.weixin.qq.com/s/Ohop0jApSII9xxOMiFhGIw) 中已经讲过一次了,原题其实是一道简单动规的题目。 + +既然这么简单为什么还要讲呢,其实本题稍加改动就是一道面试好题。 + +**改为:一步一个台阶,两个台阶,三个台阶,.......,直到 m个台阶。问有多少种不同的方法可以爬到楼顶呢?** + +1阶,2阶,.... m阶就是物品,楼顶就是背包。 + +每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。 + +问跳到楼顶有几种方法其实就是问装满背包有几种方法。 + +**此时大家应该发现这就是一个完全背包问题了!** + +和昨天的题目[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)基本就是一道题了。 + +动规五部曲分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[i]:爬到有i个台阶的楼顶,有dp[i]种方法**。 + +2. 确定递推公式 + +在[动态规划:494.目标和](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw) 、 [动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)、[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)中我们都讲过了,求装满背包有几种方法,递推公式一般都是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://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)几乎是一样的,这里我就不再重复举例了。 + + +以上分析完毕,C++代码如下: +``` +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的代码了。 + +## 总结 + +**本题看起来是一道简单题目,稍稍进阶一下其实就是一个完全背包!** + +如果我来面试的话,我就会先给候选人出一个 本题原题,看其表现,如果顺利写出来,进而在要求每次可以爬[1 - m]个台阶应该怎么写。 + +顺便再考察一下两个for循环的嵌套顺序,为什么target放外面,nums放里面。 + +这就能考察对背包问题本质的掌握程度,候选人是不是刷题背公式,一眼就看出来了。 + +这么一连套下来,如果候选人都能答出来,相信任何一位面试官都是非常满意的。 + +**本题代码不长,题目也很普通,但稍稍一进阶就可以考察完全背包,而且题目进阶的内容在leetcode上并没有原题,一定程度上就可以排除掉刷题党了,简直是面试题目的绝佳选择!** + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0072.编辑距离.md b/problems/0072.编辑距离.md new file mode 100644 index 00000000..8e6e0187 --- /dev/null +++ b/problems/0072.编辑距离.md @@ -0,0 +1,215 @@ +

+ + + + +

+

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

+ +## 72. 编辑距离 + +给你两个单词 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 由小写英文字母组成 + + +## 思路 + +编辑距离终于来了,这道题目如果大家没有了解动态规划的话,会感觉超级复杂。 + +编辑距离是用动规来解决的经典题目,这道题目看上去好像很复杂,但用动规可以很巧妙的算出最少编辑距离。 + +接下来我依然使用动规五部曲,对本题做一个详细的分析: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i][j] 表示以下标i-1为结尾的字符串word1,和以下标j-1为结尾的字符串word2,最近编辑距离为dp[i][j]**。 + +这里在强调一下:为啥要表示下标i-1为结尾的字符串呢,为啥不表示下标i为结尾的字符串呢? + +用i来表示也可以! 但我统一以下标i-1为结尾的字符串,在下面的递归公式中会容易理解一点。 + +2. 确定递推公式 + +在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下: + +* 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; + +递归公式代码如下: + +```C++ +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++代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210114162113131.jpg) + +所以在dp矩阵中一定是从左到右从上到下去遍历。 + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210114162132300.jpg) + +以上动规五部分析完毕,C++代码如下: + +```C++ +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()]; + } +}; +``` + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0077.组合.md b/problems/0077.组合.md index 0a7187d1..3dfb5216 100644 --- a/problems/0077.组合.md +++ b/problems/0077.组合.md @@ -1,5 +1,13 @@ +

+ + + + +

+

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

+ + -> 回溯法的第一道题目,就不简单呀! # 第77题. 组合 @@ -21,7 +29,8 @@ 也可以直接看我的B站视频:[带你学透回溯算法-组合问题(对应力扣题目:77.组合)](https://www.bilibili.com/video/BV1ti4y1L7cv#reply3733925949) -# 思路 +## 思路 + 本题这是回溯法的经典题目。 @@ -164,7 +173,7 @@ for循环每次从startIndex开始遍历,然后用path保存取到的节点i 代码如下: -``` +```C++ for (int i = startIndex; i <= n; i++) { // 控制树的横向遍历 path.push_back(i); // 处理节点 backtracking(n, k, i + 1); // 递归:控制树的纵向遍历,注意下一层搜索要从i+1开始 @@ -179,7 +188,7 @@ backtracking的下面部分就是回溯的操作了,撤销本次处理的结 关键地方都讲完了,组合问题C++完整代码如下: -``` +```C++ class Solution { private: vector> result; // 存放符合条件结果的集合 @@ -235,14 +244,114 @@ void backtracking(参数) { 接着用回溯法三部曲,逐步分析了函数参数、终止条件和单层搜索的过程。 -**本题其实是可以剪枝优化的,大家可以思考一下,具体如何剪枝我会在下一篇详细讲解,敬请期待!** +# 剪枝优化 -**就酱,如果对你有帮助,就帮Carl转发一下吧,让更多的同学发现这里!** +我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。 + +在遍历的过程中有如下代码: + +``` +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://img-blog.csdnimg.cn/20210130194335207.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为本次搜索的起始位置 +``` + +优化后整体代码如下: + +``` +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; + } +}; +``` + +# 剪枝总结 + +本篇我们准对求组合问题的回溯法代码做了剪枝优化,这个优化如果不画图的话,其实不好理解,也不好讲清楚。 + +所以我依然是把整个回溯过程抽象为一颗树形结构,然后可以直观的看出,剪枝究竟是剪的哪里。 -**[本题剪枝操作文章链接](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA)** -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0077.组合优化.md b/problems/0077.组合优化.md index 6f12abd0..2af123d1 100644 --- a/problems/0077.组合优化.md +++ b/problems/0077.组合优化.md @@ -1,9 +1,19 @@ +

+ + + + +

+

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

+ + -> 如果想在电脑上看文章的话,可以看这里:https://github.com/youngyangyang04/leetcode-master,已经按照顺序整理了「代码随想录」的所有文章,可以fork到自己仓库里,随时复习。**那么重点来了,来都来了,顺便给一个star吧,哈哈** 在[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)中,我们通过回溯搜索法,解决了n个数中求k个数的组合问题。 +> 可以直接看我的B栈视频讲解:[带你学透回溯算法-组合问题的剪枝操作](https://www.bilibili.com/video/BV1wi4y157er) + 文中的回溯法是可以剪枝优化的,本篇我们继续来看一下题目77. 组合。 链接:https://leetcode-cn.com/problems/combinations/ @@ -23,7 +33,7 @@ private: return; } for (int i = startIndex; i <= n; i++) { - path.push_back(i); // 处理节点 + path.push_back(i); // 处理节点 backtracking(n, k, i + 1); // 递归 path.pop_back(); // 回溯,撤销处理的节点 } @@ -38,17 +48,17 @@ public: }; ``` -## 剪枝优化 +# 剪枝优化 我们说过,回溯法虽然是暴力搜索,但也有时候可以有点剪枝优化一下的。 -在遍历的过程中有如下代码: +在遍历的过程中有如下代码: ``` -for (int i = startIndex; i <= n; i++) { - path.push_back(i); - backtracking(n, k, i + 1); - path.pop_back(); +for (int i = startIndex; i <= n; i++) { + path.push_back(i); + backtracking(n, k, i + 1); + path.pop_back(); } ``` @@ -58,9 +68,10 @@ for (int i = startIndex; i <= n; i++) { 这么说有点抽象,如图所示: - +![77.组合4](https://img-blog.csdnimg.cn/20210130194335207.png) -图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。 + +图中每一个节点(图中为矩形),就代表本层的一个for循环,那么每一层的for循环从第二个数开始遍历的话,都没有意义,都是无效遍历。 **所以,可以剪枝的地方就在递归中每一层的for循环所选择的起始位置**。 @@ -68,7 +79,7 @@ for (int i = startIndex; i <= n; i++) { 注意代码中i,就是for循环里选择的起始位置。 ``` -for (int i = startIndex; i <= n; i++) { +for (int i = startIndex; i <= n; i++) { ``` 接下来看一下优化过程如下: @@ -81,7 +92,7 @@ for (int i = startIndex; i <= n; i++) { 为什么有个+1呢,因为包括起始位置,我们要是一个左闭的集合。 -举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。 +举个例子,n = 4,k = 3, 目前已经选取的元素为0(path.size为0),n - (k - 0) + 1 即 4 - ( 3 - 0) + 1 = 2。 从2开始搜索都是合理的,可以是组合[2, 3, 4]。 @@ -98,7 +109,7 @@ for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) // i为本次搜 ``` class Solution { private: - vector> result; + vector> result; vector path; void backtracking(int n, int k, int startIndex) { if (path.size() == k) { @@ -106,7 +117,7 @@ private: return; } for (int i = startIndex; i <= n - (k - path.size()) + 1; i++) { // 优化的地方 - path.push_back(i); // 处理节点 + path.push_back(i); // 处理节点 backtracking(n, k, i + 1); path.pop_back(); // 回溯,撤销处理的节点 } @@ -120,10 +131,34 @@ public: }; ``` -# 总结 +# 总结 本篇我们准对求组合问题的回溯法代码做了剪枝优化,这个优化如果不画图的话,其实不好理解,也不好讲清楚。 所以我依然是把整个回溯过程抽象为一颗树形结构,然后可以直观的看出,剪枝究竟是剪的哪里。 **就酱,学到了就帮Carl转发一下吧,让更多的同学知道这里!** + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0078.子集.md b/problems/0078.子集.md index 8a2ff8a3..8c68843d 100644 --- a/problems/0078.子集.md +++ b/problems/0078.子集.md @@ -1,36 +1,43 @@ -> 认识本质之后,这就是一道模板题 +

+ + + + +

+

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

-# 第78题. 子集 -题目地址:https://leetcode-cn.com/problems/subsets/ +## 第78题. 子集 + +题目地址:https://leetcode-cn.com/problems/subsets/ 给定一组不含重复元素的整数数组 nums,返回该数组所有可能的子集(幂集)。 说明:解集不能包含重复的子集。 示例: -输入: nums = [1,2,3] -输出: -[ - [3], -  [1], -  [2], -  [1,2,3], -  [1,3], -  [2,3], -  [1,2], -  [] -] +输入: nums = [1,2,3] +输出: +[ + [3], +  [1], +  [2], +  [1,2,3], +  [1,3], +  [2,3], +  [1,2], +  [] +] -# 思路 +## 思路 求子集问题和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)和[回溯算法:分割问题!](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)又不一样了。 -如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,**那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!** +如果把 子集问题、组合问题、分割问题都抽象为一棵树的话,**那么组合问题和分割问题都是收集树的叶子节点,而子集问题是找树的所有节点!** -其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 +其实子集也是一种组合问题,因为它的集合是无序的,子集{1,2} 和 子集{2,1}是一样的。 -**那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!** +**那么既然是无序,取过的元素不会重复取,写回溯算法的时候,for就要从startIndex开始,而不是从0开始!** 有同学问了,什么时候for可以从0开始呢? @@ -42,9 +49,9 @@ 从图中红线部分,可以看出**遍历这个树的时候,把所有节点都记录下来,就是要求的子集集合**。 -## 回溯三部曲 +## 回溯三部曲 -* 递归函数参数 +* 递归函数参数 全局变量数组path为子集收集元素,二维数组result存放子集组合。(也可以放到递归函数参数里) @@ -58,13 +65,13 @@ vector path; void backtracking(vector& nums, int startIndex) { ``` -* 递归终止条件 +* 递归终止条件 从图中可以看出: ![78.子集](https://img-blog.csdnimg.cn/202011232041348.png) -剩余集合为空的时候,就是叶子节点。 +剩余集合为空的时候,就是叶子节点。 那么什么时候剩余集合为空呢? @@ -76,7 +83,7 @@ if (startIndex >= nums.size()) { } ``` -**其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了**。 +**其实可以不需要加终止条件,因为startIndex >= nums.size(),本层for循环本来也结束了**。 * 单层搜索逻辑 @@ -113,7 +120,7 @@ void backtracking(参数) { 可以写出如下回溯算法C++代码: -``` +```C++ class Solution { private: vector> result; @@ -140,13 +147,13 @@ public: ``` -在注释中,可以发现可以不写终止条件,因为本来我们就要遍历整颗树。 +在注释中,可以发现可以不写终止条件,因为本来我们就要遍历整颗树。 -有的同学可能担心不写终止条件会不会无限递归? +有的同学可能担心不写终止条件会不会无限递归? 并不会,因为每次递归的下一层就是从i+1开始的。 -# 总结 +## 总结 相信大家经过了 * 组合问题: @@ -166,10 +173,22 @@ public: **而组合问题、分割问题是收集树形结构中叶子节点的结果**。 -**就酱,如果感觉收获满满,就帮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),关注后就会发现和「代码随想录」相见恨晚!** +Java: -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0083.删除排序链表中的重复元素.md b/problems/0083.删除排序链表中的重复元素.md deleted file mode 100644 index b725f7f8..00000000 --- a/problems/0083.删除排序链表中的重复元素.md +++ /dev/null @@ -1,40 +0,0 @@ - -## 题目地址 - -https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list/ - -## 思路 - -这道题目没有必要设置虚拟节点,因为不会删除头结点 - -## 代码 - -``` -/** - * Definition for singly-linked list. - * struct ListNode { - * int val; - * ListNode *next; - * ListNode(int x) : val(x), next(NULL) {} - * }; - */ -class Solution { -public: - ListNode* deleteDuplicates(ListNode* head) { - ListNode* p = head; - while (p != NULL && p->next!= NULL) { - if (p->val == p->next->val) { - ListNode* tmp = p->next; - p->next = p->next->next; - delete tmp; - } else { - p = p->next; - } - } - return head; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0084.柱状图中最大的矩形.md b/problems/0084.柱状图中最大的矩形.md deleted file mode 100644 index e5057e55..00000000 --- a/problems/0084.柱状图中最大的矩形.md +++ /dev/null @@ -1,104 +0,0 @@ - -# 链接 - -https://leetcode-cn.com/problems/largest-rectangle-in-histogram/ - -## 思路 - -``` -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)。 - -## 思考一下动态规划 - -## 单调栈 - -单调栈的思路还是不容易理解的, - -想清楚从大到小,还是从小到大, - -本题是从栈底到栈头 从小到大,和 接雨水正好反过来。 - - -``` -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; - // 第一个元素已经入栈,从下表1开始 - for (int i = 1; i < heights.size(); i++) { - // 注意heights[i] 是和heights[st.top()] 比较 ,st.top()是下表 - if (heights[i] > heights[st.top()]) { - st.push(i); - } else if (heights[i] == heights[st.top()]) { - st.pop(); // 这个可以加,可以不加,效果一样,思路不同 - st.push(i); - } else { - while (heights[i] < heights[st.top()]) { // 注意是while - int mid = st.top(); - st.pop(); - 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; - } -}; - -``` - -代码精简之后: - -``` -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; - } -}; -``` diff --git a/problems/0090.子集II.md b/problems/0090.子集II.md index c8e29763..cc5fd571 100644 --- a/problems/0090.子集II.md +++ b/problems/0090.子集II.md @@ -1,7 +1,13 @@ +

+ + + + +

+

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

-> 子集问题加去重! -# 第90题.子集II +## 第90题.子集II 题目链接:https://leetcode-cn.com/problems/subsets-ii/ @@ -9,20 +15,20 @@ 说明:解集不能包含重复的子集。 -示例: -输入: [1,2,2] -输出: -[ - [2], - [1], - [1,2,2], - [2,2], - [1,2], - [] -] +示例: +输入: [1,2,2] +输出: +[ + [2], + [1], + [1,2,2], + [2,2], + [1,2], + [] +] -# 思路 +## 思路 做本题之前一定要先做[78.子集](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)。 @@ -40,7 +46,7 @@ 本题就是其实就是[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)的基础上加上了去重,去重我们在[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)也讲过了,所以我就直接给出代码了: -# C++代码 +## C++代码 ``` class Solution { @@ -110,7 +116,45 @@ public: ``` -# 总结 +## 补充 + +本题也可以不适用used数组来去重,因为递归的时候下一个startIndex是i+1而不是0。 + +如果要是全排列的话,每次要从0开始遍历,为了跳过已入栈的元素,需要使用used。 + +代码如下: + +```C++ +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。 @@ -122,11 +166,24 @@ if (i > startIndex && nums[i] == nums[i - 1] ) { } ``` -**就酱,如果感觉融会贯通了,就把「代码随想录」介绍给自己的同学朋友吧,也许他们也需要!** -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + +----------------------- +* 作者微信:[程序员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/0093.复原IP地址.md b/problems/0093.复原IP地址.md index e691852e..4cea7a3a 100644 --- a/problems/0093.复原IP地址.md +++ b/problems/0093.复原IP地址.md @@ -1,6 +1,14 @@ -> 一些录友表示跟不上现在的节奏,想从头开始打卡学习起来,可以在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,挨个看就可以了,看文章下的留言你就会发现,有很多录友都在从头打卡,你并不孤单! +

+ + + + +

+

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

-# 93.复原IP地址 + + +## 93.复原IP地址 题目地址:https://leetcode-cn.com/problems/restore-ip-addresses/ @@ -10,32 +18,32 @@ 例如:"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"] +示例 1: +输入:s = "25525511135" +输出:["255.255.11.135","255.255.111.35"] -示例 2: -输入:s = "0000" -输出:["0.0.0.0"] +示例 2: +输入:s = "0000" +输出:["0.0.0.0"] 示例 3: -输入:s = "1111" -输出:["1.1.1.1"] +输入:s = "1111" +输出:["1.1.1.1"] -示例 4: -输入:s = "010010" -输出:["0.10.0.10","0.100.1.0"] +示例 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"] +示例 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 仅由数字组成 +提示: +0 <= s.length <= 3000 +s 仅由数字组成 -# 思路 +## 思路 做这道题目之前,最好先把[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)这个做了。 @@ -48,9 +56,9 @@ s 仅由数字组成 ![93.复原IP地址](https://img-blog.csdnimg.cn/20201123203735933.png) -## 回溯三部曲 +## 回溯三部曲 -* 递归参数 +* 递归参数 在[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)中我们就提到切割问题类似组合问题。 @@ -86,7 +94,7 @@ if (pointNum == 3) { // 逗点数量为3时,分隔结束 } ``` -* 单层搜索的逻辑 +* 单层搜索的逻辑 在[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)中已经讲过在循环遍历中如何截取子串。 @@ -118,14 +126,14 @@ for (int i = startIndex; i < s.size(); i++) { } ``` -## 判断子串是否合法 +## 判断子串是否合法 最后就是在写一个判断段位是否是有效段位了。 主要考虑到如下三点: * 段位以0为开头的数字不合法 -* 段位里有非正整数字符不合法 +* 段位里有非正整数字符不合法 * 段位如果大于255了不合法 代码如下: @@ -153,7 +161,7 @@ bool isValid(const string& s, int start, int end) { } ``` -## C++代码 +## C++代码 根据[关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)给出的回溯算法模板: @@ -175,7 +183,7 @@ void backtracking(参数) { 可以写出如下回溯算法C++代码: -``` +```C++ class Solution { private: vector result;// 记录结果 @@ -229,7 +237,7 @@ public: ``` -# 总结 +## 总结 在[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)中我列举的分割字符串的难点,本题都覆盖了。 @@ -239,15 +247,100 @@ public: 在本文的树形结构图中,我已经把详细的分析思路都画了出来,相信大家看了之后一定会思路清晰不少! -**就酱,「代码随想录」值得推荐给你的朋友们!** -一些录友表示跟不上现在的节奏,想从头开始打卡学习起来,可以在在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,挨个看就可以了,别忘了打卡。 +## 其他语言版本 -**很多录友都在从头开始打卡学习,看看前面文章的留言区就知道了,你并不孤单!** +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; + } +} +``` + +python版本: + +```python +class Solution(object): + def restoreIpAddresses(self, s): + """ + :type s: str + :rtype: List[str] + """ + ans = [] + path = [] + def backtrack(path, startIndex): + if len(path) == 4: + if startIndex == len(s): + ans.append(".".join(path[:])) + return + for i in range(startIndex+1, min(startIndex+4, len(s)+1)): # 剪枝 + string = s[startIndex:i] + if not 0 <= int(string) <= 255: + continue + if not string == "0" and not string.lstrip('0') == string: + continue + path.append(string) + backtrack(path, i) + path.pop() + + backtrack([], 0) + return ans``` +``` -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉对你有帮助,不要吝啬给一个👍吧!** - +----------------------- +* 作者微信:[程序员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/0094.二叉树的中序遍历.md b/problems/0094.二叉树的中序遍历.md deleted file mode 100644 index e63819c1..00000000 --- a/problems/0094.二叉树的中序遍历.md +++ /dev/null @@ -1,84 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-inorder-traversal/ - -## 思路 - -详细题解请看这篇:[一文学通二叉树前中后序递归法与迭代法](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) - -## C++代码 - -### 递归 -``` -class Solution { -public: - void traversal(TreeNode* cur, vector& vec) { - if (cur == NULL) return; - traversal(cur->left, vec); - vec.push_back(cur->val); - traversal(cur->right, vec); - } - vector inorderTraversal(TreeNode* root) { - vector result; - traversal(root, result); - return result; - } -}; -``` - -### 栈 -``` -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(); - st.pop(); - result.push_back(cur->val); - cur = cur->right; - } - } - return result; - } -}; -``` - -### 栈 通用模板 - -``` -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; - } -}; -``` - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0096.不同的二叉搜索树.md b/problems/0096.不同的二叉搜索树.md new file mode 100644 index 00000000..2764277c --- /dev/null +++ b/problems/0096.不同的二叉搜索树.md @@ -0,0 +1,182 @@ +

+ + + + +

+

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

+ +## 96.不同的二叉搜索树 + +题目链接:https://leetcode-cn.com/problems/unique-binary-search-trees/ + +给定一个整数 n,求以 1 ... n 为节点组成的二叉搜索树有多少种? + +示例: + +![](https://img-blog.csdnimg.cn/20210113161941835.png) + +## 思路 + +这道题目描述很简短,但估计大部分同学看完都是懵懵的状态,这得怎么统计呢? + +关于什么是二叉搜索树,我们之前在讲解二叉树专题的时候已经详细讲解过了,也可以看看这篇[二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg)在回顾一波。 + +了解了二叉搜索树之后,我们应该先举几个例子,画画图,看看有没有什么规律,如图: + +![96.不同的二叉搜索树](https://img-blog.csdnimg.cn/20210107093106367.png) + +n为1的时候有一棵树,n为2有两棵树,这个是很直观的。 + +![96.不同的二叉搜索树1](https://img-blog.csdnimg.cn/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://img-blog.csdnimg.cn/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来遍历。 + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210107093253987.png) + +当然如果自己画图举例的话,基本举例到n为3就可以了,n为4的时候,画图已经比较麻烦了。 + +**我这里列到了n为5的情况,是为了方便大家 debug代码的时候,把dp数组打出来,看看哪里有问题**。 + +综上分析完毕,C++代码如下: + +```C++ +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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0098.验证二叉搜索树.md b/problems/0098.验证二叉搜索树.md index ac827324..baa3f435 100644 --- a/problems/0098.验证二叉搜索树.md +++ b/problems/0098.验证二叉搜索树.md @@ -1,8 +1,16 @@ -## 题目地址 +

+ + + + +

+

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

-> 学习完二叉搜索树的特性了,那么就验证一波 -# 98.验证二叉搜索树 +## 98.验证二叉搜索树 + +题目地址:https://leetcode-cn.com/problems/validate-binary-search-tree/ + 给定一个二叉树,判断其是否是一个有效的二叉搜索树。 @@ -12,9 +20,9 @@ * 节点的右子树只包含大于当前节点的数。 * 所有左子树和右子树自身必须也是二叉搜索树。 - +![98.验证二叉搜索树](https://img-blog.csdnimg.cn/20210203144334501.png) -# 思路 +## 思路 要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。 @@ -75,9 +83,9 @@ public: 这道题目比较容易陷入两个陷阱: -* 陷阱1 +* 陷阱1 -**不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**。 +**不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**。 写出了类似这样的代码: @@ -97,7 +105,7 @@ if (root->val > root->left->val && root->val < root->right->val) { 节点10小于左节点5,大于右节点15,但右子树里出现了一个6 这就不符合了! -* 陷阱2 +* 陷阱2 样例中最小节点 可能是int的最小值,如果这样使用最小的int来比较也是不行的。 @@ -109,7 +117,7 @@ if (root->val > root->left->val && root->val < root->right->val) { 递归三部曲: -* 确定递归函数,返回值以及参数 +* 确定递归函数,返回值以及参数 要定义一个longlong的全局变量,用来比较遍历的节点是否有序,因为后台测试数据中有int最小值,所以定义为longlong的类型,初始化为longlong最小值。 @@ -117,14 +125,14 @@ if (root->val > root->left->val && root->val < root->right->val) { 其实本题是同样的道理,我们在寻找一个不符合条件的节点,如果没有找到这个节点就遍历了整个树,如果找到不符合的节点了,立刻返回。 -代码如下: +代码如下: ``` long long maxVal = LONG_MIN; // 因为后台测试数据中有int最小值 -bool isValidBST(TreeNode* root) +bool isValidBST(TreeNode* root) ``` -* 确定终止条件 +* 确定终止条件 如果是空节点 是不是二叉搜索树呢? @@ -136,11 +144,11 @@ bool isValidBST(TreeNode* root) if (root == NULL) return true; ``` -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 -中序遍历,一直更新maxVal,一旦发现maxVal >= root->val,就返回false,注意元素相同时候也要返回false。 +中序遍历,一直更新maxVal,一旦发现maxVal >= root->val,就返回false,注意元素相同时候也要返回false。 -代码如下: +代码如下: ``` bool left = isValidBST(root->left); // 左 @@ -172,7 +180,7 @@ public: }; ``` -以上代码是因为后台数据有int最小值测试用例,所以都把maxVal改成了longlong最小值。 +以上代码是因为后台数据有int最小值测试用例,所以都把maxVal改成了longlong最小值。 如果测试数据中有 longlong的最小值,怎么办? @@ -183,7 +191,7 @@ public: ``` class Solution { public: - TreeNode* pre = NULL; // 用来记录前一个节点 + TreeNode* pre = NULL; // 用来记录前一个节点 bool isValidBST(TreeNode* root) { if (root == NULL) return true; bool left = isValidBST(root->left); @@ -199,7 +207,7 @@ public: 最后这份代码看上去整洁一些,思路也清晰。 -## 迭代法 +## 迭代法 可以用迭代法模拟二叉树中序遍历,对前中后序迭代法生疏的同学可以看这两篇[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg),[二叉树:前中后序迭代方式统一写法](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) @@ -233,7 +241,7 @@ public: 在[二叉树:二叉搜索树登场!](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg)中我们分明写出了痛哭流涕的简洁迭代法,怎么在这里不行了呢,因为本题是要验证二叉搜索树啊。 -# 总结 +## 总结 这道题目是一个简单题,但对于没接触过的同学还是有难度的。 @@ -241,6 +249,23 @@ public: 只要把基本类型的题目都做过,总结过之后,思路自然就开阔了。 -**就酱,学到了的话,就转发给身边需要的同学吧!** -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0100.相同的树.md b/problems/0100.相同的树.md deleted file mode 100644 index ac495f51..00000000 --- a/problems/0100.相同的树.md +++ /dev/null @@ -1,163 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/same-tree/ - - -# 100. 相同的树 - -给定两个二叉树,编写一个函数来检验它们是否相同。 - -如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 - - - -# 思路 - -在[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中,我们讲到对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了**其实我们要比较的是两个树(这两个树是根节点的左右子树)**,所以在递归遍历的过程中,也是要同时遍历两棵树。 - -理解这一本质之后,就会发现,求二叉树是否对称,和求二叉树是否相同几乎是同一道题目。 - -**如果没有读过[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)这一篇,请认真读完再做这道题,就会有感觉了。** - -递归三部曲中: - -1. 确定递归函数的参数和返回值 - -我们要比较的是两个树是否是相互相同的,参数也就是两个树的根节点。 - -返回值自然是bool类型。 - -代码如下: -``` -bool compare(TreeNode* tree1, TreeNode* tree2) -``` - -分析过程同[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)。 - -2. 确定终止条件 - -**要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。** - -节点为空的情况有: - -* tree1为空,tree2不为空,不对称,return false -* tree1不为空,tree2为空,不对称 return false -* tree1,tree2都为空,对称,返回true - -此时已经排除掉了节点为空的情况,那么剩下的就是tree1和tree2不为空的时候: - -* tree1、tree2都不为空,比较节点数值,不相同就return false - -此时tree1、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 -``` - -分析过程同[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg) - -3. 确定单层递归的逻辑 - -* 比较二叉树是否相同 :传入的是tree1的左孩子,tree2的右孩子。 -* 如果左右都相同就返回true ,有一侧不相同就返回false 。 - -代码如下: - -``` -bool left = compare(tree1->left, tree2->left); // 左子树:左、 右子树:左 -bool right = compare(tree1->right, tree2->right); // 左子树:右、 右子树:右 -bool isSame = left && right; // 左子树:中、 右子树:中(逻辑处理) -return isSame; -``` -最后递归的C++整体代码如下: - -``` -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); - } -}; -``` - - -**我给出的代码并不简洁,但是把每一步判断的逻辑都清楚的描绘出来了。** - -如果上来就看网上各种简洁的代码,看起来真的很简单,但是很多逻辑都掩盖掉了,而题解可能也没有把掩盖掉的逻辑说清楚。 - -**盲目的照着抄,结果就是:发现这是一道“简单题”,稀里糊涂的就过了,但是真正的每一步判断逻辑未必想到清楚。** - -当然我可以把如上代码整理如下: - -## 递归 - -``` -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); - } -}; -``` - -## 迭代法 - -``` -lass 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; - } -}; -``` - -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0101.对称二叉树.md b/problems/0101.对称二叉树.md index b454aa7d..561d0470 100644 --- a/problems/0101.对称二叉树.md +++ b/problems/0101.对称二叉树.md @@ -1,17 +1,23 @@ -## 题目地址 -https://leetcode-cn.com/problems/symmetric-tree/ +

+ + + + +

+

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

-> 又是一道“简单题” -# 101. 对称二叉树 +## 101. 对称二叉树 -给定一个二叉树,检查它是否是镜像对称的。 +题目地址:https://leetcode-cn.com/problems/symmetric-tree/ - +给定一个二叉树,检查它是否是镜像对称的。 -# 思路 +![101. 对称二叉树](https://img-blog.csdnimg.cn/20210203144607387.png) -**首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!** +## 思路 + +**首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!** 对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了**其实我们要比较的是两个树(这两个树是根节点的左右子树)**,所以在递归遍历的过程中,也是要同时遍历两棵树。 @@ -19,7 +25,7 @@ https://leetcode-cn.com/problems/symmetric-tree/ 比较的是两个子树的里侧和外侧的元素是否相等。如图所示: - +![101. 对称二叉树1](https://img-blog.csdnimg.cn/20210203144624414.png) 那么遍历的顺序应该是什么样的呢? @@ -35,11 +41,11 @@ https://leetcode-cn.com/problems/symmetric-tree/ 那么我们先来看看递归法的代码应该怎么写。 -## 递归法 +## 递归法 -### 递归三部曲 +递归三部曲 -1. 确定递归函数的参数和返回值 +1. 确定递归函数的参数和返回值 因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。 @@ -50,15 +56,15 @@ https://leetcode-cn.com/problems/symmetric-tree/ bool compare(TreeNode* left, TreeNode* right) ``` -2. 确定终止条件 +2. 确定终止条件 要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。 节点为空的情况有:(**注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点**) -* 左节点为空,右节点不为空,不对称,return false +* 左节点为空,右节点不为空,不对称,return false * 左不为空,右为空,不对称 return false -* 左右都为空,对称,返回true +* 左右都为空,对称,返回true 此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空: @@ -70,19 +76,19 @@ 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 == NULL && right == NULL) return true; else if (left->val != right->val) return false; // 注意这里我没有使用else ``` 注意上面最后一种情况,我没有使用else,而是elseif, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。 -3. 确定单层递归的逻辑 +3. 确定单层递归的逻辑 此时才进入单层递归的逻辑,单层递归的逻辑就是处理 右节点都不为空,且数值相同的情况。 * 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。 -* 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。 +* 比较内测是否对称,传入左节点的右孩子,右节点的左孩子。 * 如果左右都对称就返回true ,有一侧不对称就返回false 。 代码如下: @@ -153,7 +159,7 @@ public: **所以建议大家做题的时候,一定要想清楚逻辑,每一步做什么。把道题目所有情况想到位,相应的代码写出来之后,再去追求简洁代码的效果。** -## 迭代法 +## 迭代法 这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。 @@ -163,7 +169,8 @@ public: 通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等,如动画所示: - +![101.对称二叉树](https://tva1.sinaimg.cn/large/008eGmZEly1gnwcimlj8lg30hm0bqnpd.gif) + 如下的条件判断和递归的逻辑是一样的。 @@ -179,14 +186,14 @@ public: que.push(root->left); // 将左子树头结点加入队列 que.push(root->right); // 将右子树头结点加入队列 while (!que.empty()) { // 接下来就要判断这这两个树是否相互翻转 - TreeNode* leftNode = que.front(); que.pop(); + TreeNode* leftNode = que.front(); que.pop(); TreeNode* rightNode = que.front(); que.pop(); if (!leftNode && !rightNode) { // 左节点为空、右节点为空,此时说明是对称的 continue; } // 左右一个节点不为空,或者都不为空但数值不相同,返回false - if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { + if ((!leftNode || !rightNode || (leftNode->val != rightNode->val))) { return false; } que.push(leftNode->left); // 加入左节点左孩子 @@ -232,7 +239,7 @@ public: }; ``` -# 总结 +## 总结 这次我们又深度剖析了一道二叉树的“简单题”,大家会发现,真正的把题目搞清楚其实并不简单,leetcode上accept了和真正掌握了还是有距离的。 @@ -243,4 +250,38 @@ public: 如果已经做过这道题目的同学,读完文章可以再去看看这道题目,思考一下,会有不一样的发现! -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + +JavaScript +```javascript +var isSymmetric = function(root) { + return check(root, root) +}; + +const check = (leftPtr, rightPtr) => { + // 如果只有根节点,返回true + if (!leftPtr && !rightPtr) return true + // 如果左右节点只存在一个,则返回false + if (!leftPtr || !rightPtr) return false + + return leftPtr.val === rightPtr.val && check(leftPtr.left, rightPtr.right) && check(leftPtr.right, rightPtr.left) +} +``` + + +----------------------- +* 作者微信:[程序员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/0102.二叉树的层序遍历.md b/problems/0102.二叉树的层序遍历.md index 7b404623..cfbe09f3 100644 --- a/problems/0102.二叉树的层序遍历.md +++ b/problems/0102.二叉树的层序遍历.md @@ -1,7 +1,12 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ +

+ + + + +

+

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

-> 我要打十个! +# 二叉树的层序遍历 看完这篇文章虽然不能打十个,但是可以迅速打八个!而且够快! @@ -13,17 +18,19 @@ https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ * 637.二叉树的层平均值 * 429.N叉树的前序遍历 * 515.在每个树行中找最大值 -* 116. 填充每个节点的下一个右侧节点指针 +* 116. 填充每个节点的下一个右侧节点指针 * 117.填充每个节点的下一个右侧节点指针II -# 102.二叉树的层序遍历 +## 102.二叉树的层序遍历 + +题目地址:https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ 给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。 - +![102.二叉树的层序遍历](https://img-blog.csdnimg.cn/20210203144842988.png) -## 思路 +思路: 我们之前讲过了三篇关于二叉树的深度优先遍历的文章: @@ -41,14 +48,13 @@ https://leetcode-cn.com/problems/binary-tree-level-order-traversal/ 使用队列实现二叉树广度优先遍历,动画如下: -![102二叉树的层序遍历.mp4](ad3d58a5-b8ee-42a5-bc89-6ad4d9e3cbf2) - +![102二叉树的层序遍历](https://tva1.sinaimg.cn/large/008eGmZEly1gnad5itmk8g30iw0cqe83.gif) 这样就实现了层序从左到右遍历二叉树。 代码如下:**这份代码也可以作为二叉树层序遍历的模板,以后再打七个就靠它了**。 -## C++代码 +C++代码: ``` class Solution { @@ -77,19 +83,21 @@ public: **此时我们就掌握了二叉树的层序遍历了,那么如下五道leetcode上的题目,只需要修改模板的一两行代码(不能再多了),便可打倒!** -# 107.二叉树的层次遍历 II +## 107.二叉树的层次遍历 II -给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) +题目链接:https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/ - +给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历) -## 思路 +![107.二叉树的层次遍历II](https://img-blog.csdnimg.cn/20210203151058308.png) -相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。 +思路: -## C++代码 +相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。 -``` +C++代码: + +```C++ class Solution { public: vector> levelOrderBottom(TreeNode* root) { @@ -99,7 +107,7 @@ public: while (!que.empty()) { int size = que.size(); vector vec; - for (int i = 0; i < size; i++) { + for (int i = 0; i < size; i++) { TreeNode* node = que.front(); que.pop(); vec.push_back(node->val); @@ -116,19 +124,21 @@ public: ``` -# 199.二叉树的右视图 +## 199.二叉树的右视图 + +题目链接:https://leetcode-cn.com/problems/binary-tree-right-side-view/ 给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。 - +![199.二叉树的右视图](https://img-blog.csdnimg.cn/20210203151307377.png) -## 思路 +思路: 层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。 -## C++代码 +C++代码: -``` +```C++ class Solution { public: vector rightSideView(TreeNode* root) { @@ -150,19 +160,21 @@ public: }; ``` -# 637.二叉树的层平均值 +## 637.二叉树的层平均值 -给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。 +题目链接:https://leetcode-cn.com/problems/average-of-levels-in-binary-tree/ - +给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。 -## 思路 +![637.二叉树的层平均值](https://img-blog.csdnimg.cn/20210203151350500.png) + +思路: 本题就是层序遍历的时候把一层求个总和在取一个均值。 -## C++代码 +C++代码: -``` +```C++ class Solution { public: vector averageOfLevels(TreeNode* root) { @@ -172,7 +184,7 @@ public: while (!que.empty()) { int size = que.size(); double sum = 0; // 统计每一层的和 - for (int i = 0; i < size; i++) { + for (int i = 0; i < size; i++) { TreeNode* node = que.front(); que.pop(); sum += node->val; @@ -187,31 +199,32 @@ public: ``` -# 429.N叉树的层序遍历 +## 429.N叉树的层序遍历 + +题目链接:https://leetcode-cn.com/problems/n-ary-tree-level-order-traversal/ 给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。 例如,给定一个 3叉树 : - - +![429. N叉树的层序遍历](https://img-blog.csdnimg.cn/20210203151439168.png) 返回其层序遍历: -[ - [1], - [3,2,4], - [5,6] -] +[ + [1], + [3,2,4], + [5,6] +] -## 思路 +思路: -这道题依旧是模板题,只不过一个节点有多个孩子了 +这道题依旧是模板题,只不过一个节点有多个孩子了 -## C++代码 +C++代码: -``` +```C++ class Solution { public: vector> levelOrder(Node* root) { @@ -221,7 +234,7 @@ public: while (!que.empty()) { int size = que.size(); vector vec; - for (int i = 0; i < size; i++) { + for (int i = 0; i < size; i++) { Node* node = que.front(); que.pop(); vec.push_back(node->val); @@ -237,19 +250,21 @@ public: }; ``` -# 515.在每个树行中找最大值 +## 515.在每个树行中找最大值 -您需要在二叉树的每一行中找到最大的值。 +题目链接:https://leetcode-cn.com/problems/find-largest-value-in-each-tree-row/ - +您需要在二叉树的每一行中找到最大的值。 -## 思路 +![515.在每个树行中找最大值](https://img-blog.csdnimg.cn/20210203151532153.png) + +思路: 层序遍历,取每一层的最大值 -## C++代码 +C++代码: -``` +```C++ class Solution { public: vector largestValues(TreeNode* root) { @@ -273,29 +288,35 @@ public: }; ``` -# 116.填充每个节点的下一个右侧节点指针 +## 116.填充每个节点的下一个右侧节点指针 + +题目链接:https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/ 给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下: +``` struct Node { int val; Node *left; Node *right; Node *next; } +``` + + 填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。 初始状态下,所有 next 指针都被设置为 NULL。 - +![116.填充每个节点的下一个右侧节点指针](https://img-blog.csdnimg.cn/20210203152044855.jpg) -## 思路 +思路: 本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了 -## C++代码 +C++代码: -``` +```C++ class Solution { public: Node* connect(Node* root) { @@ -328,15 +349,17 @@ public: }; ``` -# 117.填充每个节点的下一个右侧节点指针II +## 117.填充每个节点的下一个右侧节点指针II -## 思路 +题目地址:https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node-ii/ + +思路: 这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道 -## C++代码 +C++代码: -``` +```C++ class Solution { public: Node* connect(Node* root) { @@ -369,7 +392,7 @@ public: ``` -# 总结 +## 总结 二叉树的层序遍历,就是图论中的广度优先搜索在二叉树中的应用,需要借助队列来实现(此时是不是又发现队列的应用了)。 @@ -381,13 +404,32 @@ public: * 637.二叉树的层平均值 * 429.N叉树的前序遍历 * 515.在每个树行中找最大值 -* 116. 填充每个节点的下一个右侧节点指针 +* 116. 填充每个节点的下一个右侧节点指针 * 117.填充每个节点的下一个右侧节点指针II -如果非要打十个,还得找叶师傅! +如果非要打十个,还得找叶师傅! - +![我要打十个](https://tva1.sinaimg.cn/large/008eGmZEly1gnadnltbpjg309603w4qp.gif) -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0104.二叉树的最大深度.md b/problems/0104.二叉树的最大深度.md index 3aa5c4b6..814beb55 100644 --- a/problems/0104.二叉树的最大深度.md +++ b/problems/0104.二叉树的最大深度.md @@ -1,15 +1,19 @@ -(寻找更节点可以用unordered_map来优化一下,元素都是独一无二的) +

+ + + + +

+

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

-## 题目地址 -https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/ - -> “简单题”系列 看完本篇可以一起做了如下两道题目: * 104.二叉树的最大深度 * 559.N叉树的最大深度 -# 104.二叉树的最大深度 +## 104.二叉树的最大深度 + +题目地址:https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/ 给定一个二叉树,找出其最大深度。 @@ -17,16 +21,14 @@ https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/ 说明: 叶子节点是指没有子节点的节点。 -示例: -给定二叉树 [3,9,20,null,null,15,7], +示例: +给定二叉树 [3,9,20,null,null,15,7], - +![104. 二叉树的最大深度](https://img-blog.csdnimg.cn/20210203153031914.png) 返回它的最大深度 3 。 -# 思路 - -## 递归法 +### 递归法 本题其实也要后序遍历(左右中),依然是因为要通过递归函数的返回值做计算树的高度。 @@ -41,7 +43,7 @@ int getDepth(TreeNode* node) 2. 确定终止条件:如果为空节点的话,就返回0,表示高度为0。 -代码如下: +代码如下: ``` if (node == NULL) return 0; ``` @@ -59,7 +61,7 @@ return depth; 所以整体C++代码如下: -``` +```C++ class Solution { public: int getDepth(TreeNode* node) { @@ -76,7 +78,7 @@ public: ``` 代码精简之后C++代码如下: -``` +```C++ class Solution { public: int maxDepth(TreeNode* root) { @@ -90,7 +92,7 @@ public: **精简之后的代码根本看不出是哪种遍历方式,也看不出递归三部曲的步骤,所以如果对二叉树的操作还不熟练,尽量不要直接照着精简代码来学。** -## 迭代法 +### 迭代法 使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。 @@ -104,7 +106,7 @@ public: C++代码如下: -``` +```C++ class Solution { public: int maxDepth(TreeNode* root) { @@ -127,10 +129,11 @@ public: }; ``` -那么我们可以顺便解决一下N叉树的最大深度问题 +那么我们可以顺便解决一下N叉树的最大深度问题 -# 559.N叉树的最大深度 -https://leetcode-cn.com/problems/maximum-depth-of-n-ary-tree/ +## 559.N叉树的最大深度 + +题目地址:https://leetcode-cn.com/problems/maximum-depth-of-n-ary-tree/ 给定一个 N 叉树,找到其最大深度。 @@ -138,19 +141,19 @@ https://leetcode-cn.com/problems/maximum-depth-of-n-ary-tree/ 例如,给定一个 3叉树 : - +![559.N叉树的最大深度](https://img-blog.csdnimg.cn/2021020315313214.png) 我们应返回其最大深度,3。 -# 思路 +思路: 依然可以提供递归法和迭代法,来解决这个问题,思路是和二叉树思路一样的,直接给出代码如下: -## 递归法 +### 递归法 C++代码: -``` +```C++ class Solution { public: int maxDepth(Node* root) { @@ -163,17 +166,17 @@ public: } }; ``` -## 迭代法 +### 迭代法 依然是层序遍历,代码如下: -``` +```C++ class Solution { public: int maxDepth(Node* root) { queue que; if (root != NULL) que.push(root); - int depth = 0; + int depth = 0; while (!que.empty()) { int size = que.size(); depth++; // 记录深度 @@ -190,9 +193,9 @@ public: }; ``` -使用栈来模拟后序遍历依然可以 +使用栈来模拟后序遍历依然可以 -``` +```C++ class Solution { public: int maxDepth(TreeNode* root) { @@ -223,4 +226,23 @@ public: } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0105.从前序与中序遍历序列构造二叉树.md b/problems/0105.从前序与中序遍历序列构造二叉树.md deleted file mode 100644 index eb39b313..00000000 --- a/problems/0105.从前序与中序遍历序列构造二叉树.md +++ /dev/null @@ -1,8 +0,0 @@ -# 链接 - -https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/ - -# 思路: - -详细见 -[0106.从中序与后序遍历序列构造二叉树](https://github.com/youngyangyang04/leetcode/blob/master/problems/0106.从中序与后序遍历序列构造二叉树.md) diff --git a/problems/0106.从中序与后序遍历序列构造二叉树.md b/problems/0106.从中序与后序遍历序列构造二叉树.md index 892f2167..f1f30b71 100644 --- a/problems/0106.从中序与后序遍历序列构造二叉树.md +++ b/problems/0106.从中序与后序遍历序列构造二叉树.md @@ -1,15 +1,20 @@ -## 题目地址 -https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/ +

+ + + + +

+

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

-> 给出两个序列 (可以加unorder_map优化一下) -看完本文,可以一起解决如下两道题目 +看完本文,可以一起解决如下两道题目 -* 106.从中序与后序遍历序列构造二叉树 +* 106.从中序与后序遍历序列构造二叉树 * 105.从前序与中序遍历序列构造二叉树 +## 106.从中序与后序遍历序列构造二叉树 -# 106.从中序与后序遍历序列构造二叉树 +题目地址:https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorder-traversal/ 根据一棵树的中序遍历与后序遍历构造二叉树。 @@ -22,9 +27,9 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树: - +![106. 从中序与后序遍历序列构造二叉树1](https://img-blog.csdnimg.cn/20210203154316774.png) -## 思路 +### 思路 首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。 @@ -32,7 +37,7 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde 流程如图: - +![106.从中序与后序遍历序列构造二叉树](https://img-blog.csdnimg.cn/20210203154249860.png) 那么代码应该怎么写呢? @@ -42,9 +47,9 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde * 第一步:如果数组大小为零的话,说明是空节点了。 -* 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。 +* 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。 -* 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点 +* 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点 * 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组) @@ -54,34 +59,34 @@ https://leetcode-cn.com/problems/construct-binary-tree-from-inorder-and-postorde 不难写出如下代码:(先把框架写出来) -``` - TreeNode* traversal (vector& inorder, vector& postorder) { +```C++ +TreeNode* traversal (vector& inorder, vector& postorder) { - // 第一步 - if (postorder.size() == 0) return NULL; + // 第一步 + if (postorder.size() == 0) return NULL; - // 第二步:后序遍历数组最后一个元素,就是当前的中间节点 - int rootValue = postorder[postorder.size() - 1]; - TreeNode* root = new TreeNode(rootValue); + // 第二步:后序遍历数组最后一个元素,就是当前的中间节点 + int rootValue = postorder[postorder.size() - 1]; + TreeNode* root = new TreeNode(rootValue); - // 叶子节点 - if (postorder.size() == 1) return root; + // 叶子节点 + 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; + // 第三步:找切割点 + int delimiterIndex; + for (delimiterIndex = 0; delimiterIndex < inorder.size(); delimiterIndex++) { + if (inorder[delimiterIndex] == rootValue) break; } + + // 第四步:切割中序数组,得到 中序左数组和中序右数组 + // 第五步:切割后序数组,得到 后序左数组和后序右数组 + + // 第六步 + root->left = traversal(中序左数组, 后序左数组); + root->right = traversal(中序右数组, 后序右数组); + + return root; +} ``` **难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。** @@ -148,9 +153,9 @@ root->right = traversal(rightInorder, rightPostorder); 完整代码如下: -### C++完整代码 +### C++完整代码 -``` +```C++ class Solution { private: TreeNode* traversal (vector& inorder, vector& postorder) { @@ -204,7 +209,7 @@ public: 加了日志的代码如下:(加了日志的代码不要在leetcode上提交,容易超时) -``` +```C++ class Solution { private: TreeNode* traversal (vector& inorder, vector& postorder) { @@ -272,7 +277,7 @@ public: 下面给出用下表索引写出的代码版本:(思路是一样的,只不过不用重复定义vector了,每次用下表索引来分割) ### C++优化版本 -``` +```C++ class Solution { private: // 中序区间:[inorderBegin, inorderEnd),后序区间[postorderBegin, postorderEnd) @@ -320,7 +325,7 @@ public: 那么这个版本写出来依然要打日志进行调试,打日志的版本如下:(**该版本不要在leetcode上提交,容易超时**) -``` +```C++ class Solution { private: TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& postorder, int postorderBegin, int postorderEnd) { @@ -389,7 +394,9 @@ public: }; ``` -# 105.从前序与中序遍历序列构造二叉树 +## 105.从前序与中序遍历序列构造二叉树 + +题目地址:https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/ 根据一棵树的前序遍历与中序遍历构造二叉树。 @@ -402,9 +409,9 @@ public: 中序遍历 inorder = [9,3,15,20,7] 返回如下的二叉树: - +![105. 从前序与中序遍历序列构造二叉树](https://img-blog.csdnimg.cn/20210203154626672.png) -## 思路 +### 思路 本题和106是一样的道理。 @@ -412,7 +419,7 @@ public: 带日志的版本C++代码如下: (**带日志的版本仅用于调试,不要在leetcode上提交,会超时**) -``` +```C++ class Solution { private: TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& preorder, int preorderBegin, int preorderEnd) { @@ -434,14 +441,14 @@ private: // 中序右区间,左闭右开[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; + int rightPreorderEnd = preorderEnd; cout << "----------" << endl; cout << "leftInorder :"; @@ -486,7 +493,7 @@ public: 105.从前序与中序遍历序列构造二叉树,最后版本,C++代码: -``` +```C++ class Solution { private: TreeNode* traversal (vector& inorder, int inorderBegin, int inorderEnd, vector& preorder, int preorderBegin, int preorderEnd) { @@ -508,14 +515,14 @@ private: // 中序右区间,左闭右开[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; + int rightPreorderEnd = preorderEnd; root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd); root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd); @@ -533,7 +540,7 @@ public: }; ``` -# 思考题 +## 思考题 前序和中序可以唯一确定一颗二叉树。 @@ -545,17 +552,17 @@ public: 举一个例子: - +![106.从中序与后序遍历序列构造二叉树2](https://img-blog.csdnimg.cn/20210203154720326.png) tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 -那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树! +那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树! 所以前序和后序不能唯一确定一颗二叉树! -# 总结 +## 总结 之前我们讲的二叉树题目都是各种遍历二叉树,这次开始构造二叉树了,思路其实比较简单,但是真正代码实现出来并不容易。 @@ -569,8 +576,24 @@ tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。 认真研究完本篇,相信大家对二叉树的构造会清晰很多。 -如果学到了,就赶紧转发给身边需要的同学吧! - -加个油! +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0107.二叉树的层次遍历II.md b/problems/0107.二叉树的层次遍历II.md deleted file mode 100644 index 91ca6a94..00000000 --- a/problems/0107.二叉树的层次遍历II.md +++ /dev/null @@ -1,46 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/ - -## 思路 - -这道题目相对于[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md),就把结果倒叙过来,就可以了。 - -层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。 - -需要借用一个辅助数据结构队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** - -使用队列实现广度优先遍历,动画如下: - - - -这样就实现了层序从左到右遍历二叉树。 - -代码如下:这份代码也可以作为二叉树层序遍历的模板。 - -## C++代码 - -``` -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++) {// 这里一定要使用固定大小size,不要使用que.size() - 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; - - } -}; -``` diff --git a/problems/0108.将有序数组转换为二叉搜索树.md b/problems/0108.将有序数组转换为二叉搜索树.md index 3afc8d6a..12d47e6a 100644 --- a/problems/0108.将有序数组转换为二叉搜索树.md +++ b/problems/0108.将有序数组转换为二叉搜索树.md @@ -1,6 +1,17 @@ -> 构造二叉搜索树,一不小心就平衡了 +

+ + + + +

+

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

-# 108.将有序数组转换为二叉搜索树 + +> 构造二叉搜索树,一不小心就平衡了 + +## 108.将有序数组转换为二叉搜索树 + +题目链接:https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/ 将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。 @@ -10,13 +21,13 @@ ![108.将有序数组转换为二叉搜索树](https://img-blog.csdnimg.cn/20201022164420763.png) -# 思路 +## 思路 做这道题目之前大家可以了解一下这几道: * [106.从中序与后序遍历序列构造二叉树](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) * [654.最大二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)中其实已经讲过了,如果根据数组构造一颗二叉树。 -* [701.二叉搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA) +* [701.二叉搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA) * [450.删除二叉搜索树中的节点](https://mp.weixin.qq.com/s/-p-Txvch1FFk3ygKLjPAKw) @@ -39,21 +50,21 @@ 取哪一个都可以,只不过构成了不同的平衡二叉搜索树。 -例如:输入:[-10,-3,0,5,9] +例如:输入:[-10,-3,0,5,9] 如下两棵树,都是这个数组的平衡二叉搜索树: - +![108.将有序数组转换为二叉搜索树](https://code-thinking.cdn.bcebos.com/pics/108.%E5%B0%86%E6%9C%89%E5%BA%8F%E6%95%B0%E7%BB%84%E8%BD%AC%E6%8D%A2%E4%B8%BA%E4%BA%8C%E5%8F%89%E6%90%9C%E7%B4%A2%E6%A0%91.png) -如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。 +如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。 **这也是题目中强调答案不是唯一的原因。 理解这一点,这道题目算是理解到位了**。 -## 递归 +## 递归 递归三部曲: -* 确定递归函数返回值及其参数 +* 确定递归函数返回值及其参数 删除二叉树节点,增加二叉树节点,都是用递归函数的返回值来完成,这样是比较方便的。 @@ -67,7 +78,7 @@ ``` // 左闭右闭区间[left, right] -TreeNode* traversal(vector& nums, int left, int right) +TreeNode* traversal(vector& nums, int left, int right) ``` 这里注意,**我这里定义的是左闭右闭区间,在不断分割的过程中,也会坚持左闭右闭的区间,这又涉及到我们讲过的循环不变量**。 @@ -75,7 +86,7 @@ TreeNode* traversal(vector& nums, int left, int right) 在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg),[35.搜索插入位置](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) 和[59.螺旋矩阵II](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg)都详细讲过循环不变量。 -* 确定递归终止条件 +* 确定递归终止条件 这里定义的是左闭右闭的区间,所以当区间 left > right的时候,就是空节点了。 @@ -85,22 +96,22 @@ TreeNode* traversal(vector& nums, int left, int right) if (left > right) return nullptr; ``` -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 -首先取数组中间元素的位置,不难写出`int mid = (left + right) / 2;`,**这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在[二分法](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)中尤其需要注意!** +首先取数组中间元素的位置,不难写出`int mid = (left + right) / 2;`,**这么写其实有一个问题,就是数值越界,例如left和right都是最大int,这么操作就越界了,在[二分法](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q)中尤其需要注意!** -所以可以这么写:`int mid = left + ((right - left) / 2);` +所以可以这么写:`int mid = left + ((right - left) / 2);` -但本题leetcode的测试数据并不会越界,所以怎么写都可以。但需要有这个意识! +但本题leetcode的测试数据并不会越界,所以怎么写都可以。但需要有这个意识! -取了中间位置,就开始以中间位置的元素构造节点,代码:`TreeNode* root = new TreeNode(nums[mid]);`。 +取了中间位置,就开始以中间位置的元素构造节点,代码:`TreeNode* root = new TreeNode(nums[mid]);`。 接着划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点。 最后返回root节点,单层递归整体代码如下: ``` -int mid = left + ((right - left) / 2); +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); @@ -111,12 +122,12 @@ return root; * 递归整体代码如下: -``` +```C++ class Solution { private: TreeNode* traversal(vector& nums, int left, int right) { if (left > right) return nullptr; - int mid = left + ((right - left) / 2); + 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); @@ -139,7 +150,7 @@ public: 模拟的就是不断分割的过程,C++代码如下:(我已经详细注释) -``` +```C++ class Solution { public: TreeNode* sortedArrayToBST(vector& nums) { @@ -158,7 +169,7 @@ public: nodeQue.pop(); int left = leftQue.front(); leftQue.pop(); int right = rightQue.front(); rightQue.pop(); - int mid = left + ((right - left) / 2); + int mid = left + ((right - left) / 2); curNode->val = nums[mid]; // 将mid对应的元素给中间节点 @@ -181,7 +192,7 @@ public: }; ``` -# 总结 +## 总结 **在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) 和 [二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)之后,我们顺理成章的应该构造一下二叉搜索树了,一不小心还是一棵平衡二叉搜索树**。 @@ -189,9 +200,27 @@ public: 此时相信大家应该对通过递归函数的返回值来增删二叉树很熟悉了,这也是常规操作。 -在定义区间的过程中我们又一次强调了循环不变量的重要性。 +在定义区间的过程中我们又一次强调了循环不变量的重要性。 最后依然给出迭代的方法,其实就是模拟取中间元素,然后不断分割去构造二叉树的过程。 -**就酱,如果对你有帮助的话,也转发给身边需要的同学吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0110.平衡二叉树.md b/problems/0110.平衡二叉树.md index 09d5598a..5d55910c 100644 --- a/problems/0110.平衡二叉树.md +++ b/problems/0110.平衡二叉树.md @@ -1,9 +1,17 @@ -## 题目地址 -https://leetcode-cn.com/problems/balanced-binary-tree/ +

+ + + + +

+

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

+ > 求高度还是求深度,你搞懂了不? -# 110.平衡二叉树 +## 110.平衡二叉树 + +题目地址:https://leetcode-cn.com/problems/balanced-binary-tree/ 给定一个二叉树,判断它是否是高度平衡的二叉树。 @@ -13,7 +21,7 @@ https://leetcode-cn.com/problems/balanced-binary-tree/ 给定二叉树 [3,9,20,null,null,15,7] - +![110.平衡二叉树](https://img-blog.csdnimg.cn/2021020315542230.png) 返回 true 。 @@ -21,11 +29,11 @@ https://leetcode-cn.com/problems/balanced-binary-tree/ 给定二叉树 [1,2,2,3,3,null,null,4,4] - +![110.平衡二叉树1](https://img-blog.csdnimg.cn/20210203155447919.png) 返回 false 。 -# 题外话 +## 题外话 咋眼一看这道题目和[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)很像,其实有很大区别。 @@ -36,7 +44,7 @@ https://leetcode-cn.com/problems/balanced-binary-tree/ 但leetcode中强调的深度和高度很明显是按照节点来计算的,如图: - +![110.平衡二叉树2](https://img-blog.csdnimg.cn/20210203155515650.png) 关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。 @@ -48,7 +56,7 @@ https://leetcode-cn.com/problems/balanced-binary-tree/ 在[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中,如果真正求取二叉树的最大深度,代码应该写成如下:(前序遍历) -``` +```C++ class Solution { public: int result; @@ -82,7 +90,7 @@ public: 注意以上代码是为了把细节体现出来,简化一下代码如下: -``` +```C++ class Solution { public: int result; @@ -106,15 +114,15 @@ public: }; ``` -# 本题思路 +## 本题思路 -## 递归 +### 递归 此时大家应该明白了既然要求比较高度,必然是要后序遍历。 递归三步曲分析: -1. 明确递归函数的参数和返回值 +1. 明确递归函数的参数和返回值 参数的话为传入的节点指针,就没有其他参数需要传递了,返回值要返回传入节点为根节点树的深度。 @@ -132,9 +140,9 @@ public: int getDepth(TreeNode* node) ``` -2. 明确终止条件 +2. 明确终止条件 -递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的书高度为0 +递归的过程中依然是遇到空节点了为终止,返回0,表示当前节点为根节点的书高度为0 代码如下: @@ -144,7 +152,7 @@ if (node == NULL) { } ``` -3. 明确单层递归的逻辑 +3. 明确单层递归的逻辑 如何判断当前传入节点为根节点的二叉树是否是平衡二叉树呢,当然是左子树高度和右子树高度相差。 @@ -154,7 +162,7 @@ if (node == NULL) { ``` int leftDepth = depth(node->left); // 左 -if (leftDepth == -1) return -1; +if (leftDepth == -1) return -1; int rightDepth = depth(node->right); // 右 if (rightDepth == -1) return -1; @@ -172,7 +180,7 @@ return result; ``` int leftDepth = getDepth(node->left); -if (leftDepth == -1) return -1; +if (leftDepth == -1) return -1; int rightDepth = getDepth(node->right); if (rightDepth == -1) return -1; return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth); @@ -182,7 +190,7 @@ return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth); getDepth整体代码如下: -``` +```C++ int getDepth(TreeNode* node) { if (node == NULL) { return 0; @@ -197,7 +205,7 @@ int getDepth(TreeNode* node) { 最后本题整体递归代码如下: -``` +```C++ class Solution { public: // 返回以该节点为根节点的二叉树的高度,如果不是二叉搜索树了则返回-1 @@ -212,12 +220,12 @@ public: return abs(leftDepth - rightDepth) > 1 ? -1 : 1 + max(leftDepth, rightDepth); } bool isBalanced(TreeNode* root) { - return getDepth(root) == -1 ? false : true; + return getDepth(root) == -1 ? false : true; } }; ``` -## 迭代 +### 迭代 在[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中我们可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。 @@ -227,7 +235,7 @@ public: 代码如下: -``` +```C++ // cur节点的最大深度,就是cur的高度 int getDepth(TreeNode* cur) { stack st; @@ -334,7 +342,7 @@ public: 因为对于回溯算法已经是非常复杂的递归了,如果在用迭代的话,就是自己给自己找麻烦,效率也并不一定高。 -# 总结 +## 总结 通过本题可以了解求二叉树深度 和 二叉树高度的差异,求深度适合用前序遍历,而求高度适合用后序遍历。 @@ -342,4 +350,23 @@ public: 但是递归方式是一定要掌握的! -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0111.二叉树的最小深度.md b/problems/0111.二叉树的最小深度.md index eebfd76e..430cd5d6 100644 --- a/problems/0111.二叉树的最小深度.md +++ b/problems/0111.二叉树的最小深度.md @@ -1,10 +1,17 @@ +

+ + + + +

+

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

-## 题目地址 -https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/ > 和求最大深度一个套路? -# 111.二叉树的最小深度 +## 111.二叉树的最小深度 + +题目地址:https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/ 给定一个二叉树,找出其最小深度。 @@ -16,11 +23,11 @@ https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/ 给定二叉树 [3,9,20,null,null,15,7], - +![111.二叉树的最小深度1](https://img-blog.csdnimg.cn/2021020315582586.png) -返回它的最小深度 2. +返回它的最小深度 2. -# 思路 +## 思路 看完了这篇[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg),再来看看如何求最小深度。 @@ -28,13 +35,13 @@ https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/ 遍历顺序上依然是后序遍历(因为要比较递归返回之后的结果),但在处理中间节点的逻辑上,最大深度很容易理解,最小深度可有一个误区,如图: - +![111.二叉树的最小深度](https://img-blog.csdnimg.cn/20210203155800503.png) 这就重新审题了,题目中说的是:**最小深度是从根节点到最近叶子节点的最短路径上的节点数量。**,注意是**叶子节点**。 什么是叶子节点,左右孩子都为空的节点才是叶子节点! -## 递归法 +## 递归法 来来来,一起递归三部曲: @@ -48,9 +55,9 @@ https://leetcode-cn.com/problems/minimum-depth-of-binary-tree/ int getDepth(TreeNode* node) ``` -2. 确定终止条件 +2. 确定终止条件 -终止条件也是遇到空节点返回0,表示当前节点的高度为0。 +终止条件也是遇到空节点返回0,表示当前节点的高度为0。 代码如下: @@ -58,7 +65,7 @@ int getDepth(TreeNode* node) if (node == NULL) return 0; ``` -3. 确定单层递归的逻辑 +3. 确定单层递归的逻辑 这块和求最大深度可就不一样了,一些同学可能会写如下代码: ``` @@ -70,7 +77,7 @@ return result; 这个代码就犯了此图中的误区: - +![111.二叉树的最小深度](https://img-blog.csdnimg.cn/20210203155800503.png) 如果这么求的话,没有左孩子的分支会算为最短深度。 @@ -80,7 +87,7 @@ return result; 代码如下: -``` +```C++ int leftDepth = getDepth(node->left); // 左 int rightDepth = getDepth(node->right); // 右 // 中 @@ -92,14 +99,14 @@ if (node->left == NULL && node->right != NULL) {  if (node->left != NULL && node->right == NULL) {      return 1 + leftDepth; } -int result = 1 + min(leftDepth, rightDepth); +int result = 1 + min(leftDepth, rightDepth); return result; ``` -遍历的顺序为后序(左右中),可以看出:**求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。** +遍历的顺序为后序(左右中),可以看出:**求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。** 整体递归代码如下: -``` +```C++ class Solution { public: int getDepth(TreeNode* node) { @@ -115,7 +122,7 @@ public: if (node->left != NULL && node->right == NULL) {      return 1 + leftDepth; } - int result = 1 + min(leftDepth, rightDepth); + int result = 1 + min(leftDepth, rightDepth); return result; } @@ -127,7 +134,7 @@ public: 精简之后代码如下: -``` +```C++ class Solution { public: int minDepth(TreeNode* root) { @@ -155,7 +162,7 @@ public: 代码如下:(详细注释) -``` +```C++ class Solution { public: @@ -182,4 +189,23 @@ public: }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0112.路径总和.md b/problems/0112.路径总和.md index 29fc1ca2..40df1e7a 100644 --- a/problems/0112.路径总和.md +++ b/problems/0112.路径总和.md @@ -1,4 +1,11 @@ -## 题目地址 +

+ + + + +

+

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

+ > 递归函数什么时候需要返回值 @@ -9,7 +16,9 @@ * 112. 路径总和 * 113. 路径总和II -# 112. 路径总和 +## 112. 路径总和 + +题目地址:https://leetcode-cn.com/problems/path-sum/ 给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。 @@ -18,16 +27,15 @@ 示例:  给定如下二叉树,以及目标和 sum = 22, - +![112.路径总和1](https://img-blog.csdnimg.cn/20210203160355234.png) 返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。 -# 思路 - +### 思路 这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和。 -## 递归 +### 递归 可以使用深度优先遍历的方式(本题前中后序都可以,无所谓,因为中节点也没有处理逻辑)来遍历二叉树 @@ -47,7 +55,7 @@ 如图所示: - +![112.路径总和](https://img-blog.csdnimg.cn/2021020316051216.png) 图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。 @@ -58,7 +66,7 @@ bool traversal(TreeNode* cur, int count) // 注意函数的返回类型 ``` -2. 确定终止条件 +2. 确定终止条件 首先计数器如何统计这一条路径的和呢? @@ -75,15 +83,15 @@ if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点 if (!cur->left && !cur->right) return false; // 遇到叶子节点而没有找到合适的边,直接返回 ``` -3. 确定单层递归的逻辑 +3. 确定单层递归的逻辑 -因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。 +因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。 递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回。 代码如下: -``` +```C++ if (cur->left) { // 左 (空节点不遍历) // 遇到叶子节点返回true,则直接返回true if (traversal(cur->left, count - cur->left->val)) return true; // 注意这里有回溯的逻辑 @@ -101,15 +109,15 @@ return false; 为了把回溯的过程体现出来,可以改为如下代码: -``` +```C++ if (cur->left) { // 左 count -= cur->left->val; // 递归,处理节点; if (traversal(cur->left, count)) return true; count += cur->left->val; // 回溯,撤销处理结果 } -if (cur->right) { // 右 +if (cur->right) { // 右 count -= cur->right->val; - if (traversal(cur->right, count - cur->right->val)) return true; + if (traversal(cur->right, count - cur->right->val)) return true; count += cur->right->val; } return false; @@ -118,7 +126,7 @@ return false; 整体代码如下: -``` +```C++ class Solution { private: bool traversal(TreeNode* cur, int count) { @@ -146,9 +154,9 @@ public: }; ``` -以上代码精简之后如下: +以上代码精简之后如下: -``` +```C++ class Solution { public: bool hasPathSum(TreeNode* root, int sum) { @@ -164,11 +172,11 @@ public: **是不是发现精简之后的代码,已经完全看不出分析的过程了,所以我们要把题目分析清楚之后,在追求代码精简。** 这一点我已经强调很多次了! -## 迭代 +### 迭代 -如果使用栈模拟递归的话,那么如果做回溯呢? +如果使用栈模拟递归的话,那么如果做回溯呢? -**此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。** +**此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。** C++就我们用pair结构来存放这个栈里的元素。 @@ -178,7 +186,7 @@ C++就我们用pair结构来存放这个栈里的元素。 如下代码是使用栈模拟的前序遍历,如下:(详细注释) -``` +```C++ class Solution { public: @@ -210,7 +218,9 @@ public: 如果大家完全理解了本地的递归方法之后,就可以顺便把leetcode上113. 路径总和II做了。 -# 113. 路径总和II +## 113. 路径总和II + +题目地址:https://leetcode-cn.com/problems/path-sum-ii/ 给定一个二叉树和一个目标和,找到所有从根节点到叶子节点路径总和等于给定目标和的路径。 @@ -220,27 +230,27 @@ public: 给定如下二叉树,以及目标和 sum = 22, - +![113.路径总和II1.png](https://img-blog.csdnimg.cn/20210203160854654.png) -## 思路 +### 思路 113.路径总和II要遍历整个树,找到所有路径,**所以递归函数不要返回值!** 如图: - +![113.路径总和II](https://img-blog.csdnimg.cn/20210203160922745.png) 为了尽可能的把细节体现出来,我写出如下代码(**这份代码并不简洁,但是逻辑非常清晰**) -``` +```C++ class Solution { private: vector> result; vector path; // 递归函数不需要返回值,因为我们要遍历整个树 void traversal(TreeNode* cur, int count) { - if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点切找到了和为sum的路径 + if (!cur->left && !cur->right && count == 0) { // 遇到了叶子节点且找到了和为sum的路径 result.push_back(path); return; } @@ -278,7 +288,7 @@ public: 至于113. 路径总和II 的迭代法我并没有写,用迭代方式记录所有路径比较麻烦,也没有必要,如果大家感兴趣的话,可以再深入研究研究。 -# 总结 +## 总结 本篇通过leetcode上112. 路径总和 和 113. 路径总和II 详细的讲解了 递归函数什么时候需要返回值,什么不需要返回值。 @@ -286,6 +296,27 @@ public: 对于112. 路径总和,我依然给出了递归法和迭代法,这种题目其实用迭代法会复杂一些,能掌握递归方式就够了! -今天是长假最后一天了,内容多一些,也是为了尽快让大家恢复学习状态,哈哈。 -加个油! + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0113.路径总和II.md b/problems/0113.路径总和II.md deleted file mode 100644 index b12f27f2..00000000 --- a/problems/0113.路径总和II.md +++ /dev/null @@ -1,76 +0,0 @@ - -## 链接 - -## 思路 - -这道题目与其说是递归,不如说是回溯问题,题目要找到所有的路径。 - -这道题目相对于[112. 路径总和](https://leetcode-cn.com/problems/path-sum/) ,是要求出所有的路径和。 - - -**相信很多同学都疑惑递归的过程中究竟什么时候需要返回值,什么时候不需要返回值?** - -我在[112. 路径总和题解](https://leetcode-cn.com/problems/path-sum/solution/112-lu-jing-zong-he-di-gui-hui-su-die-dai-xiang-ji/)中给出了详细的解释。 - -**如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。** - -而本题要遍历整个树,找到所有路径,**所以本题的递归函数不要返回值!** - -如图: - - - - -这道题目其实比[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)简单一些,大家做完了本题,可以在做[112. 路径总和](https://leetcode-cn.com/problems/path-sum/)。 - -为了尽可能的把回溯过程体现出来,我写出如下代码(**这个代码一定不是最简洁的,但是比较清晰的,过于简洁的代码不方便读者理解**) - - -## 回溯C++代码 - -``` -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; - } -}; -``` - -这道题目也可以用迭代法,相对于112.路径总和,每个节点不仅要保存当前路径和,也要保存当前路径,其实比较麻烦,也没有必要,因为回溯法虽然也是递归,但是如果用迭代来实现回溯法的话,是很费劲的,因为回溯的过程需要用栈模拟出来非常麻烦。 - -这也是为什么我在后面讲解回溯算法的时候,都是使用递归,也没有人会有栈模拟回溯算法(自己找麻烦,哈哈)。 diff --git a/problems/0115.不同的子序列.md b/problems/0115.不同的子序列.md new file mode 100644 index 00000000..d48f598b --- /dev/null +++ b/problems/0115.不同的子序列.md @@ -0,0 +1,162 @@ +

+ + + + +

+

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

+ +## 115.不同的子序列 + +题目链接:https://leetcode-cn.com/problems/distinct-subsequences/ + +给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 + +字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是) + +题目数据保证答案符合 32 位带符号整数范围。 + +![115.不同的子序列示例](https://code-thinking.cdn.bcebos.com/pics/115.%E4%B8%8D%E5%90%8C%E7%9A%84%E5%AD%90%E5%BA%8F%E5%88%97%E7%A4%BA%E4%BE%8B.jpg) + +提示: + +0 <= s.length, t.length <= 1000 +s 和 t 由英文字母组成 + +## 思路 + +这道题目如果不是子序列,而是要求连续序列的,那就可以考虑用KMP。 + +这道题目相对于72. 编辑距离,简单了不少,因为本题相当于只有删除操作,不用考虑替换增加之类的。 + +但相对于刚讲过的[动态规划:392.判断子序列](https://mp.weixin.qq.com/s/2pjT4B4fjfOx5iB6N6xyng)就有难度了,这道题目双指针法可就做不了了,来看看动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j]:以i-1为结尾的s子序列中出现以j-1为结尾的t的个数为dp[i][j]。 + +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[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]; + +3. dp数组如何初始化 + +从递推公式dp[i][j] = dp[i - 1][j - 1] + dp[i - 1][j]; 和 dp[i][j] = dp[i - 1][j]; 中可以看出dp[i][0] 和dp[0][j]是一定要初始化的。 + +每次当初始化的时候,都要回顾一下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。 + +初始化分析完毕,代码如下: + +```C++ +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]都是根据左上方和正上方推出来的。 + +所以遍历的时候一定是从上到下,从左到右,这样保证dp[i][j]可以根据之前计算出来的数值进行计算。 + +代码如下: + +```C++ +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://code-thinking.cdn.bcebos.com/pics/115.%E4%B8%8D%E5%90%8C%E7%9A%84%E5%AD%90%E5%BA%8F%E5%88%97.jpg) + +如果写出来的代码怎么改都通过不了,不妨把dp数组打印出来,看一看,是不是这样的。 + + +动规五部曲分析完毕,代码如下: + +```C++ +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()]; + } +}; +``` + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0116.填充每个节点的下一个右侧节点指针.md b/problems/0116.填充每个节点的下一个右侧节点指针.md deleted file mode 100644 index def34c1d..00000000 --- a/problems/0116.填充每个节点的下一个右侧节点指针.md +++ /dev/null @@ -1,101 +0,0 @@ - - -# 链接 -https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node/ - -## 思路 - - -注意题目提示内容,: -* 你只能使用常量级额外空间。 -* 使用递归解题也符合要求,本题中递归程序占用的栈空间不算做额外的空间复杂度。 - -基本上就是要求使用递归了,迭代的方式一定会用到栈或者队列。 - -### 递归 - -一想用递归怎么做呢,虽然层序遍历是最直观的,但是递归的方式确实不好想。 - -如图,假如当前操作的节点是cur: - - - -最关键的点是可以通过上一层递归 搭出来的线,进行本次搭线。 - -图中cur节点为元素4,那么搭线的逻辑代码:(**注意注释中操作1和操作2和图中的对应关系**) - -``` -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; -} -``` - -理解到这里,使用前序遍历,那么不难写出如下代码: - -如果对二叉树的前中后序不了解看这篇:[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) - - -``` -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://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)。 - -层序遍历本来就是一层一层的去遍历,记录一层的头结点(nodePre),然后让nodePre指向当前遍历的节点就可以了。 - -代码如下: - -``` - -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; - } -}; -``` diff --git a/problems/0117.填充每个节点的下一个右侧节点指针II.md b/problems/0117.填充每个节点的下一个右侧节点指针II.md deleted file mode 100644 index 43b56b91..00000000 --- a/problems/0117.填充每个节点的下一个右侧节点指针II.md +++ /dev/null @@ -1,41 +0,0 @@ - -# 链接 -https://leetcode-cn.com/problems/populating-next-right-pointers-in-each-node-ii/ - -## 思路 - -这道题目使用递归还是有难度的,不是完美二叉树,应该怎么办。 - -## C++代码 - -``` -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; - } -}; -``` diff --git a/problems/0121.买卖股票的最佳时机.md b/problems/0121.买卖股票的最佳时机.md new file mode 100644 index 00000000..3d564892 --- /dev/null +++ b/problems/0121.买卖股票的最佳时机.md @@ -0,0 +1,215 @@ +

+ + + + +

+

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

+ +## 121. 买卖股票的最佳时机 + +题目链接:https://leetcode-cn.com/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。 + + +## 思路 + +### 暴力 + +这道题目最直观的想法,就是暴力,找最优间距了。 + +``` +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++代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210224225642465.png) + + +dp[5][1]就是最终结果。 + +为什么不是dp[5][0]呢? + +**因为本题中不持有股票状态所得金钱一定比持有股票状态得到的多!** + +以上分析完毕,C++代码如下: + +```C++ +// 版本一 +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状态就可以了,可以使用滚动数组来节省空间,代码如下: + +```C++ +// 版本二 +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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0122.买卖股票的最佳时机II.md b/problems/0122.买卖股票的最佳时机II.md index 00470497..1a9b4f7f 100644 --- a/problems/0122.买卖股票的最佳时机II.md +++ b/problems/0122.买卖股票的最佳时机II.md @@ -1,6 +1,13 @@ -> 贪心有时候比动态规划更巧妙,更好用! +

+ + + + +

+

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

-# 122.买卖股票的最佳时机II + +## 122.买卖股票的最佳时机II 题目链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii/ @@ -11,35 +18,35 @@ 注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 -示例 1: -输入: [7,1,5,3,6,4] -输出: 7 -解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4。随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。 +示例 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 +示例 2: +输入: [1,2,3,4,5] +输出: 4 解释: 在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。注意你不能在第 1 天和第 2 天接连购买股票,之后再将它们卖出。因为这样属于同时参与了多笔交易,你必须在再次购买前出售掉之前的股票。 -示例 3: -输入: [7,6,4,3,1] -输出: 0 -解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 +示例 3: +输入: [7,6,4,3,1] +输出: 0 +解释: 在这种情况下, 没有交易完成, 所以最大利润为 0。 -提示: +提示: * 1 <= prices.length <= 3 * 10 ^ 4 * 0 <= prices[i] <= 10 ^ 4 -# 思路 +## 思路 本题首先要清楚两点: * 只有一只股票! -* 当前只有买股票或者买股票的操作 +* 当前只有买股票或者买股票的操作 想获得利润至少要两天为一个交易单元。 -## 贪心算法 +## 贪心算法 这道题目可能我们只会想,选一个低的买入,在选个高的卖,在选一个低的买入.....循环反复。 @@ -88,7 +95,7 @@ public: * 时间复杂度O(n) * 空间复杂度O(1) -## 动态规划 +## 动态规划 动态规划将在下一个系列详细讲解,本题解先给出我的C++代码(带详细注释),感兴趣的同学可以自己先学习一下。 @@ -114,7 +121,7 @@ public: * 时间复杂度O(n) * 空间复杂度O(n) -# 总结 +## 总结 股票问题其实是一个系列的,属于动态规划的范畴,因为目前在讲解贪心系列,所以股票问题会在之后的动态规划系列中详细讲解。 @@ -122,13 +129,24 @@ public: **本题中理解利润拆分是关键点!** 不要整块的去看,而是把整体利润拆为每天的利润。 -一旦想到这里了,很自然就会想到贪心了,即:只收集每天的正利润,最后稳稳的就是最大利润了。 +一旦想到这里了,很自然就会想到贪心了,即:只收集每天的正利润,最后稳稳的就是最大利润了。 -就酱,「代码随想录」是技术公众号里的一抹清流,值得推荐给你的朋友同学们! +## 其他语言版本 -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +Java: + + +Python: + + +Go: + +----------------------- +* 作者微信:[程序员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/0122.买卖股票的最佳时机II(动态规划).md b/problems/0122.买卖股票的最佳时机II(动态规划).md new file mode 100644 index 00000000..3444ca73 --- /dev/null +++ b/problems/0122.买卖股票的最佳时机II(动态规划).md @@ -0,0 +1,150 @@ +

+ + + + +

+

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

+ +## 122.买卖股票的最佳时机II + +题目链接:https://leetcode-cn.com/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 + +## 思路 + +本题我们在讲解贪心专题的时候就已经讲解过了[贪心算法:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg),只不过没有深入讲解动态规划的解法,那么这次我们再好好分析一下动规的解法。 + + +本题和[121. 买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)的唯一区别本题股票可以买卖多次了(注意只有一只股票,所以再次购买前要出售掉之前的股票) + +**在动规五部曲中,这个区别主要是体现在递推公式上,其他都和[121. 买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)一样一样的**。 + +所以我们重点讲一讲递推公式。 + +这里重申一下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://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况**。 + +在[121. 买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)中,因为股票全程只能买卖一次,所以如果买入股票,那么第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://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)就是一样的逻辑,卖出股票收获利润(可能是负值)天经地义!** + +代码如下:(注意代码中的注释,标记了和121.买卖股票的最佳时机唯一不同的地方) + +```C++ +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://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)的代码几乎一样,唯一的区别在: + +``` +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++代码如下: + +```C++ +// 版本二 +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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0123.买卖股票的最佳时机III.md b/problems/0123.买卖股票的最佳时机III.md new file mode 100644 index 00000000..0e718cf1 --- /dev/null +++ b/problems/0123.买卖股票的最佳时机III.md @@ -0,0 +1,209 @@ +

+ + + + +

+

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

+ +## 123.买卖股票的最佳时机III + +题目链接:https://leetcode-cn.com/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 + +## 思路 + + +这道题目相对 [121.买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ) 和 [122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w) 难了不少。 + +关键在于至多买卖两次,这意味着可以买卖一次,可以买卖两次,也可以不买卖。 + +接来下我用动态规划五部曲详细分析一下: + +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]状态,有两个具体操作: + +* 操作一:第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天做第一次卖出的操作,这个初始值应该是多少呢? + +首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0, + +从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。 + +所以dp[0][2] = 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://img-blog.csdnimg.cn/20201228181724295.png) + +大家可以看到红色框为最后两次卖出的状态。 + +现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。 + +所以最终最大利润是dp[4][4] + +以上五部都分析完了,不难写出如下代码: + +```C++ +// 版本一 +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++版本: + +```C++ +// 版本二 +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,对所得现金没有影响。相当于今天买入股票又卖出股票,等于没有操作,保持昨天卖出股票的状态了。 + +**这种写法看上去简单,其实思路很绕,不建议大家这么写,这么思考,很容易把自己绕进去!** + +对于本题,把版本一的写法研究明白,足以! + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0127.单词接龙.md b/problems/0127.单词接龙.md deleted file mode 100644 index 575fc4c0..00000000 --- a/problems/0127.单词接龙.md +++ /dev/null @@ -1,73 +0,0 @@ - -## 题目链接 - -https://leetcode-cn.com/problems/word-ladder/ - -## 思路 - -以示例1为例,从这个图中可以看出 hit 到 cog的路线,不止一条,有三条,两条是最短的长度为5,一条长度为6。 - - - -本题只需要求出最短长度就可以了,不用找出路径。 - -所以这道题要解决两个问题: - -* 图中的线是如何连在一起的 -* 起点和终点的最短路径长度 - - -首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个,所以判断点与点之间的关系,要自己判断是不是差一个字符,如果差一个字符,那就是有链接。 - -然后就是求起点和终点的最短路径长度,**这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径**。因为广搜就是以起点中心向四周扩散的搜索。 - -本题如果用深搜,会非常麻烦。 - -另外需要有一个注意点: - -* 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环! -* 本题给出集合是数组型的,可以转成set结构,查找更快一些 - -C++代码如下:(详细注释) - -``` -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; - } -}; -``` -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[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/0129.求根到叶子节点数字之和.md b/problems/0129.求根到叶子节点数字之和.md deleted file mode 100644 index 7d061a43..00000000 --- a/problems/0129.求根到叶子节点数字之和.md +++ /dev/null @@ -1,160 +0,0 @@ -## 链接 -https://leetcode-cn.com/problems/sum-root-to-leaf-numbers/ - -## 思路 - -本题和[113.路径总和II](https://github.com/youngyangyang04/leetcode-master/blob/master/problems/0113.%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8CII.md)是类似的思路,做完这道题,可以顺便把[113.路径总和II](https://github.com/youngyangyang04/leetcode-master/blob/master/problems/0113.%E8%B7%AF%E5%BE%84%E6%80%BB%E5%92%8CII.md) 和 [112.路径总和](https://github.com/youngyangyang04/leetcode/blob/master/problems/0112.路径总和.md) 做了。 - -结合112.路径总和 和 113.路径总和II,我在讲了[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg),如果大家对二叉树递归函数什么时候需要返回值很迷茫,可以看一下。 - -接下来在看本题,就简单多了,本题其实需要使用回溯,但一些同学可能都不知道自己用了回溯,在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)中,我详细讲解了二叉树的递归中,如何使用了回溯。 - -接下来我们来看题: - -首先思路很明确,就是要遍历整个树把更节点到叶子节点组成的数字相加。 - -那么先按递归三部曲来分析: - -### 递归三部曲 - -如果对递归三部曲不了解的话,可以看这里:[二叉树:前中后递归详解](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA) - -* 确定递归函数返回值及其参数 - -这里我们要遍历整个二叉树,且需要要返回值做逻辑处理,所有返回值为void,在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)中,详细讲解了返回值问题。 - -参数只需要把根节点传入,此时还需要定义两个全局遍历,一个是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,代码如下: - -``` -int vectorToInt(const vector& vec) { - int sum = 0; - for (int i = 0; i < vec.size(); i++) { - sum = sum * 10 + vec[i]; - } - return sum; -} -``` - - -* 确定递归单层逻辑 - -本题其实采用前中后序都不无所谓, 因为也没有中间几点的处理逻辑。 - -这里主要是当左节点不为空,path收集路径,并递归左孩子,右节点同理。 - -**但别忘了回溯**。 - -如图: - - - - -代码如下: - -``` - // 中 -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(); // 回溯 -} -``` - -这里要注意回溯和递归要永远在一起,一个递归,对应一个回溯,是一对一的关系,有的同学写成如下代码: - -``` -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++代码如下: - -``` -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; - } -}; -``` - -# 总结 - -过于简洁的代码,很容易让初学者忽视了本题中回溯的精髓,甚至作者本身都没有想清楚自己用了回溯。 - -**我这里提供的代码把整个回溯过程充分体现出来,希望可以帮助大家看的明明白白!** diff --git a/problems/0131.分割回文串.md b/problems/0131.分割回文串.md index ee2e6079..01ff35c5 100644 --- a/problems/0131.分割回文串.md +++ b/problems/0131.分割回文串.md @@ -1,7 +1,15 @@ +

+ + + + +

+

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

+ > 切割问题其实是一种组合问题! -# 131.分割回文串 +## 131.分割回文串 题目链接:https://leetcode-cn.com/problems/palindrome-partitioning/ @@ -9,16 +17,18 @@ 返回 s 所有可能的分割方案。 -示例: -输入: "aab" -输出: -[ - ["aa","b"], - ["a","a","b"] -] +示例: +输入: "aab" +输出: +[ + ["aa","b"], + ["a","a","b"] +] -# 思路 +## 思路 + +关于本题,大家也可以看我在B站的视频讲解:[131.分割回文串(B站视频)](https://www.bilibili.com/video/BV1c54y1e7k6) 本题这涉及到两个关键问题: @@ -29,9 +39,9 @@ 这种题目,想用for循环暴力解法,可能都不那么容易写出来,所以要换一种暴力的方式,就是回溯。 -一些同学可能想不清楚 回溯究竟是如果切割字符串呢? +一些同学可能想不清楚 回溯究竟是如何切割字符串呢? -我们来分析一下切割,**其实切割问题类似组合问题**。 +我们来分析一下切割,**其实切割问题类似组合问题**。 例如对于字符串abcdef: @@ -42,15 +52,15 @@ 所以切割问题,也可以抽象为一颗树形结构,如图: -![131.分割回文串](https://img-blog.csdnimg.cn/20201123203228309.png) +![131.分割回文串](https://code-thinking.cdn.bcebos.com/pics/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) 递归用来纵向遍历,for循环用来横向遍历,切割线(就是图中的红线)切割到字符串的结尾位置,说明找到了一个切割方法。 此时可以发现,切割问题的回溯搜索的过程和组合问题的回溯搜索的过程是差不多的。 -## 回溯三部曲 +## 回溯三部曲 -* 递归函数参数 +* 递归函数参数 全局变量数组path存放切割后回文的子串,二维数组result存放结果集。 (这两个参数可以放到函数参数里) @@ -60,7 +70,7 @@ 代码如下: -``` +```C++ vector> result; vector path; // 放已经回文的子串 void backtracking (const string& s, int startIndex) { @@ -68,7 +78,7 @@ void backtracking (const string& s, int startIndex) { * 递归函数终止条件 -![131.分割回文串](https://img-blog.csdnimg.cn/20201123203228309.png) +![131.分割回文串](https://code-thinking.cdn.bcebos.com/pics/131.%E5%88%86%E5%89%B2%E5%9B%9E%E6%96%87%E4%B8%B2.jpg) 从树形结构的图中可以看出:切割线切到了字符串最后面,说明找到了一种切割方法,此时就是本层递归的终止终止条件。 @@ -78,7 +88,7 @@ void backtracking (const string& s, int startIndex) { 所以终止条件代码如下: -``` +```C++ void backtracking (const string& s, int startIndex) { // 如果起始位置已经大于s的大小,说明已经找到了一组分割方案了 if (startIndex >= s.size()) { @@ -90,7 +100,7 @@ void backtracking (const string& s, int startIndex) { * 单层搜索的逻辑 -**来看看在递归循环,中如何截取子串呢?** +**来看看在递归循环,中如何截取子串呢?** 在`for (int i = startIndex; i < s.size(); i++)`循环中,我们 定义了起始位置startIndex,那么 [startIndex, i] 就是要截取的子串。 @@ -98,7 +108,7 @@ void backtracking (const string& s, int startIndex) { 代码如下: -``` +```C++ for (int i = startIndex; i < s.size(); i++) { if (isPalindrome(s, startIndex, i)) { // 是回文子串 // 获取[startIndex,i]在s中的子串 @@ -137,11 +147,11 @@ for (int i = startIndex; i < s.size(); i++) { 此时关键代码已经讲解完毕,整体代码如下(详细注释了) -# C++整体代码 +## C++整体代码 根据Carl给出的回溯算法模板: -``` +```C++ void backtracking(参数) { if (终止条件) { 存放结果; @@ -200,7 +210,7 @@ public: }; ``` -# 总结 +## 总结 这道题目在leetcode上是中等,但可以说是hard的题目了,但是代码其实就是按照模板的样子来的。 @@ -212,9 +222,9 @@ public: * 如何模拟那些切割线 * 切割问题中递归如何终止 * 在递归循环中如何截取子串 -* 如何判断回文 +* 如何判断回文 -**我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力**。 +**我们平时在做难题的时候,总结出来难究竟难在哪里也是一种需要锻炼的能力**。 一些同学可能遇到题目比较难,但是不知道题目难在哪里,反正就是很难。其实这样还是思维不够清晰,这种总结的能力需要多接触多锻炼。 @@ -224,17 +234,34 @@ public: **但接下来如何模拟切割线,如何终止,如何截取子串,其实都不好想,最后判断回文算是最简单的了**。 +关于模拟切割线,其实就是index是上一层已经确定了的分割线,i是这一层试图寻找的新分割线 + 除了这些难点,**本题还有细节,例如:切割过的地方不能重复切割所以递归函数需要传入i + 1**。 所以本题应该是一个道hard题目了。 **可能刷过这道题目的录友都没感受到自己原来克服了这么多难点,就把这道题目AC了**,这应该叫做无招胜有招,人码合一,哈哈哈。 -当然,本题131.分割回文串还可以用暴力搜索一波,132.分割回文串II和1278.分割回文串III 爆搜就会超时,需要使用动态规划了,我们会在动态规划系列中,详细讲解! -**就酱,如果感觉「代码随想录」不错,就把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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0134.加油站.md b/problems/0134.加油站.md index 491dc61a..9c72e84d 100644 --- a/problems/0134.加油站.md +++ b/problems/0134.加油站.md @@ -1,6 +1,13 @@ -今天开始继续贪心题目系列,让大家久等啦! +

+ + + + +

+

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

-# 134. 加油站 + +## 134. 加油站 题目链接:https://leetcode-cn.com/problems/gas-station/ @@ -16,38 +23,37 @@ * 输入数组均为非空数组,且长度相同。 * 输入数组中的元素均为非负数。 -示例 1: -输入: -gas = [1,2,3,4,5] -cost = [3,4,5,1,2] +示例 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 可为起始索引。 +输出: 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] +示例 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 升汽油。 -因此,无论怎样,你都不可能绕环路行驶一周。 +输出: -1 +解释: +你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 +我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 +开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 +开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 +你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 +因此,无论怎样,你都不可能绕环路行驶一周。 -# 思路 -## 暴力方法 +## 暴力方法 暴力的方法很明显就是O(n^2)的,遍历每一个加油站为起点的情况,模拟一圈。 @@ -59,7 +65,7 @@ cost = [3,4,3] C++代码如下: -``` +```C++ class Solution { public: int canCompleteCircuit(vector& gas, vector& cost) { @@ -76,20 +82,20 @@ public: return -1; } }; -``` +``` * 时间复杂度O(n^2) * 空间复杂度O(n) C++暴力解法在leetcode上提交也可以过。 -## 贪心算法(方法一) +## 贪心算法(方法一) 直接从全局进行贪心选择,情况如下: -* 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的 +* 情况一:如果gas的总和小于cost总和,那么无论从哪里出发,一定是跑不了一圈的 * 情况二:rest[i] = gas[i]-cost[i]为一天剩下的油,i从0开始计算累加到最后一站,如果累加没有出现负数,说明从0出发,油就没有断过,那么0就是起点。 -* 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点。 +* 情况三:如果累加的最小值是负数,汽车就要从非0节点出发,从后向前,看哪个节点能这个负数填平,能把这个负数填平的节点就是出发节点。 C++代码如下: @@ -111,7 +117,7 @@ public: // 情况3 for (int i = gas.size() - 1; i >= 0; i--) { int rest = gas[i] - cost[i]; - min += rest; + min += rest; if (min >= 0) { return i; } @@ -131,7 +137,7 @@ public: 但不管怎么说,解法毕竟还是巧妙的,不用过于执着于其名字称呼。 -## 贪心算法(方法二) +## 贪心算法(方法二) 可以换一个思路,首先如果总油量减去总消耗大于等于零那么一定可以跑完一圈,说明 各个站点的加油站 剩油量rest[i]相加一定是大于等于零的。 @@ -142,7 +148,7 @@ i从0开始累加rest[i],和记为curSum,一旦curSum小于零,说明[0, i 如图: ![134.加油站](https://img-blog.csdnimg.cn/20201213162821958.png) -那么为什么一旦[i,j] 区间和为负数,起始位置就可以是j+1呢,j+1后面就不会出现更大的负数? +那么为什么一旦[i,j] 区间和为负数,起始位置就可以是j+1呢,j+1后面就不会出现更大的负数? 如果出现更大的负数,就是更新j,那么起始位置又变成新的j+1了。 @@ -179,7 +185,7 @@ public: **说这种解法为贪心算法,才是是有理有据的,因为全局最优解是根据局部最优推导出来的**。 -# 总结 +## 总结 对于本题首先给出了暴力解法,暴力解法模拟跑一圈的过程其实比较考验代码技巧的,要对while使用的很熟练。 @@ -187,12 +193,24 @@ public: 对于第二种贪心方法,才真正体现出贪心的精髓,用局部最优可以推出全局最优,进而求得起始位置。 -就酱,「代码随想录」值得推荐给身边每一位学习算法的同学朋友,很多录友关注后都感觉相见恨晚! -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[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),关注后就会发现和「代码随想录」相见恨晚!** +## 其他语言版本 -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** + +Java: + + +Python: + + +Go: + +----------------------- +* 作者微信:[程序员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/0135.分发糖果.md b/problems/0135.分发糖果.md index 80fbf596..0595cff6 100644 --- a/problems/0135.分发糖果.md +++ b/problems/0135.分发糖果.md @@ -1,32 +1,38 @@ -> 好了 +

+ + + + +

+

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

-# 135. 分发糖果 +## 135. 分发糖果 -链接:https://leetcode-cn.com/problems/candy/ +链接:https://leetcode-cn.com/problems/candy/ 老师想给孩子们分发糖果,有 N 个孩子站成了一条直线,老师会根据每个孩子的表现,预先给他们评分。 你需要按照以下要求,帮助老师给这些孩子分发糖果: -* 每个孩子至少分配到 1 个糖果。 +* 每个孩子至少分配到 1 个糖果。 * 相邻的孩子中,评分高的孩子必须获得更多的糖果。 那么这样下来,老师至少需要准备多少颗糖果呢? -示例 1: -输入: [1,0,2] -输出: 5 -解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。 +示例 1: +输入: [1,0,2] +输出: 5 +解释: 你可以分别给这三个孩子分发 2、1、2 颗糖果。 -示例 2: -输入: [1,2,2] -输出: 4 -解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。 -第三个孩子只得到 1 颗糖果,这已满足上述两个条件。 +示例 2: +输入: [1,2,2] +输出: 4 +解释: 你可以分别给这三个孩子分发 1、2、1 颗糖果。 +第三个孩子只得到 1 颗糖果,这已满足上述两个条件。 -# 思路 +## 思路 这道题目一定是要确定一边之后,再确定另一边,例如比较每一个孩子的左边,然后再比较右边,**如果两边一起考虑一定会顾此失彼**。 @@ -37,12 +43,12 @@ 局部最优可以推出全局最优。 -如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1 +如果ratings[i] > ratings[i - 1] 那么[i]的糖 一定要比[i - 1]的糖多一个,所以贪心:candyVec[i] = candyVec[i - 1] + 1 代码如下: ```C++ -// 从前向后 +// 从前向后 for (int i = 1; i < ratings.size(); i++) { if (ratings[i] > ratings[i - 1]) candyVec[i] = candyVec[i - 1] + 1; } @@ -52,13 +58,13 @@ for (int i = 1; i < ratings.size(); i++) { ![135.分发糖果](https://img-blog.csdnimg.cn/20201117114916878.png) -再确定左孩子大于右孩子的情况(从后向前遍历) +再确定左孩子大于右孩子的情况(从后向前遍历) -遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢? +遍历顺序这里有同学可能会有疑问,为什么不能从前向后遍历呢? -因为如果从前向后遍历,根据 ratings[i + 1] 来确定 ratings[i] 对应的糖果,那么每次都不能利用上前一次的比较结果了。 +因为如果从前向后遍历,根据 ratings[i + 1] 来确定 ratings[i] 对应的糖果,那么每次都不能利用上前一次的比较结果了。 -**所以确定左孩子大于右孩子的情况一定要从后向前遍历!** +**所以确定左孩子大于右孩子的情况一定要从后向前遍历!** 如果 ratings[i] > ratings[i + 1],此时candyVec[i](第i个小孩的糖果数量)就有两个选择了,一个是candyVec[i + 1] + 1(从右边这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。 @@ -107,7 +113,7 @@ public: }; ``` -# 总结 +## 总结 这在leetcode上是一道困难的题目,其难点就在于贪心的策略,如果在考虑局部的时候想两边兼顾,就会顾此失彼。 @@ -118,11 +124,24 @@ public: 这样从局部最优推出了全局最优,即:相邻的孩子中,评分高的孩子获得更多的糖果。 -就酱,如果感觉「代码随想录」干货满满,就推荐给身边的朋友同学们吧,关注后就会发现相见恨晚! -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[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),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0139.单词拆分.md b/problems/0139.单词拆分.md index aed68179..ec996565 100644 --- a/problems/0139.单词拆分.md +++ b/problems/0139.单词拆分.md @@ -1,12 +1,50 @@ +

+ + + + +

+

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

+# 动态规划:单词拆分 -// 如何往 完全背包上靠? -// 用多次倒是可以往 完全背包上靠一靠 -// 和单词分割的问题有点像 +## 139.单词拆分 -[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) +题目链接:https://leetcode-cn.com/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://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q),就是枚举字符串的所有分割情况。 + +[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q):是枚举分割后的所有子串,判断是否回文。 + +本道是枚举分割所有字符串,判断是否在字典里出现过。 + +那么这里我也给出回溯法C++代码: + +```C++ class Solution { private: bool backtracking (const string& s, const unordered_set& wordSet, int startIndex) { @@ -29,15 +67,25 @@ public: }; ``` +* 时间复杂度:O(2^n),因为每一个单词都有两个状态,切割和不切割 +* 空间复杂度:O(n),算法递归系统调用栈的空间 + +那么以上代码很明显要超时了,超时的数据如下: + ``` "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab" ["a","aa","aaa","aaaa","aaaaa","aaaaaa","aaaaaaa","aaaaaaaa","aaaaaaaaa","aaaaaaaaaa"] ``` -可以使用一个一维数组保存一下,递归过程中计算的结果,C++代码如下: +递归的过程中有很多重复计算,可以使用数组保存一下递归过程中计算的结果。 -使用memory数组保存 每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。 -``` +这个叫做记忆化递归,这种方法我们之前已经提过很多次了。 + +使用memory数组保存每次计算的以startIndex起始的计算结果,如果memory[startIndex]里已经被赋值了,直接用memory[startIndex]的结果。 + +C++代码如下: + +```C++ class Solution { private: bool backtracking (const string& s, @@ -68,29 +116,134 @@ public: }; ``` +这个时间复杂度其实也是:O(2^n)。只不过对于上面那个超时测试用例优化效果特别明显。 -得好好分析一下,完全背包和01背包,这个对于刷leetcode太重要了 +**这个代码就可以AC了,当然回溯算法不是本题的主菜,背包才是!** -注意这里要空出一个 dp[0] 来做起始位置 -``` +## 背包问题 + +单词就是物品,字符串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循环遍历物品**。 + +对这个结论还有疑问的同学可以看这篇[本周小结!(动态规划系列五)](https://mp.weixin.qq.com/s/znj-9j8mWymRFaPjJN2Qnw),这篇本周小节中,我做了如下总结: + +求组合数:[动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) +求排列数:[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA) +求最小数:[动态规划:322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)、[动态规划:279.完全平方数](https://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ) + +本题最终要求的是是否都出现过,所以对出现单词集合里的元素是组合还是排列,并不在意! + +**那么本题使用求排列的方式,还是求组合的方式都可以**。 + +即:外层for循环遍历物品,内层for遍历背包 或者 外层for遍历背包,内层for循环遍历物品 都是可以的。 + +但本题还有特殊性,因为是要求子串,最好是遍历背包放在外循环,将遍历物品放在内循环。 + +如果要是外层for循环遍历物品,内层for遍历背包,就需要把所有的子串都预先放在一个容器里。(如果不理解的话,可以自己尝试这么写一写就理解了) + +**所以最终我选择的遍历顺序为:遍历背包放在外循环,将遍历物品放在内循环。内循环从前到后**。 + + +5. 举例推导dp[i] + +以输入: s = "leetcode", wordDict = ["leet", "code"]为例,dp状态如图: + +![139.单词拆分](https://img-blog.csdnimg.cn/20210202162652727.jpg) + +dp[s.size()]就是最终结果。 + +动规五部曲分析完毕,C++代码如下: + +```C++ 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++) { + 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; } } - //for (int k = 0; k <=i; k++) cout << dp[k] << " "; - //cout << endl; } return dp[s.size()]; } }; ``` -时间复杂度起始是O(n^3),因为substr返回子串的副本是O(n)的复杂度(n是substring的长度) +* 时间复杂度:O(n^3),因为substr返回子串的副本是O(n)的复杂度(这里的n是substring的长度) +* 空间复杂度:O(n) + + +## 总结 + +本题和我们之前讲解回溯专题的[回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q)非常像,所以我也给出了对应的回溯解法。 + +稍加分析,便可知道本题是完全背包,而且是求能否组成背包,所以遍历顺序理论上来讲 两层for循环谁先谁后都可以! + +但因为分割子串的特殊性,遍历背包放在外循环,将遍历物品放在内循环更方便一些。 + +本题其实递推公式都不是重点,遍历顺序才是重点,如果我直接把代码贴出来,估计同学们也会想两个for循环的顺序理所当然就是这样,甚至都不会想为什么遍历背包的for循环为什么在外层。 + +不分析透彻不是Carl的风格啊,哈哈 + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0141.环形链表.md b/problems/0141.环形链表.md deleted file mode 100644 index bdb12c5b..00000000 --- a/problems/0141.环形链表.md +++ /dev/null @@ -1,50 +0,0 @@ - -## 思路 - -可以使用快慢指针法, 分别定义 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重合。 - -动画如下: - - - - - -## C++代码如下 - -``` -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,不仅仅要找环,还要找环的入口。 - -142.环形链表II题解:[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) diff --git a/problems/0142.环形链表II.md b/problems/0142.环形链表II.md index 48aaed93..9622affc 100644 --- a/problems/0142.环形链表II.md +++ b/problems/0142.环形链表II.md @@ -1,11 +1,20 @@ +

+ + + + +

+

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

+ + + > 找到有没有环已经很不容易了,还要让我找到环的入口? -# 题目地址 + +## 142.环形链表II + https://leetcode-cn.com/problems/linked-list-cycle-ii/ - -# 第142题.环形链表II - 题意: 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 @@ -15,7 +24,7 @@ https://leetcode-cn.com/problems/linked-list-cycle-ii/ ![循环链表](https://img-blog.csdnimg.cn/20200816110112704.png) -# 思路 +## 思路 这道题目,不仅考察对链表的操作,而且还需要一些数学运算。 @@ -24,7 +33,7 @@ https://leetcode-cn.com/problems/linked-list-cycle-ii/ * 判断链表是否环 * 如果有环,如何找到这个环的入口 -## 判断链表是否有环 +### 判断链表是否有环 可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 @@ -34,11 +43,12 @@ https://leetcode-cn.com/problems/linked-list-cycle-ii/ 那么来看一下,**为什么fast指针和slow指针一定会相遇呢?** -可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 +可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 会发现最终都是这种情况, 如下图: - +![142环形链表1](https://img-blog.csdnimg.cn/20210318162236720.png) + fast和slow各自再走一步, fast和slow就相遇了 @@ -46,11 +56,10 @@ fast和slow各自再走一步, fast和slow就相遇了 动画如下: - - +![141.环形链表](https://tva1.sinaimg.cn/large/008eGmZEly1goo4xglk9yg30fs0b6u0x.gif) -## 如果有环,如何找到这个环的入口 +### 如果有环,如何找到这个环的入口 **此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。** @@ -58,7 +67,7 @@ fast和slow各自再走一步, fast和slow就相遇了 环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 从相遇节点 再到环形入口节点节点数为 z。 如图所示: - +![142环形链表2](https://img-blog.csdnimg.cn/20210318162938397.png) 那么相遇时: slow指针走过的节点数为: `x + y`, @@ -68,9 +77,9 @@ fast指针走过的节点数:` x + y + n (y + z)`,n为fast指针在环内走 `(x + y) * 2 = x + y + n (y + z)` -两边消掉一个(x+y): `x + y = n (y + z) ` +两边消掉一个(x+y): `x + y = n (y + z) ` -因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。 +因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。 所以要求x ,将x单独放在左面:`x = n (y + z) - y` , @@ -78,7 +87,7 @@ fast指针走过的节点数:` x + y + n (y + z)`,n为fast指针在环内走 这个公式说明什么呢? -先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。 +先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。 当 n为1的时候,公式就化解为 `x = z`, @@ -91,17 +100,16 @@ fast指针走过的节点数:` x + y + n (y + z)`,n为fast指针在环内走 动画如下: - +![142.环形链表II(求入口)](https://tva1.sinaimg.cn/large/008eGmZEly1goo58gauidg30fw0bi4qr.gif) -那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。 +那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。 其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。 +代码如下: -# C++代码 - -``` +```C++ /** * Definition for singly-linked list. * struct ListNode { @@ -136,32 +144,32 @@ public: ## 补充 -在推理过程中,大家可能有一个疑问就是:**为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?** +在推理过程中,大家可能有一个疑问就是:**为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?** 即文章[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)中如下的地方: - +![142环形链表5](https://img-blog.csdnimg.cn/20210318165123581.png) 首先slow进环的时候,fast一定是先进环来了。 如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子: - +![142环形链表3](https://img-blog.csdnimg.cn/2021031816503266.png) 可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。 重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图: - +![142环形链表4](https://img-blog.csdnimg.cn/2021031816515727.png) 那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。 -因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 +因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 **也就是说slow一定没有走到环入口3,而fast已经到环入口3了**。 -这说明什么呢? +这说明什么呢? **在slow开始走的那一环已经和fast相遇了**。 @@ -169,9 +177,44 @@ public: 好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)的补充。 -# 总结 +## 总结 这次可以说把环形链表这道题目的各个细节,完完整整的证明了一遍,说这是全网最详细讲解不为过吧,哈哈。 -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +## 其他语言版本 + + +Java: + + +Python: + +```python +class Solution: + def detectCycle(self, head: ListNode) -> ListNode: + slow, fast = head, head + while fast and fast.next: + slow = slow.next + fast = fast.next.next + # 如果相遇 + if slow == fast: + p = head + q = slow + while p!=q: + p = p.next + q = q.next + #你也可以return q + return p + + return None +``` + +Go: + + +----------------------- +* 作者微信:[程序员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/0143.重排链表.md b/problems/0143.重排链表.md deleted file mode 100644 index 9011692b..00000000 --- a/problems/0143.重排链表.md +++ /dev/null @@ -1,161 +0,0 @@ - -# 思路 - -本篇将给出三种C++实现的方法 - -* 数组模拟 -* 双向队列模拟 -* 直接分割链表 - -## 方法一 - -把链表放进数组中,然后通过双指针法,一前一后,来遍历数组,构造链表。 - -代码如下: - -``` -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++; - } - if (vec.size() % 2 == 0) { // 如果是偶数,还要多处理中间的一个 - cur->next = vec[i]; - cur = cur->next; - } - cur->next = nullptr; // 注意结尾 - } -}; -``` - -## 方法二 - -把链表放进双向队列,然后通过双向队列一前一后弹出数据,来构造新的链表。这种方法比操作数组容易一些,不用双指针模拟一前一后了 -``` -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; // 注意结尾 - } -}; -``` - -## 方法三 - -将链表分割成两个链表,然后把第二个链表反转,之后在通过两个链表拼接成新的链表。 - -如图: - - - -这种方法,比较难,平均切割链表,看上去很简单,真正代码写的时候有很多细节,同时两个链表最后拼装整一个新的链表也有一些细节需要注意! - -代码如下: - -``` -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; - } - } -}; -``` diff --git a/problems/0144.二叉树的前序遍历.md b/problems/0144.二叉树的前序遍历.md deleted file mode 100644 index 7e2e1260..00000000 --- a/problems/0144.二叉树的前序遍历.md +++ /dev/null @@ -1,387 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/binary-tree-preorder-traversal/ - -# 思路 -这篇文章,**彻底讲清楚应该如何写递归,并给出了前中后序三种不同的迭代法,然后分析迭代法的代码风格为什么没有统一,最后给出统一的前中后序迭代法的代码,帮大家彻底吃透二叉树的深度优先遍历。** - -对二叉树基础理论还不清楚的话,可以看看这个[关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A)。 - -[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)介绍了二叉树的前后中序的递归遍历方式。 - -[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)介绍了二叉树的前后中序迭代写法。 - -[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) 介绍了二叉树前中后迭代方式的统一写法。 - -以下开始开始正文: - -* 二叉树深度优先遍历 - * 前序遍历: [0144.二叉树的前序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) - * 后序遍历: [0145.二叉树的后序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0145.二叉树的后序遍历.md) - * 中序遍历: [0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md) -* 二叉树广度优先遍历 - * 层序遍历:[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) - -这几道题目建议大家都做一下,本题解先只写二叉树深度优先遍历,二叉树广度优先遍历请看题解[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) - -这里想帮大家一下,明确一下二叉树的遍历规则: - -![二叉树前后中遍历](https://img-blog.csdnimg.cn/20200808191505393.png) - -以上述中,前中后序遍历顺序如下: - -* 前序遍历(中左右):5 4 1 2 6 7 8 -* 中序遍历(左中右):1 4 2 5 7 6 8 -* 后序遍历(左右中):1 2 4 7 8 6 5 - -# 递归法 - -接下来我们来好好谈一谈递归,为什么很多同学看递归算法都是“一看就会,一写就废”。主要是对递归不成体系,没有方法论,每次写递归算法 ,都是靠玄学来写代码,代码能不能编过都靠运气。 - -这里帮助大家确定下来递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法! - - -1. **确定递归函数的参数和返回值:** -确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。 - -2. **确定终止条件:** -写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。 - -3. **确定单层递归的逻辑:** -确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。 - -好了,我们确认了递归的三要素,接下来就来练练手: - -**以下以前序遍历为例:** - -1. **确定递归函数的参数和返回值**:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: - -``` -void traversal(TreeNode* cur, vector& vec) -``` - -2. **确定终止条件**:在递归的过程中,如何算是递归结束了呢,当然是当前遍历的节点是空了,那么本层递归就要要结束了,所以如果当前遍历的这个节点是空,就直接return,代码如下: - -``` -if (cur == NULL) return; -``` - -3. 确定单层递归的逻辑:前序遍历是中左右的循序,所以在单层递归的逻辑,是要先取中节点的数值,代码如下: - -``` -vec.push_back(cur->val); // 中 -traversal(cur->left, vec); // 左 -traversal(cur->right, vec); // 右 -``` - -单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,在看一下完整代码: - -前序遍历: - -``` -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; - } -}; -``` - -中序遍历: - -``` - void traversal(TreeNode* cur, vector& vec) { - if (cur == NULL) return; - traversal(cur->left, vec); // 左 - vec.push_back(cur->val); // 中 - traversal(cur->right, vec); // 右 - } -``` - -后序遍历: - -``` - void traversal(TreeNode* cur, vector& vec) { - if (cur == NULL) return; - traversal(cur->left, vec); // 左 - traversal(cur->right, vec); // 右 - vec.push_back(cur->val); // 中 - } -``` - -# 迭代法 - -实践过的同学,应该会发现使用迭代法实现先中后序遍历,很难写出统一的代码,不像是递归法,实现了其中的一种遍历方式,其他两种只要稍稍改一下节点顺序就可以了。而迭代法,貌似需要每一种遍历都要写出不同风格的代码。 - -那么接下来我先带大家看一看其中的根本原因,其实是可以针对三种遍历方式,使用迭代法可以写出统一风格的代码。 - -前序遍历(迭代法)不难写出如下代码: - -``` -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; - } -}; -``` - -这时会发现貌似使用迭代法写出先序遍历并不难,确实不难,但难却难在,我们再用迭代法写中序遍历的时候,发现套路又不一样了,目前的这个逻辑无法直接应用到中序遍历上。 - -## 前后中遍历迭代法不统一的写法 - -为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作,**一个是处理:将元素放进result数组中,一个是访问:遍历节点。** - -分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,要先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,**因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。** - -那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了**处理顺序和访问顺序是不一致的。** - -那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。 - -**中序遍历,可以写出如下代码:** - -``` -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(); - st.pop(); - result.push_back(cur->val); - cur = cur->right; - } - } - return result; - } -}; - -``` - -再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序 -,然后在反转result数组,输出的结果顺序就是左右中了,如下图: -![前序到后序](https://img-blog.csdnimg.cn/20200808200338924.png) - -**所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:** - -``` -class Solution { -public: - - vector postorderTraversal(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->left); // 相对于前序遍历,这更改一下入栈顺序 - st.push(node->right); - } - reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了 - return result; - } -}; - -``` - -此时我们实现了前后中遍历的三种迭代法,**是不是发现迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。** - -**重头戏来了,接下来介绍一下统一写法。** - -## 前后中遍历迭代法统一的写法 - -我们以中序遍历为例,之前说使用栈的话,**无法同时解决处理过程和访问过程不一致的情况**,那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记,标记就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 - -中序遍历代码如下: -``` -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; - } -}; -``` - -看代码有点抽象我们来看一下动画(中序遍历): - - - -前序遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**) - -``` -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; - } -}; -``` - -后续遍历代码如下: (**注意此时我们仅仅和中序遍历相对改变了两行代码的顺序**) - -``` -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; - } -}; -``` - -## C++代码 - -### 递归 -``` -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; - } -}; -``` - -### 栈 -``` -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; - } -}; -``` - -### 栈 通用模板 - -``` -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; - } -}; -``` - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0145.二叉树的后序遍历.md b/problems/0145.二叉树的后序遍历.md deleted file mode 100644 index 74727ba6..00000000 --- a/problems/0145.二叉树的后序遍历.md +++ /dev/null @@ -1,88 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-postorder-traversal/ - -## 思路 - -详细题解请看这篇:[一文学通二叉树前中后序递归法与迭代法](https://github.com/youngyangyang04/leetcode/blob/master/problems/0144.二叉树的前序遍历.md) - -## C++代码 - -### 递归 - -``` -class Solution { -public: - void traversal(TreeNode* root, vector& vec) { - if (root == NULL) return; - traversal(root->left, vec); - traversal(root->right, vec); - vec.push_back(root->val); - } - vector postorderTraversal(TreeNode* root) { - vector result; - traversal(root, result); - return result; - - } -}; -``` - -### 栈 - -先中右左遍历,然后再反转 - -``` -class Solution { -public: - - vector postorderTraversal(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->left); // 相对于前序遍历,这更改一下入栈顺序 - st.push(node->right); - } - reverse(result.begin(), result.end()); // 将结果反转之后就是左右中的顺序了 - return result; - } -}; -``` -### 栈 通用模板 - -详细代码注释看 [0094.二叉树的中序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0094.二叉树的中序遍历.md) - -``` -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; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0147.对链表进行插入排序.md b/problems/0147.对链表进行插入排序.md deleted file mode 100644 index f834e0ba..00000000 --- a/problems/0147.对链表进行插入排序.md +++ /dev/null @@ -1,47 +0,0 @@ - -## 思路 - -这道题目还是很考察链表操作的。 - -如果不用虚拟头结点的话,这道题会很麻烦,对虚拟头结点不熟悉的同学,可以看这篇:[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)。 - -如果想整体掌握链表操作的话,看一下这篇[链表大总结](https://mp.weixin.qq.com/s/vK0JjSTHfpAbs8evz5hH8A)。 - -本题在模拟插入排序的过程中,一共要有三次改变节点指针的操作,如果不画一个图,很容易搞蒙了。 - -我举一个排序插入节点2的例子,并详细标注了每一个步骤,晚上一个插入操作有五步,如下: - - - -代码中注释的步骤和图中都是一一对应的,对着图看代码,就比较清晰了,C++代码如下: - -``` -class Solution { -public: - ListNode* insertionSortList(ListNode* head) { - if (head == nullptr) return head; - - ListNode* dummyHead = new ListNode(0); // 定一个虚拟头结点 - ListNode* cur = head; - ListNode* pre = dummyHead; - - while (cur != nullptr) { - while (pre->next != nullptr && pre->next->val < cur->val) { - pre = pre->next; - } - // 在pre和prenext之间插入数据 - ListNode* next = cur->next; // 步骤一:保存curnext - cur->next = pre->next; // 步骤二 - pre->next = cur; // 步骤三 - pre = dummyHead; // 步骤四:pre重新指向虚拟头结点来找下一个插入位置 - cur = next; // 步骤五:cur的前一个节点的下一个节点指向保存的next - } - return dummyHead->next; - } -}; -``` - -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[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/0150.逆波兰表达式求值.md b/problems/0150.逆波兰表达式求值.md index dc85b481..4e7365f7 100644 --- a/problems/0150.逆波兰表达式求值.md +++ b/problems/0150.逆波兰表达式求值.md @@ -1,9 +1,20 @@ -## 题目地址 -https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/ +

+ + + + +

+

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

+ + + > 这不仅仅是一道好题,也展现出计算机的思考方式 -# 150. 逆波兰表达式求值 +# 150. 逆波兰表达式求值 + +https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/ + 根据 逆波兰表示法,求表达式的值。 有效的运算符包括 + ,  - ,  * ,  / 。每个运算对象可以是整数,也可以是另一个逆波兰表达式。 @@ -14,28 +25,28 @@ https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/ 给定逆波兰表达式总是有效的。换句话说,表达式总会得出有效数值且不存在除数为 0 的情况。   -示例 1: -输入: ["2", "1", "+", "3", " * "] -输出: 9 -解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 +示例 1: +输入: ["2", "1", "+", "3", " * "] +输出: 9 +解释: 该算式转化为常见的中缀算术表达式为:((2 + 1) * 3) = 9 -示例 2: -输入: ["4", "13", "5", "/", "+"] -输出: 6 -解释: 该算式转化为常见的中缀算术表达式为:(4 + (13 / 5)) = 6 +示例 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 +示例 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   逆波兰表达式:是一种后缀表达式,所谓后缀就是指算符写在后面。 @@ -50,7 +61,7 @@ https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/ * 适合用栈操作运算:遇到数字则入栈;遇到算符则取出栈顶两个数字进行计算,并将结果压入栈中。 -# 思路 +# 思路 在上一篇文章中[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)提到了 递归就是用栈来实现的。 @@ -60,18 +71,17 @@ https://leetcode-cn.com/problems/evaluate-reverse-polish-notation/ 但我们没有必要从二叉树的角度去解决这个问题,只要知道逆波兰表达式是用后续遍历的方式把二叉树序列化了,就可以了。 -在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)中的对对碰游戏是不是就非常像了。** +在进一步看,本题中每一个子表达式要得出一个结果,然后拿这个结果再进行运算,那么**这岂不就是一个相邻字符串消除的过程,和[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)中的对对碰游戏是不是就非常像了。** 如动画所示: - +![150.逆波兰表达式求值](https://code-thinking.cdn.bcebos.com/gifs/150.%E9%80%86%E6%B3%A2%E5%85%B0%E8%A1%A8%E8%BE%BE%E5%BC%8F%E6%B1%82%E5%80%BC.gif) 相信看完动画大家应该知道,这和[1047. 删除字符串中的所有相邻重复项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg)是差不错的,只不过本题不要相邻元素做消除了,而是做运算! -代码如下: +C++代码如下: -## C++代码 -``` +```C++ class Solution { public: int evalRPN(vector& tokens) { @@ -97,13 +107,13 @@ public: }; ``` -# 题外话 +# 题外话 我们习惯看到的表达式都是中缀表达式,因为符合我们的习惯,但是中缀表达式对于计算机来说就不是很友好了。 例如:4 + 13 / 5,这就是中缀表达式,计算机从左到右去扫描的话,扫到13,还要判断13后面是什么运算法,还要比较一下优先级,然后13还和后面的5做运算,做完运算之后,还要向前回退到 4 的位置,继续做加法,你说麻不麻烦! -那么将中缀表达式,转化为后缀表达式之后:["4", "13", "5", "/", "+"] ,就不一样了,计算机可以利用栈里顺序处理,不需要考虑优先级了。也不用回退了, **所以后缀表达式对计算机来说是非常友好的。** +那么将中缀表达式,转化为后缀表达式之后:["4", "13", "5", "/", "+"] ,就不一样了,计算机可以利用栈里顺序处理,不需要考虑优先级了。也不用回退了, **所以后缀表达式对计算机来说是非常友好的。** 可以说本题不仅仅是一道好题,也展现出计算机的思考方式。 @@ -115,5 +125,57 @@ public: -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +## 其他语言版本 + +java: + +```Java +public class EvalRPN { + + public int evalRPN(String[] tokens) { + Deque stack = new LinkedList(); + for (String token : tokens) { + char c = token.charAt(0); + if (!isOpe(token)) { + stack.addFirst(stoi(token)); + } else if (c == '+') { + stack.push(stack.pop() + stack.pop()); + } else if (c == '-') { + stack.push(- stack.pop() + stack.pop()); + } else if (c == '*') { + stack.push( stack.pop() * stack.pop()); + } else { + int num1 = stack.pop(); + int num2 = stack.pop(); + stack.push( num2/num1); + } + } + return stack.pop(); + } + + + private boolean isOpe(String s) { + return s.length() == 1 && s.charAt(0) <'0' || s.charAt(0) >'9'; + } + + private int stoi(String s) { + return Integer.valueOf(s); + } + + + public static void main(String[] args) { + new EvalRPN().evalRPN(new String[] {"10","6","9","3","+","-11","*","/","*","17","+","5","+"}); + } + +} +``` + + + + +----------------------- +* 作者微信:[程序员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/0151.翻转字符串里的单词.md b/problems/0151.翻转字符串里的单词.md index 35258a55..d81c139d 100644 --- a/problems/0151.翻转字符串里的单词.md +++ b/problems/0151.翻转字符串里的单词.md @@ -1,29 +1,37 @@ +

+ + + + +

+

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

+ -## 题目地址 -https://leetcode-cn.com/problems/reverse-words-in-a-string/ > 综合考察字符串操作的好题。 -# 题目:151.翻转字符串里的单词 +# 151.翻转字符串里的单词 + +https://leetcode-cn.com/problems/reverse-words-in-a-string/ 给定一个字符串,逐个翻转字符串中的每个单词。 -示例 1: -输入: "the sky is blue" -输出: "blue is sky the" +示例 1: +输入: "the sky is blue" +输出: "blue is sky the" -示例 2: -输入: "  hello world!  " -输出: "world! hello" -解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 +示例 2: +输入: "  hello world!  " +输出: "world! hello" +解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 -示例 3: -输入: "a good   example" -输出: "example good a" -解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 +示例 3: +输入: "a good   example" +输出: "example good a" +解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 -# 思路 +# 思路 **这道题目可以说是综合考察了字符串的多种操作。** @@ -44,13 +52,13 @@ https://leetcode-cn.com/problems/reverse-words-in-a-string/ 如动画所示: - +![151翻转字符串里的单词](https://tva1.sinaimg.cn/large/008eGmZEly1gp0kv5gl4mg30gy0c4nbp.gif) 这样我们就完成了翻转字符串里的单词。 思路很明确了,我们说一说代码的实现细节,就拿移除多余空格来说,一些同学会上来写如下代码: -``` +```C++ void removeExtraSpaces(string& s) { for (int i = s.size() - 1; i > 0; i--) { if (s[i] == s[i - 1] && s[i] == ' ') { @@ -72,7 +80,7 @@ void removeExtraSpaces(string& s) { 如果不仔细琢磨一下erase的时间复杂读,还以为以上的代码是O(n)的时间复杂度呢。 -想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作,erase实现原理题目:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA),最优的算法来移除元素也要O(n)。 +想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作,erase实现原理题目:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA),最优的算法来移除元素也要O(n)。 erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。 @@ -82,7 +90,7 @@ erase操作上面还套了一个for循环,那么以上代码移除冗余空格 那么使用双指针来移除冗余空格代码如下: fastIndex走的快,slowIndex走的慢,最后slowIndex就标记着移除多余空格后新字符串的长度。 -``` +```C++ void removeExtraSpaces(string& s) { int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 // 去掉字符串前面的空格 @@ -131,7 +139,7 @@ void reverse(string& s, int start, int end) { 效率: - + ``` class Solution { @@ -195,5 +203,27 @@ public: } }; ``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0155.最小栈.md b/problems/0155.最小栈.md deleted file mode 100644 index 6b5fb643..00000000 --- a/problems/0155.最小栈.md +++ /dev/null @@ -1,51 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/min-stack/ - -## 思路 - -有的同学一开始会把这道题目想简单了,用一个变量记录最小值不就得了,其实是如果要是弹出了这个最小值的话,我们还要记录次小值,所以需要一个辅助数组来记录次小值。 - -我这里使用数组来实现栈,在用一个数组来放当前栈里最小数值,同时使用辅助数组来记录 - -## C++代码 - -``` -class MinStack { -public: - vector vec; - vector minVec; - /** initialize your data structure here. */ - MinStack() { - } - void push(int x) { - vec.push_back(x); - if (minVec.size() == 0) { - minVec.push_back(x); - } else if (x <= minVec[minVec.size() - 1]) { // 这里一定是下小于等于,防止多个最小值的情况 - minVec.push_back(x); - } - } - void pop() { - if (vec.size() == 0) { // 防止下面的操作会导致越界 - return; - } - if (vec[vec.size() - 1] == minVec[minVec.size() - 1]) { - minVec.pop_back(); - } - vec.pop_back(); - } - - int top() { - // 这里有越界的危险,但是题目也没有说如果栈为空,top()应该返回啥,所以就默认测试用例没有上来直接top的用例了 - return vec[vec.size() - 1]; - } - - int getMin() { - // 这里有越界的危险,但是题目也没有说如果栈为空,getMin()应该返回啥,所以就默认测试用例没有上来直接getMin的用例了 - return minVec[minVec.size() - 1]; - } -}; - -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0188.买卖股票的最佳时机IV.md b/problems/0188.买卖股票的最佳时机IV.md new file mode 100644 index 00000000..11c5805b --- /dev/null +++ b/problems/0188.买卖股票的最佳时机IV.md @@ -0,0 +1,186 @@ +

+ + + + +

+

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

+ +## 188.买卖股票的最佳时机IV + +题目链接:https://leetcode-cn.com/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 + +## 思路 + +这道题目可以说是[动态规划:123.买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)的进阶版,这里要求至多有k次交易。 + +动规五部曲,分析如下: + +1. 确定dp数组以及下标的含义 + +在[动态规划:123.买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)中,我是定义了一个二维dp数组,本题其实依然可以用一个二维dp数组。 + +使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j] + +j的状态表示为: + +* 0 表示不操作 +* 1 第一次买入 +* 2 第一次卖出 +* 3 第二次买入 +* 4 第二次卖出 +* ..... + +**大家应该发现规律了吧 ,除了0以外,偶数就是卖出,奇数就是买入**。 + +题目要求是至多有K笔交易,那么j的范围就定义为 2 * k + 1 就可以了。 + +所以二维dp数组的C++定义为: + +``` +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][0]); + +同理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][i] + prices[i], dp[i][2]) + +同理可以类比剩下的状态,代码如下: + +```C++ +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://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)最大的区别就是这里要类比j为奇数是买,偶数是卖剩的状态**。 + +3. dp数组如何初始化 + +第0天没有操作,这个最容易想到,就是0,即:dp[0][0] = 0; + +第0天做第一次买入的操作,dp[0][1] = -prices[0]; + +第0天做第一次卖出的操作,这个初始值应该是多少呢? + +首先卖出的操作一定是收获利润,整个股票买卖最差情况也就是没有盈利即全程无操作现金为0, + +从递推公式中可以看出每次是取最大值,那么既然是收获利润如果比0还小了就没有必要收获这个利润了。 + +所以dp[0][2] = 0; + +第0天第二次买入操作,初始值应该是多少呢? + +不用管第几次,现在手头上没有现金,只要买入,现金就做相应的减少。 + +第二次买入操作,初始化为:dp[0][3] = -prices[0]; + +**所以同理可以推出dp[0][j]当j为奇数的时候都初始化为 -prices[0]** + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20201229100358221.png) + +最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解。 + +以上分析完毕,C++代码如下: + +```C++ +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表示买还是卖的状态,从定义上来讲是比较直观。 + +但感觉三维数组操作起来有些麻烦,我是直接用二维数组来模拟三位数组的情况,代码看起来也清爽一些。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0198.打家劫舍.md b/problems/0198.打家劫舍.md new file mode 100644 index 00000000..b9ccec45 --- /dev/null +++ b/problems/0198.打家劫舍.md @@ -0,0 +1,128 @@ +

+ + + + +

+

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

+ +## 198.打家劫舍 + +题目链接:https://leetcode-cn.com/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 + + +## 思路 + +打家劫舍是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]); + +代码如下: + +```C++ +vector dp(nums.size()); +dp[0] = nums[0]; +dp[1] = max(nums[0], nums[1]); +``` + +4. 确定遍历顺序 + +dp[i] 是根据dp[i - 2] 和 dp[i - 1] 推导出来的,那么一定是从前到后遍历! + +代码如下: +```C++ +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://img-blog.csdnimg.cn/20210221170954115.jpg) + +红框dp[nums.size() - 1]为结果。 + +以上分析完毕,C++代码如下: + +```C++ +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]; + } +}; +``` + +## 总结 + +打家劫舍是DP解决的经典题目,这道题也是打家劫舍入门级题目,后面我们还会变种方式来打劫的。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0199.二叉树的右视图.md b/problems/0199.二叉树的右视图.md deleted file mode 100644 index ce17ca72..00000000 --- a/problems/0199.二叉树的右视图.md +++ /dev/null @@ -1,46 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-right-side-view/ - -## 思路 - -这里再讲一遍二叉树的广度优先遍历(层序遍历) - -需要借用一个辅助数据结构队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** - -使用队列实现广度优先遍历,动画如下: - - - -这样就实现了层序从左到右遍历二叉树。 - -建议先做一下这道题目[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md), 在做这道,就会发现这是一道 广度优先搜索模板题目,层序遍历的时候,将每一层的最后元素放入result数组中, - -层序遍历的时候,将单层的最后面的元素放进数组中,随后返回数组就可以了。 - -代码如下: - -## C++代码 - -``` -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; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0202.快乐数.md b/problems/0202.快乐数.md index f0e52376..d6b1cdd2 100644 --- a/problems/0202.快乐数.md +++ b/problems/0202.快乐数.md @@ -1,10 +1,19 @@ -# 题目地址 -https://leetcode-cn.com/problems/happy-number/ +

+ + + + +

+

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

+ + > 该用set的时候,还是得用set # 第202题. 快乐数 +https://leetcode-cn.com/problems/happy-number/ + 编写一个算法来判断一个数 n 是不是快乐数。 「快乐数」定义为:对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和,然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。如果 可以变为  1,那么这个数就是快乐数。 @@ -13,17 +22,17 @@ https://leetcode-cn.com/problems/happy-number/ **示例:** -输入:19 -输出:true -解释: -1^2 + 9^2 = 82 -8^2 + 2^2 = 68 -6^2 + 8^2 = 100 -1^2 + 0^2 + 0^2 = 1 +输入: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会重复出现,这对解题很重要!** @@ -35,9 +44,9 @@ https://leetcode-cn.com/problems/happy-number/ **还有一个难点就是求和的过程,如果对取数值各个位上的单数操作不熟悉的话,做这道题也会比较艰难。** -# C++代码 +C++代码如下: -``` +```C++ class Solution { public: // 取数值各个位上的单数之和 @@ -67,5 +76,26 @@ public: } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0203.移除链表元素.md b/problems/0203.移除链表元素.md index f2a4909d..e6667091 100644 --- a/problems/0203.移除链表元素.md +++ b/problems/0203.移除链表元素.md @@ -1,62 +1,69 @@ -## 题目地址 +

+ + + + +

+

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

-https://leetcode-cn.com/problems/remove-linked-list-elements/ > 链表操作中,可以使用原链表来直接进行删除操作,也可以设置一个虚拟头结点在进行删除操作,接下来看一看哪种方式更方便。 -# 第203题:移除链表元素 +# 203.移除链表元素 + +https://leetcode-cn.com/problems/remove-linked-list-elements/ 题意:删除链表中等于给定值 val 的所有节点。 ![203题目示例](https://img-blog.csdnimg.cn/20200814104441179.png) -# 思路 +# 思路 这里以链表 1 4 2 4 来举例,移除元素4。 - +![203_链表删除元素1](https://img-blog.csdnimg.cn/20210316095351161.png) 如果使用C,C++编程语言的话,不要忘了还要从内存中删除这两个移除的节点, 清理节点内存之后如图: - +![203_链表删除元素2](https://img-blog.csdnimg.cn/20210316095418280.png) **当然如果使用java ,python的话就不用手动管理内存了。** 还要说明一下,就算使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养生手动清理内存的习惯。 -这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了, +这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了, 那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢? 这里就涉及如下链表操作的两种方式: -* **直接使用原来的链表来进行删除操作。** +* **直接使用原来的链表来进行删除操作。** * **设置一个虚拟头结点在进行删除操作。** 来看第一种操作:直接使用原来的链表来进行移除。 - +![203_链表删除元素3](https://img-blog.csdnimg.cn/2021031609544922.png) 移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。 所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。 - +![203_链表删除元素4](https://img-blog.csdnimg.cn/20210316095512470.png) 依然别忘将原头结点从内存中删掉。 - +![203_链表删除元素5](https://img-blog.csdnimg.cn/20210316095543775.png) 这样移除了一个头结点,是不是发现,在单链表中移除头结点 和 移除其他节点的操作方式是不一样,其实在写代码的时候也会发现,需要单独写一段逻辑来处理移除头结点的情况。 -那么可不可以 以一种统一的逻辑来移除 链表的节点呢。 +那么可不可以 以一种统一的逻辑来移除 链表的节点呢。 其实**可以设置一个虚拟头结点**,这样原链表的所有节点就都可以按照统一的方式进行移除了。 来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。 - +![203_链表删除元素6](https://img-blog.csdnimg.cn/20210316095619221.png) 这里来给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素1。 @@ -71,11 +78,11 @@ https://leetcode-cn.com/problems/remove-linked-list-elements/ **直接使用原来的链表来进行移除节点操作:** -``` +```C++ class Solution { public: ListNode* removeElements(ListNode* head, int val) { - // 删除头结点 + // 删除头结点 while (head != NULL && head->val == val) { // 注意这里不是if ListNode* tmp = head; head = head->next; @@ -100,14 +107,13 @@ public: **设置一个虚拟头结点在进行移除节点操作:** -``` +```C++ 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; @@ -117,11 +123,33 @@ public: cur = cur->next; } } - return dummyHead->next; + head = dummyHead->next; + delete dummyHead; + return head; } }; + ``` -**我将算法学习相关的资料已经整理到了Github上:https://github.com/youngyangyang04/leetcode-master,里面还有leetcode刷题攻略、各个类型经典题目刷题顺序、思维导图看一看一定会有所收获!** -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0205.同构字符串.md b/problems/0205.同构字符串.md deleted file mode 100644 index 38d5579e..00000000 --- a/problems/0205.同构字符串.md +++ /dev/null @@ -1,34 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/isomorphic-strings/ - -## 思路 - -使用两个map 保存 s[i] 到 t[j] 和 t[j] 到 s[i] 的映射关系,如果发现对应不上,立刻返回 false - -## C++代码 - -``` -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」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0206.翻转链表.md b/problems/0206.翻转链表.md index 8fc9d9cd..b465cdf9 100644 --- a/problems/0206.翻转链表.md +++ b/problems/0206.翻转链表.md @@ -1,24 +1,31 @@ -## 题目地址 +

+ + + + +

+

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

-https://leetcode-cn.com/problems/reverse-linked-list/ > 反转链表的写法很简单,一些同学甚至可以背下来但过一阵就忘了该咋写,主要是因为没有理解真正的反转过程。 -# 第206题:反转链表 +# 206.反转链表 + +https://leetcode-cn.com/problems/reverse-linked-list/ 题意:反转一个单链表。 示例: -输入: 1->2->3->4->5->NULL +输入: 1->2->3->4->5->NULL 输出: 5->4->3->2->1->NULL -# 思路 +# 思路 如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。 其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示: - +![206_反转链表](https://img-blog.csdnimg.cn/20210218090901207.png) 之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改表next指针的方向。 @@ -26,7 +33,7 @@ https://leetcode-cn.com/problems/reverse-linked-list/ 我们拿有示例中的链表来举例,如动画所示: - +![](https://tva1.sinaimg.cn/large/008eGmZEly1gnrf1oboupg30gy0c44qp.gif) 首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。 @@ -41,7 +48,7 @@ https://leetcode-cn.com/problems/reverse-linked-list/ # C++代码 ## 双指针法 -``` +```C++ class Solution { public: ListNode* reverseList(ListNode* head) { @@ -62,12 +69,12 @@ public: ## 递归法 -递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。 +递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。 关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。 具体可以看代码(已经详细注释),**双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。** -``` +```C++ class Solution { public: ListNode* reverse(ListNode* pre,ListNode* cur){ @@ -89,5 +96,24 @@ public: }; ``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0209.长度最小的子数组.md b/problems/0209.长度最小的子数组.md index 1dd5ba15..1b00c4e3 100644 --- a/problems/0209.长度最小的子数组.md +++ b/problems/0209.长度最小的子数组.md @@ -1,18 +1,13 @@ -

- -

- - + + - -

+

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

-> 滑动窗口拯救了你 -# 题目209.长度最小的子数组 +## 209.长度最小的子数组 题目链接: https://leetcode-cn.com/problems/minimum-size-subarray-sum/ @@ -20,18 +15,18 @@ 示例: -输入:s = 7, nums = [2,3,1,2,4,3] -输出:2 -解释:子数组 [4,3] 是该条件下的长度最小的子数组。 +输入:s = 7, nums = [2,3,1,2,4,3] +输出:2 +解释:子数组 [4,3] 是该条件下的长度最小的子数组。 -# 暴力解法 +## 暴力解法 这道题目暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列,时间复杂度很明显是O(n^2) 。 代码如下: -``` +```C++ class Solution { public: int minSubArrayLen(int s, vector& nums) { @@ -54,10 +49,10 @@ public: } }; ``` -时间复杂度:O(n^2) -空间复杂度:O(1) +时间复杂度:$O(n^2)$ +空间复杂度:$O(1)$ -# 滑动窗口 +## 滑动窗口 接下来就开始介绍数组操作中另一个重要的方法:**滑动窗口**。 @@ -65,7 +60,7 @@ public: 这里还是以题目中的示例来举例,s=7, 数组是 2,3,1,2,4,3,来看一下查找的过程: - +![209.长度最小的子数组](https://code-thinking.cdn.bcebos.com/gifs/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 是最短距离。 @@ -73,9 +68,9 @@ public: 在本题中实现滑动窗口,主要确定如下三点: -* 窗口内是什么? +* 窗口内是什么? * 如何移动窗口的起始位置? -* 如何移动窗口的结束位置? +* 如何移动窗口的结束位置? 窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。 @@ -85,14 +80,13 @@ public: 解题的关键在于 窗口的起始位置如何移动,如图所示: - +![leetcode_209](https://img-blog.csdnimg.cn/20210312160441942.png) 可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。** +C++代码如下: -# C++滑动窗口代码 - -``` +```C++ class Solution { public: int minSubArrayLen(int s, vector& nums) { @@ -115,12 +109,72 @@ public: }; ``` -时间复杂度:O(n) -空间复杂度:O(1) +时间复杂度:$O(n)$ +空间复杂度:$O(1)$ + +**一些录友会疑惑为什么时间复杂度是O(n)**。 + +不要以为for里放一个while就以为是$O(n^2)$啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是2 * n 也就是$O(n)$。 + +## 其他语言补充 + +python: + +```python +class Solution: + def minSubArrayLen(self, s: int, nums: List[int]) -> int: + # 定义一个无限大的数 + res = float("inf") + Sum = 0 + index = 0 + for i in range(len(nums)): + Sum += nums[i] + while Sum >= s: + res = min(res, i-index+1) + Sum -= nums[index] + index += 1 + return 0 if res==float("inf") else res +``` + +## 相关题目推荐 + +* 904.水果成篮 +* 76.最小覆盖子串 -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** -

- -

+ +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + +JavaScript: +``` +var minSubArrayLen = (target, nums) => { + let left = 0, right = 0,win = Infinity,sum = 0; + while(right < nums.length){ + sum += nums[right]; + while(sum >= target){ + win = right - left + 1 < win? right - left + 1 : win; + sum -= nums[left]; + left++; + } + right++; + } + return win === Infinity? 0:win; +}; +``` + +----------------------- +* 作者微信:[程序员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/0213.打家劫舍II.md b/problems/0213.打家劫舍II.md new file mode 100644 index 00000000..55e71bc0 --- /dev/null +++ b/problems/0213.打家劫舍II.md @@ -0,0 +1,115 @@ +

+ + + + +

+

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

+ +## 213.打家劫舍II + +题目链接:https://leetcode-cn.com/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 + +## 思路 + +这道题目和[198.打家劫舍](https://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw)是差不多的,唯一区别就是成环了。 + +对于一个数组,成环的话主要有如下三种情况: + +* 情况一:考虑不包含首尾元素 + +![213.打家劫舍II](https://img-blog.csdnimg.cn/20210129160748643.jpg) + +* 情况二:考虑包含首元素,不包含尾元素 + +![213.打家劫舍II1](https://img-blog.csdnimg.cn/20210129160821374.jpg) + +* 情况三:考虑包含尾元素,不包含首元素 + +![213.打家劫舍II2](https://img-blog.csdnimg.cn/20210129160842491.jpg) + +**注意我这里用的是"考虑"**,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素! 对于情况三,取nums[1] 和 nums[3]就是最大的。 + +**而情况二 和 情况三 都包含了情况一了,所以只考虑情况二和情况三就可以了**。 + +分析到这里,本题其实比较简单了。 剩下的和[198.打家劫舍](https://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw)就是一样的了。 + +代码如下: + +```C++ +// 注意注释中的情况二情况三,以及把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]; + } +}; +``` + +## 总结 + +成环之后还是难了一些的, 不少题解没有把“考虑房间”和“偷房间”说清楚。 + +这就导致大家会有这样的困惑:情况三怎么就包含了情况一了呢? 本文图中最后一间房不能偷啊,偷了一定不是最优结果。 + +所以我在本文重点强调了情况一二三是“考虑”的范围,而具体房间偷与不偷交给递推公式去抉择。 + +这样大家就不难理解情况二和情况三包含了情况一了。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0216.组合总和III.md b/problems/0216.组合总和III.md index cdcb3aac..11a8eb8f 100644 --- a/problems/0216.组合总和III.md +++ b/problems/0216.组合总和III.md @@ -1,30 +1,40 @@ +

+ + + + +

+

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

+ + + > 别看本篇选的是组合总和III,而不是组合总和,本题和上一篇[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)相比难度刚刚好! -# 第216题.组合总和III +# 216.组合总和III 链接:https://leetcode-cn.com/problems/combination-sum-iii/ 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数,并且每种组合中不存在重复的数字。 -说明: +说明: * 所有数字都是正整数。 * 解集不能包含重复的组合。  -示例 1: -输入: k = 3, n = 7 -输出: [[1,2,4]] +示例 1: +输入: k = 3, n = 7 +输出: [[1,2,4]] -示例 2: -输入: k = 3, n = 9 -输出: [[1,2,6], [1,3,5], [2,3,4]] +示例 2: +输入: k = 3, n = 9 +输出: [[1,2,6], [1,3,5], [2,3,4]] -# 思路 +## 思路 -本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。 +本题就是在[1,2,3,4,5,6,7,8,9]这个集合中找到和为n的k个数的组合。 -相对于[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),无非就是多了一个限制,本题是要找到和为n的k个数的组合,而整个集合已经是固定的了[1,...,9]。 +相对于[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),无非就是多了一个限制,本题是要找到和为n的k个数的组合,而整个集合已经是固定的了[1,...,9]。 想到这一点了,做过[77. 组合](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)之后,本题是简单一些了。 @@ -41,24 +51,24 @@ ## 回溯三部曲 -* **确定递归函数参数** +* **确定递归函数参数** 和[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)一样,依然需要一维数组path来存放符合条件的结果,二维数组result来存放结果集。 -这里我依然定义path 和 result为全局变量。 +这里我依然定义path 和 result为全局变量。 至于为什么取名为path?从上面树形结构中,可以看出,结果其实就是一条根节点到叶子节点的路径。 ``` -vector> result; // 存放结果集 +vector> result; // 存放结果集 vector path; // 符合条件的结果 ``` 接下来还需要如下参数: -* targetSum(int)目标和,也就是题目中的n。 -* k(int)就是题目中要求k个数的集合。 -* sum(int)为已经收集的元素的总和,也就是path里元素的总和。 +* targetSum(int)目标和,也就是题目中的n。 +* k(int)就是题目中要求k个数的集合。 +* sum(int)为已经收集的元素的总和,也就是path里元素的总和。 * startIndex(int)为下一层for循环搜索的起始位置。 所以代码如下: @@ -66,17 +76,17 @@ vector path; // 符合条件的结果 ``` vector> result; vector path; -void backtracking(int targetSum, int k, int sum, int startIndex) +void backtracking(int targetSum, int k, int sum, int startIndex) ``` 其实这里sum这个参数也可以省略,每次targetSum减去选取的元素数值,然后判断如果targetSum为0了,说明收集到符合条件的结果了,我这里为了直观便于理解,还是加一个sum参数。 还要强调一下,回溯法中递归函数参数很难一次性确定下来,一般先写逻辑,需要啥参数了,填什么参数。 -* 确定终止条件 +* 确定终止条件 什么时候终止呢? -在上面已经说了,k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。 +在上面已经说了,k其实就已经限制树的深度,因为就取k个元素,树再往下深了没有意义。 所以如果path.size() 和 k相等了,就终止。 @@ -107,8 +117,8 @@ 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(); // 回溯 + sum -= i; // 回溯 + path.pop_back(); // 回溯 } ``` @@ -119,11 +129,11 @@ for (int i = startIndex; i <= 9; i++) { ``` class Solution { private: - vector> result; // 存放结果集 + vector> result; // 存放结果集 vector path; // 符合条件的结果 - // targetSum:目标和,也就是题目中的n。 - // k:题目中要求k个数的集合。 - // sum:已经收集的元素的总和,也就是path里元素的总和。 + // targetSum:目标和,也就是题目中的n。 + // k:题目中要求k个数的集合。 + // sum:已经收集的元素的总和,也就是path里元素的总和。 // startIndex:下一层for循环搜索的起始位置。 void backtracking(int targetSum, int k, int sum, int startIndex) { if (path.size() == k) { @@ -149,7 +159,7 @@ public: }; ``` -## 剪枝 +## 剪枝 这道题目,剪枝操作其实是很容易想到了,想必大家看上面的树形图的时候已经想到了。 @@ -202,7 +212,7 @@ public: }; ``` -# 总结 +# 总结 开篇就介绍了本题与[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)的区别,相对来说加了元素总和的限制,如果做完[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)再做本题在合适不过。 @@ -210,9 +220,25 @@ public: 相信做完本题,大家对组合问题应该有初步了解了。 -**就酱,如果感觉对你有帮助,就帮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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0219.存在重复元素II.md b/problems/0219.存在重复元素II.md deleted file mode 100644 index 4835a7f0..00000000 --- a/problems/0219.存在重复元素II.md +++ /dev/null @@ -1,51 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/contains-duplicate-ii/ - -## 思路 - -使用哈希策略,map数据结构来记录数组元素和对应的元素所在下表,看代码,已经详细注释。 - -## C++代码 - -``` -class Solution { -public: - bool containsNearbyDuplicate(vector& nums, int k) { - unordered_map map; // key: 数组元素, value:元素所在下表 - for (int i = 0; i < nums.size(); i++) { - // 找到了在索引i之前就出现过nums[i]这个元素 - if (map.find(nums[i]) != map.end()) { - int distance = i - map[nums[i]]; - if (distance <= k) { - return true; - } - map[nums[i]] = i; // 更新元素nums[i]所在的最新位置i - } else { // 如果map里面没有,就把插入一条数据<元素,元素所在的下表> - map[nums[i]] = i; - } - } - return false; - } -}; -``` - -代码精简之后 - -``` -class Solution { -public: - bool containsNearbyDuplicate(vector& nums, int k) { - unordered_map map; - for (int i = 0; i < nums.size(); i++) { - if (map.find(nums[i]) != map.end() && i - map[nums[i]] <= k) return true; - map[nums[i]] = i; - } - return false; - } -}; -``` - - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0220.存在重复元素III.md b/problems/0220.存在重复元素III.md deleted file mode 100644 index 42fcb2f4..00000000 --- a/problems/0220.存在重复元素III.md +++ /dev/null @@ -1,29 +0,0 @@ -## 题目地址 - -## 思路 - - -## C++代码 - - -``` -class Solution { -public: - bool containsNearbyAlmostDuplicate(vector& nums, int k, int t) { - multiset s; - if(nums.empty() || k==0) return false; - for(int i=0;ik){ - s.erase(nums[i-k-1]); - } - auto index = s.lower_bound(nums[i]-t); - if(index!=s.end() && abs(*index-nums[i])<=t) return true; - s.insert(nums[i]); - - } - return false; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0222.完全二叉树的节点个数.md b/problems/0222.完全二叉树的节点个数.md index d3be52c9..367fa717 100644 --- a/problems/0222.完全二叉树的节点个数.md +++ b/problems/0222.完全二叉树的节点个数.md @@ -1,15 +1,39 @@ +

+ + + + +

+

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

-# 222.完全二叉树的节点个数 + +## 222.完全二叉树的节点个数 题目地址:https://leetcode-cn.com/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 +* 题目数据保证输入的树是 完全二叉树 + + +## 思路 本篇给出按照普通二叉树的求法以及利用完全二叉树性质的求法。 @@ -21,7 +45,7 @@ 递归遍历的顺序依然是后序(左右中)。 -### 递归 +### 递归 如果对求二叉树深度还不熟悉的话,看这篇:[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)。 @@ -34,7 +58,7 @@ int getNodesNum(TreeNode* cur) { 2. 确定终止条件:如果为空节点的话,就返回0,表示节点数为0。 -代码如下: +代码如下: ``` if (cur == NULL) return 0; @@ -53,7 +77,7 @@ return treeNum; 所以整体C++代码如下: -``` +```C++ // 版本一 class Solution { private: @@ -72,7 +96,7 @@ public: ``` 代码精简之后C++代码如下: -``` +```C++ // 版本二 class Solution { public: @@ -83,19 +107,19 @@ public: }; ``` -时间复杂度:O(n) +时间复杂度:O(n) 空间复杂度:O(logn),算上了递归系统栈占用的空间 **网上基本都是这个精简的代码版本,其实不建议大家照着这个来写,代码确实精简,但隐藏了一些内容,连遍历的顺序都看不出来,所以初学者建议学习版本一的代码,稳稳的打基础**。 -### 迭代法 +### 迭代法 如果对求二叉树层序遍历还不熟悉的话,看这篇:[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)。 那么只要模板少做改动,加一个变量result,统计节点数量就可以了 -``` +```C++ class Solution { public: int countNodes(TreeNode* root) { @@ -116,7 +140,7 @@ public: } }; ``` -时间复杂度:O(n) +时间复杂度:O(n) 空间复杂度:O(n) ## 完全二叉树 @@ -163,10 +187,25 @@ public: }; ``` -时间复杂度:O(logn * logn) -空间复杂度:O(logn) +时间复杂度:O(logn * logn) +空间复杂度:O(logn) -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** +## 其他语言版本 -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0225.用队列实现栈.md b/problems/0225.用队列实现栈.md index 00aae08f..49b5c62b 100644 --- a/problems/0225.用队列实现栈.md +++ b/problems/0225.用队列实现栈.md @@ -1,11 +1,19 @@ -## 题目地址 +

+ + + + +

+

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

+ -https://leetcode-cn.com/problems/implement-stack-using-queues/ > 用队列实现栈还是有点别扭。 # 225. 用队列实现栈 +https://leetcode-cn.com/problems/implement-stack-using-queues/ + 使用队列实现栈的下列操作: * push(x) -- 元素 x 入栈 @@ -20,7 +28,7 @@ https://leetcode-cn.com/problems/implement-stack-using-queues/ * 你可以假设所有操作都是有效的(例如, 对一个空的栈不会调用 pop 或者 top 操作)。 -# 思路 +# 思路 (这里要强调是单向队列) @@ -36,26 +44,25 @@ https://leetcode-cn.com/problems/implement-stack-using-queues/ 但是依然还是要用两个队列来模拟栈,只不过没有输入和输出的关系,而是另一个队列完全用又来备份的! -如下面动画所示,**用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用**,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。 +如下面动画所示,**用两个队列que1和que2实现队列的功能,que2其实完全就是一个备份的作用**,把que1最后面的元素以外的元素都备份到que2,然后弹出最后面的元素,再把其他元素从que2导回que1。 -模拟的队列执行语句如下: -queue.push(1); -queue.push(2); -queue.pop(); // 注意弹出的操作 -queue.push(3); -queue.push(4); +模拟的队列执行语句如下: +queue.push(1); +queue.push(2); +queue.pop(); // 注意弹出的操作 +queue.push(3); +queue.push(4); queue.pop(); // 注意弹出的操作 -queue.pop(); -queue.pop(); -queue.empty(); +queue.pop(); +queue.pop(); +queue.empty(); - +![225.用队列实现栈](https://code-thinking.cdn.bcebos.com/gifs/225.%E7%94%A8%E9%98%9F%E5%88%97%E5%AE%9E%E7%8E%B0%E6%A0%88.gif) 详细如代码注释所示: -# C++代码 -``` +```C++ class MyStack { public: queue que1; @@ -104,13 +111,11 @@ public: 其实这道题目就是用一个队里就够了。 -**一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。** +**一个队列在模拟栈弹出元素的时候只要将队列头部的元素(除了最后一个元素外) 重新添加到队列尾部,此时在去弹出元素就是栈的顺序了。** -代码如下: +C++优化代码 -# C++优化代码 - -``` +```C++ class MyStack { public: queue que; @@ -146,5 +151,80 @@ public: } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +## 其他语言版本 + + +Java: + + +Python: + +```python +from collections import deque +class MyStack: + def __init__(self): + """ + Initialize your data structure here. + """ + #使用两个队列来实现 + self.que1 = deque() + self.que2 = deque() + + def push(self, x: int) -> None: + """ + Push element x onto stack. + """ + self.que1.append(x) + + def pop(self) -> int: + """ + Removes the element on top of the stack and returns that element. + """ + size = len(self.que1) + size -= 1#这里先减一是为了保证最后面的元素 + while size > 0: + size -= 1 + self.que2.append(self.que1.popleft()) + + + result = self.que1.popleft() + self.que1, self.que2= self.que2, self.que1#将que2和que1交换 que1经过之前的操作应该是空了 + #一定注意不能直接使用que1 = que2 这样que2的改变会影响que1 可以用浅拷贝 + return result + + def top(self) -> int: + """ + Get the top element. + """ + return self.que1[-1] + + def empty(self) -> bool: + """ + Returns whether the stack is empty. + """ + #print(self.que1) + if len(self.que1) == 0: + return True + else: + return False + + +# Your MyStack object will be instantiated and called as such: +# obj = MyStack() +# obj.push(x) +# param_2 = obj.pop() +# param_3 = obj.top() +# param_4 = obj.empty() +``` + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0226.翻转二叉树.md b/problems/0226.翻转二叉树.md index 04780c6f..afc1f144 100644 --- a/problems/0226.翻转二叉树.md +++ b/problems/0226.翻转二叉树.md @@ -1,15 +1,23 @@ -## 题目地址 -https://leetcode-cn.com/problems/invert-binary-tree/ +

+ + + + +

+

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

-# 226.翻转二叉树 + +## 226.翻转二叉树 + +题目地址:https://leetcode-cn.com/problems/invert-binary-tree/ 翻转一棵二叉树。 - +![226.翻转二叉树](https://img-blog.csdnimg.cn/20210203192644329.png) -这道题目背后有一个让程序员心酸的故事,听说 Homebrew的作者Max Howell,就是因为没在白板上写出翻转二叉树,最后被Google拒绝了。(真假不做判断,权当一个乐子哈) +这道题目背后有一个让程序员心酸的故事,听说 Homebrew的作者Max Howell,就是因为没在白板上写出翻转二叉树,最后被Google拒绝了。(真假不做判断,权当一个乐子哈) -# 题外话 +## 题外话 这道题目是非常经典的题目,也是比较简单的题目(至少一看就会)。 @@ -17,15 +25,15 @@ https://leetcode-cn.com/problems/invert-binary-tree/ 如果做过这道题的同学也建议认真看完,相信一定有所收获! -# 思路 +## 思路 我们之前介绍的都是各种方式遍历二叉树,这次要翻转了,感觉还是有点懵逼。 -这得怎么翻转呢? +这得怎么翻转呢? 如果要从整个树来看,翻转还真的挺复杂,整个树以中间分割线进行翻转,如图: - +![226.翻转二叉树1](https://img-blog.csdnimg.cn/20210203192724351.png) 可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。 @@ -45,7 +53,7 @@ https://leetcode-cn.com/problems/invert-binary-tree/ 我们下文以前序遍历为例,通过动画来看一下翻转的过程: - +![翻转二叉树](https://tva1.sinaimg.cn/large/008eGmZEly1gnakm26jtog30e409s4qp.gif) 我们来看一下递归三部曲: @@ -59,7 +67,7 @@ https://leetcode-cn.com/problems/invert-binary-tree/ TreeNode* invertTree(TreeNode* root) ``` -2. 确定终止条件 +2. 确定终止条件 当前节点为空的时候,就返回 @@ -79,7 +87,7 @@ invertTree(root->right); 基于这递归三步法,代码基本写完,C++代码如下: -``` +```C++ class Solution { public: TreeNode* invertTree(TreeNode* root) { @@ -100,7 +108,7 @@ public: C++代码迭代法(前序遍历) -``` +```C++ class Solution { public: TreeNode* invertTree(TreeNode* root) { @@ -110,7 +118,7 @@ public: while(!st.empty()) { TreeNode* node = st.top(); // 中 st.pop(); - swap(node->left, node->right); + swap(node->left, node->right); if(node->right) st.push(node->right); // 右 if(node->left) st.push(node->left); // 左 } @@ -125,7 +133,7 @@ public: C++代码如下迭代法(前序遍历) -``` +```C++ class Solution { public: TreeNode* invertTree(TreeNode* root) { @@ -157,7 +165,7 @@ public: 也就是层序遍历,层数遍历也是可以翻转这棵树的,因为层序遍历也可以把每个节点的左右孩子都翻转一遍,代码如下: -``` +```C++ class Solution { public: TreeNode* invertTree(TreeNode* root) { @@ -179,7 +187,7 @@ public: ``` 如果对以上代码不理解,或者不清楚二叉树的层序遍历,可以看这篇[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog) -# 总结 +## 总结 针对二叉树的问题,解题之前一定要想清楚究竟是前中后序遍历,还是层序遍历。 @@ -191,4 +199,22 @@ public: 大家一定也有自己的解法,但一定要成方法论,这样才能通用,才能举一反三! -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0232.用栈实现队列.md b/problems/0232.用栈实现队列.md index 754443dd..9907b476 100644 --- a/problems/0232.用栈实现队列.md +++ b/problems/0232.用栈实现队列.md @@ -1,16 +1,24 @@ -## 题目地址 +

+ + + + +

+

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

+ + +> 工作上一定没人这么搞,但是考察对栈、队列理解程度的好题 + +# 232.用栈实现队列 + https://leetcode-cn.com/problems/implement-queue-using-stacks/ -> 工作上一定没人这么搞,但是考察对栈、队列理解程度的好题 - -# 232. 用栈实现队列 - 使用栈实现队列的下列操作: -push(x) -- 将一个元素放入队列的尾部。 -pop() -- 从队列首部移除元素。 -peek() -- 返回队列首部的元素。 -empty() -- 返回队列是否为空。 +push(x) -- 将一个元素放入队列的尾部。 +pop() -- 从队列首部移除元素。 +peek() -- 返回队列首部的元素。 +empty() -- 返回队列是否为空。   示例: @@ -30,7 +38,7 @@ queue.empty(); // 返回 false * 你所使用的语言也许不支持栈。你可以使用 list 或者 deque(双端队列)来模拟一个栈,只要是标准的栈操作即可。 * 假设所有操作都是有效的 (例如,一个空的队列不会调用 pop 或者 peek 操作)。 -# 思路 +## 思路 这是一道模拟题,不涉及到具体算法,考察的就是对栈和队列的掌握程度。 @@ -38,18 +46,18 @@ queue.empty(); // 返回 false 下面动画模拟以下队列的执行过程如下: -执行语句: -queue.push(1); -queue.push(2); -queue.pop(); **注意此时的输出栈的操作** -queue.push(3); -queue.push(4); -queue.pop(); -queue.pop();**注意此时的输出栈的操作** -queue.pop(); -queue.empty(); +执行语句: +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://code-thinking.cdn.bcebos.com/gifs/232.%E7%94%A8%E6%A0%88%E5%AE%9E%E7%8E%B0%E9%98%9F%E5%88%97%E7%89%88%E6%9C%AC2.gif) 在push数据的时候,只要数据放进输入栈就好,**但在pop的时候,操作就复杂一些,输出栈如果为空,就把进栈数据全部导入进来(注意是全部导入)**,再从出栈弹出数据,如果输出栈不为空,则直接从出栈弹出数据就可以了。 @@ -57,11 +65,9 @@ queue.empty(); 在代码实现的时候,会发现pop() 和 peek()两个函数功能类似,代码实现上也是类似的,可以思考一下如何把代码抽象一下。 -代码如下: +C++代码如下: -# C++代码 - -``` +```C++ class MyQueue { public: stack stIn; @@ -105,7 +111,7 @@ public: ``` -# 拓展 +## 拓展 可以看出peek()的实现,直接复用了pop()。 @@ -118,5 +124,25 @@ public: 同事们就会逐渐认可你的工作态度和工作能力,自己的口碑都是这么一点一点积累起来的!在同事圈里口碑起来了之后,你就发现自己走上了一个正循环,以后的升职加薪才少不了你!哈哈哈 -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0234.回文链表.md b/problems/0234.回文链表.md deleted file mode 100644 index d37044d7..00000000 --- a/problems/0234.回文链表.md +++ /dev/null @@ -1,122 +0,0 @@ - -## 题目链接 -https://leetcode-cn.com/problems/palindrome-linked-list/ - -## 思路 - -### 数组模拟 - -最直接的想法,就是把链表装成数组,然后再判断是否回文。 - -代码也比较简单。如下: - -``` -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每次添加节点重新开辟空间 - -``` -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的节点数值 - -如图所示: - - - -代码如下: - -``` -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; - } -}; -``` - -栈 - diff --git a/problems/0235.二叉搜索树的最近公共祖先.md b/problems/0235.二叉搜索树的最近公共祖先.md index 3104b297..cb9de8b0 100644 --- a/problems/0235.二叉搜索树的最近公共祖先.md +++ b/problems/0235.二叉搜索树的最近公共祖先.md @@ -1,11 +1,15 @@ -## 链接 -https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ +

+ + + + +

+

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

-> 二叉搜索树的最近公共祖先问题如约而至 -# 235. 二叉搜索树的最近公共祖先 +## 235. 二叉搜索树的最近公共祖先 -链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ +链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ 给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。 @@ -32,7 +36,7 @@ https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ * 所有节点的值都是唯一的。 * p、q 为不同节点且均存在于给定的二叉搜索树中。 -## 思路 +## 思路 做过[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)题目的同学应该知道,利用回溯从底向上搜索,遇到一个节点的左子树里有p,右子树里有q,那么当前节点就是最近公共祖先。 @@ -42,15 +46,15 @@ https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ 其实只要从上到下遍历的时候,cur节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。 -理解这一点,本题就很好解了。 +理解这一点,本题就很好解了。 -和[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)不同,普通二叉树求最近公共祖先需要使用回溯,从底向上来查找,二叉搜索树就不用了,因为搜索树有序(相当于自带方向),那么只要从上向下遍历就可以了。 +和[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)不同,普通二叉树求最近公共祖先需要使用回溯,从底向上来查找,二叉搜索树就不用了,因为搜索树有序(相当于自带方向),那么只要从上向下遍历就可以了。 那么我们可以采用前序遍历(其实这里没有中节点的处理逻辑,遍历顺序无所谓了)。 如图所示:p为节点3,q为节点5 - +![235.二叉搜索树的最近公共祖先](https://img-blog.csdnimg.cn/20210204150858927.png) 可以看出直接按照指定的方向,就可以找到节点4,为最近公共祖先,而且不需要遍历整棵树,找到结果直接返回! @@ -66,10 +70,10 @@ https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-search-tree/ 代码如下: ``` -TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) +TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) ``` -* 确定终止条件 +* 确定终止条件 遇到空返回就可以了,代码如下: @@ -79,17 +83,17 @@ if (cur == NULL) return cur; 其实都不需要这个终止条件,因为题目中说了p、q 为不同节点且均存在于给定的二叉搜索树中。也就是说一定会找到公共祖先的,所以并不存在遇到空的情况。 -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 在遍历二叉搜索树的时候就是寻找区间[p->val, q->val](注意这里是左闭又闭) 那么如果 cur->val 大于 p->val,同时 cur->val 大于q->val,那么就应该向左遍历(说明目标区间在左子树上)。 -**需要注意的是此时不知道p和q谁大,所以两个都要判断** +**需要注意的是此时不知道p和q谁大,所以两个都要判断** 代码如下: -``` +```C++ if (cur->val > p->val && cur->val > q->val) { TreeNode* left = traversal(cur->left, p, q); if (left != NULL) { @@ -98,10 +102,10 @@ if (cur->val > p->val && cur->val > q->val) { } ``` -**细心的同学会发现,在这里调用递归函数的地方,把递归函数的返回值left,直接return**。 +**细心的同学会发现,在这里调用递归函数的地方,把递归函数的返回值left,直接return**。 -在[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。 +在[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)中,如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树。 搜索一条边的写法: @@ -143,7 +147,7 @@ return cur; 那么整体递归代码如下: -``` +```C++ class Solution { private: TreeNode* traversal(TreeNode* cur, TreeNode* p, TreeNode* q) { @@ -173,7 +177,7 @@ public: 精简后代码如下: -``` +```C++ class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { @@ -194,7 +198,7 @@ public: 迭代代码如下: -``` +```C++ class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { @@ -212,7 +216,7 @@ public: 灵魂拷问:是不是又被简单的迭代法感动到痛哭流涕? -# 总结 +## 总结 对于二叉搜索树的最近祖先问题,其实要比[普通二叉树公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)简单的多。 @@ -220,4 +224,23 @@ public: 最后给出了对应的迭代法,二叉搜索树的迭代法甚至比递归更容易理解,也是因为其有序性(自带方向性),按照目标区间找就行了。 -**就酱,学到了,就转发给身边需要学习的同学吧!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0236.二叉树的最近公共祖先.md b/problems/0236.二叉树的最近公共祖先.md index 5b4ac984..17096d48 100644 --- a/problems/0236.二叉树的最近公共祖先.md +++ b/problems/0236.二叉树的最近公共祖先.md @@ -1,9 +1,17 @@ -## 链接 -https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/ +

+ + + + +

+

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

+ > 本来是打算将二叉树和二叉搜索树的公共祖先问题一起讲,后来发现篇幅过长了,只能先说一说二叉树的公共祖先问题。 -# 236. 二叉树的最近公共祖先 +## 236. 二叉树的最近公共祖先 + +题目链接:https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/ 给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。 @@ -13,21 +21,21 @@ https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/ ![236. 二叉树的最近公共祖先](https://img-blog.csdnimg.cn/20201016173414722.png) -示例 1: -输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 -输出: 3 -解释: 节点 5 和节点 1 的最近公共祖先是节点 3。 +示例 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。因为根据定义最近公共祖先节点可以为节点本身。 +示例 2: +输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 +输出: 5 +解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。 说明: * 所有节点的值都是唯一的。 * p、q 为不同节点且均存在于给定的二叉树中。 -## 思路 +## 思路 遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。 @@ -39,25 +47,25 @@ https://leetcode-cn.com/problems/lowest-common-ancestor-of-a-binary-tree/ 接下来就看如何判断一个节点是节点q和节点p的公共公共祖先呢。 -**如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。** +**如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。** 使用后序遍历,回溯的过程,就是从低向上遍历节点,一旦发现如何这个条件的节点,就是最近公共节点了。 递归三部曲: -* 确定递归函数返回值以及参数 +* 确定递归函数返回值以及参数 需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了。 -但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。 +但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p。 代码如下: ``` -TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) +TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) ``` -* 确定终止条件 +* 确定终止条件 如果找到了 节点p或者q,或者遇到空节点,就返回。 @@ -67,13 +75,13 @@ TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) if (root == q || root == p || root == NULL) return root; ``` -* 确定单层递归逻辑 +* 确定单层递归逻辑 值得注意的是 本题函数有返回值,是因为回溯的过程需要递归函数的返回值做判断,但本题我们依然要遍历树的所有节点。 我们在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)中说了 递归函数有返回值就是要遍历某一条边,但有返回值也要看如何处理返回值! -如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢? +如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢? 搜索一条边的写法: @@ -95,11 +103,11 @@ left与right的逻辑处理; **在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)**。 -那么为什么要遍历整颗树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。 +那么为什么要遍历整颗树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。 如图: - +![236.二叉树的最近公共祖先](https://img-blog.csdnimg.cn/2021020415105872.png) 就像图中一样直接返回7,多美滋滋。 @@ -128,11 +136,11 @@ TreeNode* right = lowestCommonAncestor(root->right, p, q); **如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然**。 -这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢? +这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢? 如图: - +![236.二叉树的最近公共祖先1](https://img-blog.csdnimg.cn/20210204151125844.png) 图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去! @@ -142,7 +150,7 @@ TreeNode* right = lowestCommonAncestor(root->right, p, q); 代码如下: -``` +```C++ if (left == NULL && right != NULL) return right; else if (left != NULL && right == NULL) return left; else { // (left == NULL && right == NULL) @@ -153,13 +161,13 @@ else { // (left == NULL && right == NULL) 那么寻找最小公共祖先,完整流程图如下: - +![236.二叉树的最近公共祖先2](https://img-blog.csdnimg.cn/202102041512582.png) **从图中,大家可以看到,我们是如何回溯遍历整颗二叉树,将结果返回给头结点的!** 整体代码如下: -``` +```C++ class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { @@ -180,7 +188,7 @@ public: 稍加精简,代码如下: -``` +```C++ class Solution { public: TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) { @@ -194,13 +202,13 @@ public: }; ``` -# 总结 +## 总结 这道题目刷过的同学未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的。 **那么我给大家归纳如下三点**: -1. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式。 +1. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式。 2. 在回溯的过程中,必然要遍历整颗二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断。 @@ -210,4 +218,23 @@ public: 本题没有给出迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了。 -**就酱,转发给身边需要学习的同学吧!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0237.删除链表中的节点.md b/problems/0237.删除链表中的节点.md deleted file mode 100644 index b2ae2be9..00000000 --- a/problems/0237.删除链表中的节点.md +++ /dev/null @@ -1,23 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/delete-node-in-a-linked-list/ - -## 思路 - -题目中已经说了是非末尾节点,所以不用对末尾节点的情况经行判断 - -**链表中所有节点的值都是唯一的。** 题目题意强调这一点,是为什么,因为 我们删除的node在内存地址 删除的并不是这个。 -## 代码 - -``` -class Solution { -public: - void deleteNode(ListNode* node) { - node->val = node->next->val;// 将前一个值赋给当前node - node->next = node->next->next; // 将当前node的next 指向下下node - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0239.滑动窗口最大值.md b/problems/0239.滑动窗口最大值.md index 1a0b32ab..709fa09e 100644 --- a/problems/0239.滑动窗口最大值.md +++ b/problems/0239.滑动窗口最大值.md @@ -1,9 +1,18 @@ -## 题目地址 -https://leetcode-cn.com/problems/sliding-window-maximum/ +

+ + + + +

+

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

+ + > 要用啥数据结构呢? -# 239. 滑动窗口最大值 +# 239. 滑动窗口最大值 + +https://leetcode-cn.com/problems/sliding-window-maximum/ 给定一个数组 nums,有一个大小为 k 的滑动窗口从数组的最左侧移动到数组的最右侧。你只可以看到在滑动窗口内的 k 个数字。滑动窗口每次只向右移动一位。 @@ -12,15 +21,14 @@ https://leetcode-cn.com/problems/sliding-window-maximum/ 进阶: 你能在线性时间复杂度内解决此题吗? -   - + 提示: -1 <= nums.length <= 10^5 --10^4 <= nums[i] <= 10^4 -1 <= k <= nums.length +1 <= nums.length <= 10^5 +-10^4 <= nums[i] <= 10^4 +1 <= k <= nums.length @@ -34,13 +42,13 @@ https://leetcode-cn.com/problems/sliding-window-maximum/ 有的同学可能会想用一个大顶堆(优先级队列)来存放这个窗口里的k个数字,这样就可以知道最大的最大值是多少了, **但是问题是这个窗口是移动的,而大顶堆每次只能弹出最大值,我们无法移除其他数值,这样就造成大顶堆维护的不是滑动窗口里面的数值了。所以不能用大顶堆。** -此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。 +此时我们需要一个队列,这个队列呢,放进去窗口里的元素,然后随着窗口的移动,队列也一进一出,每次移动之后,队列告诉我们里面的最大值是什么。 这个队列应该长这个样子: ``` -class MyQueue { +class MyQueue { public: void pop(int value) { } @@ -66,7 +74,7 @@ public: 大家此时应该陷入深思..... -**其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。** +**其实队列没有必要维护窗口里的所有元素,只需要维护有可能成为窗口里最大值的元素就可以了,同时保证队里里的元素数值是由大到小的。** 那么这个维护元素单调递减的队列就叫做**单调队列,即单调递减或单调递增的队列。C++中没有直接支持单调队列,需要我们自己来一个单调队列** @@ -76,22 +84,23 @@ public: 动画如下: - +![239.滑动窗口最大值](https://code-thinking.cdn.bcebos.com/gifs/239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC.gif) 对于窗口里的元素{2, 3, 5, 1 ,4},单调队列里只维护{5, 4} 就够了,保持单调队列里单调递减,此时队列出口元素就是窗口里最大元素。 -此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口经行滑动呢? +此时大家应该怀疑单调队列里维护着{5, 4} 怎么配合窗口经行滑动呢? 设计单调队列的时候,pop,和push操作要保持如下规则: 1. pop(value):如果窗口移除的元素value等于单调队列的出口元素,那么队列弹出元素,否则不用任何操作 -2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 +2. push(value):如果push的元素value大于入口元素的数值,那么就将队列入口的元素弹出,直到push元素的数值小于等于队列入口元素的数值为止 保持如上规则,每次窗口移动的时候,只要问que.front()就可以返回当前窗口的最大值。 为了更直观的感受到单调队列的工作过程,以题目示例为例,输入: nums = [1,3,-1,-3,5,3,6,7], 和 k = 3,动画如下: - + +![239.滑动窗口最大值-2](https://code-thinking.cdn.bcebos.com/gifs/239.%E6%BB%91%E5%8A%A8%E7%AA%97%E5%8F%A3%E6%9C%80%E5%A4%A7%E5%80%BC-2.gif) 那么我们用什么数据结构来实现这个单调队列呢? @@ -99,7 +108,7 @@ public: 基于刚刚说过的单调队列pop和push的规则,代码不难实现,如下: -``` +```C++ class MyQueue { //单调队列(从大到小) public: deque que; // 使用deque来实现单调队列 @@ -110,7 +119,7 @@ public: que.pop_front(); } } - // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 + // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 // 这样就保持了队列里的数值是单调从大到小的了。 void push(int value) { while (!que.empty() && value > que.back()) { @@ -129,9 +138,9 @@ public: 这样我们就用deque实现了一个单调队列,接下来解决滑动窗口最大值的问题就很简单了,直接看代码吧。 -# C++代码 +C++代码如下: -``` +```C++ class Solution { private: class MyQueue { //单调队列(从大到小) @@ -144,7 +153,7 @@ private: que.pop_front(); } } - // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 + // 如果push的数值大于入口元素的数值,那么就将队列后端的数值弹出,直到push的数值小于等于队列入口元素的数值为止。 // 这样就保持了队列里的数值是单调从大到小的了。 void push(int value) { while (!que.empty() && value > que.back()) { @@ -186,12 +195,30 @@ public: # 扩展 -大家貌似对单调队列 都有一些疑惑,首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题哈。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本地中的单调队列实现就是固定的写法哈。 +大家貌似对单调队列 都有一些疑惑,首先要明确的是,题解中单调队列里的pop和push接口,仅适用于本题哈。单调队列不是一成不变的,而是不同场景不同写法,总之要保证队列里单调递减或递增的原则,所以叫做单调队列。 不要以为本地中的单调队列实现就是固定的写法哈。 大家貌似对deque也有一些疑惑,C++中deque是stack和queue默认的底层实现容器(这个我们之前已经讲过啦),deque是可以两边扩展的,而且deque里元素并不是严格的连续分布的。 -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0242.有效的字母异位词.md b/problems/0242.有效的字母异位词.md index 92326b02..9c492431 100644 --- a/problems/0242.有效的字母异位词.md +++ b/problems/0242.有效的字母异位词.md @@ -1,10 +1,17 @@ -## 题目地址 +

+ + + + +

+

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

-https://leetcode-cn.com/problems/valid-anagram/ > 数组就是简单的哈希表,但是数组的大小可不是无限开辟的 -# 第242题.有效的字母异位词 +# 242.有效的字母异位词 + +https://leetcode-cn.com/problems/valid-anagram/ 给定两个字符串 s 和 t ,编写一个函数来判断 t 是否是 s 的字母异位词。 @@ -15,7 +22,7 @@ https://leetcode-cn.com/problems/valid-anagram/ # 思路 -先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。 +先看暴力的解法,两层for循环,同时还要记录字符是否重复出现,很明显时间复杂度是 O(n^2)。 暴力的方法这里就不做介绍了,直接看一下有没有更优的方式。 @@ -25,11 +32,11 @@ https://leetcode-cn.com/problems/valid-anagram/ 需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。 -为了方便举例,判断一下字符串s= "aee", t = "eae"。 +为了方便举例,判断一下字符串s= "aee", t = "eae"。 操作动画如下: - +![242.有效的字母异位词](https://tva1.sinaimg.cn/large/008eGmZEly1govxyg83bng30ds09ob29.gif) 定义一个数组叫做record用来上记录字符串s里字符出现的次数。 @@ -47,8 +54,8 @@ https://leetcode-cn.com/problems/valid-anagram/ 看完这篇哈希表总结:[哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg),详细就可以哈希表的各种用法非常清晰了。 -# C++ 代码 -``` +C++ 代码如下: +```C++ class Solution { public: bool isAnagram(string s, string t) { @@ -71,8 +78,25 @@ public: } }; ``` -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[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),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0257.二叉树的所有路径.md b/problems/0257.二叉树的所有路径.md index d63fa41d..a104b27c 100644 --- a/problems/0257.二叉树的所有路径.md +++ b/problems/0257.二叉树的所有路径.md @@ -1,18 +1,26 @@ -## 题目地址 -https://leetcode-cn.com/problems/binary-tree-paths/ +

+ + + + +

+

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

+ > 以为只用了递归,其实还用了回溯 -# 257. 二叉树的所有路径 +## 257. 二叉树的所有路径 + +题目地址:https://leetcode-cn.com/problems/binary-tree-paths/ 给定一个二叉树,返回所有从根节点到叶子节点的路径。 说明: 叶子节点是指没有子节点的节点。 示例: - +![257.二叉树的所有路径1](https://img-blog.csdnimg.cn/2021020415161576.png) -# 思路 +## 思路 这道题目要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。 @@ -20,21 +28,21 @@ https://leetcode-cn.com/problems/binary-tree-paths/ 前序遍历以及回溯的过程如图: - +![257.二叉树的所有路径](https://img-blog.csdnimg.cn/20210204151702443.png) 我们先使用递归的方式,来做前序遍历。**要知道递归和回溯就是一家的,本题也需要回溯。** ## 递归 -1. 递归函数函数参数以及返回值 +1. 递归函数函数参数以及返回值 要传入根节点,记录每一条路径的path,和存放结果集的result,这里递归不需要返回值,代码如下: ``` -void traversal(TreeNode* cur, vector& path, vector& result) +void traversal(TreeNode* cur, vector& path, vector& result) ``` -2. 确定递归终止条件 +2. 确定递归终止条件 再写递归的时候都习惯了这么写: @@ -61,7 +69,7 @@ if (cur->left == NULL && cur->right == NULL) { 这里使用vector 结构path来记录路径,所以要把vector 结构的path转为string格式,在把这个string 放进 result里。 -**那么为什么使用了vector 结构来记录路径呢?** 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。 +**那么为什么使用了vector 结构来记录路径呢?** 因为在下面处理单层递归逻辑的时候,要做回溯,使用vector方便来做回溯。 可能有的同学问了,我看有些人的代码也没有回溯啊。 @@ -86,7 +94,7 @@ if (cur->left == NULL && cur->right == NULL) { // 遇到叶子节点 因为是前序遍历,需要先处理中间节点,中间节点就是我们要记录路径上的节点,先放进path中。 -`path.push_back(cur->val);` +`path.push_back(cur->val);` 然后是递归和回溯的过程,上面说过没有判断cur是否为空,那么在这里递归的时候,如果为空就不进行下一层递归了。 @@ -117,7 +125,7 @@ path.pop_back(); 这个回溯就要很大的问题,我们知道,**回溯和递归是一一对应的,有一个递归,就要有一个回溯**,这么写的话相当于把递归和回溯拆开了, 一个在花括号里,一个在花括号外。 -**所以回溯要和递归永远在一起,世界上最遥远的距离是你在花括号里,而我在花括号外!** +**所以回溯要和递归永远在一起,世界上最遥远的距离是你在花括号里,而我在花括号外!** 那么代码应该这么写: @@ -134,7 +142,7 @@ if (cur->right) { 那么本题整体代码如下: -``` +```C++ class Solution { private: @@ -175,7 +183,7 @@ public: 那么如上代码可以精简成如下代码: -``` +```C++ class Solution { private: @@ -211,11 +219,11 @@ public: -**综合以上,第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现了出来了。** +**综合以上,第二种递归的代码虽然精简但把很多重要的点隐藏在了代码细节里,第一种递归写法虽然代码多一些,但是把每一个逻辑处理都完整的展现了出来了。** -## 迭代法 +## 迭代法 至于非递归的方式,我们可以依然可以使用前序遍历的迭代方式来模拟遍历路径的过程,对该迭代方式不了解的同学,可以看文章[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)和[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)。 @@ -223,7 +231,7 @@ public: C++代码如下: -``` +```C++ class Solution { public: vector binaryTreePaths(TreeNode* root) { @@ -254,7 +262,7 @@ public: ``` 当然,使用java的同学,可以直接定义一个成员变量为object的栈`Stack stack = new Stack<>();`,这样就不用定义两个栈了,都放到一个栈里就可以了。 -# 总结 +## 总结 **本文我们开始初步涉及到了回溯,很多同学过了这道题目,可能都不知道自己其实使用了回溯,回溯和递归都是相伴相生的。** @@ -268,4 +276,24 @@ public: -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0279.完全平方数.md b/problems/0279.完全平方数.md index 9903df5c..39260926 100644 --- a/problems/0279.完全平方数.md +++ b/problems/0279.完全平方数.md @@ -1,20 +1,141 @@ -没有问你组合方式,而是问你最小个数 +

+ + + + +

+

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

+# 动态规划:一样的套路,再求一次完全平方数 + +## 279.完全平方数 + +题目地址:https://leetcode-cn.com/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 + +## 思路 + +可能刚看这种题感觉没啥思路,又平方和的,又最小数的。 + +**我来把题目翻译一下:完全平方数就是物品(可以无限件使用),凑个正整数n就是背包,问凑满这个背包最少有多少物品?** + +感受出来了没,这么浓厚的完全背包氛围,而且和昨天的题目[动态规划:322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)就是一样一样的! + +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i]:和为i的完全平方数的最少数量为dp[i]** + +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[i]一定要初始为最大值,这样dp[j]在递推的时候才不会被初始值覆盖**。 + +4. 确定遍历顺序 + +我们知道这是完全背包, + +如果求组合数就是外层for循环遍历物品,内层for遍历背包。 + +如果求排列数就是外层for遍历背包,内层for循环遍历物品。 + +在[动态规划:322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)中我们就深入探讨了这个问题,本题也是一样的,是求最小数! + +**所以本题外层for遍历背包,里层for遍历物品,还是外层for遍历物品,内层for遍历背包,都是可以的!** + +我这里先给出外层遍历背包,里层遍历物品的代码: + +```C++ +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://img-blog.csdnimg.cn/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++代码 + +以上动规五部曲分析完毕C++代码如下: + +```C++ +// 版本一 class Solution { public: int numSquares(int n) { - vector sum; - for (int i = 1; i * i <= n; i++) { - sum.push_back(i * i); - } vector dp(n + 1, INT_MAX); dp[0] = 0; - for (int i = 0; i < sum.size(); i++) { - for (int j = 1; j <= n; j++) { - if (j - sum[i] >= 0 && dp[j - sum[i]] != INT_MAX) { - dp[j] = min(dp[j - sum[i]] + 1, dp[j]); + 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]; + } +}; +``` + +同样我在给出先遍历物品,在遍历背包的代码,一样的可以AC的。 + +```C++ +// 版本二 +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]); } } } @@ -23,20 +144,33 @@ public: }; ``` +## 总结 -// 排列的逻辑 -``` -class Solution { -public: - int numSquares(int n) { - vector dp(n + 1, 0); - for (int i = 0; i <= n; i++) { - dp[i] = i; // 最多也就是i都是1组成的情况 - for (int j = 1; j * j <= i; j++) { - dp[i] = min(dp[i - j * j] + 1, dp[i]); - } - } - return dp[n]; - } -}; -``` +如果大家认真做了昨天的题目[动态规划:322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ),今天这道就非常简单了,一样的套路一样的味道。 + +但如果没有按照「代码随想录」的题目顺序来做的话,做动态规划或者做背包问题,上来就做这道题,那还是挺难的! + +经过前面的训练这道题已经是简单题了,哈哈哈 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0283.移动零.md b/problems/0283.移动零.md deleted file mode 100644 index 638f2cdf..00000000 --- a/problems/0283.移动零.md +++ /dev/null @@ -1,40 +0,0 @@ - -# 思路 - -做这道题目之前,大家可以做一做[27.移除元素](https://leetcode-cn.com/problems/remove-element/),我在讲解数组系列的时候针对移除元素写了这篇题解:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - -这道题目,使用暴力的解法,可以两层for循环,模拟数组删除元素(也就是向前覆盖)的过程。 - -好了,我们说一说双指针法,大家如果对双指针还不熟悉,可以看我的这篇总结[双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA)。 - -双指针法在数组移除元素中,可以达到O(n)的时间复杂度,在[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA)里已经详细讲解了,那么本题和移除元素其实是一个套路。 - -**相当于对整个数组移除元素0,然后slowIndex之后都是移除元素0的冗余元素,把这些元素都赋值为0就可以了**。 - -如动画所示: - - - -C++代码如下: - -``` -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; - } - } -}; -``` -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[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/0300.最长上升子序列.md b/problems/0300.最长上升子序列.md index 5582c8ac..2bc5cf9c 100644 --- a/problems/0300.最长上升子序列.md +++ b/problems/0300.最长上升子序列.md @@ -1,22 +1,86 @@ +

+ + + + +

+

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

+ +## 300.最长递增子序列 + +题目链接:https://leetcode-cn.com/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 + ## 思路 -* dp[i]的定义 -dp[i]表示i之前包括i的最长上升子序列。 +最长上升子序列是动规的经典题目,这里dp[i]是可以根据dp[j] (j < i)推导出来的,那么依然用动规五部曲来分析详细一波: +1. dp[i]的定义 -* dp[i]的初始化 +**dp[i]表示i之前包括i的最长上升子序列**。 -每一个i,对应的dp[i](即最长上升子序列)起始大小至少是1. - - -* 状态转移方程 +2. 状态转移方程 位置i的最长升序子序列等于j从0到i-1各个位置的最长升序子序列 + 1 的最大值。 -if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 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,遍历i的循环里外层,遍历j则在内层,代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210110170945618.jpg) + + +如果代码写出来,但一直AC不了,那么就把dp数组打印出来,看看对不对! + +以上五部分析完毕,C++代码如下: + +```C++ class Solution { public: int lengthOfLIS(vector& nums) { @@ -28,10 +92,36 @@ public: if (nums[i] > nums[j]) dp[i] = max(dp[i], dp[j] + 1); } if (dp[i] > result) result = dp[i]; // 取长的子序列 - //for (int j = 0 ; j < nums.size(); j++) cout << dp[j] << " "; - //cout << endl; } return result; } }; ``` + +杨老师的这个专栏很不错,他本身也是Oracle 首席工程师,对Java有极其深刻的理解,讲的内容很硬核,适合使用Java语言的录友们用来进阶!作为面试突击手册非常合适, 所以推荐给大家!现在下单输入口令:javahexin,可以省40元那[机智] + +## 总结 + +本题最关键的是要想到dp[i]由哪些状态可以推出来,并取最大值,那么很自然就能想到递推公式:dp[i] = max(dp[i], dp[j] + 1); + +子序列问题是动态规划的一个重要系列,本题算是入门题目,好戏刚刚开始! + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0309.最佳买卖股票时机含冷冻期.md b/problems/0309.最佳买卖股票时机含冷冻期.md new file mode 100644 index 00000000..15bba0b7 --- /dev/null +++ b/problems/0309.最佳买卖股票时机含冷冻期.md @@ -0,0 +1,178 @@ +

+ + + + +

+

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

+ +## 309.最佳买卖股票时机含冷冻期 + +题目链接:https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-with-cooldown/ + +给定一个整数数组,其中第 i 个元素代表了第 i 天的股票价格 。​ + +设计一个算法计算出最大利润。在满足以下约束条件下,你可以尽可能地完成更多的交易(多次买卖一支股票): + +* 你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。 +* 卖出股票后,你无法在第二天买入股票 (即冷冻期为 1 天)。 + +示例: +输入: [1,2,3,0,2] +输出: 3 +解释: 对应的交易状态为: [买入, 卖出, 冷冻期, 买入, 卖出] + + +## 思路 + +> 之前我们在[动态规划:最佳买卖股票时机含冷冻期](https://mp.weixin.qq.com/s/IgC0iWWCDpYL9ZbTHGHgfw)讲过一次这道题目,讲解的过程感觉不是很严谨,和录友们也聊过这个问题,本着对大家负责的态度,有问题的地方我都会及时纠正,所以重新发文讲解一下。 + +相对于[动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w),本题加上了一个冷冻期 + + +在[动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w) 中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。 + +动规五部曲,分析如下: + +1. 确定dp数组以及下标的含义 + +dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]。 + +**其实本题很多同学搞的比较懵,是因为出现冷冻期之后,状态其实是比较复杂度**,例如今天买入股票、今天卖出股票、今天是冷冻期,都是不能操作股票的。 +具体可以区分出如下四个状态: + +* 状态一:买入股票状态(今天买入股票,或者是之前就买入了股票然后没有操作) +* 卖出股票状态,这里就有两种卖出股票状态 + * 状态二:两天前就卖出了股票,度过了冷冻期,一直没操作,今天保持卖出股票状态 + * 状态三:今天卖出了股票 +* 状态四:今天为冷冻期状态,但冷冻期状态不可持续,只有一天! + +j的状态为: + +* 0:状态一 +* 1:状态二 +* 2:状态三 +* 3:状态四 + +很多题解为什么讲的比较模糊,是因为把这四个状态合并成三个状态了,其实就是把状态二和状态四合并在一起了。 + +从代码上来看确实可以合并,但从逻辑上分析合并之后就很难理解了,所以我下面的讲解是按照这四个状态来的,把每一个状态分析清楚。 + +**注意这里的每一个状态,例如状态一,是买入股票状态并不是说今天已经就买入股票,而是说保存买入股票的状态即:可能是前几天买入的,之后一直没操作,所以保持买入股票的状态**。 + +2. 确定递推公式 + + +达到买入股票状态(状态一)即: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]; + +综上分析,递推代码如下: + +```C++ +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],买入股票所省现金为负数。 + +保持卖出股票状态(状态二),第0天没有卖出dp[0][1]初始化为0就行, + +今天卖出了股票(状态三),同样dp[0][2]初始化为0,因为最少收益就是0,绝不会是负数。 + +同理dp[0][3]也初始为0。 + + +4. 确定遍历顺序 + +从递归公式上可以看出,dp[i] 依赖于 dp[i-1],所以是从前向后遍历。 + +5. 举例推导dp数组 + +以 [1,2,3,0,2] 为例,dp数组如下: + +![309.最佳买卖股票时机含冷冻期](https://img-blog.csdnimg.cn/2021032317451040.png) + +最后结果去是 状态二,状态三,和状态四的最大值,不少同学会把状态四忘了,状态四是冷冻期,最后一天如果是冷冻期也可能是最大值。 + +代码如下: + +```C++ +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) + +当然,空间复杂度可以优化,定义一个dp[2][4]大小的数组就可以了,就保存前一天的当前的状态,感兴趣的同学可以自己去写一写,思路是一样的。 + +## 总结 + +这次把冷冻期这道题目,讲的很透彻了,细分为四个状态,其状态转移也十分清晰,建议大家都按照四个状态来分析,如果只划分三个状态确实很容易给自己绕进去。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0316.去除重复字母.md b/problems/0316.去除重复字母.md deleted file mode 100644 index 787cc371..00000000 --- a/problems/0316.去除重复字母.md +++ /dev/null @@ -1,52 +0,0 @@ -# 思路 - -// 浓浓的单调栈气息 - -这道题目一点都不简单,输入单调栈里情况比较多的题目。 - -需要解决如下问题: -// 这不是单纯遇到小的 栈里就弹出,判断是否在栈里,情况2 -// 如何记录它之前有没有出现过呢,也就是情况1 - - -情况1: -输入:"bbcaac" -输出:"ac" -预期结果:"bac" - -情况2 -输入:"abacb" -输出:"acb" -预期结果:"abc" - -情况3: -aba 输出 a 预期是ab - -``` -class Solution { -public: - string removeDuplicateLetters(string s) { - int letterCount[26] = {0}; - for (int i = 0; i < s.size(); i++) { - letterCount[s[i] - 'a']++; - } - bool isIn[26] = {false}; // 1 已经在栈里,0 不在栈里 - string st; - for (int i = 0; i < s.size(); i++) { - while(!st.empty() - && s[i] < st.back() - && letterCount[st.back() - 'a'] > 0 // 保证字符串i之后还有这个栈顶元素,栈才能做弹出操作,情况3 - && isIn[s[i] - 'a'] == false) { // 如果栈里已经有s[i]了,跳过:情况2 - isIn[st.back() - 'a'] = false; - st.pop_back(); - } - if (isIn[s[i] - 'a'] == false) { - st.push_back(s[i]); - isIn[s[i] - 'a'] = true; - } - letterCount[s[i] - 'a']--; // 只要用过了就减一:情况1 - } - return st; - } -}; -``` diff --git a/problems/0322.零钱兑换.md b/problems/0322.零钱兑换.md index 2a872131..fbb9c6df 100644 --- a/problems/0322.零钱兑换.md +++ b/problems/0322.零钱兑换.md @@ -1,50 +1,127 @@ +

+ + + + +

+

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

+# 动态规划: 给我个机会,我再兑换一次零钱 -[1] 0 ,输出的是0,不是-1啊,这颗真是天坑j +## 322. 零钱兑换 + +题目链接:https://leetcode-cn.com/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 + +## 思路 + +在[动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)中我们已经兑换一次零钱了,这次又要兑换,套路不一样! + +题目中说每种硬币的数量是无限的,可以看出是典型的完全背包问题。 + +动规五部曲分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[j]:凑足总额为j所需钱币的最少个数为dp[j]** + +2. 确定递推公式 + +得到dp[j](考虑coins[i]),只有一个来源,dp[j - coins[i]](没有考虑coins[i])。 + +凑足总额为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的元素都是应该是最大值。 + +代码如下: ``` -// dp初始化很重要 +vector dp(amount + 1, INT_MAX); +dp[0] = 0; +``` + +4. 确定遍历顺序 + +本题求钱币最小个数,**那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。**。 + +所以本题并不强调集合是组合还是排列。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +在动态规划专题我们讲过了求组合数是[动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ),求排列数是[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)。 + +**所以本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!** + +那么我采用coins放在外循环,target在内循环的方式。 + +本题钱币数量可以无限使用,那么是完全背包。所以遍历的内循环是正序 + +综上所述,遍历顺序为:coins(物品)放在外循环,target(背包)在内循环。且内循环正序。 + +5. 举例推导dp数组 + +以输入:coins = [1, 2, 5], amount = 5为例 + +![322.零钱兑换](https://img-blog.csdnimg.cn/20210201111833906.jpg) + +dp[amount]为最终结果。 + +## C++代码 +以上分析完毕,C++ 代码如下: + +```C++ +// 版本一 class Solution { public: int coinChange(vector& coins, int amount) { - //int dp[10003] = {0}; // 并没有给所有元素赋值0 - if (amount == 0) return 0; // 这个要注意 - vector dp(10003, 0); - // 不能这么初始化啊,[2147483647],2 这种例子 直接gg,但是这种初始化有助于理解 - for (int i = 0; i < coins.size(); i++) { - if (coins[i] <= amount) // 还必须要加这个判断 - dp[coins[i]] = 1; - } - for (int i = 1; i <= amount; i++) { - for (int j = 0; j < coins.size(); j++) { - if (i - coins[j] >= 0 && dp[i - coins[j]]!=0 ) { - if (dp[i] == 0) dp[i] = dp[i - coins[j]] + 1; - else dp[i] = min(dp[i - coins[j]] + 1, dp[i]); - } - } - //for (int k = 0 ; k<= amount; k++) { - // cout << dp[k] << " "; - //} - //cout << endl; - } - if (dp[amount] == 0) return -1; - return dp[amount]; - - } -}; -``` - -我用求组合的思路也过了, -``` -class Solution { -public: - int coinChange(vector& coins, int amount) { - //int dp[10003] = {0}; // 并没有给所有元素赋值0 - //if (amount == 0) return 0; - vector dp(10003, INT_MAX); + vector dp(amount + 1, INT_MAX); dp[0] = 0; - for (int i = 0 ;i < coins.size(); i++) { // 求组合 - for (int j = 1; j <= amount; j++) { - if (j - coins[i] >= 0 && dp[j - coins[i]] != INT_MAX) { + 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]); } } @@ -55,18 +132,18 @@ public: }; ``` -这种标记d代码简短,但思路有点绕 -``` +对于遍历方式遍历背包放在外循环,遍历物品放在内循环也是可以的,我就直接给出代码了 + +```C++ +// 版本二 class Solution { public: int coinChange(vector& coins, int amount) { - //int dp[10003] = {0}; // 并没有给所有元素赋值0 - // if (amount == 0) return 0; 这个都可以省略了,但很多同学不知道 还需要注意这个 - vector dp(10003, 0); - for (int i = 1; i <= amount; i++) { - dp[i] = INT_MAX; - for (int j = 0; j < coins.size(); j++) { - if (i - coins[j] >= 0 && dp[i - coins[j]]!=INT_MAX ) { + 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]); } } @@ -76,3 +153,46 @@ public: } }; ``` + +## 总结 + +细心的同学看网上的题解,**可能看一篇是遍历背包的for循环放外面,看一篇又是遍历背包的for循环放里面,看多了都看晕了**,到底两个for循环应该是什么先后关系。 + +能把遍历顺序讲明白的文章几乎找不到! + +这也是大多数同学学习动态规划的苦恼所在,有的时候递推公式很简单,难在遍历顺序上! + +但最终又可以稀里糊涂的把题目过了,也不知道为什么这样可以过,反正就是过了,哈哈 + +那么这篇文章就把遍历顺序分析的清清楚楚。 + +[动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)中求的是组合数,[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)中求的是排列数。 + +**而本题是要求最少硬币数量,硬币是组合数还是排列数都无所谓!所以两个for循环先后顺序怎样都可以!** + +这也是我为什么要先讲518.零钱兑换II 然后再讲本题即:322.零钱兑换,这是Carl的良苦用心那。 + +相信大家看完之后,对背包问题中的遍历顺序又了更深的理解了。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0332.重新安排行程.md b/problems/0332.重新安排行程.md index 8ea8fc7a..756ecc86 100644 --- a/problems/0332.重新安排行程.md +++ b/problems/0332.重新安排行程.md @@ -1,29 +1,37 @@ +

+ + + + +

+

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

+ > 这也可以用回溯法? 其实深搜和回溯也是相辅相成的,毕竟都用递归。 -# 332.重新安排行程 +## 332.重新安排行程 题目地址:https://leetcode-cn.com/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"] +示例 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"]。但是它自然排序更大更靠后。 +示例 2: +输入:[["JFK","SFO"],["JFK","ATL"],["SFO","ATL"],["ATL","JFK"],["ATL","SFO"]] +输出:["JFK","ATL","JFK","SFO","ATL","SFO"] +解释:另一种有效的行程是 ["JFK","SFO","ATL","JFK","ATL","SFO"]。但是它自然排序更大更靠后。 -# 思路 +## 思路 这道题目还是很难的,之前我们用回溯法解决了如下问题:[组合问题](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ),[分割问题](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA),[子集问题](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA),[排列问题](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw)。 @@ -35,7 +43,7 @@ **这里就是先给大家拓展一下,原来回溯法还可以这么玩!** -**这道题目有几个难点:** +**这道题目有几个难点:** 1. 一个行程中,如果航班处理不好容易变成一个圈,成为死循环 2. 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ? @@ -56,11 +64,11 @@ 有多种解法,字母序靠前排在前面,让很多同学望而退步,如何该记录映射关系呢 ? -一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。 +一个机场映射多个机场,机场之间要靠字母序排列,一个机场映射多个机场,可以使用std::unordered_map,如果让多个机场之间再有顺序的话,就是用std::map 或者std::multimap 或者 std::multiset。 -如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)。 +如果对map 和 set 的实现机制不太了解,也不清楚为什么 map、multimap就是有序的同学,可以看这篇文章[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)。 -这样存放映射关系可以定义为 `unordered_map> targets` 或者 `unordered_map> targets`。 +这样存放映射关系可以定义为 `unordered_map> targets` 或者 `unordered_map> targets`。 含义如下: @@ -71,7 +79,7 @@ **再说一下为什么一定要增删元素呢,正如开篇我给出的图中所示,出发机场和到达机场是会重复的,搜索的过程没及时删除目的机场就会死循环。** -所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用`unordered_map> targets`。 +所以搜索的过程中就是要不断的删multiset里的元素,那么推荐使用`unordered_map> targets`。 在遍历 `unordered_map<出发机场, map<到达机场, 航班次数>> targets`的过程中,**可以使用"航班次数"这个字段的数字做相应的增减,来标记到达机场是否使用过了。** @@ -107,7 +115,7 @@ void backtracking(参数) { * 递归函数参数 -在讲解映射关系的时候,已经讲过了,使用`unordered_map> targets;` 来记录航班的映射关系,我定义为全局变量。 +在讲解映射关系的时候,已经讲过了,使用`unordered_map> targets;` 来记录航班的映射关系,我定义为全局变量。 当然把参数放进函数里传进去也是可以的,我是尽量控制函数里参数的长度。 @@ -141,9 +149,9 @@ result.push_back("JFK"); // 起始机场 * 递归终止条件 -拿题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。 +拿题目中的示例为例,输入: [["MUC", "LHR"], ["JFK", "MUC"], ["SFO", "SJC"], ["LHR", "SFO"]] ,这是有4个航班,那么只要找出一种行程,行程里的机场个数是5就可以了。 -所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。 +所以终止条件是:我们回溯遍历的过程中,遇到的机场个数,如果达到了(航班数量+1),那么我们就找到了一个行程,把所有航班串在一起了。 代码如下: @@ -157,7 +165,7 @@ if (result.size() == ticketNum + 1) { * 单层搜索的逻辑 -回溯的过程中,如何遍历一个机场所对应的所有机场呢? +回溯的过程中,如何遍历一个机场所对应的所有机场呢? 这里刚刚说过,在选择映射函数的时候,不能选择`unordered_map> targets`, 因为一旦有元素增删multiset的迭代器就会失效,当然可能有牛逼的容器删除元素迭代器不会失效,这里就不在讨论了。 @@ -167,25 +175,23 @@ if (result.size() == ticketNum + 1) { 遍历过程如下: -``` - 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++; - } +```C++ +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++代码如下: -# C++代码 - -``` +```C++ class Solution { private: // unordered_map<出发机场, map<到达机场, 航班次数>> targets @@ -226,14 +232,14 @@ public: for (pair& target : targets[result[result.size() - 1]]) ``` pair里要有const,因为map中的key是不可修改的,所以是`pair`。 - + 如果不加const,也可以复制一份pair,例如这么写: ``` for (pairtarget : targets[result[result.size() - 1]]) ``` -# 总结 +## 总结 本题其实可以算是一道hard的题目了,关于本题的难点我在文中已经列出了。 @@ -246,7 +252,156 @@ for (pairtarget : targets[result[result.size() - 1]]) 就酱,很多录友表示和「代码随想录」相见恨晚,那么帮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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 +java 版本: + +```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); + } +} +``` + +python: + +```python +class Solution: + def findItinerary(self, tickets: List[List[str]]) -> List[str]: + # defaultdic(list) 是为了方便直接append + tickets_dict = defaultdict(list) + for item in tickets: + tickets_dict[item[0]].append(item[1]) + ''' + tickets_dict里面的内容是这样的 + {'JFK': ['SFO', 'ATL'], 'SFO': ['ATL'], 'ATL': ['JFK', 'SFO']}) + ''' + path = ["JFK"] + def backtracking(start_point): + # 终止条件 + if len(path) == len(tickets) + 1: + return True + tickets_dict[start_point].sort() + for _ in tickets_dict[start_point]: + #必须及时删除,避免出现死循环 + end_point = tickets_dict[start_point].pop(0) + path.append(end_point) + # 只要找到一个就可以返回了 + if backtracking(end_point): + return True + path.pop() + tickets_dict[start_point].append(end_point) + + backtracking("JFK") + return path +``` + +C语言版本: + +```C +char **result; +bool *used; +int g_found; + +int cmp(const void *str1, const void *str2) +{ + const char **tmp1 = *(char**)str1; + const char **tmp2 = *(char**)str2; + int ret = strcmp(tmp1[0], tmp2[0]); + if (ret == 0) { + return strcmp(tmp1[1], tmp2[1]); + } + return ret; +} + +void backtracting(char *** tickets, int ticketsSize, int* returnSize, char *start, char **result, bool *used) +{ + if (*returnSize == ticketsSize + 1) { + g_found = 1; + return; + } + for (int i = 0; i < ticketsSize; i++) { + if ((used[i] == false) && (strcmp(start, tickets[i][0]) == 0)) { + result[*returnSize] = (char*)malloc(sizeof(char) * 4); + memcpy(result[*returnSize], tickets[i][1], sizeof(char) * 4); + (*returnSize)++; + used[i] = true; + /*if ((*returnSize) == ticketsSize + 1) { + return; + }*/ + backtracting(tickets, ticketsSize, returnSize, tickets[i][1], result, used); + if (g_found) { + return; + } + (*returnSize)--; + used[i] = false; + } + } + return; +} + +char ** findItinerary(char *** tickets, int ticketsSize, int* ticketsColSize, int* returnSize){ + if (tickets == NULL || ticketsSize <= 0) { + return NULL; + } + result = malloc(sizeof(char*) * (ticketsSize + 1)); + used = malloc(sizeof(bool) * ticketsSize); + memset(used, false, sizeof(bool) * ticketsSize); + result[0] = malloc(sizeof(char) * 4); + memcpy(result[0], "JFK", sizeof(char) * 4); + g_found = 0; + *returnSize = 1; + qsort(tickets, ticketsSize, sizeof(tickets[0]), cmp); + backtracting(tickets, ticketsSize, returnSize, "JFK", result, used); + *returnSize = ticketsSize + 1; + return result; +} +``` + + +----------------------- +* 作者微信:[程序员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/0337.打家劫舍III.md b/problems/0337.打家劫舍III.md new file mode 100644 index 00000000..50b06e22 --- /dev/null +++ b/problems/0337.打家劫舍III.md @@ -0,0 +1,235 @@ +

+ + + + +

+

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

+ + +## 337.打家劫舍 III + +题目链接:https://leetcode-cn.com/problems/house-robber-iii/ + +在上次打劫完一条街道之后和一圈房屋后,小偷又发现了一个新的可行窃的地区。这个地区只有一个入口,我们称之为“根”。 除了“根”之外,每栋房子有且只有一个“父“房子与之相连。一番侦察之后,聪明的小偷意识到“这个地方的所有房屋的排列类似于一棵二叉树”。 如果两个直接相连的房子在同一天晚上被打劫,房屋将自动报警。 + +计算在不触动警报的情况下,小偷一晚能够盗取的最高金额。 + +![337.打家劫舍III](https://img-blog.csdnimg.cn/20210223173849619.png) + +## 思路 + +这道题目和 [198.打家劫舍](https://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw),[213.打家劫舍II](https://mp.weixin.qq.com/s/kKPx4HpH3RArbRcxAVHbeQ)也是如出一辙,只不过这个换成了树。 + +如果对树的遍历不够熟悉的话,那本题就有难度了。 + +对于树的话,首先就要想到遍历方式,前中后序(深度优先搜索)还是层序遍历(广度优先搜索)。 + +**本题一定是要后序遍历,因为通过递归函数的返回值来做下一步计算**。 + +与198.打家劫舍,213.打家劫舍II一样,关键是要讨论当前节点抢还是不抢。 + +如果抢了当前节点,两个孩子就不是动,如果没抢当前节点,就可以考虑抢左右孩子(**注意这里说的是“考虑”**) + +### 暴力递归 + +代码如下: + +```C++ +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(logn) 算上递推系统栈的空间 + +当然以上代码超时了,这个递归的过程中其实是有重复计算了。 + +我们计算了root的四个孙子(左右孩子的孩子)为头结点的子树的情况,又计算了root的左右孩子为头结点的子树的情况,计算左右孩子的时候其实又把孙子计算了一遍。 + +### 记忆化递推 + +所以可以使用一个map把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。 + +代码如下: + +```C++ +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(logn) 算上递推系统栈的空间 + + +### 动态规划 + +在上面两种方法,其实对一个节点 投与不投得到的最大金钱都没有做记录,而是需要实时计算。 + +而动态规划其实就是使用状态转移容器来记录状态的变化,这里可以使用一个长度为2的数组,记录当前节点偷与不偷所得到的的最大金钱。 + +**这道题目算是树形dp的入门题目,因为是在树上进行状态转移,我们在讲解二叉树的时候说过递归三部曲,那么下面我以递归三部曲为框架,其中融合动规五部曲的内容来进行讲解**。 + +1. 确定递归函数的参数和返回值 + +这里我们要求一个节点 偷与不偷的两个状态所得到的金钱,那么返回值就是一个长度为2的数组。 + +参数为当前节点,代码如下: + +```C++ +vector robTree(TreeNode* cur) { +``` + +其实这里的返回数组就是dp数组。 + +所以dp数组(dp table)以及下标的含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。 + +**所以本题dp数组就是一个长度为2的数组!** + +那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢? + +**别忘了在递归的过程中,系统栈会保存每一层递归的参数**。 + +如果还不理解的话,就接着往下看,看到代码就理解了哈。 + +2. 确定终止条件 + +在遍历的过程中,如果遇到空间点的话,很明显,无论偷还是不偷都是0,所以就返回 +``` +if (cur == NULL) return vector{0, 0}; +``` +这也相当于dp数组的初始化 + + +3. 确定遍历顺序 + +首先明确的是使用后序遍历。 因为通过递归函数的返回值来做下一步计算。 + +通过递归左节点,得到左节点偷与不偷的金钱。 + +通过递归右节点,得到右节点偷与不偷的金钱。 + +代码如下: + +```C++ +// 下标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}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱} + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210129181331613.jpg) + +**最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱**。 + +递归三部曲与动规五部曲分析完毕,C++代码如下: + +```C++ +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(logn) 算上递推系统栈的空间 + +## 总结 + +这道题是树形DP的入门题目,通过这道题目大家应该也了解了,所谓树形DP就是在树上进行递归公式的推导。 + +**所以树形DP也没有那么神秘!** + +只不过平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解! + +大家还记不记得我在讲解贪心专题的时候,讲到这道题目:[贪心算法:我要监控二叉树!](https://mp.weixin.qq.com/s/kCxlLLjWKaE6nifHC3UL2Q),这也是贪心算法在树上的应用。**那我也可以把这个算法起一个名字,叫做树形贪心**,哈哈哈 + +“树形贪心”词汇从此诞生,来自「代码随想录」 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0343.整数拆分.md b/problems/0343.整数拆分.md index 3ba67945..e7550285 100644 --- a/problems/0343.整数拆分.md +++ b/problems/0343.整数拆分.md @@ -1,36 +1,110 @@ +

+ + + + +

+

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

-// 拆成两个 还是拆成三个呢 +## 343. 整数拆分 -# 思路 +给定一个正整数 n,将其拆分为至少两个正整数的和,并使这些整数的乘积最大化。 返回你可以获得的最大乘积。 -## 动态规划 +示例 1: +输入: 2 +输出: 1 +解释: 2 = 1 + 1, 1 × 1 = 1。 -* 明确dp[i]的含义 +示例 2: +输入: 10 +输出: 36 +解释: 10 = 3 + 3 + 4, 3 × 3 × 4 = 36。 +说明: 你可以假设 n 不小于 2 且不大于 58。 -dp[i]表示 分拆数字i,可以得到的最大乘积。 +## 思路 -* dp的初始化 +看到这道题目,都会想拆成两个呢,还是三个呢,还是四个.... -初始化dp[i] = i,这里的初始化 不是为了让 i的最大乘积是dp[i],而是为了做递推公式的时候,dp[i]可以表示i这个数字,好用来做乘法。 +我们来看一下如何使用动规来解决。 -* 递归公式 +### 动态规划 -可以想 dp[i]的最大乘积是怎么得到的呢? +动规五部曲,分析如下: -**一定是dp[j]的最大乘积 * dp[i - j]的最大乘积,那么只需要遍历一下j(取值范围[2,i-1)),取此时dp[i]的最大值就可以了** +1. 确定dp数组(dp table)以及下标的含义 -递推公式:dp[i] = max(dp[i], dp[i - j] * dp[j]); +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)); + +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]。 + +枚举j的时候,是从1开始的。i是从3开始,这样dp[i - j]就是dp[2]正好可以通过我们初始化的数值求出来。 + +所以遍历顺序为: ``` +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)); + } +} +``` + +5. 举例推导dp数组 + +举例当n为10 的时候,dp数组里的数值,如下: + +![343.整数拆分](https://img-blog.csdnimg.cn/20210104173021581.png) + +以上动规五部曲分析完毕,C++代码如下: + +```C++ class Solution { public: int integerBreak(int n) { - if (n <= 3) return 1 * (n - 1); // 处理 2和3的情况 - int dp[60]; - for (int i = 0; i <= n; i++) dp[i] = i; // 初始化 - for (int i = 4; i <= n ; i++) { - for (int j = 2; j < i - 1; j++) { - dp[i] = max(dp[i], dp[i - j] * dp[j]); + 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)); } } return dp[n]; @@ -38,8 +112,91 @@ public: }; ``` -# 贪心 +* 时间复杂度:O(n^2) +* 空间复杂度:O(n) -本题也可以用贪心,但是真的需要数学证明证明其合理性,网上有很多贪心的代码,每次拆成3就可以了,代码很简单,大家如果感兴趣可以自己去查一查。 +### 贪心 -我这里就不做证明了。 +本题也可以用贪心,每次拆成n个3,如果剩下是4,则保留4,然后相乘,**但是这个结论需要数学证明其合理性!** + +我没有证明,而是直接用了结论。感兴趣的同学可以自己再去研究研究数学证明哈。 + +给出我的C++代码如下: + +```C++ +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) + +## 总结 + +本题掌握其动规的方法,就可以了,贪心的解法确实简单,但需要有数学证明,如果能自圆其说也是可以的。 + +其实这道题目的递推公式并不好想,而且初始化的地方也很有讲究,我在写本题的时候一开始写的代码是这样的: + +```C++ +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 - 1; 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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0344.反转字符串.md b/problems/0344.反转字符串.md index 89605974..ddb9805d 100644 --- a/problems/0344.反转字符串.md +++ b/problems/0344.反转字符串.md @@ -1,10 +1,19 @@ -## 题目地址 +

+ + + + +

+

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

+ + + +> 打基础的时候,不要太迷恋于库函数。 + +# 344.反转字符串 + https://leetcode-cn.com/problems/reverse-string/ -> 打基础的时候,不要太迷恋于库函数。 - -# 题目:344. 反转字符串 - 编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 @@ -27,7 +36,7 @@ https://leetcode-cn.com/problems/reverse-string/ 对于这道题目一些同学直接用C++里的一个库函数 reverse,调一下直接完事了, 相信每一门编程语言都有这样的库函数。 -如果这么做题的话,这样大家不会清楚反转字符串的实现原理了。 +如果这么做题的话,这样大家不会清楚反转字符串的实现原理了。 但是也不是说库函数就不能用,是要分场景的。 @@ -49,23 +58,24 @@ https://leetcode-cn.com/problems/reverse-string/ 大家应该还记得,我们已经讲过了[206.反转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg)。 -在反转链表中,使用了双指针的方法。 +在反转链表中,使用了双指针的方法。 那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。 因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。 -如果对数组和链表原理不清楚的同学,可以看这两篇,[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ),[必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg)。 +如果对数组和链表原理不清楚的同学,可以看这两篇,[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ),[必须掌握的数组理论知识](https://mp.weixin.qq.com/s/X7R55wSENyY62le0Fiawsg)。 对于字符串,我们定义两个指针(也可以说是索引下表),一个从字符串前面,一个从字符串后面,两个指针同时向中间移动,并交换元素。 以字符串`hello`为例,过程如下: - +![344.反转字符串](https://tva1.sinaimg.cn/large/008eGmZEly1gp0fvi91pfg30de0akwnq.gif) + 不难写出如下C++代码: -``` +```C++ void reverseString(vector& s) { for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) { swap(s[i],s[j]); @@ -75,13 +85,13 @@ void reverseString(vector& s) { 循环里只要做交换s[i] 和s[j]操作就可以了,那么我这里使用了swap 这个库函数。大家可以使用。 -因为相信大家都知道交换函数如何实现,而且这个库函数仅仅是解题中的一部分, 所以这里使用库函数也是可以的。 +因为相信大家都知道交换函数如何实现,而且这个库函数仅仅是解题中的一部分, 所以这里使用库函数也是可以的。 swap可以有两种实现。 一种就是常见的交换数值: -``` +```C++ int tmp = s[i]; s[i] = s[j]; s[j] = tmp; @@ -90,14 +100,14 @@ s[j] = tmp; 一种就是通过位运算: -``` +```C++ s[i] ^= s[j]; s[j] ^= s[i]; s[i] ^= s[j]; ``` -这道题目还是比较简单的,但是我正好可以通过这道题目说一说在刷题的时候,使用库函数的原则。 +这道题目还是比较简单的,但是我正好可以通过这道题目说一说在刷题的时候,使用库函数的原则。 如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。 @@ -112,7 +122,7 @@ s[i] ^= s[j]; ## C++代码 -``` +```C++ class Solution { public: void reverseString(vector& s) { @@ -122,5 +132,26 @@ public: } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0347.前K个高频元素.md b/problems/0347.前K个高频元素.md index 2a532aa4..1902bd68 100644 --- a/problems/0347.前K个高频元素.md +++ b/problems/0347.前K个高频元素.md @@ -1,39 +1,48 @@ -## 题目地址 +

+ + + + +

+

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

+ + -https://leetcode-cn.com/problems/top-k-frequent-elements/ > 前K个大数问题,老生常谈,不得不谈 # 347.前 K 个高频元素 +https://leetcode-cn.com/problems/top-k-frequent-elements/ + 给定一个非空的整数数组,返回其中出现频率前 k 高的元素。 -示例 1: -输入: nums = [1,1,1,2,2,3], k = 2 -输出: [1,2] +示例 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 个高频元素的集合是唯一的。 -你可以按任意顺序返回答案。 +示例 2: +输入: nums = [1], k = 1 +输出: [1] + +提示: +你可以假设给定的 k 总是合理的,且 1 ≤ k ≤ 数组中不相同的元素的个数。 +你的算法的时间复杂度必须优于 O(n log n) , n 是数组的大小。 +题目数据保证答案唯一,换句话说,数组中前 k 个高频元素的集合是唯一的。 +你可以按任意顺序返回答案。 # 思路 这道题目主要涉及到如下三块内容: 1. 要统计元素出现频率 2. 对频率排序 -3. 找出前K个高频元素 +3. 找出前K个高频元素 首先统计元素出现的频率,这一类的问题可以使用map来进行统计。 -然后是对频率进行排序,这里我们可以使用一种 容器适配器就是**优先级队列**。 +然后是对频率进行排序,这里我们可以使用一种 容器适配器就是**优先级队列**。 -什么是优先级队列呢? +什么是优先级队列呢? 其实**就是一个披着队列外衣的堆**,因为优先级队列对外接口只是从队头取元素,从队尾添加元素,再无其他取元素的方式,看起来就是一个队列。 @@ -41,9 +50,9 @@ https://leetcode-cn.com/problems/top-k-frequent-elements/ 缺省情况下priority_queue利用max-heap(大顶堆)完成对元素的排序,这个大顶堆是以vector为表现形式的complete binary tree(完全二叉树)。 -什么是堆呢? +什么是堆呢? -**堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。** 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 +**堆是一颗完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。** 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆。 所以大家经常说的大顶堆(堆头是最大元素),小顶堆(堆头是最小元素),如果懒得自己实现的话,就直接用priority_queue(优先级队列)就可以了,底层实现都是一样的,从小到大排就是小顶堆,从大到小排就是大顶堆。 @@ -51,7 +60,7 @@ https://leetcode-cn.com/problems/top-k-frequent-elements/ 为什么不用快排呢, 使用快排要将map转换为vector的结构,然后对整个数组进行排序, 而这种场景下,我们其实只需要维护k个有序的序列就可以了,所以使用优先级队列是最优的。 -此时要思考一下,是使用小顶堆呢,还是大顶堆? +此时要思考一下,是使用小顶堆呢,还是大顶堆? 有的同学一想,题目要求前 K 个高频元素,那么果断用大顶堆啊。 @@ -61,14 +70,13 @@ https://leetcode-cn.com/problems/top-k-frequent-elements/ 寻找前k个最大元素流程如图所示:(图中的频率只有三个,所以正好构成一个大小为3的小顶堆,如果频率更多一些,则用这个小顶堆进行扫描) - +![347.前K个高频元素](https://code-thinking.cdn.bcebos.com/pics/347.%E5%89%8DK%E4%B8%AA%E9%AB%98%E9%A2%91%E5%85%83%E7%B4%A0.jpg) -我们来看一下代码: +我们来看一下C++代码: -# C++代码 -``` +```C++ // 时间复杂度:O(nlogk) // 空间复杂度:O(n) class Solution { @@ -90,8 +98,8 @@ public: // 对频率排序 // 定义一个小顶堆,大小为k priority_queue, vector>, mycomparison> pri_que; - - // 用固定大小为k的小顶堆,扫面所有频率的数值 + + // 用固定大小为k的小顶堆,扫面所有频率的数值 for (unordered_map::iterator it = map.begin(); it != map.end(); it++) { pri_que.push(*it); if (pri_que.size() > k) { // 如果堆的大小大于了K,则队列弹出,保证堆的大小一直为k @@ -118,5 +126,25 @@ public: 优先级队列的定义正好反过来了,可能和优先级队列的源码实现有关(我没有仔细研究),我估计是底层实现上优先队列队首指向后面,队尾指向最前面的缘故! -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0349.两个数组的交集.md b/problems/0349.两个数组的交集.md index 2b4fef56..c196b467 100644 --- a/problems/0349.两个数组的交集.md +++ b/problems/0349.两个数组的交集.md @@ -1,20 +1,29 @@ +

+ + + + +

+

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

+ + -## 题目地址 -https://leetcode-cn.com/problems/intersection-of-two-arrays/ > 如果哈希值比较少、特别分散、跨度非常大,使用数组就造成空间的极大浪费! -# 第349题. 两个数组的交集 +# 349. 两个数组的交集 + +https://leetcode-cn.com/problems/intersection-of-two-arrays/ 题意:给定两个数组,编写一个函数来计算它们的交集。 -![349. 两个数组的交集](https://img-blog.csdnimg.cn/20200818193523911.png) +![349. 两个数组的交集](https://img-blog.csdnimg.cn/20200818193523911.png) **说明:** -输出结果中的每个元素一定是唯一的。 +输出结果中的每个元素一定是唯一的。 我们可以不考虑输出结果的顺序。 -# 思路 +# 思路 这道题目,主要要学会使用一种哈希数据结构:unordered_set,这个数据结构可以解决很多类似的问题。 @@ -24,7 +33,7 @@ https://leetcode-cn.com/problems/intersection-of-two-arrays/ 那么用数组来做哈希表也是不错的选择,例如[242. 有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig),[0383.赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) -但是要注意,**使用数据来做哈希的题目,都限制了数值的大小。** +但是要注意,**使用数组来做哈希的题目,是因为题目都限制了数值的大小。** 而这道题目没有限制数值的大小,就无法使用数组来做哈希表了。 @@ -42,8 +51,8 @@ std::set和std::multiset底层实现都是红黑树,std::unordered_set的底 ![set哈希法](https://img-blog.csdnimg.cn/2020080918570417.png) -# C++代码 -``` +C++代码如下: +```C++ class Solution { public: vector intersection(vector& nums1, vector& nums2) { @@ -59,5 +68,25 @@ public: } }; ``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0350.两个数组的交集II.md b/problems/0350.两个数组的交集II.md deleted file mode 100644 index 2320b801..00000000 --- a/problems/0350.两个数组的交集II.md +++ /dev/null @@ -1,36 +0,0 @@ - -## 题目地址 - -https://leetcode-cn.com/problems/intersection-of-two-arrays-ii/ - -## 思路 - -这道题目,看上去和349两个数组的交集题目描述是一样的,其实这两道题解法相差还是很大的,编号349题目结果是去重的 - - -而本题才求的真正的交集,求这两个集合元素的交集,需要掌握另一个哈希数据结构unordered_map - - -## 代码 - -``` -class Solution { -public: - vector intersect(vector& nums1, vector& nums2) { - vector result; - unordered_map map; - for (int num : nums1) { - map[num]++; - } - for (int num : nums2) { - if (map[num] > 0) { - result.push_back(num); - map[num]--; - } - } - return result; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0376.摆动序列.md b/problems/0376.摆动序列.md index c6682a21..6ae76526 100644 --- a/problems/0376.摆动序列.md +++ b/problems/0376.摆动序列.md @@ -1,7 +1,15 @@ +

+ + + + +

+

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

+ > 本周讲解了[贪心理论基础](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg),以及第一道贪心的题目:[贪心算法:分发饼干](https://mp.weixin.qq.com/s/YSuLIAYyRGlyxbp9BNC1uw),可能会给大家一种贪心算法比较简单的错觉,好了,接下来几天的题目难度要上来了,哈哈。 -# 376. 摆动序列 +## 376. 摆动序列 题目链接:https://leetcode-cn.com/problems/wiggle-subsequence/ @@ -11,28 +19,28 @@ 给定一个整数序列,返回作为摆动序列的最长子序列的长度。 通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 -示例 1: -输入: [1,7,4,9,2,5] -输出: 6 -解释: 整个序列均为摆动序列。 +示例 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]。 +示例 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 +示例 3: +输入: [1,2,3,4,5,6,7,8,9] +输出: 2 -## 思路 +## 思路 本题要求通过从原始序列中删除一些(也可以不删除)元素来获得子序列,剩下的元素保持其原始顺序。 -相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢? +相信这么一说吓退不少同学,这要求最大摆动序列又可以修改数组,这得如何修改呢? -来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢? +来分析一下,要求删除元素使其达到最大摆动序列,应该删除什么元素呢? 用示例二来举例,如图所示: @@ -82,25 +90,39 @@ public: } }; ``` -时间复杂度O(n) -空间复杂度O(1) +时间复杂度O(n) +空间复杂度O(1) -# 总结 +## 总结 **贪心的题目说简单有的时候就是常识,说难就难在都不知道该怎么用贪心**。 本题大家如果要去模拟删除元素达到最长摆动子序列的过程,那指定绕里面去了,一时半会拔不出来。 -而这道题目有什么技巧说一下子能想到贪心么? +而这道题目有什么技巧说一下子能想到贪心么? 其实也没有,类似的题目做过了就会想到。 此时大家就应该了解了:保持区间波动,只需要把单调区间上的元素移除就可以了。 -就酱,「代码随想录」值得介绍给身边每一位学习算法的同学! - -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0377.组合总和Ⅳ.md b/problems/0377.组合总和Ⅳ.md index 0db1d908..b0b83718 100644 --- a/problems/0377.组合总和Ⅳ.md +++ b/problems/0377.组合总和Ⅳ.md @@ -1,15 +1,121 @@ +

+ + + + +

+

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

+# 动态规划:Carl称它为排列总和! -和之前个回溯法的各个总和串起来一波,本题用回溯稳稳的超时 +## 377. 组合总和 Ⅳ -``` +题目链接:https://leetcode-cn.com/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。 + +## 思路 + +本题题目描述说是求组合,但又说是可以元素相同顺序不同的组合算两个组合,**其实就是求排列!** + +弄清什么是组合,什么是排列很重要。 + +组合不强调顺序,(1,5)和(5,1)是同一个组合。 + +排列强调顺序,(1,5)和(5,1)是两个不同的排列。 + +大家在公众号里学习回溯算法专题的时候,一定做过这两道题目[回溯算法:39.组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)和[回溯算法:40.组合总和II](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ)会感觉这两题和本题很像! + +但其本质是本题求的是排列总和,而且仅仅是求排列总和的个数,并不是把所有的排列都列出来。 + +**如果本题要把排列都列出来的话,只能使用回溯算法爆搜**。 + +动规五部曲分析如下: + +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://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw) 和 [动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)中我们已经讲过了,求装满背包有几种方法,递推公式一般都是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://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) 中就已经讲过了。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面! + +所以本题遍历顺序最终遍历顺序:**target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历**。 + +5. 举例来推导dp数组 + +我们再来用示例中的例子推导一下: + +![377.组合总和Ⅳ](https://img-blog.csdnimg.cn/20210131174250148.jpg) + +如果代码运行处的结果不是想要的结果,就把dp[i]都打出来,看看和我们推导的一不一样。 + +以上分析完毕,C++代码如下: + +```C++ 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) { + 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]]; } } @@ -17,30 +123,42 @@ public: return dp[target]; } }; + ``` +C++测试用例有超过两个树相加超过int的数据,所以需要在if里加上dp[i] < INT_MAX - dp[i - num]。 -C++测试用例有超过两个树相加超过int的数据 -超限的情况 +但java就不用考虑这个限制,java里的int也是四个字节吧,也有可能leetcode后台对不同语言的测试数据不一样。 + +## 总结 + +**求装满背包有几种方法,递归公式都是一样的,没有什么差别,但关键在于遍历顺序!** + +本题与[动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)就是一个鲜明的对比,一个是求排列,一个是求组合,遍历顺序完全不同。 + +如果对遍历顺序没有深度理解的话,做这种完全背包的题目会很懵逼,即使题目刷过了可能也不太清楚具体是怎么过的。 + +此时大家应该对动态规划中的遍历顺序又有更深的理解了。 -一些题解会直接用ull usigned long long -java 也是四个字节,理论上没有差别,可能后台java和C++测试用例不同,bug..... -``` -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 num : nums) { - if (i - num >= 0 && dp[i] < INT_MAX - dp[i - num]) { - dp[i] += dp[i - num]; - } - } - } - return dp[target]; - } -}; -``` + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0383.赎金信.md b/problems/0383.赎金信.md index b0440583..ca9b6061 100644 --- a/problems/0383.赎金信.md +++ b/problems/0383.赎金信.md @@ -1,9 +1,18 @@ -# 题目地址 -https://leetcode-cn.com/problems/ransom-note/ +

+ + + + +

+

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

+ + > 在哈希法中有一些场景就是为数组量身定做的。 -# 第383题. 赎金信 +# 383. 赎金信 + +https://leetcode-cn.com/problems/ransom-note/ 给定一个赎金信 (ransom) 字符串和一个杂志(magazine)字符串,判断第一个字符串 ransom 能不能由第二个字符串 magazines 里面的字符构成。如果可以构成,返回 true ;否则返回 false。 @@ -17,7 +26,7 @@ canConstruct("a", "b") -> false canConstruct("aa", "ab") -> false canConstruct("aa", "aab") -> true -# 思路 +# 思路 这道题目和[242.有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)很像,[242.有效的字母异位词](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)相当于求 字符串a 和 字符串b 是否可以相互组成 ,而这道题目是求 字符串a能否组成字符串b,而不用管字符串b 能不能组成字符串a。 @@ -27,7 +36,7 @@ canConstruct("aa", "aab") -> true * 第二点 “你可以假设两个字符串均只含有小写字母。” *说明只有小写字母*,这一点很重要 -# 暴力解法 +# 暴力解法 那么第一个思路其实就是暴力枚举了,两层for循环,不断去寻找,代码如下: @@ -40,14 +49,14 @@ public: for (int i = 0; i < magazine.length(); i++) { for (int j = 0; j < ransomNote.length(); j++) { // 在ransomNote中找到和magazine相同的字符 - if (magazine[i] == ransomNote[j]) { + if (magazine[i] == ransomNote[j]) { ransomNote.erase(ransomNote.begin() + j); // ransomNote删除这个字符 break; } } } // 如果ransomNote为空,则说明magazine的字符可以组成ransomNote - if (ransomNote.length() == 0) { + if (ransomNote.length() == 0) { return true; } return false; @@ -60,7 +69,7 @@ public: # 哈希解法 -因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。 +因为题目所只有小写字母,那可以采用空间换取时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数。 然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。 @@ -68,7 +77,7 @@ public: 一些同学可能想,用数组干啥,都用map完事了,**其实在本题的情况下,使用map的空间消耗要比数组大一些的,因为map要维护红黑树或者哈希表,而且还要做哈希函数。 所以数组更加简单直接有效!** -代码如下: +代码如下: ```C++ // 时间复杂度: O(n) @@ -79,12 +88,12 @@ public: int record[26] = {0}; for (int i = 0; i < magazine.length(); i++) { // 通过recode数据记录 magazine里各个字符出现次数 - record[magazine[i]-'a'] ++; + record[magazine[i]-'a'] ++; } for (int j = 0; j < ransomNote.length(); j++) { // 遍历ransomNote,在record里对应的字符个数做--操作 - record[ransomNote[j]-'a']--; - // 如果小于零说明 magazine里出现的字符,ransomNote没有 + record[ransomNote[j]-'a']--; + // 如果小于零说明ransomNote里出现的字符,magazine没有 if(record[ransomNote[j]-'a'] < 0) { return false; } @@ -94,5 +103,26 @@ public: }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0392.判断子序列.md b/problems/0392.判断子序列.md new file mode 100644 index 00000000..95e0b124 --- /dev/null +++ b/problems/0392.判断子序列.md @@ -0,0 +1,158 @@ +

+ + + + +

+

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

+ + +## 392.判断子序列 + +题目链接:https://leetcode-cn.com/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 + +两个字符串都只由小写字符组成。 + + +## 思路 + +(这道题可以用双指针的思路来实现,时间复杂度就是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为结尾的字符串呢? + +用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]; + + +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://img-blog.csdnimg.cn/20210303173115966.png) + +如果要是定义的dp[i][j]是以下标i为结尾的字符串s和以下标j为结尾的字符串t,初始化就比较麻烦了。 + +这里dp[i][0]和dp[0][j]是没有含义的,仅仅是为了给递推公式做前期铺垫,所以初始化为0。 + +**其实这里只初始化dp[i][0]就够了,但一起初始化也方便,所以就一起操作了**,代码如下: + +``` +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://img-blog.csdnimg.cn/20210303172354155.jpg) + +5. 举例推导dp数组 + +以示例一为例,输入:s = "abc", t = "ahbgdc",dp状态转移图如下: + +![392.判断子序列2](https://img-blog.csdnimg.cn/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++代码如下: + +```C++ +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) + +## 总结 + +这道题目算是编辑距离的入门题目(毕竟这里只是涉及到减法),也是动态规划解决的经典题型。 + +这一类题都是题目读上去感觉很复杂,模拟一下也发现很复杂,用动规分析完了也感觉很复杂,但是最终代码却很简短。 + +编辑距离的题目最能体现出动规精髓和巧妙之处,大家可以好好体会一下。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0402.移掉K位数字.md b/problems/0402.移掉K位数字.md deleted file mode 100644 index c7112bff..00000000 --- a/problems/0402.移掉K位数字.md +++ /dev/null @@ -1,11 +0,0 @@ - -## 思路 - -https://www.cnblogs.com/gzshan/p/12560566.html 图不错 - -有点难度 - - -暴力的解法,其实也不是那么好写的, 首字符去0,k没消耗完,等等这些情况 - -有点难,卡我很久 diff --git a/problems/0404.左叶子之和.md b/problems/0404.左叶子之和.md index 1050a90c..8ff2b320 100644 --- a/problems/0404.左叶子之和.md +++ b/problems/0404.左叶子之和.md @@ -1,17 +1,23 @@ -## 题目地址 -https://leetcode-cn.com/problems/sum-of-left-leaves/ +

+ + + + +

+

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

-> 有的题目就是 -# 404.左叶子之和 +## 404.左叶子之和 + +题目地址:https://leetcode-cn.com/problems/sum-of-left-leaves/ 计算给定二叉树的所有左叶子之和。 示例: - +![404.左叶子之和1](https://img-blog.csdnimg.cn/20210204151927654.png) -# 思路 +## 思路 **首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。** @@ -19,7 +25,7 @@ https://leetcode-cn.com/problems/sum-of-left-leaves/ 大家思考一下如下图中二叉树,左叶子之和究竟是多少? - +![404.左叶子之和](https://img-blog.csdnimg.cn/20210204151949672.png) **其实是0,因为这棵树根本没有左叶子!** @@ -40,31 +46,31 @@ if (node->left != NULL && node->left->left == NULL && node->left->right == NULL) 递归三部曲: -1. 确定递归函数的参数和返回值 +1. 确定递归函数的参数和返回值 -判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int +判断一个树的左叶子节点之和,那么一定要传入树的根节点,递归函数的返回值为数值之和,所以为int 使用题目中给出的函数就可以了。 -2. 确定终止条件 +2. 确定终止条件 依然是 ``` if (root == NULL) return 0; ``` -3. 确定单层递归的逻辑 +3. 确定单层递归的逻辑 当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。 代码如下: -``` +```C++ int leftValue = sumOfLeftLeaves(root->left); // 左 int rightValue = sumOfLeftLeaves(root->right); // 右 // 中 int midValue = 0; -if (root->left && !root->left->left && !root->left->right) { +if (root->left && !root->left->left && !root->left->right) { midValue = root->left->val; } int sum = midValue + leftValue + rightValue; @@ -75,7 +81,7 @@ return sum; 整体递归代码如下: -``` +```C++ class Solution { public: int sumOfLeftLeaves(TreeNode* root) { @@ -96,7 +102,7 @@ public: 以上代码精简之后如下: -``` +```C++ class Solution { public: int sumOfLeftLeaves(TreeNode* root) { @@ -110,14 +116,14 @@ public: }; ``` -## 迭代法 +## 迭代法 本题迭代法使用前中后序都是可以的,只要把左叶子节点统计出来,就可以了,那么参考文章 [二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)和[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)中的写法,可以写出一个前序遍历的迭代法。 判断条件都是一样的,代码如下: -``` +```C++ class Solution { public: @@ -140,12 +146,33 @@ public: }; ``` -# 总结 +## 总结 这道题目要求左叶子之和,其实是比较绕的,因为不能判断本节点是不是左叶子节点。 -此时就要通过节点的父节点来判断其左孩子是不是左叶子了。 +此时就要通过节点的父节点来判断其左孩子是不是左叶子了。 **平时我们解二叉树的题目时,已经习惯了通过节点的左右孩子判断本节点的属性,而本题我们要通过节点的父节点判断本节点的属性。** 希望通过这道题目,可以扩展大家对二叉树的解题思路。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0406.根据身高重建队列.md b/problems/0406.根据身高重建队列.md index fa7a3607..b5260a8c 100644 --- a/problems/0406.根据身高重建队列.md +++ b/problems/0406.根据身高重建队列.md @@ -1,40 +1,35 @@ -

- -

- - + + - -

+

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

-> 就不能好好站个队 -# 406.根据身高重建队列 +## 406.根据身高重建队列 -题目链接:https://leetcode-cn.com/problems/queue-reconstruction-by-height/ +题目链接:https://leetcode-cn.com/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]] 是重新构造后的队列。 +示例 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]] +示例 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]] 提示: @@ -44,7 +39,7 @@ 题目数据确保队列可以被重建 -# 思路 +## 思路 本题有两个维度,h和k,看到这种题目一定要想如何确定一个维度,然后在按照另一个维度重新排列。 @@ -62,7 +57,7 @@ **此时我们可以确定一个维度了,就是身高,前面的节点一定都比本节点高!** -那么只需要按照k为下标重新插入队列就可以了,为什么呢? +那么只需要按照k为下标重新插入队列就可以了,为什么呢? 以图中{5,2} 为例: @@ -89,16 +84,16 @@ 回归本题,整个插入过程如下: -排序完的people: -[[7,0], [7,1], [6,1], [5,0], [5,2],[4,4]] +排序完的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]] +插入[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]] 此时就按照题目的要求完成了重新排列。 @@ -123,20 +118,12 @@ public: } }; ``` -* 时间复杂度O(nlogn + n^3) +* 时间复杂度O(nlogn + n^2) * 空间复杂度O(n) -大家会发现这个n^3 是怎么来的? +但使用vector是非常费时的,C++中vector(可以理解是一个动态数组,底层是普通数组实现的)如果插入元素大于预先普通数组大小,vector底部会有一个扩容的操作,即申请两倍于原先普通数组的大小,然后把数据拷贝到另一个更大的数组上。 -其实数组的插入操作复杂度是O(n^2):寻找插入元素位置O(1),插入元素O(n^2),因为插入元素后面的元素要整体向后移。 - -如果对数组的增删时间复杂度不清楚的话,可以做做这道题目[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA),数组中插入元素和删除元素都是O(n^2)的复杂度。 - -我们就是要模拟一个插入队列的行为,所以不应该使用数组,而是要使用链表! - -链表的插入操作复杂度是O(n):寻找插入元素位置O(n),插入元素O(1)。 - -可以看出使用链表的插入效率要比普通数组高出一个数量级! +所以使用vector(动态数组)来insert,是费时的,插入再拷贝的话,单纯一个插入的操作就是O(n^2)了,甚至可能拷贝好几次,就不止O(n^2)了。 改成链表之后,C++代码如下: @@ -156,21 +143,23 @@ public: int position = people[i][1]; // 插入到下标为position的位置 std::list>::iterator it = que.begin(); while (position--) { // 寻找在插入位置 - it++; + it++; } - que.insert(it, people[i]); + que.insert(it, people[i]); } return vector>(que.begin(), que.end()); } }; ``` -* 时间复杂度O(nlogn + n^2) +* 时间复杂度O(nlogn + n^2) * 空间复杂度O(n) 大家可以把两个版本的代码提交一下试试,就可以发现其差别了! -# 总结 +关于本题使用数组还是使用链表的性能差异,我在[贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ)中详细讲解了一波 + +## 总结 关于出现两个维度一起考虑的情况,我们已经做过两道题目了,另一道就是[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ)。 @@ -182,20 +171,32 @@ public: **对使用某一种语言容器的使用,特性的选择都会不同程度上影响效率**。 -所以很多人都说写算法题用什么语言都可以,主要体现在算法思维上,其实我是同意的但也不同意。 +所以很多人都说写算法题用什么语言都可以,主要体现在算法思维上,其实我是同意的但也不同意。 对于看别人题解的同学,题解用什么语言其实影响不大,只要题解把所使用语言特性优化的点讲出来,大家都可以看懂,并使用自己语言的时候注意一下。 对于写题解的同学,刷题用什么语言影响就非常大,如果自己语言没有学好而强调算法和编程语言没关系,其实是会误伤别人的。 -**这也是我为什么统一使用C++写题解的原因**,其实用其他语言java、python、php、go啥的,我也能写,我的Github上也有用这些语言写的小项目,但写题解的话,我就不能保证把语言特性这块讲清楚,所以我始终坚持使用最熟悉的C++写题解。 +**这也是我为什么统一使用C++写题解的原因**,其实用其他语言java、python、php、go啥的,我也能写,我的Github上也有用这些语言写的小项目,但写题解的话,我就不能保证把语言特性这块讲清楚,所以我始终坚持使用最熟悉的C++写题解。 **而且我在写题解的时候涉及语言特性,一般都会后面加上括号说明一下。没办法,认真负责就是我,哈哈**。 -就酱,「代码随想录」一直都是干货满满,值得介绍给身边的朋友同学们! +## 其他语言版本 -**循序渐进学算法,认准「代码随想录」,Carl手把手带你过关斩将!** -

- -

+Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0416.分割等和子集.md b/problems/0416.分割等和子集.md index 9cf5d0c2..257c4d7d 100644 --- a/problems/0416.分割等和子集.md +++ b/problems/0416.分割等和子集.md @@ -1,60 +1,144 @@ +

+ + + + +

+

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

+# 动态规划:分割等和子集可以用01背包! +## 416. 分割等和子集 -* 473. 火柴拼正方形 (回溯算法) -* 698. 划分为k个相等的子集 +题目链接:https://leetcode-cn.com/problems/partition-equal-subset-sum/ -一起再回忆一下回溯算法 +题目难易:中等 -|[0473.火柴拼正方形](https://github.com/youngyangyang04/leetcode/blob/master/problems/0473.火柴拼正方形.md) |深度优先搜索|中等| **回溯算法** 和698.划分为k个相等的子集差不多| +给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +注意: +每个数组中的元素不会超过 100 +数组的大小不会超过 200 + +示例 1: +输入: [1, 5, 11, 5] +输出: true +解释: 数组可以分割成 [1, 5, 5] 和 [11]. +  +示例 2: +输入: [1, 2, 3, 5] +输出: false +解释: 数组不能分割成两个元素和相等的子集. ## 思路 +这道题目初步看,是如下两题几乎是一样的,大家可以用回溯法,解决如下两题 + +* 698.划分为k个相等的子集 +* 473.火柴拼正方形 + 这道题目是要找是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。 -那么只要找到集合里能够出现 sum / 2 的集合,就算是可以分割成两个相同元素和子集了。 +那么只要找到集合里能够出现 sum / 2 的子集总和,就算是可以分割成两个相同元素和子集了。 -本来是我是想用回溯暴力搜索出所有答案的,各种剪枝,还是超时了,不想在调了,放弃回溯,直接上01背包吧。 +本题是可以用回溯暴力搜索出所有答案的,但最后超时了,也不想再优化了,放弃回溯,直接上01背包吧。 -如下的讲解中,我讲的重点是如何把01背包应用到此题,而不是讲01背包,如果对01背包本身还不理解的同学,需要额外学习一下基础知识,我后面也会在[代码随想录](https://img-blog.csdnimg.cn/20200815195519696.png)里深度讲解背包问题。 +如果对01背包不够了解,建议仔细看完如下两篇: -### 背包问题 +* [动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA) -背包问题,大家都知道,就是书包,书包可以容纳的体积n, 然后有各种商品,每一种商品体积为m,价值为z,问如果把书包塞满(不一定能塞满),书包里的商品最大价值总和是多少。 +## 01背包问题 + +背包问题,大家都知道,有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。 **背包问题有多种背包方式,常见的有:01背包、完全背包、多重背包、分组背包和混合背包等等。** -要注意背包问题问题中商品是不是可以重复放入。 +要注意题目描述中商品是不是可以重复放入。 **即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。** **要明确本题中我们要使用的是01背包,因为元素我们只能用一次。** -为了让大家对背包问题有一个整体的了解,可以看如下图: - - - 回归主题:首先,本题要求集合里能否出现总和为 sum / 2 的子集。 那么来一一对应一下本题,看看背包问题如果来解决。 -**只有确定了如下四点,才能把背包问题,套到本题上来。** +**只有确定了如下四点,才能把01背包问题套到本题上来。** -* 背包的体积为sum / 2 -* 背包要放入的商品(集合里的元素)体积为 元素的数值,价值也为元素的数值 +* 背包的体积为sum / 2 +* 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值 * 背包如何正好装满,说明找到了总和为 sum / 2 的子集。 -* 背包中每一个元素一定是不可重复放入。 +* 背包中每一个元素是不可重复放入。 + +以上分析完,我们就可以套用01背包,来解决这个问题了。 + +动规五部曲分析如下: + +1. 确定dp数组以及下标的含义 + +01背包中,dp[i] 表示: 容量为j的背包,所背的物品价值可以最大为dp[j]。 + +**套到本题,dp[i]表示 背包总容量是i,最大可以凑成i的子集总和为dp[i]**。 + +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]); -定义里数组为dp[],dp[i] 表示 背包中放入体积为i的商品,最大价值为dp[i]。 +3. dp数组如何初始化 -套到本题,dp[i]表示 背包中总和是i,最大可以凑成总和为i的元素总和为dp[i]。 +在01背包,一维dp如何初始化,已经讲过, -dp[i]一定是小于等于i的,因为背包不能装入超过自身体积的商品(这里理解为元素数值)。 +从dp[j]的定义来看,首先dp[0]一定是0。 -**如果dp[i] == i 说明,集合中的元素正好可以凑成总和i,理解这一点很重要。** +如果如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,如果题目给的价值有负数,那么非0下标就要初始化为负无穷。 -## C++代码如下(详细注释 ) +**这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了**。 + +本题题目中 只包含正整数的非空数组,所以非0下标的元素初始化为0就可以了。 + +代码如下: + +```C++ +// 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 +// 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了 +vector dp(10001, 0); ``` + +4. 确定遍历顺序 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒叙遍历! + +代码如下: + +```C++ +// 开始 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[i]的数值一定是小于等于i的。 + +**如果dp[i] == i 说明,集合中的子集总和正好可以凑成总和i,理解这一点很重要。** + +用例1,输入[1,5,11,5] 为例,如图: + +![416.分割等和子集2](https://img-blog.csdnimg.cn/20210110104240545.png) + +最后dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。 + +综上分析完毕,C++代码如下: + +```C++ class Solution { public: bool canPartition(vector& nums) { @@ -62,109 +146,57 @@ public: // dp[i]中的i表示背包内总和 // 题目中说:每个数组中的元素不会超过 100,数组的大小不会超过 200 - // 那么背包内总和不会大于20000,所以定义一个20000大的数组。 - vector dp(20001, 0); + // 总和不会大于20000,背包最大只需要其中一半,所以10001大小就可以了 + vector dp(10001, 0); for (int i = 0; i < nums.size(); i++) { sum += nums[i]; } if (sum % 2 == 1) return false; int target = sum / 2; - // 开始 01背包 + // 开始 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 + // 集合中的元素正好可以凑成总和target if (dp[target] == target) return true; - return false; - } -}; -``` - -### 暴力 - -本来是想用回溯暴力搜索出所有答案的,各种剪枝,还是超时了,不想在调了,放弃回溯,直接上01背包吧。 - -**需要尝试一下记忆化递归** - -回溯搜索超时的代码如下: - -``` -class Solution { -private: - int target; - bool backtracking(vector& nums, int startIndex, int pathSum, vector& used) { - for (int i = startIndex; i < nums.size(); i++) { - if (pathSum > target) break; // 剪枝 - if (target < nums[i]) break; // 剪枝 - if (i > 0 && nums[i] == nums[i - 1] && used[i - 1] == false) { // 去重 - continue; - } - pathSum += nums[i]; - used[i] = true; - if (pathSum == target) return true; - if (backtracking(nums, i + 1, pathSum, used)) return true; - used[i] = false; - pathSum -= nums[i]; - } return false; } - -public: - bool canPartition(vector& nums) { - vector used(nums.size(), false); - sort(nums.begin(), nums.end()); - int sum = 0; - for (int i = 0; i < nums.size(); i++) sum += nums[i]; - if (sum % 2 == 1) return false; - target = sum / 2; - cout << "sum:" << sum << " target:" << target << endl; - return backtracking(nums, 0, 0, used); - } -}; - -``` - -``` -class Solution { -private: - bool backtracking(vector& nums, - int k, - int target, // 子集目标和 - int cur, // 当前目标和 - int startIndex, // 起始位置 - vector& used) { // 标记是否使用过 - if (k == 0) return true; // 找到了k个相同子集 - if (cur == target) { // 发现一个合格子集,然后重新开始寻找 - return backtracking(nums, k - 1, target, 0, 0, used); // k-1 - } - for (int i = startIndex; i < nums.size(); i++) { - if (cur + nums[i] <= target && !used[i]) { - used[i] = true; - if (backtracking(nums, k, target, cur + nums[i], i + 1, used)) { - return true; - } - used[i] = false; - } - } - return false; - } - -public: - bool canPartition(vector& nums) { - if (nums.size() < 2) return false; // 火柴数量小于4凑不上正方形 - int sum = 0; - for (int i = 0; i < nums.size(); i++) { - sum += nums[i]; - } - if (sum % 2 != 0) return false; - int target = sum / 2; - vector used(nums.size(), false); - - return backtracking(nums, 2, target, 0, 0, used); - - } }; ``` + +* 时间复杂度:O(n^2) +* 空间复杂度:O(n),虽然dp数组大小为一个常数,但是大常数 + +## 总结 + +这道题目就是一道01背包应用类的题目,需要我们拆解题目,然后套入01背包的场景。 + +01背包相对于本题,主要要理解,题目中物品是nums[i],重量是nums[i]i,价值也是nums[i],背包体积是sum/2。 + +看代码的话,就可以发现,基本就是按照01背包的写法来的。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0429.N叉树的层序遍历.md b/problems/0429.N叉树的层序遍历.md deleted file mode 100644 index 0b495446..00000000 --- a/problems/0429.N叉树的层序遍历.md +++ /dev/null @@ -1,37 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/sum-of-left-leaves/ - -## 思路 - -层序遍历,和 102题目一个套路 - -## C++代码 - -### 递归法 - -``` -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++) { // 这里一定要使用固定大小size,不要使用que.size() - 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; - - } -}; -``` diff --git a/problems/0434.字符串中的单词数.md b/problems/0434.字符串中的单词数.md deleted file mode 100644 index 14adc189..00000000 --- a/problems/0434.字符串中的单词数.md +++ /dev/null @@ -1,30 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/number-of-segments-in-a-string/ - -## 思路 - -可以把问题抽象为 遇到s[i]是空格,s[i+1]不是空格,就统计一个单词, 然后特别处理一下第一个字符不是空格的情况 - -## C++代码 - -``` -class Solution { -public: - int countSegments(string s) { - int count = 0; - for (int i = 0; i < s.size(); i++) { - // 第一个字符不是空格的情况 - if (i == 0 && s[i] != ' ') { - count++; - } - // 只要s[i]是空格,s[i+1]不是空格,count就加1 - if (i + 1 < s.size() && s[i] == ' ' && s[i + 1] != ' ') { - count++; - } - } - return count; - } -}; -``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0435.无重叠区间.md b/problems/0435.无重叠区间.md index fcb2a9af..3df496f4 100644 --- a/problems/0435.无重叠区间.md +++ b/problems/0435.无重叠区间.md @@ -1,22 +1,64 @@ +

+ + + + +

+

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

-## 思路 -这道题目如果真的去模拟去重复区间的行为,是非常麻烦的,还要有删除区间。 +## 435. 无重叠区间 + +题目链接:https://leetcode-cn.com/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 +解释: 你不需要移除任何区间,因为它们已经是无重叠的了。 + +## 思路 **相信很多同学看到这道题目都冥冥之中感觉要排序,但是究竟是按照右边界排序,还是按照左边界排序呢?** -按照右边界排序,从左向右遍历,右边界越小越好,因为右边界越小,留给下一个区间的空间就越大,所以可以从左向右遍历,优先选右边界小的。 +这其实是一个难点! -按照左边界排序,那么就是从右向左遍历,左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。 +按照右边界排序,就要从左向右遍历,因为右边界越小越好,只要右边界越小,留给下一个区间的空间就越大,所以从左向右遍历,优先选右边界小的。 -如果按照左边界排序,还从左向右遍历的话,要处理各个区间右边界的各种情况,就比较复杂了,这其实也就不是贪心了。 +按照左边界排序,就要从右向左遍历,因为左边界数值越大越好(越靠右),这样就给前一个区间的空间就越大,所以可以从右向左遍历。 +如果按照左边界排序,还从左向右遍历的话,其实也可以,逻辑会有所不同。 + +一些同学做这道题目可能真的去模拟去重复区间的行为,这是比较麻烦的,还要去删除区间。 + +题目只是要求移除区间的个数,没有必要去真实的模拟删除区间! **我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了**。 +此时问题就是要求非交叉区间的最大个数。 + +右边界排序之后,局部最优:优先选右边界小的区间,所以从左向右遍历,留给下一个区间的空间大一些,从而尽量避免交叉。全局最优:选取最多的非交叉区间。 + +局部最优推出全局最优,试试贪心! + 这里记录非交叉区间的个数还是有技巧的,如图: - +![435.无重叠区间](https://img-blog.csdnimg.cn/20201221201553618.png) 区间,1,2,3,4,5,6都按照右边界排好序。 @@ -24,9 +66,9 @@ 接下来就是找大于区间1结束位置的区间,是从区间4开始。**那有同学问了为什么不从区间5开始?别忘已经是按照右边界排序的了**。 -区间4结束之后,在找到区间6,所以一共记录非交叉区间的个数是三个。 +区间4结束之后,在找到区间6,所以一共记录非交叉区间的个数是三个。 -总共区间个数为6,减去非交叉区间的个数(3),为3。移除区间的最小数量就是3。 +总共区间个数为6,减去非交叉区间的个数3。移除区间的最小数量就是3。 C++代码如下: @@ -41,7 +83,7 @@ public: if (intervals.size() == 0) return 0; sort(intervals.begin(), intervals.end(), cmp); int count = 1; // 记录非交叉区间的个数 - int end = intervals[0][1]; + int end = intervals[0][1]; // 记录区间分割点 for (int i = 1; i < intervals.size(); i++) { if (end <= intervals[i][0]) { end = intervals[i][1]; @@ -52,6 +94,106 @@ public: } }; ``` +* 时间复杂度:O(nlogn) ,有一个快排 +* 空间复杂度:O(1) -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[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),期待你的关注! +大家此时会发现如此复杂的一个问题,代码实现却这么简单! +## 总结 + +本题我认为难度级别可以算是hard级别的! + +总结如下难点: + +* 难点一:一看题就有感觉需要排序,但究竟怎么排序,按左边界排还是右边界排。 +* 难点二:排完序之后如何遍历,如果没有分析好遍历顺序,那么排序就没有意义了。 +* 难点三:直接求重复的区间是复杂的,转而求最大非重复区间个数。 +* 难点四:求最大非重复区间个数时,需要一个分割点来做标记。 + +**这四个难点都不好想,但任何一个没想到位,这道题就解不了**。 + +一些录友可能看网上的题解代码很简单,照葫芦画瓢稀里糊涂的就过了,但是其题解可能并没有把问题难点讲清楚,然后自己再没有钻研的话,那么一道贪心经典区间问题就这么浪费掉了。 + +贪心就是这样,代码有时候很简单(不是指代码短,而是逻辑简单),但想法是真的难! + +这和动态规划还不一样,动规的代码有个递推公式,可能就看不懂了,而贪心往往是直白的代码,但想法读不懂,哈哈。 + +**所以我把本题的难点也一一列出,帮大家不仅代码看的懂,想法也理解的透彻!** + +## 补充 + +本题其实和[贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw)非常像,弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。 + +把[贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw)代码稍做修改,就可以AC本题。 + +```C++ +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,具体原因我还没有仔细看,后面有空再补充。 +```C++ +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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0450.删除二叉搜索树中的节点.md b/problems/0450.删除二叉搜索树中的节点.md index eb607879..ed0cb9d7 100644 --- a/problems/0450.删除二叉搜索树中的节点.md +++ b/problems/0450.删除二叉搜索树中的节点.md @@ -1,11 +1,17 @@ -## 题目地址 -https://leetcode-cn.com/problems/delete-node-in-a-bst/ +

+ + + + +

+

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

+ > 二叉搜索树删除节点就涉及到结构调整了 -# 450.删除二叉搜索树中的节点 +## 450.删除二叉搜索树中的节点 -题目链接: https://leetcode-cn.com/problems/delete-node-in-a-bst/ +题目链接: https://leetcode-cn.com/problems/delete-node-in-a-bst/ 给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。 @@ -19,40 +25,40 @@ https://leetcode-cn.com/problems/delete-node-in-a-bst/ ![450.删除二叉搜索树中的节点](https://img-blog.csdnimg.cn/20201020171048265.png) -# 思路 +## 思路 搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑,做好心里准备。 -## 递归 +## 递归 递归三部曲: -* 确定递归函数参数以及返回值 +* 确定递归函数参数以及返回值 说道递归函数的返回值,在[二叉树:搜索树中的插入操作](https://mp.weixin.qq.com/s/lwKkLQcfbCNX2W-5SOeZEA)中通过递归返回值来加入新节点, 这里也可以通过递归返回值删除节点。 代码如下: ``` -TreeNode* deleteNode(TreeNode* root, int key) +TreeNode* deleteNode(TreeNode* root, int key) ``` -* 确定终止条件 +* 确定终止条件 遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了 ``` -if (root == nullptr) return root; +if (root == nullptr) return root; ``` -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 这里就把平衡二叉树中删除节点遇到的情况都搞清楚。 有以下五种情况: -* 第一种情况:没找到删除的节点,遍历到空节点直接返回了 -* 找到删除的节点 +* 第一种情况:没找到删除的节点,遍历到空节点直接返回了 +* 找到删除的节点 * 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 * 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点 * 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 @@ -60,6 +66,7 @@ if (root == nullptr) return root; 第五种情况有点难以理解,看下面动画: +![450.删除二叉搜索树中的节点](https://tva1.sinaimg.cn/large/008eGmZEly1gnbj3k596mg30dq0aigyz.gif) 动画中颗二叉搜索树中,删除元素7, 那么删除节点(元素7)的左孩子就是5,删除节点(元素7)的右子树的最左面节点是元素8。 @@ -72,18 +79,18 @@ if (root == nullptr) return root; 代码如下: -``` +```C++ if (root->val == key) { // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点 - if (root->left == nullptr) return root->right; + if (root->left == nullptr) return root->right; // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 - else if (root->right == nullptr) return root->left; + else if (root->right == nullptr) return root->left; // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置 // 并返回删除节点右孩子为新的根节点。 - else { + else { TreeNode* cur = root->right; // 找右子树最左面的节点 - while(cur->left != nullptr) { + while(cur->left != nullptr) { cur = cur->left; } cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置 @@ -105,7 +112,7 @@ return root; **整体代码如下:(注释中:情况1,2,3,4,5和上面分析严格对应)** -``` +```C++ class Solution { public: TreeNode* deleteNode(TreeNode* root, int key) { @@ -113,14 +120,14 @@ public: if (root->val == key) { // 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点 // 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点 - if (root->left == nullptr) return root->right; + if (root->left == nullptr) return root->right; // 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点 - else if (root->right == nullptr) return root->left; + else if (root->right == nullptr) return root->left; // 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置 // 并返回删除节点右孩子为新的根节点。 - else { + else { TreeNode* cur = root->right; // 找右子树最左面的节点 - while(cur->left != nullptr) { + while(cur->left != nullptr) { cur = cur->left; } cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置 @@ -137,7 +144,7 @@ public: }; ``` -### 普通二叉树的删除方式 +## 普通二叉树的删除方式 这里我在介绍一种通用的删除,普通二叉树的删除方式(没有使用搜索树的特性,遍历整棵树),用交换值的操作来删除目标节点。 @@ -150,14 +157,14 @@ public: 代码如下:(关键部分已经注释) -``` +```C++ 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; + return root->left; } TreeNode *cur = root->right; while (cur->left) { @@ -174,13 +181,13 @@ public: 这个代码是简短一些,思路也巧妙,但是不太好想,实操性不强,推荐第一种写法! -## 迭代法 +## 迭代法 删除节点的迭代法还是复杂一些的,但其本质我在递归法里都介绍了,最关键就是删除节点的操作(动画模拟的过程) 代码如下: -``` +```C++ class Solution { private: // 将目标节点(删除节点)的左子树放到 目标节点的右子树的最左面节点的左孩子位置上 @@ -222,13 +229,13 @@ public: }; ``` -# 总结 +## 总结 读完本篇,大家会发现二叉搜索树删除节点比增加节点复杂的多。 **因为二叉搜索树添加节点只需要在叶子上添加就可以的,不涉及到结构的调整,而删除节点操作涉及到结构的调整**。 -这里我们依然使用递归函数的返回值来完成把节点从二叉树中移除的操作。 +这里我们依然使用递归函数的返回值来完成把节点从二叉树中移除的操作。 **这里最关键的逻辑就是第五种情况(删除一个左右孩子都不为空的节点),这种情况一定要想清楚**。 @@ -240,12 +247,22 @@ public: 迭代法其实不太容易写出来,所以如果是初学者的话,彻底掌握第一种递归写法就够了。 -**就酱,又是干货满满的一篇,大家加油!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: - - - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +----------------------- +* 作者微信:[程序员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/0452.用最少数量的箭引爆气球.md b/problems/0452.用最少数量的箭引爆气球.md index e2034bbe..7372ea92 100644 --- a/problems/0452.用最少数量的箭引爆气球.md +++ b/problems/0452.用最少数量的箭引爆气球.md @@ -1,33 +1,80 @@ +

+ + + + +

+

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

-# 思路 -接下来在想一下如何使用最少的弓箭。 +## 452. 用最少数量的箭引爆气球 -直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢? +题目链接:https://leetcode-cn.com/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 + +## 思路 + +如何使用最少的弓箭呢? + +直觉上来看,貌似只射重叠最多的气球,用的弓箭一定最少,那么有没有当前重叠了三个气球,我射两个,留下一个和后面的一起射这样弓箭用的更少的情况呢? + +尝试一下举反例,发现没有这种情况。 + +那么就试一试贪心吧!局部最优:当气球出现重叠,一起射,所用弓箭最少。全局最优:把所有气球射爆所用弓箭最少。 + +**算法确定下来了,那么如何模拟气球射爆的过程呢?是在数组中移除元素还是做标记呢?** 如果真实的模拟射气球的过程,应该射一个,气球数组就remove一个元素,这样最直观,毕竟气球被射了。 -但又想一下,如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remove气球,记录一下箭的数量就可以了。 - -> PS:本文[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),关注后就会发现和「代码随想录」相见恨晚! +但仔细思考一下就发现:如果把气球排序之后,从前到后遍历气球,被射过的气球仅仅跳过就行了,没有必要让气球数组remote气球,只要记录一下箭的数量就可以了。 以上为思考过程,已经确定下来使用贪心了,那么开始解题。 **为了让气球尽可能的重叠,需要对数组进行排序**。 -那么按照气球起始位置排序,还是按照气球终止位置排序呢? +那么按照气球起始位置排序,还是按照气球终止位置排序呢? 其实都可以!只不过对应的遍历顺序不同,我就按照气球的起始位置排序了。 -既然按照其实位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。 +既然按照其实位置排序,那么就从前向后遍历气球数组,靠左尽可能让气球重复。 -从前向后遍历遇到重叠的气球了怎么办? +从前向后遍历遇到重叠的气球了怎么办? -如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭。 +**如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭**。 以题目示例: [[10,16],[2,8],[1,6],[7,12]]为例,如图:(方便起见,已经排序) @@ -62,17 +109,48 @@ public: }; ``` -时间复杂度O(nlogn) -空间复杂度O(1) +* 时间复杂度O(nlogn),因为有一个快排 +* 空间复杂度O(1) -# 注意事项 +可以看出代码并不复杂。 + +## 注意事项 注意题目中说的是:满足 xstart ≤ x ≤ xend,则该气球会被引爆。那么说明两个气球挨在一起不重叠也可以一起射爆, 所以代码中 `if (points[i][0] > points[i - 1][1])` 不能是>= +## 总结 -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** +这道题目贪心的思路很简单也很直接,就是重复的一起射了,但本题我认为是有难度的。 -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +就算思路都想好了,模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了。 +而且寻找重复的气球,寻找重叠气球最小右边界,其实都有代码技巧。 + +贪心题目有时候就是这样,看起来很简单,思路很直接,但是一写代码就感觉贼复杂无从下手。 + +这里其实是需要代码功底的,那代码功底怎么练? + +**多看多写多总结!** + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0454.四数相加II.md b/problems/0454.四数相加II.md index 7695d7ec..71dcc1ae 100644 --- a/problems/0454.四数相加II.md +++ b/problems/0454.四数相加II.md @@ -1,10 +1,18 @@ -# 题目地址 -https://leetcode-cn.com/problems/4sum-ii/ +

+ + + + +

+

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

+ > 需要哈希的地方都能找到map的身影 # 第454题.四数相加II +https://leetcode-cn.com/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 。 @@ -26,13 +34,13 @@ D = [ 0, 2] 2. (1, 1, 0, 0) -> A[1] + B[1] + C[0] + D[0] = 2 + (-1) + (-1) + 0 = 0 -# 思路 +# 思路 -本题咋眼一看好像和[0015.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A),[0018.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)差不多,其实差很多。 +本题咋眼一看好像和[0015.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A),[0018.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)差不多,其实差很多。 **本题是使用哈希法的经典题目,而[0015.三数之和](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A),[0018.四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g)并不合适使用哈希法**,因为三数之和和四数之和这两道题目使用哈希法在不超时的情况下做到对结果去重是很困难的,很有多细节需要处理。 -**而这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!** +**而这道题目是四个独立的数组,只要找到A[i] + B[j] + C[k] + D[l] = 0就可以,不用考虑有重复的四个元素相加等于0的情况,所以相对于题目18. 四数之和,题目15.三数之和,还是简单了不少!** 如果本题想难度升级:就是给出一个数组(而不是四个数组),在这里找出四个元素相加等于0,答案中不可以包含重复的四元组,大家可以思考一下,后续的文章我也会讲到的。 @@ -44,14 +52,14 @@ D = [ 0, 2] 4. 在遍历大C和大D数组,找到如果 0-(c+d) 在map中出现过的话,就用count把map中key对应的value也就是出现次数统计出来。 5. 最后返回统计值 count 就可以了 -# C++代码 +C++代码: -``` +```C++ 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中 + // 遍历大A和大B数组,统计两个数组元素之和,和出现的次数,放到map中 for (int a : A) { for (int b : B) { umap[a + b]++; @@ -72,5 +80,26 @@ public: ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0455.分发饼干.md b/problems/0455.分发饼干.md index d86e775d..ca3eba74 100644 --- a/problems/0455.分发饼干.md +++ b/problems/0455.分发饼干.md @@ -1,8 +1,13 @@ -> 贪心的第一道题目,快看看你够不够贪心 +

+ + + + +

+

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

-通知:一些录友表示经常看不到每天的文章,现在公众号已经不按照发送时间推荐了,而是根据一些规则乱序推送,所以可能关注了「代码随想录」也一直看不到文章,建议把「代码随想录」设置星标哈,设置星标之后,每天就按发文时间推送了,我每天都是定时8:35发送的,嗷嗷准时! -# 455.分发饼干 +## 455.分发饼干 题目链接:https://leetcode-cn.com/problems/assign-cookies/ @@ -10,21 +15,21 @@ 对每个孩子 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。 +示例 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. +示例 2: +输入: g = [1,2], s = [1,2,3] +输出: 2 +解释: +你有两个孩子和三块小饼干,2个孩子的胃口值分别是1,2。 +你拥有的饼干数量和尺寸都足以让所有孩子满足。 +所以你应该输出2.   提示: @@ -33,7 +38,7 @@ * 1 <= g[i], s[j] <= 2^31 - 1 -## 思路 +## 思路 为了了满足更多的小孩,就不要造成饼干尺寸的浪费。 @@ -54,7 +59,7 @@ C++代码整体如下: -``` +```C++ // 时间复杂度:O(nlogn) // 空间复杂度:O(1) class Solution { @@ -79,39 +84,49 @@ public: 有的同学看到要遍历两个数组,就想到用两个for循环,那样逻辑其实就复杂了。 -**也可以换一个思路,小饼干先喂饱小胃口** +**也可以换一个思路,小饼干先喂饱小胃口** 代码如下: -``` +```C++ class Solution { public: int findContentChildren(vector& g, vector& s) { sort(g.begin(),g.end()); sort(s.begin(),s.end()); - int res = 0; int index = 0; for(int i = 0;i < s.size();++i){ if(index < g.size() && g[index] <= s[i]){ index++; - res++; } } - return res; + return index; } }; ``` -# 总结 +## 总结 这道题是贪心很好的一道入门题目,思路还是比较容易想到的。 文中详细介绍了思考的过程,**想清楚局部最优,想清楚全局最优,感觉局部最优是可以推出全局最优,并想不出反例,那么就试一试贪心**。 -就酱,「代码随想录」值得介绍给身边的朋友同学们! - -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0459.重复的子字符串.md b/problems/0459.重复的子字符串.md index da163490..5abc999a 100644 --- a/problems/0459.重复的子字符串.md +++ b/problems/0459.重复的子字符串.md @@ -1,28 +1,37 @@ +

+ + + + +

+

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

+ + -## 题目地址 -https://leetcode-cn.com/problems/repeated-substring-pattern/ > KMP算法还能干这个 -# 题目459.重复的子字符串 +# 459.重复的子字符串 + +https://leetcode-cn.com/problems/repeated-substring-pattern/ 给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。 -示例 1: -输入: "abab" -输出: True -解释: 可由子字符串 "ab" 重复两次构成。 +示例 1: +输入: "abab" +输出: True +解释: 可由子字符串 "ab" 重复两次构成。 -示例 2: -输入: "aba" -输出: False +示例 2: +输入: "aba" +输出: False -示例 3: -输入: "abcabcabcabc" -输出: True -解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。) +示例 3: +输入: "abcabcabcabc" +输出: True +解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。) -# 思路 +# 思路 这又是一道标准的KMP的题目。 @@ -40,30 +49,32 @@ https://leetcode-cn.com/problems/repeated-substring-pattern/ 这里就要说一说next数组了,next 数组记录的就是最长相同前后缀( [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) 这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。 -最长相等前后缀的长度为:next[len - 1] + 1。 +最长相等前后缀的长度为:next[len - 1] + 1。 -数组长度为:len。 +数组长度为:len。 如果len % (len - (next[len - 1] + 1)) == 0 ,则说明 (数组长度-最长相等前后缀的长度) 正好可以被 数组的长度整除,说明有该字符串有重复的子字符串。 +**数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。** + + **强烈建议大家把next数组打印出来,看看next数组里的规律,有助于理解KMP算法** 如图: - +![459.重复的子字符串_1](https://code-thinking.cdn.bcebos.com/pics/459.%E9%87%8D%E5%A4%8D%E7%9A%84%E5%AD%90%E5%AD%97%E7%AC%A6%E4%B8%B2_1.png) -此时next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时 字符串asdfasdfasdf的最长相同前后缀的长度。 +next[len - 1] = 7,next[len - 1] + 1 = 8,8就是此时字符串asdfasdfasdf的最长相同前后缀的长度。 (len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。 -代码如下: -# C++代码 -``` +代码如下:(这里使用了前缀表统一减一的实现方式) + +```C++ class Solution { public: - // KMP里标准构建next数组的过程 void getNext (int* next, const string& s){ next[0] = -1; int j = -1; @@ -92,12 +103,12 @@ public: }; ``` -# 前缀表不右移 C++代码 -``` +前缀表(不减一)的代码实现 + +```C++ class Solution { public: - // KMP里标准构建next数组的过程 void getNext (int* next, const string& s){ next[0] = 0; int j = 0; @@ -126,7 +137,7 @@ public: }; ``` -# 拓展 +# 拓展 此时我们已经分享了三篇KMP的文章,首先是[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)讲解KMP算法的基础理论,给出next数组究竟是如何来了,前缀表又是怎么回事,为什么要选择前缀表。 @@ -134,5 +145,24 @@ public: 后来很多同学反馈说:搞不懂前后缀,什么又是最长相同前后缀(最长公共前后缀我认为这个用词不准确),以及为什么前缀表要统一减一(右移)呢,不减一行不行?针对这些问题,我在[字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ)中又给出了详细的讲解。 -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0463.岛屿的周长.md b/problems/0463.岛屿的周长.md deleted file mode 100644 index b2a40d8c..00000000 --- a/problems/0463.岛屿的周长.md +++ /dev/null @@ -1,80 +0,0 @@ -## 题目链接 -https://leetcode-cn.com/problems/island-perimeter/ - -## 思路 - -岛屿问题最容易让人想到BFS或者DFS,但是这道题还真的没有必要,别把简单问题搞复杂了。 - -### 解法一: - -遍历每一个空格,遇到岛屿,计算其上下左右的情况,遇到水域或者出界的情况,就可以计算边了。 - -如图: - - - -C++代码如下:(详细注释) - -```C++ -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++代码如下:(详细注释) - -```C++ -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; - } -}; -``` - diff --git a/problems/0473.火柴拼正方形.md b/problems/0473.火柴拼正方形.md deleted file mode 100644 index fccfedfb..00000000 --- a/problems/0473.火柴拼正方形.md +++ /dev/null @@ -1,42 +0,0 @@ - -698.划分为k个相等的子集 的代码几乎不用改动,就可以AC -``` -class Solution { -private: - bool backtracking(vector& nums, - int k, - int target, // 子集目标和 - int cur, // 当前目标和 - int startIndex, // 起始位置 - vector& used) { // 标记是否使用过 - if (k == 0) return true; // 找到了k个相同子集 - if (cur == target) { // 发现一个合格子集,然后重新开始寻找 - return backtracking(nums, k - 1, target, 0, 0, used); // k-1 - } - for (int i = startIndex; i < nums.size(); i++) { - if (cur + nums[i] <= target && !used[i]) { - used[i] = true; - if (backtracking(nums, k, target, cur + nums[i], i + 1, used)) { - return true; - } - used[i] = false; - } - } - return false; - } -public: - bool makesquare(vector& nums) { - if (nums.size() < 4) return false; // 火柴数量小于4凑不上正方形 - int sum = 0; - for (int i = 0; i < nums.size(); i++) { - sum += nums[i]; - } - if (sum % 4 != 0) return false; - int target = sum / 4; - vector used(nums.size(), false); - - return backtracking(nums, 4, target, 0, 0, used); - - } -}; -``` diff --git a/problems/0474.一和零.md b/problems/0474.一和零.md index 29a81a0a..7970c38e 100644 --- a/problems/0474.一和零.md +++ b/problems/0474.一和零.md @@ -1,33 +1,178 @@ +

+ + + + +

+

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

+# 动态规划:一和零! -// 即使做了很多动态规划的题目,做这个依然懵逼 -// 这道题目有点 程序员自己给自己出难进急转弯的意思 -// 该子集中 最多 有 m 个 0 和 n 个 1 。 指的是整体子集 -// 这是二维背包,多重背包 -// dp[i][j] 有i个0,j个1最大有多少个子集,但是遍历的时候 顶部是哪里呢? +## 474.一和零 -搞不懂 leetcode后台是什么牛逼的编译器,初始化int dp[101][101] = {0}; 可以 ,int dp[101][101];就不行,有其他默认值,坑死。 -代码我做了实验,后台会拿findMaxForm,运行两次,取第二次的结果,dp有上次记录的数值。 +题目链接:https://leetcode-cn.com/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 + +## 思路 + +这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢哈哈。 + +来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。 + +其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系 + +![416.分割等和子集1](https://img-blog.csdnimg.cn/20210117171307407.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://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中已经讲解了,01背包的dp数组初始化为0就可以。 + +因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。 + +4. 确定遍历顺序 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中,我们讲到了01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历! + +那么本题也是,物品就是strs里的字符串,背包容量就是题目描述中的m和n。 + +代码如下: +```C++ +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://img-blog.csdnimg.cn/20210120111201512.jpg) + + +以上动规五部曲分析完毕,C++代码如下: + +```C++ class Solution { public: int findMaxForm(vector& strs, int m, int n) { - int dp[101][101] = {0}; // 默认初始化0 - for (int i = 0; i < strs.size(); i++) { + vector> dp(m + 1, vector (n + 1, 0)); // 默认初始化0 + for (string str : strs) { // 遍历物品 int oneNum = 0, zeroNum = 0; - for (char c : strs[i]) { + for (char c : str) { if (c == '0') zeroNum++; else oneNum++; } - // 果然还是从后向前,模拟01背包 - for (int j = m; j >= zeroNum; j--) { - for (int k = n; k >= oneNum; k--) { - dp[j][k] = max(dp[j][k], dp[j - zeroNum][k - oneNum] + 1); + 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]; } }; - ``` + +## 总结 + +不少同学刷过这道提,可能没有总结这究竟是什么背包。 + +这道题的本质是有两个维度的01背包,如果大家认识到这一点,对这道题的理解就比较深入了。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0486.预测赢家.md b/problems/0486.预测赢家.md deleted file mode 100644 index 7b0e083c..00000000 --- a/problems/0486.预测赢家.md +++ /dev/null @@ -1,279 +0,0 @@ -## 题目地址 - -## 思路 - -在做这道题目的时候,最直接的想法,就是计算出player1可能得到的最大分数,然后用数组总和减去player1的得分就是player2的得分,然后两者比较一下就可以了。 - -那么问题是如何计算player1可能得到的最大分数呢。 - -## 单独计算玩家得分 - -以player1选数字的过程,画图如下: - - - -可以发现是一个递归的过程。 - -按照递归三部曲来: - -1. 确定递归函数的含义,参数以及返回值。 - -定义函数getScore,就是用来获取玩家1的最大得分。 参数为start 和 end 代表获取[start, end]这个区间的最大值,当然还需要传入nums。 - -返回值就是玩家1的最大得分。 - -代码如下: - -``` -int getScore(vector& nums, int start, int end) { -``` - - -2. 确定终止条件 - -当start == end的时候,玩家A的得分就是nums[start],代码如下: -``` - if (start == end) { - return nums[start]; - } -``` - -3. 确定单层递归逻辑 - -玩家1的得分,等于集合左元素的数值+ 玩家2选择后集合的最小值(因为玩家2也是最聪明的) - - -而且剩余集合中的元素数量为2,或者大于2,的处理逻辑是不一样的! - -如图:当集合中的元素数量大于2,那么玩家1先选,玩家2依然有选择的权利。 - -所以代码如下: -``` - if ((end - start) >= 2) { - selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); - selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); - } -``` - - -如图:当集合中的元素数量等于2,那么玩家1先选,玩家2没得选。 - - -所以代码如下: -``` - if ((end - start) == 1) { - selectLeft = nums[start]; - selectRight = nums[end]; - } -``` - -单层递归逻辑代码如下: - -``` - int selectLeft, selectRight; - if ((end - start) >= 2) { - selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); - selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); - } - if ((end - start) == 1) { - selectLeft = nums[start]; - selectRight = nums[end]; - } - - return max(selectLeft, selectRight); -``` - -这些可以写出这道题目整体代码如下: - -``` -class Solution { -private: -int getScore(vector& nums, int start, int end) { - if (start == end) { - return nums[start]; - } - int selectLeft, selectRight; - if ((end - start) >= 2) { - selectLeft = nums[start] + min(getScore(nums, start + 2, end), getScore(nums, start + 1, end - 1)); - selectRight = nums[end] + min(getScore(nums, start + 1, end - 1), getScore(nums, start, end - 2)); - } - if ((end - start) == 1) { - selectLeft = nums[start]; - selectRight = nums[end]; - } - - return max(selectLeft, selectRight); -} -public: - bool PredictTheWinner(vector& nums) { - int sum = 0; - for (int i : nums) { - sum += i; - } - int player1 = getScore(nums, 0, nums.size() - 1); - int player2 = sum - player1; - return player1 >= player2; - } -}; -``` - -可以有一个优化,就是把重复计算的数值提取出来,如下: -``` -class Solution { -private: -int getScore(vector& nums, int start, int end) { - int selectLeft, selectRight; - int gap = end - start; - if (gap == 0) { - return nums[start]; - } else if (gap == 1) { // 此时直接取左右的值就可以 - selectLeft = nums[start]; - selectRight = nums[end]; - } else if (gap >= 2) { // 如果gap大于2,递归计算selectLeft和selectRight - // 计算的过程为什么用min,因为要按照对手也是最聪明的来计算。 - int num = getScore(nums, start + 1, end - 1); - selectLeft = nums[start] + - min(getScore(nums, start + 2, end), num); - selectRight = nums[end] + - min(num, getScore(nums, start, end - 2)); - } - return max(selectLeft, selectRight); -} -public: - bool PredictTheWinner(vector& nums) { - int sum = 0; - for (int i : nums) { - sum += i; - } - int player1 = getScore(nums, 0, nums.size() - 1); - int player2 = sum - player1; - // 如果最终两个玩家的分数相等,那么玩家 1 仍为赢家,所以是大于等于。 - return player1 >= player2; - } -}; -``` - -## 计算两个玩家的差值 - -以上是单独计算出两个选手的得分,逻辑上直观,但是代码确实比较冗余。 - -因为就我们要求的结果其实就是两个选手的胜负,那么不用两个选手的得分,而是把问题转换为两个选手所拿元素的差值。 - -代码如下: - -``` -class Solution { -private: -int getScore(vector& nums, int start, int end) { - if (end == start) { - return nums[start]; - } - int selectLeft = nums[start] - getScore(nums, start + 1, end); - int selectRight = nums[end] - getScore(nums, start, end - 1); - return max(selectLeft, selectRight); -} -public: - bool PredictTheWinner(vector& nums) { - return getScore(nums, 0, nums.size() - 1) >=0 ; - } -}; -``` - -计算的过程有一些是冗余的,在递归的过程中,可以使用一个memory数组记录一下中间结果,代码如下: - -``` -class Solution { -private: -int getScore(vector& nums, int start, int end, int memory[21][21]) { - if (end == start) { - return nums[start]; - } - if (memory[start][end]) return memory[start][end]; - int selectLeft = nums[start] - getScore(nums, start + 1, end, memory); - int selectRight = nums[end] - getScore(nums, start, end - 1, memory); - memory[start][end] = max(selectLeft, selectRight); - return memory[start][end]; -} -public: - bool PredictTheWinner(vector& nums) { - int memory[21][21] = {0}; // 记录递归中中间结果 - return getScore(nums, 0, nums.size() - 1, memory) >= 0 ; - } -}; -``` - -此时效率已经比较高了 - - -那么在看一下动态规划的思路。 - - -## 动态规划 - - -定义一个二维数组,先明确是用来干什么的,dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j)。 - -假如玩家1先取左端 nums[i],那么玩家2能赢对方的差值是dp[i+1][j] ,如果玩家1先取取右端 nums[j],玩家2能赢对方的差值就是dp[i][j-1], - -那么 不难理解如下公式: - -`dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); ` - - -确定了状态转移公式之后,就要想想如何遍历。 - -一些同学确定的方程,却不知道该如何遍历这个遍历推算出方程的结果,我们来看一下。 - -首先要给dp[i][j]进行初始化,首先当i == j的时候,nums[i]就是dp[i][j]的值。 - -代码如下: - -``` -// 当i == j的时候,nums[i]就是dp[i][j] -for (int i = 0; i < nums.size(); i++) { - dp[i][i] = nums[i]; -} -``` - -接下来就要推导公式了,首先要知道最终求是dp[0][nums.size() - 1]是否大于等于0,也就是求dp[0][nums.size() - 1] 至关重要。 - -从下图中,可以看出在推导方程的时候一定要从右下角向上推导,而且矩阵左半部分根本不用管! - - - -按照上图中的规则,不难列出推导公式的循环方式如下: - -``` -for(int i = nums.size() - 2; i >= 0; i--) { - for (int j = i + 1; j < nums.size(); j++) { - dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); - } -} - -``` - -最后整体动态规划的代码: - -## - -``` -class Solution { -public: - bool PredictTheWinner(vector& nums) { - // dp[i][j] 表示两个玩家在数组 i 到 j 区间内游戏能赢对方的差值(i <= j) - int dp[22][22] = {0}; - // 当i == j的时候,nums[i]就是dp[i][j] - for (int i = 0; i < nums.size(); i++) { - dp[i][i] = nums[i]; - } - for(int i = nums.size() - 2; i >= 0; i--) { - for (int j = i + 1; j < nums.size(); j++) { - dp[i][j] = max((nums[i] - dp[i + 1][j]), (nums[j] - dp[i][j - 1])); - } - } - return dp[0][nums.size() - 1] >= 0; - } -}; -``` - - diff --git a/problems/0491.递增子序列.md b/problems/0491.递增子序列.md index 15112c04..5deec0ee 100644 --- a/problems/0491.递增子序列.md +++ b/problems/0491.递增子序列.md @@ -1,7 +1,15 @@ +

+ + + + +

+

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

+ > 和子集问题有点像,但又处处是陷阱 -# 491.递增子序列 +## 491.递增子序列 题目链接:https://leetcode-cn.com/problems/increasing-subsequences/ @@ -12,13 +20,13 @@ 输入: [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]。 * 给定数组中可能包含重复数字,相等的数字应该被视为递增的一种情况。 -# 思路 +## 思路 这个递增子序列比较像是取有序的子集。而且本题也要求不能有相同的递增子序列。 @@ -41,7 +49,7 @@ ## 回溯三部曲 -* 递归函数参数 +* 递归函数参数 本题求子序列,很明显一个元素不能重复使用,所以需要startIndex,调整下一层递归的起始位置。 @@ -50,10 +58,10 @@ ``` vector> result; vector path; -void backtracking(vector& nums, int startIndex) +void backtracking(vector& nums, int startIndex) ``` -* 终止条件 +* 终止条件 本题其实类似求子集问题,也是要遍历树形结构找每一个节点,所以和[回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA)一样,可以不加终止条件,startIndex每次都会加1,并不会无限递归。 @@ -66,7 +74,7 @@ if (path.size() > 1) { } ``` -* 单层搜索逻辑 +* 单层搜索逻辑 ![491. 递增子序列1](https://img-blog.csdnimg.cn/20201124200229824.png) 在图中可以看出,**同一父节点下的同层上使用过的元素就不能在使用了** @@ -94,9 +102,7 @@ for (int i = startIndex; i < nums.size(); i++) { 最后整体C++代码如下: -## C++代码 - -``` +```C++ // 版本一 class Solution { private: @@ -129,7 +135,7 @@ public: }; ``` -## 优化 +## 优化 以上代码用我用了`unordered_set`来记录本层元素是否重复使用。 @@ -137,11 +143,11 @@ public: 注意题目中说了,数值范围[-100,100],所以完全可以用数组来做哈希。 -程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。 +程序运行的时候对unordered_set 频繁的insert,unordered_set需要做哈希映射(也就是把key通过hash function映射为唯一的哈希值)相对费时间,而且每次重新定义set,insert的时候其底层的符号表也要做相应的扩充,也是费事的。 那么优化后的代码如下: -``` +```C++ // 版本二 class Solution { private: @@ -179,7 +185,7 @@ public: -# 总结 +## 总结 本题题解清一色都说是深度优先搜索,但我更倾向于说它用回溯法,而且本题我也是完全使用回溯法的逻辑来分析的。 @@ -190,7 +196,22 @@ public: **就酱,如果感觉「代码随想录」很干货,就帮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),关注后就会发现和「代码随想录」相见恨晚!** +## 其他语言版本 -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0494.目标和.md b/problems/0494.目标和.md index 7eb5d3ca..2fb2a5eb 100644 --- a/problems/0494.目标和.md +++ b/problems/0494.目标和.md @@ -1,36 +1,73 @@ +

+ + + + +

+

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

+# 动态规划:目标和! +## 494. 目标和 -// 这道题小细节很多 -// 转为01 背包 思路不好想啊 -// dp数组难在如何初始化 -// dp 数组 通常比较长 +题目链接:https://leetcode-cn.com/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://mp.weixin.qq.com/s/r73thpBnK1tXndFDtlsdCQ)的录友,看到这道题,应该有一种直觉,就是感觉好像回溯法可以爆搜出来。 -事实确实如此,下面我也会给出响应的代码,只不过会超时,哈哈。 +事实确实如此,下面我也会给出相应的代码,只不过会超时,哈哈。 这道题目咋眼一看和动态规划背包啥的也没啥关系。 -本题要如何是表达式结果为target, +本题要如何使表达式结果为target, -既然为target,那么就一定有 left组合 - right组合 = target,中的left 和right一定是固定大小的,因为left + right要等于sum,而sum是固定的。 +既然为target,那么就一定有 left组合 - right组合 = target。 + +left + right等于sum,而sum是固定的。 公式来了, left - (sum - left) = target -> left = (target + sum)/2 。 target是固定的,sum是固定的,left就可以求出来。 -此时问题就是在集合nums中找出和为left的组合。 +此时问题就是在集合nums中找出和为left的组合。 -在回溯算法系列中,一起学过这道题目[回溯算法:39. 组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)的录友应该感觉很熟悉,这不就是组合总和问题么? +## 回溯算法 + +在回溯算法系列中,一起学过这道题目[回溯算法:39. 组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)的录友应该感觉很熟悉,这不就是组合总和问题么? 此时可以套组合总和的回溯法代码,几乎不用改动。 当然,也可以转变成序列区间选+ 或者 -,使用回溯法,那就是另一个解法。 -但无论哪种回溯法,时间复杂度都是是O(2^n)级别,**所以最后超时了**。 - 我也把代码给出来吧,大家可以了解一下,回溯的解法,以下是本题转变为组合总和问题的回溯法代码: -``` + +```C++ class Solution { private: vector> result; @@ -39,7 +76,6 @@ private: 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]; @@ -68,49 +104,155 @@ public: }; ``` -## 动态规划 +当然以上代码超时了。 -使用背包要明确dp[i]表示的是什么,i表示的又是什么? +也可以使用记忆化回溯,但这里我就不在回溯上下功夫了,直接看动规吧 -填满i(包括i)这么大容积的包,有dp[i]种方法。 +## 动态规划 +如何转化为01背包问题呢。 + +假设加法的总和为x,那么减法对应的总和就是sum - x。 + +所以我们要求的是 x - (sum - x) = S + +x = (S + sum) / 2 + +**此时问题就转化为,装满容量为x背包,有几种方法**。 + +大家看到(S + sum) / 2 应该担心计算的过程中向下取整有没有影响。 + +这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以: + +```C++ +if ((S + sum) % 2 == 1) return 0; // 此时没有方案 +``` + +**看到这种表达式,应该本能的反应,两个int相加数值可能溢出的问题,当然本题并没有溢出**。 + +再回归到01背包问题,为什么是01背包呢? + +因为每个物品(题目中的1)只用一次! + +这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。 + +本题则是装满有几种方法。其实这就是一个组合问题了。 + +1. 确定dp数组以及下标的含义 + +dp[j] 表示:填满j(包括j)这么大容积的包,有dp[i]种方法 + +其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。 + +下面我都是统一使用一维数组进行讲解, 二维降为一维(滚动数组),其实就是上一层拷贝下来,这个我在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)也有介绍。 + +2. 确定递推公式 + +有哪些来源可以推出dp[j]呢? + +不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]中方法。 + +那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种方法。 + +举一个例子,nums[i] = 2: dp[3],填满背包容量为3的话,有dp[3]种方法。 + +那么只需要搞到一个2(nums[i]),有dp[3]方法可以凑齐容量为3的背包,相应的就有多少种方法可以凑齐容量为5的背包。 + +那么需要把 这些方法累加起来就可以了,dp[i] += dp[j - nums[i]] + +所以求组合类问题的公式,都是类似这种: ``` -// 时间复杂度O(n^2) -// 空间复杂度可以说是O(n),也可以说是O(1),因为每次申请的辅助数组的大小是一个常数 +dp[j] += dp[j - nums[i]] +``` + +**这个公式在后面在讲解背包解决排列组合问题的时候还会用到!** + +3. dp数组如何初始化 + +从递归公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。 + +dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种方法,就是装0件物品。 + +dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。 + + +4. 确定遍历顺序 + +在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中,我们讲过对于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://img-blog.csdnimg.cn/20210125120743274.jpg) + +C++代码如下: + +```C++ class Solution { 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相加的时候要各位小心数值溢出的问题 - + if ((S + sum) % 2 == 1) return 0; // 此时没有方案 int bagSize = (S + sum) / 2; - int dp[1001] = {1}; + vector dp(bagSize + 1, 0); + dp[0] = 1; for (int i = 0; i < nums.size(); i++) { for (int j = bagSize; j >= nums[i]; j--) { - if (j - nums[i] >= 0) dp[j] += dp[j - nums[i]]; + dp[j] += dp[j - nums[i]]; } } return dp[bagSize]; } }; -``` -dp数组中的数值变化:(从[0 - 4]) ``` -1 1 0 0 0 -1 2 1 0 0 -1 3 3 1 0 -1 4 6 4 1 -1 5 10 10 5 -``` +* 时间复杂度O(n * m),n为正数个数,m为背包容量 +* 空间复杂度:O(m) m为背包容量 -# 总结 + +## 总结 此时 大家应该不仅想起,我们之前讲过的[回溯算法:39. 组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)是不是应该也可以用dp来做啊? 是的,如果仅仅是求个数的话,就可以用dp,但[回溯算法:39. 组合总和](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw)要求的是把所有组合列出来,还是要使用回溯法爆搜的。 +本地还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为: +``` +dp[j] += dp[j - nums[i]]; +``` + +后面我们在讲解完全背包的时候,还会用到这个递推公式! + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0496.下一个更大元素I.md b/problems/0496.下一个更大元素I.md deleted file mode 100644 index 28ae9de1..00000000 --- a/problems/0496.下一个更大元素I.md +++ /dev/null @@ -1,35 +0,0 @@ -## 链接 -https://leetcode-cn.com/problems/next-greater-element-i/ - -## 思路 - -两个数组逻辑还是有点绕 -最好还是把所有情况列出来 - -``` -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; - } -}; -``` diff --git a/problems/0501.二叉搜索树中的众数.md b/problems/0501.二叉搜索树中的众数.md index 5e727433..0e8c0d0e 100644 --- a/problems/0501.二叉搜索树中的众数.md +++ b/problems/0501.二叉搜索树中的众数.md @@ -1,10 +1,17 @@ -## 题目地址 +

+ + + + +

+

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

-https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/solution/ -> 二叉树上应该怎么求,二叉搜索树上有应该怎么求 +> 二叉树上应该怎么求,二叉搜索树上又应该怎么求? -# 501.二叉搜索树中的众数 +## 501.二叉搜索树中的众数 + +题目地址:https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/solution/ 给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。 @@ -26,13 +33,13 @@ https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/solution/ 进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内) -# 思路 +## 思路 这道题目呢,递归法我从两个维度来讲。 首先如果不是二叉搜索树的话,应该怎么解题,是二叉搜索树,又应该如何解题,两种方式做一个比较,可以加深大家对二叉树的理解。 -## 递归法 +## 递归法 ### 如果不是二叉搜索树 @@ -42,11 +49,11 @@ https://leetcode-cn.com/problems/find-mode-in-binary-search-tree/solution/ 1. 这个树都遍历了,用map统计频率 -至于用前中后序那种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病! +至于用前中后序那种遍历也不重要,因为就是要全遍历一遍,怎么个遍历法都行,层序遍历都没毛病! 这里采用前序遍历,代码如下: -``` +```C++ // map key:元素,value:出现频率 void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 if (cur == NULL) return ; @@ -57,7 +64,7 @@ void searchBST(TreeNode* cur, unordered_map& map) { // 前序遍历 } ``` -2. 把统计的出来的出现频率(即map中的value)排个序 +2. 把统计的出来的出现频率(即map中的value)排个序 有的同学可能可以想直接对map中的value排序,还真做不到,C++中如果使用std::map或者std::multimap可以对key排序,但不能对value排序。 @@ -74,15 +81,15 @@ vector> vec(map.begin(), map.end()); sort(vec.begin(), vec.end(), cmp); // 给频率排个序 ``` -3. 取前面高频的元素 +3. 取前面高频的元素 此时数组vector中已经是存放着按照频率排好序的pair,那么把前面高频的元素取出来就可以了。 代码如下: -``` +```C++ result.push_back(vec[0].first); -for (int i = 1; i < vec.size(); i++) { +for (int i = 1; i < vec.size(); i++) { // 取最高的放到result数组中 if (vec[i].second == vec[0].second) result.push_back(vec[i].first); else break; @@ -93,7 +100,7 @@ return result; 整体C++代码如下: -``` +```C++ class Solution { private: @@ -116,7 +123,7 @@ public: 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++) { + for (int i = 1; i < vec.size(); i++) { // 取最高的放到result数组中 if (vec[i].second == vec[0].second) result.push_back(vec[i].first); else break; @@ -128,17 +135,17 @@ public: **所以如果本题没有说是二叉搜索树的话,那么就按照上面的思路写!** -### 是二叉搜索树 +### 是二叉搜索树 **既然是搜索树,它中序遍历就是有序的**。 如图: - +![501.二叉搜索树中的众数1](https://img-blog.csdnimg.cn/20210204152758889.png) 中序遍历代码如下: -``` +```C++ void searchBST(TreeNode* cur) { if (cur == NULL) return ; searchBST(cur->left); // 左 @@ -150,7 +157,7 @@ void searchBST(TreeNode* cur) { 遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。 -关键是在有序数组上的话,好搞,在树上怎么搞呢? +关键是在有序数组上的话,好搞,在树上怎么搞呢? 这就考察对树的操作了。 @@ -210,7 +217,7 @@ if (count > maxCount) { // 如果计数大于最大值 关键代码都讲完了,完整代码如下:(**只需要遍历一遍二叉搜索树,就求出了众数的集合**) -``` +```C++ class Solution { private: int maxCount; // 最大频率 @@ -247,7 +254,7 @@ private: public: vector findMode(TreeNode* root) { - count = 0; + count = 0; maxCount = 0; TreeNode* pre = NULL; // 记录前一个节点 result.clear(); @@ -259,20 +266,20 @@ public: ``` -## 迭代法 +## 迭代法 只要把中序遍历转成迭代,中间节点的处理逻辑完全一样。 二叉树前中后序转迭代,传送门: -* [二叉树:前中后序迭代法](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) -* [二叉树:前中后序统一风格的迭代方式](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) +* [二叉树:前中后序迭代法](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg) +* [二叉树:前中后序统一风格的迭代方式](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg) 下面我给出其中的一种中序遍历的迭代法,其中间处理逻辑一点都没有变(我从递归法直接粘过来的代码,连注释都没改,哈哈) 代码如下: -``` +```C++ class Solution { public: vector findMode(TreeNode* root) { @@ -314,13 +321,13 @@ public: }; ``` -# 总结 +## 总结 本题在递归法中,我给出了如果是普通二叉树,应该怎么求众数。 知道了普通二叉树的做法时候,我再进一步给出二叉搜索树又应该怎么求众数,这样鲜明的对比,相信会对二叉树又有更深层次的理解了。 -在递归遍历二叉搜索树的过程中,我还介绍了一个统计最高出现频率元素集合的技巧, 要不然就要遍历两次二叉搜索树才能把这个最高出现频率元素的集合求出来。 +在递归遍历二叉搜索树的过程中,我还介绍了一个统计最高出现频率元素集合的技巧, 要不然就要遍历两次二叉搜索树才能把这个最高出现频率元素的集合求出来。 **为什么没有这个技巧一定要遍历两次呢? 因为要求的是集合,会有多个众数,如果规定只有一个众数,那么就遍历一次稳稳的了。** @@ -329,8 +336,26 @@ public: **求二叉搜索树中的众数其实是一道简单题,但大家可以发现我写了这么一大篇幅的文章来讲解,主要是为了尽量从各个角度对本题进剖析,帮助大家更快更深入理解二叉树**。 -**就酱,如果学到了的话,就转发给身边需要的同学吧,可能他们也需要!** + +> **需要强调的是 leetcode上的耗时统计是非常不准确的,看个大概就行,一样的代码耗时可以差百分之50以上**,所以leetcode的耗时统计别太当回事,知道理论上的效率优劣就行了。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: -**需要强调的是 leetcode上的耗时统计是非常不准确的,看个大概就行,一样的代码耗时可以差百分之50以上**,所以leetcode的耗时统计别太当回事,知道理论上的效率优劣就行了。 + +----------------------- +* 作者微信:[程序员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/0503.下一个更大元素II.md b/problems/0503.下一个更大元素II.md deleted file mode 100644 index fed5dd56..00000000 --- a/problems/0503.下一个更大元素II.md +++ /dev/null @@ -1,62 +0,0 @@ - -## 链接 -https://leetcode-cn.com/problems/next-greater-element-ii/ - -## 思路 - -可以把两个数组拼接在一起 - -代码性能不行啊,得找找优化的方式 - -``` -class Solution { -public: - vector nextGreaterElements(vector& nums) { - vector nums1(nums.begin(), nums.end()); - nums.insert(nums.end(), nums1.begin(), nums1.end()); - vector result(nums.size(), -1); - if (nums.size() == 0) return result; - stack st; - st.push(0); - for (int i = 0; i < nums.size(); i++) { - while (!st.empty() && nums[i] > nums[st.top()]) { - result[st.top()] = nums[i]; - st.pop(); - } - st.push(i); - } - result.resize(nums.size() / 2); - return result; - } -}; -``` - - -这样好像能高一些 -``` -class Solution { -public: - vector nextGreaterElements(vector& nums) { - vector nums1(nums.begin(), nums.end()); - nums.resize(nums.size() * 2); - int h = nums.size() / 2; - for (int i = 0; i < nums.size() / 2; i++) { - nums[h + i] = nums[i]; - } - //nums.insert(nums.end(), nums1.begin(), nums1.end()); - vector result(nums.size(), -1); - if (nums.size() == 0) return result; - stack st; - st.push(0); - for (int i = 0; i < nums.size(); i++) { - while (!st.empty() && nums[i] > nums[st.top()]) { - result[st.top()] = nums[i]; - st.pop(); - } - st.push(i); - } - result.resize(nums.size() / 2); - return result; - } -}; -``` diff --git a/problems/0509.斐波那契数.md b/problems/0509.斐波那契数.md new file mode 100644 index 00000000..8537ed8b --- /dev/null +++ b/problems/0509.斐波那契数.md @@ -0,0 +1,188 @@ +

+ + + + +

+

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

+ +## 509. 斐波那契数 + +题目地址:https://leetcode-cn.com/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://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ),[回溯法系列的回溯三部曲](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw)一样。后面慢慢大家就会体会到,动规五部曲方法的重要性。 + +### 动态规划 + +动规五部曲: + +这里我们要用一个一维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++代码如下: + +```C++ +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) + +当然可以发现,我们只需要维护两个数值就可以了,不需要记录整个序列。 + +代码如下: + +```C++ +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) + +### 递归解法 + +本题还可以使用递归解法来做 + +代码如下: + +```C++ +class Solution { +public: + int fib(int N) { + if (N < 2) return N; + return fib(N - 1) + fib(N - 2); + } +}; +``` + +* 时间复杂度:O(2^n) +* 空间复杂度:O(n) 算上了编程语言中实现递归的系统栈所占空间 + +这个递归的时间复杂度大家画一下树形图就知道了,如果不清晰的同学,可以看这篇:[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ) + + +# 总结 + +斐波那契数列这道题目是非常基础的题目,我在后面的动态规划的讲解中将会多次提到斐波那契数列! + +这里我严格按照[关于动态规划,你该了解这些!](https://leetcode-cn.com/circle/article/tNuNnM/)中的动规五部曲来分析了这道题目,一些分析步骤可能同学感觉没有必要搞的这么复杂,代码其实上来就可以撸出来。 + +但我还是强调一下,简单题是用来掌握方法论的,动规五部曲将在接下来的动态规划讲解中发挥重要作用,敬请期待! + +就酱,循序渐进学算法,认准「代码随想录」! + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0513.找树左下角的值.md b/problems/0513.找树左下角的值.md index ec4d041c..19c870c3 100644 --- a/problems/0513.找树左下角的值.md +++ b/problems/0513.找树左下角的值.md @@ -1,25 +1,31 @@ +

+ + + + +

+

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

-> 我的左下角的数值是多少? -# 513.找树左下角的值 +## 513.找树左下角的值 -给定一个二叉树,在树的最后一行找到最左边的值。 +给定一个二叉树,在树的最后一行找到最左边的值。 示例 1: - +![513.找树左下角的值](https://img-blog.csdnimg.cn/20210204152956836.png) 示例 2: - +![513.找树左下角的值1](https://img-blog.csdnimg.cn/20210204153017586.png) -# 思路 +## 思路 本地要找出树的最后一行找到最左边的值。此时大家应该想起用层序遍历是非常简单的了,反而用递归的话会比较难一点。 我们依然还是先介绍递归法。 -## 递归 +## 递归 咋眼一看,这道题目用递归的话就就一直向左遍历,最后一个就是答案呗? @@ -39,7 +45,7 @@ 递归三部曲: -1. 确定递归函数的参数和返回值 +1. 确定递归函数的参数和返回值 参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度。 这里就不需要返回值了,所以递归函数的返回类型为void。 @@ -53,17 +59,17 @@ int maxleftValue; // 全局变量 最大深度最左节点的数值 void traversal(TreeNode* root, int leftLen) ``` -有的同学可能疑惑,为啥不能递归函数的返回值返回最长深度呢? +有的同学可能疑惑,为啥不能递归函数的返回值返回最长深度呢? 其实很多同学都对递归函数什么时候要有返回值,什么时候不能有返回值很迷茫。 -**如果需要遍历整颗树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!** +**如果需要遍历整颗树,递归函数就不能有返回值。如果需要遍历某一条固定路线,递归函数就一定要有返回值!** 初学者可能对这个结论不太理解,别急,后面我会安排一道题目专门讲递归函数的返回值问题。这里大家暂时先了解一下。 -本题我们是要遍历整个树找到最深的叶子节点,需要遍历整颗树,所以递归函数没有返回值。 +本题我们是要遍历整个树找到最深的叶子节点,需要遍历整颗树,所以递归函数没有返回值。 -2. 确定终止条件 +2. 确定终止条件 当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度。 @@ -73,20 +79,20 @@ void traversal(TreeNode* root, int leftLen) if (root->left == NULL && root->right == NULL) { if (leftLen > maxLen) { maxLen = leftLen; // 更新最大深度 - maxleftValue = root->val; // 最大深度最左面的数值 + maxleftValue = root->val; // 最大深度最左面的数值 } return; } ``` -3. 确定单层递归的逻辑 +3. 确定单层递归的逻辑 在找最大深度的时候,递归的过程中依然要使用回溯,代码如下: -``` +```C++ // 中 if (root->left) { // 左 - leftLen++; // 深度加一 + leftLen++; // 深度加一 traversal(root->left, leftLen); leftLen--; // 回溯,深度减一 } @@ -100,7 +106,7 @@ return; 完整代码如下: -``` +```C++ class Solution { public: int maxLen = INT_MIN; @@ -121,7 +127,7 @@ public: if (root->right) { leftLen++; traversal(root->right, leftLen); - leftLen--; // 回溯 + leftLen--; // 回溯 } return; } @@ -134,7 +140,7 @@ public: 当然回溯的地方可以精简,精简代码如下: -``` +```C++ class Solution { public: int maxLen = INT_MIN; @@ -167,7 +173,7 @@ public: ## 迭代法 -本题使用层序遍历再合适不过了,比递归要好理解的多! +本题使用层序遍历再合适不过了,比递归要好理解的多! 只需要记录最后一行第一个节点的数值就可以了。 @@ -175,7 +181,7 @@ public: 代码如下: -``` +```C++ class Solution { public: int findBottomLeftValue(TreeNode* root) { @@ -197,12 +203,32 @@ public: }; ``` -# 总结 +## 总结 本题涉及如下几点: * 递归求深度的写法,我们在[二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww)中详细的分析了深度应该怎么求,高度应该怎么求。 * 递归中其实隐藏了回溯,在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)中讲解了究竟哪里使用了回溯,哪里隐藏了回溯。 * 层次遍历,在[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)深度讲解了二叉树层次遍历。 - 所以本题涉及到的点,我们之前都讲解过,这些知识点需要同学们灵活运用,这样就举一反三了。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0514.自由之路.md b/problems/0514.自由之路.md deleted file mode 100644 index ede77835..00000000 --- a/problems/0514.自由之路.md +++ /dev/null @@ -1,42 +0,0 @@ - -//dp[i][j],key的0~i位字符拼写后,ring的第j位对齐12:00方向,需要的最小步数 -//前提:key[i] = ring[j],若不满足,dp[i][j] = INT_MAX - -这道题目我服! 没做出来 - -https://blog.csdn.net/qq_41855420/article/details/89058979 - -``` -class Solution { -public: - int findRotateSteps(string ring, string key) { - //int dp[101][101] = {0}; - int n = ring.size(); - vector> dp(key.size() + 1, vector(ring.size(), 0)); - for (int i = key.size() - 1; i >= 0; i--) { - for (int j = 0; j < ring.size(); j++) { - dp[i][j] = INT_MAX; - for (int k = 0; k < ring.size(); k++) { - if (ring[k] == key[i]) { - int diff = abs(j - k); - int step = min(diff, n - diff); - dp[i][j] = min(dp[i][j], step + dp[i + 1][k]); - } - } - } - } - for (int i = 0; i < dp.size(); i++) { - for (int j = 0; j < dp[0].size(); j++) { - cout << dp[i][j] << " "; - } - cout << endl; - } - return dp[0][0] + key.size(); - } -}; -``` - -2 3 4 5 5 4 3 -2 1 0 0 1 2 3 - - diff --git a/problems/0515.在每个树行中找最大值.md b/problems/0515.在每个树行中找最大值.md deleted file mode 100644 index e1c4863a..00000000 --- a/problems/0515.在每个树行中找最大值.md +++ /dev/null @@ -1,6 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/find-largest-value-in-each-tree-row/ -## 思路 -详见: - * [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) diff --git a/problems/0516.最长回文子序列.md b/problems/0516.最长回文子序列.md new file mode 100644 index 00000000..813e75f5 --- /dev/null +++ b/problems/0516.最长回文子序列.md @@ -0,0 +1,165 @@ +

+ + + + +

+

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

+ +## 516.最长回文子序列 +题目链接:https://leetcode-cn.com/problems/longest-palindromic-subsequence/ + +给定一个字符串 s ,找到其中最长的回文子序列,并返回该序列的长度。可以假设 s 的最大长度为 1000 。 + +示例 1: +输入: "bbbab" +输出: 4 +一个可能的最长回文子序列为 "bbbb"。 + +示例 2: +输入:"cbbd" +输出: 2 +一个可能的最长回文子序列为 "bb"。 + +提示: + +* 1 <= s.length <= 1000 +* s 只包含小写英文字母 + + +## 思路 + +我们刚刚做过了 [动态规划:回文子串](https://mp.weixin.qq.com/s/2WetyP6IYQ6VotegepVpEw),求的是回文子串,而本题要求的是回文子序列, 要搞清楚这两者之间的区别。 + +**回文子串是要连续的,回文子序列可不是连续的!** 回文子串,回文子序列都是动态规划经典题目。 + +回文子串,可以做这两题: + +* 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://img-blog.csdnimg.cn/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://img-blog.csdnimg.cn/20210127151420476.jpg) + +代码如下: + +```C++ +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]才不会被初始值覆盖。 + +```C++ +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] + 2 和 dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 可以看出,dp[i][j]是依赖于dp[i + 1][j - 1] 和 dp[i + 1][j], + +也就是从矩阵的角度来说,dp[i][j] 下一行的数据。 **所以遍历i的时候一定要从下到上遍历,这样才能保证,下一行的数据是经过计算的**。 + +递推公式:dp[i][j] = dp[i + 1][j - 1] + 2,dp[i][j] = max(dp[i + 1][j], dp[i][j - 1]) 分别对应着下图中的红色箭头方向,如图: + +![516.最长回文子序列2](https://img-blog.csdnimg.cn/20210127151452993.jpg) + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210127151521432.jpg) + +红色框即:dp[0][s.size() - 1]; 为最终结果。 + +以上分析完毕,C++代码如下: + +```C++ +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]; + } +}; +``` + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0518.零钱兑换II.md b/problems/0518.零钱兑换II.md index b60829a3..46e52cc0 100644 --- a/problems/0518.零钱兑换II.md +++ b/problems/0518.零钱兑换II.md @@ -1,41 +1,205 @@ +

+ + + + +

+

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

+# 动态规划:给你一些零钱,你要怎么凑? -// 计算有多少种方式 -// 完全背包 for循环的顺序啊,先是哪个for 后是哪个for定不下来 +## 518. 零钱兑换 II -排列 -``` -class Solution { -public: - int change(int amount, vector& coins) { - int dp[50001] = {0}; - dp[0] = 1; - for (int i = 0; i <= amount; i++) { - for (int j = 0; j < coins.size(); j++) { // 这是组合把??? - if (i - coins[j] >= 0) dp[i] += dp[i - coins[j]]; - } - for (int j = 0; j <= amount; j++) { - cout << dp[j] << " "; - } - cout << endl; - } - return dp[amount]; +链接:https://leetcode-cn.com/problems/coin-change-2/ + +难度:中等 + +给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。  + +示例 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://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw) + +但本题和纯完全背包不一样,**纯完全背包是能否凑成总金额,而本题是要求凑成总金额的个数!** + +注意题目描述中是凑成总金额的硬币组合数,为什么强调是组合数呢? + +例如示例一: + +5 = 2 + 2 + 1 + +5 = 2 + 1 + 2 + +这是一种组合,都是 2 2 1。 + +如果问的是排列数,那么上面就是两种排列了。 + +**组合不强调元素之间的顺序,排列强调元素之间的顺序**。 其实这一点我们在讲解回溯算法专题的时候就讲过了哈。 + +那我为什么要介绍这些呢,因为这和下文讲解遍历顺序息息相关! + +回归本题,动规五步曲来分析如下: + +1. 确定dp数组以及下标的含义 + +dp[j]:凑成总金额j的货币组合数为dp[j] + +2. 确定递推公式 + +dp[j] (考虑coins[i]的组合总和) 就是所有的dp[j - coins[i]](不考虑coins[i])相加。 + +所以递推公式:dp[j] += dp[j - coins[i]]; + +**这个递推公式大家应该不陌生了,我在讲解01背包题目的时候在这篇[动态规划:目标和!](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw)中就讲解了,求装满背包有几种方法,一般公式都是:dp[j] += dp[j - nums[i]];** + +3. dp数组如何初始化 + +首先dp[0]一定要为1,dp[0] = 1是 递归公式的基础。 + +从dp[i]的含义上来讲就是,凑成总金额0的货币组合数为1。 + +下标非0的dp[j]初始化为0,这样累计加dp[j - coins[i]]的时候才不会影响真正的dp[j] + +4. 确定遍历顺序 + +本题中我们是外层for循环遍历物品(钱币),内层for遍历背包(金钱总额),还是外层for遍历背包(金钱总额),内层for循环遍历物品(钱币)呢? + + +我在[动态规划:关于完全背包,你该了解这些!](https://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw)中讲解了完全背包的两个for循环的先后顺序都是可以的。 + +**但本题就不行了!** + +因为纯完全背包求得是能否凑成总和,和凑成总和的元素有没有顺序没关系,即:有顺序也行,没有顺序也行! + +而本题要求凑成总和的组合数,元素之间要求没有顺序。 + +所以纯完全背包是能凑成总结就行,不用管怎么凑的。 + +本题是求凑出来的方案个数,且每个方案个数是为组合数。 + +那么本题,两个for循环的先后顺序可就有说法了。 + +我们先来看 外层for循环遍历物品(钱币),内层for遍历背包(金钱总额)的情况。 + +代码如下: + +```C++ +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交换顺序,代码如下: + ``` +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://img-blog.csdnimg.cn/20210120181331461.jpg) + +最后红色框dp[amount]为最终结果。 + +以上分析完毕,C++代码如下: + +```C++ class Solution { public: int change(int amount, vector& coins) { - int dp[50001] = {0}; + vector dp(amount + 1, 0); dp[0] = 1; - for (int i = 0; i < coins.size(); i++) { // 一个钱币只在序列里出现一次 - for (int j = 0; j <= amount; j++) { - if (j - coins[i] >= 0) dp[j] += dp[j - coins[i]]; + 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]; } }; ``` +是不是发现代码如此精简,哈哈 + +## 总结 + +本题的递推公式,其实我们在[动态规划:目标和!](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw)中就已经讲过了,**而难点在于遍历顺序!** + +在求装满背包有几种方案的时候,认清遍历顺序是非常关键的。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +可能说到排列数录友们已经有点懵了,后面Carl还会安排求排列数的题目,到时候在对比一下,大家就会发现神奇所在! + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0530.二叉搜索树的最小绝对差.md b/problems/0530.二叉搜索树的最小绝对差.md index 6a7f8e8e..d5abc692 100644 --- a/problems/0530.二叉搜索树的最小绝对差.md +++ b/problems/0530.二叉搜索树的最小绝对差.md @@ -1,11 +1,17 @@ +

+ + + + +

+

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

-## 题目地址 - -https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ > 利用二叉搜索树的特性搞起! -# 530.二叉搜索树的最小绝对差 +## 530.二叉搜索树的最小绝对差 + +题目地址:https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ 给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。 @@ -15,9 +21,9 @@ https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ 提示:树中至少有 2 个节点。 -# 思路 +## 思路 -题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。 +题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。 **注意是二叉搜索树**,二叉搜索树可是有序的。 @@ -33,14 +39,14 @@ https://leetcode-cn.com/problems/minimum-absolute-difference-in-bst/ 代码如下: -``` +```C++ class Solution { private: vector vec; void traversal(TreeNode* root) { if (root == NULL) return; traversal(root->left); - vec.push_back(root->val); // 将二叉搜索树转换为有序数组 + vec.push_back(root->val); // 将二叉搜索树转换为有序数组 traversal(root->right); } public: @@ -63,13 +69,13 @@ public: 如图: - +![530.二叉搜索树的最小绝对差](https://img-blog.csdnimg.cn/20210204153247458.png) 一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了。 代码如下: -``` +```C++ class Solution { private: int result = INT_MAX; @@ -93,13 +99,13 @@ public: 是不是看上去也并不复杂! -## 迭代 +## 迭代 看过这两篇[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg),[二叉树:前中后序迭代方式的写法就不能统一一下么?](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)文章之后,不难写出两种中序遍历的迭代法。 下面我给出其中的一种中序遍历的迭代法,代码如下: -``` +```C++ class Solution { public: int getMinimumDifference(TreeNode* root) { @@ -115,7 +121,7 @@ public: cur = st.top(); st.pop(); if (pre != NULL) { // 中 - result = min(result, cur->val - pre->val); + result = min(result, cur->val - pre->val); } pre = cur; cur = cur->right; // 右 @@ -126,7 +132,7 @@ public: }; ``` -# 总结 +## 总结 **遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点。** @@ -134,8 +140,29 @@ public: 后面我将继续介绍一系列利用二叉搜索树特性的题目。 -**就酱,感觉学到了,就转发给身边需要的同学吧** + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0538.把二叉搜索树转换为累加树.md b/problems/0538.把二叉搜索树转换为累加树.md index 84dada49..a5f4c43c 100644 --- a/problems/0538.把二叉搜索树转换为累加树.md +++ b/problems/0538.把二叉搜索树转换为累加树.md @@ -1,13 +1,13 @@ +

+ + + + +

+

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

-## 题目地址 -https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ - -> 祝大家1024节日快乐!! - -今天应该是一个程序猿普天同庆的日子,所以今天的题目比较简单,只要认真把前面每天的文章都看了,今天的题目就是分分钟的事了,大家可以愉快过节! - -# 538.把二叉搜索树转换为累加树 +## 538.把二叉搜索树转换为累加树 题目链接:https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ @@ -23,20 +23,20 @@ https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ ![538.把二叉搜索树转换为累加树](https://img-blog.csdnimg.cn/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] +输入:[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] +示例 2: +输入:root = [0,null,1] +输出:[1,null,1] -示例 3: -输入:root = [1,0,2] -输出:[3,3,2] +示例 3: +输入:root = [1,0,2] +输出:[3,3,2] -示例 4: -输入:root = [3,2,4,1] -输出:[7,9,4,10] +示例 4: +输入:root = [3,2,4,1] +输出:[7,9,4,10] 提示: @@ -45,7 +45,7 @@ https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ * 树中的所有值 互不相同 。 * 给定的树为二叉搜索树。 -# 思路 +## 思路 一看到累加树,相信很多小伙伴都会疑惑:如何累加?遇到一个节点,然后在遍历其他节点累加?怎么一想这么麻烦呢。 @@ -65,13 +65,13 @@ https://leetcode-cn.com/problems/convert-bst-to-greater-tree/ 遍历顺序如图所示: - +![538.把二叉搜索树转换为累加树](https://img-blog.csdnimg.cn/20210204153440666.png) -本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。 +本题依然需要一个pre指针记录当前遍历节点cur的前一个节点,这样才方便做累加。 pre指针的使用技巧,我们在[二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ)和[二叉树:我的众数是多少?](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg)都提到了,这是常用的操作手段。 -* 递归函数参数以及返回值 +* 递归函数参数以及返回值 这里很明确了,不需要递归函数的返回值做什么操作了,要遍历整棵树。 @@ -81,10 +81,10 @@ pre指针的使用技巧,我们在[二叉树:搜索树的最小绝对差](ht ``` int pre; // 记录前一个节点的数值 -void traversal(TreeNode* cur) +void traversal(TreeNode* cur) ``` -* 确定终止条件 +* 确定终止条件 遇空就终止。 @@ -92,7 +92,7 @@ void traversal(TreeNode* cur) if (cur == NULL) return; ``` -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 注意**要右中左来遍历二叉树**, 中节点的处理逻辑就是让cur的数值加上前一个节点的数值。 @@ -107,7 +107,7 @@ traversal(cur->left); // 左 递归法整体代码如下: -``` +```C++ class Solution { private: int pre; // 记录前一个节点的数值 @@ -127,13 +127,13 @@ public: }; ``` -## 迭代法 +## 迭代法 迭代法其实就是中序模板题了,在[二叉树:前中后序迭代法](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)和[二叉树:前中后序统一方式迭代法](https://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)可以选一种自己习惯的写法。 这里我给出其中的一种,代码如下: -``` +```C++ class Solution { private: int pre; // 记录前一个节点的数值 @@ -162,12 +162,29 @@ public: }; ``` -# 总结 +## 总结 经历了前面各种二叉树增删改查的洗礼之后,这道题目应该比较简单了。 **好了,二叉树已经接近尾声了,接下来就是要对二叉树来一个大总结了**。 -最后再次祝大家1024节日快乐,哈哈哈! + +## 其他语言版本 +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0541.反转字符串II.md b/problems/0541.反转字符串II.md index ca31a744..6a84006e 100644 --- a/problems/0541.反转字符串II.md +++ b/problems/0541.反转字符串II.md @@ -1,11 +1,18 @@ +

+ + + + +

+

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

-# 题目地址 -https://leetcode-cn.com/problems/reverse-string-ii/ > 简单的反转还不够,我要花式反转 -# 题目:541. 反转字符串II +# 541. 反转字符串II + +https://leetcode-cn.com/problems/reverse-string-ii/ 给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。 @@ -18,20 +25,20 @@ https://leetcode-cn.com/problems/reverse-string-ii/ 输入: s = "abcdefg", k = 2 输出: "bacdfeg" -# 思路 +# 思路 这道题目其实也是模拟,实现题目中规定的反转规则就可以了。 一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 -其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。 +其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。 因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。 **所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。** 性能如下: - + 那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用reverse来实现反转也没毛病,毕竟不是解题关键部分。 @@ -60,7 +67,7 @@ public: 那么我们也可以实现自己的reverse函数,其实和题目[344. 反转字符串](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)道理是一样的。 -下面我实现的reverse函数区间是左闭右闭区间,代码如下: +下面我实现的reverse函数区间是左闭右闭区间,代码如下: ``` class Solution { public: @@ -85,5 +92,27 @@ public: }; ``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0559.N叉树的最大深度.md b/problems/0559.N叉树的最大深度.md deleted file mode 100644 index ad424230..00000000 --- a/problems/0559.N叉树的最大深度.md +++ /dev/null @@ -1,46 +0,0 @@ -## 地址 - -## 思路 - -## C++代码 - -### 递归法 - -``` -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; - } -}; -``` -### 迭代法 - -``` -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(); - vector vec; - 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; - } -}; -``` diff --git a/problems/0572.另一个树的子树.md b/problems/0572.另一个树的子树.md deleted file mode 100644 index 7b787736..00000000 --- a/problems/0572.另一个树的子树.md +++ /dev/null @@ -1,70 +0,0 @@ - - -判断二叉树是否对称,是否相等,是否对称,都是一个套路 - -## C++代码 - -``` -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 isSubtree(TreeNode* s, TreeNode* t) { - if (s == NULL) return false; - if (compare(s, t)) return true; - if(isSubtree(s->left, t)) return true; - if (isSubtree(s->right, t)) return true; - return false; - } -}; -``` - - -``` -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; - } - - bool isSubtree(TreeNode* s, TreeNode* t) { - stack st; - vector result; - if (s == NULL) return false; - st.push(s); - while (!st.empty()) { - TreeNode* node = st.top(); // 中 - st.pop(); - if(isSameTree(node, t)) return true; - if (node->right) st.push(node->right); // 右(空节点不入栈) - if (node->left) st.push(node->left); // 左(空节点不入栈) - } - return false; - } -}; -``` diff --git a/problems/0575.分糖果.md b/problems/0575.分糖果.md deleted file mode 100644 index 831fc9b8..00000000 --- a/problems/0575.分糖果.md +++ /dev/null @@ -1,40 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/distribute-candies/ - -## 思路 - -糖果的种类是可妹妹先来,所以思路先求出一共有多少种类型的糖果,需要注意: 数组中数字的大小也就是糖果的种类取值范围在[负十万和 正十万之间], 依然可以定义一个数组,通过哈希法求出有多少类型的糖果,那么糖果种类可以是负数 怎么办呢,可以把 定一个 20万大小的数组 ,就可以把糖果的全部类型映射到数组的下表了。 - -通过哈希法,可以求出了糖果的类型数量,如果糖果种类大于糖果总数的一半了,返回 糖果数量的一半就好,因为妹妹已经得到种类最多的糖果了,否则,就是返回 糖果的种类。 - -## 代码 - -``` -class Solution { -public: - int distributeCandies(vector& candies) { - // 初始化一个record数组,因为糖果种类的数值在范围[-100,000, 100,000]内 - // 将这个范围的数值统一加上100000,可以映射到record数组的索引下表了 - // record数组大小必须大于等于200001,这样才能取到200000这个下表索引 - int record[200001] = {0}; - // 通过record来记录糖果的种类 - for (int i = 0; i < candies.size(); i++) { - record[candies[i] + 100000]++; - } - // 统计糖果种类的数量 - int count = 0; - for (int i = 0; i < 200001; i++) { - if (record[i] != 0) { - count ++; - } - } - int half = candies.size() / 2; - // 如果糖果种类大于糖果总数的一半了,return 糖果数量的一半 - // 否则,就是return 糖果的种类。 - return count > half ? half : count; - } -}; -``` - -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0583.两个字符串的删除操作.md b/problems/0583.两个字符串的删除操作.md new file mode 100644 index 00000000..9679347a --- /dev/null +++ b/problems/0583.两个字符串的删除操作.md @@ -0,0 +1,121 @@ +

+ + + + +

+

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

+ +## 583. 两个字符串的删除操作 + +题目链接:https://leetcode-cn.com/problems/delete-operation-for-two-strings/ + +给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。 + +示例: + +输入: "sea", "eat" +输出: 2 +解释: 第一步将"sea"变为"ea",第二步将"eat"变为"ea" + +## 思路 + +本题和[动态规划:115.不同的子序列](https://mp.weixin.qq.com/s/1SULY2XVSROtk_hsoVLu8A)相比,其实就是两个字符串可以都可以删除了,情况虽说复杂一些,但整体思路是不变的。 + +这次是两个字符串可以相互删了,这种题目也知道用动态规划的思路来解,动规五部曲,分析如下: + +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}); + + +3. dp数组如何初始化 + +从递推公式中,可以看出来,dp[i][0] 和 dp[0][j]是一定要初始化的。 + +dp[i][0]:word2为空字符串,以i-1为结尾的字符串word2要删除多少个元素,才能和word1相同呢,很明显dp[i][0] = i。 + +dp[0][j]的话同理,所以代码如下: + +```C++ +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.两个字符串的删除操作](https://img-blog.csdnimg.cn/20210118163801914.jpg) + + +以上分析完毕,代码如下: + +```C++ +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] + 2, dp[i - 1][j] + 1, dp[i][j - 1] + 1}); + } + } + } + return dp[word1.size()][word2.size()]; + } +}; + +``` + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0589.N叉树的前序遍历.md b/problems/0589.N叉树的前序遍历.md deleted file mode 100644 index dfb7cf78..00000000 --- a/problems/0589.N叉树的前序遍历.md +++ /dev/null @@ -1,56 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/ - -## 思路 - - - -## 递归C++代码 - -``` -class Solution { -private: - vector result; - void traversal (Node* root) { - if (root == NULL) return; - result.push_back(root->val); - for (int i = 0; i < root->children.size(); i++) { - traversal(root->children[i]); - } - } - -public: - vector preorder(Node* root) { - result.clear(); - traversal(root); - return result; - } -}; -``` - -## 迭代法C++代码 - -``` -class Solution { - -public: - vector preorder(Node* root) { - vector result; - if (root == NULL) return result; - stack st; - st.push(root); - while (!st.empty()) { - Node* node = st.top(); - st.pop(); - result.push_back(node->val); - // 注意要倒叙,这样才能达到前序(中左右)的效果 - for (int i = node->children.size() - 1; i >= 0; i--) { - if (node->children[i] != NULL) { - st.push(node->children[i]); - } - } - } - return result; - } -}; -``` diff --git a/problems/0590.N叉树的后序遍历.md b/problems/0590.N叉树的后序遍历.md deleted file mode 100644 index 36c98fef..00000000 --- a/problems/0590.N叉树的后序遍历.md +++ /dev/null @@ -1,54 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/n-ary-tree-postorder-traversal/ -## 思路 - -## 递归C++代码 - -``` -class Solution { -private: - vector result; - void traversal (Node* root) { - if (root == NULL) return; - for (int i = 0; i < root->children.size(); i++) { // 子孩子 - traversal(root->children[i]); - } - result.push_back(root->val); // 中 - - } - -public: - vector postorder(Node* root) { - result.clear(); - traversal(root); - return result; - - } -}; -``` - -## 迭代法C++代码 - -``` -class Solution { -public: - vector postorder(Node* root) { - vector result; - if (root == NULL) return result; - stack st; - st.push(root); - while (!st.empty()) { - Node* node = st.top(); - st.pop(); - result.push_back(node->val); - for (int i = 0; i < node->children.size(); i++) { // 相对于前序遍历,这里反过来 - if (node->children[i] != NULL) { - st.push(node->children[i]); - } - } - } - reverse(result.begin(), result.end()); // 反转数组 - return result; - } -}; -``` diff --git a/problems/0617.合并二叉树.md b/problems/0617.合并二叉树.md index cfecb9a1..adc0703b 100644 --- a/problems/0617.合并二叉树.md +++ b/problems/0617.合并二叉树.md @@ -1,9 +1,15 @@ -## 题目地址 -https://leetcode-cn.com/problems/merge-two-binary-trees/ +

+ + + + +

+

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

-> 合并一下 -# 617.合并二叉树 +## 617.合并二叉树 + +题目地址:https://leetcode-cn.com/problems/merge-two-binary-trees/ 给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。 @@ -11,19 +17,19 @@ https://leetcode-cn.com/problems/merge-two-binary-trees/ 示例 1: - +![617.合并二叉树](https://img-blog.csdnimg.cn/20210204153634809.png) 注意: 合并必须从两个树的根节点开始。 -# 思路 +## 思路 -相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢? +相信这道题目很多同学疑惑的点是如何同时遍历两个二叉树呢? 其实和遍历一个树逻辑是一样的,只不过传入两个树的节点,同时操作。 -## 递归 +## 递归 -二叉树使用递归,就要想使用前中后哪种遍历方式? +二叉树使用递归,就要想使用前中后哪种遍历方式? **本题使用哪种遍历都是可以的!** @@ -31,7 +37,7 @@ https://leetcode-cn.com/problems/merge-two-binary-trees/ 动画如下: - +![617.合并二叉树](https://tva1.sinaimg.cn/large/008eGmZEly1gnbjjq8h16g30e20cwnpd.gif) 那么我们来按照递归三部曲来解决: @@ -45,7 +51,7 @@ https://leetcode-cn.com/problems/merge-two-binary-trees/ TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { ``` -2. **确定终止条件:** +2. **确定终止条件:** 因为是传入了两个树,那么就有两个树遍历的节点t1 和 t2,如果t1 == NULL 了,两个树合并就应该是 t2 了啊(如果t2也为NULL也无所谓,合并之后就是NULL)。 @@ -77,14 +83,14 @@ t1 的右子树:是 合并 t1右子树 t2右子树之后的右子树。 代码如下: ``` - t1->left = mergeTrees(t1->left, t2->left); - t1->right = mergeTrees(t1->right, t2->right); - return t1; +t1->left = mergeTrees(t1->left, t2->left); +t1->right = mergeTrees(t1->right, t2->right); +return t1; ``` 此时前序遍历,完整代码就写出来了,如下: -``` +```C++ class Solution { public: TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { @@ -101,7 +107,7 @@ public: 那么中序遍历也是可以的,代码如下: -``` +```C++ class Solution { public: TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { @@ -118,7 +124,7 @@ public: 后序遍历依然可以,代码如下: -``` +```C++ class Solution { public: TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { @@ -139,7 +145,7 @@ public: 不修改输入树的结构,前序遍历,代码如下: -``` +```C++ class Solution { public: TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { @@ -155,7 +161,7 @@ public: }; ``` -## 迭代法 +## 迭代法 使用迭代法,如何同时处理两棵树呢? @@ -163,7 +169,7 @@ public: 本题我们也使用队列,模拟的层序遍历,代码如下: -``` +```C++ class Solution { public: TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) { @@ -203,14 +209,14 @@ public: }; ``` -# 拓展 +## 拓展 当然也可以秀一波指针的操作,这是我写的野路子,大家就随便看看就行了,以防带跑遍了。 如下代码中,想要更改二叉树的值,应该传入指向指针的指针。 代码如下:(前序遍历) -``` +```C++ class Solution { public: void process(TreeNode** t1, TreeNode** t2) { @@ -235,7 +241,7 @@ public: }; ``` -# 总结 +## 总结 合并二叉树,也是二叉树操作的经典题目,如果没有接触过的话,其实并不简单,因为我们习惯了操作一个二叉树,一起操作两个二叉树,还会有点懵懵的。 @@ -245,6 +251,23 @@ public: 最后拓展中,我给了一个操作指针的野路子,大家随便看看就行了,如果学习C++的话,可以在去研究研究。 -就酱,学到了的话,就转发给身边需要的同学吧! -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0637.二叉树的层平均值.md b/problems/0637.二叉树的层平均值.md deleted file mode 100644 index 5181d342..00000000 --- a/problems/0637.二叉树的层平均值.md +++ /dev/null @@ -1,50 +0,0 @@ -## 题目地址 - -## 思路 - -这道题目就是考察二叉树的层序遍历。 - -二叉树的层序遍历还有这两道题目,大家可以先做一下,其实都是一个思路 - -* [0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md) 单纯的层寻遍历 -* |[0107.二叉树的层次遍历II](https://github.com/youngyangyang04/leetcode/blob/master/problems/0107.二叉树的层次遍历II.md) 相对于[0102.二叉树的层序遍历](https://github.com/youngyangyang04/leetcode/blob/master/problems/0102.二叉树的层序遍历.md)就是把每一层倒叙输出就可以了 - -想要学习二叉树的深度遍历可以看这里[彻底吃透前中后序递归法](https://leetcode-cn.com/problems/binary-tree-preorder-traversal/solution/dai-ma-sui-xiang-lu-chi-tou-qian-zhong-hou-xu-de-d/)。 - -而本题呢,就是把每一层的结果求一个和,然后取平均值。 - -层序遍历一个二叉树,需要借用一个辅助数据结构队列来实现,**队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。** - -使用队列实现广度优先遍历,动画如下: - - - -这样就实现了层序从左到右遍历二叉树。 - -代码如下:(这份代码也可以作为二叉树层序遍历的模板)。 - -## C++代码 - -``` -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++) { // 这里一定要使用固定大小size,不要使用que.size() - 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; - } -}; -``` diff --git a/problems/0647.回文子串.md b/problems/0647.回文子串.md new file mode 100644 index 00000000..fbc9133e --- /dev/null +++ b/problems/0647.回文子串.md @@ -0,0 +1,238 @@ +

+ + + + +

+

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

+ +## 647. 回文子串 + +题目链接:https://leetcode-cn.com/problems/palindromic-substrings/ + +给定一个字符串,你的任务是计算这个字符串中有多少个回文子串。 + +具有不同开始位置或结束位置的子串,即使是由相同的字符组成,也会被视作不同的子串。 + +示例 1: + +输入:"abc" +输出:3 +解释:三个回文子串: "a", "b", "c" + +示例 2: + +输入:"aaa" +输出:6 +解释:6个回文子串: "a", "a", "a", "aa", "aa", "aaa" + +提示: + +输入的字符串长度不会超过 1000 。 + +## 暴力解法 + +两层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。 + +以上三种情况分析完了,那么递归公式如下: + +```C++ +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://img-blog.csdnimg.cn/20210121171032473.jpg) + +如果这矩阵是从上到下,从左到右遍历,那么会用到没有计算过的dp[i + 1][j - 1],也就是根据不确定是不是回文的区间[i+1,j-1],来判断了[i,j]是不是回文,那结果一定是不对的。 + +**所以一定要从下到上,从左到右遍历,这样保证dp[i + 1][j - 1]都是经过计算的**。 + +有的代码实现是优先遍历列,然后遍历行,其实也是一个道理,都是为了保证dp[i + 1][j - 1]都是经过计算的。 + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210121171059951.jpg) + +图中有6个true,所以就是有6个回文子串。 + +**注意因为dp[i][j]的定义,所以j一定是大于等于i的,那么在填充dp[i][j]的时候一定是只填充右上半部分**。 + +以上分析完毕,C++代码如下: + +```C++ +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; + } +}; +``` +以上代码是为了凸显情况一二三,当然是可以简洁一下的,如下: + +```C++ +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) + +## 双指针法 + +动态规划的空间复杂度是偏高的,我们再看一下双指针法。 + +首先确定回文串,就是找中心然后想两边扩散看是不是对称的就可以了。 + +**在遍历中心点的时候,要注意中心点有两种情况**。 + +一个元素可以作为中心点,两个元素也可以作为中心点。 + +那么有人同学问了,三个元素还可以做中心点呢。其实三个元素就可以由一个元素左右添加元素得到,四个元素则可以由两个元素左右添加元素得到。 + +所以我们在计算的时候,要注意一个元素为中心点和两个元素为中心点的情况。 + +**这两种情况可以放在一起计算,但分别计算思路更清晰,我倾向于分别计算**,代码如下: + +```C++ +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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0649.Dota2参议院.md b/problems/0649.Dota2参议院.md deleted file mode 100644 index d6a2a12f..00000000 --- a/problems/0649.Dota2参议院.md +++ /dev/null @@ -1,70 +0,0 @@ - -## 思路 - -这道题 题意太绕了,我举一个更形象的例子给大家捋顺一下。 - -例如输入"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://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg) ,相信看完之后对贪心就有基本的了解了。 - -## 代码实现 - -实现代码,在每一轮循环的过程中,去过模拟优先消灭身后的对手,其实是比较麻烦的。 - -这里有一个技巧,就是用一个变量记录当前参议员之前有几个敌对对手了,进而判断自己是否被消灭了。这个变量我用flag来表示。 - -C++代码如下: - - -``` -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"; - } -}; -``` - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0654.最大二叉树.md b/problems/0654.最大二叉树.md index 64efda5c..af133e0c 100644 --- a/problems/0654.最大二叉树.md +++ b/problems/0654.最大二叉树.md @@ -1,9 +1,15 @@ -## 题目地址 -https://leetcode-cn.com/problems/maximum-binary-tree/ +

+ + + + +

+

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

-> 用数组构建二叉树都是一样的套路 -# 654.最大二叉树 +## 654.最大二叉树 + +题目地址:https://leetcode-cn.com/problems/maximum-binary-tree/ 给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下: @@ -15,21 +21,21 @@ https://leetcode-cn.com/problems/maximum-binary-tree/ 示例 : - +![654.最大二叉树](https://img-blog.csdnimg.cn/20210204154534796.png) 提示: -给定的数组的大小在 [1, 1000] 之间。 +给定的数组的大小在 [1, 1000] 之间。 -## 思路 +## 思路 最大二叉树的构建过程如下: - +![654.最大二叉树](https://tva1.sinaimg.cn/large/008eGmZEly1gnbjuvioezg30dw0921ck.gif) 构造树一般采用的是前序遍历,因为先构造中间节点,然后递归构造左子树和右子树。 -* 确定递归函数的参数和返回值 +* 确定递归函数的参数和返回值 参数就是传入的是存放元素的数组,返回该数组构造的二叉树的头结点,返回类型是指向节点的指针。 @@ -39,7 +45,7 @@ https://leetcode-cn.com/problems/maximum-binary-tree/ TreeNode* constructMaximumBinaryTree(vector& nums) ``` -* 确定终止条件 +* 确定终止条件 题目中说了输入的数组大小一定是大于等于1的,所以我们不用考虑小于1的情况,那么当递归遍历的时候,如果传入的数组大小为1,说明遍历到了叶子节点了。 @@ -55,7 +61,7 @@ if (nums.size() == 1) { } ``` -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 这里有三步工作 @@ -75,7 +81,7 @@ TreeNode* node = new TreeNode(0); node->val = maxValue; ``` -2. 最大值所在的下表左区间 构造左子树 +2. 最大值所在的下表左区间 构造左子树 这里要判断maxValueIndex > 0,因为要保证左区间至少有一个数值。 @@ -87,11 +93,11 @@ if (maxValueIndex > 0) { } ``` -3. 最大值所在的下表右区间 构造右子树 +3. 最大值所在的下表右区间 构造右子树 判断maxValueIndex < (nums.size() - 1),确保右区间至少有一个数值。 -代码如下: +代码如下: ``` if (maxValueIndex < (nums.size() - 1)) { @@ -101,7 +107,7 @@ if (maxValueIndex < (nums.size() - 1)) { ``` 这样我们就分析完了,整体代码如下:(详细注释) -``` +```C++ class Solution { public: TreeNode* constructMaximumBinaryTree(vector& nums) { @@ -122,7 +128,7 @@ public: node->val = maxValue; // 最大值所在的下表左区间 构造左子树 if (maxValueIndex > 0) { - vector newVec(nums.begin(), nums.begin() + maxValueIndex); + vector newVec(nums.begin(), nums.begin() + maxValueIndex); node->left = constructMaximumBinaryTree(newVec); } // 最大值所在的下表右区间 构造右子树 @@ -141,7 +147,7 @@ public: 优化后代码如下: -``` +```C++ class Solution { private: // 在左闭右开区间[left, right),构造二叉树 @@ -171,15 +177,15 @@ public: }; ``` -# 拓展 +## 拓展 -可以发现上面的代码看上去简洁一些,**主要是因为第二版其实是允许空节点进入递归,所以不用在递归的时候加判断节点是否为空** +可以发现上面的代码看上去简洁一些,**主要是因为第二版其实是允许空节点进入递归,所以不用在递归的时候加判断节点是否为空** 第一版递归过程:(加了if判断,为了不让空节点进入递归) -``` +```C++ if (maxValueIndex > 0) { // 这里加了判断是为了不让空节点进入递归 - vector newVec(nums.begin(), nums.begin() + maxValueIndex); + vector newVec(nums.begin(), nums.begin() + maxValueIndex); node->left = constructMaximumBinaryTree(newVec); } @@ -204,7 +210,7 @@ root->right = traversal(nums, maxValueIndex + 1, right); 第二版相应的终止条件,是遇到空节点,也就是数组区间为0,就终止了。 -# 总结 +## 总结 这道题目其实和 [二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) 是一个思路,比[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) 还简单一些。 @@ -212,8 +218,24 @@ root->right = traversal(nums, maxValueIndex + 1, right); 一些同学也会疑惑,什么时候递归函数前面加if,什么时候不加if,这个问题我在最后也给出了解释。 -其实就是不同代码风格的实现,**一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。** +其实就是不同代码风格的实现,**一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +----------------------- +* 作者微信:[程序员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/0657.机器人能否返回原点.md b/problems/0657.机器人能否返回原点.md deleted file mode 100644 index fb74c751..00000000 --- a/problems/0657.机器人能否返回原点.md +++ /dev/null @@ -1,38 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/robot-return-to-origin/ - -## 思路 - -这道题目还是挺简单的,大家不要想复杂了,一波哈希法又一波图论算法啥的,哈哈。 - -其实就是,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++代码 - -``` -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」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0669.修剪二叉搜索树.md b/problems/0669.修剪二叉搜索树.md index 641416b0..41f684f4 100644 --- a/problems/0669.修剪二叉搜索树.md +++ b/problems/0669.修剪二叉搜索树.md @@ -1,13 +1,16 @@ +

+ + + + +

+

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

-## 题目链接 - -https://leetcode-cn.com/problems/trim-a-binary-search-tree/ > 如果不对递归有深刻的理解,本题有点难 - > 单纯移除一个节点那还不够,要修剪! -# 669. 修剪二叉搜索树 +## 669. 修剪二叉搜索树 题目链接:https://leetcode-cn.com/problems/trim-a-binary-search-tree/ @@ -17,7 +20,7 @@ https://leetcode-cn.com/problems/trim-a-binary-search-tree/ ![669.修剪二叉搜索树1](https://img-blog.csdnimg.cn/20201014173219142.png) -# 思路 +## 思路 相信看到这道题目大家都感觉是一道简单题(事实上leetcode上也标明是简单)。 @@ -29,7 +32,7 @@ https://leetcode-cn.com/problems/trim-a-binary-search-tree/ 不难写出如下代码: -``` +```C++ class Solution { public: TreeNode* trimBST(TreeNode* root, int low, int high) { @@ -45,7 +48,7 @@ public: 我们在重新关注一下第二个示例,如图: - +![669.修剪二叉搜索树](https://img-blog.csdnimg.cn/20210204155302751.png) **所以以上的代码是不可行的!** @@ -55,12 +58,12 @@ public: 在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图: - +![669.修剪二叉搜索树1](https://img-blog.csdnimg.cn/20210204155327203.png) 理解了最关键部分了我们在递归三部曲: -* 确定递归函数的参数以及返回值 +* 确定递归函数的参数以及返回值 这里我们为什么需要返回值呢? @@ -76,7 +79,7 @@ public: TreeNode* trimBST(TreeNode* root, int low, int high) ``` -* 确定终止条件 +* 确定终止条件 修剪的操作并不是在终止条件上进行的,所以就是遇到空节点返回就可以了。 @@ -84,7 +87,7 @@ TreeNode* trimBST(TreeNode* root, int low, int high) if (root == nullptr ) return nullptr; ``` -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 如果root(当前节点)的元素小于low的数值,那么应该递归右子树,并返回右子树符合条件的头结点。 @@ -118,11 +121,11 @@ root->right = trimBST(root->right, low, high); // root->right接入符合条件 return root; ``` -此时大家是不是还没发现这多余的节点究竟是如何从二叉树中移除的呢? +此时大家是不是还没发现这多余的节点究竟是如何从二叉树中移除的呢? -在回顾一下上面的代码,针对下图中二叉树的情况: +在回顾一下上面的代码,针对下图中二叉树的情况: - +![669.修剪二叉搜索树1](https://img-blog.csdnimg.cn/20210204155327203.png) 如下代码相当于把节点0的右孩子(节点2)返回给上一层, ``` @@ -142,7 +145,7 @@ root->left = trimBST(root->left, low, high); 最后整体代码如下: -``` +```C++ class Solution { public: TreeNode* trimBST(TreeNode* root, int low, int high) { @@ -162,15 +165,15 @@ public: }; ``` -精简之后代码如下: +精简之后代码如下: -``` +```C++ 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); + 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; @@ -180,26 +183,26 @@ public: 只看代码,其实不太好理解节点是符合移除的,这一块大家可以自己在模拟模拟! -## 迭代法 +## 迭代法 因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。 在剪枝的时候,可以分为三步: -* 将root移动到[L, R] 范围内,注意是左闭右闭区间 -* 剪枝左子树 +* 将root移动到[L, R] 范围内,注意是左闭右闭区间 +* 剪枝左子树 * 剪枝右子树 代码如下: -``` +```C++ class Solution { public: TreeNode* trimBST(TreeNode* root, int L, int R) { if (!root) return nullptr; // 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭 - while (root->val < L || root->val > R) { + while (root != nullptr && (root->val < L || root->val > R)) { if (root->val < L) root = root->right; // 小于L往右走 else root = root->left; // 大于R往左走 } @@ -225,14 +228,32 @@ public: }; ``` -# 总结 +## 总结 修剪二叉搜索树其实并不难,但在递归法中大家可看出我费了很大的功夫来讲解如何删除节点的,这个思路其实是比较绕的。 -最终的代码倒是很简洁。 +最终的代码倒是很简洁。 **如果不对递归有深刻的理解,这道题目还是有难度的!** 本题我依然给出递归法和迭代法,初学者掌握递归就可以了,如果想进一步学习,就把迭代法也写一写。 -**就酱,如果学到了,就转发给身边需要的同学吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0674.最长连续递增序列.md b/problems/0674.最长连续递增序列.md new file mode 100644 index 00000000..ad18029e --- /dev/null +++ b/problems/0674.最长连续递增序列.md @@ -0,0 +1,173 @@ +

+ + + + +

+

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

+ +## 674. 最长连续递增序列 + +题目链接:https://leetcode-cn.com/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 + + +## 思路 + +本题相对于昨天的[动态规划:300.最长递增子序列](https://mp.weixin.qq.com/s/f8nLO3JGfgriXep_gJQpqQ)最大的区别在于“连续”。 + +本题要求的是最长**连续**递增序列 + +### 动态规划 + +动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +**dp[i]:以下标i为结尾的数组的连续递增的子序列长度为dp[i]**。 + +注意这里的定义,一定是以下标i为结尾,并不是说一定以下标0为起始位置。 + +2. 确定递推公式 + +如果 nums[i + 1] > nums[i],那么以 i+1 为结尾的数组的连续递增的子序列长度 一定等于 以i为结尾的数组的连续递增的子序列长度 + 1 。 + +即:dp[i + 1] = dp[i] + 1; + +**注意这里就体现出和[动态规划:300.最长递增子序列](https://mp.weixin.qq.com/s/f8nLO3JGfgriXep_gJQpqQ)的区别!** + +因为本题要求连续递增子序列,所以就必要比较nums[i + 1]与nums[i],而不用去比较nums[j]与nums[i] (j是在0到i之间遍历)。 + +既然不用j了,那么也不用两层for循环,本题一层for循环就行,比较nums[i + 1] 和 nums[i]。 + +这里大家要好好体会一下! + +3. dp数组如何初始化 + +以下标i为结尾的数组的连续递增的子序列长度最少也应该是1,即就是nums[i]这一个元素。 + +所以dp[i]应该初始1; + +4. 确定遍历顺序 + +从递推公式上可以看出, dp[i + 1]依赖dp[i],所以一定是从前向后遍历。 + +本文在确定递推公式的时候也说明了为什么本题只需要一层for循环,代码如下: + +```C++ +for (int i = 0; i < nums.size() - 1; i++) { + if (nums[i + 1] > nums[i]) { // 连续记录 + dp[i + 1] = dp[i] + 1; // 递推公式 + } +} +``` + +5. 举例推导dp数组 + +已输入nums = [1,3,5,4,7]为例,dp数组状态如下: + +![674.最长连续递增序列](https://img-blog.csdnimg.cn/20210204103529742.jpg) + +**注意这里要取dp[i]里的最大值,所以dp[2]才是结果!** + +以上分析完毕,C++代码如下: + +```C++ +class Solution { +public: + int findLengthOfLCIS(vector& nums) { + if (nums.size() == 0) return 0; + int result = 1; + vector dp(nums.size() ,1); + for (int i = 0; i < nums.size() - 1; i++) { + if (nums[i + 1] > nums[i]) { // 连续记录 + dp[i + 1] = dp[i] + 1; + } + if (dp[i + 1] > result) result = dp[i + 1]; + } + return result; + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +### 贪心 + +这道题目也可以用贪心来做,也就是遇到nums[i + 1] > nums[i]的情况,count就++,否则count为1,记录count的最大值就可以了。 + +代码如下: + +```C++ +class Solution { +public: + int findLengthOfLCIS(vector& nums) { + if (nums.size() == 0) return 0; + int result = 1; // 连续子序列最少也是1 + int count = 1; + for (int i = 0; i < nums.size() - 1; i++) { + if (nums[i + 1] > nums[i]) { // 连续记录 + count++; + } else { // 不连续,count从头开始 + count = 1; + } + if (count > result) result = count; + } + return result; + } +}; +``` +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +## 总结 + +本题也是动规里子序列问题的经典题目,但也可以用贪心来做,大家也会发现贪心好像更简单一点,而且空间复杂度仅是O(1)。 + +在动规分析中,关键是要理解和[动态规划:300.最长递增子序列](https://mp.weixin.qq.com/s/f8nLO3JGfgriXep_gJQpqQ)的区别。 + +**要联动起来,才能理解递增子序列怎么求,递增连续子序列又要怎么求**。 + +概括来说:不连续递增子序列的跟前0-i 个状态有关,连续递增的子序列只跟前一个状态有关 + +本篇我也把区别所在之处重点介绍了,关键在递推公式和遍历方法上,大家可以仔细体会一波! + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0685.冗余连接II.md b/problems/0685.冗余连接II.md deleted file mode 100644 index d8b5f5ec..00000000 --- a/problems/0685.冗余连接II.md +++ /dev/null @@ -1,175 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/redundant-connection-ii/ - -## 思路 - -先重点读懂题目中的这句**该图由一个有着N个节点 (节点值不重复1, 2, ..., N) 的树及一条附加的边构成。附加的边的两个顶点包含在1到N中间,这条附加的边不属于树中已存在的边。** - -**这说明题目中的图原本是是一棵树,只不过在不增加节点的情况下多加了一条边!** - -还有**若有多个答案,返回最后出现在给定二维数组的答案。**这说明在两天边都可以删除的情况下,要删顺序靠后的! - - -那么有如下三种情况,前两种情况是出现入度为2的点,如图: - - - -且只有一个节点入度为2,为什么不看出度呢,出度没有意义,一颗树中随便一个父节点就有多个出度。 - - -第三种情况是没有入度为2的点,那么图中一定出现了有向环(**注意这里强调是有向环!**) - -如图: - - - - -首先先计算节点的入度,代码如下: - -``` - int inDegree[N] = {0}; // 记录节点入度 - n = edges.size(); // 边的数量 - for (int i = 0; i < n; i++) { - inDegree[edges[i][1]]++; // 统计入度 - } -``` - -前两种入度为2的情况,一定是删除指向入度为2的节点的两条边其中的一条,如果删了一条,判断这个图是一个树,那么这条边就是答案,同时注意要从后向前遍历,因为如果两天边删哪一条都可以成为树,就删最后那一条。 - -代码如下: - -``` - 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的情况,那么一定有有向环,找到构成环的边就是要删除的边。 - -可以定义一个函数,代码如下: - -``` -// 在有向图里找到删除的那条边,使其变成树,返回值就是要删除的边 -vector getRemoveEdge(const vector>& edges) -``` - -此时 大家应该知道了,我们要实现两个最为关键的函数: - -* `isTreeAfterRemoveEdge()` 判断删一个边之后是不是树了 -* `getRemoveEdge` 确定图中一定有了有向环,那么要找到需要删除的那条边 - -此时应该是用到**并查集**了,并查集为什么可以判断 一个图是不是树呢? - -**因为如果两个点所在的边在添加图之前如果就可以在并查集里找到了相同的根,那么这条边添加上之后 这个图一定不是树了** - -这里对并查集就不展开过多的讲解了,翻到了自己九年前写过了一篇并查集的文章[并查集学习](https://blog.csdn.net/youngyangyang04/article/details/6447435),哈哈,那时候还太年轻,写不咋地,有空我会重写一篇! - -本题代码如下:(详细注释了) - -## C++代码 - -``` - -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); - - } -}; -``` - - - - - - - diff --git a/problems/0698.划分为k个相等的子集.md b/problems/0698.划分为k个相等的子集.md deleted file mode 100644 index 23382bbe..00000000 --- a/problems/0698.划分为k个相等的子集.md +++ /dev/null @@ -1,42 +0,0 @@ - -使用回溯法,首先要明确这是组合问题,即集合里是不强调顺序的,所以要用startIndex - -``` -class Solution { -private: - bool backtracking(vector& nums, - int k, - int target, // 子集目标和 - int cur, // 当前目标和 - int startIndex, // 起始位置 - vector& used) { // 标记是否使用过 - if (k == 0) return true; // 找到了k个相同子集 - if (cur == target) { // 发现一个合格子集,然后重新开始寻找 - return backtracking(nums, k - 1, target, 0, 0, used); // k-1 - } - for (int i = startIndex; i < nums.size(); i++) { - if (cur + nums[i] <= target && !used[i]) { - used[i] = true; - if (backtracking(nums, k, target, cur + nums[i], i + 1, used)) { - return true; - } - used[i] = false; - } - } - return false; - } -public: - bool canPartitionKSubsets(vector& nums, int k) { - //sort(nums.begin(), nums.end()); 不需要排序 - int sum = 0; - for (int i = 0; i < nums.size(); i++) { - sum += nums[i]; - } - if (sum % k != 0) return false; - int target = sum / k; - vector used(nums.size(), false); - - return backtracking(nums, k, target, 0, 0, used); - } -}; -``` diff --git a/problems/0700.二叉搜索树中的搜索.md b/problems/0700.二叉搜索树中的搜索.md index cdf713be..5c1cdfdf 100644 --- a/problems/0700.二叉搜索树中的搜索.md +++ b/problems/0700.二叉搜索树中的搜索.md @@ -1,19 +1,25 @@ -## 题目地址 -https://leetcode-cn.com/problems/search-in-a-binary-search-tree/ +

+ + + + +

+

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

-> 二叉搜索树登场! -# 700.二叉搜索树中的搜索 +## 700.二叉搜索树中的搜索 + +题目地址:https://leetcode-cn.com/problems/search-in-a-binary-search-tree/ 给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。 例如, - +![700.二叉搜索树中的搜索](https://img-blog.csdnimg.cn/20210204155522476.png) -在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。 +在上述示例中,如果要找的值是 5,但因为没有节点值为 5,我们应该返回 NULL。 -# 思路 +## 思路 之前我们讲了都是普通二叉树,那么接下来看看二叉搜索树。 @@ -23,7 +29,7 @@ https://leetcode-cn.com/problems/search-in-a-binary-search-tree/ * 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值; * 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值; -* 它的左、右子树也分别为二叉搜索树 +* 它的左、右子树也分别为二叉搜索树 这就决定了,二叉搜索树,递归遍历和迭代遍历和普通二叉树都不一样。 @@ -60,7 +66,7 @@ if (root == NULL || root->val == val) return root; 代码如下: ``` -if (root->val > val) return searchBST(root->left, val); // 注意这里加了return +if (root->val > val) return searchBST(root->left, val); // 注意这里加了return if (root->val < val) return searchBST(root->right, val); return NULL; ``` @@ -73,7 +79,7 @@ return NULL; 整体代码如下: -``` +```C++ class Solution { public: TreeNode* searchBST(TreeNode* root, int val) { @@ -93,7 +99,7 @@ public: 对于一般二叉树,递归过程中还有回溯的过程,例如走一个左方向的分支走到头了,那么要调头,在走右分支。 -而**对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。** +而**对于二叉搜索树,不需要回溯的过程,因为节点的有序性就帮我们确定了搜索的方向。** 例如要搜索元素为3的节点,**我们不需要搜索其他节点,也不需要做回溯,查找的路径已经规划好了。** @@ -103,7 +109,7 @@ public: 所以迭代法代码如下: -``` +```C++ class Solution { public: TreeNode* searchBST(TreeNode* root, int val) { @@ -119,7 +125,7 @@ public: 第一次看到了如此简单的迭代法,是不是感动的痛哭流涕,哭一会~ -# 总结 +## 总结 本篇我们介绍了二叉搜索树的遍历方式,因为二叉搜索树的有序性,遍历的时候要比普通二叉树简单很多。 @@ -129,9 +135,25 @@ public: 文中我依然给出递归和迭代两种方式,可以看出写法都非常简单,就是利用了二叉搜索树有序的特点。 -就酱,如果学到了,就转发给身边需要的同学吧! -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0701.二叉搜索树中的插入操作.md b/problems/0701.二叉搜索树中的插入操作.md index babb3025..760509d9 100644 --- a/problems/0701.二叉搜索树中的插入操作.md +++ b/problems/0701.二叉搜索树中的插入操作.md @@ -1,11 +1,15 @@ -## 题目地址 -https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ +

+ + + + +

+

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

-> 开始修改二叉搜索树 -# 701.二叉搜索树中的插入操作 +## 701.二叉搜索树中的插入操作 -链接:https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ +链接:https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ 给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。 @@ -20,7 +24,7 @@ https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ * -10^8 <= val <= 10^8 * 新值和原始二叉搜索树中的任意节点值都不同 -# 思路 +## 思路 其实这道题目其实是一道简单题目,**但是题目中的提示:有多种有效的插入方式,还可以重构二叉搜索树,一下子吓退了不少人**,瞬间感觉题目复杂了很多。 @@ -28,7 +32,7 @@ https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ 如下演示视频中可以看出:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。 - +![701.二叉搜索树中的插入操作](https://tva1.sinaimg.cn/large/008eGmZEly1gnbk63ina5g30eo08waja.gif) 例如插入元素10 ,需要找到末尾节点插入便可,一样的道理来插入元素15,插入元素0,插入元素6,**需要调整二叉树的结构么? 并不需要。**。 @@ -40,7 +44,7 @@ https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ 递归三部曲: -* 确定递归函数参数以及返回值 +* 确定递归函数参数以及返回值 参数就是根节点指针,以及要插入元素,这里递归函数要不要有返回值呢? @@ -53,7 +57,7 @@ https://leetcode-cn.com/problems/insert-into-a-binary-search-tree/ 代码如下: ``` -TreeNode* insertIntoBST(TreeNode* root, int val) +TreeNode* insertIntoBST(TreeNode* root, int val) ``` * 确定终止条件 @@ -71,9 +75,9 @@ if (root == NULL) { 这里把添加的节点返回给上一层,就完成了父子节点的赋值操作了,详细再往下看。 -* 确定单层递归的逻辑 +* 确定单层递归的逻辑 -此时要明确,需要遍历整棵树么? +此时要明确,需要遍历整棵树么? 别忘了这是搜索树,遍历整颗搜索树简直是对搜索树的侮辱,哈哈。 @@ -92,7 +96,7 @@ return root; 整体代码如下: -``` +```C++ class Solution { public: TreeNode* insertIntoBST(TreeNode* root, int val) { @@ -118,11 +122,11 @@ TreeNode* parent; // 记录遍历节点的父节点 void traversal(TreeNode* cur, int val) ``` -没有返回值,需要记录上一个节点(parent),遇到空节点了,就让parent左孩子或者右孩子指向新插入的节点。然后结束递归。 +没有返回值,需要记录上一个节点(parent),遇到空节点了,就让parent左孩子或者右孩子指向新插入的节点。然后结束递归。 代码如下: -``` +```C++ class Solution { private: TreeNode* parent; @@ -168,7 +172,7 @@ public: 代码如下: -``` +```C++ class Solution { public: TreeNode* insertIntoBST(TreeNode* root, int val) { @@ -191,7 +195,7 @@ public: }; ``` -# 总结 +## 总结 首先在二叉搜索树中的插入操作,大家不用恐惧其重构搜索树,其实根本不用重构。 @@ -199,8 +203,23 @@ public: 最后依然给出了迭代的方法,迭代的方法就需要记录当前遍历节点的父节点了,这个和没有返回值的递归函数实现的代码逻辑是一样的。 -**就酱,学到了就转发给身边需要的同学吧!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +----------------------- +* 作者微信:[程序员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/0704.二分查找.md b/problems/0704.二分查找.md new file mode 100644 index 00000000..9261e135 --- /dev/null +++ b/problems/0704.二分查找.md @@ -0,0 +1,165 @@ +

+ + + + +

+

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

+ + +## 704. 二分查找 + +题目链接:https://leetcode-cn.com/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]之间。 + + +## 思路 + +**这道题目的前提是数组为有序数组**,同时题目还强调**数组中无重复元素**,因为一旦有重复元素,使用二分查找法返回的元素下标可能不是唯一的,这些都是使用二分法的前提条件,当大家看到题目描述满足如上条件的时候,可要想一想是不是可以用二分法了。 + +二分查找涉及的很多的边界条件,逻辑比较简单,但就是写不好。例如到底是 `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://img-blog.csdnimg.cn/20210311153055723.jpg) + +代码如下:(详细注释) + +```C++ +// 版本一 +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; + } +}; + +``` + +### 二分法第二种写法 + +如果说定义 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://img-blog.csdnimg.cn/20210311153123632.jpg) + +代码如下:(详细注释) + +```C++ +// 版本二 +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; + } +}; +``` + +## 总结 + +二分法是非常重要的基础算法,为什么很多同学对于二分法都是**一看就会,一写就废**? + +其实主要就是对区间的定义没有理解清楚,在循环中没有始终坚持根据查找区间的定义来做边界处理。 + +区间的定义就是不变量,那么在循环中坚持根据查找区间的定义来做边界处理,就是循环不变量规则。 + +本篇根据两种常见的区间定义,给出了两种二分法的写法,每一个边界为什么这么处理,都根据区间的定义做了详细介绍。 + +相信看完本篇应该对二分法有更深刻的理解了。 + +## 相关题目推荐 + +* [35.搜索插入位置](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) +* 34.在排序数组中查找元素的第一个和最后一个位置 +* 69.x 的平方根 +* 367.有效的完全平方数 + + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0705.设计哈希集合.md b/problems/0705.设计哈希集合.md deleted file mode 100644 index f6d56e1c..00000000 --- a/problems/0705.设计哈希集合.md +++ /dev/null @@ -1,35 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/design-hashset/ - -## 思路 - -使用数组便可以实现这个哈希集合 - -## 代码 -``` -class MyHashSet { - -public: - vector hashTable; - /** Initialize your data structure here. */ - MyHashSet() { - vector table(1000001, 0); - hashTable = table; - } - - void add(int key) { - hashTable[key] = 1; - } - - void remove(int key) { - hashTable[key] = 0; - } - - /** Returns true if this set contains the specified element */ - bool contains(int key) { - return hashTable[key]; - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 - diff --git a/problems/0707.设计链表.md b/problems/0707.设计链表.md index 01bd71f3..f98051c9 100644 --- a/problems/0707.设计链表.md +++ b/problems/0707.设计链表.md @@ -1,10 +1,17 @@ -# 题目地址 +

+ + + + +

+

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

-https://leetcode-cn.com/problems/design-linked-list/ > 听说这道题目把链表常见的五个操作都覆盖了? -# 第707题:设计链表 +# 707.设计链表 + +https://leetcode-cn.com/problems/design-linked-list/ 题意: @@ -42,14 +49,14 @@ https://leetcode-cn.com/problems/design-linked-list/ **链表操作的两种方式:** -1. 直接使用原来的链表来进行操作。 +1. 直接使用原来的链表来进行操作。 2. 设置一个虚拟头结点在进行操作。 下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)。 -## 代码 -``` +## 代码 +```C++ class MyLinkedList { public: // 定义链表节点结构体 @@ -143,5 +150,25 @@ private: }; ``` -> 更过算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0714.买卖股票的最佳时机含手续费.md b/problems/0714.买卖股票的最佳时机含手续费.md index 3b5b6340..92697a64 100644 --- a/problems/0714.买卖股票的最佳时机含手续费.md +++ b/problems/0714.买卖股票的最佳时机含手续费.md @@ -1,24 +1,62 @@ +

+ + + + +

+

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

+ + +## 714. 买卖股票的最佳时机含手续费 + +题目链接:https://leetcode-cn.com/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://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg),多添加了一个条件就是手续费。 -## 贪心算法 +## 贪心算法 -在[贪心算法:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg)中使用贪心策略不用关系具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。 +在[贪心算法:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg)中使用贪心策略不用关心具体什么时候买卖,只要收集每天的正利润,最后稳稳的就是最大利润了。 -而本题有了手续费,就要关系什么时候买卖了,因为只计算所获得利润,可能不足以手续费。 +而本题有了手续费,就要关系什么时候买卖了,因为计算所获得利润,需要考虑买卖利润可能不足以手续费的情况。 如果使用贪心策略,就是最低值买,最高值(如果算上手续费还盈利)就卖。 此时无非就是要找到两个点,买入日期,和卖出日期。 * 买入日期:其实很好想,遇到更低点就记录一下。 -* 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+费用),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天。 +* 卖出日期:这个就不好算了,但也没有必要算出准确的卖出日期,只要当前价格大于(最低价格+手续费),就可以收获利润,至于准确的卖出日期,就是连续收获利润区间里的最后一天(并不需要计算是具体哪一天)。 -所以我们在做收获利润操作的时候其实有两种情况: +所以我们在做收获利润操作的时候其实有三种情况: * 情况一:收获利润的这一天并不是收获利润区间里的最后一天(不是真正的卖出,相当于持有股票),所以后面要继续收获利润。 -* 情况二:收获利润的这一天是收获利润区间里的最后一天(相当于真正的卖出了),后面要重新记录最小价格了。 +* 情况二:前一天是收获利润区间里的最后一天(相当于真正的卖出了),今天要重新记录最小价格了。 +* 情况三:不作操作,保持原有状态(买入,卖出,不买不卖) 贪心算法C++代码如下: @@ -29,8 +67,13 @@ public: 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) minPrice = prices[i]; + + // 情况三:保持原有状态(因为此时买则不便宜,卖则亏本) + if (prices[i] >= minPrice && prices[i] <= minPrice + fee) { + continue; + } // 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出 if (prices[i] > minPrice + fee) { @@ -43,14 +86,16 @@ public: }; ``` -从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费, -**所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!** +* 时间复杂度:O(n) +* 空间复杂度:O(1) -理解这里很关键,其实也是核心所在,很多题解关于这块都没有说清楚。 +从代码中可以看出对情况一的操作,因为如果还在收获利润的区间里,表示并不是真正的卖出,而计算利润每次都要减去手续费,**所以要让minPrice = prices[i] - fee;,这样在明天收获利润的时候,才不会多减一次手续费!** -## 动态规划 +大家也可以发现,情况三,那块代码是可以删掉的,我是为了让代码表达清晰,所以没有精简。 -我在「代码随想录」公众号里正在讲解贪心算法,将在下一个系列详细讲解动态规划,所以本题解先给出我的C++代码(带详细注释),感兴趣的同学可以自己先学习一下。 +## 动态规划 + +我在公众号「代码随想录」里将在下一个系列详细讲解动态规划,所以本题解先给出我的C++代码(带详细注释),感兴趣的同学可以自己先学习一下。 相对于[贪心算法:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg)的动态规划解法中,只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 @@ -66,9 +111,7 @@ public: 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] - fee); } return max(dp[n - 1][0], dp[n - 1][1]); @@ -76,6 +119,9 @@ public: }; ``` +* 时间复杂度:O(n) +* 空间复杂度:O(n) + 当然可以对空间经行优化,因为当前状态只是依赖前一个状态。 C++ 代码如下: @@ -96,7 +142,32 @@ public: } }; ``` +* 时间复杂度:O(n) +* 空间复杂度:O(1) -细心的同学可能发现,在计算saleStock的时候 使用的已经是最新的holdStock了,理论上应该使用上一个状态的holdStock即(i-1时候的holdstock),但是 +## 总结 + +本题贪心的思路其实是比较难的,动态规划才是常规做法,但也算是给大家拓展一下思路,感受一下贪心的魅力。 + +后期我们在讲解 股票问题系列的时候,会用动规的方式把股票问题穿个线。 +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0714.买卖股票的最佳时机含手续费(动态规划).md b/problems/0714.买卖股票的最佳时机含手续费(动态规划).md new file mode 100644 index 00000000..3926643d --- /dev/null +++ b/problems/0714.买卖股票的最佳时机含手续费(动态规划).md @@ -0,0 +1,112 @@ +

+ + + + +

+

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

+ +## 714.买卖股票的最佳时机含手续费 + +题目链接:https://leetcode-cn.com/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://mp.weixin.qq.com/s/olWrUuDEYw2Jx5rMeG7XAg) + +使用贪心算法,的性能是: +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +那么我们再来看看是使用动规的方法如何解题。 + +相对于[动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w),本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 + +唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。 + +这里重申一下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://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w)的区别就是这里需要多一个减去手续费的操作**。 + +以上分析完毕,C++代码如下: + +```C++ +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: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0718.最长重复子数组.md b/problems/0718.最长重复子数组.md new file mode 100644 index 00000000..c20ea79f --- /dev/null +++ b/problems/0718.最长重复子数组.md @@ -0,0 +1,171 @@ +

+ + + + +

+

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

+ +## 718. 最长重复子数组 + +题目链接:https://leetcode-cn.com/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 + + +## 思路 + +注意题目中说的子数组,其实就是连续子序列。这种问题动规最拿手,动规五部曲分析如下: + +1. 确定dp数组(dp table)以及下标的含义 + +dp[i][j] :以下标i - 1为结尾的A,和以下标j - 1为结尾的B,最长重复子数组长度为dp[i][j]。 + +此时细心的同学应该发现,那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]的最大值记录下来。 + +代码如下: + +```C++ +for (int i = 1; i <= A.size(); i++) { + for (int j = 1; j <= B.size(); j++) { + if (A[i - 1] == B[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://img-blog.csdnimg.cn/2021011215282060.jpg) + +以上五部曲分析完毕,C++代码如下: + +```C++ +class Solution { +public: + int findLength(vector& A, vector& B) { + vector> dp (A.size() + 1, vector(B.size() + 1, 0)); + int result = 0; + for (int i = 1; i <= A.size(); i++) { + for (int j = 1; j <= B.size(); j++) { + if (A[i - 1] == B[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://img-blog.csdnimg.cn/2021011215282060.jpg) + +我们可以看出dp[i][j]都是由dp[i - 1][j - 1]推出。那么压缩为一维数组,也就是dp[j]都是由dp[j - 1]推出。 + +也就是相当于可以把上一层dp[i - 1][j]拷贝到下一层dp[i][j]来继续用。 + +**此时遍历B数组的时候,就要从后向前遍历,这样避免重复覆盖**。 + +``` +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) + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0738.单调递增的数字.md b/problems/0738.单调递增的数字.md index dd228772..d423d4d6 100644 --- a/problems/0738.单调递增的数字.md +++ b/problems/0738.单调递增的数字.md @@ -1,10 +1,38 @@ +

+ + + + +

+

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

+ + +## 738.单调递增的数字 + +给定一个非负整数 N,找出小于或等于 N 的最大的整数,同时这个整数需要满足其各个位数上的数字是单调递增。 + +(当且仅当每个相邻位数上的数字 x 和 y 满足 x <= y 时,我们称这个整数是单调递增的。) + +示例 1: +输入: N = 10 +输出: 9 + +示例 2: +输入: N = 1234 +输出: 1234 + +示例 3: +输入: N = 332 +输出: 299 + +说明: N 是在 [0, 10^9] 范围内的一个整数。 -# 思路 ## 暴力解法 -暴力一波 果然超时了 +题意很简单,那么首先想的就是暴力解法了,来我提大家暴力一波,结果自然是超时! +代码如下: ```C++ class Solution { private: @@ -27,9 +55,12 @@ public: } }; ``` -## 贪心算法 +* 时间复杂度:O(n * m) m为n的数字长度 +* 空间复杂度:O(1) -题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。 +## 贪心算法 + +题目要求小于等于N的最大单调递增的整数,那么拿一个两位的数字来举例。 例如:98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]--,然后strNum[i]给为9,这样这个整数就是89,即小于98的最大的单调递增整数。 @@ -41,14 +72,15 @@ public: **但这里局部最优推出全局最优,还需要其他条件,即遍历顺序,和标记从哪一位开始统一改成9**。 -此时是从前向后遍历还是从后向前遍历呢? +此时是从前向后遍历还是从后向前遍历呢? -这里其实还有一个贪心选择,对于“遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9”的情况,这个strNum[i - 1]--的操作应该是越靠后越好。 +从前向后遍历的话,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]减一,但此时如果strNum[i - 1]减一了,可能又小于strNum[i - 2]。 -因为这样才能让这个单调递增整数尽可能的大。例如:对于5486,第一位的5能不减一尽量不减一,因为这个减一对整体损失最大。 +这么说有点抽象,举个例子,数字:332,从前向后遍历的话,那么就把变成了329,此时2又小于了第一位的3了,真正的结果应该是299。 -所以要从后向前遍历,遇到strNum[i - 1] > strNum[i]的情况,让strNum[i - 1]--,然后strNum[i]给为9,这样保证这个减一的操作尽可能在后面进行(即整数的尽可能小的位数上进行)。 +**所以从前后向遍历会改变已经遍历过的结果!** +那么从后向前遍历,就可以重复利用上次比较得出的结果了,从后向前遍历332的数值变化为:332 -> 329 -> 299 确定了遍历顺序之后,那么此时局部最优就可以推出全局,找不出反例,试试贪心。 @@ -76,7 +108,35 @@ public: }; ``` -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +* 时间复杂度:O(n) n 为数字长度 +* 空间复杂度:O(n) 需要一个字符串,转化为字符串操作更方便 +## 总结 + +本题只要想清楚个例,例如98,一旦出现strNum[i - 1] > strNum[i]的情况(非单调递增),首先想让strNum[i - 1]减一,strNum[i]赋值9,这样这个整数就是89。就可以很自然想到对应的贪心解法了。 + +想到了贪心,还要考虑遍历顺序,只有从后向前遍历才能重复利用上次比较的结果。 + +最后代码实现的时候,也需要一些技巧,例如用一个flag来标记从哪里开始赋值9。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0739.每日温度.md b/problems/0739.每日温度.md deleted file mode 100644 index 12d9a55f..00000000 --- a/problems/0739.每日温度.md +++ /dev/null @@ -1,54 +0,0 @@ - -# 思路 - -使用递减栈(从大到小 栈底到栈头) - -``` -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()]) { // 把i放入栈 - st.push(i); - } else if (T[i] == T[st.top()]) { // 相同也要把i放进去,例如 1 1 1 1 2 - 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; - } -}; -``` -建议一开始 都把每种情况分析好,不要上来看简短的代码,关键逻辑都被隐藏了。 - -精简代码如下: - -``` -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++) { - while (!st.empty() && T[i] > T[st.top()]) { // 注意栈不能为空 - result[st.top()] = i - st.top(); - st.pop(); - } - st.push(i); - - } - return result; - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0746.使用最小花费爬楼梯.md b/problems/0746.使用最小花费爬楼梯.md new file mode 100644 index 00000000..e87f782c --- /dev/null +++ b/problems/0746.使用最小花费爬楼梯.md @@ -0,0 +1,220 @@ +

+ + + + +

+

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

+ +## 746. 使用最小花费爬楼梯 + +题目链接:https://leetcode-cn.com/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://mp.weixin.qq.com/s/Ohop0jApSII9xxOMiFhGIw)的花费版本。 + +**注意题目描述:每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯** + +所以示例1中只花费一个15 就可以到阶梯顶,最后一步可以理解为 不用花费。 + +读完题大家应该知道指定需要动态规划的,贪心是不可能了。 + +1. 确定dp数组以及下标的含义 + +使用动态规划,就要有一个数组来记录状态,本题只需要一个一维数组dp[i]就可以了。 + +**dp[i]的定义:到达第i个台阶所花费的最少体力为dp[i]**。(注意这里认为是第一步一定是要花费) + +**对于dp数组的定义,大家一定要清晰!** + +2. 确定递推公式 + +**可以有两个途径得到dp[i],一个是dp[i-1] 一个是dp[i-2]**。 + +那么究竟是选dp[i-1]还是dp[i-2]呢? + +一定是选最小的,所以dp[i] = min(dp[i - 1], dp[i - 2]) + cost[i]; + +**注意这里为什么是加cost[i],而不是cost[i-1],cost[i-2]之类的**,因为题目中说了:每当你爬上一个阶梯你都要花费对应的体力值 + +3. dp数组如何初始化 + +根据dp数组的定义,dp数组初始化其实是比较难的,因为不可能初始化为第i台阶所花费的最少体力。 + +那么看一下递归公式,dp[i]由dp[i-1],dp[i-2]推出,既然初始化所有的dp[i]是不可能的,那么只初始化dp[0]和dp[1]就够了,其他的最终都是dp[0]dp[1]推出。 + +所以初始化代码为: + +``` +vector dp(cost.size()); +dp[0] = cost[0]; +dp[1] = cost[1]; +``` + +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数组的状态变化,如下: + +![746.使用最小花费爬楼梯](https://img-blog.csdnimg.cn/2021010621363669.png) + +如果大家代码写出来有问题,就把dp数组打印出来,看看和如上推导的是不是一样的。 + +以上分析完毕,整体C++代码如下: + +```C++ +// 版本一 +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]); + } +}; +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(n) + +还可以优化空间复杂度,因为dp[i]就是由前两位推出来的,那么也不用dp数组了,C++代码如下: + +```C++ +// 版本二 +class Solution { +public: + int minCostClimbingStairs(vector& cost) { + int dp0 = cost[0]; + int dp1 = cost[1]; + for (int i = 2; i < cost.size(); i++) { + int dpi = min(dp0, dp1) + cost[i]; + dp0 = dp1; // 记录一下前两位 + dp1 = dpi; + } + return min(dp0, dp1); + } +}; + +``` + +* 时间复杂度:O(n) +* 空间复杂度:O(1) + +**当然我不建议这么写,能写出版本一就可以了,直观简洁!** + +在后序的讲解中,可能我会忽略这种版本二的写法,大家只要知道有这么个写法就可以了哈。 + +## 拓展 + +这道题描述也确实有点魔幻。 + +题目描述为:每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。 + +示例1: + +输入:cost = [10, 15, 20] +输出:15 + + +**从题目描述可以看出:要不是第一步不需要花费体力,要不就是第最后一步不需要花费体力,我个人理解:题意说的其实是第一步是要支付费用的!**。因为是当你爬上一个台阶就要花费对应的体力值! + +所以我定义的dp[i]意思是也是第一步是要花费体力的,最后一步不用花费体力了,因为已经支付了。 + +当然也可以样,定义dp[i]为:第一步是不花费体力,最后一步是花费体力的。 + +所以代码这么写: + +```C++ +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()]; + } +}; +``` + +这么写看上去比较顺,但是就是感觉和题目描述的不太符。哈哈,也没有必要这么细扣题意了,大家只要知道,题目的意思反正就是要不是第一步不花费,要不是最后一步不花费,都可以。 + + +# 总结 + +大家可以发现这道题目相对于 昨天的[动态规划:爬楼梯](https://mp.weixin.qq.com/s/Ohop0jApSII9xxOMiFhGIw)有难了一点,但整体思路是一样。 + +从[动态规划:斐波那契数](https://mp.weixin.qq.com/s/ko0zLJplF7n_4TysnPOa_w)到 [动态规划:爬楼梯](https://mp.weixin.qq.com/s/Ohop0jApSII9xxOMiFhGIw)再到今天这道题目,录友们感受到循序渐进的梯度了嘛。 + +每个系列开始的时候,都有录友和我反馈说题目太简单了,赶紧上难度,但也有录友和我说有点难了,快跟不上了。 + +其实我选的题目都是有目的性的,就算是简单题,也是为了练习方法论,然后难度都是梯度上来的,一环扣一环。 + +但我也可以随便选来一道难题讲呗,这其实是最省事的,不用管什么题目顺序,看心情找一道就讲。 + +难的是把题目按梯度排好,循序渐进,再按照统一方法论把这些都串起来,哈哈,所以大家不要催我哈,按照我的节奏一步一步来就行啦。 + +学算法,认准「代码随想录」,没毛病! + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0763.划分字母区间.md b/problems/0763.划分字母区间.md index 6a0716a3..1b93edf7 100644 --- a/problems/0763.划分字母区间.md +++ b/problems/0763.划分字母区间.md @@ -1,22 +1,53 @@ -## 题目链接 -https://leetcode-cn.com/problems/partition-labels/ +

+ + + + +

+

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

-## 思路 -一想到分割字符串就想到了回溯,但本题其实不用那么复杂。 +## 763.划分字母区间 + +题目链接: https://leetcode-cn.com/problems/partition-labels/ + +字符串 S 由小写字母组成。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。返回一个表示每个字符串片段的长度的列表。 + +示例: +输入:S = "ababcbacadefegdehijhklij" +输出:[9,7,8] +解释: +划分结果为 "ababcbaca", "defegde", "hijhklij"。 +每个字母最多出现在一个片段中。 +像 "ababcbacadefegde", "hijhklij" 的划分是错误的,因为划分的片段数较少。 +  +提示: + +* S的长度在[1, 500]之间。 +* S只包含小写字母 'a' 到 'z' 。 + +## 思路 + +一想到分割字符串就想到了回溯,但本题其实不用回溯去暴力搜索。 + +题目要求同一字母最多出现在一个片段中,那么如何把同一个字母的都圈在同一个区间里呢? + +如果没有接触过这种题目的话,还挺有难度的。 + +在遍历的过程中相当于是要找每一个字母的边界,**如果找到之前遍历过的所有字母的最远边界,说明这个边界就是分割点了**。此时前面出现过所有字母,最远也就到这个边界了。 可以分为如下两步: -* 统计每一个字符最后出现的位置 -* 从头遍历字符,如果找到之前字符最大出现位置下标和当前下标相等,则找到了分割点 +* 统计每一个字符最后出现的位置 +* 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点 如图: - +![763.划分字母区间](https://img-blog.csdnimg.cn/20201222191924417.png) 明白原理之后,代码并不复杂,如下: -``` +```C++ class Solution { public: vector partitionLabels(string S) { @@ -38,4 +69,33 @@ public: } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +* 时间复杂度:O(n) +* 空间复杂度:O(1) 使用的hash数组是固定大小 + +## 总结 + +这道题目leetcode标记为贪心算法,说实话,我没有感受到贪心,找不出局部最优推出全局最优的过程。就是用最远出现距离模拟了圈字符的行为。 + +但这道题目的思路是很巧妙的,所以有必要介绍给大家做一做,感受一下。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0767.重构字符串.md b/problems/0767.重构字符串.md deleted file mode 100644 index 3498f9af..00000000 --- a/problems/0767.重构字符串.md +++ /dev/null @@ -1,60 +0,0 @@ - -> **如果对做了一些题目,对字符串还没有整体了解的话,可以看这篇:[字符串经典题目大总结!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg),相信字符串各种操作就非常清晰了。** - -扫了一圈题解,感觉大家的解答都比较高端,我来一个朴实无华的版本。 - -分为如下三步: - -* 用map统计频率字符频率 -* 转为vector(即数组)按频率从大到小排序 -* 按奇数位顺序插入,插满之后按偶数位顺序插入 - -**为什么要先按奇数位插入呢?** - -先按奇数位插入,保证最大的字符分散开,因为奇数位总是>=偶数位! - -C++代码如下: - -``` -class Solution { -private: - bool static cmp (const pair& a, const pair& b) { - return a.second > b.second; // 按照频率从大到小排序 - } -public: - string reorganizeString(string S) { - unordered_map umap; - int maxFreq = 0; - for (char s : S) { - umap[s]++; - maxFreq = max(umap[s], maxFreq); - } - if (2 * maxFreq - 1 > S.size()) return ""; - - vector> vec(umap.begin(), umap.end()); - sort(vec.begin(), vec.end(), cmp); // 给频率排个序 - - string result(S); - int index = 0;// 先按奇数位散开 - for (int i = 0; i < vec.size(); i++) { - while (vec[i].second--) { - result[index] = vec[i].first; - index += 2; - if (index >= S.size()) index = 1; // 奇数位插满了插偶数位 - } - } - return result; - } -}; -``` - -* 时间复杂度O(nlogn) -* 空间复杂度O(n) - -关于leetcode统计的击败多少多少用户,大家不必过于在意,想好代码的时间复杂度就够了,这个击败多少用户,多提交几次可能就击败100%了。 -![767. 重构字符串](https://img-blog.csdnimg.cn/202011301035035.png) - -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** - -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** - diff --git a/problems/0841.钥匙和房间.md b/problems/0841.钥匙和房间.md deleted file mode 100644 index 16473f1a..00000000 --- a/problems/0841.钥匙和房间.md +++ /dev/null @@ -1,84 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/keys-and-rooms/ - -## 思路 - -其实这道题的本质就是判断各个房间所连成的有向图,说明不用访问所有的房间。 - -如图所示: - - - -示例1就可以访问所有的房间,因为通过房间里的key将房间连在了一起。 - -示例2中,就不能访问所有房间,从图中就可以看出,房间2是一个孤岛,我们从0出发,无论怎么遍历,都访问不到房间2。 - -认清本质问题之后,**使用 广度优先搜索(BFS) 还是 深度优先搜索(DFS) 都是可以的。** - -代码如下: - -## BFS C++代码 - -``` -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); - } -}; -``` - -## DFS C++代码 - -``` -class Solution { -private: - void dfs(int key, const vector>& rooms, vector& visited) { - if (visited[key]) { - return; - } - visited[key] = 1; - vector keys = rooms[key]; - for (int key : keys) { - // 深度优先搜索遍历 - dfs(key, rooms, visited); - } - } -public: - bool canVisitAllRooms(vector>& rooms) { - vector visited(rooms.size(), 0); - dfs(0, rooms, visited); - //检查是否都访问到了 - for (int i : visited) { - if (i == 0) return false; - } - return true; - } -}; -``` - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0844.比较含退格的字符串.md b/problems/0844.比较含退格的字符串.md deleted file mode 100644 index 47031a70..00000000 --- a/problems/0844.比较含退格的字符串.md +++ /dev/null @@ -1,122 +0,0 @@ -感觉像是使用栈 - -## 思路 - -本文将给出 空间复杂度O(n)的栈模拟方法 以及空间复杂度是O(1)的双指针方法。 - -### 普通方法(使用栈的思路) - -这道题目一看就是要使用栈的节奏,这种匹配(消除)问题也是栈的擅长所在,跟着一起刷题的同学应该知道,在[栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg),我就已经提过了一次使用栈来做类似的事情了。 - -**那么本题,确实可以使用栈的思路,但是没有必要使用栈,因为最后比较的时候还要比较栈里的元素,有点麻烦**。 - -这里直接使用字符串string,来作为栈,末尾添加和弹出,string都有相应的接口,最后比较的时候,只要比较两个字符串就可以了,比比较栈里的元素方便一些。 - -代码如下: - -``` -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,可以把这块公共逻辑抽离出来,代码精简如下: - -``` -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。 - -代码如下: - -``` -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」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0860.柠檬水找零.md b/problems/0860.柠檬水找零.md index 21bc66bc..c718b0cd 100644 --- a/problems/0860.柠檬水找零.md +++ b/problems/0860.柠檬水找零.md @@ -1,7 +1,13 @@ +

+ + + + +

+

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

-> 给我来一杯柠檬水 -# 860.柠檬水找零 +## 860.柠檬水找零 题目链接:https://leetcode-cn.com/problems/lemonade-change/ @@ -15,38 +21,38 @@ 如果你能给每位顾客正确找零,返回 true ,否则返回 false 。 -示例 1: -输入:[5,5,5,10,20] -输出:true -解释: -前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 -第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 -第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 -由于所有客户都得到了正确的找零,所以我们输出 true。 +示例 1: +输入:[5,5,5,10,20] +输出:true +解释: +前 3 位顾客那里,我们按顺序收取 3 张 5 美元的钞票。 +第 4 位顾客那里,我们收取一张 10 美元的钞票,并返还 5 美元。 +第 5 位顾客那里,我们找还一张 10 美元的钞票和一张 5 美元的钞票。 +由于所有客户都得到了正确的找零,所以我们输出 true。 -示例 2: -输入:[5,5,10] -输出:true +示例 2: +输入:[5,5,10] +输出:true -示例 3: -输入:[10,10] -输出:false +示例 3: +输入:[10,10] +输出:false -示例 4: -输入:[5,5,10,10,20] -输出:false -解释: -前 2 位顾客那里,我们按顺序收取 2 张 5 美元的钞票。 -对于接下来的 2 位顾客,我们收取一张 10 美元的钞票,然后返还 5 美元。 -对于最后一位顾客,我们无法退回 15 美元,因为我们现在只有两张 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  -# 思路 +## 思路 这是前几天的leetcode每日一题,感觉不错,给大家讲一下。 @@ -60,13 +66,13 @@ * 情况一:账单是5,直接收下。 * 情况二:账单是10,消耗一个5,增加一个10 -* 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5 +* 情况三:账单是20,优先消耗一个10和一个5,如果不够,再消耗三个5 此时大家就发现 情况一,情况二,都是固定策略,都不用我们来做分析了,而唯一不确定的其实在情况三。 而情况三逻辑也不复杂甚至感觉纯模拟就可以了,其实情况三这里是有贪心的。 -账单是20的情况,为什么要优先消耗一个10和一个5呢? +账单是20的情况,为什么要优先消耗一个10和一个5呢? **因为美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能!** @@ -108,18 +114,31 @@ public: }; ``` -# 总结 +## 总结 -咋眼一看好像很复杂,分析清楚之后,会发现逻辑其实非常固定。 +咋眼一看好像很复杂,分析清楚之后,会发现逻辑其实非常固定。 这道题目可以告诉大家,遇到感觉没有思路的题目,可以静下心来把能遇到的情况分析一下,只要分析到具体情况了,一下子就豁然开朗了。 如果一直陷入想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。 -就酱,如果感觉「代码随想录」干货满满,就帮忙宣传一波吧,感激不尽! + +## 其他语言版本 -> **我是[程序员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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** +Java: -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0879.盈利计划.md b/problems/0879.盈利计划.md deleted file mode 100644 index 00f50de4..00000000 --- a/problems/0879.盈利计划.md +++ /dev/null @@ -1,39 +0,0 @@ - -(待完整) -https://www.cnblogs.com/grandyang/p/11108205.html - -* 如果g >= group[k - 1],则说明犯罪k可以执行,因为人数已经足够了 -* 如果profit[k - 1] >= p,则说明只执行犯罪k就能达到利润p,可以单独执行,接着我们考虑必须执行犯罪k,然后加上前面的利润不低于p - profit[k - 1]的方案,这样凑出的方案利润也会超过p -* 否则犯罪k不能执行,此时只能直接将前面 dp[k - 1][g][p]利润超过p的方案搬过来 - -``` -class Solution { -public: - int profitableSchemes(int G, int P, vector& group, vector& profit) { - int K = group.size(), MOD = 1e9 + 7; - //dp[k][g][p]使用计划[1,2,3,... k],最多g个人,盈利不少于p个方案个数 - vector > > dp(K + 1, vector >(G + 1, vector(P + 1, 0))); - for (int k = 1; k <= K; ++k){ - for (int g = 1; g <= G; ++g){ - for (int p = 0; p <= P; ++p){ - //如果人数g能够执行第k种犯罪(人数g >= 第k种犯罪需要的人数group[k - 1]) - if (g >= group[k - 1]){ - if (profit[k - 1] >= p){ - //如果执行第k种犯罪获得的利润profit[k - 1]不小于p,则可以单独执行该犯罪 - dp[k][g][p] += 1; - } - //将第k个方案产生的利润profit[k]与之前方案产生的利润凑出不少于p - //dp[k - 1][g - group[k]][p > profit[k] ? p - profit[k] : 0]表示在使用[1, k - 1]这些犯罪,使用人数不超过g - group[k],获得路润不少于p - profit[k] - //这样前面的产生p - profit[k]的利润再加上执行第k个犯罪的利润profit[k]就能到达到利润不少于p(注意p 可能大于 profit[k],所以不能为负数) - dp[k][g][p] = (dp[k][g][p] + dp[k - 1][g - group[k - 1]][p > profit[k - 1] ? p - profit[k - 1] : 0]) % MOD; - } - //不执行第k个犯罪,直接将前面的利润超过p的方案搬过来 - dp[k][g][p] = (dp[k][g][p] + dp[k - 1][g][p]) % MOD; - } - } - } - return dp[K][G][P]; - } -}; - -``` diff --git a/problems/0901.股票价格跨度.md b/problems/0901.股票价格跨度.md deleted file mode 100644 index e3b546ea..00000000 --- a/problems/0901.股票价格跨度.md +++ /dev/null @@ -1,39 +0,0 @@ - -这道题也不是单调栈的简单题 - -需要记录 传入的所有price - -注意 股票价格相同的情况 - -需要自己维护一个数组,用stack来记录下表, 因为数组下标从0开始的,注意 栈为空的两种情况(代码中注释)。 - -如果这道题,不用这个应用场景,而是直接给出输入数组的话,会简单一些。 - -这道题目他们都用两个栈来实现,貌似代码简介一些,但我这个最直观。 可以画一个图 - -``` -class StockSpanner { -public: - stack st; - vector prices; - StockSpanner() { - } - - int next(int price) { - prices.push_back(price); - while (!st.empty() && prices[st.top()] <= price) { // 注意这里等于的情况 - st.pop(); - } - int result; - int curPriceIndex = prices.size() - 1; - if (!st.empty()) { // 栈不为空,求差值 - result = curPriceIndex - st.top(); - } else { // 栈为空 - if (prices.size() == 1) result = 1; // 如果是放入第一个元素就是,result是1 - else result = curPriceIndex + 1; // 不是放入的第一个元素了,那么就应该是当前索引+1 - } - st.push(curPriceIndex); - return result; - } -}; -``` diff --git a/problems/0922.按奇偶排序数组II.md b/problems/0922.按奇偶排序数组II.md deleted file mode 100644 index fb04c7ee..00000000 --- a/problems/0922.按奇偶排序数组II.md +++ /dev/null @@ -1,88 +0,0 @@ - -## 思路 -这道题目直接的想法可能是两层for循环再加上used数组表示使用过的元素。这样的的时间复杂度是O(n^2)。 - -### 方法一 -其实这道题可以用很朴实的方法,时间复杂度就就是O(n)了,C++代码如下: - -``` -class Solution { -public: - vector sortArrayByParityII(vector& A) { - vector even(A.size() / 2); // 初始化就确定数组大小,节省开销 - vector odd(A.size() / 2); - vector result(A.size()); - int evenIndex = 0; - int oddIndex = 0; - int resultIndex = 0; - // 把A数组放进偶数数组,和奇数数组 - for (int i = 0; i < A.size(); i++) { - if (A[i] % 2 == 0) even[evenIndex++] = A[i]; - else odd[oddIndex++] = A[i]; - } - // 把偶数数组,奇数数组分别放进result数组中 - for (int i = 0; i < evenIndex; i++) { - result[resultIndex++] = even[i]; - result[resultIndex++] = odd[i]; - } - return result; - } -}; -``` - -时间复杂度:O(n) -空间复杂度:O(n) - -### 方法二 -以上代码我是建了两个辅助数组,而且A数组还相当于遍历了两次,用辅助数组的好处就是思路清晰,优化一下就是不用这两个辅助树,代码如下: - -``` -class Solution { -public: - vector sortArrayByParityII(vector& A) { - vector result(A.size()); - int evenIndex = 0; // 偶数下表 - int oddIndex = 1; // 奇数下表 - for (int i = 0; i < A.size(); i++) { - if (A[i] % 2 == 0) { - result[evenIndex] = A[i]; - evenIndex += 2; - } - else { - result[oddIndex] = A[i]; - oddIndex += 2; - } - } - return result; - } -}; -``` - -时间复杂度O(n) -空间复杂度O(n) - -### 方法三 - -当然还可以在原数组上修改,连result数组都不用了。 - -``` -class Solution { -public: - vector sortArrayByParityII(vector& A) { - int oddIndex = 1; - for (int i = 0; i < A.size(); i += 2) { - if (A[i] % 2 == 1) { // 在偶数位遇到了奇数 - while(A[oddIndex] % 2 != 0) oddIndex += 2; // 在奇数位找一个偶数 - swap(A[i], A[oddIndex]); // 替换 - } - } - return A; - } -}; -``` - -时间复杂度:O(n) -空间复杂度:O(1) - -这里时间复杂度并不是O(n^2),因为偶数位和奇数位都只操作一次,不是n/2 * n/2的关系,而是n/2 + n/2的关系! - diff --git a/problems/0925.长按键入.md b/problems/0925.长按键入.md deleted file mode 100644 index 6c6ba699..00000000 --- a/problems/0925.长按键入.md +++ /dev/null @@ -1,61 +0,0 @@ - -## 思路 - -这道题目一看以为是哈希,仔细一看不行,要有顺序。 - -所以模拟同时遍历两个数组,进行对比就可以了。 - -对比的时候需要一下几点: - -* 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++代码: - -``` -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」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/0941.有效的山脉数组.md b/problems/0941.有效的山脉数组.md deleted file mode 100644 index 85a9c745..00000000 --- a/problems/0941.有效的山脉数组.md +++ /dev/null @@ -1,44 +0,0 @@ -## 题目链接 - -https://leetcode-cn.com/problems/valid-mountain-array/ - -## 思路 - -判断是山峰,主要就是要严格的保存左边到中间,和右边到中间是递增的。 - -这样可以使用两个指针,left和right,让其按照如下规则移动,如图: - - - -**注意这里还是有一些细节,例如如下两点:** - -* 因为left和right是数组下表,移动的过程中注意不要数组越界 -* 如果left或者right没有移动,说明是一个单调递增或者递减的数组,依然不是山峰 - -C++代码如下: - -``` -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://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) - - -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[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/0968.监控二叉树.md b/problems/0968.监控二叉树.md index a5dbdefb..ab2bd2e5 100644 --- a/problems/0968.监控二叉树.md +++ b/problems/0968.监控二叉树.md @@ -1,28 +1,75 @@ -# 题目地址 -https://leetcode-cn.com/problems/binary-tree-cameras/ +

+ + + + +

+

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

-## 思路 -这道题目其实不是那么好理解的,题目举的示例不是很典型,会误以为摄像头必须要放在中间,其实放哪里都可以只要覆盖了就行。 +## 968.监控二叉树 -这道题目难在两点: +题目地址 : https://leetcode-cn.com/problems/binary-tree-cameras/ -1. 需要确定遍历方式 -2. 需要状态转移的方程 +给定一个二叉树,我们在树的节点上安装摄像头。 -我们之前做动态规划的时候,只要最难的地方在于确定状态转移方程,至于遍历方式无非就是在数组或者二维数组上。 +节点上的每个摄影头都可以监视其父对象、自身及其直接子对象。 -**本题并不是动态规划,其本质是贪心,但我们要确定状态转移方式,而且要在树上进行推导,所以难度就上来了,一些同学知道这道题目难,但其实说不上难点究竟在哪。** +计算监控树的所有节点所需的最小摄像头数量。 -1. 需要确定遍历方式 +示例 1: -首先先确定遍历方式,才能确定转移方程,那么该如何遍历呢? +![](https://img-blog.csdnimg.cn/20201229175736596.png) -在安排选择摄像头的位置的时候,**我们要从底向上进行推导,因为尽量让叶子节点的父节点安装摄像头,这样摄像头的数量才是最少的**,这也是本道贪心的原理所在! +输入:[0,0,null,0,0] +输出:1 +解释:如图所示,一台摄像头足以监控所有节点。 -如何从低向上推导呢? +示例 2: -就是后序遍历也就是左右中的顺序,这样就可以从下到上进行推导了。 +![](https://img-blog.csdnimg.cn/2020122917584449.png) + +输入:[0,0,null,0,null,0,null,null,0] +输出:2 +解释:需要至少两个摄像头来监视树的所有节点。 上图显示了摄像头放置的有效位置之一。 + +提示: + +* 给定树的节点数的范围是 [1, 1000]。 +* 每个节点的值都是 0。 + + +## 思路 + +这道题目首先要想,如何放置,才能让摄像头最小的呢? + +从题目中示例,其实可以得到启发,**我们发现题目示例中的摄像头都没有放在叶子节点上!** + +这是很重要的一个线索,摄像头可以覆盖上中下三层,如果把摄像头放在叶子节点上,就浪费的一层的覆盖。 + +所以把摄像头放在叶子节点的父节点位置,才能充分利用摄像头的覆盖面积。 + +那么有同学可能问了,为什么不从头结点开始看起呢,为啥要从叶子节点看呢? + +因为头结点放不放摄像头也就省下一个摄像头, 叶子节点放不放摄像头省下了的摄像头数量是指数阶别的。 + +**所以我们要从下往上看,局部最优:让叶子节点的父节点安摄像头,所用摄像头最少,整体最优:全部摄像头数量所用最少!** + +局部最优推出全局最优,找不出反例,那么就按照贪心来! + +此时,大体思路就是从低到上,先给叶子节点父节点放个摄像头,然后隔两个节点放一个摄像头,直至到二叉树头结点。 + +此时这道题目还有两个难点: + +1. 二叉树的遍历 +2. 如何隔两个节点放一个摄像头 + + +### 确定遍历顺序 + +在二叉树中如何从低向上推导呢? + +可以使用后序遍历也就是左右中的顺序,这样就可以在回溯的过程中从下到上进行推导了。 后序遍历代码如下: @@ -36,40 +83,42 @@ https://leetcode-cn.com/problems/binary-tree-cameras/ int right = traversal(cur->right); // 右 逻辑处理 // 中 - return ; } ``` **注意在以上代码中我们取了左孩子的返回值,右孩子的返回值,即left 和 right, 以后推导中间节点的状态** -2. 需要状态转移的方程 +### 如何隔两个节点放一个摄像头 -确定了遍历顺序,再看看这个状态应该如何转移,先来看看每个节点可能有几种状态: +此时需要状态转移的公式,大家不要和动态的状态转移公式混到一起,本题状态转移没有择优的过程,就是单纯的状态转移! -可以说有如下三种: +来看看这个状态应该如何转移,先来看看每个节点可能有几种状态: -* 该节点无覆盖 +有如下三种: + +* 该节点无覆盖 * 本节点有摄像头 * 本节点有覆盖 我们分别有三个数字来表示: -* 0:该节点无覆盖 +* 0:该节点无覆盖 * 1:本节点有摄像头 * 2:本节点有覆盖 -大家应该找不出第四个节点的状态了。 +大家应该找不出第四个节点的状态了。 **一些同学可能会想有没有第四种状态:本节点无摄像头,其实无摄像头就是 无覆盖 或者 有覆盖的状态,所以一共还是三个状态。** -**那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢? ** +**因为在遍历树的过程中,就会遇到空节点,那么问题来了,空节点究竟是哪一种状态呢? 空节点表示无覆盖? 表示有摄像头?还是有覆盖呢?** + 回归本质,为了让摄像头数量最少,我们要尽量让叶子节点的父节点安装摄像头,这样才能摄像头的数量最少。 -那么空节点不能是无覆盖的状态,这样叶子节点就可以放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。 +那么空节点不能是无覆盖的状态,这样叶子节点就要放摄像头了,空节点也不能是有摄像头的状态,这样叶子节点的父节点就没有必要放摄像头了,而是可以把摄像头放在叶子节点的爷爷节点上。 -**所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了** +**所以空节点的状态只能是有覆盖,这样就可以在叶子节点的父节点放摄像头了** 接下来就是递推关系。 @@ -78,30 +127,30 @@ https://leetcode-cn.com/problems/binary-tree-cameras/ 代码如下: ``` - // 空节点,该节点有覆盖 - if (cur == NULL) return 2; +// 空节点,该节点有覆盖 +if (cur == NULL) return 2; ``` 递归的函数,以及终止条件已经确定了,再来看单层逻辑处理。 主要有如下四类情况: -1. 情况1:左右节点都有覆盖 +* 情况1:左右节点都有覆盖 左孩子有覆盖,右孩子有覆盖,那么此时中间节点应该就是无覆盖的状态了。 如图: - +![968.监控二叉树2](https://img-blog.csdnimg.cn/20201229203710729.png) 代码如下: ``` - // 左右节点都有覆盖 - if (left == 2 && right == 2) return 0; +// 左右节点都有覆盖 +if (left == 2 && right == 2) return 0; ``` -2. 情况2:左右节点至少有一个无覆盖的情况 +* 情况2:左右节点至少有一个无覆盖的情况 如果是以下情况,则中间节点(父节点)应该放摄像头: @@ -117,13 +166,13 @@ left == 2 && right == 0 左节点覆盖,右节点无覆盖 代码如下: ``` - if (left == 0 || right == 0) { - result++; - return 1; - } +if (left == 0 || right == 0) { + result++; + return 1; +} ``` -3. 情况3:左右节点至少有一个有摄像头 +* 情况3:左右节点至少有一个有摄像头 如果是以下情况,其实就是 左右孩子节点有一个有摄像头了,那么其父节点就应该是2(覆盖的状态) @@ -134,12 +183,12 @@ left == 1 && right == 1 左右节点都有摄像头 代码如下: ``` - if (left == 1 || right == 1) return 2; +if (left == 1 || right == 1) return 2; ``` **从这个代码中,可以看出,如果left == 1, right == 0 怎么办?其实这种条件在情况2中已经判断过了**,如图: - +![968.监控二叉树1](https://img-blog.csdnimg.cn/2020122920362355.png) 这种情况也是大多数同学容易迷惑的情况。 @@ -147,27 +196,28 @@ left == 1 && right == 1 左右节点都有摄像头 以上都处理完了,递归结束之后,可能头结点 还有一个无覆盖的情况,如图: - +![968.监控二叉树3](https://img-blog.csdnimg.cn/20201229203742446.png) 所以递归结束之后,还要判断根节点,如果没有覆盖,result++,代码如下: ``` - int minCameraCover(TreeNode* root) { - result = 0; - if (traversal(root) == 0) { // root 无覆盖 - result++; - } - return result; +int minCameraCover(TreeNode* root) { + result = 0; + if (traversal(root) == 0) { // root 无覆盖 + result++; } + return result; +} ``` 以上四种情况我们分析完了,代码也差不多了,整体代码如下: -(**以下我的代码是可以精简的,但是我是为了把情况说清楚,特别把每种情况列出来,因为精简之后的代码读者不好理解。**) +(**以下我的代码注释很详细,为了把情况说清楚,特别把每种情况列出来。**) -## C++代码 +## C++代码 -``` +```C++ +// 版本一 class Solution { private: int result; @@ -217,4 +267,67 @@ public: } }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +在以上代码的基础上,再进行精简,代码如下: + +```C++ +// 版本二 +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; + } +}; + + +``` + +大家可能会惊讶,居然可以这么简短,**其实就是在版本一的基础上,使用else把一些情况直接覆盖掉了**。 + +在网上关于这道题解可以搜到很多这种神级别的代码,但都没讲不清楚,如果直接看代码的话,指定越看越晕,**所以建议大家对着版本一的代码一步一步来哈,版本二中看不中用!**。 + +## 总结 + +本题的难点首先是要想到贪心的思路,然后就是遍历和状态推导。 + +在二叉树上进行状态推导,其实难度就上了一个台阶了,需要对二叉树的操作非常娴熟。 + +这道题目是名副其实的hard,大家感受感受,哈哈。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/0973.最接近原点的K个点.md b/problems/0973.最接近原点的K个点.md deleted file mode 100644 index 5090614f..00000000 --- a/problems/0973.最接近原点的K个点.md +++ /dev/null @@ -1,83 +0,0 @@ - -## 思路 - -这道题其实我在讲解队列的时候,就已经讲过了,在[栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng)中,我介绍了一种队列, 优先级队列,其实就是大(小)顶堆。 - -大家有精力的话也可以做做[347.前 K 个高频元素](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng),347是求前k的高频元素,本题呢,其实是求前k的低频元素。 - -所以套路都是一样一样的。 - -有的同学会用快排,其实快排的话把所有元素都排序了,时间复杂度是O(nlogn),而使用优先级队列时间复杂度为O(nlogk),因为只需要维护k个元素有序。 - -然后就是为什么要定义大顶堆呢? - -因为本地要求最小k个数,每次添加堆,都是从顶部把最大的弹出去,然后堆里留下的就是最小的k个数了。 - -C++代码如下: - -``` -// 版本一 -class Solution { -public: - // 大顶堆比较函数 - class mycomparison { - public: - bool operator()(const pair>& lhs, const pair>& rhs) { - return lhs.first < rhs.first; - } - }; - vector> kClosest(vector>& points, int K) { - // 定义一个大顶堆 - priority_queue>, vector>>, mycomparison> pri_que; - for(int i = 0; i < points.size(); i++) { - int x = points[i][0]; - int y = points[i][1]; - pair> p(x * x + y * y, points[i]); // key:距离,value是(x,y) - pri_que.push(p); - if (pri_que.size() > K) { // 如果队列的大小大于了K,则队列弹出,保证队列的大小一直为k - pri_que.pop(); - } - } - vector> result(K); // 把队列里元素放入数组 - for (int i = 0; i < K; i++) { - result[i] = pri_que.top().second; - pri_que.pop(); - } - return result; - } -}; -``` - -以上是为了完整的体现出优先级队列的定义以及比较过程。 - -如果要简化一下,就用默认的配置就可以。代码如下: - -``` -// 版本二 -class Solution { -public: - vector> kClosest(vector>& points, int K) { - // 默认大顶堆,按照pair的key排序 - priority_queue>, vector>>> pri_que; - for(int i = 0; i < points.size(); i++) { - int x = points[i][0]; - int y = points[i][1]; - pair> p(x * x + y * y, points[i]); // key:距离,value是(x,y) - pri_que.push(p); - if (pri_que.size() > K) { // 如果队列的大小大于了K,则队列弹出,保证队列的大小一直为k - pri_que.pop(); - } - } - vector> result(K); // 把队列里元素放入数组 - for (int i = 0; i < K; i++) { - result[i] = pri_que.top().second; - pri_que.pop(); - } - return result; - } -}; -``` - - -> 我是[程序员Carl](https://github.com/youngyangyang04),组队刷题可以找我,本文[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/0977.有序数组的平方.md b/problems/0977.有序数组的平方.md deleted file mode 100644 index 61af8ab6..00000000 --- a/problems/0977.有序数组的平方.md +++ /dev/null @@ -1,76 +0,0 @@ - -## 思路 - -### 暴力排序 - -最直观的相反,莫过于:每个数平方之后,排个序,美滋滋,代码如下: - -``` -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 + nlogn)。 - -### 双指针法 - -数组其实是有序的, 只不过负数平方之后可能成为最大数了。 - -那么数组平方的最大值就在数组的两端,不是最左边就是最右边,不可能是中间。 - -此时可以考虑双指针法了,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];` 。 - -如动画所示: - - - -不难写出如下代码: - -``` -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 + nlogn)还是提升不少的。 - -效率如下: - - - -**这里还是说一下,大家不必太在意leetcode上执行用时,打败多少多少用户,这个就是一个玩具,非常不准确。** - -做题的时候自己能分析出来时间复杂度就可以了,至于leetcode上执行用时,大概看一下就行,只要达到最优的时间复杂度就可以了, - -一样的代码多提交几次可能就击败百分之百了..... - -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/1002.查找常用字符.md b/problems/1002.查找常用字符.md deleted file mode 100644 index f9506010..00000000 --- a/problems/1002.查找常用字符.md +++ /dev/null @@ -1,112 +0,0 @@ -# 题目地址 -https://leetcode-cn.com/problems/find-common-characters/ - -## 思路 - -这道题意一起就有点绕,不是那么容易懂,其实就是26个小写字符中有字符 在所有字符串里都出现的话,就输出,重复的也算。 - -例如: - -输入:["ll","ll","ll"] -输出:["l","l"] - -这道题目一眼看上去,就是用哈希法,**“小写字符”,“出现频率”, 这些关键字都是为哈希法量身定做的啊** - -首先可以想到的是暴力解法,一个字符串一个字符串去搜,时间复杂度是O(n^m),n是字符串长度,m是有几个字符串。 - -可以看出这是指数级别的时间复杂度,非常高,而且代码实现也不容易,因为要统计 重复的字符,还要适当的替换或者去重。 - -那我们还是哈希法吧。如果对哈希法不了解,可以这这篇文章:[关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA)。 - -如果对用数组来做哈希法不了解的话,可以看这篇:[哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)。 - -了解了哈希法,理解了数组在哈希法中的应用之后,可以来看解题思路了。 - -整体思路就是统计出搜索字符串里26个字符的出现的频率,然后取每个字符频率最小值,最后转成输出格式就可以了。 - -如图: - - - -先统计第一个字符串所有字符出现的次数,代码如下: - -``` -int hash[26] = {0}; // 用来统计所有字符串里字符出现的最小频率 -for (int i = 0; i < A[0].size(); i++) { // 用第一个字符串给hash初始化 - hash[A[0][i] - 'a']++; -} -``` - -接下来,把其他字符串里字符的出现次数也统计出来一次放在hashOtherStr中。 - -然后hash 和 hashOtherStr 取最小值,这是本题关键所在,此时取最小值,就是 一个字符在所有字符串里出现的最小次数了。 - -代码如下: - -``` -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转正题目要求的输出格式就可以了。 - -代码如下: - -``` -// 将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++代码如下: - -``` -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」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/problems/1005.K次取反后最大化的数组和.md b/problems/1005.K次取反后最大化的数组和.md index 75e8be97..b2073e14 100644 --- a/problems/1005.K次取反后最大化的数组和.md +++ b/problems/1005.K次取反后最大化的数组和.md @@ -1,8 +1,13 @@ -> 很多录友都反馈昨天的题目:[贪心算法:跳跃游戏II](https://mp.weixin.qq.com/s/kJBcsJ46DKCSjT19pxrNYg) 很难,这样我就放心了,哈哈,因为我刚刚讲解贪心的时候一些录友会建议我:贪心没有必要单独讲,直接讲动规就可以了。应该不少没有深入接触过贪心的同学,都会感觉就贪心嘛,有啥难的。现在我们可以发现贪心的道理虽然简单,但解决问题都很巧妙,难度上不照动规差多少。 -> 今天是一道简单题,关键在于培养贪心的解题思路! +

+ + + + +

+

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

-# 1005.K次取反后最大化的数组和 +## 1005.K次取反后最大化的数组和 题目地址:https://leetcode-cn.com/problems/maximize-sum-of-array-after-k-negations/ @@ -11,29 +16,29 @@ 以这种方式修改数组后,返回数组可能的最大和。 示例 1: -输入:A = [4,2,3], K = 1 -输出:5 -解释:选择索引 (1,) ,然后 A 变为 [4,-2,3]。 +输入: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]。 +示例 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]。 +示例 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 -# 思路 +## 思路 -本题思路其实比较好想了,如何可以让数组和最大呢? +本题思路其实比较好想了,如何可以让数组和最大呢? 贪心的思路,局部最优:让绝对值大的负数变为正数,当前数值达到最大,整体最优:整个数组和达到最大。 @@ -45,16 +50,16 @@ 虽然这道题目大家做的时候,可能都不会去想什么贪心算法,一鼓作气,就AC了。 -**我这里其实是为了给大家展现出来 经常被大家忽略的贪心思路,这么一道简单题,就用了两次贪心!** +**我这里其实是为了给大家展现出来 经常被大家忽略的贪心思路,这么一道简单题,就用了两次贪心!** 那么本题的解题步骤为: -* 第一步:将数组按照绝对值大小从大到小排序,**注意要按照绝对值的大小** -* 第二步:从前向后遍历,遇到负数将其变为正数,同时K-- +* 第一步:将数组按照绝对值大小从大到小排序,**注意要按照绝对值的大小** +* 第二步:从前向后遍历,遇到负数将其变为正数,同时K-- * 第三步:如果K还大于0,那么反复转变数值最小的元素,将K用完 * 第四步:求和 -对应C++代码如下: +对应C++代码如下: ```C++ class Solution { @@ -70,7 +75,7 @@ public: K--; } } - if (K % 2 == 1) A[A.size() - 1] *= -1; // 第三步 + if (K % 2 == 1) A[A.size() - 1] *= -1; // 第三步 int result = 0; for (int a : A) result += a; // 第四步 return result; @@ -78,21 +83,34 @@ public: }; ``` -# 总结 +## 总结 贪心的题目如果简单起来,会让人简单到开始怀疑:本来不就应该这么做么?这也算是算法?我认为这不是贪心? 本题其实很简单,不会贪心算法的同学都可以做出来,但是我还是全程用贪心的思路来讲解。 -因为贪心的思考方式一定要有! +因为贪心的思考方式一定要有! **如果没有贪心的思考方式(局部最优,全局最优),很容易陷入贪心简单题凭感觉做,贪心难题直接不会做,其实这样就锻炼不了贪心的思考方式了**。 所以明知道是贪心简单题,也要靠贪心的思考方式来解题,这样对培养解题感觉很有帮助。 -此时,有没有感觉Carl为了给大家写出优质的题解真的是煞费苦心啊!! 哈哈,还不赶紧帮忙宣传一波「代码随想录」,让更多的小伙伴知道这里,这样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/20201124161234338.png),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** +Java: + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/1035.不相交的线.md b/problems/1035.不相交的线.md new file mode 100644 index 00000000..6f2b6646 --- /dev/null +++ b/problems/1035.不相交的线.md @@ -0,0 +1,90 @@ +

+ + + + +

+

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

+ +## 1035.不相交的线 + +我们在两条独立的水平线上按给定的顺序写下 A 和 B 中的整数。 + +现在,我们可以绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且我们绘制的直线不与任何其他连线(非水平线)相交。 + +以这种方法绘制线条,并返回我们可以绘制的最大连线数。 + +![1035.不相交的线](https://img-blog.csdnimg.cn/2021032116363533.png) + +## 思路 + +相信不少录友看到这道题目都没啥思路,我们来逐步分析一下。 + +绘制一些连接两个数字 A[i] 和 B[j] 的直线,只要 A[i] == B[j],且直线不能相交! + +直线不能相交,这就是说明在字符串A中 找到一个与字符串B相同的子序列,且这个子序列不能改变相对顺序,只要相对顺序不改变,链接相同数字的直线就不会相交。 + +拿示例一A = [1,4,2], B = [1,2,4]为例,相交情况如图: + +![1035.不相交的线](https://img-blog.csdnimg.cn/20210321164517460.png) + +其实也就是说A和B的最长公共子序列是[1,4],长度为2。 这个公共子序列指的是相对顺序不变(即数字4在字符串A中数字1的后面,那么数字4也应该在字符串B数字1的后面) + +这么分析完之后,大家可以发现:**本题说是求绘制的最大连线数,其实就是求两个字符串的最长公共子序列的长度!** + +那么本题就和我们刚刚讲过的这道题目[动态规划:1143.最长公共子序列](https://mp.weixin.qq.com/s/Qq0q4HaE4TyasCTj2WGFOg)就是一样一样的了。 + +一样到什么程度呢? 把字符串名字改一下,其他代码都不用改,直接copy过来就行了。 + +其实本题就是求最长公共子序列的长度,介于我们刚刚讲过[动态规划:1143.最长公共子序列](https://mp.weixin.qq.com/s/Qq0q4HaE4TyasCTj2WGFOg),所以本题我就不再做动规五部曲分析了。 + +如果大家有点遗忘了最长公共子序列,就再看一下这篇:[动态规划:1143.最长公共子序列](https://mp.weixin.qq.com/s/Qq0q4HaE4TyasCTj2WGFOg) + +本题代码如下: + +```C++ +class Solution { +public: + int maxUncrossedLines(vector& A, vector& B) { + vector> dp(A.size() + 1, vector(B.size() + 1, 0)); + for (int i = 1; i <= A.size(); i++) { + for (int j = 1; j <= B.size(); j++) { + if (A[i - 1] == B[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[A.size()][B.size()]; + } +}; +``` + +## 总结 + +看到代码大家也可以发现其实就是求两个字符串的最长公共子序列,但如果没有做过[1143.最长公共子序列](https://mp.weixin.qq.com/s/Qq0q4HaE4TyasCTj2WGFOg),本题其实还有很有难度的。 + +这是Carl为什么要先讲[1143.最长公共子序列](https://mp.weixin.qq.com/s/Qq0q4HaE4TyasCTj2WGFOg)再讲本题,大家会发现一个正确的刷题顺序对算法学习是非常重要的! + +这也是Carl做了很多题目(包括ACM和力扣)才总结出来的规律,大家仔细体会一下哈。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/1047.删除字符串中的所有相邻重复项.md b/problems/1047.删除字符串中的所有相邻重复项.md index 1962e78d..7a06f02d 100644 --- a/problems/1047.删除字符串中的所有相邻重复项.md +++ b/problems/1047.删除字符串中的所有相邻重复项.md @@ -1,9 +1,19 @@ -## 题目地址 -https://leetcode-cn.com/problems/remove-all-adjacent-duplicates-in-string/ +

+ + + + +

+

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

+ + + > 匹配问题都是栈的强项 -# 1047. 删除字符串中的所有相邻重复项 +# 1047. 删除字符串中的所有相邻重复项 + +https://leetcode-cn.com/problems/remove-all-adjacent-duplicates-in-string/ 给出由小写字母组成的字符串 S,重复项删除操作会选择两个相邻且相同的字母,并删除它们。 @@ -12,20 +22,20 @@ https://leetcode-cn.com/problems/remove-all-adjacent-duplicates-in-string/ 在完成所有重复项删除操作后返回最终的字符串。答案保证唯一。 -示例: -输入:"abbaca" -输出:"ca" -解释: +示例: +输入:"abbaca" +输出:"ca" +解释: 例如,在 "abbaca" 中,我们可以删除 "bb" 由于两字母相邻且相同,这是此时唯一可以执行删除操作的重复项。之后我们得到字符串 "aaca",其中又只有 "aa" 可以执行重复项删除操作,所以最后的字符串为 "ca"。   -提示: -1 <= S.length <= 20000 -S 仅由小写英文字母组成。 +提示: +1 <= S.length <= 20000 +S 仅由小写英文字母组成。 -# 思路 +# 思路 -## 题外话 +## 题外话 这道题目就像是我们玩过的游戏对对碰,如果相同的元素放在挨在一起就要消除。 @@ -35,7 +45,7 @@ S 仅由小写英文字母组成。 游戏开发可能使用栈结构,编程语言的一些功能实现也会使用栈结构,实现函数递归调用就需要栈,但不是每种编程语言都支持递归,例如: - +![1047.删除字符串中的所有相邻重复项](https://img-blog.csdnimg.cn/20210309093252776.png) **递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中**,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。 @@ -54,13 +64,13 @@ S 仅由小写英文字母组成。 如动画所示: - +![1047.删除字符串中的所有相邻重复项](https://code-thinking.cdn.bcebos.com/gifs/1047.%E5%88%A0%E9%99%A4%E5%AD%97%E7%AC%A6%E4%B8%B2%E4%B8%AD%E7%9A%84%E6%89%80%E6%9C%89%E7%9B%B8%E9%82%BB%E9%87%8D%E5%A4%8D%E9%A1%B9.gif) -从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒叙的,所以在对字符串进行反转一下,就得到了最终的结果。 +从栈中弹出剩余元素,此时是字符串ac,因为从栈里弹出的元素是倒叙的,所以在对字符串进行反转一下,就得到了最终的结果。 -## C++代码 +C++代码 : -``` +```C++ class Solution { public: string removeDuplicates(string S) { @@ -88,7 +98,7 @@ public: 代码如下: -``` +```C++ class Solution { public: string removeDuplicates(string S) { @@ -106,5 +116,24 @@ public: }; ``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/1049.最后一块石头的重量II.md b/problems/1049.最后一块石头的重量II.md index 3fd745bd..6f654fac 100644 --- a/problems/1049.最后一块石头的重量II.md +++ b/problems/1049.最后一块石头的重量II.md @@ -1,24 +1,172 @@ +

+ + + + +

+

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

+# 动态规划:最后一块石头的重量 II -// 如何转化为01背包问题 +## 1049. 最后一块石头的重量 II -尽量让石头分成,重量相同的两堆,这样就化解成 01背包问题了。 +题目链接:https://leetcode-cn.com/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 + +## 思路 + +如果对背包问题不都熟悉先看这两篇: + +* [动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA) + +本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,**这样就化解成01背包问题了**。 + +是不是感觉和昨天讲解的[416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)非常像了。 + +本题物品的重量为store[i],物品的价值也为store[i]。 + +对应着01背包里的物品重量weight[i]和 物品价值value[i]。 + +接下来进行动规五步曲: + +1. 确定dp数组以及下标的含义 + +**dp[j]表示容量(这里说容量更形象,其实就是重量)为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]);** + +一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎。 + +还是要牢记dp[j]的含义,要知道dp[j - stones[i]]为 容量为j - 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://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒叙遍历! + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/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++代码如下: + +```C++ class Solution { public: int lastStoneWeightII(vector& stones) { - vector dp(30001, 0); + 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 >= 0; j--) { - if (j - stones[i] >= 0) { - dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]); - } + 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://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)几乎是一样的,只是最后对dp[target]的处理方式不同。 + +[416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)相当于是求背包是否正好装满,而本题是求背包最多能装多少。 + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/1143.最长公共子序列.md b/problems/1143.最长公共子序列.md new file mode 100644 index 00000000..0664b0d9 --- /dev/null +++ b/problems/1143.最长公共子序列.md @@ -0,0 +1,145 @@ +

+ + + + +

+

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

+ +## 1143.最长公共子序列 + +给定两个字符串 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 +输入的字符串只含有小写英文字符。 + +## 思路 + +本题和[动态规划:718. 最长重复子数组](https://mp.weixin.qq.com/s/U5WaWqBwdoxzQDotOdWqZg)区别在于这里不要求是连续的了,但要有相对顺序,即:"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也可以,大家可以试一试! + +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]); + +代码如下: + +```C++ +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]应该是多少呢? + +test1[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://img-blog.csdnimg.cn/20210204115139616.jpg) + +那么为了在递推的过程中,这三个方向都是经过计算的数值,所以要从前向后,从上到下来遍历这个矩阵。 + +5. 举例推导dp数组 + +以输入:text1 = "abcde", text2 = "ace" 为例,dp状态如图: + +![1143.最长公共子序列1](https://img-blog.csdnimg.cn/20210210150215918.jpg) + +最后红框dp[text1.size()][text2.size()]为最终结果 + +以上分析完毕,C++代码如下: + +```C++ +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()]; + } +}; +``` + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/1207.独一无二的出现次数.md b/problems/1207.独一无二的出现次数.md deleted file mode 100644 index e6488cf1..00000000 --- a/problems/1207.独一无二的出现次数.md +++ /dev/null @@ -1,46 +0,0 @@ - -## 链接 -https://leetcode-cn.com/problems/unique-number-of-occurrences/ - -## 思路 - -这道题目数组在是哈希法中的经典应用,如果对数组在哈希法中的使用还不熟悉的同学可以看这两篇:[数组在哈希法中的应用](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig)和[哈希法:383. 赎金信](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) - -进而可以学习一下[set在哈希法中的应用](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA),以及[map在哈希法中的应用](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ) - -回归本题,**本题强调了-1000 <= arr[i] <= 1000**,那么就可以用数组来做哈希,arr[i]作为哈希表(数组)的下标,那么arr[i]可以是负数,怎么办?负数不能做数组下标。 - - -**此时可以定义一个2000大小的数组,例如int count[2002];**,统计的时候,将arr[i]统一加1000,这样就可以统计arr[i]的出现频率了。 - -题目中要求的是是否有相同的频率出现,那么需要再定义一个哈希表(数组)用来记录频率是否重复出现过,bool fre[1002]; 定义布尔类型的就可以了,**因为题目中强调1 <= arr.length <= 1000,所以哈希表大小为1000就可以了**。 - -如图所示: - - - - -C++代码如下: - -``` -class Solution { -public: - bool uniqueOccurrences(vector& arr) { - int count[2002] = {0}; // 统计数字出现的频率 - for (int i = 0; i < arr.size(); i++) { - count[arr[i] + 1000]++; - } - bool fre[1002] = {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; - } -}; -``` -> **我是[程序员Carl](https://github.com/youngyangyang04),更多[精彩算法文章](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/1221.分割平衡字符串.md b/problems/1221.分割平衡字符串.md deleted file mode 100644 index 757c483f..00000000 --- a/problems/1221.分割平衡字符串.md +++ /dev/null @@ -1,19 +0,0 @@ - -这是贪心啊,LRLR 这本身就是平衡子串 , 但要LR这么分割,这是贪心 - - -``` -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; - } -}; -``` diff --git a/problems/1356.根据数字二进制下1的数目排序.md b/problems/1356.根据数字二进制下1的数目排序.md deleted file mode 100644 index 6437eb39..00000000 --- a/problems/1356.根据数字二进制下1的数目排序.md +++ /dev/null @@ -1,71 +0,0 @@ - -## 题目链接 -https://leetcode-cn.com/problems/sort-integers-by-the-number-of-1-bits/ - -## 思路 - -这道题其实是考察如何计算一个数的二进制中1的数量。 - -我提供两种方法: - -* 方法一: - -朴实无华挨个计算1的数量,最多就是循环n的二进制位数,32位。 - -``` -int bitCount(int n) { - int count = 0; // 计数器 - while (n > 0) { - if((n & 1) == 1) count++; // 当前位是1,count++ - n >>= 1 ; // n向右移位 - } - return count; -} -``` - -* 方法二 - -这种方法,只循环n的二进制中1的个数次,比方法一高效的多 - -``` -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; - } -}; -``` diff --git a/problems/1365.有多少小于当前数字的数字.md b/problems/1365.有多少小于当前数字的数字.md deleted file mode 100644 index 723547d3..00000000 --- a/problems/1365.有多少小于当前数字的数字.md +++ /dev/null @@ -1,75 +0,0 @@ - -两层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里存放的就是相同元素最左面的数值和下标了**。 -代码如下: - -``` -int hash[101]; -for (int i = vec.size() - 1; i >= 0; i--) { // 从后向前,记录 vec[i] 对应的下标 - hash[vec[i]] = i; -} -``` - -最后在遍历原数组nums,用hash快速找到每一个数值 对应的 小于这个数值的个数。存放在将结果存放在另一个数组中。 - -代码如下: - -``` -// 此时hash里保存的每一个元素数值 对应的 小于这个数值的个数 -for (int i = 0; i < nums.size(); i++) { - vec[i] = hash[nums[i]]; -} -``` - -流程如图: - - - - -关键地方讲完了,整体C++代码如下: -``` -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(nlogn) - - -今天上午有事情,题解写的匆忙,下午我会继续完善题解。 - - diff --git a/problems/1382.将二叉搜索树变平衡.md b/problems/1382.将二叉搜索树变平衡.md deleted file mode 100644 index 9d3a2b65..00000000 --- a/problems/1382.将二叉搜索树变平衡.md +++ /dev/null @@ -1,46 +0,0 @@ - -## 题目地址 -https://leetcode-cn.com/problems/balance-a-binary-search-tree/ - -## 思路 - -这道题目,可以中序遍历把二叉树转变为有序数组,然后在根据有序数组构造平衡二叉搜索树。 - -建议做这道题之前,先看如下两篇题解: -* [98.验证二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q) 学习二叉搜索树的特性 -* [108.将有序数组转换为二叉搜索树](https://mp.weixin.qq.com/s/sy3ygnouaZVJs8lhFgl9mw) 学习如何通过有序数组构造二叉搜索树 - -这两道题目做过之后,本题分分钟就可以做出来了。 - -代码如下: - -``` -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); - } -}; -``` diff --git a/problems/1403.非递增顺序的最小子序列.md b/problems/1403.非递增顺序的最小子序列.md deleted file mode 100644 index 1957678c..00000000 --- a/problems/1403.非递增顺序的最小子序列.md +++ /dev/null @@ -1,25 +0,0 @@ - -你说这是简单题吧,也是这就使用了贪心算法 - -``` -class Solution { -private: - static bool cmp(int a, int b) { - return a > b; - } -public: - vector minSubsequence(vector& nums) { - sort(nums.begin(), nums.end(), cmp); - int sum = 0; - for (int i = 0; i < nums.size(); i++) sum += nums[i]; - vector result; - int resultSum = 0; - for (int i = 0; i < nums.size(); i++) { - resultSum += nums[i]; - result.push_back(nums[i]); - if (resultSum > (sum - resultSum)) break; - } - return result; - } -}; -``` diff --git a/problems/1518.换酒问题.md b/problems/1518.换酒问题.md deleted file mode 100644 index 3dbd7739..00000000 --- a/problems/1518.换酒问题.md +++ /dev/null @@ -1,69 +0,0 @@ - -# 1518.换酒问题 - -小区便利店正在促销,用 numExchange 个空酒瓶可以兑换一瓶新酒。你购入了 numBottles 瓶酒。 - -如果喝掉了酒瓶中的酒,那么酒瓶就会变成空的。 - -请你计算 最多 能喝到多少瓶酒。 - - -![1518.换酒问题](https://img-blog.csdnimg.cn/20201215173958151.png) - -输入:numBottles = 9, numExchange = 3 -输出:13 -解释:你可以用 3 个空酒瓶兑换 1 瓶酒。 -所以最多能喝到 9 + 3 + 1 = 13 瓶酒。 - -![1518.换酒问题](https://img-blog.csdnimg.cn/20201215174130529.png) - -输入:numBottles = 15, numExchange = 4 -输出:19 -解释:你可以用 4 个空酒瓶兑换 1 瓶酒。 -所以最多能喝到 15 + 3 + 1 = 19 瓶酒。 - -示例 3: -输入:numBottles = 5, numExchange = 5 -输出:6 - -示例 4: -输入:numBottles = 2, numExchange = 3 -输出:2 - -提示: - -* 1 <= numBottles <= 100 -* 2 <= numExchange <= 100 - -# 思路 - -这道题目其实是很简单的了,简单到大家都不以为这是贪心算法,哈哈 - -来分析一下: - -局部最优:每次换酒用尽可能多的酒瓶。全局最优:喝到最多的酒。 - -局部最优可以推出全局最优,那么这就是贪心! - -本题其实 - - -每次环境 - -其实思路是简单的,但本题在 - -``` -// 这道题还是有陷阱啊,15 4 这个例子,答案应该是19 而不是18 -class Solution { -public: - int numWaterBottles(int numBottles, int numExchange) { - int result = numBottles; - while (numBottles / numExchange) { - result += numBottles / numExchange; - // 所以不是 numBottles = (numBottles / numExchange) - numBottles = (numBottles / numExchange) + (numBottles % numExchange); - } - return result; - } -}; -``` diff --git a/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md b/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md new file mode 100644 index 00000000..32277bb1 --- /dev/null +++ b/problems/O(n)的算法居然超时了,此时的n究竟是多大?.md @@ -0,0 +1,238 @@ +

+ + + + +

+

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

+# 程序提交之后为什么会超时?O(n)的算法会超时,n究竟是多大? + + +一些同学可能对计算机运行的速度还没有概念,就是感觉计算机运行速度应该会很快,那么在leetcode上做算法题目的时候为什么会超时呢? + +计算机究竟1s可以执行多少次操作呢? 接下来探讨一下这个问题。 + +# 超时是怎么回事 + +![程序超时](https://img-blog.csdnimg.cn/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(nlogn),使用加法运算来统一测试。 + +```C++ +// O(n) +void function1(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + k++; + } +} + +``` + +```C++ +// 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++; + } + } + +} +``` + +```C++ +// 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 注释掉 +```C++ +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://img-blog.csdnimg.cn/20200729200018460.png) + +O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下O(n^2) 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。 + +![程序超时3](https://img-blog.csdnimg.cn/2020072919590970.png) + +O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。 + +在推测一下O(nlogn)的话, 1s可以处理的数据规模是什么呢? + +理论上应该是比 O(n)少一个数量级,因为logn的复杂度 其实是很快,看一下实验数据。 + +![程序超时4](https://img-blog.csdnimg.cn/20200729195729407.png) + +O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 + +这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。 + +**整体测试数据整理如下:** + +![程序超时1](https://img-blog.csdnimg.cn/20201208231559175.png) + +至于O(logn) 和O(n^3) 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。 + +# 完整测试代码 + +```C++ +#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的大小。 + +建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。 + +这样,大家应该对程序超时时候的数据规模有一个整体的认识了。 + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/leetcode的耗时统计.md b/problems/leetcode的耗时统计.md deleted file mode 100644 index 8e38e30e..00000000 --- a/problems/leetcode的耗时统计.md +++ /dev/null @@ -1,8 +0,0 @@ - -还有一些录友会很关心leetcode上的耗时统计。 - -这个是很不准确的,相同的代码多提交几次,大家就知道怎么回事了。 - -leetcode上的计时应该是以4ms为单位,有的多提交几次,多个4ms就多击败50%,所以比较夸张,如果程序运行是几百ms的级别,可以看看leetcode上的耗时,因为它的误差10几ms对最终影响不大。 - -**所以我的题解基本不会写击败百分之多少多少,没啥意义,时间复杂度分析清楚了就可以了**,至于回溯算法不用分析时间复杂度了,都是一样的爆搜,就看谁剪枝厉害了。 diff --git a/problems/为了绝杀编辑距离,卡尔做了三步铺垫.md b/problems/为了绝杀编辑距离,卡尔做了三步铺垫.md new file mode 100644 index 00000000..60334d1f --- /dev/null +++ b/problems/为了绝杀编辑距离,卡尔做了三步铺垫.md @@ -0,0 +1,184 @@ +

+ + + + +

+

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

+ +# 动态规划之编辑距离总结篇 + +本周我们讲了动态规划之终极绝杀:编辑距离,为什么叫做终极绝杀呢? + +细心的录友应该知道,我们在前三篇动态规划的文章就一直为 编辑距离 这道题目做铺垫。 + +## 判断子序列 + +[动态规划:392.判断子序列](https://mp.weixin.qq.com/s/2pjT4B4fjfOx5iB6N6xyng) 给定字符串 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://mp.weixin.qq.com/s/1SULY2XVSROtk_hsoVLu8A) 给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。 + +本题虽然也只有删除操作,不用考虑替换增加之类的,但相对于[动态规划:392.判断子序列](https://mp.weixin.qq.com/s/2pjT4B4fjfOx5iB6N6xyng)就有难度了,这道题目双指针法可就做不了。 + + +当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]; + + +状态转移方程: +```C++ +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://mp.weixin.qq.com/s/a8BerpqSf76DCqkPDJrpYg)给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。 + +本题和[动态规划:115.不同的子序列](https://mp.weixin.qq.com/s/1SULY2XVSROtk_hsoVLu8A)相比,其实就是两个字符串可以都可以删除了,情况虽说复杂一些,但整体思路是不变的。 + + +* 当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}); + +状态转移方程: +```C++ +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://mp.weixin.qq.com/s/8aG71XjSgZG6kZbiAdkJnQ) 给你两个单词 word1 和 word2,请你计算出将 word1 转换成 word2 所使用的最少操作数 。 + + +编辑距离终于来了,**有了前面三道题目的铺垫,应该有思路了**,本题是两个字符串可以增删改,比 [动态规划:判断子序列](https://mp.weixin.qq.com/s/2pjT4B4fjfOx5iB6N6xyng),[动态规划:不同的子序列](https://mp.weixin.qq.com/s/1SULY2XVSROtk_hsoVLu8A),[动态规划:两个字符串的删除操作](https://mp.weixin.qq.com/s/a8BerpqSf76DCqkPDJrpYg)都要复杂的多。 + + +在确定递推公式的时候,首先要考虑清楚编辑的几种操作,整理如下: + +* 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; + +递归公式代码如下: + +```C++ +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://mp.weixin.qq.com/s/8aG71XjSgZG6kZbiAdkJnQ) ,Carl的良苦用心呀,你们体会到了嘛! + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/二叉树中递归带着回溯.md b/problems/二叉树中递归带着回溯.md index 3cb00f24..bf15e39b 100644 --- a/problems/二叉树中递归带着回溯.md +++ b/problems/二叉树中递归带着回溯.md @@ -1,7 +1,97 @@ - -在上一面 +

+ + + + +

+

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

+# 二叉树:以为使用了递归,其实还隐藏着回溯 + +> 补充一波 + +昨天的总结篇中[还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg),有两处问题需要说明一波。 + +## 求相同的树 + +[还在玩耍的你,该总结啦!(本周小结之二叉树)](https://mp.weixin.qq.com/s/QMBUTYnoaNfsVHlUADEzKg)中求100.相同的树的代码中,我笔误贴出了 求对称树的代码了,细心的同学应该都发现了。 + +那么如下我再给出求100. 相同的树 的代码,如下: + +```C++ +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://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg) 仅仅修改了变量的名字(为了符合判断相同树的语境)和 遍历的顺序。 + +大家应该会体会到:**认清[判断对称树](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)本质之后, 对称树的代码 稍作修改 就可以直接用来AC 100.相同的树。** + +## 递归中隐藏着回溯 + +在[二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)中我强调了本题其实是用到了回溯的,并且给出了第一个版本的代码,把回溯的过程充分的提现了出来。 + +如下的代码充分的体现出回溯:(257. 二叉树的所有路径) + +```C++ +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. 二叉树的所有路径) ``` class Solution { private: @@ -11,19 +101,8 @@ private: 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(); - } + if (cur->left) traversal(cur->left, path + "->", result); // 左 回溯就隐藏在这里 + if (cur->right) traversal(cur->right, path + "->", result); // 右 回溯就隐藏在这里 } public: @@ -33,40 +112,81 @@ public: if (root == NULL) return result; traversal(root, path, result); return result; - } }; ``` -没有回溯了 +上面的代码,大家貌似感受不到回溯了,其实**回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。** + +为了把这份精简代码的回溯过程展现出来,大家可以试一试把: + ``` -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) { - path += "->"; - traversal(cur->left, path, result); // 左 - } - if (cur->right) { - path += "->"; - 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; - - } -}; +if (cur->left) traversal(cur->left, path + "->", result); // 左 回溯就隐藏在这里 ``` + +改成如下代码: + +``` +path += "->"; +traversal(cur->left, path, result); // 左 +``` + +即: + +``` + +if (cur->left) { + path += "->"; + traversal(cur->left, path, result); // 左 +} +if (cur->right) { + path += "->"; + traversal(cur->right, path, result); // 右 +} +``` + +此时就没有回溯了,这个代码就是通过不了的了。 + +如果想把回溯加上,就要 在上面代码的基础上,加上回溯,就可以AC了。 + +``` +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(); +} +``` + +**大家应该可以感受出来,如果把 `path + "->"`作为函数参数就是可以的,因为并有没有改变path的数值,执行完递归函数之后,path依然是之前的数值(相当于回溯了)** + +如果有点遗忘了,建议把这篇[二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)在仔细看一下,然后再看这里的总结,相信会豁然开朗。 + +这里我尽量把逻辑的每一个细节都抠出来展现了,希望对大家有所帮助! + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/二叉树总结.md b/problems/二叉树总结篇.md similarity index 77% rename from problems/二叉树总结.md rename to problems/二叉树总结篇.md index 299b0e3a..05c13a47 100644 --- a/problems/二叉树总结.md +++ b/problems/二叉树总结篇.md @@ -1,3 +1,13 @@ +

+ + + + +

+

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

+ +# 二叉树:总结篇!(需要掌握的二叉树技能都在这里了) + > 力扣二叉树大总结! 不知不觉二叉树已经和我们度过了**三十三天**,[「代码随想录」](https://img-blog.csdnimg.cn/20200815195519696.png)里已经发了**三十三篇二叉树的文章**,详细讲解了**30+二叉树经典题目**,一直坚持下来的录友们一定会二叉树有深刻理解了。 @@ -10,113 +20,111 @@ 公众号的发文顺序,就是循序渐进的,所以如下分类基本就是按照文章发文顺序来的,我再做一个系统性的分类。 -# 二叉树的理论基础 +## 二叉树的理论基础 * [关于二叉树,你该了解这些!](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/guKwV-gSNbA1CcbvkMtHBg) * 递归:后序,求根节点最大高度就是最大深度,通过递归函数的返回值做计算树的高度 * 迭代:层序遍历 -* [二叉树:求最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA) +* [二叉树:求最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA) * 递归:后序,求根节点最小高度就是最小深度,注意最小深度的定义 - * 迭代:层序遍历 -* [二叉树:求有多少个节点](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw) + * 迭代:层序遍历 +* [二叉树:求有多少个节点](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/Osw4LQD2xVUnCJ-9jrYxJA) * 递归:前序,方便让父节点指向子节点,涉及回溯处理根节点到叶子的所有路径 * 迭代:一个栈模拟递归,一个栈来存放对应的遍历路径 -* [二叉树:递归中如何隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) +* [二叉树:递归中如何隐藏着回溯](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类型是为了搜索一条边,没有返回值是搜索整棵树。 + * 递归:顺序无所谓,递归函数返回值为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) - * 递归:前序,数组中间节点分割 + * 递归:前序,数组中间节点分割 * 迭代:较复杂,通过三个队列来模拟 -# 阶段总结 +## 阶段总结 大家以上题目都做过了,也一定要看如下阶段小结。 @@ -124,10 +132,10 @@ * [本周小结!(二叉树系列一)](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/JLLpx3a_8jurXcz6ovgxtg) * [本周小结!(二叉树系列四)](https://mp.weixin.qq.com/s/CbdtOTP0N-HIP7DR203tSg) -# 最后总结 +## 最后总结 **在二叉树题目选择什么遍历顺序是不少同学头疼的事情,我们做了这么多二叉树的题目了,Carl给大家大体分分类**。 @@ -144,10 +152,26 @@ **最后,二叉树系列就这么完美结束了,估计这应该是最长的系列了,感谢大家33天的坚持与陪伴,接下来我们又要开始新的系列了「回溯算法」!** -**录友们打卡的时候也说一说自己的感想吧!哈哈** - -> **我是[程序员Carl](https://github.com/youngyangyang04),可以找我[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)** -**如果感觉题解对你有帮助,不要吝啬给一个👍吧!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/二叉树理论基础.md b/problems/二叉树理论基础.md new file mode 100644 index 00000000..726fc7a8 --- /dev/null +++ b/problems/二叉树理论基础.md @@ -0,0 +1,204 @@ +

+ + + + +

+

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

+# 二叉树理论基础篇 + +我们要开启新的征程了,大家跟上! + +说道二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容在啰嗦一遍,所以一下我讲的都是一些比较重点的内容。 + +相信只要耐心看完,都会有所收获。 + +## 二叉树的种类 + +在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。 + +### 满二叉树 + +满二叉树:如果一棵二叉树只有度为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的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼! + +## 总结 + +二叉树是一种基础数据结构,在算法面试中都是常客,也是众多数据结构的基石。 + +本篇我们介绍了二叉树的种类、存储方式、遍历方式以及定义,比较全面的介绍了二叉树各个方面的重点,帮助大家扫一遍基础。 + +**说道二叉树,就不得不说递归,很多同学对递归都是又熟悉又陌生,递归的代码一般很简短,但每次都是一看就会,一写就废。** + + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/二叉树的理论基础.md b/problems/二叉树的理论基础.md index a305f05b..271e1f4e 100644 --- a/problems/二叉树的理论基础.md +++ b/problems/二叉树的理论基础.md @@ -1,16 +1,24 @@ -# 二叉树理论基础 +

+ + + + +

+

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

+ +## 二叉树理论基础 我们要开启新的征程了,大家跟上! -说道二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容在啰嗦一遍,所以一下我讲的都是一些比较重点的内容。 +说道二叉树,大家对于二叉树其实都很熟悉了,本文呢我也不想教科书式的把二叉树的基础内容再啰嗦一遍,所以一下我讲的都是一些比较重点的内容。 相信只要耐心看完,都会有所收获。 -# 二叉树的种类 +## 二叉树的种类 在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。 -## 满二叉树 +### 满二叉树 满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。 @@ -18,14 +26,14 @@ -这棵二叉树为满二叉树,也可以说深度为k,有2^k-1个节点的二叉树。 +这棵二叉树为满二叉树,也可以说深度为 k,有 $(2^k)-1$ 个节点的二叉树。 -## 完全二叉树 +### 完全二叉树 什么是完全二叉树? -完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^h -1  个节点。 +完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1 ~ $2^{(h-1)}$  个节点。 **大家要自己看完全二叉树的定义,很多同学对完全二叉树其实不是真正的懂了。** @@ -37,7 +45,7 @@ **之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。** -## 二叉搜索树 +### 二叉搜索树 前面介绍的书,都没有数值的,而二叉搜索树是有数值的了,**二叉搜索树是一个有序树**。 @@ -50,7 +58,7 @@ -## 平衡二叉搜索树 +### 平衡二叉搜索树 平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。 @@ -65,7 +73,7 @@ **所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!** -# 二叉树的存储方式 +## 二叉树的存储方式 **二叉树可以链式存储,也可以顺序存储。** @@ -85,23 +93,23 @@ 用数组来存储二叉树如何遍历的呢? -**如果父节点的数组下表是i,那么它的左孩子就是i * 2 + 1,右孩子就是 i * 2 + 2。** +**如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。** 但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。 **所以大家要了解,用数组依然可以表示二叉树。** -# 二叉树的遍历方式 +## 二叉树的遍历方式 关于二叉树的遍历方式,要知道二叉树遍历的基本方式都有哪些。 -一些同学用做了很多二叉树的题目了,可能知道前中后序遍历,可能知道层序遍历,但是却没有框架。 +一些同学用做了很多二叉树的题目了,可能知道前序、中序、后序遍历,可能知道层序遍历,但是却没有框架。 我这里把二叉树的几种遍历方式列出来,大家就可以一一串起来了。 二叉树主要有两种遍历方式: -1. 深度优先遍历:先往深走,遇到叶子节点再往回走。 -2. 广度优先遍历:一层一层的去遍历。 +1. 深度优先遍历:先往深走,遇到叶子节点再往回走。 +2. 广度优先遍历:一层一层的去遍历。 **这两种遍历是图论中最基本的两种遍历方式**,后面在介绍图论的时候 还会介绍到。 @@ -111,13 +119,13 @@ * 前序遍历(递归法,迭代法) * 中序遍历(递归法,迭代法) * 后序遍历(递归法,迭代法) -* 广度优先遍历 +* 广度优先遍历 * 层次遍历(迭代法) -在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。 +在深度优先遍历中:有三个顺序,前序、中序、后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。 -**这里前中后,其实指的就是中间节点的遍历顺序**,只要大家记住 前中后序指的就是中间节点的位置就可以了。 +**这里前、中、后,其实指的就是中间节点的遍历顺序**,只要大家记住 前序、中序、后序指的就是中间节点的位置就可以了。 看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式 @@ -129,17 +137,17 @@ -最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。 +最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前序、中序、后序遍历,使用递归是比较方便的。 -**之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构**,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。 +**之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构**,也就说前序、中序、后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。 而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。 **这里其实我们又了解了栈与队列的一个应用场景了。** 具体的实现我们后面都会讲的,这里大家先要清楚这些理论基础。 - -# 二叉树的定义 + +## 二叉树的定义 刚刚我们说过了二叉树有两种存储方式顺序存储,和链式存储,顺序存储就是用数组来存,这个定义没啥可说的,我们来看看链式存储的二叉树节点的定义方式。 @@ -155,7 +163,7 @@ struct TreeNode { }; ``` -大家会发现二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子. +大家会发现二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子. 这里要提醒大家要注意二叉树节点定义的书写方式。 @@ -163,7 +171,7 @@ struct TreeNode { 因为我们在刷leetcode的时候,节点的定义默认都定义好了,真到面试的时候,需要自己写节点定义的时候,有时候会一脸懵逼! -# 总结 +## 总结 二叉树是一种基础数据结构,在算法面试中都是常客,也是众多数据结构的基石。 @@ -171,6 +179,24 @@ struct TreeNode { **说道二叉树,就不得不说递归,很多同学对递归都是又熟悉又陌生,递归的代码一般很简短,但每次都是一看就会,一写就废。** -**那么请跟住Carl的节奏,不仅彻底掌握二叉树的递归遍历,还有迭代遍历!** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/二叉树的统一迭代法.md b/problems/二叉树的统一迭代法.md index fe8466e3..3ea4eeaf 100644 --- a/problems/二叉树的统一迭代法.md +++ b/problems/二叉树的统一迭代法.md @@ -1,3 +1,14 @@ +

+ + + + +

+

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

+ + + +# 二叉树的统一迭代法 > 统一写法是一种什么感觉 @@ -19,11 +30,11 @@ 如何标记呢,**就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。** 这种方法也可以叫做标记法。 -# 迭代法中序遍历 +## 迭代法中序遍历 中序遍历代码如下:(详细注释) -``` +```C++ class Solution { public: vector inorderTraversal(TreeNode* root) { @@ -54,7 +65,7 @@ public: 看代码有点抽象我们来看一下动画(中序遍历): - +![中序遍历迭代(统一写法)](https://tva1.sinaimg.cn/large/008eGmZEly1gnbmq3btubg30em09ue82.gif) 动画中,result数组就是最终结果集。 @@ -62,11 +73,11 @@ public: 此时我们再来看前序遍历代码。 -# 迭代法前序遍历 +## 迭代法前序遍历 迭代法前序遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) -``` +```C++ class Solution { public: vector preorderTraversal(TreeNode* root) { @@ -93,11 +104,11 @@ public: }; ``` -# 迭代法后序遍历 +## 迭代法后序遍历 后续遍历代码如下: (**注意此时我们和中序遍历相比仅仅改变了两行代码的顺序**) -``` +```C++ class Solution { public: vector postorderTraversal(TreeNode* root) { @@ -126,7 +137,7 @@ public: }; ``` -# 总结 +## 总结 此时我们写出了统一风格的迭代法,不用在纠结于前序写出来了,中序写不出来的情况了。 @@ -134,3 +145,26 @@ public: 所以大家根据自己的个人喜好,对于二叉树的前中后序遍历,选择一种自己容易理解的递归和迭代法。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/二叉树的迭代遍历.md b/problems/二叉树的迭代遍历.md index 17403bb3..2647616b 100644 --- a/problems/二叉树的迭代遍历.md +++ b/problems/二叉树的迭代遍历.md @@ -1,3 +1,14 @@ +

+ + + + +

+

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

+ + +# 二叉树的迭代遍历 + > 听说还可以用非递归的方式 看完本篇大家可以使用迭代法,再重新解决如下三道leetcode上的题目: @@ -22,11 +33,11 @@ 动画如下: - +![二叉树前序遍历(迭代法)](https://tva1.sinaimg.cn/large/008eGmZEly1gnbmss7603g30eq0d4b2a.gif) 不难写出如下代码: (**注意代码中空节点不入栈**) -``` +```C++ class Solution { public: vector preorderTraversal(TreeNode* root) { @@ -69,11 +80,11 @@ public: 动画如下: - +![二叉树中序遍历(迭代法)](https://tva1.sinaimg.cn/large/008eGmZEly1gnbmuj244bg30eq0d4kjm.gif) **中序遍历,可以写出如下代码:** -``` +```C++ class Solution { public: vector inorderTraversal(TreeNode* root) { @@ -105,7 +116,7 @@ public: **所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:** -``` +```C++ class Solution { public: vector postorderTraversal(TreeNode* root) { @@ -127,7 +138,7 @@ public: ``` -# 总结 +## 总结 此时我们用迭代法写出了二叉树的前后中序遍历,大家可以看出前序和中序是完全两种代码风格,并不想递归写法那样代码稍做调整,就可以实现前后中序。 @@ -139,3 +150,25 @@ public: 当然可以,这种写法,还不是很好理解,我们将在下一篇文章里重点讲解,敬请期待! + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/二叉树的递归遍历.md b/problems/二叉树的递归遍历.md index 1bc9a00b..6c005191 100644 --- a/problems/二叉树的递归遍历.md +++ b/problems/二叉树的递归遍历.md @@ -1,5 +1,14 @@ +

+ + + + +

+

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

-# 二叉树: 一入递归深似海,从此offer是路人 + + +# 二叉树的递归遍历 > 一看就会,一写就废! @@ -22,6 +31,7 @@ 好了,我们确认了递归的三要素,接下来就来练练手: + **以下以前序遍历为例:** 1. **确定递归函数的参数和返回值**:因为要打印出前序遍历节点的数值,所以参数里需要传入vector在放节点的数值,除了这一点就不需要在处理什么数据了也不需要有返回值,所以递归函数返回类型就是void,代码如下: @@ -48,7 +58,7 @@ traversal(cur->right, vec); // 右 前序遍历: -``` +```C++ class Solution { public: void traversal(TreeNode* cur, vector& vec) { @@ -69,24 +79,24 @@ public: 中序遍历: -``` - void traversal(TreeNode* cur, vector& vec) { - if (cur == NULL) return; - traversal(cur->left, vec); // 左 - vec.push_back(cur->val); // 中 - traversal(cur->right, vec); // 右 - } +```C++ +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + vec.push_back(cur->val); // 中 + traversal(cur->right, vec); // 右 +} ``` 后序遍历: -``` - void traversal(TreeNode* cur, vector& vec) { - if (cur == NULL) return; - traversal(cur->left, vec); // 左 - traversal(cur->right, vec); // 右 - vec.push_back(cur->val); // 中 - } +```C++ +void traversal(TreeNode* cur, vector& vec) { + if (cur == NULL) return; + traversal(cur->left, vec); // 左 + traversal(cur->right, vec); // 右 + vec.push_back(cur->val); // 中 +} ``` 此时大家可以做一做leetcode上三道题目,分别是: @@ -98,3 +108,25 @@ public: 可能有同学感觉前后中序遍历的递归太简单了,要打迭代法(非递归),别急,我们明天打迭代法,打个通透! + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/关于时间复杂度,你不知道的都在这里!.md b/problems/关于时间复杂度,你不知道的都在这里!.md new file mode 100644 index 00000000..b4ff89fa --- /dev/null +++ b/problems/关于时间复杂度,你不知道的都在这里!.md @@ -0,0 +1,180 @@ +

+ + + + +

+

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

+ +相信每一位录友都接触过时间复杂度,「代码随想录」已经也讲了一百多道经典题目了,是时候对时间复杂度来一个深度的剖析了,很早之前就写过一篇,当时文章还没有人看,Carl感觉有价值的东西值得让更多的人看到,哈哈。 + +所以重新整理的时间复杂度文章,正式和大家见面啦! + +## 究竟什么是时间复杂度 + +**时间复杂度是一个函数,它定性描述该算法的运行时间**。 + +我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的答题时间。 + +那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认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://img-blog.csdnimg.cn/20200728185745611.png) + +我们主要关心的还是一般情况下的数据形式。 + +**面试中说道算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 + + +## 不同数据规模的差异 + +如下图中可以看出不同算法的时间复杂度在不同数据输入规模下的差异。 + +![时间复杂度,不同数据规模的差异](https://img-blog.csdnimg.cn/20200728191447384.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(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://img-blog.csdnimg.cn/20200728191447349.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 * logn) 。 + +之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 O(m * n * logn + n * m)。 + +我们对O(m * n * logn + n * m) 进行简化操作,把m * n提取出来变成 O(m * n * (logn + 1)),再省略常数项最后的时间复杂度是 O(m * n * logn)。 + +最后很明显O(m * n * logn) 要优于O(m * n * n)! + +所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。 + +这就是我们通过分析两种算法的时间复杂度得来的。 + +**当然这不是这道题目的最优解,我仅仅是用这道题目来讲解一下时间复杂度**。 + +# 总结 + +本篇讲解了什么是时间复杂度,复杂度是用来干什么,以及数据规模对时间复杂度的影响。 + +还讲解了被大多数同学忽略的大O的定义以及log究竟是以谁为底的问题。 + +再分析了如何简化复杂的时间复杂度,最后举一个具体的例子,把本篇的内容串起来。 + +相信看完本篇,大家对时间复杂度的认识会深刻很多! + +如果感觉「代码随想录」很不错,赶快推荐给身边的朋友同学们吧,他们发现和「代码随想录」相见恨晚! + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/前序/BAT级别技术面试流程和注意事项都在这里了.md b/problems/前序/BAT级别技术面试流程和注意事项都在这里了.md new file mode 100644 index 00000000..f8598d3f --- /dev/null +++ b/problems/前序/BAT级别技术面试流程和注意事项都在这里了.md @@ -0,0 +1,222 @@ + + +

+ + + + +

+ +大型互联网企业一般通过几轮技术面试来考察大家的各项能力,一般流程如下: + +* 一面机试:一般会考选择题和编程题 +* 二面基础算法面:就是基础的算法都是该专栏要讲的 +* 三面综合技术面:会考察编程语言,计算机基础知识 ,以及了解项目经历等等 +* 四面技术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也不会只聊很严肃的话题, 也会聊一聊家常之类的,问一问 家在哪里?在介绍一下公司薪酬福利待遇,这些就比较放松了 + +## 总结 + +这里面试流程就是这样了, 还是那句话 不是所有公司都按照这个流程来面试,但是如果是一线互联网公司,一般都会从我说的这几方面来考察大家 +大家加油! + + +------------------------ + +* 微信:[程序员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/前序/On的算法居然超时了,此时的n究竟是多大?.md b/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md new file mode 100644 index 00000000..f640abad --- /dev/null +++ b/problems/前序/On的算法居然超时了,此时的n究竟是多大?.md @@ -0,0 +1,229 @@ + + +

+ + + + +

+ +一些同学可能对计算机运行的速度还没有概念,就是感觉计算机运行速度应该会很快,那么在leetcode上做算法题目的时候为什么会超时呢? + +计算机究竟1s可以执行多少次操作呢? 接下来探讨一下这个问题。 + +# 超时是怎么回事 + +![程序超时](https://img-blog.csdnimg.cn/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(nlogn),使用加法运算来统一测试。 + +```C++ +// O(n) +void function1(long long n) { + long long k = 0; + for (long long i = 0; i < n; i++) { + k++; + } +} + +``` + +```C++ +// 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++; + } + } + +} +``` + +```C++ +// 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 注释掉 +```C++ +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://img-blog.csdnimg.cn/20200729200018460.png) + +O(n)的算法,1s内大概计算机可以运行 5 * (10^8)次计算,可以推测一下O(n^2) 的算法应该1s可以处理的数量级的规模是 5 * (10^8)开根号,实验数据如下。 + +![程序超时3](https://img-blog.csdnimg.cn/2020072919590970.png) + +O(n^2)的算法,1s内大概计算机可以运行 22500次计算,验证了刚刚的推测。 + +在推测一下O(nlogn)的话, 1s可以处理的数据规模是什么呢? + +理论上应该是比 O(n)少一个数量级,因为logn的复杂度 其实是很快,看一下实验数据。 + +![程序超时4](https://img-blog.csdnimg.cn/20200729195729407.png) + +O(nlogn)的算法,1s内大概计算机可以运行 2 * (10^7)次计算,符合预期。 + +这是在我个人PC上测出来的数据,不能说是十分精确,但数量级是差不多的,大家也可以在自己的计算机上测一下。 + +**整体测试数据整理如下:** + +![程序超时1](https://img-blog.csdnimg.cn/20201208231559175.png) + +至于O(logn) 和O(n^3) 等等这些时间复杂度在1s内可以处理的多大的数据规模,大家可以自己写一写代码去测一下了。 + +# 完整测试代码 + +```C++ +#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的大小。 + +建议录友们也都自己做一做实验,测一测,看看是不是和我的测出来的结果差不多。 + +这样,大家应该对程序超时时候的数据规模有一个整体的认识了。 + +就酱,如果感觉「代码随想录」很干货,就帮忙宣传一波吧,很多录友发现这里之后都感觉相见恨晚! + + +------------------------ + +* 微信:[程序员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/前序/上海互联网公司总结.md b/problems/前序/上海互联网公司总结.md new file mode 100644 index 00000000..40478cb6 --- /dev/null +++ b/problems/前序/上海互联网公司总结.md @@ -0,0 +1,134 @@ + +

+ + + + +

+ +# 上海互联网公司总结 + +**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** + +## 一线互联网 + +* 百度(上海) +* 阿里(上海) +* 腾讯(上海) +* 字节跳动(上海) +* 蚂蚁金服(上海) + +## 外企IT/互联网/硬件 + +* 互联网 + * Google(上海) + * 微软(上海) + * LeetCode/力扣(上海) + * unity(上海)游戏引擎 + * SAP(上海)主要产品是ERP + * PayPal(上海)在线支付鼻祖 + * eBay(上海)电子商务公司 +* 偏硬件 + * IBM(上海) + * Tesla(上海)特斯拉 + * Cisco(上海)思科 + * Intel(上海) + * AMD(上海)半导体产品领域 + * EMC(上海)易安信是美国信息存储资讯科技公司 + * NVIDIA(上海)英伟达是GPU(图形处理器)的发明者,人工智能计算的引领者 + +## 二线互联网 + +* 拼多多(总部) +* 饿了么(总部)阿里旗下。 +* 哈啰出行(总部)阿里旗下 +* 盒马(总部)阿里旗下 +* 哔哩哔哩(总部) +* 阅文集团(总部)腾讯旗下 +* 爱奇艺(上海)百度旗下 +* 携程(总部) +* 京东(上海) +* 网易(上海) +* 美团点评(上海) +* 唯品会(上海) + +## 硬件巨头 (有软件/互联网业务) + +华为(上海) + +## 三线互联网 + +* PPTV(总部) +* 微盟(总部)企业云端商业及营销解决方案提供商 +* 喜马拉雅(总部) +* 陆金所(总部)全球领先的线上财富管理平台 +* 口碑(上海)阿里旗下。 +* 三七互娱(上海) +* 趣头条(总部) +* 巨人网络(总部)游戏公司 +* 盛大网络(总部)游戏公司 +* UCloud(总部)云服务提供商 +* 达达集团(总部)本地即时零售与配送平台 +* 众安保险(总部)在线财产保险 +* 触宝(总部)触宝输入法等多款APP +* 平安系列 + +## 明星创业公司 + +* 小红书(总部) +* 叮咚买菜(总部) +* 蔚来汽车(总部) +* 七牛云(总部) +* 得物App(总部)品潮流尖货装备交易、球鞋潮品鉴别查验、互动潮流社区 +* 收钱吧(总部)开创了中国移动支付市场“一站式收款” +* 蜻蜓FM(总部)音频内容聚合平台 +* 流利说(总部)在线教育 +* Soul(总部)社交软件 +* 美味不用等(总部)智慧餐饮服务商 +* 微鲸科技(总部)专注于智能家居领域 +* 途虎养车(总部) +* 米哈游(总部)游戏公司 +* 莉莉丝游戏(总部)游戏公司 +* 樊登读书(总部)在线教育 + +## AI独角兽公司 + +* 依图科技(总部)和旷视,商汤对标,都是做安防视觉 +* 深兰科技(总部)致力于人工智能基础研究和应用开发 + +## 其他行业,涉及互联网 +* 花旗、摩根大通等一些列金融巨头 +* 百姓网 +* 找钢网 +* 安居客 +* 前程无忧 +* 东方财富 +* 三大电信运营商:中国移动、中国电信、中国联通 +* 沪江英语 +* 各大银行 + +通知:很多同学感觉自己基础还比较薄弱,想循序渐进的从头学一遍数据结构与算法,那你来对地方了。在公众号左下角「算法汇总」里已经按照各个系列难易程度排好顺序了,大家跟着文章顺序打卡学习就可以了,留言区有很多录友都在从头打卡!「算法汇总」会持续更新,大家快去看看吧! + +## 总结 + +大家如果看了[北京有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/BKrjK4myNB-FYbMqW9f3yw)和[深圳原来有这么多互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q)就可以看出中国互联网氛围最浓的当然是北京,其次就是上海! + +很多人说深圳才是第二,上海没有产生BAT之类的企业。 + +**那么来看看上海在垂直领域上是如何独领风骚的,视频领域B站,电商领域拼多多小红书,生活周边有饿了么,大众点评(现与美团合并),互联网金融有蚂蚁金服和陆金所,出行领域有行业老大携程,而且BAT在上海都有部门还是很大的团队,再加上上海众多的外企,以及金融公司(有互联网业务)**。 + +此时就能感受出来,上海的互联网氛围要比深圳强很多! + +好了,希望这份list可以帮助到想在上海发展的录友们。 + +相对于北京和上海,深圳互联网公司断层很明显,腾讯一家独大,二线三线垂直行业的公司很少,所以说深圳腾讯的员工流动性相对是较低的,因为基本没得选。 + + +------------------------ + +* 微信:[程序员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/前序/什么是核心代码模式,什么又是ACM模式?.md b/problems/前序/什么是核心代码模式,什么又是ACM模式?.md new file mode 100644 index 00000000..70651817 --- /dev/null +++ b/problems/前序/什么是核心代码模式,什么又是ACM模式?.md @@ -0,0 +1,124 @@ + +

+ + + + +

+ +-------------------------- +现在很多企业都在牛客上进行面试,**很多录友和我反馈说搞不懂牛客上输入代码的ACM模式**。 + +什么是ACM输入模式呢? 就是自己构造输入数据格式,把要需要处理的容器填充好,OJ不会给你任何代码,包括include哪些函数都要自己写,最后也要自己控制返回数据的格式。 + +而力扣上是核心代码模式,就是把要处理的数据都已经放入容器里,可以直接写逻辑,例如这样: + +```C++ +class Solution { +public: + int minimumTotal(vector>& triangle) { + + } +}; +``` + +**如果大家从一开始学习算法就一直在力扣上的话,突然切到牛客网上的ACM模式会很不适应**。 + +因为我上学的时候就搞ACM,在POJ(北大的在线判题系统)和ZOJ(浙大的在线判题系统)上刷过6、7百道题目了,对这种ACM模式就很熟悉。 + +接下来我给大家讲一下ACM模式应该如何写。 + +这里我拿牛客上 腾讯2020校园招聘-后台 的面试题目来举一个例子,本题我不讲解题思路,只是拿本题为例讲解ACM输入输出格式。 + +题目描述: + +由于业绩优秀,公司给小Q放了 n 天的假,身为工作狂的小Q打算在在假期中工作、锻炼或者休息。他有个奇怪的习惯:不会连续两天工作或锻炼。只有当公司营业时,小Q才能去工作,只有当健身房营业时,小Q才能去健身,小Q一天只能干一件事。给出假期中公司,健身房的营业情况,求小Q最少需要休息几天。 + +输入描述: +第一行一个整数 表示放假天数 +第二行 n 个数 每个数为0或1,第 i 个数表示公司在第 i 天是否营业 +第三行 n 个数 每个数为0或1,第 i 个数表示健身房在第 i 天是否营业 +(1为营业 0为不营业) + +输出描述: +一个整数,表示小Q休息的最少天数 + +示例一: +输入: +4 +1 1 0 0 +0 1 1 0 + +输出: +2 + + +这道题如果要是力扣上的核心代码模式,OJ应该直接给出如下代码: + +```C++ +class Solution { +public: + int getDays(vector& work, vector& gym) { + // 处理逻辑 + } +}; +``` + +以上代码中我们直接写核心逻辑就行了,work数组,gym数组都是填好的,直接拿来用就行,处理完之后 return 结果就完事了。 + +那么看看ACM模式我们要怎么写呢。 + +ACM模式要求写出来的代码是直接可以本地运行的,所以我们需要自己写include哪些库函数,构造输入用例,构造输出用例。 + +拿本题来说,为了让代码可以运行,需要include这些库函数: + +```C++ +#include +#include +using namespace std; +``` + + +然后开始写主函数,来处理输入用例了,示例一 是一个完整的测试用例,一般我们测了一个用例还要测第二个用例,所以用:while(cin>>n) 来输入数据。 + +这里输入的n就是天数,得到天数之后,就可以来构造work数组和gym数组了。 + +此时就已经完成了输入用例构建,然后就是处理逻辑了,最后返回结果。 + +完整代码如下: + +```C++ +#include +#include +using namespace std; +int main() { + int n; + while (cin >> n) { + vector gym(n); + vector work(n); + for (int i = 0; i < n; i++) cin >> work[i]; + for (int i = 0; i < n; i++) cin >> gym[i]; + int result = 0; + + // 处理逻辑 + + cout << result << endl; + } + return 0; +} +``` + +可以看出ACM模式要比核心代码模式多写不少代码,相对来说ACM模式更锻炼代码能力,而核心代码模式是把侧重点完全放在算法逻辑上。 + +**国内企业现在很多都用牛客来进行面试,所以这种ACM模式大家还有必要熟悉一下**,以免面试的时候因为输入输出搞不懂而错失offer。 + +如果大家有精力的话,也可以去POJ上去刷刷题,POJ是ACM选手首选OJ,输入模式也是ACM模式。 + + +------------------------ + +* 微信:[程序员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/前序/代码风格.md b/problems/前序/代码风格.md new file mode 100644 index 00000000..2be1ac36 --- /dev/null +++ b/problems/前序/代码风格.md @@ -0,0 +1,145 @@ + + +

+ + + + +

+ +-------------------------- + +# 看了这么多代码,谈一谈代码风格! + +其实在交流群里经常能看到大家发出来的代码,可以看出很多录友对代码规范应该不甚了解,代码看起来并不舒服。 + +所以呢,我给大家讲一讲代码规范,我主要以C++代码为例。 + +需要强调一下,代码规范并不是仅仅是让代码看着舒服,这是一个很重要的习惯。 + +# 题外话 + +工作之后,**特别是在大厂,看谁的技术牛不牛逼,不用看谁写出多牛逼的代码,就代码风格扫一眼,立刻就能看出来是正规军还是野生程序员**。 + +很多人甚至不屑于了解代码规范,认为实现功能就行,这种观点其实在上个世纪是很普遍的,因为那时候一般写代码不需要合作,自己一个人撸整个项目,想怎么写就怎么写。 + +现在一些小公司,甚至大公司里的某些技术团队也不注重代码规范,赶进度撸出功能就完事,这种情况就要分两方面看: + +* 第一种情况:这个项目在业务上赚到钱了,每年年终好几十万,那项目前期还关心啥代码风格,赶进度把功能撸出来,赚钱就完事了,例如15年的王者荣耀。 + +* 第二种情况:这个项目没赚到钱,半死不活的,代码还没有设计也没有规范,这样对技术人员的伤害就非常大了。 + +**而不注重代码风格的团队,99.99%都是第二种情况**,如果你赶上了第一种情况,那就恭喜你了,本文下面的内容可以不用看了,哈哈。 + +# 代码规范 + +## 变量命名 + +这里我简单说一说规范问题。 + +**权威的C++规范以Google为主**,我给大家下载了一份中文版本,在公众号「代码随想录」后台回复:googlec++编程规范,就可以领取。 + +**具体的规范要以自己团队风格为主**,融入团队才是最重要的。 + +我先来说说变量的命名。 + +主流有如下三种变量规则: + +* 小驼峰、大驼峰命名法 +* 下划线命名法 +* 匈牙利命名法 + +小驼峰,第一个单词首字母小写,后面其他单词首字母大写。例如 `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://img-blog.csdnimg.cn/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.移动零的代码,大家可以看一下整体风格,注意空格的细节! +```C++ +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的规范来,代码风格其实统一就行,没有严格的说谁对谁错。 + +# 总结 + +如果还是学生,使用C++的话,可以按照题解中我的代码风格来,还是比较标准的。 + +如果不是C++就自己选一种代码风格坚持下来, + +如果已经工作的录友,就要融入团队的代码风格了,团队怎么写,自己就怎么来,毕竟不是一个人在战斗。 + +就酱,以后我还会陆续分享,关于代码,求职,学习工作之类的内容。 + + +------------------------ + +* 微信:[程序员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/前序/关于时间复杂度,你不知道的都在这里!.md b/problems/前序/关于时间复杂度,你不知道的都在这里!.md new file mode 100644 index 00000000..bd3bd284 --- /dev/null +++ b/problems/前序/关于时间复杂度,你不知道的都在这里!.md @@ -0,0 +1,174 @@ + +

+ + + + +

+ +-------------------------- + +Carl大胆断言:这可能是你见过对时间复杂度分析最通透的一篇文章了。 + +相信每一位录友都接触过时间复杂度,「代码随想录」已经也讲了一百多道经典题目了,是时候对时间复杂度来一个深度的剖析了,很早之前就写过一篇,当时文章还没有人看,Carl感觉有价值的东西值得让更多的人看到,哈哈。 + +所以重新整理的时间复杂度文章,正式和大家见面啦! + +## 究竟什么是时间复杂度 + +**时间复杂度是一个函数,它定性描述该算法的运行时间**。 + +我们在软件开发中,时间复杂度就是用来方便开发者估算出程序运行的答题时间。 + +那么该如何估计程序运行时间呢,通常会估算算法的操作单元数量来代表程序消耗的时间,这里默认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://img-blog.csdnimg.cn/20200728185745611.png) + +我们主要关心的还是一般情况下的数据形式。 + +**面试中说道算法的时间复杂度是多少指的都是一般情况**。但是如果面试官和我们深入探讨一个算法的实现以及性能的时候,就要时刻想着数据用例的不一样,时间复杂度也是不同的,这一点是一定要注意的。 + + +## 不同数据规模的差异 + +如下图中可以看出不同算法的时间复杂度在不同数据输入规模下的差异。 + +![时间复杂度,不同数据规模的差异](https://img-blog.csdnimg.cn/20200728191447384.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(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://img-blog.csdnimg.cn/20200728191447349.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 * logn) 。 + +之后还要遍历一遍这n个字符串找出两个相同的字符串,别忘了遍历的时候依然要比较字符串,所以总共的时间复杂度是 O(m * n * logn + n * m)。 + +我们对O(m * n * logn + n * m) 进行简化操作,把m * n提取出来变成 O(m * n * (logn + 1)),再省略常数项最后的时间复杂度是 O(m * n * logn)。 + +最后很明显O(m * n * logn) 要优于O(m * n * n)! + +所以先把字符串集合排序再遍历一遍找到两个相同字符串的方法要比直接暴力枚举的方式更快。 + +这就是我们通过分析两种算法的时间复杂度得来的。 + +**当然这不是这道题目的最优解,我仅仅是用这道题目来讲解一下时间复杂度**。 + +# 总结 + +本篇讲解了什么是时间复杂度,复杂度是用来干什么,以及数据规模对时间复杂度的影响。 + +还讲解了被大多数同学忽略的大O的定义以及log究竟是以谁为底的问题。 + +再分析了如何简化复杂的时间复杂度,最后举一个具体的例子,把本篇的内容串起来。 + +相信看完本篇,大家对时间复杂度的认识会深刻很多! + +如果感觉「代码随想录」很不错,赶快推荐给身边的朋友同学们吧,他们发现和「代码随想录」相见恨晚! + + + +------------------------ + +* 微信:[程序员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/前序/关于空间复杂度,可能有几个疑问?.md b/problems/前序/关于空间复杂度,可能有几个疑问?.md new file mode 100644 index 00000000..63940116 --- /dev/null +++ b/problems/前序/关于空间复杂度,可能有几个疑问?.md @@ -0,0 +1,77 @@ + + +

+ + + + +

+ +# 空间复杂度分析 + +* [关于时间复杂度,你不知道的都在这里!](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) + +那么一直还没有讲空间复杂度,所以打算陆续来补上,内容不难,大家可以读一遍文章就有整体的了解了。 + +什么是空间复杂度呢? + +是对一个算法在运行过程中占用内存空间大小的量度,记做S(n)=O(f(n)。 + +空间复杂度(Space Complexity)记作S(n) 依然使用大O来表示。利用程序的空间复杂度,可以对程序运行中需要多少内存有个预先估计。 + +关注空间复杂度有两个常见的相关问题 + +1. 空间复杂度是考虑程序(可执行文件)的大小么? + +很多同学都会混淆程序运行时内存大小和程序本身的大小。这里强调一下**空间复杂度是考虑程序运行时占用内存的大小,而不是可执行文件的大小。** + +2. 空间复杂度是准确算出程序运行时所占用的内存么? + +不要以为空间复杂度就已经精准的掌握了程序的内存使用大小,很有多因素会影响程序真正内存使用大小,例如编译器的内存对齐,编程语言容器的底层实现等等这些都会影响到程序内存的开销。 + +所以空间复杂度是预先大体评估程序内存使用的大小。 + +说到空间复杂度,我想同学们在OJ(online judge)上应该遇到过这种错误,就是超出内存限制,一般OJ对程序运行时的所消耗的内存都有一个限制。 + +为了避免内存超出限制,这也需要我们对算法占用多大的内存有一个大体的预估。 + +同样在工程实践中,计算机的内存空间也不是无限的,需要工程师对软件运行时所使用的内存有一个大体评估,这都需要用到算法空间复杂度的分析。 + +来看一下例子,什么时候的空间复杂度是O(1)呢,C++代码如下: + +```C++ +int j = 0; +for (int i = 0; i < n; i++) { + j++; +} + +``` +第一段代码可以看出,随着n的变化,所需开辟的内存空间并不会随着n的变化而变化。即此算法空间复杂度为一个常量,所以表示为大 O(1)。 + +什么时候的空间复杂度是O(n)? + +当消耗空间和输入参数n保持线性增长,这样的空间复杂度为O(n),来看一下这段C++代码 +```C++ +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的情况**。 + +至于如何求递归的空间复杂度,我会在专门写一篇文章来介绍的,敬请期待! + +------------------------ + +* 微信:[程序员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/前序/刷了这么多题,你了解自己代码的内存消耗么?.md b/problems/前序/刷了这么多题,你了解自己代码的内存消耗么?.md new file mode 100644 index 00000000..df555c3b --- /dev/null +++ b/problems/前序/刷了这么多题,你了解自己代码的内存消耗么?.md @@ -0,0 +1,154 @@ + + + +

+ + + + +

+ +理解代码的内存消耗,最关键是要知道自己所用编程语言的内存管理。 + +## 不同语言的内存管理 + +不同的编程语言各自的内存管理方式。 + +* C/C++这种内存堆空间的申请和释放完全靠自己管理 +* Java 依赖JVM来做内存管理,不了解jvm内存管理的机制,很可能会因一些错误的代码写法而导致内存泄漏或内存溢出 +* Python内存管理是由私有堆空间管理的,所有的python对象和数据结构都存储在私有堆空间中。程序员没有访问堆的权限,只有解释器才能操作。 + +例如Python万物皆对象,并且将内存操作封装的很好,**所以python的基本数据类型所用的内存会要远大于存放纯数据类型所占的内存**,例如,我们都知道存储int型数据需要四个字节,但是使用Python 申请一个对象来存放数据的话,所用空间要远大于四个字节。 + +## C++的内存管理 + +以C++为例来介绍一下编程语言的内存管理。 + +如果我们写C++的程序,就要知道栈和堆的概念,程序运行时所需的内存空间分为 固定部分,和可变部分,如下: + +![C++内存空间](https://img-blog.csdnimg.cn/20210309165950660.png) + +固定部分的内存消耗 是不会随着代码运行产生变化的, 可变部分则是会产生变化的 + +更具体一些,一个由C/C++编译的程序占用的内存分为以下几个部分: + +* 栈区(Stack) :由编译器自动分配释放,存放函数的参数值,局部变量的值等,其操作方式类似于数据结构中的栈。 +* 堆区(Heap) :一般由程序员分配释放,若程序员不释放,程序结束时可能由OS收回 +* 未初始化数据区(Uninitialized Data): 存放未初始化的全局变量和静态变量 +* 初始化数据区(Initialized Data):存放已经初始化的全局变量和静态变量 +* 程序代码区(Text):存放函数体的二进制代码 + +代码区和数据区所占空间都是固定的,而且占用的空间非常小,那么看运行时消耗的内存主要看可变部分。 + +在可变部分中,栈区间的数据在代码块执行结束之后,系统会自动回收,而堆区间数据是需要程序员自己回收,所以也就是造成内存泄漏的发源地。 + +**而Java、Python的话则不需要程序员去考虑内存泄漏的问题,虚拟机都做了这些事情**。 + +## 如何计算程序占用多大内存 + +想要算出自己程序会占用多少内存就一定要了解自己定义的数据类型的大小,如下: + +![C++数据类型的大小](https://img-blog.csdnimg.cn/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++代码输出的各个数据类型大小是多少? + +```C++ +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://img-blog.csdnimg.cn/20200804193307347.png) + +一字节的char占用了四个字节,空了三个字节的内存地址,int数据从地址4开始。 + +此时,直接将地址4,5,6,7处的四个字节数据读取到即可。 + +第二种是没有内存对齐的情况如图: + +![非内存对齐](https://img-blog.csdnimg.cn/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数据 + +此时一共需要两次寻址,一次合并的操作。 + +**大家可能会发现内存对齐岂不是浪费的内存资源么?** + +是这样的,但事实上,相对来说计算机内存资源一般都是充足的,我们更希望的是提高运行速度。 + +**编译器一般都会做内存对齐的优化操作,也就是说当考虑程序真正占用的内存大小的时候,也需要认识到内存对齐的影响**。 + + +## 总结 + +不少同学对这方面的知识很欠缺,基本处于盲区,通过这一篇大家可以初步补齐一下这块。 + +之后也可以有意识的去学习自己所用的编程语言是如何管理内存的,这些也是程序员的内功。 + +------------------------ + +* 微信:[程序员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/前序/力扣上的代码想在本地编译运行?.md b/problems/前序/力扣上的代码想在本地编译运行?.md new file mode 100644 index 00000000..04e19021 --- /dev/null +++ b/problems/前序/力扣上的代码想在本地编译运行?.md @@ -0,0 +1,71 @@ + +

+ + + + +

+ +很多录友都问过我一个问题,就是力扣上的代码如何在本地编译运行? + +其实在代码随想录刷题群里也经常出现这个场景,就是录友发一段代码上来,问大家这个代码怎么有问题? 如果我看到了一般我的回复:都是把那几个变量或者数组打印一下看看对不对,就知道了。 + +然后录友就问了:如何打日志呢? + +其实在力扣上打日志也挺方便的,我一般调试就是直接在力扣上打日志,偶尔需要把代码粘到本例来运行添加日志debug一下。 + +在力扣上直接打日志,这个就不用讲,C++的话想打啥直接cout啥就可以了。 + +我来说一说力扣代码如何在本题运行。 + +毕竟我们天天用力扣刷题,也应该知道力扣上的代码如何在本地编译运行。 + +其实挺简单的,大家看一遍就会了。 + +我拿我们刚讲过的这道题[动态规划:使用最小花费爬楼梯](https://mp.weixin.qq.com/s/djZB9gkyLFAKcQcSvKDorA)来做示范。 + +力扣746. 使用最小花费爬楼梯,完整的可以在直接本地运行的C++代码如下: + +```C++ +#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,哈哈。 + + + +------------------------ + +* 微信:[程序员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/前序/北京互联网公司总结.md b/problems/前序/北京互联网公司总结.md new file mode 100644 index 00000000..f1c9ad9a --- /dev/null +++ b/problems/前序/北京互联网公司总结.md @@ -0,0 +1,120 @@ + + +

+ + + + +

+ +# 北京互联网公司总结 + +**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** + +如果要在北京找工作,这份list可以作为一个大纲,寻找自己合适的公司。 + +## 一线互联网 + +* 百度(总部) +* 阿里(北京) +* 腾讯(北京) +* 字节跳动(总部) + +## 外企 + +* 微软(北京)微软中国主要就是北京和苏州 +* Hulu(北京)美国的视频网站,听说福利待遇超级棒 +* Airbnb(北京)房屋租赁平台 +* Grab(北京)东南亚第一大出行 App +* 印象笔记(北京)evernote在中国的独立品牌 +* FreeWheel(北京)美国最大的视频广告管理和投放平台 +* amazon(北京)全球最大的电商平台 + +## 二线互联网 + +* 美团点评(总部) +* 京东(总部) +* 网易(北京) +* 滴滴出行(总部) +* 新浪(总部) +* 快手(总部) +* 搜狐(总部) +* 搜狗(总部) +* 360(总部) + +## 硬件巨头 (有软件/互联网业务) + +* 华为(北京) +* 联想(总部) +* 小米(总部)后序要搬到武汉,互联网业务也是小米重头 + +## 三线互联网 + +* 爱奇艺(总部) +* 去哪儿网(总部) +* 知乎(总部) +* 豆瓣(总部) +* 当当网(总部) +* 完美世界(总部)游戏公司 +* 昆仑万维(总部)游戏公司 +* 58同城(总部) +* 陌陌(总部) +* 金山软件(北京)包括金山办公软件 +* 用友网络科技(总部)企业服务ERP提供商 +* 映客直播(总部) +* 猎豹移动(总部) +* 一点资讯(总部) +* 国双(总部)企业级大数据和人工智能解决方案提供商 + +## 明星创业公司 + +可以发现北京一堆在线教育的公司,可能教育要紧盯了政策变化,所以都要在北京吧 + +* 好未来(总部)在线教育 +* 猿辅导(总部)在线教育 +* 跟谁学(总部)在线教育 +* 作业帮(总部)在线教育 +* VIPKID(总部)在线教育 +* 雪球(总部)股市资讯 +* 唱吧(总部) +* 每日优鲜(总部)让每个人随时随地享受食物的美好 +* 微店(总部) +* 罗辑思维(总部)得到APP +* 值得买科技(总部)让每一次消费产生幸福感 +* 拉勾网(总部)互联网招聘 + +## AI独角兽公司 + +* 商汤科技(总部)专注于计算机视觉和深度学习 +* 旷视科技(总部)人工智能产品和解决方案公司 +* 第四范式(总部)人工智能技术与服务提供商 +* 地平线机器人(总部)边缘人工智能芯片的全球领导者 +* 寒武纪(总部)全球智能芯片领域的先行者 + +## 互联网媒体 + +* 央视网 +* 搜房网 +* 易车网 +* 链家网 +* 自如网 +* 汽车之家 + +## 总结 + +可能是我写总结写习惯了,什么文章都要有一个总结,哈哈,那么我就总结一下。 + +北京的互联网氛围绝对是最好的(暂不讨论户口和房价问题),大家如果看了[深圳原来有这么多互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q)这篇之后,**会发现北京互联网外企和二线互联网公司数量多的优势,在深圳的互联网公司断档比较严重,如果去不了为数不多的一线公司,可选择的余地就非常少了,而北京选择的余地就很多!** + +相对来说,深圳的硬件企业更多一些,因为珠三角制造业配套比较完善。而大多数互联网公司其实就是媒体公司,当然要靠近政治文化中心,这也是有原因的。 + +就酱,我也会陆续整理其他城市的互联网公司,希望对大家有所帮助。 + +------------------------ + +* 微信:[程序员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/前序/广州互联网公司总结.md b/problems/前序/广州互联网公司总结.md new file mode 100644 index 00000000..47b7bf77 --- /dev/null +++ b/problems/前序/广州互联网公司总结.md @@ -0,0 +1,83 @@ + +

+ + + + +

+ +# 广州互联网公司总结 + +**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** + +## 一线互联网 + +* 微信(总部) 有点难进! + +## 二线 +* 网易(总部)主要是游戏 + +## 三线 + +* 唯品会(总部) +* 欢聚时代(总部)旗下YY,虎牙,YY最近被浑水做空,不知百度还要不要收购了 +* 酷狗音乐(总部) +* UC浏览器(总部)现在隶属阿里创始人何小鹏现在搞小鹏汽车 +* 荔枝FM(总部)用户可以在手机上开设自己的电台和录制节目 +* 映客直播(总部)股票已经跌成渣了 +* 爱范儿(总部) +* 三七互娱(总部)游戏公司 +* 君海游戏(总部)游戏公司 +* 4399游戏(总部)游戏公司 +* 多益网络(总部)游戏公司 + +## 硬件巨头 (有软件/互联网业务) +* 小鹏汽车(总部)新能源汽车小霸王 + +## 创业公司 + +* 妈妈网(总部)母婴行业互联网公司 +* 云徙科技(总部)数字商业云服务提供商 +* Fordeal(总部)中东领先跨境电商平台 +* Mobvista(总部)移动数字营销 +* 久邦GOMO(总部)游戏 +* 深海游戏(总部)游戏 + +## 国企 + +* 中国电信广州研发(听说没有996) + + +## 总结 + +同在广东省,难免不了要和深圳对比,大家如果看了这篇:[深圳原来有这么多互联网公司,你都知道么?](https://mp.weixin.qq.com/s/3VJHF2zNohBwDBxARFIn-Q)就能感受到鲜明的对比了。 + +广州大厂高端岗位其实比较少,本土只有微信和网易,微信呢毕竟还是腾讯的分部,而网易被很多人认为是杭州企业,其实网易总部在广州。 + +广州是唯一一个一线城市没有自己本土互联网巨头的城市,所以网易选择在广州扎根还是很正确的,毕竟杭州是阿里的天下,广州也应该扶持一把本土的互联网公司。 + +虽然对于互联网从业人员来说,广州的岗位要比深圳少很多,**但是!!广州的房价整体要比深圳低30%左右,而且广州的教育,医疗,公共资源完全碾压深圳**。 + +教育方面:大学广州有两个985,四个211,深圳这方面就不用说了,大家懂得。 + +基础教育方面深圳的小学初中高中学校数量远远不够用,小孩上学竞争很激烈,我也是经常听同事们说,耳濡目染了。 + +而医疗上基本深圳看不了的病都要往广州跑,深圳的医院数量也不够用。 + +在生活节奏上,广州更慢一些,更有生活的气息,而深圳生存下去的气息更浓烈一些。 + +所以很多在深圳打拼多年的IT从业者选择去广州安家也是有原因的。 + +但也有很多从广州跑到深圳的,深圳发展的机会更多,而广州教育医疗更丰富,房价不高(相对深圳)。 + + + + +------------------------ + +* 微信:[程序员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/前序/成都互联网公司总结.md b/problems/前序/成都互联网公司总结.md new file mode 100644 index 00000000..2f32849e --- /dev/null +++ b/problems/前序/成都互联网公司总结.md @@ -0,0 +1,81 @@ + + +

+ + + + +

+ + +# 成都互联网公司总结 + +**排名不分先后,个人总结难免有所疏漏,欢迎补充!** + +## 一线互联网 +* 腾讯(成都) 游戏,王者荣耀就在成都! +* 阿里(成都) +* 蚂蚁金服(成都) +* 字节跳动(成都) + +## 硬件巨头 (有软件/互联网业务) + +* 华为(成都) +* OPPO(成都) + +## 二线互联网 + +* 京东(成都) +* 美团(成都) +* 滴滴(成都) + +## 三线互联网 + +* 完美世界 (成都)游戏 +* 聚美优品 (成都) +* 陌陌 (成都) +* 爱奇艺(成都) + +## 外企互联网 + +* NAVER China (成都)搜索引擎公司,主要针对韩国市场 + +## 创业公司 + +* tap4fun(总部)游戏 +* 趣乐多(总部)游戏 +* 天上友嘉(总部)游戏 +* 三七互娱(成都)游戏 +* 咕咚(总部)智能运动 +* 百词斩(总部)在线教育 +* 晓多科技(总部)AI方向 +* 萌想科技(总部)实习僧 +* Camera360(总部)移动影像社区 +* 医联 (总部)医疗解决方案提供商 +* 小明太极 (总部)原创漫画文娱内容网站以及相关APP +* 小鸡叫叫(总部)致力于儿童教育的智慧解决方案 + + +## AI独角兽公司 + +* 科大讯飞(成都) +* 商汤(成都) + +## 总结 + +可以看出成都相对一线城市的互联网氛围确实差了很多。**但是!成都已经是在内陆城市中甚至二线城市中的佼佼者了!** + +从公司的情况上也可以看出:**成都互联网行业目前的名片是“游戏”**,腾讯、完美世界等大厂,还有无数小厂都在成都搞游戏,可能成都的天然属性就是娱乐,这里是游戏的沃土吧。 + +相信大家如果在一些招聘平台上去搜,其实很多公司都在成都,但都是把客服之类的工作安排在成都,而我在列举的时候尽量把研发相关在成都的公司列出来,这样对大家更有帮助。 + + + +------------------------ + +* 微信:[程序员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/前序/杭州互联网公司总结.md b/problems/前序/杭州互联网公司总结.md new file mode 100644 index 00000000..23cd4183 --- /dev/null +++ b/problems/前序/杭州互联网公司总结.md @@ -0,0 +1,92 @@ + + +

+ + + + +

+ +# 杭州互联网公司总结 + +**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** + +## 一线互联网 + +* 阿里巴巴(总部) +* 蚂蚁金服(总部)阿里旗下 +* 阿里云(总部)阿里旗下 +* 网易(杭州) 网易云音乐 +* 字节跳动(杭州)抖音分部 + +## 外企 + +* ZOOM (杭州研发中心)全球知名云视频会议服务提供商 +* infosys(杭州)印度公司,据说工资相对不高 +* 思科(杭州) + +## 二线互联网 + +* 滴滴(杭州) +* 快手(杭州) + +## 硬件巨头 (有软件/互联网业务) + +* 海康威视(总部)安防三巨头 +* 浙江大华(总部)安防三巨头 +* 杭州宇视(总部) 安防三巨头 +* 萤石 +* 华为(杭州) +* vivo(杭州) +* oppo(杭州) +* 魅族(杭州) + +## 三线互联网 + +* 蘑菇街(总部)女性消费者的电子商务网站 +* 有赞(总部)帮助商家进行网上开店、社交营销 +* 菜鸟网络(杭州) +* 花瓣网(总部)图片素材领导者 +* 兑吧(总部)用户运营服务平台 +* 同花顺(总部)网上股票证券交易分析软件 +* 51信用卡(总部)信用卡管理 +* 虾米(总部)已被阿里收购 +* 曹操出行(总部) +* 口碑网 (总部) + +## AI独角兽公司 + +* 旷视科技(杭州) +* 商汤(杭州) + +## 创业公司 + +* e签宝(总部)做电子签名 +* 婚礼纪(总部)好多结婚的朋友都用 +* 大搜车(总部)中国领先的汽车交易服务供应商 +* 二更(总部)自媒体 +* 丁香园(总部) + + +## 总结 + +杭州距离上海非常近,难免不了和上海做对比,上海是金融之都,如果看了[上海有这些互联网公司,你都知道么?](https://mp.weixin.qq.com/s/iW4_rXQzc0fJDuSmPTUVdQ)就会发现上海互联网也是仅次于北京的。 + +而杭州是阿里的大本营,到处都有阿里的影子,虽然有网易在,但是也基本是盖过去了,很多中小公司也都是阿里某某高管出来创业的。 + +杭州的阿里带动了杭州的电子商务领域热度非常高,如果你想做电商想做直播带货想做互联网营销,杭州都是圣地! + +如果要是写代码的话,每年各种节日促销,加班996应该是常态,电商公司基本都是这样,当然如果赶上一个好领导的话,回报也是很丰厚的。 + +「代码随想录」一直都是干活满满,值得介绍给每一位学习算法的同学! + + + +------------------------ + +* 微信:[程序员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/前序/深圳互联网公司总结.md b/problems/前序/深圳互联网公司总结.md new file mode 100644 index 00000000..b7d15686 --- /dev/null +++ b/problems/前序/深圳互联网公司总结.md @@ -0,0 +1,86 @@ + +

+ + + + +

+ +# 深圳互联网公司总结 + +**个人总结难免有所疏忽,欢迎大家补充,公司好坏没有排名哈!** + +## 一线互联网 + +* 腾讯(总部深圳) +* 百度(深圳) +* 阿里(深圳) +* 字节跳动(深圳) + +## 硬件巨头 (有软件/互联网业务) + +* 华为(总部深圳) +* 中兴(总部深圳) +* 海能达(总部深圳) +* oppo(总部深圳) +* vivo(总部深圳) +* 深信服(总部深圳) +* 大疆(总部深圳,无人机巨头) +* 一加手机(总部深圳) +* 柔宇科技(国内领先的柔性屏幕制造商,最近正在准备上市) + +## 二线大厂 + +* 快手(深圳) +* 京东(深圳) +* 顺丰(总部深圳) + +## 三线大厂 + +* 富途证券(2020年成功赴美上市,主要经营港股美股) +* 微众银行(总部深圳) +* 招银科技(总部深圳) +* 平安系列(平安科技、平安寿险、平安产险、平安金融、平安好医生等) +* Shopee(东南亚最大的电商平台,最近发展势头非常强劲) +* 有赞(深圳) +* 迅雷(总部深圳) +* 金蝶(总部深圳) +* 随手记(总部深圳) + +## AI独角兽公司 + +* 商汤科技(人工智能领域的独角兽) +* 追一科技(一家企业级智能服务AI公司) +* 超多维科技 (计算机视觉、裸眼3D) +* 优必选科技 (智能机器人、人脸识别) + +## 明星创业公司 + +* 丰巢科技(让生活更简单) +* 人人都是产品经理(全球领先的产品经理和运营人 学习、交流、分享平台) +* 大丰收(综合农业互联网服务平台) +* 小鹅通(专注新教育的技术服务商) +* 货拉拉(拉货就找货拉拉) +* 编程猫(少儿编程教育头部企业) +* HelloTalk(全球最大的语言学习社交社区) +* 大宇无限( 拥有SnapTube, Lark Player 等多款广受海外新兴市场用户欢迎的产品) +* 知识星球(深圳大成天下公司出品) +* XMind(隶属深圳市爱思软件技术有限公司,思维导图软件) +* 小赢科技(以技术重塑人类的金融体验) + +## 其他行业(有软件/互联网业务) + +* 三大电信运营商:中国移动、中国电信、中国联通 +* 房产企业:恒大、万科 +* 中信深圳 +* 广发证券,深交所 +* 珍爱网(珍爱网是国内知名的婚恋服务网站之一) + +------------------------ + +* 微信:[程序员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/前序/程序员写文档工具.md b/problems/前序/程序员写文档工具.md new file mode 100644 index 00000000..34f2f777 --- /dev/null +++ b/problems/前序/程序员写文档工具.md @@ -0,0 +1,141 @@ + + + +

+ + + + +

+ +# 程序员应该用什么用具来写文档? + +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记录起来吧 + + +------------------------ + +* 微信:[程序员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/前序/程序员简历.md b/problems/前序/程序员简历.md new file mode 100644 index 00000000..af52df8e --- /dev/null +++ b/problems/前序/程序员简历.md @@ -0,0 +1,139 @@ + +

+ + + + +

+ +-------------------------- + +# 程序员的简历应该这么写!!(附简历模板) + +> Carl多年积累的简历技巧都在这里了 + +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://img-blog.csdnimg.cn/20200803175538158.png) + +这里是简历模板中Markdown的代码:https://github.com/youngyangyang04/Markdown-Resume-Template ,可以fork到自己Github仓库上,按照这个模板来修改自己的简历。 + +**Word版本的简历,大家可以在公众号「代码随想录」后台回复:简历模板,就可以获取!** + +## 总结 + +**好的简历是敲门砖,同时也不要在简历上花费过多的精力,好的简历以及面试技巧都是锦上添花**,真的求得心得的offer靠的还是真才实学。 + +如何真才实学呢? 跟着「代码随想录」一起刷题呀,哈哈 + +大家此时可以再重审一遍自己的简历,如果发现哪里的不足,面试前要多准备多练习。 + +就酱,「代码随想录」就是这么干货,Carl多年积累的简历技巧都毫不保留的写出来了,如果感觉对你有帮助,就宣传一波「代码随想录」吧,值得大家的关注! + +------------------------ + +* 微信:[程序员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/前序/递归算法的时间与空间复杂度分析.md b/problems/前序/递归算法的时间与空间复杂度分析.md new file mode 100644 index 00000000..c8ef4723 --- /dev/null +++ b/problems/前序/递归算法的时间与空间复杂度分析.md @@ -0,0 +1,273 @@ + +

+ + + + +

+ +# 递归算法的时间与空间复杂度分析! + +之前在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ)中详细讲解了递归算法的时间复杂度,但没有讲空间复杂度。 + +本篇讲通过求斐波那契数列和二分法再来深入分析一波递归算法的时间和空间复杂度,细心看完,会刷新对递归的认知! + + +## 递归求斐波那契数列的性能分析 + +先来看一下求斐波那契数的递归写法。 + +```C++ +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://img-blog.csdnimg.cn/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的时候,这段递归求斐波那契代码的耗时。 + +```C++ +#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)这种指数级别的复杂度是非常大的。 + +所以这种求斐波那契数的算法看似简洁,其实时间复杂度非常高,一般不推荐这样来实现斐波那契。 + +其实罪魁祸首就是这里的两次递归,导致了时间复杂度以指数上升。 + +```C++ +return fibonacci(i-1) + fibonacci(i-2); +``` + +可不可以优化一下这个递归算法呢。 主要是减少递归的调用次数。 + +来看一下如下代码: + +```C++ +// 版本二 +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) + +此时再来测一下耗时情况验证一下: + +```C++ +#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(0, 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://img-blog.csdnimg.cn/20210305094749554.png) + +递归第n个斐波那契数的话,递归调用栈的深度就是n。 + +那么每次递归的空间复杂度是O(1), 调用栈深度为n,所以这段递归代码的空间复杂度就是O(n)。 + +```C++ +int fibonacci(int i) { + if(i <= 0) return 0; + if(i == 1) return 1; + return fibonacci(i-1) + fibonacci(i-2); +} +``` + + +最后对各种求斐波那契数列方法的性能做一下分析,如题: + +![递归的空间复杂度分析](https://img-blog.csdnimg.cn/20210305095227356.png) + +可以看出,求斐波那契数的时候,使用递归算法并不一定是在性能上是最优的,但递归确实简化的代码层面的复杂度。 + +### 二分法(递归实现)的性能分析 + +带大家再分析一段二分查找的递归实现。 + +```C++ +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)是非常耗时的。 + +通过本篇大家应该对递归算法的时间复杂度和空间复杂度有更加深刻的理解了。 + + + +------------------------ + +* 微信:[程序员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/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md b/problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md new file mode 100644 index 00000000..cb6aa604 --- /dev/null +++ b/problems/前序/通过一道面试题目,讲一讲递归算法的时间复杂度!.md @@ -0,0 +1,158 @@ + + +

+ + + + +

+ + +# 通过一道面试题目,讲一讲递归算法的时间复杂度! + +> 本篇通过一道面试题,一个面试场景,来好好分析一下如何求递归算法的时间复杂度。 + +相信很多同学对递归算法的时间复杂度都很模糊,那么这篇来给大家通透的讲一讲。 + +**同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码**。 + +这是为什么呢? + +如果对递归的时间复杂度理解的不够深入的话,就会这样! + +那么我通过一道简单的面试题,模拟面试的场景,来带大家逐步分析递归算法的时间复杂度,最后找出最优解,来看看同样是递归,怎么就写成了O(n)的代码。 + +面试题:求x的n次方 + +想一下这么简单的一道题目,代码应该如何写呢。最直观的方式应该就是,一个for循环求出结果,代码如下: + +```C++ +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),此时面试官会说,有没有效率更好的算法呢。 + +**如果此时没有思路,不要说:我不会,我不知道了等等**。 + +可以和面试官探讨一下,询问:“可不可以给点提示”。面试官提示:“考虑一下递归算法”。 + +那么就可以写出了如下这样的一个递归的算法,使用递归解决了这个问题。 + +``` +int function2(int x, int n) { + if (n == 0) { + return 1; // return 1 同样是因为0次方是等于1的 + } + return function2(x, n - 1) * x; +} +``` +面试官问:“那么这个代码的时间复杂度是多少?”。 + +一些同学可能一看到递归就想到了O(logn),其实并不是这样,递归算法的时间复杂度本质上是要看: **递归的次数 * 每次递归中的操作次数**。 + +那再来看代码,这里递归了几次呢? + +每次n-1,递归了n次时间复杂度是O(n),每次进行了一个乘法操作,乘法操作的时间复杂度一个常数项O(1),所以这份代码的时间复杂度是 n * 1 = O(n)。 + +这个时间复杂度就没有达到面试官的预期。于是又写出了如下的递归算法的代码: + +``` +int function3(int x, int n) { + if (n == 0) { + return 1; + } + 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://img-blog.csdnimg.cn/20201209193909426.png) + +当前这颗二叉树就是求x的n次方,n为16的情况,n为16的时候,进行了多少次乘法运算呢? + +这棵树上每一个节点就代表着一次递归并进行了一次相乘操作,所以进行了多少次递归的话,就是看这棵树上有多少个节点。 + +熟悉二叉树话应该知道如何求满二叉树节点数量,这颗满二叉树的节点数量就是`2^3 + 2^2 + 2^1 + 2^0 = 15`,可以发现:**这其实是等比数列的求和公式,这个结论在二叉树相关的面试题里也经常出现**。 + +这么如果是求x的n次方,这个递归树有多少个节点呢,如下图所示:(m为深度,从0开始) + +![递归求时间复杂度](https://img-blog.csdnimg.cn/20200728195531892.png) + +**时间复杂度忽略掉常数项`-1`之后,这个递归算法的时间复杂度依然是O(n)**。对,你没看错,依然是O(n)的时间复杂度! + +此时面试官就会说:“这个递归的算法依然还是O(n)啊”, 很明显没有达到面试官的预期。 + +那么O(logn)的递归算法应该怎么写呢? + +想一想刚刚给出的那份递归算法的代码,是不是有哪里比较冗余呢,其实有重复计算的部分。 + +于是又写出如下递归算法的代码: + +``` +int function4(int x, int n) { + if (n == 0) { + return 1; + } + 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(logn)的时间复杂度,其实这是O(n)的算法! + +``` +int function3(int x, int n) { + if (n == 0) { + return 1; + } + if (n % 2 == 1) { + return function3(x, n / 2) * function3(x, n / 2)*x; + } + return function3(x, n / 2) * function3(x, n / 2); +} +``` +可以看出这道题目非常简单,但是又很考究算法的功底,特别是对递归的理解,这也是我面试别人的时候用过的一道题,所以整个情景我才写的如此逼真,哈哈。 + +大厂面试的时候最喜欢用“简单题”来考察候选人的算法功底,注意这里的“简单题”可并不一定真的简单​哦! + +如果认真读完本篇,相信大家对递归算法的有一个新的认识的,同一道题目,同样是递归,效率可是不一样的! + + +------------------------ + +* 微信:[程序员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/剑指Offer05.替换空格.md b/problems/剑指Offer05.替换空格.md index 129044d5..7881adf3 100644 --- a/problems/剑指Offer05.替换空格.md +++ b/problems/剑指Offer05.替换空格.md @@ -1,17 +1,23 @@ -## 题目地址 -https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/ +

+ + + + +

+

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

-> 遇到对字符串或者数组做填充或删除的操作时,都要想想从后向前操作怎么样。 # 题目:剑指Offer 05.替换空格 +https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/ + 请实现一个函数,把字符串 s 中的每个空格替换成"%20"。 -示例 1: -输入:s = "We are happy." -输出:"We%20are%20happy." +示例 1: +输入:s = "We are happy." +输出:"We%20are%20happy." -# 思路 +# 思路 如果想把这道题目做到极致,就不要只用额外的辅助空间了! @@ -21,26 +27,26 @@ https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/ i指向新长度的末尾,j指向旧长度的末尾。 - +![替换空格](https://tva1.sinaimg.cn/large/e6c9d24ely1go6qmevhgpg20du09m4qp.gif) -有同学问了,为什么要从后向前填充,从前向后填充不行么? +有同学问了,为什么要从后向前填充,从前向后填充不行么? 从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。 -**其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** +**其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** 这么做有两个好处: -1. 不用申请新数组。 +1. 不用申请新数组。 2. 从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。 时间复杂度,空间复杂度均超过100%的用户。 - + -## C++代码 +## C++代码 -``` +```C++ class Solution { public: string replaceSpace(string s) { @@ -70,7 +76,7 @@ public: }; ``` -时间复杂度:O(n) +时间复杂度:O(n) 空间复杂度:O(1) 此时算上本题,我们已经做了七道双指针相关的题目了分别是: @@ -82,7 +88,7 @@ public: * [142.环形链表II](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) * [344.反转字符串](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) -# 拓展 +# 拓展 这里也给大家拓展一下字符串和数组有什么差别, @@ -108,12 +114,33 @@ for (int i = 0; i < a.size(); i++) { } ``` -那么vector< char > 和 string 又有什么区别呢? +那么vector< char > 和 string 又有什么区别呢? 其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。 所以想处理字符串,我们还是会定义一个string类型。 -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/剑指Offer52.两个链表的第一个公共节点.md b/problems/剑指Offer52.两个链表的第一个公共节点.md deleted file mode 100644 index eadf3af4..00000000 --- a/problems/剑指Offer52.两个链表的第一个公共节点.md +++ /dev/null @@ -1,2 +0,0 @@ - -面试题02.07.链表相交 重复题目 diff --git a/problems/剑指Offer58-I.翻转单词顺序.md b/problems/剑指Offer58-I.翻转单词顺序.md deleted file mode 100644 index 5267fa11..00000000 --- a/problems/剑指Offer58-I.翻转单词顺序.md +++ /dev/null @@ -1,2 +0,0 @@ - -详见:[0151.翻转字符串里的单词](https://github.com/youngyangyang04/leetcode/blob/master/problems/0151.翻转字符串里的单词.md) diff --git a/problems/剑指Offer58-II.左旋转字符串.md b/problems/剑指Offer58-II.左旋转字符串.md index 3429ee99..3e9ab11f 100644 --- a/problems/剑指Offer58-II.左旋转字符串.md +++ b/problems/剑指Offer58-II.左旋转字符串.md @@ -1,25 +1,33 @@ -# 题目地址 -https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/ +

+ + + + +

+

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

+ > 反转个字符串还有这么多用处? # 题目:剑指Offer58-II.左旋转字符串 +https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/ + 字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。 -示例 1: -输入: s = "abcdefg", k = 2 -输出: "cdefgab" +示例 1: +输入: s = "abcdefg", k = 2 +输出: "cdefgab" -示例 2: -输入: s = "lrloseumgh", k = 6 -输出: "umghlrlose" +示例 2: +输入: s = "lrloseumgh", k = 6 +输出: "umghlrlose"   -限制: -1 <= k < s.length <= 10000 +限制: +1 <= k < s.length <= 10000 -# 思路 +# 思路 为了让本题更有意义,提升一下本题难度:**不能申请额外空间,只能在本串上操作**。 @@ -30,19 +38,19 @@ https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/ 这道题目也非常类似,依然可以通过局部反转+整体反转 达到左旋转的目的。 -具体步骤为: +具体步骤为: -1. 反转区间为前n的子串 -2. 反转区间为n到末尾的子串 -3. 反转整个字符串 +1. 反转区间为前n的子串 +2. 反转区间为n到末尾的子串 +3. 反转整个字符串 最后就可以得到左旋n的目的,而不用定义新的字符串,完全在本串上操作。 -例如 :示例1中 输入:字符串abcdefg,n=2 +例如 :示例1中 输入:字符串abcdefg,n=2 如图: - + 最终得到左旋2个单元的字符串:cdefgab @@ -50,7 +58,7 @@ https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/ # C++代码 -``` +```C++ class Solution { public: string reverseLeftWords(string s, int n) { @@ -84,4 +92,24 @@ public: **如果想让这套题目有意义,就不要申请额外空间。** -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/剑指Offer59-I.滑动窗口的最大值.md b/problems/剑指Offer59-I.滑动窗口的最大值.md deleted file mode 100644 index ec8fee37..00000000 --- a/problems/剑指Offer59-I.滑动窗口的最大值.md +++ /dev/null @@ -1,45 +0,0 @@ - -原理:[0239.滑动窗口最大值](https://github.com/youngyangyang04/leetcode/blob/master/problems/0239.滑动窗口最大值.md) |滑动窗口/队列 |困难| **单调队列**| - -``` -class Solution { -public: - class MyQueue { //单调队列(从大到小) - public: - deque que; // 使用deque来实现单调队列 - void pop(int value) { - if (!que.empty() && value == que.front()) { - que.pop_front(); - } - } - void push(int value) { - while (!que.empty() && value > que.back()) { - que.pop_back(); - } - que.push_back(value); - - } - int front() { - return que.front(); - } - }; - vector maxSlidingWindow(vector& nums, int k) { - MyQueue que; - vector result; - if (nums.empty()) { - return 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; - } -}; - -``` diff --git a/problems/动态规划-股票问题总结篇.md b/problems/动态规划-股票问题总结篇.md new file mode 100644 index 00000000..370eea58 --- /dev/null +++ b/problems/动态规划-股票问题总结篇.md @@ -0,0 +1,489 @@ +

+ + + + +

+

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

+ +之前我们已经把力扣上股票系列的题目都讲过的,但没有来一篇股票总结,来帮大家高屋建瓴,所以总结篇这就来了! + +![股票问题总结](https://code-thinking.cdn.bcebos.com/pics/%E8%82%A1%E7%A5%A8%E9%97%AE%E9%A2%98%E6%80%BB%E7%BB%93.jpg) + +* [动态规划:121.买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ) +* [动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w) +* [动态规划:123.买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg) +* [动态规划:188.买卖股票的最佳时机IV](https://mp.weixin.qq.com/s/jtxZJWAo2y5sUsW647Z5cw) +* [动态规划:309.最佳买卖股票时机含冷冻期](https://mp.weixin.qq.com/s/TczJGFAPnkjH9ET8kwH1OA) +* [动态规划:714.买卖股票的最佳时机含手续费](https://mp.weixin.qq.com/s/2Cd_uINjerZ25VHH0K2IBQ) + +## 卖股票的最佳时机 + +[动态规划:121.买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ),**股票只能买卖一次,问最大利润**。 + +【贪心解法】 + +取最左最小值,取最右最大值,那么得到的差值就是最大利润,代码如下: +```C++ +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]); + +代码如下: + +```C++ +// 版本一 +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) + +使用滚动数组,代码如下: + +```C++ +// 版本二 +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://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w)可以多次买卖股票,问最大收益。 + + +【贪心解法】 + +收集每天的正利润便可,代码如下: + +```C++ +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://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)唯一不同的地方,就是推导dp[i][0]的时候,第i天买入股票的情况**。 + +在[121. 买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)中,因为股票全程只能买卖一次,所以如果买入股票,那么第i天持有股票即dp[i][0]一定就是 -prices[i]。 + +而本题,因为一只股票可以买卖多次,所以当第i天买入股票的时候,所持有的现金可能有之前买卖过的利润。 + +代码如下:(注意代码中的注释,标记了和121.买卖股票的最佳时机唯一不同的地方) + +```C++ +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://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)最多买卖两次,问最大收益。 + +【动态规划】 + +一天一共就有五个状态, +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]); + +代码如下: + +```C++ +// 版本一 +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++版本: + +```C++ +// 版本二 +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://mp.weixin.qq.com/s/jtxZJWAo2y5sUsW647Z5cw) 最多买卖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][0]); + +同理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][i] + prices[i], dp[i][2]) + +同理可以类比剩下的状态,代码如下: + +```C++ +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]); +} +``` + +整体代码如下: + +```C++ +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://mp.weixin.qq.com/s/TczJGFAPnkjH9ET8kwH1OA) 可以多次买卖但每次卖出有冷冻期1天。 + +相对于[动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w),本题加上了一个冷冻期。 + + +在[动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w) 中有两个状态,持有股票后的最多现金,和不持有股票的最多现金。本题则可以花费为四个状态 + +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]; + +综上分析,递推代码如下: + +```C++ +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]; +``` + +整体代码如下: + +```C++ +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://mp.weixin.qq.com/s/2Cd_uINjerZ25VHH0K2IBQ) 可以多次买卖,但每次有手续费。 + + +相对于[动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w),本题只需要在计算卖出操作的时候减去手续费就可以了,代码几乎是一样的。 + +唯一差别在于递推公式部分,所以本篇也就不按照动规五部曲详细讲解了,主要讲解一下递推公式部分。 + +这里重申一下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://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w)的区别就是这里需要多一个减去手续费的操作**。 + +以上分析完毕,代码如下: + +```C++ +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次,从冷冻期再到手续费,最后再来一个股票大总结,可以说对股票系列完美收官了。 + +「代码随想录」值得推荐给身边每一位学习算法的朋友同学们,关注后都会发现相见恨晚! + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/动态规划理论基础.md b/problems/动态规划理论基础.md new file mode 100644 index 00000000..ae02e69a --- /dev/null +++ b/problems/动态规划理论基础.md @@ -0,0 +1,143 @@ +

+ + + + +

+

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

+ +## 什么是动态规划 + +动态规划,英文:Dynamic Programming,简称DP,如果某一问题有很多重叠子问题,使用动态规划是最有效的。 + +所以动态规划中每一个状态一定是由上一个状态推导出来的,**这一点就区分于贪心**,贪心没有状态推导,而是从局部直接选最优的, + +在[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中我举了一个背包问题的例子。 + +例如:有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背包的问题,那么就需要在把对应的理论知识讲解一下。 + +大家会发现,我讲解的理论基础并不是教科书上各种动态规划的定义,错综复杂的公式。 + +这里理论基础篇已经是非常偏实用的了,每个知识点都是在解题实战中非常有用的内容,大家要重视起来哈。 + +今天我们开始新的征程了,你准备好了么? + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/双指针总结.md b/problems/双指针总结.md index 166bfe56..5ee1b4f6 100644 --- a/problems/双指针总结.md +++ b/problems/双指针总结.md @@ -1,3 +1,11 @@ +

+ + + + +

+

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

+ > 又是一波总结 相信大家已经对双指针法很熟悉了,但是双指针法并不隶属于某一种数据结构,我们在讲解数组,链表,字符串都用到了双指针法,所有有必要针对双指针法做一个总结。 @@ -86,3 +94,22 @@ for (int i = 0; i < array.size(); i++) { 本文中一共介绍了leetcode上九道使用双指针解决问题的经典题目,除了链表一些题目一定要使用双指针,其他题目都是使用双指针来提高效率,一般是将O(n^2)的时间复杂度,降为O(n)。 建议大家可以把文中涉及到的题目在好好做一做,琢磨琢磨,基本对双指针法就不在话下了。 +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/周总结/20200927二叉树周末总结.md b/problems/周总结/20200927二叉树周末总结.md new file mode 100644 index 00000000..f983a929 --- /dev/null +++ b/problems/周总结/20200927二叉树周末总结.md @@ -0,0 +1,205 @@ + +# 本周小结!(二叉树) + +**周日我做一个针对本周的打卡留言疑问以及在刷题群里的讨论内容做一下梳理吧。**,这样也有助于大家补一补本周的内容,消化消化。 + +**注意这个周末总结和系列总结还是不一样的(二叉树还远没有结束),这个总结是针对留言疑问以及刷题群里讨论内容的归纳。** + +## 周一 + +本周我们开始讲解了二叉树,在[关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A)中讲解了二叉树的理论基础。 + +有同学会把红黑树和二叉平衡搜索树弄分开了,其实红黑树就是一种二叉平衡搜索树,这两个树不是独立的,所以C++中map、multimap、set、multiset的底层实现机制是二叉平衡搜索树,再具体一点是红黑树。 + +对于二叉树节点的定义,C++代码如下: + +``` +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); +``` + +没有构造函数的话就要这么写: + +``` +TreeNode* a = new TreeNode(); +a->val = 9; +a->left = NULL; +a->right = NULL; +``` + +在介绍前中后序遍历的时候,有递归和迭代(非递归),还有一种牛逼的遍历方式:morris遍历。 + +morris遍历是二叉树遍历算法的超强进阶算法,morris遍历可以将非递归遍历中的空间复杂度降为O(1),感兴趣大家就去查一查学习学习,比较小众,面试几乎不会考。我其实也没有研究过,就不做过多介绍了。 + +## 周二 + +在[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)中讲到了递归三要素,以及前中后序的递归写法。 + +文章中我给出了leetcode上三道二叉树的前中后序题目,但是看完[二叉树:一入递归深似海,从此offer是路人](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA),依然可以解决n叉树的前后序遍历,在leetcode上分别是 +* 589. N叉树的前序遍历 +* 590. N叉树的后序遍历 + +大家可以再去把这两道题目做了。 + +## 周三 + +在[二叉树:听说递归能做的,栈也能做!](https://mp.weixin.qq.com/s/c_zCrGHIVlBjUH_hJtghCg)中我们开始用栈来实现递归的写法,也就是所谓的迭代法。 + +细心的同学发现文中前后序遍历空节点是入栈的,其实空节点入不入栈都差不多,但感觉空节点不入栈确实清晰一些,符合文中动画的演示。 + +前序遍历空节点不入栈的代码:(注意注释部分,和文章中的区别) + +``` +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; + } +}; + +``` + +后序遍历空节点不入栈的代码:(注意注释部分,和文章中的区别) + +``` +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://mp.weixin.qq.com/s/WKg0Ty1_3SZkztpHubZPRg)中我们使用空节点作为标记,给出了统一的前中后序迭代法。 + +此时又多了一种前中后序的迭代写法,那么有同学问了:前中后序迭代法是不是一定要统一来写,这样才算是规范。 + +其实没必要,还是自己感觉哪一种更好记就用哪种。 + +但是**一定要掌握前中后序一种迭代的写法,并不因为某种场景的题目一定要用迭代,而是现场面试的时候,面试官看你顺畅的写出了递归,一般会进一步考察能不能写出相应的迭代。** + +## 周五 + +在[二叉树:层序遍历登场!](https://mp.weixin.qq.com/s/Gb3BjakIKGNpup2jYtTzog)中我们介绍了二叉树的另一种遍历方式(图论中广度优先搜索在二叉树上的应用)即:层序遍历。 + +看完这篇文章,去leetcode上怒刷五题,文章中 编号107题目的样例图放错了(原谅我匆忙之间总是手抖),但不影响大家理解。 + +只有同学发现leetcode上“515. 在每个树行中找最大值”,也是层序遍历的应用,依然可以分分钟解决,所以就是一鼓作气解决六道了,哈哈。 + +**层序遍历遍历相对容易一些,只要掌握基本写法(也就是框架模板),剩下的就是在二叉树每一行遍历的时候做做逻辑修改。** + +## 周六 + +在[二叉树:你真的会翻转二叉树么?](https://mp.weixin.qq.com/s/6gY1MiXrnm-khAAJiIb5Bg)中我们把翻转二叉树这么一道简单又经典的问题,充分的剖析了一波,相信就算做过这道题目的同学,看完本篇之后依然有所收获! + + +**文中我指的是递归的中序遍历是不行的,因为使用递归的中序遍历,某些节点的左右孩子会翻转两次。** + +如果非要使用递归中序的方式写,也可以,如下代码就可以避免节点左右孩子翻转两次的情况: + +``` +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/周总结/20201003二叉树周末总结.md b/problems/周总结/20201003二叉树周末总结.md new file mode 100644 index 00000000..b7c123bc --- /dev/null +++ b/problems/周总结/20201003二叉树周末总结.md @@ -0,0 +1,258 @@ +# 本周小结!(二叉树系列二) + +本周赶上了十一国庆,估计大家已经对本周末没什么概念了,但是我们该做总结还是要做总结的。 + +本周的主题其实是**简单但并不简单**,本周所选的题目大多是看一下就会的题目,但是大家看完本周的文章估计也发现了,二叉树的简答题目其实里面都藏了很多细节。 这些细节我都给大家展现了出来。 + + +## 周一 + +本周刚开始我们讲解了判断二叉树是否对称的写法, [二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)。 + +这道题目的本质是要比较两个树(这两个树是根节点的左右子树),遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。 + +而本题的迭代法中我们使用了队列,需要注意的是这不是层序遍历,而且仅仅通过一个容器来成对的存放我们要比较的元素,认识到这一点之后就发现:用队列,用栈,甚至用数组,都是可以的。 + +那么做完本题之后,在看如下两个题目。 +* 100.相同的树 +* 572.另一个树的子树 + +**[二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)中的递归法和迭代法只需要稍作修改其中一个树的遍历顺序,便可刷了100.相同的树。** + +100.相同的树的递归代码如下: + +```C++ +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); + } +}; +``` + +100.相同的树,精简之后代码如下: + +```C++ +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.相同的树,迭代法代码如下: + +```C++ +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://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中,我们讲解了如何求二叉树的最大深度。 + +本题可以使用前序,也可以使用后序遍历(左右中),使用前序求的就是深度,使用后序呢求的是高度。 + +**而根节点的高度就是二叉树的最大深度**,所以本题中我们通过后序求的根节点高度来求的二叉树最大深度,所以[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)中使用的是后序遍历。 + +本题当然也可以使用前序,代码如下:(**充分表现出求深度回溯的过程**) +```C++ +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; + } +}; +``` + +**可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!** + +注意以上代码是为了把细节体现出来,简化一下代码如下: + +```C++ +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://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA)中,我们讲解如何求二叉树的最小深度, 这道题目要是稍不留心很容易犯错。 + +**注意这里最小深度是从根节点到最近叶子节点的最短路径上的节点数量。注意是叶子节点。** + +什么是叶子节点,左右孩子都为空的节点才是叶子节点! + +**求二叉树的最小深度和求二叉树的最大深度的差别主要在于处理左右孩子不为空的逻辑。** + +注意到这一点之后 递归法和迭代法 都可以参照[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg)写出来。 + +## 周四 + +我们在[二叉树:我有多少个节点?](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw)中,讲解了如何求二叉树的节点数量。 + +这一天是十一长假的第一天,又是双节,所以简单一些,只要把之前两篇[二叉树:看看这些树的最大深度](https://mp.weixin.qq.com/s/guKwV-gSNbA1CcbvkMtHBg), [二叉树:看看这些树的最小深度](https://mp.weixin.qq.com/s/BH8-gPC3_QlqICDg7rGSGA)都认真看了的话,这道题目可以分分钟刷掉了。 + +估计此时大家对这一类求二叉树节点数量以及求深度应该非常熟练了。 + +## 周五 + +在[二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww)中讲解了如何判断二叉树是否是平衡二叉树 + +今天讲解一道判断平衡二叉树的题目,其实 方法上我们之前讲解深度的时候都讲过了,但是这次我们通过这道题目彻底搞清楚二叉树高度与深度的问题,以及对应的遍历方式。 + +二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数。 +二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数。 + +**但leetcode中强调的深度和高度很明显是按照节点来计算的**。 + +关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。 + +当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。 + +虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。 + +**例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!** + +讲了这么多二叉树题目的迭代法,有的同学会疑惑,迭代法中究竟什么时候用队列,什么时候用栈? + +**如果是模拟前中后序遍历就用栈,如果是适合层序遍历就用队列,当然还是其他情况,那么就是 先用队列试试行不行,不行就用栈。** + +## 周六 + +在[二叉树:找我的所有路径?](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA)中正式涉及到了回溯,很多同学过了这道题目,可能都不知道自己使用了回溯,其实回溯和递归都是相伴相生的。最后我依然给出了迭代法的版本。 + +我在题解中第一个版本的代码会把回溯的过程充分体现出来,如果大家直接看简洁的代码版本,很可能就会忽略的回溯的存在。 + +我在文中也强调了这一点。 + +有的同学还不理解 ,文中精简之后的递归代码,回溯究竟隐藏在哪里了。 + +文中我明确的说了:**回溯就隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。** + +如果还不理解的话,可以把 +``` +traversal(cur->left, path + "->", result); +``` + +改成 +``` +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://img-blog.csdnimg.cn/2021013018121150.png) diff --git a/problems/周总结/20201010二叉树周末总结.md b/problems/周总结/20201010二叉树周末总结.md new file mode 100644 index 00000000..d62fa5a5 --- /dev/null +++ b/problems/周总结/20201010二叉树周末总结.md @@ -0,0 +1,89 @@ + +# 本周小结!(二叉树系列三) + + +## 周一 + +在[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)中,通过leetcode [257.二叉树的所有路径这道题目](https://mp.weixin.qq.com/s/Osw4LQD2xVUnCJ-9jrYxJA),讲解了递归如何隐藏着回溯,一些代码会把回溯的过程都隐藏了起来了,甚至刷过这道题的同学可能都不知道自己用了回溯。 + +文章中第一版代码把每一个细节都展示了输出来了,大家可以清晰的看到回溯的过程。 + +然后给出了第二版优化后的代码,分析了其回溯隐藏在了哪里,如果要把这个回溯扣出来的话,在第二版的基础上应该怎么改。 + +主要需要理解:**回溯隐藏在traversal(cur->left, path + "->", result);中的 path + "->"。 每次函数调用完,path依然是没有加上"->" 的,这就是回溯了。** + + +## 周二 + +在文章[二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) 中提供了另一个判断节点属性的思路,平时我们习惯了使用通过节点的左右孩子判断本节点的属性,但发现使用这个思路无法判断左叶子。 + +此时需要相连的三层之间构成的约束条件,也就是要通过节点的父节点以及孩子节点来判断本节点的属性。 + +这道题目可以扩展大家对二叉树的解题思路。 + + +## 周三 + +在[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中的题目如果使用递归的写法还是有点难度的,层次遍历反而很简单。 + +题目其实就是要在树的**最后一行**找到**最左边的值**。 + +**如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。** + +在这篇文章中,我们使用递归算法实实在在的求了一次深度,然后使用靠左的遍历,保证求得靠左的最大深度,而且又一次使用了回溯。 + +如果对二叉树的高度与深度又有点模糊了,在看这里[二叉树:我平衡么?](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww),回忆一下吧。 + +[二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw)中把我们之前讲过的内容都过了一遍,此外,还用前序遍历的技巧求得了靠左的最大深度。 + +**求二叉树的各种最值,就想应该采用什么样的遍历顺序,确定了遍历循序,其实就和数组求最值一样容易了。** + + +## 周四 + +在[二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg)中通过两道题目,彻底说清楚递归函数的返回值问题。 + +一般情况下:**如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。** + +特别是有些时候 递归函数的返回值是bool类型,一些同学会疑惑为啥要加这个,其实就是为了找到一条边立刻返回。 + +其实还有一种就是后序遍历需要根据左右递归的返回值推出中间节点的状态,这种需要有返回值,例如[222.完全二叉树](https://mp.weixin.qq.com/s/2_eAjzw-D0va9y4RJgSmXw),[110.平衡二叉树](https://mp.weixin.qq.com/s/isUS-0HDYknmC0Rr4R8mww),这几道我们之前也讲过。 + +## 周五 + +之前都是讲解遍历二叉树,这次该构造二叉树了,在[二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg)中,我们通过前序和中序,后序和中序,构造了唯一的一颗二叉树。 + +**构造二叉树有三个注意的点:** + +* 分割时候,坚持区间不变量原则,左闭右开,或者左闭又闭。 +* 分割的时候,注意后序 或者 前序已经有一个节点作为中间节点了,不能继续使用了。 +* 如何使用切割后的后序数组来切合中序数组?利用中序数组大小一定是和后序数组的大小相同这一特点来进行切割。 + +这道题目代码实现并不简单,大家啃下来之后,二叉树的构造应该不是问题了。 + +**最后我还给出了为什么前序和后序不能唯一构成一棵二叉树,因为没有中序遍历就无法确定左右部分,也就无法分割。** + +## 周六 + +知道了如何构造二叉树,那么使用一个套路就可以解决文章[二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w)中的问题。 + +**注意类似用数组构造二叉树的题目,每次分隔尽量不要定义新的数组,而是通过下表索引直接在原数组上操作,这样可以节约时间和空间上的开销。** + +文章中我还给出了递归函数什么时候加if,什么时候不加if,其实就是控制空节点(空指针)是否进入递归,是不同的代码实现方式,都是可以的。 + +**一般情况来说:如果让空节点(空指针)进入递归,就不加if,如果不让空节点进入递归,就加if限制一下, 终止条件也会相应的调整。** + +## 总结 + +本周我们深度讲解了如下知识点: + +1. [递归中如何隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) +2. [如何通过三层关系确定左叶子](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) +3. [如何通过二叉树深度来判断左下角的值](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw) +4. [递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg) +5. [前序和中序,后序和中序构造唯一二叉树](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) +6. [使用数组构造某一特性的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w) + +**如果大家一路跟下来,一定收获满满,如果周末不做这个总结,大家可能都不知道自己收获满满,啊哈!** + + diff --git a/problems/周总结/20201017二叉树周末总结.md b/problems/周总结/20201017二叉树周末总结.md new file mode 100644 index 00000000..e642bfb2 --- /dev/null +++ b/problems/周总结/20201017二叉树周末总结.md @@ -0,0 +1,118 @@ + + +# 本周小结!(二叉树系列四) + +> 这已经是二叉树的第四周总结了,二叉树是非常重要的数据结构,也是面试中的常客,所以有必要一步一步帮助大家彻底掌握二叉树! + +## 周一 + +在[二叉树:合并两个二叉树](https://mp.weixin.qq.com/s/3f5fbjOFaOX_4MXzZ97LsQ)中讲解了如何合并两个二叉树,平时我们都习惯了操作一个二叉树,一起操作两个树可能还有点陌生。 + +其实套路是一样,只不过一起操作两个树的指针,我们之前讲过求 [二叉树:我对称么?](https://mp.weixin.qq.com/s/Kgf0gjvlDlNDfKIH2b1Oxg)的时候,已经初步涉及到了 一起遍历两颗二叉树了。 + +**迭代法中,一般一起操作两个树都是使用队列模拟类似层序遍历,同时处理两个树的节点,这种方式最好理解,如果用模拟递归的思路的话,要复杂一些。** + +## 周二 + +周二开始讲解一个新的树,二叉搜索树,开始要换一个思路了,如果没有利用好二叉搜索树的特性,就容易把简单题做成了难题了。 + +学习[二叉搜索树的特性](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg),还是比较容易的。 + +大多是二叉搜索树的题目,其实都离不开中序遍历,因为这样就是有序的。 + +至于迭代法,相信大家看到文章中如此简单的迭代法的时候,都会感动的痛哭流涕。 + +## 周三 + +了解了二搜索树的特性之后, 开始验证[一颗二叉树是不是二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q)。 + +首先在此强调一下二叉搜索树的特性: + +* 节点的左子树只包含小于当前节点的数。 +* 节点的右子树只包含大于当前节点的数。 +* 所有左子树和右子树自身必须也是二叉搜索树。 + +那么我们在验证二叉搜索树的时候,有两个陷阱: + +* 陷阱一 + +**不能单纯的比较左节点小于中间节点,右节点大于中间节点就完事了**,而是左子树都小于中间节点,右子树都大于中间节点。 + +* 陷阱二 + +在一个有序序列求最值的时候,不要定义一个全局遍历,然后遍历序列更新全局变量求最值。因为最值可能就是int 或者 longlong的最小值。 + +推荐要通过前一个数值(pre)和后一个数值比较(cur),得出最值。 + +**在二叉树中通过两个前后指针作比较,会经常用到**。 + +本文[二叉树:我是不是一棵二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q)中迭代法中为什么没有周一那篇那么简洁了呢,因为本篇是验证二叉搜索树,前提默认它是一棵普通二叉树,所以还是要回归之前老办法。 + +## 周四 + +了解了[二叉搜索树](https://mp.weixin.qq.com/s/vsKrWRlETxCVsiRr8v_hHg),并且知道[如何判断二叉搜索树](https://mp.weixin.qq.com/s/8odY9iUX5eSi0eRFSXFD4Q),本篇就很简单了。 + +**要知道二叉搜索树和中序遍历是好朋友!** + +在[二叉树:搜索树的最小绝对差](https://mp.weixin.qq.com/s/Hwzml6698uP3qQCC1ctUQQ)中强调了要利用搜索树的特性,把这道题目想象成在一个有序数组上求两个数最小差值,这就是一道送分题了。 + +**需要明确:在有序数组求任意两数最小值差等价于相邻两数的最小值差**。 + +同样本题也需要用pre节点记录cur节点的前一个节点。(这种写法一定要掌握) + +## 周五 + +此时大家应该知道遇到二叉搜索树,就想是有序数组,那么在二叉搜索树中求二叉搜索树众数就很简单了。 + +在[二叉树:我的众数是多少?](https://mp.weixin.qq.com/s/KSAr6OVQIMC-uZ8MEAnGHg)中我给出了如果是普通二叉树,应该如何求众数的集合,然后进一步讲解了二叉搜索树应该如何求众数集合。 + +在求众数集合的时候有一个技巧,因为题目中众数是可以有多个的,所以一般的方法需要遍历两遍才能求出众数的集合。 + +**但可以遍历一遍就可以求众数集合,使用了适时清空结果集的方法**,这个方法还是很巧妙的。相信仔细读了文章的同学会惊呼其巧妙! + +**所以大家不要看题目简单了,就不动手做了,我选的题目,一般不会简单到不用动手的程度,哈哈**。 + +## 周六 + +在[二叉树:公共祖先问题](https://mp.weixin.qq.com/s/n6Rk3nc_X3TSkhXHrVmBTQ)中,我们开始讲解如何在二叉树中求公共祖先的问题,本来是打算和二叉搜索树一起讲的,但发现篇幅过长,所以先讲二叉树的公共祖先问题。 + +**如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。** + +这道题目的看代码比较简单,而且好像也挺好理解的,但是如果把每一个细节理解到位,还是不容易的。 + +主要思考如下几点: + +* 如何从底向上遍历? +* 遍历整棵树,还是遍历局部树? +* 如何把结果传到根节点的? + +这些问题都需要弄清楚,上来直接看代码的话,是可能想不到这些细节的。 + +公共祖先问题,还是有难度的,初学者还是需要慢慢消化! + +## 总结 + +本周我们讲了[如何合并两个二叉树](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/n6Rk3nc_X3TSkhXHrVmBTQ)。 + +现在已经讲过了几种二叉树了,二叉树,二叉平衡树,完全二叉树,二叉搜索树,后面还会有平衡二叉搜索树。 那么一些同学难免会有混乱了,我针对如下三个问题,帮大家在捋顺一遍: + +1. 平衡二叉搜索数是不是二叉搜索树和平衡二叉树的结合? + +是的,是二叉搜索树和平衡二叉树的结合。 + +2. 平衡二叉树与完全二叉树的区别在于底层节点的位置? + +是的,完全二叉树底层必须是从左到右连续的,且次底层是满的。 + +3. 堆是完全二叉树和排序的结合,而不是平衡二叉搜索树? + +堆是一棵完全二叉树,同时保证父子节点的顺序关系(有序)。 **但完全二叉树一定是平衡二叉树,堆的排序是父节点大于子节点,而搜索树是父节点大于左孩子,小于右孩子,所以堆不是平衡二叉搜索树**。 + +大家如果每天坚持跟下来,会发现又是充实的一周![机智] + diff --git a/problems/周总结/20201030回溯周末总结.md b/problems/周总结/20201030回溯周末总结.md new file mode 100644 index 00000000..cbb0eb8a --- /dev/null +++ b/problems/周总结/20201030回溯周末总结.md @@ -0,0 +1,115 @@ + + +

+ + + + +

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

+ + + + +

+ + +正好也给「算法汇总」添加一个新专题-算法性能分析,以后如果有空余时间还会陆续更新这个模块,大家如果经常看「算法汇总」的话,就会发现,「算法汇总」里已经更新的三个模块「编程素养」「求职」「算法性能分析」,内容越来越丰满了,大家现在就可以去看看哈。 + +后面在算法题目之余,我还会继续更新这几个模块的! + +# 周一 + +在[程序员的简历应该这么写!!(附简历模板)](https://mp.weixin.qq.com/s/nCTUzuRTBo1_R_xagVszsA)中以我自己的总结经验为例讲一讲大家应该如何写简历。 + +主要有如下几点: + +* 简历篇幅不要过长 +* 谨慎使用“精通” +* 拿不准的绝对不要写在简历上 +* 项目经验中要突出自己的贡献 +* 面试中如何变被动为主动 +* 博客的重要性 + +最后还给出我自己的简历模板。 + +每一个点我都在文章中详细讲解了应该怎么写,平时应该如何积累,以及面前如何准备。 + +如果大家把以上几点都注意到了,那就是一份优秀的简历了,至少简历上就没啥毛病,剩下的就看自己的技术功底和临场发挥了。 + +一些录友会问我学校不好怎么办,没有项目经验怎么办之类的问题。 + +其实这就不在简历技巧的范围内了。 + +对于学校的话,某些公司可能有硬性要求,但如果能力特别出众,机会也是很大的。 不过说实话,大家都是普通人,真正技术能力出众的选手毕竟是少数。 + +**而且面试其实挺看缘分的**,相信大家应该都遇到过这种情景:同一家公司面别人的时候问题贼简单,然后人家就顺利拿offer,一到自己面的时候难题就上来了。 + +至于项目经验,没有项目,就要自己找找项目来做。 + +我的Github上有一些我曾经写过的一些小项目,大家可以去看看:https://github.com/youngyangyang04 + +**最后就是要端正写简历的心态,写简历是在自己真实背景和水平下,把自己各个方面包装到极致!** + + +# 周二 + +在[关于时间复杂度,你不知道的都在这里!](https://mp.weixin.qq.com/s/LWBfehW1gMuEnXtQjJo-sw)中详细讲解了时间复杂度,很多被大家忽略的内容,在文中都做了详细的解释。 + +文中涉及如下问题: + +* 究竟什么是大O?大O表示什么意思?严格按照大O的定义来说,快排应该是O(n^2)的算法! +* O(n^2)的算法为什么有时候比O(n)的算法更优? +* 什么时间复杂度为什么可以忽略常数项? +* 如何简化复杂的时间复杂度表达式,原理是什么? +* O(logn)中的log究竟是以谁为底? + +这些问题大家可能懵懵懂懂的了解一些,但一细问又答不上来。 + +相信看完本篇[关于时间复杂度,你不知道的都在这里!](https://mp.weixin.qq.com/s/LWBfehW1gMuEnXtQjJo-sw),以上问题大家就理解的清晰多了。 + +文中最后还运用以上知识通过一道简单的题目具体分析了一下其时间复杂度,给出两种方法究竟谁最优。 + +可以说从理论到实战将时间复杂度讲的明明白白。 + + +# 周三 + +在[O(n)的算法居然超时了,此时的n究竟是多大?](https://mp.weixin.qq.com/s/73ryNsuPFvBQkt6BbhNzLA)中介绍了大家在leetcode上提交代码经常遇到的一个问题-超时! + +估计很多录友知道算法超时了,但没有注意过 O(n)的算法,如果1s内出结果,这个n究竟是多大? + +文中从计算机硬件出发,分析计算机的计算性能,然后亲自做实验,整理出数据如下: + +![程序超时1](https://img-blog.csdnimg.cn/20201208231559175.png) + +**大家有一个数量级上的概念就可以了!** + +正如文中说到的,**作为一名合格的程序员,至少要知道我们的程序是1s后出结果还是一年后出结果**。 + + +# 周四 + +在[通过一道面试题目,讲一讲递归算法的时间复杂度!](https://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ)中,讲一讲如果计算递归算法的时间复杂度。 + +递归的时间复杂度等于**递归的次数 * 每次递归中的操作次数**。 + +所以了解究竟递归了多少次就是重点。 + +文中通过一道简单的面试题:求x的n次方(**注意:这道面试题大厂面试官经常用!**),还原面试场景,来带大家深入了解一下递归的时间复杂度。 + +文中给出了四个版本的代码实现,并逐一分析了其时间复杂度。 + +此时大家就会发现,同一道题目,同样使用递归算法,有的同学会写出了O(n)的代码,有的同学就写出了O(logn)的代码。 + +其本质是要对递归的时间复杂度有清晰的认识,才能运用递归来有效的解决问题! + +相信看了本篇之后,对递归的时间复杂度分析就已经有深刻的理解了。 + + +# 总结 + +本周讲解的内容都是经常被大家忽略的知识点,而通常这种知识点,才最能发现一位候选人的编程功底。 + +因为之前一直都是在持续更新算法题目的文章,这周说一说算法性能分析,感觉也是换了换口味,哈哈。 + +同时大家也会发现,**大厂面试官最喜欢用“简单题”(就是看起来很简单,其实非常考验技术功底的题目),而不是要手撕红黑树之类的**。 + +所以基础很重要,本周我介绍的内容其实都不难,看过的话都懂了,都是基础内容,但很多同学都把这些内容忽略掉了。 + +这其实也正常,咱们上学的时候教科书上基本没有实用的重点,而一般求职算法书也不讲这些,所以这方面内容可以靠看「代码随想录」的文章,当然更要靠自己多琢磨,多专研,多实践! + +**下周开始恢复贪心题目系列**,后序有空我还会陆续讲一讲类似本周的基础内容,在「算法汇总」的那几个模块都会持续更新的。 + +就酱,「代码随想录」是技术公众号里的一抹清流,值得推荐给身边的朋友同学们! + + diff --git a/problems/周总结/20201217贪心周末总结.md b/problems/周总结/20201217贪心周末总结.md new file mode 100644 index 00000000..4a634da5 --- /dev/null +++ b/problems/周总结/20201217贪心周末总结.md @@ -0,0 +1,99 @@ + + +# 本周小结!(贪心算法系列三) + +对于贪心,大多数同学都会感觉,不就是常识嘛,这算啥算法,那么本周的题目就可以带大家初步领略一下贪心的巧妙,贪心算法往往妙的出其不意。 + +## 周一 + +在[贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw)中给出每一个加油站的汽油和开到这个加油站的消耗,问汽车能不能开一圈。 + +这道题目咋眼一看,感觉是一道模拟题,模拟一下汽车从每一个节点出发看看能不能开一圈,时间复杂度是O(n^2)。 + +即使用模拟这种情况,也挺考察代码技巧的。 + +**for循环适合模拟从头到尾的遍历,而while循环适合模拟环形遍历,对于本题的场景要善于使用while!** + +如果代码功力不到位,就模拟这种情况,可能写的也会很费劲。 + +本题的贪心解法,我给出两种解法。 + +对于解法一,其实我并不认为这是贪心,因为没有找出局部最优,而是直接从全局最优的角度上思考问题,但思路很巧妙,值得学习一下。 + +对于解法二,贪心的局部最优:当前累加rest[j]的和curSum一旦小于0,起始位置至少要是j+1,因为从j开始一定不行。全局最优:找到可以跑一圈的起始位置。 + +这里是可以从局部最优推出全局最优的,想不出反例,那就试试贪心。 + +**解法二就体现出贪心的精髓,同时大家也会发现,虽然贪心是常识,有些常识并不容易,甚至很难!** + +## 周二 + +在[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ)中我们第一次接触了需要考虑两个维度的情况。 + +例如这道题,是先考虑左边呢,还是考虑右边呢? + +**先考虑哪一边都可以! 就别两边一起考虑,那样就把自己陷进去了**。 + +先贪心一边,局部最优:只要右边评分比左边大,右边的孩子就多一个糖果,全局最优:相邻的孩子中,评分高的右孩子获得比左边孩子更多的糖果 + +如图: +![135.分发糖果](https://img-blog.csdnimg.cn/20201117114916878.png) + + +接着在贪心另一边,左孩子大于右孩子,左孩子的糖果就要比右孩子多。 + +此时candyVec[i](第i个小孩的糖果数量,左孩子)就有两个选择了,一个是candyVec[i + 1] + 1(从右孩子这个加1得到的糖果数量),一个是candyVec[i](之前比较右孩子大于左孩子得到的糖果数量)。 + +那么第二次贪心的局部最优:取candyVec[i + 1] + 1 和 candyVec[i] 最大的糖果数量,保证第i个小孩的糖果数量即大于左边的也大于右边的。全局最优:相邻的孩子中,评分高的孩子获得更多的糖果。 + +局部最优可以推出全局最优。 + +如图: +![135.分发糖果1](https://img-blog.csdnimg.cn/20201117115658791.png) + + +## 周三 + +在[贪心算法:柠檬水找零](https://mp.weixin.qq.com/s/0kT4P-hzY7H6Ae0kjQqnZg)中我们模拟了买柠檬水找零的过程。 + +这道题目刚一看,可能会有点懵,这要怎么找零才能保证完整全部账单的找零呢? + +**但仔细一琢磨就会发现,可供我们做判断的空间非常少!** + +美元10只能给账单20找零,而美元5可以给账单10和账单20找零,美元5更万能! + +局部最优:遇到账单20,优先消耗美元10,完成本次找零。全局最优:完成全部账单的找零。 + +局部最优可以推出全局最优。 + +所以把能遇到的情况分析一下,只要分析到具体情况了,一下子就豁然开朗了。 + +这道题目其实是一道简单题,但如果一开始就想从整体上寻找找零方案,就会把自己陷进去,各种情况一交叉,只会越想越复杂了。 + +## 周四 + +在[贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw)中,我们再一次遇到了需要考虑两个维度的情况。 + +之前我们已经做过一道类似的了就是[贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ),但本题比分发糖果难不少! + +[贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw)中依然是要确定一边,然后在考虑另一边,两边一起考虑一定会蒙圈。 + +那么本题先确定k还是先确定h呢,也就是究竟先按h排序呢,还先按照k排序呢? + +这里其实很考察大家的思考过程,如果按照k来从小到大排序,排完之后,会发现k的排列并不符合条件,身高也不符合条件,两个维度哪一个都没确定下来。 + +**所以先从大到小按照h排个序,再来贪心k**。 + +此时局部最优:优先按身高高的people的k来插入。插入操作过后的people满足队列属性。全局最优:最后都做完插入操作,整个队列满足题目队列属性。 + +局部最优可以推出全局最优,找不出反例,那么就来贪心。 + +## 总结 + +「代码随想录」里已经讲了十一道贪心题目了,大家可以发现在每一道题目的讲解中,我都是把什么是局部最优,和什么是全局最优说清楚。 + +虽然有时候感觉贪心就是常识,但如果真正是常识性的题目,其实是模拟题,就不是贪心算法了!例如[贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw)中的贪心方法一,其实我就认为不是贪心算法,而是直接从全局最优的角度上来模拟,因为方法里没有体现局部最优的过程。 + +而且大家也会发现,贪心并没有想象中的那么简单,贪心往往妙的出其不意,触不及防!哈哈 + + diff --git a/problems/周总结/20201224贪心周末总结.md b/problems/周总结/20201224贪心周末总结.md new file mode 100644 index 00000000..cdc62168 --- /dev/null +++ b/problems/周总结/20201224贪心周末总结.md @@ -0,0 +1,104 @@ + + +# 本周小结!(贪心算法系列四) + +## 周一 + +在[贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw)中,我们开始讲解了重叠区间问题,用最少的弓箭射爆所有气球,其本质就是找到最大的重叠区间。 + +按照左边界经行排序后,如果气球重叠了,重叠气球中右边边界的最小值 之前的区间一定需要一个弓箭 + +如图: + +![452.用最少数量的箭引爆气球](https://img-blog.csdnimg.cn/20201123101929791.png) + +模拟射气球的过程,很多同学真的要去模拟了,实时把气球从数组中移走,这么写的话就复杂了,从前向后遍历重复的只要跳过就可以的。 + +## 周二 + +在[贪心算法:无重叠区间](https://mp.weixin.qq.com/s/oFOEoW-13Bm4mik-aqAOmw)中要去掉最少的区间,来让所有区间没有重叠。 + +我来按照右边界排序,从左向右记录非交叉区间的个数。最后用区间总数减去非交叉区间的个数就是需要移除的区间个数了。 + +如图: + +![435.无重叠区间](https://img-blog.csdnimg.cn/20201221201553618.png) + +细心的同学就发现了,此题和 [贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw)非常像。 + +弓箭的数量就相当于是非交叉区间的数量,只要把弓箭那道题目代码里射爆气球的判断条件加个等号(认为[0,1][1,2]不是相邻区间),然后用总区间数减去弓箭数量 就是要移除的区间数量了。 + +把[贪心算法:用最少数量的箭引爆气球](https://mp.weixin.qq.com/s/HxVAJ6INMfNKiGwI88-RFw)代码稍做修改,别可以AC本题。 + +修改后的C++代码如下: +```C++ +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://mp.weixin.qq.com/s/pdX4JwV1AOpc_m90EcO2Hw)中我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。 + +这道题目leetcode上标的是贪心,其实我不认识是贪心,因为没感受到局部最优和全局最优的关系。 + +但不影响这是一道好题,思路很不错,**通过字符出现最远距离取并集的方法,把出现过的字符都圈到一个区间里**。 + +解题过程分如下两步: + +* 统计每一个字符最后出现的位置 +* 从头遍历字符,并更新字符的最远出现下标,如果找到字符最远出现位置下标和当前下标相等了,则找到了分割点 + +如图: + +![763.划分字母区间](https://img-blog.csdnimg.cn/20201222191924417.png) + + +## 周四 + +[贪心算法:合并区间](https://mp.weixin.qq.com/s/royhzEM5tOkUFwUGrNStpw)中要合并所有重叠的区间。 + +相信如果录友们前几天区间问题的题目认真练习了,今天题目就应该算简单一些了。 + +按照左边界排序,排序之后局部最优:每次合并都取最大的右边界,这样就可以合并更多的区间了,整体最优:合并所有重叠的区间。 + +具体操作:按照左边界从小到大排序之后,如果 intervals[i][0] < intervals[i - 1][1] 即intervals[i]左边界 < intervals[i - 1]右边界,则一定有重复,因为intervals[i]的左边界一定是大于等于intervals[i - 1]的左边界。 + +如图: + +![56.合并区间](https://img-blog.csdnimg.cn/20201223200632791.png) + + +## 总结 + +本周的主题就是用贪心算法来解决区间问题,进过本周的学习,大家应该对区间的各种合并分割有一定程度的了解了。 + +其实很多区间的合并操作看起来都是常识,其实贪心算法有时候就是常识,哈哈,但也别小看了贪心算法。 + +在[贪心算法:合并区间](https://mp.weixin.qq.com/s/royhzEM5tOkUFwUGrNStpw)中就说过,对于贪心算法,很多同学都是:「如果能凭常识直接做出来,就会感觉不到自己用了贪心, 一旦第一直觉想不出来, 可能就一直想不出来了」。 + +所以还是要多看多做多练习! + +**「代码随想录」里总结的都是经典题目,大家跟着练就节省了不少选择题目的时间了**。 + + diff --git a/problems/周总结/20210107动规周末总结.md b/problems/周总结/20210107动规周末总结.md new file mode 100644 index 00000000..24700941 --- /dev/null +++ b/problems/周总结/20210107动规周末总结.md @@ -0,0 +1,151 @@ + +这周我们正式开始动态规划的学习! + +## 周一 + +在[关于动态规划,你该了解这些!](https://mp.weixin.qq.com/s/ocZwfPlCWrJtVGACqFNAag)中我们讲解了动态规划的基础知识。 + +首先讲一下动规和贪心的区别,其实大家不用太强调理论上的区别,做做题,就感受出来了。 + +然后我们讲了动规的五部曲: + +1. 确定dp数组(dp table)以及下标的含义 +2. 确定递推公式 +3. dp数组如何初始化 +4. 确定遍历顺序 +5. 举例推导dp数组 + +后序我们在讲解动规的题目时候,都离不开这五步! + +本周都是简单题目,大家可能会感觉 按照这五部来好麻烦,凭感觉随手一写,直接就过,越到后面越会感觉,凭感觉这个事还是不靠谱的,哈哈。 + +最后我们讲了动态规划题目应该如何debug,相信一些录友做动规的题目,一旦报错也是凭感觉来改。 + +其实只要把dp数组打印出来,哪里有问题一目了然! + +**如果代码写出来了,一直AC不了,灵魂三问:** + +1. 这道题目我举例推导状态转移公式了么? +2. 我打印dp数组的日志了么? +3. 打印出来了dp数组和我想的一样么? + +哈哈,专治各种代码写出来了但AC不了的疑难杂症。 + +## 周二 + +这道题目[动态规划:斐波那契数](https://mp.weixin.qq.com/s/ko0zLJplF7n_4TysnPOa_w)是当之无愧的动规入门题。 + +简单题,我们就是用来了解方法论的,用动规五部曲走一遍,题目其实已经把递推公式,和dp数组如何初始化都给我们了。 + +## 周三 + +[动态规划:爬楼梯](https://mp.weixin.qq.com/s/Ohop0jApSII9xxOMiFhGIw) 这道题目其实就是斐波那契数列。 + +但正常思考过程应该是推导完递推公式之后,发现这是斐波那契,而不是上来就知道这是斐波那契。 + +在这道题目的第三步,确认dp数组如何初始化,其实就可以看出来,对dp[i]定义理解的深度。 + +dp[0]其实就是一个无意义的存在,不用去初始化dp[0]。 + +有的题解是把dp[0]初始化为1,然后遍历的时候i从2开始遍历,这样是可以解题的,然后强行解释一波dp[0]应该等于1的含义。 + +一个严谨的思考过程,应该是初始化dp[1] = 1,dp[2] = 2,然后i从3开始遍历,代码如下: + +```C++ +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阶楼顶。 + +这又有难度了,这其实是一个完全背包问题,但力扣上没有这种题目,所以后续我在讲解背包问题的时候,今天这道题还会拿从背包问题的角度上来再讲一遍。 + +这里我先给出我的实现代码: + +```C++ +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://mp.weixin.qq.com/s/I6ZXFbw09NR31F5CJR_geQ)中,以我自己面试别人的真实经历,通过求x的n次方 这么简单的题目,就可以考察候选人对算法性能以及递归的理解深度,录友们可以看看,绝对有收获! + +## 周四 + +这道题目[动态规划:使用最小花费爬楼梯](https://mp.weixin.qq.com/s/djZB9gkyLFAKcQcSvKDorA)就是在爬台阶的基础上加了一个花费, + +这道题描述也确实有点魔幻。 + +题目描述为:每当你爬上一个阶梯你都要花费对应的体力值,一旦支付了相应的体力值,你就可以选择向上爬一个阶梯或者爬两个阶梯。 + +示例1: + +输入:cost = [10, 15, 20] +输出:15 + + +**从题目描述可以看出:要不是第一步不需要花费体力,要不就是第最后一步不需要花费体力,我个人理解:题意说的其实是第一步是要支付费用的!**。因为是当你爬上一个台阶就要花费对应的体力值! + +所以我定义的dp[i]意思是也是第一步是要花费体力的,最后一步不用花费体力了,因为已经支付了。 + +之后一些录友在留言区说 可以定义dp[i]为:第一步是不花费体力,最后一步是花费体力的。 + +所以代码也可以这么写: + +```C++ +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/周总结/20210114动规周末总结.md b/problems/周总结/20210114动规周末总结.md new file mode 100644 index 00000000..acce0fb2 --- /dev/null +++ b/problems/周总结/20210114动规周末总结.md @@ -0,0 +1,159 @@ + +## 周一 + +[动态规划:不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A)中求从出发点到终点有几种路径,只能向下或者向右移动一步。 + +我们提供了三种方法,但重点讲解的还是动规,也是需要重点掌握的。 + +**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://img-blog.csdnimg.cn/20201209113631392.png) + + +## 周二 + +[动态规划:不同路径还不够,要有障碍!](https://mp.weixin.qq.com/s/lhqF0O4le9-wvalptOVOww)相对于[动态规划:不同路径](https://mp.weixin.qq.com/s/MGgGIt4QCpFMROE9X9he_A)添加了障碍。 + +dp[i][j]定义依然是:表示从(0 ,0)出发,到(i, j) 有dp[i][j]条不同的路径。 + + +本题难点在于初始化,如果(i, 0) 这条边有了障碍之后,障碍之后(包括障碍)都是走不到的位置了,所以障碍之后的dp[i][0]应该还是初始值0。 + +如图: + +![63.不同路径II](https://img-blog.csdnimg.cn/20210104114513928.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://img-blog.csdnimg.cn/20210104114548983.png) + +对应的dp table 如图: + +![63.不同路径II2](https://img-blog.csdnimg.cn/20210104114610256.png) + + +## 周三 + +[动态规划:整数拆分,你要怎么拆?](https://mp.weixin.qq.com/s/cVbyHrsWH_Rfzlj-ESr01A)给出一个整数,问有多少种拆分的方法。 + +这道题目就有点难度了,题目中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://img-blog.csdnimg.cn/20210104173021581.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://mp.weixin.qq.com/s/cVbyHrsWH_Rfzlj-ESr01A)总结里,我也给出了递推公式dp[i] = max(dp[i], dp[i - j] * dp[j])这种写法。 + +对于这种写法,一位录友总结的很好,意思就是:如果递推公式是dp[i-j] * dp[j],这样就相当于强制把一个数至少拆分成四份。 + +dp[i-j]至少是两个数的乘积,dp[j]又至少是两个数的乘积,但其实3以下的数,数的本身比任何它的拆分乘积都要大了,所以文章中初始化的时候才要特殊处理。 + +## 周四 + +[动态规划:不同的二叉搜索树](https://mp.weixin.qq.com/s/8VE8pDrGxTf8NEVYBDwONw)给出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://img-blog.csdnimg.cn/20210107093253987.png) + +## 总结 + +本周题目已经开始点难度了,特别是[动态规划:不同的二叉搜索树](https://mp.weixin.qq.com/s/8VE8pDrGxTf8NEVYBDwONw)这道题目,明显感觉阅读量很低,可能是因为确实有点难吧。 + +我现在也陷入了纠结,题目一简单,就会有录友和我反馈说题目太简单了,题目一难,阅读量就特别低。 + +我也好难那,哈哈哈。 + +**但我还会坚持规划好的路线,难度循序渐进,并以面试经典题目为准,该简单的时候就是简单,同时也不会因为阅读量低就放弃有难度的题目!**。 + +录友们看到这是不是得给个Carl点个赞啊[让我看看]。 + +预告,我们下周正式开始讲解背包问题,经典的不能再经典,也是比较难的一类动态规划的题目了,录友们上车抓稳咯。 + diff --git a/problems/周总结/20210121动规周末总结.md b/problems/周总结/20210121动规周末总结.md new file mode 100644 index 00000000..dc0e7a46 --- /dev/null +++ b/problems/周总结/20210121动规周末总结.md @@ -0,0 +1,160 @@ +# 本周小结!(动态规划系列三) +本周我们正式开始讲解背包问题,也是动规里非常重要的一类问题。 + +背包问题其实有很多细节,如果了解个大概,然后也能一气呵成把代码写出来,但稍稍变变花样可能会陷入迷茫了。 + +开始回顾一下本周的内容吧! + +## 周一 + +[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)中,我们开始介绍了背包问题。 + +首先对于背包的所有问题中,01背包是最最基础的,其他背包也是在01背包的基础上稍作变化。 + +所以我才花费这么大精力去讲解01背包。 + +关于其他几种常用的背包,大家看这张图就了然于胸了: + +![416.分割等和子集1](https://img-blog.csdnimg.cn/20210117171307407.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数组如何初始化 + +```C++ +// 初始化 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数组在遍历顺序上,外层遍历物品 ,内层遍历背包容量 和 外层遍历背包容量 ,内层遍历物品 都是可以的!** + +但是先遍历物品更好理解。代码如下: + +```C++ +// 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://img-blog.csdnimg.cn/20210118163425129.jpg) + +最终结果就是dp[2][4]。 + + +## 周二 + +[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中把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数组遍历顺序 + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210110103614769.png) + + +## 周三 + +[动态规划:416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)中我们开始用01背包来解决问题。 + +只有确定了如下四点,才能把01背包问题套到本题上来。 + +* 背包的体积为sum / 2 +* 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值 +* 背包如何正好装满,说明找到了总和为 sum / 2 的子集。 +* 背包中每一个元素是不可重复放入。 + +接下来就是一个完整的01背包问题,大家应该可以轻松做出了。 + +## 周四 + +[动态规划:1049. 最后一块石头的重量 II](https://mp.weixin.qq.com/s/WbwAo3jaUaNJjvhHgq0BGg)这道题目其实和[动态规划:416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)是非常像的。 + +本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。 + +[动态规划:416. 分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ)相当于是求背包是否正好装满,而本题是求背包最多能装多少。 + +这两道题目是对dp[target]的处理方式不同。这也考验的对dp[i]定义的理解。 + + +## 总结 + +总体来说,本周信息量还是比较大的,特别对于对动态规划还不够了解的同学。 + +但如果坚持下来把,我在文章中列出的每一个问题,都仔细思考,消化为自己的知识,那么进步一定是飞速的。 + +有的同学可能看了看背包递推公式,上来就能撸它几道题目,然后背包问题就这么过去了,其实这样是很不牢固的。 + +就像是我们讲解01背包的时候,花了那么大力气才把每一个细节都讲清楚,这里其实是基础,后面的背包问题怎么变,基础比较牢固自然会有自己的一套思考过程。 + + diff --git a/problems/周总结/20210128动规周末总结.md b/problems/周总结/20210128动规周末总结.md new file mode 100644 index 00000000..bd597e41 --- /dev/null +++ b/problems/周总结/20210128动规周末总结.md @@ -0,0 +1,141 @@ +# 本周小结!(动态规划系列四) + +## 周一 + +[动态规划:目标和!](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw)要求在数列之间加入+ 或者 -,使其和为S。 + +所有数的总和为sum,假设加法的总和为x,那么可以推出x = (S + sum) / 2。 + +S 和 sum都是固定的,那此时问题就转化为01背包问题(数列中的数只能使用一次): 给你一些物品(数字),装满背包(就是x)有几种方法。 + +1. 确定dp数组以及下标的含义 + +**dp[j] 表示:填满j(包括j)这么大容积的包,有dp[i]种方法** + +2. 确定递推公式 + +dp[i] += dp[j - nums[j]] + +**注意:求装满背包有几种方法类似的题目,递推公式基本都是这样的**。 + +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://img-blog.csdnimg.cn/20210125120743274.jpg) + +## 周二 + +这道题目[动态规划:一和零!](https://mp.weixin.qq.com/s/x-u3Dsp76DlYqtCe0xEKJw)算有点难度。 + +**不少同学都以为是多重背包,其实这是一道标准的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://img-blog.csdnimg.cn/20210120111201512.jpg) + +## 周三 + +此时01背包我们就讲完了,正式开始完全背包。 + +在[动态规划:关于完全背包,你该了解这些!](https://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw)中我们讲解了完全背包的理论基础。 + +其实完全背包和01背包区别就是完全背包的物品是无限数量。 + +递推公式也是一样的,但难点在于遍历顺序上! + +完全背包的物品是可以添加多次的,所以遍历背包容量要从小到大去遍历,即: + +```C++ +// 先遍历物品,再遍历背包 +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://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw)也给出了详细的解释。 + +这个细节是很多同学忽略掉的点,其实也不算细节了,**相信不少同学在写背包的时候,两层for循环的先后循序搞不清楚,靠感觉来的**。 + +所以理解究竟是先遍历啥,后遍历啥非常重要,这也体现出遍历顺序的重要性! + +在文中,我也强调了是对纯完全背包,两个for循环先后循序无所谓,那么题目稍有变化,可就有所谓了。 + +## 周四 + +在[动态规划:给你一些零钱,你要怎么凑?](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ)中就是给你一堆零钱(零钱个数无限),为凑成amount的组合数有几种。 + +**注意这里组合数和排列数的区别!** + +看到无限零钱个数就知道是完全背包, + +但本题不是纯完全背包了(求是否能装满背包),而是求装满背包有几种方法。 + +这里在遍历顺序上可就有说法了。 + +* 如果求组合数就是外层for循环遍历物品,内层for遍历背包。 +* 如果求排列数就是外层for遍历背包,内层for循环遍历物品。 + +这里同学们需要理解一波,我在文中也给出了详细的解释,下周我们将介绍求排列数的完全背包题目来加深对这个遍历顺序的理解。 + + +## 总结 + +相信通过本周的学习,大家已经初步感受到遍历顺序的重要性! + +很多对动规理解不深入的同学都会感觉:动规嘛,就是把递推公式推出来其他都easy了。 + +其实这是一种错觉,或者说对动规理解的不够深入! + +我在动规专题开篇介绍[关于动态规划,你该了解这些!](https://mp.weixin.qq.com/s/ocZwfPlCWrJtVGACqFNAag)中就强调了 **递推公式仅仅是 动规五部曲里的一小部分, dp数组的定义、初始化、遍历顺序,哪一点没有搞透的话,即使知道递推公式,遇到稍稍难一点的动规题目立刻会感觉写不出来了**。 + +此时相信大家对动规五部曲也有更深的理解了,同样也验证了Carl之前讲过的:**简单题是用来学习方法论的,而遇到难题才体现出方法论的重要性!** + + diff --git a/problems/周总结/20210204动规周末总结.md b/problems/周总结/20210204动规周末总结.md new file mode 100644 index 00000000..db14f7f3 --- /dev/null +++ b/problems/周总结/20210204动规周末总结.md @@ -0,0 +1,202 @@ +# 本周小结!(动态规划系列五) + +## 周一 + +[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)中给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数(顺序不同的序列被视作不同的组合)。 + +题目面试虽然是组合,但又强调顺序不同的序列被视作不同的组合,其实这道题目求的是排列数! + +递归公式:dp[i] += dp[i - nums[j]]; + +这个和前上周讲的组合问题又不一样,关键就体现在遍历顺序上! + +在[动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) 中就已经讲过了。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +如果把遍历nums(物品)放在外循环,遍历target的作为内循环的话,举一个例子:计算dp[4]的时候,结果集只有 {1,3} 这样的集合,不会有{3,1}这样的集合,因为nums遍历放在外层,3只能出现在1后面! + +所以本题遍历顺序最终遍历顺序:**target(背包)放在外循环,将nums(物品)放在内循环,内循环从前到后遍历**。 + +```C++ +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://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA)中我们进阶了一下。 + +改为:每次可以爬 1 、 2、.....、m 个台阶。问有多少种不同的方法可以爬到楼顶呢? + +1阶,2阶,.... m阶就是物品,楼顶就是背包。 + +每一阶可以重复使用,例如跳了1阶,还可以继续跳1阶。 + +问跳到楼顶有几种方法其实就是问装满背包有几种方法。 + +**此时大家应该发现这就是一个完全背包问题了!** + + +和昨天的题目[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)基本就是一道题了,遍历顺序也是一样一样的! + +代码如下: +```C++ +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://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数(每种硬币的数量是无限的)。 + +这里我们都知道这是完全背包。 + +递归公式:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); + +关键看遍历顺序。 + +本题求钱币最小个数,**那么钱币有顺序和没有顺序都可以,都不影响钱币的最小个数。**。 + +所以本题并不强调集合是组合还是排列。 + +**那么本题的两个for循环的关系是:外层for循环遍历物品,内层for遍历背包或者外层for遍历背包,内层for循环遍历物品都是可以的!** + + +外层for循环遍历物品,内层for遍历背包: +```C++ +// 版本一 +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循环遍历物品: + +```C++ +// 版本二 +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://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ)给定正整数 n,找到若干个完全平方数(比如 1, 4, 9, 16, ...)使得它们的和等于 n。你需要让组成和的完全平方数的个数最少(平方数可以重复使用)。 + + +如果按顺序把前面的文章都看了,这道题目就是简单题了。 dp[i]的定义,递推公式,初始化,遍历顺序,都是和[动态规划:322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ) 一样一样的。 + +要是没有前面的基础上来做这道题,那这道题目就有点难度了。 + +**这也体现了刷题顺序的重要性**。 + +先遍历背包,在遍历物品: + +```C++ +// 版本一 +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]; + } +}; +``` + +先遍历物品,在遍历背包: + +```C++ +// 版本二 +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://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) +求排列数:[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA) +求最小数:[动态规划:322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)、[动态规划:279.完全平方数](https://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ) + +此时我们就已经把完全背包的遍历顺序研究的透透的了! + + diff --git a/problems/周总结/20210225动规周末总结.md b/problems/周总结/20210225动规周末总结.md new file mode 100644 index 00000000..739d0469 --- /dev/null +++ b/problems/周总结/20210225动规周末总结.md @@ -0,0 +1,302 @@ + +本周我们主要讲解了打家劫舍系列,这个系列也是dp解决的经典问题,那么来看看我们收获了哪些呢,一起来回顾一下吧。 + +## 周一 + +[动态规划:开始打家劫舍!](https://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw)中就是给一个数组相邻之间不能连着偷,如果偷才能得到最大金钱。 + +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://img-blog.csdnimg.cn/20210221170954115.jpg) + +红框dp[nums.size() - 1]为结果。 + +## 周二 + +[动态规划:继续打家劫舍!](https://mp.weixin.qq.com/s/kKPx4HpH3RArbRcxAVHbeQ)就是数组成环了,然后相邻的不能连着偷。 + +这里主要考虑清楚三种情况: + +* 情况一:考虑不包含首尾元素 + +![213.打家劫舍II](https://img-blog.csdnimg.cn/20210129160748643.jpg) + +* 情况二:考虑包含首元素,不包含尾元素 + +![213.打家劫舍II1](https://img-blog.csdnimg.cn/20210129160821374.jpg) + +* 情况三:考虑包含尾元素,不包含首元素 + +![213.打家劫舍II2](https://img-blog.csdnimg.cn/20210129160842491.jpg) + +需要注意的是,**“考虑” 不等于 “偷”**,例如情况三,虽然是考虑包含尾元素,但不一定要选尾部元素!对于情况三,取nums[1] 和 nums[3]就是最大的。 + +所以情况二 和 情况三 都包含了情况一了,**所以只考虑情况二和情况三就可以了**。 + +成环之后还是难了一些的, 不少题解没有把“考虑房间”和“偷房间”说清楚。 + +这就导致大家会有这样的困惑:“情况三怎么就包含了情况一了呢?本文图中最后一间房不能偷啊,偷了一定不是最优结果”。 + +所以我在本文重点强调了情况一二三是“考虑”的范围,而具体房间偷与不偷交给递推公式去抉择。 + +剩下的就和[动态规划:开始打家劫舍!](https://mp.weixin.qq.com/s/UZ31WdLEEFmBegdgLkJ8Dw)是一个逻辑了。 + +## 周三 + +[动态规划:还要打家劫舍!](https://mp.weixin.qq.com/s/BOJ1lHsxbQxUZffXlgglEQ)这次是在一颗二叉树上打家劫舍了,条件还是一样的,相临的不能偷。 + +这道题目是树形DP的入门题目,其实树形DP其实就是在树上进行递推公式的推导,没有什么神秘的。 + +这道题目我给出了暴力的解法: + +```C++ +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把计算过的结果保存一下,这样如果计算过孙子了,那么计算孩子的时候可以复用孙子节点的结果。 + +代码如下: + +```C++ +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. 确定递归函数的参数和返回值 + +```C++ +vector robTree(TreeNode* cur) { +``` + +dp数组含义:下标为0记录不偷该节点所得到的的最大金钱,下标为1记录偷该节点所得到的的最大金钱。 + +**所以本题dp数组就是一个长度为2的数组!** + +那么有同学可能疑惑,长度为2的数组怎么标记树中每个节点的状态呢? + +**别忘了在递归的过程中,系统栈会保存每一层递归的参数**。 + +2. 确定终止条件 + +在遍历的过程中,如果遇到空间点的话,很明显,无论偷还是不偷都是0,所以就返回 +``` +if (cur == NULL) return vector{0, 0}; +``` +3. 确定遍历顺序 + +采用后序遍历,代码如下: + +```C++ +// 下标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}; 即:{不偷当前节点得到的最大金钱,偷当前节点得到的最大金钱} + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20210129181331613.jpg) + +**最后头结点就是 取下标0 和 下标1的最大值就是偷得的最大金钱**。 + + +树形DP为什么比较难呢? + +因为平时我们习惯了在一维数组或者二维数组上推导公式,一下子换成了树,就需要对树的遍历方式足够了解! + +大家还记不记得我在讲解贪心专题的时候,讲到这道题目:[贪心算法:我要监控二叉树!](https://mp.weixin.qq.com/s/kCxlLLjWKaE6nifHC3UL2Q),这也是贪心算法在树上的应用。**那我也可以把这个算法起一个名字,叫做树形贪心**,哈哈哈 + +“树形贪心”词汇从此诞生,来自「代码随想录」 + + +## 周四 + +[动态规划:买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ) 一段时间,只能买买一次,问最大收益。 + +这里我给出了三中解法: + +暴力解法代码: +``` +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++ +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) + +动规解法,版本一,代码如下: + +```C++ +// 版本一 +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状态就可以了,可以使用滚动数组来节省空间,代码如下: + +```C++ +// 版本二 +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/周总结/20210304动规周末总结.md b/problems/周总结/20210304动规周末总结.md new file mode 100644 index 00000000..977b41e0 --- /dev/null +++ b/problems/周总结/20210304动规周末总结.md @@ -0,0 +1,204 @@ + +本周的主题就是股票系列,来一起回顾一下吧 + +## 周一 + +[动态规划:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w)中股票可以买买多了次! + +这也是和[121. 买卖股票的最佳时机](https://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)的唯一区别(注意只有一只股票,所以再次购买前要出售掉之前的股票) + +重点在于递推公式公式的不同。 + +在回顾一下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://mp.weixin.qq.com/s/keWo5qYJY4zmHn3amfXdfQ)的代码几乎一样,唯一的区别在: + +``` +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://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)中最多只能完成两笔交易。 + +**这意味着可以买卖一次,可以买卖两次,也可以不买卖**。 + + +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://img-blog.csdnimg.cn/20201228181724295.png) + +可以看到红色框为最后两次卖出的状态。 + +现在最大的时候一定是卖出的状态,而两次卖出的状态现金最大一定是最后一次卖出。 + +所以最终最大利润是dp[4][4] + +## 周三 + +[动态规划:买卖股票的最佳时机IV](https://mp.weixin.qq.com/s/jtxZJWAo2y5sUsW647Z5cw)最多可以完成 k 笔交易。 + +相对于上一道[动态规划:123.买卖股票的最佳时机III](https://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg),本题需要通过前两次的交易,来类比前k次的交易 + + +1. 确定dp数组以及下标的含义 + +使用二维数组 dp[i][j] :第i天的状态为j,所剩下的最大现金是dp[i][j] + +j的状态表示为: + +* 0 表示不操作 +* 1 第一次买入 +* 2 第一次卖出 +* 3 第二次买入 +* 4 第二次卖出 +* ..... + +**除了0以外,偶数就是卖出,奇数就是买入**。 + + +2. 确定递推公式 + +还要强调一下:dp[i][1],**表示的是第i天,买入股票的状态,并不是说一定要第i天买入股票,这是很多同学容易陷入的误区**。 + +```C++ +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://mp.weixin.qq.com/s/Sbs157mlVDtAR0gbLpdKzg)最大的区别就是这里要类比j为奇数是买,偶数是卖剩的状态**。 + +3. dp数组如何初始化 + +**dp[0][j]当j为奇数的时候都初始化为 -prices[0]** + +代码如下: + +```C++ +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://img-blog.csdnimg.cn/20201229100358221.png) + +最后一次卖出,一定是利润最大的,dp[prices.size() - 1][2 * k]即红色部分就是最后求解。 + +## 周四 + +[动态规划:最佳买卖股票时机含冷冻期](https://mp.weixin.qq.com/s/IgC0iWWCDpYL9ZbTHGHgfw)尽可能地完成更多的交易(多次买卖一支股票),但有冷冻期,冷冻期为1天 + +相对于[动态规划:122.买卖股票的最佳时机II](https://mp.weixin.qq.com/s/d4TRWFuhaY83HPa6t5ZL-w),本题加上了一个冷冻期 + + +**本题则需要第三个状态:不持有股票(冷冻期)的最多现金**。 + +动规五部曲,分析如下: + +1. 确定dp数组以及下标的含义 + +**dp[i][j],第i天状态为j,所剩的最多现金为dp[i][j]**。 + +j的状态为: + +* 1:持有股票后的最多现金 +* 2:不持有股票(能购买)的最多现金 +* 3:不持有股票(冷冻期)的最多现金 + +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了。 + +代码如下: +``` +vector> dp(n, vector(3, 0)); +``` + +**初始化其实很有讲究,很多同学可能是稀里糊涂的全都初始化0,反正就可以通过,但没有想清楚,为什么都初始化为0**。 + +4. 确定遍历顺序 + +从递归公式上可以看出,dp[i] 依赖于 dp[i-1],所以是从前向后遍历。 + +5. 举例推导dp数组 + +以 [1,2,3,0,2] 为例,dp数组如下: + +![309.最佳买卖股票时机含冷冻期](https://img-blog.csdnimg.cn/20201229163725348.png) + +最后两个状态 不持有股票(能购买) 和 不持有股票(冷冻期)都有可能最后结果,取最大的。 + +## 总结 + +下周还会有一篇股票系列的文章,**股票系列后面我也会单独写一篇总结,来高度概括一下,这样大家会对股票问题就有一个整体性的理解了**。 + + diff --git a/problems/哈希表总结.md b/problems/哈希表总结.md index edecef77..d10f934a 100644 --- a/problems/哈希表总结.md +++ b/problems/哈希表总结.md @@ -1,3 +1,11 @@ +

+ + + + +

+

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

+ > 哈希表总结篇如约而至 @@ -115,6 +123,26 @@ std::unordered_map 底层实现为哈希,std::map 和std::multimap 的底层 相信通过这个总结篇,大家可以对哈希表有一个全面的了解。 -**就酱,如果关注「代码随想录」之后收获满满,就转发给身边的同学朋友吧!** + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/哈希表理论基础.md b/problems/哈希表理论基础.md new file mode 100644 index 00000000..ba097239 --- /dev/null +++ b/problems/哈希表理论基础.md @@ -0,0 +1,152 @@ +

+ + + + +

+

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

+ + +# 哈希表 + +首先什么是 哈希表,哈希表(英文名字为Hash table,国内也有一些算法书籍翻译为散列表,大家看到这两个名称知道都是指hash table就可以了)。 + +> 哈希表是根据关键码的值而直接进行访问的数据结构。 + +这么这官方的解释可能有点懵,其实直白来讲其实数组就是一张哈希表。 + +哈希表中关键码就是数组的索引下表,然后通过下表直接访问数组中的元素,如下图所示: + +![哈希表1](https://img-blog.csdnimg.cn/20210104234805168.png) + +那么哈希表能解决什么问题呢,**一般哈希表都是用来快速判断一个元素是否出现集合里。** + +例如要查询一个名字是否在这所学校里。 + +要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1) 就可以做到。 + +我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。 + +将学生姓名映射到哈希表上就涉及到了**hash function ,也就是哈希函数**。 + +# 哈希函数 + +哈希函数,把学生的姓名直接映射为哈希表上的索引,然后就可以通过查询索引下表快速知道这位同学是否在这所学校里了。 + +哈希函数如下图所示,通过hashCode把名字转化为数值,一般hashcode是通过特定编码方式,可以将其他数据格式转化为不同的数值,这样就把学生名字映射为哈希表上的索引数字了。 + +![哈希表2](https://img-blog.csdnimg.cn/2021010423484818.png) + +如果hashCode得到的数值大于 哈希表的大小了,也就是大于tableSize了,怎么办呢? + +此时为了保证映射出来的索引数值都落在哈希表上,我们会在再次对数值做一个取模的操作,就要我们就保证了学生姓名一定可以映射到哈希表上了。 + +此时问题又来了,哈希表我们刚刚说过,就是一个数组。 + +如果学生的数量大于哈希表的大小怎么办,此时就算哈希函数计算的再均匀,也避免不了会有几位学生的名字同时映射到哈希表 同一个索引下表的位置。 + +接下来**哈希碰撞**登场 + +# 哈希碰撞 + +如图所示,小李和小王都映射到了索引下表 1的位置,**这一现象叫做哈希碰撞**。 + +![哈希表3](https://img-blog.csdnimg.cn/2021010423494884.png) + +一般哈希碰撞有两种解决方法, 拉链法和线性探测法。 + +## 拉链法 + +刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了 + +![哈希表4](https://img-blog.csdnimg.cn/20210104235015226.png) + +(数据规模是dataSize, 哈希表的大小为tableSize) + +其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。 + +## 线性探测法 + +使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。 + +例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示: + +![哈希表5](https://img-blog.csdnimg.cn/20210104235109950.png) + +其实关于哈希碰撞还有非常多的细节,感兴趣的同学可以再好好研究一下,这里我就不再赘述了。 + +# 常见的三种哈希结构 + +当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。 + +* 数组 +* set (集合) +* 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::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::unordered_map 底层实现为哈希表,std::map 和std::multimap 的底层实现是红黑树。同理,std::map 和std::multimap 的key也是有序的(这个问题也经常作为面试题,考察对语言容器底层的理解)。 + +当我们要使用集合来解决哈希问题的时候,优先使用unordered_set,因为它的查询和增删效率是最优的,如果需要集合是有序的,那么就用set,如果要求不仅有序还要有重复数据的话,那么就用multiset。 + +那么再来看一下map ,在map 是一个key value 的数据结构,map中,对key是有限制,对value没有限制的,因为key的存储方式使用红黑树实现的。 + +其他语言例如:java里的HashMap ,TreeMap 都是一样的原理。可以灵活贯通。 + +虽然std::set、std::multiset 的底层实现是红黑树,不是哈希表,但是std::set、std::multiset 依然使用哈希函数来做映射,只不过底层的符号表使用了红黑树来存储数据,所以使用这些数据结构来解决映射问题的方法,我们依然称之为哈希法。 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) + +# 总结 + +总结一下,**当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法**。 + +但是哈希法也是**牺牲了空间换取了时间**,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。 + +如果在做面试题目的时候遇到需要判断一个元素是否出现过的场景也应该第一时间想到哈希法! + +预告下篇讲解一波哈希表面试题的解题套路,我们下期见! + +都看到这了,还有sei!sei没读懂单独找我! + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/回溯总结.md b/problems/回溯总结.md index d8b9ed06..db9c9c61 100644 --- a/problems/回溯总结.md +++ b/problems/回溯总结.md @@ -1,3 +1,11 @@ +

+ + + + +

+

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

+ > 20张树形结构图、14道精选回溯题目,21篇回溯法精讲文章,由浅入深,一气呵成,这是全网最强回溯算法总结! # 回溯法理论基础 @@ -428,3 +436,22 @@ N皇后问题分析: +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/回溯算法去重问题的另一种写法.md b/problems/回溯算法去重问题的另一种写法.md index aa022631..be69b068 100644 --- a/problems/回溯算法去重问题的另一种写法.md +++ b/problems/回溯算法去重问题的另一种写法.md @@ -1,13 +1,20 @@ +

+ + + + +

+

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

-# 本周小结!(回溯算法系列三)续集 +# 回溯算法去重问题的另一种写法 -> 在 [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) 中一位录友对 整颗树的本层和同一节点的本层有疑问,也让我重新思考了一下,发现这里确实有问题,所以专门写一篇来纠正,感谢录友们的积极交流哈! +> 在 [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) 中一位录友对 整颗树的本层和同一节点的本层有疑问,也让我重新思考了一下,发现这里确实有问题,所以专门写一篇来纠正,感谢录友们的积极交流哈! 接下来我再把这块再讲一下。 在[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)中的去重和 [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ)中的去重 都是 同一父节点下本层的去重。 -[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)也可以使用set针对同一父节点本层去重,但子集问题一定要排序,为什么呢? +[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ)也可以使用set针对同一父节点本层去重,但子集问题一定要排序,为什么呢? 我用没有排序的集合{2,1,2,2}来举例子画一个图,如图: @@ -17,7 +24,7 @@ 那么下面我针对[回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) 给出使用set来对本层去重的代码实现。 -# 90.子集II +## 90.子集II used数组去重版本: [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) @@ -58,9 +65,9 @@ public: 针对留言区录友们的疑问,我再补充一些常见的错误写法, -## 错误写法一 +### 错误写法一 -把uset定义放到类成员位置,然后模拟回溯的样子 insert一次,erase一次。 +把uset定义放到类成员位置,然后模拟回溯的样子 insert一次,erase一次。 例如: @@ -77,7 +84,7 @@ private: if (uset.find(nums[i]) != uset.end()) { continue; } - uset.insert(nums[i]); // 递归之前insert + uset.insert(nums[i]); // 递归之前insert path.push_back(nums[i]); backtracking(nums, i + 1, used); path.pop_back(); @@ -95,9 +102,9 @@ private: 可以看出一旦把unordered_set uset放在类成员位置,它控制的就是整棵树,包括树枝。 -**所以这么写不行!** +**所以这么写不行!** -## 错误写法二 +### 错误写法二 有同学把 unordered_set uset; 放到类成员位置,然后每次进入单层的时候用uset.clear()。 @@ -129,7 +136,7 @@ uset已经是全局变量,本层的uset记录了一个元素,然后进入下 **组合问题和排列问题,其实也可以使用set来对同一节点下本层去重,下面我都分别给出实现代码**。 -# 40. 组合总和 II +## 40. 组合总和 II 使用used数组去重版本:[回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) @@ -153,7 +160,7 @@ private: uset.insert(candidates[i]); // 记录元素 sum += candidates[i]; path.push_back(candidates[i]); - backtracking(candidates, target, sum, i + 1); + backtracking(candidates, target, sum, i + 1); sum -= candidates[i]; path.pop_back(); } @@ -170,7 +177,7 @@ public: }; ``` -# 47. 全排列 II +## 47. 全排列 II 使用used数组去重版本:[回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA) @@ -192,7 +199,7 @@ private: continue; } if (used[i] == false) { - uset.insert(nums[i]); // 记录元素 + uset.insert(nums[i]); // 记录元素 used[i] = true; path.push_back(nums[i]); backtracking(nums, used); @@ -213,7 +220,7 @@ public: }; ``` -# 两种写法的性能分析 +## 两种写法的性能分析 需要注意的是:**使用set去重的版本相对于used数组的版本效率都要低很多**,大家在leetcode上提交,能明显发现。 @@ -223,11 +230,11 @@ public: **使用set去重,不仅时间复杂度高了,空间复杂度也高了**,在[本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA)中分析过,组合,子集,排列问题的空间复杂度都是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/tLkt9PSo42X60w8i94ViiA)的一个点做一下纠正,没想到又写出来这么多! @@ -237,9 +244,23 @@ used数组可是全局变量,每层与每层之间公用一个used数组,所 **其实这就是相互学习的过程,交流一波之后都对题目理解的更深刻了,我如果发现文中有问题,都会在评论区或者下一篇文章中即时修正,保证不会给大家带跑偏!** -就酱,「代码随想录」一直都是干货满满,公众号里的一抹清流,值得推荐给身边的每一位同学朋友! -> **我是[程序员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),关注后就会发现和「代码随想录」相见恨晚!** +## 其他语言版本 -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/回溯算法理论基础.md b/problems/回溯算法理论基础.md index d3581599..3aba34db 100644 --- a/problems/回溯算法理论基础.md +++ b/problems/回溯算法理论基础.md @@ -1,9 +1,17 @@ +

+ + + + +

+

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

-> 今天开始正式回溯法的讲解,老规矩,先概述 -# 什么是回溯法 +> 可以配合我的B站视频:[带你学透回溯算法(理论篇)](https://www.bilibili.com/video/BV1cy4y167mM/) 一起学习! -回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 +# 什么是回溯法 + +回溯法也可以叫做回溯搜索法,它是一种搜索的方式。 在二叉树系列中,我们已经不止一次,提到了回溯,例如[二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA)。 @@ -17,13 +25,13 @@ **因为回溯的本质是穷举,穷举所有可能,然后选出我们想要的答案**,如果想让回溯法高效一些,可以加一些剪枝的操作,但也改不了回溯法就是穷举的本质。 -那么既然回溯法并不高效为什么还要用它呢? +那么既然回溯法并不高效为什么还要用它呢? 因为没得选,一些问题能暴力搜出来就不错了,撑死了再剪枝一下,还没有更高效的解法。 此时大家应该好奇了,都什么问题,这么牛逼,只能暴力搜索。 -# 回溯法解决的问题 +# 回溯法解决的问题 回溯法,一般可以解决如下几种问题: @@ -55,17 +63,17 @@ 这块可能初学者还不太理解,后面的回溯算法解决的所有题目中,我都会强调这一点并画图举相应的例子,现在有一个印象就行。 -# 回溯法模板 +# 回溯法模板 这里给出Carl总结的回溯算法模板。 在讲[二叉树的递归](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)中我们说了递归三部曲,这里我再给大家列出回溯三部曲。 -* 回溯函数模板返回值以及参数 +* 回溯函数模板返回值以及参数 在回溯算法中,我的习惯是函数起名字为backtracking,这个起名大家随意。 -回溯算法中函数返回值一般为void。 +回溯算法中函数返回值一般为void。 再来看一下参数,因为回溯算法需要的参数可不像二叉树递归的时候那么容易一次性确定下来,所以一般是先写逻辑,然后需要什么参数,就填什么参数。 @@ -74,10 +82,10 @@ 回溯函数伪代码如下: ``` -void backtracking(参数) +void backtracking(参数) ``` -* 回溯函数终止条件 +* 回溯函数终止条件 既然是树形结构,那么我们在讲解[二叉树的递归](https://mp.weixin.qq.com/s/PwVIfxDlT3kRgMASWAMGhA)的时候,就知道遍历树形结构一定要有终止条件。 @@ -93,13 +101,13 @@ if (终止条件) { } ``` -* 回溯搜索的遍历过程 +* 回溯搜索的遍历过程 在上面我们提到了,回溯法一般是在集合中递归搜索,集合的大小构成了树的宽度,递归的深度构成的树的深度。 -如图: +如图: - +![回溯算法理论基础](https://img-blog.csdnimg.cn/20210130173631174.png) 注意图中,我特意举例集合大小和孩子的数量是相等的! @@ -137,7 +145,7 @@ void backtracking(参数) { ``` -**这份模板很重要,后面做回溯法的题目都靠它了!** +**这份模板很重要,后面做回溯法的题目都靠它了!** 如果从来没有学过回溯算法的录友们,看到这里会有点懵,后面开始讲解具体题目的时候就会好一些了,已经做过回溯法题目的录友,看到这里应该会感同身受了。 @@ -154,7 +162,27 @@ 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),关注后就会发现和「代码随想录」相见恨晚!** -**如果感觉对你有帮助,不要吝啬给一个👍吧!** +![](https://img-blog.csdnimg.cn/20210416110157800.png) + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/子集组合分割排列棋盘问题总结.md b/problems/子集组合分割排列棋盘问题总结.md deleted file mode 100644 index e69de29b..00000000 diff --git a/problems/字符串总结.md b/problems/字符串总结.md index 98d9b232..11e29c42 100644 --- a/problems/字符串总结.md +++ b/problems/字符串总结.md @@ -1,4 +1,11 @@ -> 该做一个总结了 +

+ + + + +

+

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

+ # 字符串:总结篇 @@ -114,3 +121,24 @@ KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易 好了字符串相关的算法知识就介绍到了这里了,明天开始新的征程,大家加油! + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/导读.md b/problems/导读.md deleted file mode 100644 index f852cd0d..00000000 --- a/problems/导读.md +++ /dev/null @@ -1,187 +0,0 @@ -很多刚刚关注「代码随想录」的录友,表示想从头开始打卡学习。 - -**打卡方式**就是在文章留言区记录:第n天打卡+自己的总结。很多录友都正在从头开始打卡,看看留言就知道了,你并不孤独,哈哈。 - -**以下是我整理的文章列表,每个系列都排好了顺序,文章顺序即刷题顺序,这是全网最详细的刷题顺序了,所以录友们挨个看就OK!** - -文章留言区的想法和总结都非常不错,大家也可以看看留言作为拓展和补充,最好同时也写一写自己的想法。 - -如果对文章有疑问,留言区一般都有相应的解答了,或者可以打卡的时候直接留言,留言的疑问我都会看到。 - -**准备好了么? 开启征程,gogogo** - -# 文章篇 - -* 编程素养 - * [看了这么多代码,谈一谈代码风格!](https://mp.weixin.qq.com/s/UR9ztxz3AyL3qdHn_zMbqw) - -* 求职 - * [程序员的简历应该这么写!!(附简历模板)](https://mp.weixin.qq.com/s/nCTUzuRTBo1_R_xagVszsA) - * [BAT级别技术面试流程和注意事项都在这里了](https://mp.weixin.qq.com/s/815qCyFGVIxwut9I_7PNFw) - * [深圳原来有这么多互联网公司,你都知道么?](https://mp.weixin.qq.com/s/Yzrkim-5bY0Df66Ao-hoqA) - * [北京有这些互联网公司,你都知道么?]() - * [上海有这些互联网公司,你都知道么?]() - * [成都有这些互联网公司,你都知道么?]() - -* 算法性能分析 - * [关于时间复杂度,你不知道的都在这里!](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/X7R55wSENyY62le0Fiawsg) - * [数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) - * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - * [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) - * [数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg) - * [数组:总结篇](https://mp.weixin.qq.com/s/LIfQFRJBH5ENTZpvixHEmg) -* 链表 - * [关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ) - * [链表:听说用虚拟头节点会方便很多?](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://mp.weixin.qq.com/s/vK0JjSTHfpAbs8evz5hH8A) - -* 哈希表 - * [关于哈希表,你该了解这些!](https://mp.weixin.qq.com/s/g8N6WmoQmsCUw3_BaWxHZA) - * [哈希表:可以拿数组当哈希表来用,但哈希值不要太大](https://mp.weixin.qq.com/s/vM6OszkM6L1Mx2Ralm9Dig) - * [哈希表:哈希值太大了,还是得用set](https://mp.weixin.qq.com/s/N9iqAchXreSVW7zXUS4BVA) - * [哈希表:今天你快乐了么?](https://mp.weixin.qq.com/s/G4Q2Zfpfe706gLK7HpZHpA) - * [哈希表:map等候多时了](https://mp.weixin.qq.com/s/uVAtjOHSeqymV8FeQbliJQ) - * [哈希表:其实需要哈希的地方都能找到map的身影](https://mp.weixin.qq.com/s/Ue8pKKU5hw_m-jPgwlHcbA) - * [哈希表:这道题目我做过?](https://mp.weixin.qq.com/s/sYZIR4dFBrw_lr3eJJnteQ) - * [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) - * [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - * [哈希表:总结篇!(每逢总结必经典)](https://mp.weixin.qq.com/s/1s91yXtarL-PkX07BfnwLg) - - -* 字符串 - * [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) - * [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw) - * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) - * [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) - * [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ) - * [字符串:KMP是时候上场了](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug) - * [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) - * [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) - * [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ) - * [字符串:前缀表不右移,难道就写不出KMP了?](https://mp.weixin.qq.com/s/p3hXynQM2RRROK5c6X7xfw) - * [字符串:总结篇!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg) - -* 双指针法 - * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) - * [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) - * [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) - * [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) - * [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) - * [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) - * [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) - * [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) - * [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) - -* 栈与队列 - * [栈与队列:来看看栈和队列不为人知的一面](https://mp.weixin.qq.com/s/VZRjOccyE09aE-MgLbCMjQ) - * [栈与队列:我用栈来实现队列怎么样?](https://mp.weixin.qq.com/s/P6tupDwRFi6Ay-L7DT4NVg) - * [栈与队列:用队列实现栈还有点别扭](https://mp.weixin.qq.com/s/yzn6ktUlL-vRG3-m5a8_Yw) - * [栈与队列:系统中处处都是栈的应用](https://mp.weixin.qq.com/s/nLlmPMsDCIWSqAtr0jbrpQ) - * [栈与队列:匹配问题都是栈的强项](https://mp.weixin.qq.com/s/eynAEbUbZoAWrk0ZlEugqg) - * [栈与队列:有没有想过计算机是如何处理表达式的?](https://mp.weixin.qq.com/s/hneh2nnLT91rR8ms2fm_kw) - * [栈与队列:滑动窗口里求最大值引出一个重要数据结构](https://mp.weixin.qq.com/s/8c6l2bO74xyMjph09gQtpA) - * [栈与队列:求前 K 个高频元素和队列有啥关系?](https://mp.weixin.qq.com/s/8hMwxoE_BQRbzCc7CA8rng) - * [栈与队列:总结篇!](https://mp.weixin.qq.com/s/xBcHyvHlWq4P13fzxEtkPg) - -* 二叉树 - * [关于二叉树,你该了解这些!](https://mp.weixin.qq.com/s/_ymfWYvTNd2GvWvC5HOE4A) - * [二叉树:一入递归深似海,从此offer是路人](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/6gY1MiXrnm-khAAJiIb5Bg) - * [本周小结!(二叉树)](https://mp.weixin.qq.com/s/JWmTeC7aKbBfGx4TY6uwuQ) - * [二叉树:我对称么?](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/QMBUTYnoaNfsVHlUADEzKg) - * [二叉树:以为使用了递归,其实还隐藏着回溯](https://mp.weixin.qq.com/s/ivLkHzWdhjQQD1rQWe6zWA) - * [二叉树:做了这么多题目了,我的左叶子之和是多少?](https://mp.weixin.qq.com/s/gBAgmmFielojU5Wx3wqFTA) - * [二叉树:我的左下角的值是多少?](https://mp.weixin.qq.com/s/MH2gbLvzQ91jHPKqiub0Nw) - * [二叉树:递归函数究竟什么时候需要返回值,什么时候不要返回值?](https://mp.weixin.qq.com/s/6TWAVjxQ34kVqROWgcRFOg) - * [二叉树:构造二叉树登场!](https://mp.weixin.qq.com/s/7r66ap2s-shvVvlZxo59xg) - * [二叉树:构造一棵最大的二叉树](https://mp.weixin.qq.com/s/1iWJV6Aov23A7xCF4nV88w) - * [本周小结!(二叉树系列三)](https://mp.weixin.qq.com/s/JLLpx3a_8jurXcz6ovgxtg) - * [二叉树:合并两个二叉树](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/n6Rk3nc_X3TSkhXHrVmBTQ) - * [本周小结!(二叉树系列四)](https://mp.weixin.qq.com/s/CbdtOTP0N-HIP7DR203tSg) - * [二叉树:搜索树的公共祖先问题](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/hZtJh4T5lIGBarY-lZJf6Q) - * [二叉树:总结篇!(需要掌握的二叉树技能都在这里了)](https://mp.weixin.qq.com/s/-ZJn3jJVdF683ap90yIj4Q) - -* 回溯算法 - * [关于回溯算法,你该了解这些!](https://mp.weixin.qq.com/s/gjSgJbNbd1eAA5WkA-HeWw) - * [回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ) - * [回溯算法:组合问题再剪剪枝](https://mp.weixin.qq.com/s/Ri7spcJMUmph4c6XjPWXQA) - * [回溯算法:求组合总和!](https://mp.weixin.qq.com/s/HX7WW6ixbFZJASkRnCTC3w) - * [回溯算法:电话号码的字母组合](https://mp.weixin.qq.com/s/e2ua2cmkE_vpYjM3j6HY0A) - * [本周小结!(回溯算法系列一)](https://mp.weixin.qq.com/s/m2GnTJdkYhAamustbb6lmw) - * [回溯算法:求组合总和(二)](https://mp.weixin.qq.com/s/FLg8G6EjVcxBjwCbzpACPw) - * [回溯算法:求组合总和(三)](https://mp.weixin.qq.com/s/_1zPYk70NvHsdY8UWVGXmQ) - * [回溯算法:分割回文串](https://mp.weixin.qq.com/s/Pb1epUTbU8fHIht-g_MS5Q) - * [回溯算法:复原IP地址](https://mp.weixin.qq.com/s/v--VmA8tp9vs4bXCqHhBuA) - * [回溯算法:求子集问题!](https://mp.weixin.qq.com/s/NNRzX-vJ_pjK4qxohd_LtA) - * [本周小结!(回溯算法系列二)](https://mp.weixin.qq.com/s/uzDpjrrMCO8DOf-Tl5oBGw) - * [回溯算法:求子集问题(二)](https://mp.weixin.qq.com/s/WJ4JNDRJgsW3eUN72Hh3uQ) - * [回溯算法:递增子序列](https://mp.weixin.qq.com/s/ePxOtX1ATRYJb2Jq7urzHQ) - * [回溯算法:排列问题!](https://mp.weixin.qq.com/s/SCOjeMX1t41wcvJq49GhMw) - * [回溯算法:排列问题(二)](https://mp.weixin.qq.com/s/9L8h3WqRP_h8LLWNT34YlA) - * [本周小结!(回溯算法系列三)](https://mp.weixin.qq.com/s/tLkt9PSo42X60w8i94ViiA) - * [本周小结!(回溯算法系列三)续集](https://mp.weixin.qq.com/s/kSMGHc_YpsqL2j-jb_E_Ag) - * [视频来了!!带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) - * [视频来了!!回溯算法:组合问题](https://mp.weixin.qq.com/s/a_r5JR93K_rBKSFplPGNAA) - * [视频来了!!回溯算法:组合问题的剪枝操作](https://mp.weixin.qq.com/s/CK0kj9lq8-rFajxL4amyEg) - * [回溯算法:重新安排行程](https://mp.weixin.qq.com/s/3kmbS4qDsa6bkyxR92XCTA) - * [回溯算法:N皇后问题](https://mp.weixin.qq.com/s/lU_QwCMj6g60nh8m98GAWg) - * [回溯算法:解数独](https://mp.weixin.qq.com/s/eWE9TapVwm77yW9Q81xSZQ) - * [一篇总结带你彻底搞透回溯算法!](https://mp.weixin.qq.com/s/r73thpBnK1tXndFDtlsdCQ) - -* 贪心算法 - * [关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg) - * [贪心算法:分发饼干](https://mp.weixin.qq.com/s/YSuLIAYyRGlyxbp9BNC1uw) - * [贪心算法:摆动序列](https://mp.weixin.qq.com/s/Xytl05kX8LZZ1iWWqjMoHA) - * [贪心算法:最大子序和](https://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg) - * [本周小结!(贪心算法系列一)](https://mp.weixin.qq.com/s/KQ2caT9GoVXgB1t2ExPncQ) - * [贪心算法:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg) - * [贪心算法:跳跃游戏](https://mp.weixin.qq.com/s/606_N9j8ACKCODoCbV1lSA) - * [贪心算法:跳跃游戏II](https://mp.weixin.qq.com/s/kJBcsJ46DKCSjT19pxrNYg) - * [贪心算法:K次取反后最大化的数组和](https://mp.weixin.qq.com/s/dMTzBBVllRm_Z0aaWvYazA) - * [本周小结!(贪心算法系列二)](https://mp.weixin.qq.com/s/RiQri-4rP9abFmq_mlXNiQ) - * [贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw) - - -(持续更新.....) - - -# 视频篇 - -* 算法 - * [带你学透KMP算法(理论篇&代码篇)](https://mp.weixin.qq.com/s/SFAs4tbo2jDgzST9AsF2xg) - * [带你学透回溯算法(理论篇)](https://mp.weixin.qq.com/s/wDd5azGIYWjbU0fdua_qBg) - * [回溯算法:组合问题](https://mp.weixin.qq.com/s/a_r5JR93K_rBKSFplPGNAA) - * [回溯算法:组合问题的剪枝操作](https://mp.weixin.qq.com/s/CK0kj9lq8-rFajxL4amyEg) -* C++ - * 听说C++ primer 太厚了 看不进去?:https://www.bilibili.com/video/BV1Z5411874t - * C++ primer 第一章,你要知道的知识点还有这些!:https://www.bilibili.com/video/BV1Kv41117Ya - * C++ primer 第二章,开讲咯!:https://www.bilibili.com/video/BV1MA411j74g - -(持续更新.....) diff --git a/problems/帮你把KMP算法学个通透.md b/problems/帮你把KMP算法学个通透.md deleted file mode 100644 index 28f8efe2..00000000 --- a/problems/帮你把KMP算法学个通透.md +++ /dev/null @@ -1,509 +0,0 @@ -> leetcode上版本 - -本题是KMP 经典题目。 - -以下文字如果看不进去,可以看我的B站视频: - -* [帮你把KMP算法学个通透!B站(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) -* [帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) - - -KMP的经典思想就是:**当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。** - -本篇将以如下顺序来讲解KMP, - -1. 什么是KMP -2. KMP可以解决什么问题 -3. 分析KMP算法里的next数组 -4. 什么是前缀表 -5. 再分析为什么要是前缀表而不是什么哈希表其他表等等,偏偏要是前缀表。 -6. 一步一步推导前缀表是怎么求的 -7. 时间复杂度分析 -8. 前缀表与next数组的关系 -9. 如何使用next数组来做一遍匹配的过程 -10. 构造next数组 -11. 使用next数组进行匹配 -12. 前缀表统一减一(右移)的KMP实现方式 -13. 前缀表不减一的KMP实现方式 -14. 总结 - -可以说步步相扣,大家要跟紧,哈哈。 - -# 什么是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.gif](https://pic.leetcode-cn.com/1599638214-EBKEhd-KMP%E7%B2%BE%E8%AE%B21.gif) - -动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说道。 - -可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。 - -但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。 - -此时就要问了**前缀表是如何记录的呢?** - -首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。 - -那么什么是前缀表:**记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** - -# 为什么一定要用前缀表 - -这就是前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢? - -回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图: - -![KMP精讲1.png](https://pic.leetcode-cn.com/1599638640-IlQSkx-image.png) - - -然后就找到了下标2,指向b,继续匹配:如图: -![KMP精讲2.png](https://pic.leetcode-cn.com/1599638670-fAVIed-image.png) - - -以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! - -**下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。** - -所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 - -**很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。** - -# 如何计算前缀表 - -接下来就要说一说怎么计算前缀表。 - -如图: -![KMP精讲5.png](https://pic.leetcode-cn.com/1599638703-pxKuJI-image.png) - - -长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。) - -![KMP精讲6.png](https://pic.leetcode-cn.com/1599638728-EedLFL-image.png) - -长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。 -![KMP精讲7.png](https://pic.leetcode-cn.com/1599638751-jYbBlh-image.png) - -长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。 - -以此类推: -长度为前4个字符的子串`aaba`,最长相同前后缀的长度为1。 -长度为前5个字符的子串`aabaa`,最长相同前后缀的长度为2。 -长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。 - -那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: -![KMP精讲8.png](https://pic.leetcode-cn.com/1599638777-OKHNjS-image.png) - - -可以看出模式串与前缀表对应位置的数字表示的就是:**下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** - -再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示: - -![KMP精讲2.gif](https://pic.leetcode-cn.com/1599638354-SppDsh-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.gif](https://pic.leetcode-cn.com/1599638403-eQrdyh-KMP%E7%B2%BE%E8%AE%B24.gif) - - -# 时间复杂度分析 - - -其中n为文本串长度,m为模式串长度,因为在匹配的过程中,根据前缀表不断调整匹配的位置,可以看出匹配的过程是O(n),之前还要单独生成next数组,时间复杂度是O(m)。所以整个KMP算法的时间复杂度是O(n+m)的。 - -暴力的解法显而易见是O(n * m),所以**KMP在字符串匹配中极大的提高的搜索的效率。** - -为了和[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)字符串命名统一,方便大家理解,以下文章统称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指向后缀终止位置(与j同理)。 - -然后还要对next数组进行初始化赋值,如下: - -``` -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开始,代码如下: - -``` -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])。 - -所以,处理前后缀不相同的情况代码如下: - -``` -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数组的函数代码如下: - -``` -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.gif](https://pic.leetcode-cn.com/1599638458-sHaHqX-KMP%E7%B2%BE%E8%AE%B23.gif) - -得到了next数组之后,就要用这个来做匹配了。 - -# 使用next数组来做匹配 - -在文本串s里 找是否出现过模式串t。 - -定义两个下标j 指向模式串起始位置,i指向文本串其实位置。 - -那么j初始值依然为-1,为什么呢? **依然因为next数组里记录的起始位置为-1。** - -i就从0开始,遍历文本串,代码如下: - -``` -for (int i = 0; i < s.size(); i++)  -``` - -接下来就是 s[i] 与 t[j + 1] (因为j从-1开始的) 经行比较。 - -如果 s[i] 与 t[j + 1] 不相同,j就要从next数组里寻找下一个匹配的位置。 - -代码如下: - -``` -while(j >= 0 && s[i] != t[j + 1]) { -    j = next[j]; -} -``` - -如果 s[i] 与 t[j + 1] 相同,那么i 和 j 同时向后移动, 代码如下: - -``` -if (s[i] == t[j + 1]) { -    j++; // i的增加在for循环里 -} -``` - -如何判断在文本串s里出现了模式串t呢,如果j指向了模式串t的末尾,那么就说明模式串t完全匹配文本串s里的某个子串了。 - -本题要在文本串字符串中找出模式串出现的第一个位置 (从0开始),所以返回当前在文本串匹配模式串的位置i 减去 模式串的长度,就是文本串字符串中出现模式串的第一个位置。 - -代码如下: - -``` -if (j == (t.size() - 1) ) { -    return (i - t.size() + 1); -} -``` - -那么使用next数组,用模式串匹配文本串的整体代码如下: - -``` -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); -    } -} -``` - -此时所有逻辑的代码都已经写出来了,本题整体代码如下: - -# 前缀表统一减一 C++代码实现 - -``` -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; - } - int next[needle.size()]; - getNext(next, 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; - } -}; - -``` - -# 前缀表(不减一)C++实现 - -那么前缀表就不减一了,也不右移的,到底行不行呢?行! - -我之前说过,这仅仅是KMP算法实现上的问题,如果就直接使用前缀表可以换一种回退方式,找j=next[j-1] 来进行回退。 - -主要就是j=next[x]这一步最为关键! - -我给出的getNext的实现为:(前缀表统一减一) - - -``` -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数组,代码如下: - -``` - 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数组也可以用来做匹配,代码要有所改动。 - -实现代码如下: - -``` -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; - } - int next[needle.size()]; - getNext(next, 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; - } -}; -``` - -# 总结 - -我们介绍了什么是KMP,KMP可以解决什么问题,然后分析KMP算法里的next数组,知道了next数组就是前缀表,再分析为什么要是前缀表而不是什么其他表。 - -接着从给出的模式串中,我们一步一步的推导出了前缀表,得出前缀表无论是统一减一还是不同意减一得到的next数组仅仅是kmp的实现方式的不同。 - -其中还分析了KMP算法的时间复杂度,并且和暴力方法做了对比。 - -然后先用前缀表统一减一得到的next数组,求得文本串s里是否出现过模式串t,并给出了具体分析代码。 - -又给出了直接用前缀表作为next数组,来做匹配的实现代码。 - -可以说把KMP的每一个细微的细节都扣了出来,毫无遮掩的展示给大家了! - -> **我是[程序员Carl](https://github.com/youngyangyang04),[组队刷题](https://img-blog.csdnimg.cn/20201115103410182.png)可以找我,本文[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/数组总结篇.md b/problems/数组总结篇.md index e4ba175b..7c4c0fed 100644 --- a/problems/数组总结篇.md +++ b/problems/数组总结篇.md @@ -1,47 +1,55 @@ -> 这个周末我们对数组做一个总结 +

+ + + + +

+

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

+ + # 数组理论基础 -数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力 +数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力 也就是说,想法很简单,但实现起来 可能就不是那么回事了。 首先要知道数组在内存中的存储方式,这样才能真正理解数组相关的面试题 -**数组是存放在连续内存空间上的相同类型数据的集合。** +**数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下表索引的方式获取到下表下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标下对应的数据。 举一个字符数组的例子,如图所示: - + -需要两点注意的是 +需要两点注意的是 -* **数组下表都是从0开始的。** -* **数组内存空间的地址是连续的** +* **数组下标都是从0开始的。** +* **数组内存空间的地址是连续的** 正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** -例如删除下表为3的元素,需要对下表为3的元素后面的所有元素都要做移动操作,如图所示: +例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: - + 而且大家如果使用C++的话,要注意vector 和 array的区别,vector的底层实现是array,严格来讲vector是容器,不是数组。 **数组的元素是不能删的,只能覆盖。** -那么二维数组直接上图,大家应该就知道怎么回事了 +那么二维数组直接上图,大家应该就知道怎么回事了 - + -**那么二维数组在内存的空间地址是连续的么?** +**那么二维数组在内存的空间地址是连续的么?** 我们来举一个例子,例如: `int[][] rating = new int[3][4];` , 这个二维数据在内存空间可不是一个 `3*4` 的连续地址空间 看了下图,就应该明白了: - + 所以**二维数据在内存中不是 `3*4` 的连续地址空间,而是四条连续的地址空间组成!** @@ -53,7 +61,7 @@ 我们之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。 -## 二分法 +## 二分法 [数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/fCf5QbPDtE6SSlZ1yh_q8Q) @@ -61,7 +69,7 @@ 可以使用暴力解法,通过这道题目,如果准求更优的算法,建议试一试用二分法,来解决这道题目 -暴力解法时间复杂度:O(n) +暴力解法时间复杂度:O(n) 二分法时间复杂度:O(logn) 在这道题目中我们讲到了**循环不变量原则**,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。 @@ -69,13 +77,13 @@ **二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力**。 -## 双指针法 +## 双指针法 * [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) -双指针法(快慢指针法):**通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** +双指针法(快慢指针法):**通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** -暴力解法时间复杂度:O(n^2) +暴力解法时间复杂度:O(n^2) 双指针时间复杂度:O(n) 这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为一下两点: @@ -85,13 +93,13 @@ 双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。 -## 滑动窗口 +## 滑动窗口 * [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) 本题介绍了数组操作中的另一个重要思想:滑动窗口。 -暴力解法时间复杂度:O(n^2) +暴力解法时间复杂度:O(n^2) 滑动窗口时间复杂度:O(n) 本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。 @@ -112,7 +120,7 @@ 相信大家又遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,踩了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实**真正解决题目的代码都是简洁的,或者有原则性的**,大家可以在这道题目中体会到这一点。 -# 总结 +# 总结 从二分法到双指针,从滑动窗口到螺旋矩阵,相信如果大家真的认真做了「代码随想录」每日推荐的题目,定会有所收获。 @@ -123,3 +131,25 @@ 最后,大家周末愉快! + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/数组理论基础.md b/problems/数组理论基础.md index c7245b2e..457cd33e 100644 --- a/problems/数组理论基础.md +++ b/problems/数组理论基础.md @@ -1,5 +1,13 @@ +

+ + + + +

+

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

-# 数组理论基础 + +## 数组理论基础 数组是非常基础的数据结构,在面试中,考察数组的题目一般在思维上都不难,主要是考察对代码的掌控能力 @@ -9,23 +17,22 @@ **数组是存放在连续内存空间上的相同类型数据的集合。** -数组可以方便的通过下表索引的方式获取到下表下对应的数据。 +数组可以方便的通过下标索引的方式获取到下标下对应的数据。 举一个字符数组的例子,如图所示: -![算法通关数组](https://img-blog.csdnimg.cn/2020121411152849.png) - +![算法通关数组](https://code-thinking.cdn.bcebos.com/pics/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%84.png) 需要两点注意的是 -* **数组下表都是从0开始的。** +* **数组下标都是从0开始的。** * **数组内存空间的地址是连续的** 正是**因为数组的在内存空间的地址是连续的,所以我们在删除或者增添元素的时候,就难免要移动其他元素的地址。** -例如删除下表为3的元素,需要对下表为3的元素后面的所有元素都要做移动操作,如图所示: +例如删除下标为3的元素,需要对下标为3的元素后面的所有元素都要做移动操作,如图所示: -![算法通关数组1](https://img-blog.csdnimg.cn/2020121411155232.png) +![算法通关数组1](https://code-thinking.cdn.bcebos.com/pics/%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是容器,不是数组。 @@ -33,20 +40,129 @@ 那么二维数组直接上图,大家应该就知道怎么回事了 -![算法通关数组2](https://img-blog.csdnimg.cn/20201214111612863.png) +![算法通关数组2](https://code-thinking.cdn.bcebos.com/pics/%E7%AE%97%E6%B3%95%E9%80%9A%E5%85%B3%E6%95%B0%E7%BB%842.png) **那么二维数组在内存的空间地址是连续的么?** -我们来举一个例子,例如: `int[][] rating = new int[3][4];` , 这个二维数据在内存空间可不是一个 `3*4` 的连续地址空间 +不同编程语言的内存管理是不一样的,以C++为例,在C++中二维数组是连续分布的,如图: -看了下图,就应该明白了: +![数组内存](https://img-blog.csdnimg.cn/20210310150641186.png) + +Java的二维数组可能是如下排列的方式: ![算法通关数组3](https://img-blog.csdnimg.cn/20201214111631844.png) -所以**二维数据在内存中不是 `3*4` 的连续地址空间,而是四条连续的地址空间组成!** +我们在[数组过于简单,但你该了解这些!](https://mp.weixin.qq.com/s/c2KABb-Qgg66HrGf8z-8Og)分别作了实验 -很多同学会以为二维数组在内存中是一片连续的地址,其实并不是。 +## 数组的经典题目 -这里面试中数组相关的理论知识就介绍完了。 +在面试中,数组是必考的基础数据结构。 -后续我将介绍面试中数组相关的五道经典面试题目,敬请期待! +其实数据的题目在思想上一般比较简单的,但是如果想高效,并不容易。 + +我们之前一共讲解了四道经典数组题目,每一道题目都代表一个类型,一种思想。 + +### 二分法 + +[704.二分查找](https://mp.weixin.qq.com/s/4X-8VRgnYRGd5LYGZ33m4w) + +在这道题目中我们讲到了**循环不变量原则**,只有在循环中坚持对区间的定义,才能清楚的把握循环中的各种细节。 + +**二分法是算法面试中的常考题,建议通过这道题目,锻炼自己手撕二分的能力**。 + +相关题目: + +* 35.搜索插入位置 +* 34.在排序数组中查找元素的第一个和最后一个位置 +* 69.x 的平方根 +* 367.有效的完全平方数 + +### 双指针法 + +[27. 移除元素](https://mp.weixin.qq.com/s/RMkulE4NIb6XsSX83ra-Ww) + +双指针法(快慢指针法):**通过一个快指针和慢指针在一个for循环下完成两个for循环的工作。** + +暴力解法时间复杂度:O(n^2) +双指针时间复杂度:O(n) + +这道题目迷惑了不少同学,纠结于数组中的元素为什么不能删除,主要是因为一下两点: + +* 数组在内存中是连续的地址空间,不能释放单一元素,如果要释放,就是全释放(程序运行结束,回收内存栈空间)。 +* C++中vector和array的区别一定要弄清楚,vector的底层实现是array,所以vector展现出友好的一些都是因为经过包装了。 + +双指针法(快慢指针法)在数组和链表的操作中是非常常见的,很多考察数组和链表操作的面试题,都使用双指针法。 + +相关题目: + +* 26.删除排序数组中的重复项 +* 283.移动零 +* 844.比较含退格的字符串 +* 977.有序数组的平方 + +### 滑动窗口 + +[209.长度最小的子数组](https://mp.weixin.qq.com/s/ewCRwVw0h0v4uJacYO7htQ) + +本题介绍了数组操作中的另一个重要思想:滑动窗口。 + +暴力解法时间复杂度:O(n^2) +滑动窗口时间复杂度:O(n) + +本题中,主要要理解滑动窗口如何移动 窗口起始位置,达到动态更新窗口大小的,从而得出长度最小的符合条件的长度。 + +**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。** + +如果没有接触过这一类的方法,很难想到类似的解题思路,滑动窗口方法还是很巧妙的。 + +相关题目: + +* 904.水果成篮 +* 76.最小覆盖子串 + +### 模拟行为 + +[59.螺旋矩阵II](https://mp.weixin.qq.com/s/Hn6-mlCPvKAdWbiFfQyaaw) + +模拟类的题目在数组中很常见,不涉及到什么算法,就是单纯的模拟,十分考察大家对代码的掌控能力。 + +在这道题目中,我们再一次介绍到了**循环不变量原则**,其实这也是写程序中的重要原则。 + +相信大家又遇到过这种情况: 感觉题目的边界调节超多,一波接着一波的判断,找边界,踩了东墙补西墙,好不容易运行通过了,代码写的十分冗余,毫无章法,其实**真正解决题目的代码都是简洁的,或者有原则性的**,大家可以在这道题目中体会到这一点。 + +相关题目: + +* 54.螺旋矩阵 +* 剑指Offer 29.顺时针打印矩阵 + +## 总结 + +从二分法到双指针,从滑动窗口到螺旋矩阵,相信如果大家真的认真做了「代码随想录」每日推荐的题目,定会有所收获。 + +**每道题目后面都有相关练习题,也别忘了去做做!** + +数组专题中讲解和相关题目已经有16道了,就不介绍太过题目了,因为数组是非常基础的数据结构后面很多专题还会用到数组,所以后面的题目依然会会间接练习数组的。 + + + +![](https://img-blog.csdnimg.cn/20210416110157800.png) + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/栈与队列总结.md b/problems/栈与队列总结.md index 81a150c5..754e84aa 100644 --- a/problems/栈与队列总结.md +++ b/problems/栈与队列总结.md @@ -1,5 +1,11 @@ +

+ + + + +

+

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

-> 学习不总结等于白学 # 栈与队列的理论基础 @@ -154,3 +160,25 @@ cd a/b/c/../../ 好了,栈与队列我们就总结到这里了,接下来Carl就要带大家开启新的篇章了,大家加油! + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/栈与队列理论基础.md b/problems/栈与队列理论基础.md new file mode 100644 index 00000000..5230fa53 --- /dev/null +++ b/problems/栈与队列理论基础.md @@ -0,0 +1,110 @@ +

+ + + + +

+

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

+ +> 来看看栈和队列不为人知的一面 + +我想栈和队列的原理大家应该很熟悉了,队列是先进先出,栈是先进后出。 + +如图所示: + +![栈与队列理论1](https://img-blog.csdnimg.cn/20210104235346563.png) + +那么我这里在列出四个关于栈的问题,大家可以思考一下。以下是以C++为例,相信使用其他编程语言的同学也对应思考一下,自己使用的编程语言里栈和队列是什么样的。 + +1. C++中stack 是容器么? +2. 我们使用的stack是属于那个版本的STL? +3. 我们使用的STL中stack是如何实现的? +4. stack 提供迭代器来遍历stack空间么? + +相信这四个问题并不那么好回答, 因为一些同学使用数据结构会停留在非常表面上的应用,稍稍往深一问,就会有好像懂,好像也不懂的感觉。 + +有的同学可能仅仅知道有栈和队列这么个数据结构,却不知道底层实现,也不清楚所使用栈和队列和STL是什么关系。 + +所以这里我在给大家扫一遍基础知识, + +首先大家要知道 栈和队列是STL(C++标准库)里面的两个数据结构。 + +C++标准库是有多个版本的,要知道我们使用的STL是哪个版本,才能知道对应的栈和队列的实现原理。 + +那么来介绍一下,三个最为普遍的STL版本: + +1. HP STL +其他版本的C++ STL,一般是以HP STL为蓝本实现出来的,HP STL是C++ STL的第一个实现版本,而且开放源代码。 + +2. P.J.Plauger STL +由P.J.Plauger参照HP STL实现出来的,被Visual C++编译器所采用,不是开源的。 + +3. SGI STL +由Silicon Graphics Computer Systems公司参照HP STL实现,被Linux的C++编译器GCC所采用,SGI STL是开源软件,源码可读性甚高。 + +接下来介绍的栈和队列也是SGI STL里面的数据结构, 知道了使用版本,才知道对应的底层实现。 + +来说一说栈,栈先进后出,如图所示: + +![栈与队列理论2](https://img-blog.csdnimg.cn/20210104235434905.png) + +栈提供push 和 pop 等等接口,所有元素必须符合先进后出规则,所以栈不提供走访功能,也不提供迭代器(iterator)。 不像是set 或者map 提供迭代器iterator来遍历所有元素。 + +**栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种容器来实现栈的功能)。** + +所以STL中栈往往不被归类为容器,而被归类为container adapter(容器适配器)。 + +那么问题来了,STL 中栈是用什么容器实现的? + +从下图中可以看出,栈的内部结构,栈的底层实现可以是vector,deque,list 都是可以的, 主要就是数组和链表的底层实现。 + +![栈与队列理论3](https://img-blog.csdnimg.cn/20210104235459376.png) + + +**我们常用的SGI STL,如果没有指定底层实现的话,默认是以deque为缺省情况下栈的低层结构。** + +deque是一个双向队列,只要封住一段,只开通另一端就可以实现栈的逻辑了。 + +**SGI STL中 队列底层实现缺省情况下一样使用deque实现的。** + +我们也可以指定vector为栈的底层实现,初始化语句如下: + +``` +std::stack > third; // 使用vector为底层容器的栈 +``` + +刚刚讲过栈的特性,对应的队列的情况是一样的。 + +队列中先进先出的数据结构,同样不允许有遍历行为,不提供迭代器, **SGI STL中队列一样是以deque为缺省情况下的底部结构。** + +也可以指定list 为起底层实现,初始化queue的语句如下: + +``` +std::queue> third; // 定义以list为底层容器的队列 +``` + +所以STL 队列也不被归类为容器,而被归类为container adapter( 容器适配器)。 + +我这里讲的都是(clck)C++ 语言中情况, 使用其他语言的同学也要思考栈与队列的底层实现问题, 不要对数据结构的使用浅尝辄止,而要深挖起内部原理,才能夯实基础。 + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/根据身高重建队列(vector原理讲解).md b/problems/根据身高重建队列(vector原理讲解).md new file mode 100644 index 00000000..e7dac2ad --- /dev/null +++ b/problems/根据身高重建队列(vector原理讲解).md @@ -0,0 +1,183 @@ +

+ + + + +

+

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

+# 贪心算法:根据身高重建队列(续集) + +在讲解[贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw)中,我们提到了使用vector(C++中的动态数组)来进行insert操作是费时的。 + +但是在解释的过程中有不恰当的地方,所以来专门写一篇文章来详细说一说这个问题。 + +使用vector的代码如下: +```C++ +// 版本一,使用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://img-blog.csdnimg.cn/20201218203611181.png) + +其直观上来看数组的insert操作是O(n)的,整体代码的时间复杂度是O(n^2)。 + +这么一分析好像和版本二链表实现的时间复杂度是一样的啊,为什么提交之后效率会差距这么大呢? +```C++ +// 版本二,使用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://img-blog.csdnimg.cn/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://img-blog.csdnimg.cn/20201218185902217.png) + +原vector中的size和capicity相同都是3,初始化为1 2 3,此时要push_back一个元素4。 + +那么底层其实就要申请一个大小为6的普通数组,并且把原元素拷贝过去,释放原数组内存,**注意图中底层数组的内存起始地址已经变了**。 + +**同时也注意此时capicity和size的变化,关键的地方我都标红了**。 + +而在[贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw)中,我们使用vector来做insert的操作,此时大家可会发现,**虽然表面上复杂度是O(n^2),但是其底层都不知道额外做了多少次全量拷贝了,所以算上vector的底层拷贝,整体时间复杂度可以认为是O(n^2 + t * n)级别的,t是底层拷贝的次数**。 + +那么是不是可以直接确定好vector的大小,不让它在动态扩容了,例如在[贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw)中已经给出了有people.size这么多的人,可以定义好一个固定大小的vector,这样我们就可以控制vector,不让它底层动态扩容。 + +这种方法需要自己模拟插入的操作,不仅没有直接调用insert接口那么方便,需要手动模拟插入操作,而且效率也不高! + +手动模拟的过程其实不是很简单的,需要很多细节,我粗略写了一个版本,如下: + +```C++ +// 版本三 +// 使用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://img-blog.csdnimg.cn/20201218200626718.png) + +这份代码就是不让vector动态扩容,全程我们自己模拟insert的操作,大家也可以直观的看出是一个O(n^2)的方法了。 + +但这份代码在leetcode上统计的耗时甚至比版本一的还高,我们都不让它动态扩容了,为什么耗时更高了呢? + +一方面是leetcode的耗时统计本来就不太准,忽高忽低的,只能测个大概。 + +另一方面:可能是就算避免的vector的底层扩容,但这个固定大小的数组,每次向后移动元素赋值的次数比方法一中移动赋值的次数要多很多。 + +因为方法一中一开始数组是很小的,插入操作,向后移动元素次数比较少,即使有偶尔的扩容操作。而方法三每次都是按照最大数组规模向后移动元素的。 + +所以对于两种使用数组的方法一和方法三,也不好确定谁优,但一定都没有使用方法二链表的效率高! + +一波分析之后,对于[贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw) ,大家就安心使用链表吧!别折腾了,哈哈,相当于我替大家折腾了一下。 + +## 总结 + +大家应该发现了,编程语言中一个普通容器的insert,delete的使用,都可能对写出来的算法的有很大影响! + +如果抛开语言谈算法,除非从来不用代码写算法纯分析,**否则的话,语言功底不到位O(n)的算法可以写出O(n^2)的性能**,哈哈。 + +相信在这里学习算法的录友们,都是想在软件行业长远发展的,都是要从事编程的工作,那么一定要深耕好一门编程语言,这个非常重要! + + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/算法模板.md b/problems/算法模板.md index 60046b68..133c798d 100644 --- a/problems/算法模板.md +++ b/problems/算法模板.md @@ -1,3 +1,10 @@ +

+ + + + +

+

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

## 二分查找法 @@ -75,7 +82,7 @@ void traversal(TreeNode* cur, vector& vec) { traversal(cur->right, vec); // 右 } ``` -中序遍历(中左右) +前序遍历(中左右) ``` void traversal(TreeNode* cur, vector& vec) { if (cur == NULL) return; @@ -238,4 +245,55 @@ void backtracking(参数) { ``` +## 并查集 + +``` + int n = 1005; // 更具题意而定 + int father[1005]; + + // 并查集初始化 + 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]); + } + // 将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; + } +``` + + (持续补充ing) +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/背包总结篇.md b/problems/背包总结篇.md new file mode 100644 index 00000000..16b84ba5 --- /dev/null +++ b/problems/背包总结篇.md @@ -0,0 +1,116 @@ +

+ + + + +

+

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

+# 听说背包问题很难? 这篇总结篇来拯救你了 + +年前我们已经把背包问题都讲完了,那么现在我们要对背包问题进行总结一番。 + +背包问题是动态规划里的非常重要的一部分,所以我把背包问题单独总结一下,等动态规划专题更新完之后,我们还会在整体总结一波动态规划。 + +关于这几种常见的背包,其关系如下: + +![416.分割等和子集1](https://img-blog.csdnimg.cn/20210117171307407.png) + +通过这个图,可以很清晰分清这几种常见背包之间的关系。 + +在讲解背包问题的时候,我们都是按照如下五部来逐步分析,相信大家也体会到,把这五部都搞透了,算是对动规来理解深入了。 + +1. 确定dp数组(dp table)以及下标的含义 +2. 确定递推公式 +3. dp数组如何初始化 +4. 确定遍历顺序 +5. 举例推导dp数组 + +**其实这五部里哪一步都很关键,但确定递推公式和确定遍历顺序都具有规律性和代表性,所以下面我从这两点来对背包问题做一做总结**。 + +## 背包递推公式 + +问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]); ,对应题目如下: +* [动态规划:416.分割等和子集](https://mp.weixin.qq.com/s/sYw3QtPPQ5HMZCJcT4EaLQ) +* [动态规划:1049.最后一块石头的重量 II](https://mp.weixin.qq.com/s/WbwAo3jaUaNJjvhHgq0BGg) + +问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下: +* [动态规划:494.目标和](https://mp.weixin.qq.com/s/2pWmaohX75gwxvBENS-NCw) +* [动态规划:518. 零钱兑换 II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) +* [动态规划:377.组合总和Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA) +* [动态规划:70. 爬楼梯进阶版(完全背包)](https://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA) + +问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下: +* [动态规划:474.一和零](https://mp.weixin.qq.com/s/x-u3Dsp76DlYqtCe0xEKJw) + +问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下: +* [动态规划:322.零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ) +* [动态规划:279.完全平方数](https://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ) + + +## 遍历顺序 + +### 01背包 + +在[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。 + +和[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。 + +**一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!** + +### 完全背包 + +说完01背包,再看看完全背包。 + +在[动态规划:关于完全背包,你该了解这些!](https://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw)中,讲解了纯完全背包的一维dp数组实现,先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。 + +但是仅仅是纯完全背包的遍历顺序是这样的,题目稍有变化,两个for循环的先后顺序就不一样了。 + +**如果求组合数就是外层for循环遍历物品,内层for遍历背包**。 + +**如果求排列数就是外层for遍历背包,内层for循环遍历物品**。 + +相关题目如下: + +* 求组合数:[动态规划:518.零钱兑换II](https://mp.weixin.qq.com/s/PlowDsI4WMBOzf3q80AksQ) +* 求排列数:[动态规划:377. 组合总和 Ⅳ](https://mp.weixin.qq.com/s/Iixw0nahJWQgbqVNk8k6gA)、[动态规划:70. 爬楼梯进阶版(完全背包)](https://mp.weixin.qq.com/s/e_wacnELo-2PG76EjrUakA) + +如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下: + +* 求最小数:[动态规划:322. 零钱兑换](https://mp.weixin.qq.com/s/dyk-xNilHzNtVdPPLObSeQ)、[动态规划:279.完全平方数](https://mp.weixin.qq.com/s/VfJT78p7UGpDZsapKF_QJQ) + + +**对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了**。 + + +## 总结 + +**这篇背包问题总结篇是对背包问题的高度概括,讲最关键的两部:递推公式和遍历顺序,结合力扣上的题目全都抽象出来了**。 + +**而且每一个点,我都给出了对应的力扣题目**。 + +最后如果你想了解多重背包,可以看这篇[动态规划:关于多重背包,你该了解这些!](https://mp.weixin.qq.com/s/b-UUUmbvG7URWyCjQkiuuQ),力扣上还没有多重背包的题目,也不是面试考察的重点。 + +如果把我本篇总结出来的内容都掌握的话,可以说对背包问题理解的就很深刻了,用来对付面试中的背包问题绰绰有余! + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/背包理论基础01背包-1.md b/problems/背包理论基础01背包-1.md new file mode 100644 index 00000000..8055d481 --- /dev/null +++ b/problems/背包理论基础01背包-1.md @@ -0,0 +1,314 @@ +

+ + + + +

+

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

+# 动态规划:关于01背包问题,你该了解这些! + +这周我们正式开始讲解背包问题! + +背包问题的经典资料当然是:背包九讲。在公众号「代码随想录」后台回复:背包九讲,就可以获得背包九讲的PDF。 + +但说实话,背包九讲对于小白来说确实不太友好,看起来还是有点费劲的,而且都是伪代码理解起来也吃力。 + +对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。 + +如果这几种背包,分不清,我这里画了一个图,如下: + +![416.分割等和子集1](https://img-blog.csdnimg.cn/20210117171307407.png) + + +至于背包九讲其其他背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。 + +而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。 + +**所以背包问题的理论基础重中之重是01背包,一定要理解透!** + +leetcode上没有纯01背包的问题,都是01背包应用方面的题目,也就是需要转化为01背包问题。 + +**所以我先通过纯01背包问题,把01背包原理讲清楚,后续再讲解leetcode题目的时候,重点就是讲解如何转化为01背包问题了**。 + +之前可能有些录友已经可以熟练写出背包了,但只要把这个文章仔细看完,相信你会意外收获! + +## 01 背包 + +有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。 + +![动态规划-背包问题](https://img-blog.csdnimg.cn/20210117175428387.jpg) + +这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。 + +这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢? + +每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。 + +**所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!** + +在下面的讲解中,我举一个例子: + +背包最大重量为4。 + +物品为: + +| | 重量 | 价值 | +| --- | --- | --- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +问背包能背的物品最大价值是多少? + +以下讲解和图示中出现的数字都是以这个例子为例。 + +## 二维dp数组01背包 + +依然动规五部曲分析一波。 + +1. 确定dp数组以及下标的含义 + +对于背包问题,有一种写法, 是使用二维数组,即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。 + +只看这个二维数组的定义,大家一定会有点懵,看下面这个图: + +![动态规划-背包问题1](https://img-blog.csdnimg.cn/20210110103003361.png) + +**要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的**,如果哪里看懵了,就来回顾一下i代表什么,j又代表什么。 + +2. 确定递推公式 + +再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 + +那么可以有两个方向推出来dp[i][j], + +* 由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j] +* 由dp[i - 1][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://img-blog.csdnimg.cn/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的物品的时候,各个容量的背包所能存放的最大价值。 + +代码如下: + +``` +// 倒叙遍历 +for (int j = bagWeight; j >= weight[0]; j--) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; // 初始化i为0时候的情况 +} +``` + +**大家应该发现,这个初始化为什么是倒叙的遍历的?正序遍历就不行么?** + +正序遍历还真就不行,dp[0][j]表示容量为j的背包存放物品0时候的最大价值,物品0的价值就是15,因为题目中说了**每个物品只有一个!**所以dp[0][j]如果不是初始值的话,就应该都是物品0的价值,也就是15。 + +但如果一旦正序遍历了,那么物品0就会被重复加入多次! 例如代码如下: +``` +// 正序遍历 +for (int j = weight[0]; j <= bagWeight; j++) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; +} +``` + +例如dp[0][1] 是15,到了dp[0][2] = dp[0][2 - 1] + 15; 也就是dp[0][2] = 30 了,那么就是物品0被重复放入了。 + +**所以一定要倒叙遍历,保证物品0只被放入一次!这一点对01背包很重要,后面在讲解滚动数组的时候,还会用到倒叙遍历来保证物品使用一次!** + + +此时dp数组初始化情况如图所示: + +![动态规划-背包问题7](https://img-blog.csdnimg.cn/20210110103109140.png) + +dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢? + + +dp[i][j]在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,因为0就是最小的了,不会影响取最大价值的结果。 + +如果题目给的价值有负数,那么非0下标就要初始化为负无穷了。例如:一个物品的价值是-2,但对应的位置依然初始化为0,那么取最大值的时候,就会取0而不是-2了,所以要初始化为负无穷。 + +**这样才能让dp数组在递归公式的过程中取最大的价值,而不是被初始值覆盖了**。 + +最后初始化代码如下: + +``` +// 初始化 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]; +} +``` + +**费了这么大的功夫,才把如何初始化讲清楚,相信不少同学平时初始化dp数组是凭感觉来的,但有时候感觉是不靠谱的**。 + +4. 确定遍历顺序 + + +在如下图中,可以看出,有两个遍历的维度:物品与背包重量 + +![动态规划-背包问题3](https://img-blog.csdnimg.cn/2021011010314055.png) + +那么问题来了,**先遍历 物品还是先遍历背包重量呢?** + +**其实都可以!! 但是先遍历物品更好理解**。 + +那么我先给出先遍历物品,然后遍历背包重量的代码。 + +``` +// 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]); + + } +} +``` + +**先遍历背包,再遍历物品,也是可以的!(注意我这里使用的二维dp数组)** + +例如这样: + +``` +// 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://img-blog.csdnimg.cn/202101101032124.png) + +再来看看先遍历背包,再遍历物品呢,如图: + +![动态规划-背包问题6](https://img-blog.csdnimg.cn/20210110103244701.png) + +**大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!** + +但先遍历物品再遍历背包这个顺序更好理解。 + +**其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了**。 + +5. 举例推导dp数组 + +来看一下对应的dp数组的数值,如图: + +![动态规划-背包问题4](https://img-blog.csdnimg.cn/20210118163425129.jpg) + +最终结果就是dp[2][4]。 + +建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。 + +**做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!** + +很多同学做dp题目,遇到各种问题,然后凭感觉东改改西改改,怎么改都不对,或者稀里糊涂就改过了。 + +主要就是自己没有动手推导一下dp数组的演变过程,如果推导明白了,代码写出来就算有问题,只要把dp数组打印出来,对比一下和自己推导的有什么差异,很快就可以发现问题了。 + + +## 完整C++测试代码 + +```C++ +void test_2_wei_bag_problem1() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int bagWeight = 4; + + // 二维数组 + 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]; + } + + // 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]); + + } + } + + cout << dp[weight.size() - 1][bagWeight] << endl; +} + +int main() { + test_2_wei_bag_problem1(); +} + +``` + + +以上遍历的过程也可以这么写: + +``` +// 遍历过程 +for(int i = 1; i < weight.size(); i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j - weight[i] >= 0) { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } +} +``` + +这么写打印出来的dp数据这就是这样: + +![动态规划-背包问题8](https://img-blog.csdnimg.cn/2021011010344372.png) + +空出来的0其实是用不上的,版本一 能把完整的dp数组打印出来,出来我用版本一来讲解。 + + +## 总结 + +讲了这么多才刚刚把二维dp的01背包讲完,**这里大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在如何初始化和遍历顺序上**。 + +可能有的同学并没有注意到初始化 和 遍历顺序的重要性,我们后面做力扣上背包面试题目的时候,大家就会感受出来了。 + +下一篇 还是理论基础,我们再来讲一维dp数组实现的01背包(滚动数组),分析一下和二维有什么区别,在初始化和遍历顺序上又有什么差异,敬请期待! + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/背包理论基础01背包-2.md b/problems/背包理论基础01背包-2.md new file mode 100644 index 00000000..f5c747c7 --- /dev/null +++ b/problems/背包理论基础01背包-2.md @@ -0,0 +1,230 @@ +

+ + + + +

+

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

+# 动态规划:关于01背包问题,你该了解这些!(滚动数组) + +昨天[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)中是用二维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数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 + +2. 一维dp数组的递推公式 + +dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢? + +dp[j]可以通过dp[j - weight[j]]推导出来,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[j - weight[i]] + value[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就可以了,如果题目给的价值有负数,那么非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只被放入一次!**,在[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)中讲解二维dp数组初始化dp[0][j]时候已经讲解到过一次。 + +举一个例子:物品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[j]的定义,或者就把两个for循环顺序颠倒一下试试!) + +**所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!**,这一点大家一定要注意。 + +5. 举例推导dp数组 + +一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下: + +![动态规划-背包问题9](https://img-blog.csdnimg.cn/20210110103614769.png) + + + +## 一维dp01背包完整C++测试代码 + +``` +void test_1_wei_bag_problem() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int bagWeight = 4; + + // 初始化 + vector dp(bagWeight + 1, 0); + 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]); + } + } + cout << dp[bagWeight] << endl; +} + +int main() { + test_1_wei_bag_problem(); +} + +``` + +可以看出,一维dp 的01背包,要比二维简洁的多! 初始化 和 遍历顺序相对简单了。 + +**所以我倾向于使用一维dp数组的写法,比较直观简洁,而且空间复杂度还降了一个数量级!** + +**在后面背包问题的讲解中,我都直接使用一维dp数组来进行推导**。 + +## 总结 + +以上的讲解可以开发一道面试题目(毕竟力扣上没原题)。 + +就是本文中的题目,要求先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。 + +然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么? + +注意以上问题都是在候选人把代码写出来的情况下才问的。 + +就是纯01背包的题目,都不用考01背包应用类的题目就可以看出候选人对算法的理解程度了。 + +**相信大家读完这篇文章,应该对以上问题都有了答案!** + +此时01背包理论基础就讲完了,我用了两篇文章把01背包的dp数组定义、递推公式、初始化、遍历顺序从二维数组到一维数组统统深度剖析了一遍,没有放过任何难点。 + +大家可以发现其实信息量还是挺大的。 + +如果把[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)和本篇的内容都理解了,后面我们在做01背包的题目,就会发现非常简单了。 + +不用再凭感觉或者记忆去写背包,而是有自己的思考,了解其本质,代码的方方面面都在自己的掌控之中。 + +即使代码没有通过,也会有自己的逻辑去debug,这样就思维清晰了。 + +接下来就要开始用这两天的理论基础去做力扣上的背包面试题目了,录友们握紧扶手,我们要上高速啦! + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/背包理论基础01背包-一维DP.md b/problems/背包理论基础01背包-一维DP.md new file mode 100644 index 00000000..48be4182 --- /dev/null +++ b/problems/背包理论基础01背包-一维DP.md @@ -0,0 +1,231 @@ +

+ + + + +

+

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

+ +# 动态规划:关于01背包问题,你该了解这些!(滚动数组) + +昨天[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)中是用二维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数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j]。 + +2. 一维dp数组的递推公式 + +dp[j]为 容量为j的背包所背的最大价值,那么如何推导dp[j]呢? + +dp[j]可以通过dp[j - weight[j]]推导出来,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[j - weight[i]] + value[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就可以了,如果题目给的价值有负数,那么非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只被放入一次!**,在[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)中讲解二维dp数组初始化dp[0][j]时候已经讲解到过一次。 + +举一个例子:物品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[j]的定义,或者就把两个for循环顺序颠倒一下试试!) + +**所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!**,这一点大家一定要注意。 + +5. 举例推导dp数组 + +一维dp,分别用物品0,物品1,物品2 来遍历背包,最终得到结果如下: + +![动态规划-背包问题9](https://img-blog.csdnimg.cn/20210110103614769.png) + + + +## 一维dp01背包完整C++测试代码 + +``` +void test_1_wei_bag_problem() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int bagWeight = 4; + + // 初始化 + vector dp(bagWeight + 1, 0); + 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]); + } + } + cout << dp[bagWeight] << endl; +} + +int main() { + test_1_wei_bag_problem(); +} + +``` + +可以看出,一维dp 的01背包,要比二维简洁的多! 初始化 和 遍历顺序相对简单了。 + +**所以我倾向于使用一维dp数组的写法,比较直观简洁,而且空间复杂度还降了一个数量级!** + +**在后面背包问题的讲解中,我都直接使用一维dp数组来进行推导**。 + +## 总结 + +以上的讲解可以开发一道面试题目(毕竟力扣上没原题)。 + +就是本文中的题目,要求先实现一个纯二维的01背包,如果写出来了,然后再问为什么两个for循环的嵌套顺序这么写?反过来写行不行?再讲一讲初始化的逻辑。 + +然后要求实现一个一维数组的01背包,最后再问,一维数组的01背包,两个for循环的顺序反过来写行不行?为什么? + +注意以上问题都是在候选人把代码写出来的情况下才问的。 + +就是纯01背包的题目,都不用考01背包应用类的题目就可以看出候选人对算法的理解程度了。 + +**相信大家读完这篇文章,应该对以上问题都有了答案!** + +此时01背包理论基础就讲完了,我用了两篇文章把01背包的dp数组定义、递推公式、初始化、遍历顺序从二维数组到一维数组统统深度剖析了一遍,没有放过任何难点。 + +大家可以发现其实信息量还是挺大的。 + +如果把[动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w)和本篇的内容都理解了,后面我们在做01背包的题目,就会发现非常简单了。 + +不用再凭感觉或者记忆去写背包,而是有自己的思考,了解其本质,代码的方方面面都在自己的掌控之中。 + +即使代码没有通过,也会有自己的逻辑去debug,这样就思维清晰了。 + +接下来就要开始用这两天的理论基础去做力扣上的背包面试题目了,录友们握紧扶手,我们要上高速啦! + +就酱,学算法,认准「代码随想录」,值得你推荐给身边每一位朋友同学们。 + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/背包理论基础01背包-二维DP.md b/problems/背包理论基础01背包-二维DP.md new file mode 100644 index 00000000..b504daa6 --- /dev/null +++ b/problems/背包理论基础01背包-二维DP.md @@ -0,0 +1,318 @@ +

+ + + + +

+

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

+ +# 背包问题理论基础 + + +这周我们正式开始讲解背包问题! + +背包问题的经典资料当然是:背包九讲。在公众号「代码随想录」后台回复:背包九讲,就可以获得背包九讲的PDF。 + +但说实话,背包九讲对于小白来说确实不太友好,看起来还是有点费劲的,而且都是伪代码理解起来也吃力。 + +对于面试的话,其实掌握01背包,和完全背包,就够用了,最多可以再来一个多重背包。 + +如果这几种背包,分不清,我这里画了一个图,如下: + +![416.分割等和子集1](https://img-blog.csdnimg.cn/20210117171307407.png) + +至于背包九讲其其他背包,面试几乎不会问,都是竞赛级别的了,leetcode上连多重背包的题目都没有,所以题库也告诉我们,01背包和完全背包就够用了。 + +而完全背包又是也是01背包稍作变化而来,即:完全背包的物品数量是无限的。 + +**所以背包问题的理论基础重中之重是01背包,一定要理解透!** + +leetcode上没有纯01背包的问题,都是01背包应用方面的题目,也就是需要转化为01背包问题。 + +**所以我先通过纯01背包问题,把01背包原理讲清楚,后续再讲解leetcode题目的时候,重点就是讲解如何转化为01背包问题了**。 + +之前可能有些录友已经可以熟练写出背包了,但只要把这个文章仔细看完,相信你会意外收获! + +## 01 背包 + +有N件物品和一个最多能被重量为W 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品只能用一次**,求解将哪些物品装入背包里物品价值总和最大。 + +![动态规划-背包问题](https://img-blog.csdnimg.cn/20210117175428387.jpg) + +这是标准的背包问题,以至于很多同学看了这个自然就会想到背包,甚至都不知道暴力的解法应该怎么解了。 + +这样其实是没有从底向上去思考,而是习惯性想到了背包,那么暴力的解法应该是怎么样的呢? + +每一件物品其实只有两个状态,取或者不取,所以可以使用回溯法搜索出所有的情况,那么时间复杂度就是O(2^n),这里的n表示物品数量。 + +**所以暴力的解法是指数级别的时间复杂度。进而才需要动态规划的解法来进行优化!** + +在下面的讲解中,我举一个例子: + +背包最大重量为4。 + +物品为: + +| | 重量 | 价值 | +| --- | --- | --- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +问背包能背的物品最大价值是多少? + +以下讲解和图示中出现的数字都是以这个例子为例。 + +## 二维dp数组01背包 + +依然动规五部曲分析一波。 + +1. 确定dp数组以及下标的含义 + +对于背包问题,有一种写法, 是使用二维数组,即**dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少**。 + +只看这个二维数组的定义,大家一定会有点懵,看下面这个图: + +![动态规划-背包问题1](https://img-blog.csdnimg.cn/20210110103003361.png) + +**要时刻记着这个dp数组的含义,下面的一些步骤都围绕这dp数组的含义进行的**,如果哪里看懵了,就来回顾一下i代表什么,j又代表什么。 + +2. 确定递推公式 + +再回顾一下dp[i][j]的含义:从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。 + +那么可以有两个方向推出来dp[i][j], + +* 由dp[i - 1][j]推出,即背包容量为j,里面不放物品i的最大价值,此时dp[i][j]就是dp[i - 1][j] +* 由dp[i - 1][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://img-blog.csdnimg.cn/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的物品的时候,各个容量的背包所能存放的最大价值。 + +代码如下: + +``` +// 倒叙遍历 +for (int j = bagWeight; j >= weight[0]; j--) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; // 初始化i为0时候的情况 +} +``` + +**大家应该发现,这个初始化为什么是倒叙的遍历的?正序遍历就不行么?** + +正序遍历还真就不行,dp[0][j]表示容量为j的背包存放物品0时候的最大价值,物品0的价值就是15,因为题目中说了**每个物品只有一个!**所以dp[0][j]如果不是初始值的话,就应该都是物品0的价值,也就是15。 + +但如果一旦正序遍历了,那么物品0就会被重复加入多次! 例如代码如下: +``` +// 正序遍历 +for (int j = weight[0]; j <= bagWeight; j++) { + dp[0][j] = dp[0][j - weight[0]] + value[0]; +} +``` + +例如dp[0][1] 是15,到了dp[0][2] = dp[0][2 - 1] + 15; 也就是dp[0][2] = 30 了,那么就是物品0被重复放入了。 + +**所以一定要倒叙遍历,保证物品0只被放入一次!这一点对01背包很重要,后面在讲解滚动数组的时候,还会用到倒叙遍历来保证物品使用一次!** + + +此时dp数组初始化情况如图所示: + +![动态规划-背包问题7](https://img-blog.csdnimg.cn/20210110103109140.png) + +dp[0][j] 和 dp[i][0] 都已经初始化了,那么其他下标应该初始化多少呢? + + +dp[i][j]在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了,因为0就是最小的了,不会影响取最大价值的结果。 + +如果题目给的价值有负数,那么非0下标就要初始化为负无穷了。例如:一个物品的价值是-2,但对应的位置依然初始化为0,那么取最大值的时候,就会取0而不是-2了,所以要初始化为负无穷。 + +**这样才能让dp数组在递归公式的过程中取最大的价值,而不是被初始值覆盖了**。 + +最后初始化代码如下: + +``` +// 初始化 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]; +} +``` + +**费了这么大的功夫,才把如何初始化讲清楚,相信不少同学平时初始化dp数组是凭感觉来的,但有时候感觉是不靠谱的**。 + +4. 确定遍历顺序 + + +在如下图中,可以看出,有两个遍历的维度:物品与背包重量 + +![动态规划-背包问题3](https://img-blog.csdnimg.cn/2021011010314055.png) + +那么问题来了,**先遍历 物品还是先遍历背包重量呢?** + +**其实都可以!! 但是先遍历物品更好理解**。 + +那么我先给出先遍历物品,然后遍历背包重量的代码。 + +``` +// 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]); + + } +} +``` + +**先遍历背包,再遍历物品,也是可以的!(注意我这里使用的二维dp数组)** + +例如这样: + +``` +// 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://img-blog.csdnimg.cn/202101101032124.png) + +再来看看先遍历背包,再遍历物品呢,如图: + +![动态规划-背包问题6](https://img-blog.csdnimg.cn/20210110103244701.png) + +**大家可以看出,虽然两个for循环遍历的次序不同,但是dp[i][j]所需要的数据就是左上角,根本不影响dp[i][j]公式的推导!** + +但先遍历物品再遍历背包这个顺序更好理解。 + +**其实背包问题里,两个for循环的先后循序是非常有讲究的,理解遍历顺序其实比理解推导公式难多了**。 + +5. 举例推导dp数组 + +来看一下对应的dp数组的数值,如图: + +![动态规划-背包问题4](https://img-blog.csdnimg.cn/20210118163425129.jpg) + +最终结果就是dp[2][4]。 + +建议大家此时自己在纸上推导一遍,看看dp数组里每一个数值是不是这样的。 + +**做动态规划的题目,最好的过程就是自己在纸上举一个例子把对应的dp数组的数值推导一下,然后在动手写代码!** + +很多同学做dp题目,遇到各种问题,然后凭感觉东改改西改改,怎么改都不对,或者稀里糊涂就改过了。 + +主要就是自己没有动手推导一下dp数组的演变过程,如果推导明白了,代码写出来就算有问题,只要把dp数组打印出来,对比一下和自己推导的有什么差异,很快就可以发现问题了。 + + +## 完整C++测试代码 + +```C++ +void test_2_wei_bag_problem1() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int bagWeight = 4; + + // 二维数组 + 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]; + } + + // 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]); + + } + } + + cout << dp[weight.size() - 1][bagWeight] << endl; +} + +int main() { + test_2_wei_bag_problem1(); +} + +``` + + +以上遍历的过程也可以这么写: + +``` +// 遍历过程 +for(int i = 1; i < weight.size(); i++) { // 遍历物品 + for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量 + if (j - weight[i] >= 0) { + dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]); + } + } +} +``` + +这么写打印出来的dp数据这就是这样: + +![动态规划-背包问题8](https://img-blog.csdnimg.cn/2021011010344372.png) + +空出来的0其实是用不上的,版本一 能把完整的dp数组打印出来,出来我用版本一来讲解。 + + +## 总结 + +讲了这么多才刚刚把二维dp的01背包讲完,**这里大家其实可以发现最简单的是推导公式了,推导公式估计看一遍就记下来了,但难就难在如何初始化和遍历顺序上**。 + +可能有的同学并没有注意到初始化 和 遍历顺序的重要性,我们后面做力扣上背包面试题目的时候,大家就会感受出来了。 + +下一篇 还是理论基础,我们再来讲一维dp数组实现的01背包(滚动数组),分析一下和二维有什么区别,在初始化和遍历顺序上又有什么差异,敬请期待! + +就酱,学算法,认准「代码随想录」,值得推荐给身边的朋友同学们,关注后都会发现相见恨晚! + + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/背包问题理论基础多重背包.md b/problems/背包问题理论基础多重背包.md new file mode 100644 index 00000000..1f0ad4c0 --- /dev/null +++ b/problems/背包问题理论基础多重背包.md @@ -0,0 +1,163 @@ +

+ + + + +

+

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

+# 动态规划:关于多重背包,你该了解这些! + +之前我们已经体统的讲解了01背包和完全背包,如果没有看过的录友,建议先把如下三篇文章仔细阅读一波。 + +* [动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA) +* [动态规划:关于完全背包,你该了解这些!](https://mp.weixin.qq.com/s/akwyxlJ4TLvKcw26KB9uJw) + +这次我们再来说一说多重背包 + +## 多重背包 + +对于多重背包,我在力扣上还没发现对应的题目,所以这里就做一下简单介绍,大家大概了解一下。 + +有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背包问题了,且每个物品只用一次。 + +这种方式来实现多重背包的代码如下: + + +```C++ +void test_multi_pack() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + vector nums = {2, 3, 2}; + int bagWeight = 10; + for (int i = 0; i < nums.size(); i++) { + while (nums[i] > 1) { // 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++) { // 遍历物品 + for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量 + dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); + } + for (int j = 0; j <= bagWeight; j++) { + cout << dp[j] << " "; + } + cout << endl; + } + cout << dp[bagWeight] << endl; + +} +int main() { + test_multi_pack(); +} + +``` + +* 时间复杂度:O(m * n * k) m:物品种类个数,n背包容量,k单类物品数量 + +也有另一种实现方式,就是把每种商品遍历的个数放在01背包里面在遍历一遍。 + +代码如下:(详看注释) + + +```C++ +void test_multi_pack() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + vector nums = {2, 3, 2}; + int bagWeight = 10; + vector dp(bagWeight + 1, 0); + + + for(int i = 0; i < weight.size(); 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]); + } + } + // 打印一下dp数组 + for (int j = 0; j <= bagWeight; j++) { + cout << dp[j] << " "; + } + cout << endl; + } + cout << dp[bagWeight] << endl; +} +int main() { + test_multi_pack(); +} +``` + +* 时间复杂度:O(m * n * k) m:物品种类个数,n背包容量,k单类物品数量 + +从代码里可以看出是01背包里面在加一个for循环遍历一个每种商品的数量。 和01背包还是如出一辙的。 + +当然还有那种二进制优化的方法,其实就是把每种物品的数量,打包成一个个独立的包。 + +和以上在循环遍历上有所不同,因为是分拆为各个包最后可以组成一个完整背包,具体原理我就不做过多解释了,大家了解一下就行,面试的话基本不会考完这个深度了,感兴趣可以自己深入研究一波。 + +## 总结 + +多重背包在面试中基本不会出现,力扣上也没有对应的题目,大家对多重背包的掌握程度知道它是一种01背包,并能在01背包的基础上写出对应代码就可以了。 + +至于背包九讲里面还有混合背包,二维费用背包,分组背包等等这些,大家感兴趣可以自己去学习学习,这里也不做介绍了,面试也不会考。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/背包问题理论基础完全背包.md b/problems/背包问题理论基础完全背包.md new file mode 100644 index 00000000..b91c99fd --- /dev/null +++ b/problems/背包问题理论基础完全背包.md @@ -0,0 +1,195 @@ +

+ + + + +

+

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

+# 动态规划:关于完全背包,你该了解这些! + +## 完全背包 + +有N件物品和一个最多能背重量为W的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。**每件物品都有无限个(也就是可以放入背包多次)**,求解将哪些物品装入背包里物品价值总和最大。 + +**完全背包和01背包问题唯一不同的地方就是,每种物品有无限件**。 + +同样leetcode上没有纯完全背包问题,都是需要完全背包的各种应用,需要转化成完全背包问题,所以我这里还是以纯完全背包问题进行讲解理论和原理。 + +在下面的讲解中,我依然举这个例子: + +背包最大重量为4。 + +物品为: + +| | 重量 | 价值 | +| --- | --- | --- | +| 物品0 | 1 | 15 | +| 物品1 | 3 | 20 | +| 物品2 | 4 | 30 | + +**每件商品都有无限个!** + +问背包能背的物品最大价值是多少? + +01背包和完全背包唯一不同就是体现在遍历顺序上,所以本文就不去做动规五部曲了,我们直接针对遍历顺序经行分析! + +关于01背包我如下两篇已经进行深入分析了: + +* [动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA) + +首先在回顾一下01背包的核心代码 +``` +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]); + } +} +``` + +我们知道01背包内嵌的循环是从大到小遍历,为了保证每个物品仅被添加一次。 + +而完全背包的物品是可以添加多次的,所以要从小到大去遍历,即: + +```C++ +// 先遍历物品,再遍历背包 +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]); + + } +} +``` + +至于为什么,我在[动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA)中也做了讲解。 + +dp状态图如下: + +![动态规划-完全背包](https://img-blog.csdnimg.cn/20210126104510106.jpg) + +相信很多同学看网上的文章,关于完全背包介绍基本就到为止了。 + +**其实还有一个很重要的问题,为什么遍历物品在外层循环,遍历背包容量在内层循环?** + +这个问题很多题解关于这里都是轻描淡写就略过了,大家都默认 遍历物品在外层,遍历背包容量在内层,好像本应该如此一样,那么为什么呢? + +难道就不能遍历背包容量在外层,遍历物品在内层? + + +看过这两篇的话: +* [动态规划:关于01背包问题,你该了解这些!](https://mp.weixin.qq.com/s/FwIiPPmR18_AJO5eiidT6w) +* [动态规划:关于01背包问题,你该了解这些!(滚动数组)](https://mp.weixin.qq.com/s/M4uHxNVKRKm5HPjkNZBnFA) + +就知道了,01背包中二维dp数组的两个for遍历的先后循序是可以颠倒了,一位dp数组的两个for循环先后循序一定是先遍历物品,再遍历背包容量。 + +**在完全背包中,对于一维dp数组来说,其实两个for循环嵌套顺序同样无所谓!** + +因为dp[j] 是根据 下标j之前所对应的dp[j]计算出来的。 只要保证下标j之前的dp[j]都是经过计算的就可以了。 + +遍历物品在外层循环,遍历背包容量在内层循环,状态如图: + +![动态规划-完全背包1](https://img-blog.csdnimg.cn/20210126104529605.jpg) + +遍历背包容量在外层循环,遍历物品在内层循环,状态如图: + +![动态规划-完全背包2](https://img-blog.csdnimg.cn/20210126104741304.jpg) + +看了这两个图,大家就会理解,完全背包中,两个for循环的先后循序,都不影响计算dp[j]所需要的值(这个值就是下标j之前所对应的dp[j])。 + +先遍历被背包在遍历物品,代码如下: + +```C++ +// 先遍历背包,再遍历物品 +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; +} +``` + +## C++测试代码 + +完整的C++测试代码如下: + +```C++ +// 先遍历物品,在遍历背包 +void test_CompletePack() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int bagWeight = 4; + vector dp(bagWeight + 1, 0); + 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]); + } + } + cout << dp[bagWeight] << endl; +} +int main() { + test_CompletePack(); +} + +``` + +```C++ + +// 先遍历背包,再遍历物品 +void test_CompletePack() { + vector weight = {1, 3, 4}; + vector value = {15, 20, 30}; + int bagWeight = 4; + + 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; +} +int main() { + test_CompletePack(); +} + +``` + + +## 总结 + +细心的同学可能发现,**全文我说的都是对于纯完全背包问题,其for循环的先后循环是可以颠倒的!** + +但如果题目稍稍有点变化,就会体现在遍历顺序上。 + +如果问装满背包有几种方式的话? 那么两个for循环的先后顺序就有很大区别了,而leetcode上的题目都是这种稍有变化的类型。 + +这个区别,我将在后面讲解具体leetcode题目中给大家介绍,因为这块如果不结合具题目,单纯的介绍原理估计很多同学会越看越懵! + +别急,下一篇就是了!哈哈 + +最后,**又可以出一道面试题了,就是纯完全背包,要求先用二维dp数组实现,然后再用一维dp数组实现,最后在问,两个for循环的先后是否可以颠倒?为什么?** +这个简单的完全背包问题,估计就可以难住不少候选人了。 + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/贪心算法总结篇.md b/problems/贪心算法总结篇.md new file mode 100644 index 00000000..01029d62 --- /dev/null +++ b/problems/贪心算法总结篇.md @@ -0,0 +1,162 @@ +

+ + + + +

+

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

+ +我刚刚开始讲解贪心系列的时候就说了,贪心系列并不打算严格的从简单到困难这么个顺序来讲解。 + +因为贪心的简单题可能往往过于简单甚至感觉不到贪心,如果我连续几天讲解简单的贪心,估计录友们一定会不耐烦了,会感觉贪心有啥好学的。 + +但贪心的难题又真的有点难,所以我是简单困难交错着讲的,这样大家就感觉难度适中,而且贪心也没有什么框架和套路,所以对刷题顺序要求没有那么高。 + +但在贪心系列,我发的题目难度会整体呈现一个阶梯状上升,细心的录友们应该有所体会。 + +在刚刚讲过的回溯系列中,大家可以发现我是严格按照框架难度顺序循序渐进讲解的,**和贪心又不一样,因为回溯法如果题目顺序没选好,刷题效果会非常差!** + +同样回溯系列也不允许简单困难交替着来,因为前后题目都是有因果关系的,**相信跟着刷过回溯系列的录友们都会明白我的良苦用心,哈哈**。 + +**每个系列都有每个系列的特点,我都会根据特点有所调整,大家看我每天的推送的题目,都不是随便找一个到就推送的,都是先有整体规划,然后反复斟酌具体题目的结果**。 + +那么在贪心总结篇里,我按难易程度以及题目类型大体归个类。 + +贪心大总结正式开始: + +## 贪心理论基础 + +在贪心系列开篇词[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg)中,我们就讲解了大家对贪心的普遍疑惑。 + +1. 贪心很简单,就是常识? + +跟着一起刷题的录友们就会发现,贪心思路往往很巧妙,并不简单。 + +2. 贪心有没有固定的套路? + +贪心无套路,也没有框架之类的,需要多看多练培养感觉才能想到贪心的思路。 + +3. 究竟什么题目是贪心呢? + +Carl个人认为:如果找出局部最优并可以推出全局最优,就是贪心,如果局部最优都没找出来,就不是贪心,可能是单纯的模拟。(并不是权威解读,一家之辞哈) + +但我们也不用过于强调什么题目是贪心,什么不是贪心,那就太学术了,毕竟学会解题就行了。 + +4. 如何知道局部最优推出全局最优,有数学证明么? + +在做贪心题的过程中,如果再来一个数据证明,其实没有必要,手动模拟一下,如果找不出反例,就试试贪心。面试中,代码写出来跑过测试用例即可,或者自己能自圆其说理由就行了 + +就像是 要用一下 1 + 1 = 2,没有必要再证明一下 1 + 1 究竟为什么等于 2。(例子极端了点,但是这个道理) + +相信大家读完[关于贪心算法,你该了解这些!](https://mp.weixin.qq.com/s/O935TaoHE9Eexwe_vSbRAg),就对贪心有了一个基本的认识了。 + + +## 贪心简单题 + +以下三道题目就是简单题,大家会发现贪心感觉就是常识。是的,如下三道题目,就是靠常识,但我都具体分析了局部最优是什么,全局最优是什么,贪心也要贪的有理有据! + +* [贪心算法:分发饼干](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://mp.weixin.qq.com/s/Xytl05kX8LZZ1iWWqjMoHA) +* [贪心算法:单调递增的数字](https://mp.weixin.qq.com/s/TAKO9qPYiv6KdMlqNq_ncg) + +### 贪心解决股票问题 + +大家都知道股票系列问题是动规的专长,其实用贪心也可以解决,而且还不止就这两道题目,但这两道比较典型,我就拿来单独说一说 + +* [贪心算法:买卖股票的最佳时机II](https://mp.weixin.qq.com/s/VsTFA6U96l18Wntjcg3fcg) +* [贪心算法:买卖股票的最佳时机含手续费](https://mp.weixin.qq.com/s/olWrUuDEYw2Jx5rMeG7XAg) + +### 两个维度权衡问题 + +在出现两个维度相互影响的情况时,两边一起考虑一定会顾此失彼,要先确定一个维度,再确定另一个一个维度。 + +* [贪心算法:分发糖果](https://mp.weixin.qq.com/s/8MwlgFfvaNYmjGwjuMlETQ) +* [贪心算法:根据身高重建队列](https://mp.weixin.qq.com/s/-2TgZVdOwS-DvtbjjDEbfw) + +在讲解本题的过程中,还强调了编程语言的重要性,模拟插队的时候,使用C++中的list(链表)替代了vector(动态数组),效率会高很多。 + +所以在[贪心算法:根据身高重建队列(续集)](https://mp.weixin.qq.com/s/K-pRN0lzR-iZhoi-1FgbSQ)详细讲解了,为什么用list(链表)更快! + +**大家也要掌握自己所用的编程语言,理解其内部实现机制,这样才能写出高效的算法!** + +## 贪心难题 + +这里的题目如果没有接触过,其实是很难想到的,甚至接触过,也一时想不出来,所以题目不要做一遍,要多练! + +### 贪心解决区间问题 + +关于区间问题,大家应该印象深刻,有一周我们专门讲解的区间问题,各种覆盖各种去重。 + +* [贪心算法:跳跃游戏](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://mp.weixin.qq.com/s/DrjIQy6ouKbpletQr0g1Fg) 其实是动态规划的题目,但贪心性能更优,很多同学也是第一次发现贪心能比动规更优的题目。 + + +[贪心算法:加油站](https://mp.weixin.qq.com/s/aDbiNuEZIhy6YKgQXvKELw)可能以为是一道模拟题,但就算模拟其实也不简单,需要把while用的很娴熟。但其实是可以使用贪心给时间复杂度降低一个数量级。 + +最后贪心系列压轴题目[贪心算法:我要监控二叉树!](https://mp.weixin.qq.com/s/kCxlLLjWKaE6nifHC3UL2Q),不仅贪心的思路不好想,而且需要对二叉树的操作特别娴熟,这就是典型的交叉类难题了。 + + +## 贪心每周总结 + +周总结里会对每周的题目中大家的疑问、相关难点或者笔误之类的进行复盘和总结。 + +如果大家发现文章哪里有问题,那么在周总结里或者文章评论区一定进行了修正,保证不会因为我的笔误或者理解问题而误导大家,哈哈。 + +所以周总结一定要看! + +* [本周小结!(贪心算法系列一)](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) + +## 总结 + +很多没有接触过贪心的同学都会感觉贪心有啥可学的,但只要跟着「代码随想录」坚持下来之后,就会发现,贪心是一种很重要的算法思维而且并不简单,贪心往往妙的出其不意,触不及防! + +**回想一下我们刚刚开始讲解贪心的时候,大家会发现自己在坚持中进步了很多!** + +这也是「代码随想录」的初衷,只要一路坚持下来,不仅基础扎实,而且进步也是飞速的。 + +**在这十八道贪心经典题目中,大家可以发现在每一道题目的讲解中,我都是把什么是局部最优,和什么是全局最优说清楚**。 + +这也是我认为判断这是一道贪心题目的依据,如果找不出局部最优,那可能就是一道模拟题。 + +不知不觉又一个系列结束了,同时也是2020年的结束。 + +**一个系列的结束,又是一个新系列的开始,我们将在明年第一个工作日正式开始动态规划,来不及解释了,录友们上车别掉队,我们又要开始新的征程!** + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/贪心算法理论基础.md b/problems/贪心算法理论基础.md index 0276d987..74efad83 100644 --- a/problems/贪心算法理论基础.md +++ b/problems/贪心算法理论基础.md @@ -1,15 +1,24 @@ +

+ + + + +

+

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

+ + # 关于贪心算法,你该了解这些! > 正式开始新的系列了,贪心算法! 通知:一些录友表示经常看不到每天的文章,现在公众号已经不按照发送时间推荐了,而是根据一些规则乱序推送,所以可能关注了「代码随想录」也一直看不到文章,建议把「代码随想录」设置星标哈,设置星标之后,每天就按发文时间推送了,我每天都是定时8:35发送的,嗷嗷准时,哈哈。 -# 什么是贪心 +## 什么是贪心 -**贪心的本质是选择每一阶段的局部最优,从而达到全局最优**。 +**贪心的本质是选择每一阶段的局部最优,从而达到全局最优**。 这么说有点抽象,来举一个例子: -例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿? +例如,有一堆钞票,你可以拿走十张,如果想达到最大的金额,你要怎么拿? 指定每次拿最大的,最终结果就是拿走最大数额的钱。 @@ -17,13 +26,13 @@ 再举一个例子如果是 有一堆盒子,你有一个背包体积为n,如何把背包尽可能装满,如果还每次选最大的盒子,就不行了。这时候就需要动态规划。动态规划的问题在下一个系列会详细讲解。 -# 贪心的套路(什么时候用贪心) +## 贪心的套路(什么时候用贪心) 很多同学做贪心的题目的时候,想不出来是贪心,想知道有没有什么套路可以一看就看出来是贪心。 **说实话贪心算法并没有固定的套路**。 -所以唯一的难点就是如何通过局部最优,推出整体最优。 +所以唯一的难点就是如何通过局部最优,推出整体最优。 那么如何能看出局部最优是否能推出整体最优呢?有没有什么固定策略或者套路呢? @@ -33,7 +42,7 @@ **最好用的策略就是举反例,如果想不到反例,那么就试一试贪心吧**。 -可有有同学认为手动模拟,举例子得出的结论不靠谱,想要严格的数学证明。 +可有有同学认为手动模拟,举例子得出的结论不靠谱,想要严格的数学证明。 一般数学证明有如下两种方法: @@ -52,11 +61,11 @@ 所以这也是为什么很多同学通过(accept)了贪心的题目,但都不知道自己用了贪心算法,**因为贪心有时候就是常识性的推导,所以会认为本应该就这么做!** -**那么刷题的时候什么时候真的需要数学推导呢?** +**那么刷题的时候什么时候真的需要数学推导呢?** 例如这道题目:[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA),这道题不用数学推导一下,就找不出环的起始位置,想试一下就不知道怎么试,这种题目确实需要数学简单推导一下。 -# 贪心一般解题步骤 +## 贪心一般解题步骤 贪心算法一般分为如下四步: @@ -67,7 +76,7 @@ 其实这个分的有点细了,真正做题的时候很难分出这么详细的解题步骤,可能就是因为贪心的题目往往还和其他方面的知识混在一起。 -# 总结 +## 总结 本篇给出了什么是贪心以及大家关心的贪心算法固定套路。 @@ -75,6 +84,28 @@ 最后给出贪心的一般解题步骤,大家可以发现这个解题步骤也是比较抽象的,不像是二叉树,回溯算法,给出了那么具体的解题套路和模板。 -本篇没有配图,其实可以找一些动漫周边或者搞笑的图配一配(符合大多数公众号文章的作风),但这不是我的风格,所以本篇文字描述足以! +本篇没有配图,其实可以找一些动漫周边或者搞笑的图配一配(符合大多数公众号文章的作风),但这不是我的风格,所以本篇文字描述足以! -就酱,「代码随想录」值得你的关注! + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/链表总结篇.md b/problems/链表总结篇.md index 16954683..e709ec9b 100644 --- a/problems/链表总结篇.md +++ b/problems/链表总结篇.md @@ -1,8 +1,17 @@ +

+ + + + +

+

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

+ + > 之前链表篇没有做总结,所以是时候总结一波 # 链表的理论基础 -在这篇文章[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ)中,介绍了如下几点: +在这篇文章[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ)中,介绍了如下几点: * 链表的种类主要为:单链表,双链表,循环链表 * 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。 @@ -11,7 +20,7 @@ **可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐**。 -# 链表经典题目 +# 链表经典题目 ## 虚拟头结点 @@ -23,7 +32,7 @@ 在[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)中,我给出了用虚拟头结点和没用虚拟头结点的代码,大家对比一下就会发现,使用虚拟头结点的好处。 -## 链表的基本操作 +## 链表的基本操作 在[链表:一道题目考察了常见的五个操作!](https://mp.weixin.qq.com/s/Cf95Lc6brKL4g2j8YyF3Mg)中,我们通设计链表把链表常见的五个操作练习了一遍。 @@ -53,7 +62,7 @@ **可以先通过迭代法,彻底弄清楚链表反转的过程!** -## 环形链表 +## 环形链表 在[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)中,讲解了在链表如何找环,以及如何找环的入口位置。 @@ -66,7 +75,7 @@ * fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。 * fast和slow都进入环里之后,fast相对于slow来说,fast是一个节点一个节点的靠近slow的,**注意是相对运动,所以fast一定可以和slow重合**。 -如果fast是一次走三个节点,那么可能会跳过slow,因为相对于slow来说,fast是两个节点移动的。 +如果fast是一次走三个节点,那么可能会跳过slow,因为相对于slow来说,fast是两个节点移动的。 确定有否有环比较容易,但是找到环的入口就不太容易了,需要点数学推理。 @@ -74,32 +83,32 @@ 这是一位录友在评论区有一个疑问,感觉这个问题很不错,但评论区根本说不清楚,我就趁着总结篇,补充一下这个证明。 -在推理过程中,**为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?** +在推理过程中,**为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y 呢?** 了解这个问题一定要先把文章[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)看了,即文章中如下的地方: - + 首先slow进环的时候,fast一定是先进环来了。 如果slow进环入口,fast也在环入口,那么把这个环展开成直线,就是如下图的样子: - + 可以看出如果slow 和 fast同时在环入口开始走,一定会在环入口3相遇,slow走了一圈,fast走了两圈。 重点来了,slow进环的时候,fast一定是在环的任意一个位置,如图: - + 那么fast指针走到环入口3的时候,已经走了k + n 个节点,slow相应的应该走了(k + n) / 2 个节点。 -因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 +因为k是小于n的(图中可以看出),所以(k + n) / 2 一定小于n。 **也就是说slow一定没有走到环入口3,而fast已经到环入口3了**。 -这说明什么呢? +这说明什么呢? **在slow开始走的那一环已经和fast相遇了**。 @@ -124,8 +133,24 @@ 如果希望从基础学起来的同学,也可以从头学起来,从头开始打卡,打卡的同时也总结自己的所学所思,一定进步飞快! -**在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,快去通关学习吧!** -![](https://img-blog.csdnimg.cn/20201030210901823.jpg) -**「代码随想录」这么用心的公众号,不分享给身边的同学朋友啥的,是不是可惜了? 哈哈** +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/链表理论基础.md b/problems/链表理论基础.md new file mode 100644 index 00000000..1dee1182 --- /dev/null +++ b/problems/链表理论基础.md @@ -0,0 +1,162 @@ +

+ + + + +

+

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

+ + +# 关于链表,你该了解这些! + +什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点是又两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。 + +链接的入口点称为列表的头结点也就是head。 + +如图所示: +![链表1](https://img-blog.csdnimg.cn/20200806194529815.png) + +# 链表的类型 + +接下来说一下链表的几种类型: + +## 单链表 + +刚刚说的就是单链表。 + +## 双链表 + +单链表中的节点只能指向节点的下一个节点。 + +双链表:每一个节点有两个指针域,一个指向下一个节点,一个指向上一个节点。 + +双链表 既可以向前查询也可以向后查询。 + +如图所示: +![链表2](https://img-blog.csdnimg.cn/20200806194559317.png) + +## 循环链表 + +循环链表,顾名思义,就是链表首尾相连。 + +循环链表可以用来解决约瑟夫环问题。 + +![链表4](https://img-blog.csdnimg.cn/20200806194629603.png) + + +# 链表的存储方式 + +了解完链表的类型,再来说一说链表在内存中的存储方式。 + +数组是在内存中是连续分布的,但是链表在内存中可不是连续分布的。 + +链表是通过指针域的指针链接在内存中各个节点。 + +所以链表中的节点在内存中不是连续分布的 ,而是散乱分布在内存中的某地址上,分配机制取决于操作系统的内存管理。 + +如图所示: + +![链表3](https://img-blog.csdnimg.cn/20200806194613920.png) + +这个链表起始节点为2, 终止节点为7, 各个节点分布在内存个不同地址空间上,通过指针串联在一起。 + +# 链表的定义 + +接下来说一说链表的定义。 + +链表节点的定义,很多同学在面试的时候都写不好。 + +这是因为平时在刷leetcode的时候,链表的节点都默认定义好了,直接用就行了,所以同学们都没有注意到链表的节点是如何定义的。 + +而在面试的时候,一旦要自己手写链表,就写的错漏百出。 + +这里我给出C/C++的定义链表节点方式,如下所示: + +``` +// 单链表 +struct ListNode { + int val; // 节点上存储的元素 + ListNode *next; // 指向下一个节点的指针 + ListNode(int x) : val(x), next(NULL) {} // 节点的构造函数 +}; +``` + +有同学说了,我不定义构造函数行不行,答案是可以的,C++默认生成一个构造函数。 + +但是这个构造函数不会初始化任何成员变化,下面我来举两个例子: + +通过自己定义构造函数初始化节点: + +``` +ListNode* head = new ListNode(5); +``` + +使用默认构造函数初始化节点: + +``` +ListNode* head = new ListNode(); +head->val = 5; +``` + +所以如果不定义构造函数使用默认构造函数的话,在初始化的时候就不能直接给变量赋值! + +# 链表的操作 + +## 删除节点 + +删除D节点,如图所示: + +![链表-删除节点](https://img-blog.csdnimg.cn/20200806195114541.png) + +只要将C节点的next指针 指向E节点就可以了。 + +那有同学说了,D节点不是依然存留在内存里么?只不过是没有在这个链表里而已。 + +是这样的,所以在C++里最好是再手动释放这个D节点,释放这块内存。 + +其他语言例如Java、Python,就有自己的内存回收机制,就不用自己手动释放了。 + +## 添加节点 + +如图所示: + +![链表-添加节点](https://img-blog.csdnimg.cn/20200806195134331.png) + +可以看出链表的增添和删除都是O(1)操作,也不会影响到其他节点。 + +但是要注意,要是删除第五个节点,需要从头节点查找到第四个节点通过next指针进行删除操作,查找的时间复杂度是O(n)。 + +# 性能分析 + +再把链表的特性和数组的特性进行一个对比,如图所示: + +![链表-链表与数据性能对比](https://img-blog.csdnimg.cn/20200806195200276.png) + +数组在定义的时候,长度就是固定的,如果想改动数组的长度,就需要重新定义一个新的数组。 + +链表的长度可以是不固定的,并且可以动态增删, 适合数据量不固定,频繁增删,较少查询的场景。 + +相信大家已经对链表足够的了解,后面我会讲解关于链表的高频面试题目,我们下期见! + + + + +## 其他语言版本 + + +Java: + + +Python: + + +Go: + + + + +----------------------- +* 作者微信:[程序员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/面试题02.07.链表相交.md b/problems/面试题02.07.链表相交.md deleted file mode 100644 index 77024d7c..00000000 --- a/problems/面试题02.07.链表相交.md +++ /dev/null @@ -1,66 +0,0 @@ -## 题目地址 -https://leetcode-cn.com/problems/intersection-of-two-linked-lists-lcci/ - -## 思路 - -本来很简洁明了的一道题,让题目描述搞的云里雾里的。 - -简单来说,就是求两个链表交点节点的**指针**。 这里同学们要注意,交点不是数值相等,而是指针相等。 - -为了方便举例,假设节点元素数值相等,则节点指针相等。 - -看如下两个链表,目前curA指向链表A的头结点,curB指向链表B的头结点: - - - -我们求出两个链表的长度,并求出两个链表长度的差值,然后让curA移动到,和curB 末尾对齐的位置,如图: - - - -此时我们就可以比较curA和curB是否相同,如果不相同,同时向后移动curA和curB,如果遇到curA == curB,则找到焦点。 - -否则循环退出返回空指针。 - -## C++代码 - -``` -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; - } -}; -``` -> 更多算法干货文章持续更新,可以微信搜索「代码随想录」第一时间围观,关注后,回复「Java」「C++」 「python」「简历模板」「数据结构与算法」等等,就可以获得我多年整理的学习资料。 diff --git a/video/.DS_Store b/video/.DS_Store deleted file mode 100644 index 5008ddfc..00000000 Binary files a/video/.DS_Store and /dev/null differ diff --git a/video/1.两数之和.mp4 b/video/1.两数之和.mp4 deleted file mode 100644 index dd6849c6..00000000 Binary files a/video/1.两数之和.mp4 and /dev/null differ diff --git a/video/101.对称二叉树.gif b/video/101.对称二叉树.gif deleted file mode 100644 index 01ee5492..00000000 Binary files a/video/101.对称二叉树.gif and /dev/null differ diff --git a/video/102二叉树的层序遍历.mp4 b/video/102二叉树的层序遍历.mp4 deleted file mode 100644 index f6f66bba..00000000 Binary files a/video/102二叉树的层序遍历.mp4 and /dev/null differ diff --git a/video/1047.删除字符串中的所有相邻重复项.gif b/video/1047.删除字符串中的所有相邻重复项.gif deleted file mode 100644 index a45c37d1..00000000 Binary files a/video/1047.删除字符串中的所有相邻重复项.gif and /dev/null differ diff --git a/video/1047.删除字符串中的所有相邻重复项.mp4 b/video/1047.删除字符串中的所有相邻重复项.mp4 deleted file mode 100644 index b8512f24..00000000 Binary files a/video/1047.删除字符串中的所有相邻重复项.mp4 and /dev/null differ diff --git a/video/141.环形链表.gif b/video/141.环形链表.gif deleted file mode 100644 index 5e5308aa..00000000 Binary files a/video/141.环形链表.gif and /dev/null differ diff --git a/video/141.环形链表.mp4 b/video/141.环形链表.mp4 deleted file mode 100644 index 19a7f223..00000000 Binary files a/video/141.环形链表.mp4 and /dev/null differ diff --git a/video/142.环形链表II.mp4 b/video/142.环形链表II.mp4 deleted file mode 100644 index b6ced104..00000000 Binary files a/video/142.环形链表II.mp4 and /dev/null differ diff --git a/video/142.环形链表II1.mp4 b/video/142.环形链表II1.mp4 deleted file mode 100644 index 317ce246..00000000 Binary files a/video/142.环形链表II1.mp4 and /dev/null differ diff --git a/video/142.环形链表II(求入口).gif b/video/142.环形链表II(求入口).gif deleted file mode 100644 index 9e5f2038..00000000 Binary files a/video/142.环形链表II(求入口).gif and /dev/null differ diff --git a/video/15.三数之和.gif b/video/15.三数之和.gif deleted file mode 100644 index 82e1eabc..00000000 Binary files a/video/15.三数之和.gif and /dev/null differ diff --git a/video/15.三数之和.mp4 b/video/15.三数之和.mp4 deleted file mode 100644 index 29bef5fc..00000000 Binary files a/video/15.三数之和.mp4 and /dev/null differ diff --git a/video/150.逆波兰表达式求值.gif b/video/150.逆波兰表达式求值.gif deleted file mode 100644 index 8e71e591..00000000 Binary files a/video/150.逆波兰表达式求值.gif and /dev/null differ diff --git a/video/150.逆波兰表达式求值.mp4 b/video/150.逆波兰表达式求值.mp4 deleted file mode 100644 index bd991a98..00000000 Binary files a/video/150.逆波兰表达式求值.mp4 and /dev/null differ diff --git a/video/151翻转字符串里的单词.gif b/video/151翻转字符串里的单词.gif deleted file mode 100644 index cd8a60fb..00000000 Binary files a/video/151翻转字符串里的单词.gif and /dev/null differ diff --git a/video/20.有效括号.mp4 b/video/20.有效括号.mp4 deleted file mode 100644 index 84eab69f..00000000 Binary files a/video/20.有效括号.mp4 and /dev/null differ diff --git a/video/206.翻转链表.gif b/video/206.翻转链表.gif deleted file mode 100644 index ad2fdafd..00000000 Binary files a/video/206.翻转链表.gif and /dev/null differ diff --git a/video/206.翻转链表.mp4 b/video/206.翻转链表.mp4 deleted file mode 100644 index 02d66ccb..00000000 Binary files a/video/206.翻转链表.mp4 and /dev/null differ diff --git a/video/209.长度最小的子数组.gif b/video/209.长度最小的子数组.gif deleted file mode 100644 index f120acc1..00000000 Binary files a/video/209.长度最小的子数组.gif and /dev/null differ diff --git a/video/209.长度最小的子数组.mp4 b/video/209.长度最小的子数组.mp4 deleted file mode 100644 index df034f52..00000000 Binary files a/video/209.长度最小的子数组.mp4 and /dev/null differ diff --git a/video/209.长度最小的子数组(缩小版).gif b/video/209.长度最小的子数组(缩小版).gif deleted file mode 100644 index f1c0b4cb..00000000 Binary files a/video/209.长度最小的子数组(缩小版).gif and /dev/null differ diff --git a/video/225.用队列实现栈.mp4 b/video/225.用队列实现栈.mp4 deleted file mode 100644 index 9aa76c34..00000000 Binary files a/video/225.用队列实现栈.mp4 and /dev/null differ diff --git a/video/232.用栈实现队列.mp4 b/video/232.用栈实现队列.mp4 deleted file mode 100644 index 1ba21a36..00000000 Binary files a/video/232.用栈实现队列.mp4 and /dev/null differ diff --git a/video/232.用栈实现队列版本2.mp4 b/video/232.用栈实现队列版本2.mp4 deleted file mode 100644 index 32db272e..00000000 Binary files a/video/232.用栈实现队列版本2.mp4 and /dev/null differ diff --git a/video/239.滑动窗口最大值.gif b/video/239.滑动窗口最大值.gif deleted file mode 100644 index 90c8f11a..00000000 Binary files a/video/239.滑动窗口最大值.gif and /dev/null differ diff --git a/video/239.滑动窗口最大值.mp4 b/video/239.滑动窗口最大值.mp4 deleted file mode 100644 index 5497d70d..00000000 Binary files a/video/239.滑动窗口最大值.mp4 and /dev/null differ diff --git a/video/242.有效的字母异位词.gif b/video/242.有效的字母异位词.gif deleted file mode 100644 index febc0bad..00000000 Binary files a/video/242.有效的字母异位词.gif and /dev/null differ diff --git a/video/242.有效的字母异位词.mp4 b/video/242.有效的字母异位词.mp4 deleted file mode 100644 index 33504290..00000000 Binary files a/video/242.有效的字母异位词.mp4 and /dev/null differ diff --git a/video/26.删除排序数组中的重复项.mp4 b/video/26.删除排序数组中的重复项.mp4 deleted file mode 100644 index 846d10ac..00000000 Binary files a/video/26.删除排序数组中的重复项.mp4 and /dev/null differ diff --git a/video/27.移除元素-双指针法.gif b/video/27.移除元素-双指针法.gif deleted file mode 100644 index 74aa479e..00000000 Binary files a/video/27.移除元素-双指针法.gif and /dev/null differ diff --git a/video/27.移除元素-暴力解法.gif b/video/27.移除元素-暴力解法.gif deleted file mode 100644 index 0ccba4dc..00000000 Binary files a/video/27.移除元素-暴力解法.gif and /dev/null differ diff --git a/video/27.移除元素-暴力解法.mp4 b/video/27.移除元素-暴力解法.mp4 deleted file mode 100644 index 5024f2d7..00000000 Binary files a/video/27.移除元素-暴力解法.mp4 and /dev/null differ diff --git a/video/27.移除元素.mp4 b/video/27.移除元素.mp4 deleted file mode 100644 index 9dc869a1..00000000 Binary files a/video/27.移除元素.mp4 and /dev/null differ diff --git a/video/283.移动零.gif b/video/283.移动零.gif deleted file mode 100644 index b42c3bf6..00000000 Binary files a/video/283.移动零.gif and /dev/null differ diff --git a/video/283.移动零.mp4 b/video/283.移动零.mp4 deleted file mode 100644 index 227b9840..00000000 Binary files a/video/283.移动零.mp4 and /dev/null differ diff --git a/video/344.反转字符串.gif b/video/344.反转字符串.gif deleted file mode 100644 index b99ef086..00000000 Binary files a/video/344.反转字符串.gif and /dev/null differ diff --git a/video/344.反转字符串.mp4 b/video/344.反转字符串.mp4 deleted file mode 100644 index 80877057..00000000 Binary files a/video/344.反转字符串.mp4 and /dev/null differ diff --git a/video/450.删除二叉搜索树中的节点.gif b/video/450.删除二叉搜索树中的节点.gif deleted file mode 100644 index d6f8a639..00000000 Binary files a/video/450.删除二叉搜索树中的节点.gif and /dev/null differ diff --git a/video/450.删除二叉搜索树中的节点.mp4 b/video/450.删除二叉搜索树中的节点.mp4 deleted file mode 100644 index a835887d..00000000 Binary files a/video/450.删除二叉搜索树中的节点.mp4 and /dev/null differ diff --git a/video/53.最大子序和.gif b/video/53.最大子序和.gif deleted file mode 100644 index 7514a5ac..00000000 Binary files a/video/53.最大子序和.gif and /dev/null differ diff --git a/video/53.最大子序和.mp4 b/video/53.最大子序和.mp4 deleted file mode 100644 index 94b62a73..00000000 Binary files a/video/53.最大子序和.mp4 and /dev/null differ diff --git a/video/617.合并二叉树.gif b/video/617.合并二叉树.gif deleted file mode 100644 index 00a8479b..00000000 Binary files a/video/617.合并二叉树.gif and /dev/null differ diff --git a/video/654.最大二叉树.gif b/video/654.最大二叉树.gif deleted file mode 100644 index 3baa8158..00000000 Binary files a/video/654.最大二叉树.gif and /dev/null differ diff --git a/video/654.最大二叉树.mp4 b/video/654.最大二叉树.mp4 deleted file mode 100644 index 70515bfb..00000000 Binary files a/video/654.最大二叉树.mp4 and /dev/null differ diff --git a/video/701.二叉搜索树中的插入操作.gif b/video/701.二叉搜索树中的插入操作.gif deleted file mode 100644 index 184937d0..00000000 Binary files a/video/701.二叉搜索树中的插入操作.gif and /dev/null differ diff --git a/video/701.二叉搜索树中的插入操作.mp4 b/video/701.二叉搜索树中的插入操作.mp4 deleted file mode 100644 index a3f00c95..00000000 Binary files a/video/701.二叉搜索树中的插入操作.mp4 and /dev/null differ diff --git a/video/844.比较含退格的字符串.gif b/video/844.比较含退格的字符串.gif deleted file mode 100644 index 39cf9b33..00000000 Binary files a/video/844.比较含退格的字符串.gif and /dev/null differ diff --git a/video/925.长按键入.gif b/video/925.长按键入.gif deleted file mode 100644 index 5e986d1a..00000000 Binary files a/video/925.长按键入.gif and /dev/null differ diff --git a/video/977.有序数组的平方.gif b/video/977.有序数组的平方.gif deleted file mode 100644 index 38b3788d..00000000 Binary files a/video/977.有序数组的平方.gif and /dev/null differ diff --git a/video/KMP精讲1.gif b/video/KMP精讲1.gif deleted file mode 100644 index 74faeba8..00000000 Binary files a/video/KMP精讲1.gif and /dev/null differ diff --git a/video/KMP精讲2.gif b/video/KMP精讲2.gif deleted file mode 100644 index 5a88d49b..00000000 Binary files a/video/KMP精讲2.gif and /dev/null differ diff --git a/video/KMP精讲3.gif b/video/KMP精讲3.gif deleted file mode 100644 index 715e2aa8..00000000 Binary files a/video/KMP精讲3.gif and /dev/null differ diff --git a/video/KMP精讲4.gif b/video/KMP精讲4.gif deleted file mode 100644 index 0f0f68ca..00000000 Binary files a/video/KMP精讲4.gif and /dev/null differ diff --git a/video/中序遍历迭代(统一写法).mp4 b/video/中序遍历迭代(统一写法).mp4 deleted file mode 100644 index 6f3c1cc2..00000000 Binary files a/video/中序遍历迭代(统一写法).mp4 and /dev/null differ diff --git a/video/二叉树中序遍历(迭代法).gif b/video/二叉树中序遍历(迭代法).gif deleted file mode 100644 index ef868ec6..00000000 Binary files a/video/二叉树中序遍历(迭代法).gif and /dev/null differ diff --git a/video/二叉树中序遍历(迭代法).mp4 b/video/二叉树中序遍历(迭代法).mp4 deleted file mode 100644 index e5eeec05..00000000 Binary files a/video/二叉树中序遍历(迭代法).mp4 and /dev/null differ diff --git a/video/二叉树前序遍历(迭代法).gif b/video/二叉树前序遍历(迭代法).gif deleted file mode 100644 index 78d2187b..00000000 Binary files a/video/二叉树前序遍历(迭代法).gif and /dev/null differ diff --git a/video/二叉树前序遍历(迭代法).mp4 b/video/二叉树前序遍历(迭代法).mp4 deleted file mode 100644 index fb92953e..00000000 Binary files a/video/二叉树前序遍历(迭代法).mp4 and /dev/null differ diff --git a/video/对称二叉树.mp4 b/video/对称二叉树.mp4 deleted file mode 100644 index 96d907fd..00000000 Binary files a/video/对称二叉树.mp4 and /dev/null differ diff --git a/video/替换空格.gif b/video/替换空格.gif deleted file mode 100644 index c98dae4f..00000000 Binary files a/video/替换空格.gif and /dev/null differ diff --git a/video/替换空格.mp4 b/video/替换空格.mp4 deleted file mode 100644 index 223743cd..00000000 Binary files a/video/替换空格.mp4 and /dev/null differ diff --git a/video/翻转二叉树.gif b/video/翻转二叉树.gif deleted file mode 100644 index afdfe7c0..00000000 Binary files a/video/翻转二叉树.gif and /dev/null differ diff --git a/video/翻转二叉树.mp4 b/video/翻转二叉树.mp4 deleted file mode 100644 index 73636b7b..00000000 Binary files a/video/翻转二叉树.mp4 and /dev/null differ