diff --git a/README.md b/README.md index 18d176a4..56148ddc 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.删除链表的倒数第N个节点.md b/problems/0019.删除链表的倒数第N个节点.md new file mode 100644 index 00000000..caedc17b --- /dev/null +++ b/problems/0019.删除链表的倒数第N个节点.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)
+
+
+
diff --git a/problems/0028.实现strStr.md b/problems/0028.实现strStr.md
new file mode 100644
index 00000000..6da5d8d1
--- /dev/null
+++ b/problems/0028.实现strStr.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。
+
+如动画所示:
+
+
+
+动画里,我特意把 子串`aa` 标记上了,这是有原因的,大家先注意一下,后面还会说道。
+
+可以看出,文本串中第六个字符b 和 模式串的第六个字符f,不匹配了。如果暴力匹配,会发现不匹配,此时就要从头匹配了。
+
+但如果使用前缀表,就不会从头匹配,而是从上次已经匹配的内容开始匹配,找到了模式串中第三个字符b继续开始匹配。
+
+此时就要问了**前缀表是如何记录的呢?**
+
+首先要知道前缀表的任务是当前位置匹配失败,找到之前已经匹配上的位置,在重新匹配,此也意味着在某个字符失配时,前缀表会告诉你下一步匹配中,模式串应该跳到哪个位置。
+
+那么什么是前缀表:**记录下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。**
+
+# 最长公共前后缀?
+
+文章中字符串的前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串;
+
+后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串。
+
+**正确理解什么是前缀什么是后缀很重要。**
+
+那么网上清一色都说 “kmp 最长公共前后缀” 又是什么回事呢?
+
+
+我查了一遍 算法导论 和 算法4里KMP的章节,都没有提到 “最长公共前后缀”这个词,也不知道从哪里来了,我理解是用“最长相等前后缀” 准确一些。
+
+**因为前缀表要求的就是相同前后缀的长度。**
+
+而最长公共前后缀里面的“公共”,更像是说前缀和后缀公共的长度。这其实并不是前缀表所需要的。
+
+所以字符串a的最长相等前后缀为0。
+字符串aa的最长相等前后缀为1。
+字符串aaa的最长相等前后缀为2。
+等等.....。
+
+
+# 为什么一定要用前缀表
+
+这就是前缀表那为啥就能告诉我们 上次匹配的位置,并跳过去呢?
+
+回顾一下,刚刚匹配的过程在下标5的地方遇到不匹配,模式串是指向f,如图:
+
+
+
+然后就找到了下标2,指向b,继续匹配:如图:
+
+
+以下这句话,对于理解为什么使用前缀表可以告诉我们匹配失败之后跳到哪里重新匹配 非常重要!
+
+**下标5之前这部分的字符串(也就是字符串aabaa)的最长相等的前缀 和 后缀字符串是 子字符串aa ,因为找到了最长相等的前缀和后缀,匹配失败的位置是后缀子串的后面,那么我们找到与其相同的前缀的后面从新匹配就可以了。**
+
+所以前缀表具有告诉我们当前位置匹配失败,跳到之前已经匹配过的地方的能力。
+
+**很多介绍KMP的文章或者视频并没有把为什么要用前缀表?这个问题说清楚,而是直接默认使用前缀表。**
+
+# 如何计算前缀表
+
+接下来就要说一说怎么计算前缀表。
+
+如图:
+
+
+
+长度为前1个字符的子串`a`,最长相同前后缀的长度为0。(注意字符串的**前缀是指不包含最后一个字符的所有以第一个字符开头的连续子串**;**后缀是指不包含第一个字符的所有以最后一个字符结尾的连续子串**。)
+
+
+长度为前2个字符的子串`aa`,最长相同前后缀的长度为1。
+
+
+长度为前3个字符的子串`aab`,最长相同前后缀的长度为0。
+
+以此类推:
+长度为前4个字符的子串`aaba`,最长相同前后缀的长度为1。
+长度为前5个字符的子串`aabaa`,最长相同前后缀的长度为2。
+长度为前6个字符的子串`aabaaf`,最长相同前后缀的长度为0。
+
+那么把求得的最长相同前后缀的长度就是对应前缀表的元素,如图:
+
+
+可以看出模式串与前缀表对应位置的数字表示的就是:**下标i之前(包括i)的字符串中,有多大长度的相同前缀后缀。**
+
+再来看一下如何利用 前缀表找到 当字符不匹配的时候应该指针应该移动的位置。如动画所示:
+
+
+
+找到的不匹配的位置, 那么此时我们要看它的前一个字符的前缀表的数值是多少。
+
+为什么要前一个字符的前缀表的数值呢,因为要找前面字符串的最长相同的前缀和后缀。
+
+所以要看前一位的 前缀表的数值。
+
+前一个字符的前缀表的数值是2, 所有把下标移动到下标2的位置继续比配。 可以再反复看一下上面的动画。
+
+最后就在文本串中找到了和模式串匹配的子串了。
+
+# 前缀表与next数组
+
+很多KMP算法的时间都是使用next数组来做回退操作,那么next数组与前缀表有什么关系呢?
+
+next数组就可以是前缀表,但是很多实现都是把前缀表统一减一(右移一位,初始位置为-1)之后作为next数组。
+
+为什么这么做呢,其实也是很多文章视频没有解释清楚的地方。
+
+其实**这并不涉及到KMP的原理,而是具体实现,next数组即可以就是前缀表,也可以是前缀表统一减一(右移一位,初始位置为-1)。**
+
+后面我会提供两种不同的实现代码,大家就明白了了。
+
+# 使用next数组来匹配
+
+以下我们以前缀表统一减一之后的next数组来做演示。
+
+有了next数组,就可以根据next数组来 匹配文本串s,和模式串t了。
+
+注意next数组是新前缀表(旧前缀表统一减一了)。
+
+匹配过程动画如下:
+
+
+
+# 时间复杂度分析
+
+其中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数组的逻辑流程动画如下:
+
+
+
+得到了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
+
+```
+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)
+
+
+
diff --git a/problems/0203.移除链表元素.md b/problems/0203.移除链表元素.md
new file mode 100644
index 00000000..430d5846
--- /dev/null
+++ b/problems/0203.移除链表元素.md
@@ -0,0 +1,140 @@
+
+
+
+> 链表操作中,可以使用原链表来直接进行删除操作,也可以设置一个虚拟头结点在进行删除操作,接下来看一看哪种方式更方便。
+
+# 203.移除链表元素
+
+https://leetcode-cn.com/problems/remove-linked-list-elements/
+
+题意:删除链表中等于给定值 val 的所有节点。
+
+
+
+# 思路
+
+这里以链表 1 4 2 4 来举例,移除元素4。
+
+
+
+如果使用C,C++编程语言的话,不要忘了还要从内存中删除这两个移除的节点, 清理节点内存之后如图:
+
+
+
+**当然如果使用java ,python的话就不用手动管理内存了。**
+
+还要说明一下,就算使用C++来做leetcode,如果移除一个节点之后,没有手动在内存中删除这个节点,leetcode依然也是可以通过的,只不过,内存使用的空间大一些而已,但建议依然要养生手动清理内存的习惯。
+
+这种情况下的移除操作,就是让节点next指针直接指向下下一个节点就可以了,
+
+那么因为单链表的特殊性,只能指向下一个节点,刚刚删除的是链表的中第二个,和第四个节点,那么如果删除的是头结点又该怎么办呢?
+
+这里就涉及如下链表操作的两种方式:
+* **直接使用原来的链表来进行删除操作。**
+* **设置一个虚拟头结点在进行删除操作。**
+
+
+来看第一种操作:直接使用原来的链表来进行移除。
+
+
+
+移除头结点和移除其他节点的操作是不一样的,因为链表的其他节点都是通过前一个节点来移除当前节点,而头结点没有前一个节点。
+
+所以头结点如何移除呢,其实只要将头结点向后移动一位就可以,这样就从链表中移除了一个头结点。
+
+
+
+
+依然别忘将原头结点从内存中删掉。
+
+
+
+这样移除了一个头结点,是不是发现,在单链表中移除头结点 和 移除其他节点的操作方式是不一样,其实在写代码的时候也会发现,需要单独写一段逻辑来处理移除头结点的情况。
+
+那么可不可以 以一种统一的逻辑来移除 链表的节点呢。
+
+其实**可以设置一个虚拟头结点**,这样原链表的所有节点就都可以按照统一的方式进行移除了。
+
+来看看如何设置一个虚拟头。依然还是在这个链表中,移除元素1。
+
+
+
+这里来给链表添加一个虚拟头结点为新的头结点,此时要移除这个旧头结点元素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)
+
+
+
diff --git a/problems/0206.翻转链表.md b/problems/0206.翻转链表.md
new file mode 100644
index 00000000..f20d95d2
--- /dev/null
+++ b/problems/0206.翻转链表.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指针的指向,直接将链表反转 ,而不用重新定义一个新的链表,如图所示:
+
+
+
+之前链表的头节点是元素1, 反转之后头结点就是元素5 ,这里并没有添加或者删除节点,仅仅是改表next指针的方向。
+
+那么接下来看一看是如何反转呢?
+
+我们拿有示例中的链表来举例,如动画所示:
+
+
+
+首先定义一个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)
+
+
diff --git a/problems/0209.长度最小的子数组.md b/problems/0209.长度最小的子数组.md
new file mode 100644
index 00000000..462f0655
--- /dev/null
+++ b/problems/0209.长度最小的子数组.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
+
+那么这里具体反转的逻辑我们要不要使用库函数呢,其实用不用都可以,使用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)
+
+
+
diff --git a/problems/0707.设计链表.md b/problems/0707.设计链表.md
new file mode 100644
index 00000000..30f9112b
--- /dev/null
+++ b/problems/0707.设计链表.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 个节点。
+
+
+
+
+# 思路
+
+如果对链表的基础知识还不太懂,可以看这篇文章:[关于链表,你该了解这些!](https://mp.weixin.qq.com/s/ntlZbEdKgnFQKZkSUAOSpQ)
+
+如果对链表的虚拟头结点不清楚,可以看这篇文章:[链表:听说用虚拟头节点会方便很多?](https://mp.weixin.qq.com/s/slM1CH5Ew9XzK93YOQYSjA)
+
+删除链表节点:
+
+
+添加链表节点:
+
+
+这道题目设计链表的五个接口:
+* 获取链表第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)
+
+
diff --git a/problems/剑指Offer05.替换空格.md b/problems/剑指Offer05.替换空格.md
new file mode 100644
index 00000000..936e229b
--- /dev/null
+++ b/problems/剑指Offer05.替换空格.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指向旧长度的末尾。
+
+
+
+有同学问了,为什么要从后向前填充,从前向后填充不行么?
+
+从前向后填充就是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)
+
+
+
diff --git a/problems/剑指Offer58-II.左旋转字符串.md b/problems/剑指Offer58-II.左旋转字符串.md
new file mode 100644
index 00000000..d75df593
--- /dev/null
+++ b/problems/剑指Offer58-II.左旋转字符串.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)
+
+
diff --git a/problems/字符串总结.md b/problems/字符串总结.md
index ba6ca736..b5bb6401 100644
--- a/problems/字符串总结.md
+++ b/problems/字符串总结.md
@@ -1,16 +1,11 @@
-
-
-
-
-
+
-需要两点注意的是
+需要两点注意的是
* **数组下标都是从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)
+
+
diff --git a/problems/链表总结篇.md b/problems/链表总结篇.md
index 8129ed25..d530f1e3 100644
--- a/problems/链表总结篇.md
+++ b/problems/链表总结篇.md
@@ -1,19 +1,16 @@
-
-
-
+
首先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 @@
如果希望从基础学起来的同学,也可以从头学起来,从头开始打卡,打卡的同时也总结自己的所学所思,一定进步飞快!
-**在公众号左下方,「算法汇总」可以找到历史文章,都是按系列排好顺序的,快去通关学习吧!**
+------------------------
-
+* 微信:[程序员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 63f0396c..4fd616b8 100644
--- a/problems/链表理论基础.md
+++ b/problems/链表理论基础.md
@@ -1,16 +1,11 @@
-
-
-