diff --git a/pics/26_封面.png b/pics/26_封面.png new file mode 100644 index 00000000..b8a2f1a1 Binary files /dev/null and b/pics/26_封面.png differ diff --git a/pics/27_封面.png b/pics/27_封面.png new file mode 100644 index 00000000..a4a2b009 Binary files /dev/null and b/pics/27_封面.png differ diff --git a/pics/35_封面.png b/pics/35_封面.png new file mode 100644 index 00000000..dc2971b5 Binary files /dev/null and b/pics/35_封面.png differ diff --git a/pics/35_搜索插入位置.png b/pics/35_搜索插入位置.png new file mode 100644 index 00000000..5fa65490 Binary files /dev/null and b/pics/35_搜索插入位置.png differ diff --git a/pics/35_搜索插入位置2.png b/pics/35_搜索插入位置2.png new file mode 100644 index 00000000..71fca151 Binary files /dev/null and b/pics/35_搜索插入位置2.png differ diff --git a/pics/35_搜索插入位置3.png b/pics/35_搜索插入位置3.png new file mode 100644 index 00000000..ec1ce1eb Binary files /dev/null and b/pics/35_搜索插入位置3.png differ diff --git a/pics/35_搜索插入位置4.png b/pics/35_搜索插入位置4.png new file mode 100644 index 00000000..efb88e22 Binary files /dev/null and b/pics/35_搜索插入位置4.png differ diff --git a/pics/35_搜索插入位置5.png b/pics/35_搜索插入位置5.png new file mode 100644 index 00000000..ec021c9d Binary files /dev/null and b/pics/35_搜索插入位置5.png differ diff --git a/pics/59_封面.png b/pics/59_封面.png new file mode 100644 index 00000000..795e8f04 Binary files /dev/null and b/pics/59_封面.png differ diff --git a/pics/leetcode_209.png b/pics/leetcode_209.png new file mode 100644 index 00000000..2278be41 Binary files /dev/null and b/pics/leetcode_209.png differ diff --git a/pics/螺旋矩阵.png b/pics/螺旋矩阵.png new file mode 100644 index 00000000..5148db46 Binary files /dev/null and b/pics/螺旋矩阵.png differ diff --git a/problems/0026.Remove-Duplicates-from-Sorted-Array.md b/problems/0026.Remove-Duplicates-from-Sorted-Array.md new file mode 100644 index 00000000..15b41d7d --- /dev/null +++ b/problems/0026.Remove-Duplicates-from-Sorted-Array.md @@ -0,0 +1,26 @@ +## 题目地址 +https://leetcode-cn.com/problems/remove-duplicates-from-sorted-array/ + +## 思路 + +此题就是O(n)的解法,拼速度的话,也就是剪剪枝 +注意题目中:你不需要考虑数组中超出新长度后面的元素。 说明是要对原数组进行操作的 + +## 解法 + + +``` +class Solution { +public: + int removeDuplicates(vector& nums) { + if (nums.empty()) return 0; // 别忘记空数组的判断 + int slowIndex = 0; + for (int fastIndex = 0; fastIndex < (nums.size() - 1); fastIndex++){ + if(nums[fastIndex] != nums[fastIndex + 1]) { // 发现和后一个不相同 + nums[++slowIndex] = nums[fastIndex + 1]; //slowIndex = 0 的数据一定是不重复的,所以直接 ++slowIndex + } + } + return slowIndex + 1; //别忘了slowIndex是从0开始的,所以返回slowIndex + 1 + } +}; +``` diff --git a/problems/0027.移除元素.md b/problems/0027.移除元素.md new file mode 100644 index 00000000..e94c3679 --- /dev/null +++ b/problems/0027.移除元素.md @@ -0,0 +1,49 @@ +> 笔者在BAT从事技术研发多年,利用工作之余重刷leetcode,更多原创文章请关注公众号「代码随想录」。 + +## 题目地址 + +https://leetcode-cn.com/problems/remove-element/ + +建议做完这道题,接着再去做 26. 删除排序数组中的重复项, 对这种类型的题目就有有所感觉 + +## 暴力解法 + +时间复杂度:O(n^2) +空间复杂度:O(1) +``` +class Solution { +public: + int removeElement(vector& nums, int val) { + int size = nums.size(); + for (int i = 0; i < size; i++) { + if (nums[i] == val) { // 发现需要移除的元素,就将数组集体向前移动一位 + for (int j = i + 1; j < size; j++) { + nums[j - 1] = nums[j]; + } + i--; // 因为下表i以后的数值都向前移动了一位,所以i也向前移动一位 + size--;// 此时数组的大小-1 + } + } + return size; + + } +}; +``` + +## 快慢指针解法 +时间复杂度:O(n) +空间复杂度:O(1) +``` +class Solution { +public: + int removeElement(vector& nums, int val) { + int slowIndex = 0; // index为 慢指针 + for (int fastIndex = 0; fastIndex < nums.size(); fastIndex++) { // i 为快指针 + if (val != nums[fastIndex]) { //将快指针对应的数值赋值给慢指针对应的数值 + nums[slowIndex++] = nums[fastIndex]; 注意这里是slowIndex++ 而不是slowIndex-- + } + } + return slowIndex; + } +}; +``` diff --git a/problems/0035.搜索插入位置.md b/problems/0035.搜索插入位置.md new file mode 100644 index 00000000..2054620e --- /dev/null +++ b/problems/0035.搜索插入位置.md @@ -0,0 +1,156 @@ + +## 题目地址 + +https://leetcode-cn.com/problems/search-insert-position/ + +## 思路 + +这道题目其实是一道很简单的题,但是为什么通过率相对来说并不高呢,我理解是大家对 边界处理的判断有所失误,导致的。 + +这道题目,我们要在数组中插入目标值,无非是这四种情况 + + + +* 目标值在数组所有元素之前 +* 目标值等于数组中某一个元素 +* 目标值插入数组中的位置 +* 目标值在数组所有元素之后 + +这四种情况确认清楚了,我们就可以尝试解题了 + +暴力解题 不一定时间消耗就非常高,关键看实现的方式,就像是二分查找时间消耗不一定就很低,是一样的。 + +这里我给出了一种简洁的暴力解法,和两种二分查找的解法 + + +## 解法:暴力枚举 + +``` +class Solution { +public: + int searchInsert(vector& nums, int target) { + for (int i = 0; i < nums.size(); i++) { + // 分别处理如下三种情况 + // 目标值在数组所有元素之前 + // 目标值等于数组中某一个元素 + // 目标值插入数组中的位置 + if (nums[i] >= target) { // 一旦发现大于或者等于target的num[i],那么i就是我们要的结果 + return i; + } + } + // 目标值在数组所有元素之后的情况 + return nums.size(); // 如果target是最大的,或者 nums为空,则返回nums的长度 + } +}; +``` +效率如下: + + +时间复杂度:O(n) +时间复杂度:O(1) + + +## 二分法 + +既然暴力解法的时间复杂度是On,我们就要尝试一下使用二分查找法。 + + + +大家注意这道题目的前提是数组是有序数组,这也是使用二分查找的基础条件 + +以后大家**只要看到面试题里给出的数组是有序数组,都可以想一想是否可以使用二分法。** + +同时题目还强调数组中无重复元素,因为一旦有重复元素,使用二分查找法返回的元素下表可能不是唯一的。 + +大体讲解一下二分法的思路,这里来举一个例子,例如在这个数组中,我们使用二分法寻找元素为5的位置,并返回其下标 + + + +二分查找涉及的很多的边界条件,逻辑比较简单,就是写不好 + +相信很多同学对二分查找法中边界条件处理不好,例如 到底是 小于 还是 小于等于, 到底是+1 呢,还是要-1呢 + +这是为什么呢,主要是**我们对区间的定义没有想清楚,这就是我们的不变量** + +我们要在二分查找的过程中,保持不变量,这也就是**循环不变量** (感兴趣的同学可以查一查) + +### 二分法第一种写法 + +以这道题目来举例,以下的代码中我们定义 target 是在一个在左闭右闭的区间里,也就是[left, right] + +这就决定了我们 这个二分法的代码如何去写,大家看如下代码 + +``` +class Solution { +public: + int searchInsert(vector& nums, int target) { + int n = nums.size(); + int left = 0; + int right = n - 1; // 我们定义target在左闭右闭的区间里,[left, right] + while (left <= right) { // 当left==right,区间[left, right]依然有效 + int middle = left + ((right - left) / 2);// 防止溢出 等同于(left + right)/2 + if (nums[middle] > target) { + right = middle - 1; // target 在左区间,所以[left, middle - 1] + } else if (nums[middle] < target) { + left = middle + 1; // target 在右区间,所以[middle + 1, right] + } else { // nums[middle] == target + return middle; + } + } + // 分别处理如下四种情况 + // 目标值在数组所有元素之前 [0, -1] + // 目标值等于数组中某一个元素 return middle; + // 目标值插入数组中的位置 [left, right],return right + 1 + // 目标值在数组所有元素之后的情况 [left, right], return right + 1 + return right + 1; + } +}; +``` +时间复杂度:O(logn) +时间复杂度:O(1) + +效率如下: + + +### 二分法第二种写法 + +如果说我们定义 target 是在一个在左闭右开的区间里,也就是[left, right) + +那么二分法的边界处理方式则截然不同。 + +不变量是[left, right)的区间,如下代码可以看出是如何在循环中坚持不变量的。 + +``` +class Solution { +public: + int searchInsert(vector& nums, int target) { + int n = nums.size(); + int left = 0; + int right = n; // 我们定义target在左闭右开的区间里,[left, right) target + while (left < right) { // 因为left == right的时候,在[left, right)是无效的空间 + int middle = left + ((right - left) >> 1); + if (nums[middle] > target) { + right = middle; // target 在左区间,在[left, middle)中 + } else if (nums[middle] < target) { + left = middle + 1; // target 在右区间,在 [middle+1, right)中 + } else { // nums[middle] == target + return middle; // 数组中找到目标值的情况,直接返回下标 + } + } + // 分别处理如下四种情况 + // 目标值在数组所有元素之前 [0,0) + // 目标值等于数组中某一个元素 return middle + // 目标值插入数组中的位置 [left, right) ,return right 即可 + // 目标值在数组所有元素之后的情况 [left, right),return right 即可 + return right; + } +}; +``` + +时间复杂度:O(logn) +时间复杂度:O(1) + +## 总结 +希望通过这道题目 ,可以帮助大家对二分法有更深的理解 + +> 笔者在BAT从事技术研发多年,利用工作之余重刷leetcode,更多原创文章请关注公众号:「代码随想录」。 diff --git a/problems/0053.最大子序和.md b/problems/0053.最大子序和.md new file mode 100644 index 00000000..d8279e23 --- /dev/null +++ b/problems/0053.最大子序和.md @@ -0,0 +1,55 @@ +> 笔者在BAT从事技术研发多年,利用工作之余重刷leetcode,更多原创技术文章欢迎关注「代码随想录」,校招社招求职内推欢迎通过公众号「代码随想录」联系我,度厂很缺人! + +> 笔者在BAT从事技术研发多年,利用工作之余重刷leetcode,希望结合自己多年的实践经验,把算法讲的更清楚,更多原创文章欢迎关注公众号「代码随想录」。 + +## 题目地址 +https://leetcode-cn.com/problems/maximum-subarray/ + +## 暴力解法 + +暴力解法的思路,第一层for 就是设置起始位置,第二层for循环遍历数组寻找最大值 + +时间复杂度:O(n^2) +空间复杂度:O(1) +``` +class Solution { +public: + int maxSubArray(vector& nums) { + int result = INT32_MIN; + int count = 0; + for (int i = 0; i < nums.size(); i++) { // 设置起始位置 + count = 0; + for (int j = i; j < nums.size(); j++) { // 每次从起始位置i开始遍历寻找最大值 + count += nums[j]; + result = count > result ? count : result; + } + } + return result; + } +}; +``` + +## 贪心解法 +贪心解法,其实不是很好理解, 看上面暴力的解法是两层for循环,那如何省掉一层for循环呢 + +其实**暴力解法中设置起始位置这个for循环是可以省略掉的**,这也是关键的一步,有了这个思路之后,就可以看一下代码了 + +时间复杂度:O(n) +空间复杂度:O(1) +``` +class Solution { +public: + int maxSubArray(vector& nums) { + int result = INT32_MIN; + int count = 0; + for (int i = 0; i < nums.size(); i++) { + count += nums[i]; + if (count > result) { // 相当于每次取各个终点的最大值 + result = count; + } + if (count <= 0) count = 0; // 相当于重置起点,因为遇到负数一定是拉低总体数值的 + } + return result; + } +}; +``` diff --git a/problems/0059.螺旋矩阵II.md b/problems/0059.螺旋矩阵II.md new file mode 100644 index 00000000..520dfb3f --- /dev/null +++ b/problems/0059.螺旋矩阵II.md @@ -0,0 +1,73 @@ +> 笔者在BAT从事技术研发多年,利用工作之余重刷leetcode,更多原创文章请关注公众号「代码随想录」。 + +## 题目地址 +https://leetcode-cn.com/problems/spiral-matrix-ii/ + +## 思路 + +模拟顺时针画矩阵的过程 + +填充上行从左到右 +填充右列从上到下 +填充下行从右到左 +填充左列从下到上 + +在模拟的过程中最重要的思想是**保持画每一条边的原则一致,即每次都是左闭右开的原则**,如果所示 + + + +很多同学,做这道题目之所以一直写不好,代码越写越乱,就是因为 在画每一条边的时候,没有保证统一原则 + +例如:模拟矩阵上边的时候 左闭右开,然后模拟矩阵右列的时候又开始了左闭又闭,那岂能不乱 + +## 解法 + +```C++ +class Solution { +public: + vector> generateMatrix(int n) { + vector> res(n, vector(n, 0)); // 使用vector定义一个二维数组 + int startx = 0, starty = 0; // 定义每循环一个圈的起始位置 + int loop = n / 2; // 每个圈循环几次 + int mid = n / 2; // 矩阵中间的位置,例如:n为3, 中间的位置就是(1,1),n为5,中间位置为(3, 3) + 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; + } +}; +``` diff --git a/problems/0209.长度最小的子数组.md b/problems/0209.长度最小的子数组.md new file mode 100644 index 00000000..3fe73055 --- /dev/null +++ b/problems/0209.长度最小的子数组.md @@ -0,0 +1,62 @@ +## 题目地址 +https://leetcode-cn.com/problems/minimum-size-subarray-sum/ + +## 思路 +这道题目 暴力解法当然是 两个for循环,然后不断的寻找符合条件的子序列 +这块我们还可以使用滑动窗口的细想来做这道题。所谓滑动窗口,**就是不断的调节子序列的起始位置,从而得出我们要想的结果** + +## 暴力解法 + +``` +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 = result < subLength ? result : subLength; + break; // 因为我们是找符合条件最短的子序列,所以一旦符合条件就break + } + } + } + // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 + return result == INT32_MAX ? 0 : result; + } +}; +``` + +## 滑动窗口 + + +``` +// 滑动窗口 +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 = result < subLength ? result : subLength; + sum -= nums[i++]; // 这里体现出滑动窗口的精髓之处,不断变更i(子序列的起始位置) + } + } + // 如果result没有被赋值的话,就返回0,说明没有符合条件的子序列 + return result == INT32_MAX ? 0 : result; + } +}; +``` + diff --git a/problems/0383.RansomNote.md b/problems/0383.RansomNote.md new file mode 100644 index 00000000..25257c17 --- /dev/null +++ b/problems/0383.RansomNote.md @@ -0,0 +1,67 @@ +## 题目地址 +https://leetcode-cn.com/problems/ransom-note/ + +## 思路 + +这道题题意很清晰,就是用判断第一个字符串ransom能不能由第二个字符串magazines里面的字符构成,但是这里需要注意两点1. + +*  第一点“为了不暴露赎金信字迹,要从杂志上搜索各个需要的字母,组成单词来表达意思”  这里*说明杂志里面的字母不可重复使用。* + +* 第二点 “你可以假设两个字符串均只含有小写字母。” *说明只有小写字母*,这一点很重要 + +## 一般解法 + +那么第一个思路其实就是暴力枚举了,两层for循环,不断去寻找,代码如下: + +```C++ +// 时间复杂度: O(n^2) +// 空间复杂度:O(1) +class Solution { +public: + bool canConstruct(string ransomNote, string magazine) { + for (int i = 0; i < magazine.length(); i++) { + for (int j = 0; j < ransomNote.length(); j++) { + if (magazine[i] == ransomNote[j]) { + ransomNote.erase(ransomNote.begin() + j); + break; + } + } + } + if (ransomNote.length() == 0) { + return true; + } + return false; + } +}; +``` + +这里时间复杂度是比较高的,而且里面还有一个字符串删除也就是erase的操作,也是费时的,当然这段代码也可以过这道题 + +我们想一想优化解法 + +## 优化解法 + +因为题目所只有小写字母,那我们可以采用空间换区时间的哈希策略, 用一个长度为26的数组还记录magazine里字母出现的次数,然后再用ransomNote去验证这个数组是否包含了ransomNote所需要的所有字母。 +代码如下: + +```C++ +// 时间复杂度: O(n) +// 空间复杂度:O(1) +class Solution { +public: + bool canConstruct(string ransomNote, string magazine) { + int record[26] = {0}; + for (int i = 0; i < magazine.length(); i++) { + record[magazine[i]-'a'] ++; + } + for (int j = 0; j < ransomNote.length(); j++) { + record[ransomNote[j]-'a']--; + if(record[ransomNote[j]-'a'] < 0) { + return false; + } + } + return true; + } +}; +``` +