diff --git a/README.md b/README.md index 18d176a423..56148ddc77 100644 --- a/README.md +++ b/README.md @@ -104,19 +104,19 @@ 1. [数组过于简单,但你该了解这些!](./problems/数组理论基础.md) 2. [数组:每次遇到二分法,都是一看就会,一写就废](./problems/0704.二分查找.md) 3. [数组:就移除个元素很难么?](./problems/0027.移除元素.md) -4. [数组:滑动窗口拯救了你](https://mp.weixin.qq.com/s/UrZynlqi4QpyLlLhBPglyg) -5. [数组:这个循环可以转懵很多人!](https://mp.weixin.qq.com/s/KTPhaeqxbMK9CxHUUgFDmg) -6. [数组:总结篇](https://mp.weixin.qq.com/s/LIfQFRJBH5ENTZpvixHEmg) +4. [数组:滑动窗口拯救了你](./problems/0209.长度最小的子数组.md) +5. [数组:这个循环可以转懵很多人!](./problems/0059.螺旋矩阵II.md) +6. [数组:总结篇](./problems/数组总结篇.md) ## 链表 -1. [关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ) -2. [链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) -3. [链表:一道题目考察了常见的五个操作!](https://mp.weixin.qq.com/s/Cf95Lc6brKL4g2j8YyF3Mg) -4. [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) -5. [链表:删除链表的倒数第 N 个结点](https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/solution/dai-ma-sui-xiang-lu-19-shan-chu-lian-bia-2hxt/) -5. [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) -6. [链表:总结篇!](https://mp.weixin.qq.com/s/vK0JjSTHfpAbs8evz5hH8A) +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) ## 哈希表 @@ -134,28 +134,25 @@ ## 字符串 -1. [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) -2. [字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw) -3. [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) -4. [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) -5. [字符串:反转个字符串还有这个用处?](https://mp.weixin.qq.com/s/PmcdiWSmmccHAONzU0ScgQ) -6. [帮你把KMP算法学个通透!(理论篇)B站视频](https://www.bilibili.com/video/BV1PD4y1o7nd) -7. [帮你把KMP算法学个通透!(代码篇)B站视频](https://www.bilibili.com/video/BV1M5411j7Xx) -8. [字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg) -9. [字符串:KMP算法还能干这个!](https://mp.weixin.qq.com/s/lR2JPtsQSR2I_9yHbBmBuQ) -10. [字符串:前缀表不右移,难道就写不出KMP了?](https://mp.weixin.qq.com/s/p3hXynQM2RRROK5c6X7xfw) -11. [字符串:总结篇!](https://mp.weixin.qq.com/s/gtycjyDtblmytvBRFlCZJg) +1. [字符串:这道题目,使用库函数一行代码搞定](./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) ## 双指针法 双指针法基本都是应用在数组,字符串与链表的题目上 -1. [数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) -2. [字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) -3. [字符串:替换空格](https://mp.weixin.qq.com/s/t0A9C44zgM-RysAQV3GZpg) -4. [字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw) -5. [链表:听说过两天反转链表又写不出来了?](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg) -6. [链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA) +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. [哈希表:解决了两数之和,那么能解决三数之和么?](https://mp.weixin.qq.com/s/r5cgZFu0tv4grBAexdcd8A) 8. [双指针法:一样的道理,能解决四数之和](https://mp.weixin.qq.com/s/nQrcco8AZJV1pAOVjeIU_g) 9. [双指针法:总结篇!](https://mp.weixin.qq.com/s/_p7grwjISfMh0U65uOyCjA) diff --git "a/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" "b/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" new file mode 100644 index 0000000000..caedc17bc3 --- /dev/null +++ "b/problems/0019.\345\210\240\351\231\244\351\223\276\350\241\250\347\232\204\345\200\222\346\225\260\347\254\254N\344\270\252\350\212\202\347\202\271.md" @@ -0,0 +1,100 @@ + + +

+ + + + +

+ +## 19.删除链表的倒数第N个节点 + +## 思路 + +双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。 + +思路是这样的,但要注意一些细节。 + +分为如下几步: + +* 首先这里我推荐大家使用虚拟头结点,这样方面处理删除实际头结点的逻辑,如果虚拟头结点不清楚,可以看这篇: [链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) + + +* 定义fast指针和slow指针,初始值为虚拟头结点,如图: + + + +* fast首先走n + 1步 ,为什么是n+1呢,因为只有这样同时移动的时候slow才能指向删除节点的上一个节点(方便做删除操作),如图: + + +* fast和slow同时移动,之道fast指向末尾,如题: + + +* 删除slow指向的下一个节点,如图: + + +此时不难写出如下C++代码: + +```C++ +class Solution { +public: + ListNode* removeNthFromEnd(ListNode* head, int n) { + ListNode* dummyHead = new ListNode(0); + dummyHead->next = head; + ListNode* slow = dummyHead; + ListNode* fast = dummyHead; + while(n-- && fast != NULL) { + fast = fast->next; + } + fast = fast->next; // fast再提前走一步,因为需要让slow指向删除节点的上一个节点 + while (fast != NULL) { + fast = fast->next; + slow = slow->next; + } + slow->next = slow->next->next; + return dummyHead->next; + } +}; +``` + + +## 其他语言补充 + +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) + +![](../pics/公众号.png) + diff --git "a/problems/0028.\345\256\236\347\216\260strStr.md" "b/problems/0028.\345\256\236\347\216\260strStr.md" new file mode 100644 index 0000000000..6da5d8d1c7 --- /dev/null +++ "b/problems/0028.\345\256\236\347\216\260strStr.md" @@ -0,0 +1,682 @@ + +

+ + + + +

+ +> 在一个串中查找是否出现过另一个串,这是KMP的看家本领。 + +# 28. 实现 strStr() + +https://leetcode-cn.com/problems/implement-strstr/ + +实现 strStr() 函数。 + +给定一个 haystack 字符串和一个 needle 字符串,在 haystack 字符串中找出 needle 字符串出现的第一个位置 (从0开始)。如果不存在,则返回  -1。 + +示例 1: +输入: haystack = "hello", needle = "ll" +输出: 2 + +示例 2: +输入: haystack = "aaaaa", needle = "bba" +输出: -1 + +说明: +当 needle 是空字符串时,我们应当返回什么值呢?这是一个在面试中很好的问题。 +对于本题而言,当 needle 是空字符串时我们应当返回 0 。这与C语言的 strstr() 以及 Java的 indexOf() 定义相符。 + + +# 思路 + +本题是KMP 经典题目。 + +以下文字如果看不进去,可以看我的B站视频: + +* [帮你把KMP算法学个通透!B站(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) +* [帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) + +KMP的经典思想就是:**当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配。** + +本篇将以如下顺序来讲解KMP, + + +* 什么是KMP +* KMP有什么用 +* 什么是前缀表 +* 为什么一定要用前缀表 +* 如何计算前缀表 +* 前缀表与next数组 +* 使用next数组来匹配 +* 时间复杂度分析 +* 构造next数组 +* 使用next数组来做匹配 +* 前缀表统一减一 C++代码实现 +* 前缀表(不减一)C++实现 +* 总结 + + +读完本篇可以顺便,把leetcode上28.实现strStr()题目做了。 + +如果文字实在看不下去,就看我在B站上的视频吧,如下: + +* [帮你把KMP算法学个通透!(理论篇)B站](https://www.bilibili.com/video/BV1PD4y1o7nd/) +* [帮你把KMP算法学个通透!(求next数组代码篇)B站](https://www.bilibili.com/video/BV1M5411j7Xx/) + + +# 什么是KMP + +说到KMP,先说一下KMP这个名字是怎么来的,为什么叫做KMP呢。 + +因为是由这三位学者发明的:Knuth,Morris和Pratt,所以取了三位学者名字的首字母。所以叫做KMP + +# KMP有什么用 + +KMP主要应用在字符串匹配上。 + +KMP的主要思想是**当出现字符串不匹配时,可以知道一部分之前已经匹配的文本内容,可以利用这些信息避免从头再去做匹配了。** + +所以如何记录已经匹配的文本内容,是KMP的重点,也是next数组肩负的重任。 + +其实KMP的代码不好理解,一些同学甚至直接把KMP代码的模板背下来。 + +没有彻底搞懂,懵懵懂懂就把代码背下来太容易忘了。 + +不仅面试的时候可能写不出来,如果面试官问:**next数组里的数字表示的是什么,为什么这么表示?** + +估计大多数候选人都是懵逼的。 + +下面Carl就带大家把KMP的精髓,next数组弄清楚。 + +# 什么是前缀表 + +写过KMP的同学,一定都写过next数组,那么这个next数组究竟是个啥呢? + +next数组就是一个前缀表(prefix table)。 + +前缀表有什么作用呢? + +**前缀表是用来回退的,它记录了模式串与主串(文本串)不匹配的时候,模式串应该从哪里开始重新匹配。** + +为了清楚的了解前缀表的来历,我们来举一个例子: + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +请记住文本串和模式串的作用,对于理解下文很重要,要不然容易看懵。所以说三遍: + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +要在文本串:aabaabaafa 中查找是否出现过一个模式串:aabaaf。 + +如动画所示: + +![KMP详解1](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B21.gif) + +动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说道。 + +可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。 + +但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。 + +此时就要问了**前缀表是如何记录的呢?** + +首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。 + +那么什么是前缀表:**记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** + +# 最长公共前后缀? + +文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串; + +后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。 + +**正确理解什么是前缀什么是后缀很重要。** + +那么网上清一色都说 “kmp 最长公共前后缀” 又是什么回事呢? + + +我查了一遍 算法导论 和 算法4里KMP的章节,都没有提到 “最长公共前后缀”这个词,也不知道从哪里来了,我理解是用“最长相等前后缀” 准确一些。 + +**因为前缀表要求的就是相同前后缀的长度。** + +而最长公共前后缀里面的“公共”,更像是说前缀和后缀公共的长度。这其实并不是前缀表所需要的。 + +所以字符串a的最长相等前后缀为0。 +字符串aa的最长相等前后缀为1。 +字符串aaa的最长相等前后缀为2。 +等等.....。 + + +# 为什么一定要用前缀表 + +这就是前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢? + +回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图: +KMP精讲1 + + +然后就找到了下标2,指向b,继续匹配:如图: +KMP精讲2 + +以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要! + +**下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。** + +所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。 + +**很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。** + +# 如何计算前缀表 + +接下来就要说一说怎么计算前缀表。 + +如图: + +KMP精讲5 + +长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。) + +KMP精讲6 +长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。 + +KMP精讲7 +长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。 + +以此类推: +长度为前4个字符的子串`aaba`,最长相同前后缀的长度为1。 +长度为前5个字符的子串`aabaa`,最长相同前后缀的长度为2。 +长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。 + +那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图: +KMP精讲8 + +可以看出模式串与前缀表对应位置的数字表示的就是:**下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。** + +再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示: + +![KMP精讲2](https://code-thinking.cdn.bcebos.com/gifs/KMP%E7%B2%BE%E8%AE%B22.gif) + +找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。 + +为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。 + +所以要看前一位的 前缀表的数值。 + +前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。 + +最后就在文本串中找到了和模式串匹配的子串了。 + +# 前缀表与next数组 + +很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢? + +next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。 + +为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。 + +其实**这并不涉及到KMP的原理,而是具体实现,next数组即可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。** + +后面我会提供两种不同的实现代码,大家就明白了了。 + +# 使用next数组来匹配 + +以下我们以前缀表统一减一之后的next数组来做演示。 + +有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。 + +注意next数组是新前缀表(旧前缀表统一减一了)。 + +匹配过程动画如下: + +![KMP精讲4](https://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在字符串匹配中极大的提高的搜索的效率。** + +为了和[字符串: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指向后缀起始位置。 + +然后还要对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数组的函数代码如下: + +```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]; // 向前回退 +        } +        if (s[i] == s[j + 1]) { // 找到相同的前后缀 +            j++; +        } +        next[i] = j; // 将j(前缀的长度)赋给next[i] +    } +} +``` + + +代码构造next数组的逻辑流程动画如下: + +![KMP精讲3](https://code-thinking.cdn.bcebos.com/gifs/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数组,用模式串匹配文本串的整体代码如下: + +```C++ +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++代码实现 + +```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的实现为:(前缀表统一减一) + +```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]; // 向前回退 +        } +        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数组,代码如下: + +```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作为数组下标的操作 + j = next[j - 1]; // 注意这里,是要找前一位的对应的回退位置了 + } + if (s[i] == s[j]) { + j++; + } + next[i] = j; + } + } + +``` + +此时如果输入的模式串为aabaaf,对应的next为 0 1 0 1 2 0,(其实这就是前缀表的数值了)。 + +那么用这样的next数组也可以用来做匹配,代码要有所改动。 + +实现代码如下: + +```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]; + } + 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的每一个细微的细节都扣了出来,毫无遮掩的展示给大家了! + + + + +## 其他语言版本实现 + +```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=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; + } +} +``` + +------------------------ + +* 微信:[程序员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/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" "b/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" new file mode 100644 index 0000000000..222be7da48 --- /dev/null +++ "b/problems/0059.\350\236\272\346\227\213\347\237\251\351\230\265II.md" @@ -0,0 +1,230 @@ + + +

+ + + + +

+ +## 59.螺旋矩阵II + +题目地址:https://leetcode-cn.com/problems/spiral-matrix-ii/ +给定一个正整数 n,生成一个包含 1 到 n2 所有元素,且元素按顺时针顺序螺旋排列的正方形矩阵。 + +示例: + +输入: 3 +输出: +[ + [ 1, 2, 3 ], + [ 8, 9, 4 ], + [ 7, 6, 5 ] +] + +## 思路 + +这道题目可以说在面试中出现频率较高的题目,**本题并不涉及到什么算法,就是模拟过程,但却十分考察对代码的掌控能力。** + +要如何画出这个螺旋排列的正方形矩阵呢? + +相信很多同学刚开始做这种题目的时候,上来就是一波判断猛如虎。 + +结果运行的时候各种问题,然后开始各种修修补补,最后发现改了这里哪里有问题,改了那里这里又跑不起来了。 + +大家还记得我们在这篇文章[数组:每次遇到二分法,都是一看就会,一写就废](https://mp.weixin.qq.com/s/4X-8VRgnYRGd5LYGZ33m4w)中讲解了二分法,提到如果要写出正确的二分法一定要坚持**循环不变量原则**。 + +而求解本题依然是要坚持循环不变量原则。 + +模拟顺时针画矩阵的过程: + +* 填充上行从左到右 +* 填充右列从上到下 +* 填充下行从右到左 +* 填充左列从下到上 + +由外向内一圈一圈这么画下去。 + +可以发现这里的边界条件非常多,在一个循环中,如此多的边界条件,如果不按照固定规则来遍历,那就是**一进循环深似海,从此offer是路人**。 + +这里一圈下来,我们要画每四条边,这四条边怎么画,每画一条边都要坚持一致的左闭右开,或者左开又闭的原则,这样这一圈才能按照统一的规则画下来。 + +那么我按照左闭右开的原则,来画一圈,大家看一下: + +![螺旋矩阵](https://img-blog.csdnimg.cn/2020121623550681.png) + +这里每一种颜色,代表一条边,我们遍历的长度,可以看出每一个拐角处的处理规则,拐角处让给新的一条边来继续画。 + +这也是坚持了每条边左闭右开的原则。 + +一些同学做这道题目之所以一直写不好,代码越写越乱。 + +就是因为在画每一条边的时候,一会左开又闭,一会左闭右闭,一会又来左闭右开,岂能不乱。 + +代码如下,已经详细注释了每一步的目的,可以看出while循环里判断的情况是很多的,代码里处理的原则也是统一的左闭右开。 + +整体C++代码如下: + +```C++ +class Solution { +public: + vector> generateMatrix(int n) { + vector> res(n, vector(n, 0)); // 使用vector定义一个二维数组 + int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 + int loop = n / 2; // 每个圈循环几次,例如n为奇数3,那么loop = 1 只是循环一圈,矩阵中间的值需要单独处理 + int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(2, 2) + int count = 1; // 用来给矩阵中每一个空格赋值 + int offset = 1; // 每一圈循环,需要控制每一条边遍历的长度 + int i,j; + while (loop --) { + i = startx; + j = starty; + + // 下面开始的四个for就是模拟转了一圈 + // 模拟填充上行从左到右(左闭右开) + for (j = starty; j < starty + n - offset; j++) { + res[startx][j] = count++; + } + // 模拟填充右列从上到下(左闭右开) + for (i = startx; i < startx + n - offset; i++) { + res[i][j] = count++; + } + // 模拟填充下行从右到左(左闭右开) + for (; j > starty; j--) { + res[i][j] = count++; + } + // 模拟填充左列从下到上(左闭右开) + for (; i > startx; i--) { + res[i][j] = count++; + } + + // 第二圈开始的时候,起始位置要各自加1, 例如:第一圈起始位置是(0, 0),第二圈起始位置是(1, 1) + startx++; + starty++; + + // offset 控制每一圈里每一条边遍历的长度 + offset += 2; + } + + // 如果n为奇数的话,需要单独给矩阵最中间的位置赋值 + if (n % 2) { + res[mid][mid] = count; + } + return res; + } +}; +``` + +## 其他语言版本 + +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 +``` + +## 类似题目 + +* 54.螺旋矩阵 +* 剑指Offer 29.顺时针打印矩阵 + +------------------------ + +* 微信:[程序员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/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" "b/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" index d382cad45b..62aebb58aa 100644 --- "a/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" +++ "b/problems/0131.\345\210\206\345\211\262\345\233\236\346\226\207\344\270\262.md" @@ -5,6 +5,7 @@

+ > 切割问题其实是一种组合问题! ## 131.分割回文串 diff --git "a/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" "b/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" new file mode 100644 index 0000000000..9bfa12d267 --- /dev/null +++ "b/problems/0142.\347\216\257\345\275\242\351\223\276\350\241\250II.md" @@ -0,0 +1,212 @@ + +

+ + + + +

+ +# 142.环形链表II + +https://leetcode-cn.com/problems/linked-list-cycle-ii/ + +题意: +给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。 + +为了表示给定链表中的环,使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。 + +**说明**:不允许修改给定的链表。 + +![循环链表](https://img-blog.csdnimg.cn/20200816110112704.png) + +# 思路 + +这道题目,不仅考察对链表的操作,而且还需要一些数学运算。 + +主要考察两知识点: + +* 判断链表是否环 +* 如果有环,如何找到这个环的入口 + +## 判断链表是否有环 + +可以使用快慢指针法, 分别定义 fast 和 slow指针,从头结点出发,fast指针每次移动两个节点,slow指针每次移动一个节点,如果 fast 和 slow指针在途中相遇 ,说明这个链表有环。 + +为什么fast 走两个节点,slow走一个节点,有环的话,一定会在环内相遇呢,而不是永远的错开呢 + +首先第一点: **fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。** + +那么来看一下,**为什么fast指针和slow指针一定会相遇呢?** + +可以画一个环,然后让 fast指针在任意一个节点开始追赶slow指针。 + +会发现最终都是这种情况, 如下图: + +![142环形链表1](https://img-blog.csdnimg.cn/20210318162236720.png) + + +fast和slow各自再走一步, fast和slow就相遇了 + +这是因为fast是走两步,slow是走一步,**其实相对于slow来说,fast是一个节点一个节点的靠近slow的**,所以fast一定可以和slow重合。 + +动画如下: + +![141.环形链表](https://tva1.sinaimg.cn/large/008eGmZEly1goo4xglk9yg30fs0b6u0x.gif) + + +## 如果有环,如何找到这个环的入口 + +**此时已经可以判断链表是否有环了,那么接下来要找这个环的入口了。** + +假设从头结点到环形入口节点 的节点数为x。 +环形入口节点到 fast指针与slow指针相遇节点 节点数为y。 +从相遇节点 再到环形入口节点节点数为 z。 如图所示: + +![142环形链表2](https://img-blog.csdnimg.cn/20210318162938397.png) + +那么相遇时: +slow指针走过的节点数为: `x + y`, +fast指针走过的节点数:` x + y + n (y + z)`,n为fast指针在环内走了n圈才遇到slow指针, (y+z)为 一圈内节点的个数A。 + +因为fast指针是一步走两个节点,slow指针一步走一个节点, 所以 fast指针走过的节点数 = slow指针走过的节点数 * 2: + +`(x + y) * 2 = x + y + n (y + z)` + +两边消掉一个(x+y): `x + y = n (y + z) ` + +因为要找环形的入口,那么要求的是x,因为x表示 头结点到 环形入口节点的的距离。 + +所以要求x ,将x单独放在左面:`x = n (y + z) - y` , + +再从n(y+z)中提出一个 (y+z)来,整理公式之后为如下公式:`x = (n - 1) (y + z) + z ` 注意这里n一定是大于等于1的,因为 fast指针至少要多走一圈才能相遇slow指针。 + +这个公式说明什么呢? + +先拿n为1的情况来举例,意味着fast指针在环形里转了一圈之后,就遇到了 slow指针了。 + +当 n为1的时候,公式就化解为 `x = z`, + +这就意味着,**从头结点出发一个指针,从相遇节点 也出发一个指针,这两个指针每次只走一个节点, 那么当这两个指针相遇的时候就是 环形入口的节点**。 + + +也就是在相遇节点处,定义一个指针index1,在头结点处定一个指针index2。 + +让index1和index2同时移动,每次移动一个节点, 那么他们相遇的地方就是 环形入口的节点。 + +动画如下: + +![142.环形链表II(求入口)](https://tva1.sinaimg.cn/large/008eGmZEly1goo58gauidg30fw0bi4qr.gif) + + +那么 n如果大于1是什么情况呢,就是fast指针在环形转n圈之后才遇到 slow指针。 + +其实这种情况和n为1的时候 效果是一样的,一样可以通过这个方法找到 环形的入口节点,只不过,index1 指针在环里 多转了(n-1)圈,然后再遇到index2,相遇点依然是环形的入口节点。 + + +# C++代码 + +``` +/** + * Definition for singly-linked list. + * struct ListNode { + * int val; + * ListNode *next; + * ListNode(int x) : val(x), next(NULL) {} + * }; + */ +class Solution { +public: + ListNode *detectCycle(ListNode *head) { + ListNode* fast = head; + ListNode* slow = head; + while(fast != NULL && fast->next != NULL) { + slow = slow->next; + fast = fast->next->next; + // 快慢指针相遇,此时从head 和 相遇点,同时查找直至相遇 + if (slow == fast) { + ListNode* index1 = fast; + ListNode* index2 = head; + while (index1 != index2) { + index1 = index1->next; + index2 = index2->next; + } + return index2; // 返回环的入口 + } + } + return NULL; + } +}; +``` + +## 补充 + +在推理过程中,大家可能有一个疑问就是:**为什么第一次在环中相遇,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。 + +**也就是说slow一定没有走到环入口3,而fast已经到环入口3了**。 + +这说明什么呢? + +**在slow开始走的那一环已经和fast相遇了**。 + +那有同学又说了,为什么fast不能跳过去呢? 在刚刚已经说过一次了,**fast相对于slow是一次移动一个节点,所以不可能跳过去**。 + +好了,这次把为什么第一次在环中相遇,slow的 步数 是 x+y 而不是 x + 若干环的长度 + y ,用数学推理了一下,算是对[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)的补充。 + +# 总结 + +这次可以说把环形链表这道题目的各个细节,完完整整的证明了一遍,说这是全网最详细讲解不为过吧,哈哈。 + +## 其他语言补充 + +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 +``` + +欢迎大家补充其他语言的版本实现哈 + +------------------------ + +* 微信:[程序员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/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" "b/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" new file mode 100644 index 0000000000..8e4832d20b --- /dev/null +++ "b/problems/0151.\347\277\273\350\275\254\345\255\227\347\254\246\344\270\262\351\207\214\347\232\204\345\215\225\350\257\215.md" @@ -0,0 +1,214 @@ + +

+ + + + +

+ + +> 综合考察字符串操作的好题。 + +# 151.翻转字符串里的单词 + +https://leetcode-cn.com/problems/reverse-words-in-a-string/ + +给定一个字符串,逐个翻转字符串中的每个单词。 + +示例 1: +输入: "the sky is blue" +输出: "blue is sky the" + +示例 2: +输入: "  hello world!  " +输出: "world! hello" +解释: 输入字符串可以在前面或者后面包含多余的空格,但是反转后的字符不能包括。 + +示例 3: +输入: "a good   example" +输出: "example good a" +解释: 如果两个单词间有多余的空格,将反转后单词间的空格减少到只含一个。 + + +# 思路 + +**这道题目可以说是综合考察了字符串的多种操作。** + + +一些同学会使用split库函数,分隔单词,然后定义一个新的string字符串,最后再把单词倒序相加,那么这道题题目就是一道水题了,失去了它的意义。 + +所以这里我还是提高一下本题的难度:**不要使用辅助空间,空间复杂度要求为O(1)。** + +不能使用辅助空间之后,那么只能在原字符串上下功夫了。 + +想一下,我们将整个字符串都反转过来,那么单词的顺序指定是倒序了,只不过单词本身也倒叙了,那么再把单词反转一下,单词不就正过来了。 + +所以解题思路如下: + +* 移除多余空格 +* 将整个字符串反转 +* 将每个单词反转 + +如动画所示: + +![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] == ' ') { + s.erase(s.begin() + i); + } + } + // 删除字符串最后面的空格 + if (s.size() > 0 && s[s.size() - 1] == ' ') { + s.erase(s.begin() + s.size() - 1); + } + // 删除字符串最前面的空格 + if (s.size() > 0 && s[0] == ' ') { + s.erase(s.begin()); + } +} +``` + +逻辑很简单,从前向后遍历,遇到空格了就erase。 + +如果不仔细琢磨一下erase的时间复杂读,还以为以上的代码是O(n)的时间复杂度呢。 + +想一下真正的时间复杂度是多少,一个erase本来就是O(n)的操作,erase实现原理题目:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA),最优的算法来移除元素也要O(n)。 + +erase操作上面还套了一个for循环,那么以上代码移除冗余空格的代码时间复杂度为O(n^2)。 + +那么使用双指针法来去移除空格,最后resize(重新设置)一下字符串的大小,就可以做到O(n)的时间复杂度。 + +如果对这个操作比较生疏了,可以再看一下这篇文章:[数组:就移除个元素很难么?](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA)是如何移除元素的。 + +那么使用双指针来移除冗余空格代码如下: fastIndex走的快,slowIndex走的慢,最后slowIndex就标记着移除多余空格后新字符串的长度。 + +```C++ +void removeExtraSpaces(string& s) { + int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 + // 去掉字符串前面的空格 + while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') { + fastIndex++; + } + for (; fastIndex < s.size(); fastIndex++) { + // 去掉字符串中间部分的冗余空格 + if (fastIndex - 1 > 0 + && s[fastIndex - 1] == s[fastIndex] + && s[fastIndex] == ' ') { + continue; + } else { + s[slowIndex++] = s[fastIndex]; + } + } + if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格 + s.resize(slowIndex - 1); + } else { + s.resize(slowIndex); // 重新设置字符串大小 + } +} +``` + +有的同学可能发现用erase来移除空格,在leetcode上性能也还行。主要是以下几点;: + +1. leetcode上的测试集里,字符串的长度不够长,如果足够长,性能差距会非常明显。 +2. leetcode的测程序耗时不是很准确的。 + +此时我们已经实现了removeExtraSpaces函数来移除冗余空格。 + +还做实现反转字符串的功能,支持反转字符串子区间,这个实现我们分别在[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)和[字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw)里已经讲过了。 + +代码如下: + +``` +// 反转字符串s中左闭又闭的区间[start, end] +void reverse(string& s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + swap(s[i], s[j]); + } +} +``` + +## 本题C++整体代码 + +效率: + + + +``` +class Solution { +public: + // 反转字符串s中左闭又闭的区间[start, end] + void reverse(string& s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + swap(s[i], s[j]); + } + } + + // 移除冗余空格:使用双指针(快慢指针法)O(n)的算法 + void removeExtraSpaces(string& s) { + int slowIndex = 0, fastIndex = 0; // 定义快指针,慢指针 + // 去掉字符串前面的空格 + while (s.size() > 0 && fastIndex < s.size() && s[fastIndex] == ' ') { + fastIndex++; + } + for (; fastIndex < s.size(); fastIndex++) { + // 去掉字符串中间部分的冗余空格 + if (fastIndex - 1 > 0 + && s[fastIndex - 1] == s[fastIndex] + && s[fastIndex] == ' ') { + continue; + } else { + s[slowIndex++] = s[fastIndex]; + } + } + if (slowIndex - 1 > 0 && s[slowIndex - 1] == ' ') { // 去掉字符串末尾的空格 + s.resize(slowIndex - 1); + } else { + s.resize(slowIndex); // 重新设置字符串大小 + } + } + + string reverseWords(string s) { + removeExtraSpaces(s); // 去掉冗余空格 + reverse(s, 0, s.size() - 1); // 将字符串全部反转 + int start = 0; // 反转的单词在字符串里起始位置 + int end = 0; // 反转的单词在字符串里终止位置 + bool entry = false; // 标记枚举字符串的过程中是否已经进入了单词区间 + for (int i = 0; i < s.size(); i++) { // 开始反转单词 + if ((!entry) || (s[i] != ' ' && s[i - 1] == ' ')) { + start = i; // 确定单词起始位置 + entry = true; // 进入单词区间 + } + // 单词后面有空格的情况,空格就是分词符 + if (entry && s[i] == ' ' && s[i - 1] != ' ') { + end = i - 1; // 确定单词终止位置 + entry = false; // 结束单词区间 + reverse(s, start, end); + } + // 最后一个结尾单词之后没有空格的情况 + if (entry && (i == (s.size() - 1)) && s[i] != ' ' ) { + end = i;// 确定单词终止位置 + entry = false; // 结束单词区间 + reverse(s, start, end); + } + } + return s; + } +}; +``` + + +------------------------ + +* 微信:[程序员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/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" "b/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" new file mode 100644 index 0000000000..430d58464c --- /dev/null +++ "b/problems/0203.\347\247\273\351\231\244\351\223\276\350\241\250\345\205\203\347\264\240.md" @@ -0,0 +1,140 @@ + +

+ + + + +

+ +> 链表操作中,可以使用原链表来直接进行删除操作,也可以设置一个虚拟头结点在进行删除操作,接下来看一看哪种方式更方便。 + +# 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指针直接指向下下一个节点就可以了, + +那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢? + +这里就涉及如下链表操作的两种方式: +* **直接使用原来的链表来进行删除操作。** +* **设置一个虚拟头结点在进行删除操作。** + + +来看第一种操作:直接使用原来的链表来进行移除。 + +![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。 + +这样是不是就可以使用和移除链表其他节点的方式统一了呢? + +来看一下,如何移除元素1 呢,还是熟悉的方式,然后从内存中删除元素1。 + +最后呢在题目中,return 头结点的时候,别忘了 `return dummyNode->next;`, 这才是新的头结点 + + +# C++代码 + +**直接使用原来的链表来进行移除节点操作:** + +```C++ +class Solution { +public: + ListNode* removeElements(ListNode* head, int val) { + // 删除头结点 + while (head != NULL && head->val == val) { // 注意这里不是if + ListNode* tmp = head; + head = head->next; + delete tmp; + } + + // 删除非头结点 + ListNode* cur = head; + while (cur != NULL && cur->next!= NULL) { + if (cur->next->val == val) { + ListNode* tmp = cur->next; + cur->next = cur->next->next; + delete tmp; + } else { + cur = cur->next; + } + } + return head; + } +}; +``` + +**设置一个虚拟头结点在进行移除节点操作:** + +```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; + cur->next = cur->next->next; + delete tmp; + } else { + cur = cur->next; + } + } + head = dummyHead->next; + delete dummyHead; + return head; + } +}; + +``` + +------------------------ + +* 微信:[程序员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/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" "b/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" new file mode 100644 index 0000000000..f20d95d29f --- /dev/null +++ "b/problems/0206.\347\277\273\350\275\254\351\223\276\350\241\250.md" @@ -0,0 +1,104 @@ + +

+ + + + +

+ +> 反转链表的写法很简单,一些同学甚至可以背下来但过一阵就忘了该咋写,主要是因为没有理解真正的反转过程。 + +# 206.反转链表 + +https://leetcode-cn.com/problems/reverse-linked-list/ + +题意:反转一个单链表。 + +示例: +输入: 1->2->3->4->5->NULL +输出: 5->4->3->2->1->NULL + +# 思路 + +如果再定义一个新的链表,实现链表元素的反转,其实这是对内存空间的浪费。 + +其实只需要改变链表的next指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示: + +![206_反转链表](https://img-blog.csdnimg.cn/20210218090901207.png) + +之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改表next指针的方向。 + +那么接下来看一看是如何反转呢? + +我们拿有示例中的链表来举例,如动画所示: + +![](https://tva1.sinaimg.cn/large/008eGmZEly1gnrf1oboupg30gy0c44qp.gif) + +首先定义一个cur指针,指向头结点,再定义一个pre指针,初始化为null。 + +然后就要开始反转了,首先要把 cur->next 节点用tmp指针保存一下,也就是保存一下这个节点。 + +为什么要保存一下这个节点呢,因为接下来要改变 cur->next 的指向了,将cur->next 指向pre ,此时已经反转了第一个节点了。 + +接下来,就是循环走如下代码逻辑了,继续移动pre和cur指针。 + +最后,cur 指针已经指向了null,循环结束,链表也反转完毕了。 此时我们return pre指针就可以了,pre指针就指向了新的头结点。 + +# C++代码 + +## 双指针法 +```C++ +class Solution { +public: + ListNode* reverseList(ListNode* head) { + ListNode* temp; // 保存cur的下一个节点 + ListNode* cur = head; + ListNode* pre = NULL; + while(cur) { + temp = cur->next; // 保存一下 cur的下一个节点,因为接下来要改变cur->next + cur->next = pre; // 翻转操作 + // 更新pre 和 cur指针 + pre = cur; + cur = temp; + } + return pre; + } +}; +``` + +## 递归法 + +递归法相对抽象一些,但是其实和双指针法是一样的逻辑,同样是当cur为空的时候循环结束,不断将cur指向pre的过程。 + +关键是初始化的地方,可能有的同学会不理解, 可以看到双指针法中初始化 cur = head,pre = NULL,在递归法中可以从如下代码看出初始化的逻辑也是一样的,只不过写法变了。 + +具体可以看代码(已经详细注释),**双指针法写出来之后,理解如下递归写法就不难了,代码逻辑都是一样的。** +```C++ +class Solution { +public: + ListNode* reverse(ListNode* pre,ListNode* cur){ + if(cur == NULL) return pre; + ListNode* temp = cur->next; + cur->next = pre; + // 可以和双指针法的代码进行对比,如下递归的写法,其实就是做了这两步 + // pre = cur; + // cur = temp; + return reverse(cur,temp); + } + ListNode* reverseList(ListNode* head) { + // 和双指针法初始化是一样的逻辑 + // ListNode* cur = head; + // ListNode* pre = NULL; + return reverse(NULL, head); + } + +}; +``` + +------------------------ + +* 微信:[程序员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/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" "b/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" new file mode 100644 index 0000000000..462f065515 --- /dev/null +++ "b/problems/0209.\351\225\277\345\272\246\346\234\200\345\260\217\347\232\204\345\255\220\346\225\260\347\273\204.md" @@ -0,0 +1,150 @@ + +

+ + + + +

+ +## 209.长度最小的子数组 + +题目链接: https://leetcode-cn.com/problems/minimum-size-subarray-sum/ + +给定一个含有 n 个正整数的数组和一个正整数 s ,找出该数组中满足其和 ≥ s 的长度最小的 连续 子数组,并返回其长度。如果不存在符合条件的子数组,返回 0。 + +示例: + +输入: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) { + int result = INT32_MAX; // 最终的结果 + int sum = 0; // 子序列的数值之和 + int subLength = 0; // 子序列的长度 + for (int i = 0; i < nums.size(); i++) { // 设置子序列起点为i + sum = 0; + for (int j = i; j < nums.size(); j++) { // 设置子序列终止位置为j + sum += nums[j]; + if (sum >= s) { // 一旦发现子序列和超过了s,更新result + subLength = j - i + 1; // 取子序列的长度 + result = result < subLength ? result : subLength; + break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break + } + } + } + // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 + return result == INT32_MAX ? 0 : result; + } +}; +``` +时间复杂度:$O(n^2)$ +空间复杂度:$O(1)$ + +## 滑动窗口 + +接下来就开始介绍数组操作中另一个重要的方法:**滑动窗口**。 + +所谓滑动窗口,**就是不断的调节子序列的起始位置和终止位置,从而得出我们要想的结果**。 + +这里还是以题目中的示例来举例,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 是最短距离。 + +其实从动画中可以发现滑动窗口也可以理解为双指针法的一种!只不过这种解法更像是一个窗口的移动,所以叫做滑动窗口更适合一些。 + +在本题中实现滑动窗口,主要确定如下三点: + +* 窗口内是什么? +* 如何移动窗口的起始位置? +* 如何移动窗口的结束位置? + +窗口就是 满足其和 ≥ s 的长度最小的 连续 子数组。 + +窗口的起始位置如何移动:如果当前窗口的值大于s了,窗口就要向前移动了(也就是该缩小了)。 + +窗口的结束位置如何移动:窗口的结束位置就是遍历数组的指针,窗口的起始位置设置为数组的起始位置就可以了。 + +解题的关键在于 窗口的起始位置如何移动,如图所示: + +![leetcode_209](https://img-blog.csdnimg.cn/20210312160441942.png) + +可以发现**滑动窗口的精妙之处在于根据当前子序列和大小的情况,不断调节子序列的起始位置。从而将O(n^2)的暴力解法降为O(n)。** + +C++代码如下: + +```C++ +class Solution { +public: + int minSubArrayLen(int s, vector& nums) { + int result = INT32_MAX; + int sum = 0; // 滑动窗口数值之和 + int i = 0; // 滑动窗口起始位置 + int subLength = 0; // 滑动窗口的长度 + for (int j = 0; j < nums.size(); j++) { + sum += nums[j]; + // 注意这里使用while,每次更新 i(起始位置),并不断比较子序列是否符合条件 + while (sum >= s) { + subLength = (j - i + 1); // 取子序列的长度 + result = result < subLength ? result : subLength; + sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置) + } + } + // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 + return result == INT32_MAX ? 0 : result; + } +}; +``` + +时间复杂度:$O(n)$ +空间复杂度:$O(1)$ + +**一些录友会疑惑为什么时间复杂度是O(n)**。 + +不要以为for里放一个while就以为是$O(n^2)$啊, 主要是看每一个元素被操作的次数,每个元素在滑动窗后进来操作一次,出去操作一次,每个元素都是被被操作两次,所以时间复杂度是2 * n 也就是$O(n)$。 + +## 其他语言补充 + +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](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/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" "b/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" index d4d02b59b6..0f4a4e1a58 100644 --- "a/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" +++ "b/problems/0216.\347\273\204\345\220\210\346\200\273\345\222\214III.md" @@ -7,6 +7,7 @@

+---------------------- > 别看本篇选的是组合总和III,而不是组合总和,本题和上一篇[回溯算法:求组合问题!](https://mp.weixin.qq.com/s/OnBjbLzuipWz_u4QfmgcqQ)相比难度刚刚好! @@ -219,12 +220,11 @@ public: 相信做完本题,大家对组合问题应该有初步了解了。 + ------------------------ * 微信:[程序员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/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" "b/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 0000000000..e6cac6391a --- /dev/null +++ "b/problems/0344.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,143 @@ + +

+ + + + +

+ +---------------------- + +> 打基础的时候,不要太迷恋于库函数。 + +# 344.反转字符串 + +https://leetcode-cn.com/problems/reverse-string/ + +编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 char[] 的形式给出。 + +不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。 + +你可以假设数组中的所有字符都是 ASCII 码表中的可打印字符。 + +示例 1: + +输入:["h","e","l","l","o"] +输出:["o","l","l","e","h"] +示例 2: + +输入:["H","a","n","n","a","h"] +输出:["h","a","n","n","a","H"] + + +# 思路 + +先说一说题外话: + +对于这道题目一些同学直接用C++里的一个库函数 reverse,调一下直接完事了, 相信每一门编程语言都有这样的库函数。 + +如果这么做题的话,这样大家不会清楚反转字符串的实现原理了。 + +但是也不是说库函数就不能用,是要分场景的。 + +如果在现场面试中,我们什么时候使用库函数,什么时候不要用库函数呢? + +**如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。** + +毕竟面试官一定不是考察你对库函数的熟悉程度, 如果使用python和java 的同学更需要注意这一点,因为python、java提供的库函数十分丰富。 + +**如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。** + +建议大家平时在leetcode上练习算法的时候本着这样的原则去练习,这样才有助于我们对算法的理解。 + +不要沉迷于使用库函数一行代码解决题目之类的技巧,不是说这些技巧不好,而是说这些技巧可以用来娱乐一下。 + +真正自己写的时候,要保证理解可以实现是相应的功能。 + +接下来再来讲一下如何解决反转字符串的问题。 + +大家应该还记得,我们已经讲过了[206.反转链表](https://mp.weixin.qq.com/s/pnvVP-0ZM7epB8y3w_Njwg)。 + +在反转链表中,使用了双指针的方法。 + +那么反转字符串依然是使用双指针的方法,只不过对于字符串的反转,其实要比链表简单一些。 + +因为字符串也是一种数组,所以元素在内存中是连续分布,这就决定了反转链表和反转字符串方式上还是有所差异的。 + +如果对数组和链表原理不清楚的同学,可以看这两篇,[关于链表,你该了解这些!](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]); + } +} +``` + +循环里只要做交换s[i] 和s[j]操作就可以了,那么我这里使用了swap 这个库函数。大家可以使用。 + +因为相信大家都知道交换函数如何实现,而且这个库函数仅仅是解题中的一部分, 所以这里使用库函数也是可以的。 + +swap可以有两种实现。 + +一种就是常见的交换数值: + +```C++ +int tmp = s[i]; +s[i] = s[j]; +s[j] = tmp; + +``` + +一种就是通过位运算: + +```C++ +s[i] ^= s[j]; +s[j] ^= s[i]; +s[i] ^= s[j]; + +``` + +这道题目还是比较简单的,但是我正好可以通过这道题目说一说在刷题的时候,使用库函数的原则。 + +如果题目关键的部分直接用库函数就可以解决,建议不要使用库函数。 + +如果库函数仅仅是 解题过程中的一小部分,并且你已经很清楚这个库函数的内部实现原理的话,可以考虑使用库函数。 + +本着这样的原则,我没有使用reverse库函数,而使用swap库函数。 + +**在字符串相关的题目中,库函数对大家的诱惑力是非常大的,因为会有各种反转,切割取词之类的操作**,这也是为什么字符串的库函数这么丰富的原因。 + +相信大家本着我所讲述的原则来做字符串相关的题目,在选择库函数的角度上会有所原则,也会有所收获。 + + +## C++代码 + +```C++ +class Solution { +public: + void reverseString(vector& s) { + for (int i = 0, j = s.size() - 1; i < s.size()/2; i++, j--) { + swap(s[i],s[j]); + } + } +}; +``` + + +------------------------ + +* 微信:[程序员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/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 0000000000..c6d814430b --- /dev/null +++ "b/problems/0459.\351\207\215\345\244\215\347\232\204\345\255\220\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,154 @@ + +

+ + + + +

+ +---------------------- + + +> KMP算法还能干这个 + +# 459.重复的子字符串 + +https://leetcode-cn.com/problems/repeated-substring-pattern/ + +给定一个非空的字符串,判断它是否可以由它的一个子串重复多次构成。给定的字符串只含有小写英文字母,并且长度不超过10000。 + +示例 1: +输入: "abab" +输出: True +解释: 可由子字符串 "ab" 重复两次构成。 + +示例 2: +输入: "aba" +输出: False + +示例 3: +输入: "abcabcabcabc" +输出: True +解释: 可由子字符串 "abc" 重复四次构成。 (或者子字符串 "abcabc" 重复两次构成。) + +# 思路 + +这又是一道标准的KMP的题目。 + +如果KMP还不够了解,可以看我的B站: + +* [帮你把KMP算法学个通透!B站(理论篇)](https://www.bilibili.com/video/BV1PD4y1o7nd/) +* [帮你把KMP算法学个通透!(求next数组代码篇)](https://www.bilibili.com/video/BV1M5411j7Xx) + + +如果KMP还不够了解,可以看我的这个视频[帮你把KMP算法学个通透!B站](https://www.bilibili.com/video/BV1PD4y1o7nd/) + +我们在[字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg)里提到了,在一个串中查找是否出现过另一个串,这是KMP的看家本领。 + +那么寻找重复子串怎么也涉及到KMP算法了呢? + +这里就要说一说next数组了,next 数组记录的就是最长相同前后缀( [字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ) 这里介绍了什么是前缀,什么是后缀,什么又是最长相同前后缀), 如果 next[len - 1] != -1,则说明字符串有最长相同的前后缀(就是字符串里的前缀子串和后缀子串相同的最长长度)。 + +最长相等前后缀的长度为:next[len - 1] + 1。 + +数组长度为: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的最长相同前后缀的长度。 + + +(len - (next[len - 1] + 1)) 也就是: 12(字符串的长度) - 8(最长公共前后缀的长度) = 4, 4正好可以被 12(字符串的长度) 整除,所以说明有重复的子字符串(asdf)。 + + +代码如下:(这里使用了前缀表统一减一的实现方式) + +```C++ +class Solution { +public: + void getNext (int* next, const string& s){ + next[0] = -1; + int j = -1; + for(int i = 1;i < s.size(); i++){ + while(j >= 0 && s[i] != s[j+1]) { + j = next[j]; + } + if(s[i] == s[j+1]) { + j++; + } + next[i] = j; + } + } + bool repeatedSubstringPattern (string s) { + if (s.size() == 0) { + return false; + } + int next[s.size()]; + getNext(next, s); + int len = s.size(); + if (next[len - 1] != -1 && len % (len - (next[len - 1] + 1)) == 0) { + return true; + } + return false; + } +}; +``` + + +前缀表(不减一)的代码实现 + +```C++ +class Solution { +public: + void getNext (int* next, const string& s){ + next[0] = 0; + int j = 0; + for(int i = 1;i < s.size(); i++){ + while(j > 0 && s[i] != s[j]) { + j = next[j - 1]; + } + if(s[i] == s[j]) { + j++; + } + next[i] = j; + } + } + bool repeatedSubstringPattern (string s) { + if (s.size() == 0) { + return false; + } + int next[s.size()]; + getNext(next, s); + int len = s.size(); + if (next[len - 1] != 0 && len % (len - (next[len - 1] )) == 0) { + return true; + } + return false; + } +}; +``` + +# 拓展 + +此时我们已经分享了三篇KMP的文章,首先是[字符串:KMP是时候上场了(一文读懂系列)](https://mp.weixin.qq.com/s/70OXnZ4Ez29CKRrUpVJmug)讲解KMP算法的基础理论,给出next数组究竟是如何来了,前缀表又是怎么回事,为什么要选择前缀表。 + +然后通过[字符串:都来看看KMP的看家本领!](https://mp.weixin.qq.com/s/Gk9FKZ9_FSWLEkdGrkecyg)讲解一道KMP的经典题目,判断文本串里是否出现过模式串,这里涉及到构造next数组的代码实现,以及使用next数组完成模式串与文本串的匹配过程。 + +后来很多同学反馈说:搞不懂前后缀,什么又是最长相同前后缀(最长公共前后缀我认为这个用词不准确),以及为什么前缀表要统一减一(右移)呢,不减一行不行?针对这些问题,我在[字符串:听说你对KMP有这些疑问?](https://mp.weixin.qq.com/s/mqx6IM2AO4kLZwvXdPtEeQ)中又给出了详细的讲解。 + +------------------------ + +* 微信:[程序员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/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" "b/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" new file mode 100644 index 0000000000..ad03193da4 --- /dev/null +++ "b/problems/0541.\345\217\215\350\275\254\345\255\227\347\254\246\344\270\262II.md" @@ -0,0 +1,103 @@ + +

+ + + + +

+ + +> 简单的反转还不够,我要花式反转 + +# 541. 反转字符串II + +https://leetcode-cn.com/problems/reverse-string-ii/ + +给定一个字符串 s 和一个整数 k,你需要对从字符串开头算起的每隔 2k 个字符的前 k 个字符进行反转。 + +如果剩余字符少于 k 个,则将剩余字符全部反转。 + +如果剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符,其余字符保持原样。 + +示例: + +输入: s = "abcdefg", k = 2 +输出: "bacdfeg" + +# 思路 + +这道题目其实也是模拟,实现题目中规定的反转规则就可以了。 + +一些同学可能为了处理逻辑:每隔2k个字符的前k的字符,写了一堆逻辑代码或者再搞一个计数器,来统计2k,再统计前k个字符。 + +其实在遍历字符串的过程中,只要让 i += (2 * k),i 每次移动 2 * k 就可以了,然后判断是否需要有反转的区间。 + +因为要找的也就是每2 * k 区间的起点,这样写,程序会高效很多。 + +**所以当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。** + +性能如下: + + +那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用reverse来实现反转也没毛病,毕竟不是解题关键部分。 + +# C++代码 + +使用C++库函数reverse的版本如下: + +``` +class Solution { +public: + string reverseStr(string s, int k) { + for (int i = 0; i < s.size(); i += (2 * k)) { + // 1. 每隔 2k 个字符的前 k 个字符进行反转 + // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 + if (i + k <= s.size()) { + reverse(s.begin() + i, s.begin() + i + k ); + continue; + } + // 3. 剩余字符少于 k 个,则将剩余字符全部反转。 + reverse(s.begin() + i, s.begin() + s.size()); + } + return s; + } +}; +``` + +那么我们也可以实现自己的reverse函数,其实和题目[344. 反转字符串](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA)道理是一样的。 + +下面我实现的reverse函数区间是左闭右闭区间,代码如下: +``` +class Solution { +public: + void reverse(string& s, int start, int end) { + for (int i = start, j = end; i < j; i++, j--) { + swap(s[i], s[j]); + } + } + string reverseStr(string s, int k) { + for (int i = 0; i < s.size(); i += (2 * k)) { + // 1. 每隔 2k 个字符的前 k 个字符进行反转 + // 2. 剩余字符小于 2k 但大于或等于 k 个,则反转前 k 个字符 + if (i + k <= s.size()) { + reverse(s, i, i + k - 1); + continue; + } + // 3. 剩余字符少于 k 个,则将剩余字符全部反转。 + reverse(s, i, s.size() - 1); + } + return s; + } +}; +``` + + + +------------------------ + +* 微信:[程序员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/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" "b/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" new file mode 100644 index 0000000000..30f9112b39 --- /dev/null +++ "b/problems/0707.\350\256\276\350\256\241\351\223\276\350\241\250.md" @@ -0,0 +1,159 @@ + +

+ + + + +

+ +> 听说这道题目把链表常见的五个操作都覆盖了? + +# 707.设计链表 + +https://leetcode-cn.com/problems/design-linked-list/ + +题意: + +在链表类中实现这些功能: + +* get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。 +* addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。 +* addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。 +* addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val  的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。 +* deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。 + + +![707示例](https://img-blog.csdnimg.cn/20200814200558953.png) + +# 思路 + +如果对链表的基础知识还不太懂,可以看这篇文章:[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ) + +如果对链表的虚拟头结点不清楚,可以看这篇文章:[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA) + +删除链表节点: +![链表-删除节点](https://img-blog.csdnimg.cn/20200806195114541.png) + +添加链表节点: +![链表-添加节点](https://img-blog.csdnimg.cn/20200806195134331.png) + +这道题目设计链表的五个接口: +* 获取链表第index个节点的数值 +* 在链表的最前面插入一个节点 +* 在链表的最后面插入一个节点 +* 在链表第index个节点前面插入一个节点 +* 删除链表的第index个节点 + +可以说这五个接口,已经覆盖了链表的常见操作,是练习链表操作非常好的一道题目 + +**链表操作的两种方式:** + +1. 直接使用原来的链表来进行操作。 +2. 设置一个虚拟头结点在进行操作。 + +下面采用的设置一个虚拟头结点(这样更方便一些,大家看代码就会感受出来)。 + + +## 代码 +```C++ +class MyLinkedList { +public: + // 定义链表节点结构体 + struct LinkedNode { + int val; + LinkedNode* next; + LinkedNode(int val):val(val), next(nullptr){} + }; + + // 初始化链表 + MyLinkedList() { + _dummyHead = new LinkedNode(0); // 这里定义的头结点 是一个虚拟头结点,而不是真正的链表头结点 + _size = 0; + } + + // 获取到第index个节点数值,如果index是非法数值直接返回-1, 注意index是从0开始的,第0个节点就是头结点 + int get(int index) { + if (index > (_size - 1) || index < 0) { + return -1; + } + LinkedNode* cur = _dummyHead->next; + while(index--){ // 如果--index 就会陷入死循环 + cur = cur->next; + } + return cur->val; + } + + // 在链表最前面插入一个节点,插入完成后,新插入的节点为链表的新的头结点 + void addAtHead(int val) { + LinkedNode* newNode = new LinkedNode(val); + newNode->next = _dummyHead->next; + _dummyHead->next = newNode; + _size++; + } + + // 在链表最后面添加一个节点 + void addAtTail(int val) { + LinkedNode* newNode = new LinkedNode(val); + LinkedNode* cur = _dummyHead; + while(cur->next != nullptr){ + cur = cur->next; + } + cur->next = newNode; + _size++; + } + + // 在第index个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 + // 如果index 等于链表的长度,则说明是新插入的节点为链表的尾结点 + // 如果index大于链表的长度,则返回空 + void addAtIndex(int index, int val) { + if (index > _size) { + return; + } + LinkedNode* newNode = new LinkedNode(val); + LinkedNode* cur = _dummyHead; + while(index--) { + cur = cur->next; + } + newNode->next = cur->next; + cur->next = newNode; + _size++; + } + + // 删除第index个节点,如果index 大于等于链表的长度,直接return,注意index是从0开始的 + void deleteAtIndex(int index) { + if (index >= _size || index < 0) { + return; + } + LinkedNode* cur = _dummyHead; + while(index--) { + cur = cur ->next; + } + LinkedNode* tmp = cur->next; + cur->next = cur->next->next; + delete tmp; + _size--; + } + + // 打印链表 + void printLinkedList() { + LinkedNode* cur = _dummyHead; + while (cur->next != nullptr) { + cout << cur->next->val << " "; + cur = cur->next; + } + cout << endl; + } +private: + int _size; + LinkedNode* _dummyHead; + +}; +``` + +------------------------ + +* 微信:[程序员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/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" "b/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" new file mode 100644 index 0000000000..936e229b5f --- /dev/null +++ "b/problems/\345\211\221\346\214\207Offer05.\346\233\277\346\215\242\347\251\272\346\240\274.md" @@ -0,0 +1,131 @@ + +

+ + + + +

+ +# 题目:剑指Offer 05.替换空格 + +https://leetcode-cn.com/problems/ti-huan-kong-ge-lcof/ + +请实现一个函数,把字符串 s 中的每个空格替换成"%20"。 + +示例 1: +输入:s = "We are happy." +输出:"We%20are%20happy." + +# 思路 + +如果想把这道题目做到极致,就不要只用额外的辅助空间了! + +首先扩充数组到每个空格替换成"%20"之后的大小。 + +然后从后向前替换空格,也就是双指针法,过程如下: + +i指向新长度的末尾,j指向旧长度的末尾。 + +![替换空格](https://tva1.sinaimg.cn/large/e6c9d24ely1go6qmevhgpg20du09m4qp.gif) + +有同学问了,为什么要从后向前填充,从前向后填充不行么? + +从前向后填充就是O(n^2)的算法了,因为每次添加元素都要将添加元素之后的所有元素向后移动。 + +**其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。** + +这么做有两个好处: + +1. 不用申请新数组。 +2. 从后向前填充元素,避免了从前先后填充元素要来的 每次添加元素都要将添加元素之后的所有元素向后移动。 + +时间复杂度,空间复杂度均超过100%的用户。 + + + +## C++代码 + +```C++ +class Solution { +public: + string replaceSpace(string s) { + int count = 0; // 统计空格的个数 + int sOldSize = s.size(); + for (int i = 0; i < s.size(); i++) { + if (s[i] == ' ') { + count++; + } + } + // 扩充字符串s的大小,也就是每个空格替换成"%20"之后的大小 + s.resize(s.size() + count * 2); + int sNewSize = s.size(); + // 从后先前将空格替换为"%20" + for (int i = sNewSize - 1, j = sOldSize - 1; j < i; i--, j--) { + if (s[j] != ' ') { + s[i] = s[j]; + } else { + s[i] = '0'; + s[i - 1] = '2'; + s[i - 2] = '%'; + i -= 2; + } + } + return s; + } +}; + +``` +时间复杂度:O(n) +空间复杂度:O(1) + +此时算上本题,我们已经做了七道双指针相关的题目了分别是: + +* [27.移除元素](https://mp.weixin.qq.com/s/wj0T-Xs88_FHJFwayElQlA) +* [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) +* [344.反转字符串](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA) + +# 拓展 + +这里也给大家拓展一下字符串和数组有什么差别, + +字符串是若干字符组成的有限序列,也可以理解为是一个字符数组,但是很多语言对字符串做了特殊的规定,接下来我来说一说C/C++中的字符串。 + +在C语言中,把一个字符串存入一个数组时,也把结束符 '\0'存入数组,并以此作为该字符串是否结束的标志。 + +例如这段代码: + +``` +char a[5] = "asd"; +for (int i = 0; a[i] != '\0'; i++) { +} +``` + +在C++中,提供一个string类,string类会提供 size接口,可以用来判断string类字符串是否结束,就不用'\0'来判断是否结束。 + +例如这段代码: + +``` +string a = "asd"; +for (int i = 0; i < a.size(); i++) { +} +``` + +那么vector< char > 和 string 又有什么区别呢? + +其实在基本操作上没有区别,但是 string提供更多的字符串处理的相关接口,例如string 重载了+,而vector却没有。 + +所以想处理字符串,我们还是会定义一个string类型。 + + + +------------------------ + +* 微信:[程序员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/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" "b/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" new file mode 100644 index 0000000000..d75df59389 --- /dev/null +++ "b/problems/\345\211\221\346\214\207Offer58-II.\345\267\246\346\227\213\350\275\254\345\255\227\347\254\246\344\270\262.md" @@ -0,0 +1,100 @@ + +

+ + + + +

+ +> 反转个字符串还有这么多用处? + +# 题目:剑指Offer58-II.左旋转字符串 + +https://leetcode-cn.com/problems/zuo-xuan-zhuan-zi-fu-chuan-lcof/ + +字符串的左旋转操作是把字符串前面的若干个字符转移到字符串的尾部。请定义一个函数实现字符串左旋转操作的功能。比如,输入字符串"abcdefg"和数字2,该函数将返回左旋转两位得到的结果"cdefgab"。 + + +示例 1: +输入: s = "abcdefg", k = 2 +输出: "cdefgab" + +示例 2: +输入: s = "lrloseumgh", k = 6 +输出: "umghlrlose" +  +限制: +1 <= k < s.length <= 10000 + +# 思路 + +为了让本题更有意义,提升一下本题难度:**不能申请额外空间,只能在本串上操作**。 + +不能使用额外空间的话,模拟在本串操作要实现左旋转字符串的功能还是有点困难的。 + + +那么我们可以想一下上一题目[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中讲过,使用整体反转+局部反转就可以实现,反转单词顺序的目的。 + +这道题目也非常类似,依然可以通过局部反转+整体反转 达到左旋转的目的。 + +具体步骤为: + +1. 反转区间为前n的子串 +2. 反转区间为n到末尾的子串 +3. 反转整个字符串 + +最后就可以得到左旋n的目的,而不用定义新的字符串,完全在本串上操作。 + +例如 :示例1中 输入:字符串abcdefg,n=2 + +如图: + + + +最终得到左旋2个单元的字符串:cdefgab + +思路明确之后,那么代码实现就很简单了 + +# C++代码 + +```C++ +class Solution { +public: + string reverseLeftWords(string s, int n) { + reverse(s.begin(), s.begin() + n); + reverse(s.begin() + n, s.end()); + reverse(s.begin(), s.end()); + return s; + } +}; +``` +是不是发现这代码也太简单了,哈哈。 + +# 总结 + +此时我们已经反转好多次字符串了,来一起回顾一下吧。 + +在这篇文章[字符串:这道题目,使用库函数一行代码搞定](https://mp.weixin.qq.com/s/X02S61WCYiCEhaik6VUpFA),第一次讲到反转一个字符串应该怎么做,使用了双指针法。 + +然后发现[字符串:简单的反转还不够!](https://mp.weixin.qq.com/s/XGSk1GyPWhfqj2g7Cb1Vgw),这里开始给反转加上了一些条件,当需要固定规律一段一段去处理字符串的时候,要想想在在for循环的表达式上做做文章。 + +后来在[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)中,要对一句话里的单词顺序进行反转,发现先整体反转再局部反转 是一个很妙的思路。 + +最后再讲到本地,本题则是先局部反转再 整体反转,与[字符串:花式反转还不够!](https://mp.weixin.qq.com/s/X3qpi2v5RSp08mO-W5Vicw)类似,但是也是一种新的思路。 + +好了,反转字符串一共就介绍到这里,相信大家此时对反转字符串的常见操作已经很了解了。 + +# 题外话 + +一些同学热衷于使用substr,来做这道题。 +其实使用substr 和 反转 时间复杂度是一样的 ,都是O(n),但是使用substr申请了额外空间,所以空间复杂度是O(n),而反转方法的空间复杂度是O(1)。 + +**如果想让这套题目有意义,就不要申请额外空间。** + +------------------------ + +* 微信:[程序员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/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" index ba6ca736de..b5bb6401e6 100644 --- "a/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" +++ "b/problems/\345\255\227\347\254\246\344\270\262\346\200\273\347\273\223.md" @@ -1,16 +1,11 @@ -

- -

+

- - + + - -

- # 字符串:总结篇 其实我们已经学习了十天的字符串了,从字符串的定义到库函数的使用原则,从各种反转到KMP算法,相信大家应该对字符串有比较深刻的认识了。 @@ -125,3 +120,10 @@ KMP算法是字符串查找最重要的算法,但彻底理解KMP并不容易 好了字符串相关的算法知识就介绍到了这里了,明天开始新的征程,大家加油! +------------------------ + +* 微信:[程序员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/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" "b/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" index a8a163d3fe..271778344a 100644 --- "a/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" +++ "b/problems/\346\225\260\347\273\204\346\200\273\347\273\223\347\257\207.md" @@ -1,58 +1,55 @@ -

- -

+

- - + + - -

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

- -

+

- - + + - -

+> 之前链表篇没有做总结,所以是时候总结一波 # 链表的理论基础 -在这篇文章[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ)中,介绍了如下几点: +在这篇文章[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ)中,介绍了如下几点: * 链表的种类主要为:单链表,双链表,循环链表 * 链表的存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。 @@ -22,7 +19,7 @@ **可以说把链表基础的知识都概括了,但又不像教科书那样的繁琐**。 -# 链表经典题目 +# 链表经典题目 ## 虚拟头结点 @@ -34,7 +31,7 @@ 在[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)中,我给出了用虚拟头结点和没用虚拟头结点的代码,大家对比一下就会发现,使用虚拟头结点的好处。 -## 链表的基本操作 +## 链表的基本操作 在[链表:一道题目考察了常见的五个操作!](https://mp.weixin.qq.com/s/Cf95Lc6brKL4g2j8YyF3Mg)中,我们通设计链表把链表常见的五个操作练习了一遍。 @@ -64,7 +61,7 @@ **可以先通过迭代法,彻底弄清楚链表反转的过程!** -## 环形链表 +## 环形链表 在[链表:环找到了,那入口呢?](https://mp.weixin.qq.com/s/_QVP3IkRZWx9zIpQRgajzA)中,讲解了在链表如何找环,以及如何找环的入口位置。 @@ -77,7 +74,7 @@ * fast指针一定先进入环中,如果fast 指针和slow指针相遇的话,一定是在环中相遇,这是毋庸置疑的。 * fast和slow都进入环里之后,fast相对于slow来说,fast是一个节点一个节点的靠近slow的,**注意是相对运动,所以fast一定可以和slow重合**。 -如果fast是一次走三个节点,那么可能会跳过slow,因为相对于slow来说,fast是两个节点移动的。 +如果fast是一次走三个节点,那么可能会跳过slow,因为相对于slow来说,fast是两个节点移动的。 确定有否有环比较容易,但是找到环的入口就不太容易了,需要点数学推理。 @@ -85,32 +82,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相遇了**。 @@ -135,8 +132,10 @@ 如果希望从基础学起来的同学,也可以从头学起来,从头开始打卡,打卡的同时也总结自己的所学所思,一定进步飞快! -**在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,快去通关学习吧!** +------------------------ -![](https://img-blog.csdnimg.cn/20201030210901823.jpg) +* 微信:[程序员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/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" "b/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" index 63f0396cfd..4fd616b86d 100644 --- "a/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" +++ "b/problems/\351\223\276\350\241\250\347\220\206\350\256\272\345\237\272\347\241\200.md" @@ -1,16 +1,11 @@ -

- -

+

- - + + - -

- # 关于链表,你该了解这些! 什么是链表,链表是一种通过指针串联在一起的线性结构,每一个节点是又两部分组成,一个是数据域一个是指针域(存放指向下一个节点的指针),最后一个节点的指针域指向null(空指针的意思)。 @@ -143,3 +138,10 @@ head->val = 5; 相信大家已经对链表足够的了解,后面我会讲解关于链表的高频面试题目,我们下期见! +------------------------ + +* 微信:[程序员Carl](https://mp.weixin.qq.com/s/b66DFkOp8OOxdZC_xLZxfw) +* B站:[代码随想录](https://space.bilibili.com/525438321) +* 知识星球:[代码随想录](https://mp.weixin.qq.com/s/QVF6upVMSbgvZy8lHZS3CQ) + +![](../pics/公众号.png)