Leetcode总结速记
前言
文章目录
- 前言
- 二分查找总结
- 1 基本流程
- 2 题目总结
- 旋转数组相关
- 贪心总结
- 基础问题
- 区间问题
- dp总结
- (1)基础问题
- (2)网格二维dp
- (3)背包问题
- 0-1背包
- 完全背包
- 打家劫舍
- 股票问题
- 子序列问题
- 链表总结
- 基础类型
- 快慢指针
- 单调栈
- 二叉树
二分查找总结
1 基本流程
二分查找通常由两种写法:
- left<=right对应的right=mid-1,对应区间为闭区间[0,n-1]. 这种写法当left==right还会进行一次循环,此次循环left = mid = right. 通常不需要后处理,通常返回-1.
- left <right对应right= mid,对应左闭右开区间[0,n). 这种写法当left==right就会退出循环,所以我们要在循环外添加一个判断,作为后处理。同时如果循环中出现nums[right],需要考虑数组越界情况。通常情况下数组取n-1,数字直接取x。通常返回left。
// End Condition: left == rightif (left != nums.size() && nums[left] == target) return left;return -1;
如上图,后处理需要先判断是否越界。
二分法的核心就在于一次舍弃一半,缩小范围,直到找到目标。
为了避免混淆,我默认采用开区间模板做题。
2 题目总结
x的平方根
这道题相当于求小于等于x的最大平方数,所以如果找到小于x的平方数,还不能跳出,需要记录并继续右移边界,找到最大的才行。核心代码:
if ((long long)mid * mid <= x) { ans = mid; left = mid + 1; }
else right = mid;
寻找峰值
对于本题来说,寻找峰值就是寻找第一个下降的点。只需要比较nums[mid] 与nums[mid + 1],从而缩减区间即可。但是这里会出现nums[mid + 1],为了避免越界,让right = nums.size() - 1。核心代码
if (nums[mid] > nums[mid + 1]) { right = mid; }
else if (nums[mid] < nums[mid + 1]) { left = mid + 1; }
在排序数组中查找元素的第一个和最后一个位置
这种题目通常有重复元素,需要找到左边界和右边界。核心解决办法就是找到目标值之后,继续查找。如果找左边界就继续向左查找,右边界就继续向右。需要注意的是,查找右边界时,当我们跳出循环的时候,left一定不等于我们想要的目标值,而是left的前一位才是目标值的右边界。所以要返回left-1才是最终结果。代码如下:
while (l < r)
{int mid = (l + r) >> 1;if (nums[mid] == target) l = mid+1;else if (nums[mid] < target) l = mid+1;else r = mid;
}
if (l == 0) return -1;
return nums[l - 1] == target ? (l - 1) : -1;
旋转数组相关
寻找旋转排序数组中的最小值
搜索旋转排序数组目标值
旋转数组将数组分成了两部分,一部分有序,一部分无序。而二分查找只能处理有序情况。
这两个题目的区别在于,最小值一定在无序部分(先排除数组单调情况),而目标值可以能在任何地方。
对于最小值: 先判断nums[left] < nums[right],因为如果单调,最小值就是nums[left] 。之后在判断nums[mid] 和nums[left]的大小一步步找无序部分即可。nums[mid]>=nums[left]左边界右移。注意,由于循环中使用到了nums[right],为了避免数组越界,初始right = n-1.
if (nums[left] < nums[right]) { return nums[left]; }
else if (nums[mid] >= nums[left])left = mid + 1;
else right = mid;
如果是重复元素求最小值,需要对nums[left] == nums[right] 做额外处理,让left++.
对于目标值: 搜索旋转排序数组的思路是,先使用二分法区分出哪边是递增区间,之后再继续使用二分法进行查找目标值。 将数组一分为二,其中一定有一个是有序的,另一个可能是有序,也能是部分有序。此时有序部分用二分法查找。无序部分再一分为二,其中一个一定有序,另一个可能有序,可能无序。就这样进行循环即可
if (nums[mid] == target) return mid;
else if (nums[mid] >= nums[l]) {if (nums[mid] > target&&nums[l]<=target) r = mid-1;else l = mid + 1;
}
else if (nums[mid] < nums[l]) {if (nums[mid] < target&&nums[r]>=target) l = mid + 1;else r = mid-1;
}
贪心总结
基础问题
最大子数组和
这道题的巧妙之处在于,为了尽可能求得最大子数组的和,当和为负数时候,直接抛弃,因为加个负数还不如不加。直接以下一个元素为起始位置继续求解即可。
分发糖果
为了尽可能的少发糖果,并且还要满足题意,我们需要进行两次遍历。相当于把约束分为两个部分:
- 当前孩子比左边孩子多
- 当前孩子比右边孩子多
分别求出这两个规则下每个学生分到的最少糖果数量,要同时满足上面两个规则,需要两个规则中的最大值
跳跃游戏
本题求解的是能否跳到最后一个下标,直接比较当前元素的下标加上该元素的跳跃长度是否大于最后一个下标即可。但是跳到最后一个下标之前,你还要保证可以跳到之前的下标才行,加上一个判断即可。
跳跃游戏2
本题求解的是能到达终点的最小步数。从当前路径出发,作为一个贪心的人,我会 判断能跳到的下标哪个更远,找到能跳到最远的下标,一直执行这个步骤即可。创建一个变量end,用来表示当前能到达的最大下标,当走到end时候,步数加一,直到到达终点。
if (maxPos >= i)
{maxPos = max(maxPos, i + nums[i]);if (i == end){end = maxPos;++step;}
}
区间问题
对于区间问题来说,这类题目基本上有一个类似的做法。通常输入是一个二维数组,不过每个元素有两个元素,通常是区间的起始位置。区间问题首先要排序,有时候是按照终点有时候是起点,所以两种都要会写。
//按照起始位置排序
sort(intervals.begin(), intervals.end(), [](vector<int> &a, vector<int> &b) { return a[0] < b[0]; });
//按照结束位置排序
sort(intervals.begin(), intervals.end(), [](vector<int> &a, vector<int> &b) { return a[1] < b[1]; });
合并区间
新建一个数组,判断数组区间的末尾是否与每个区间重叠,重叠的话取两区间最大。注意新建的数组数量与原数组不同,所以需要一个变量维护下标。核心代码:
if (intervals[i][0] <= result[j][1])result[j][1] = max(result[j][1], intervals[i][1]);else{result.push_back(intervals[i]);j++;}
无重叠区间
贪心选择:当两个区间发生重叠时,总是移除结束位置较大的那个区间。这样我才能移除更少。
if(intervals[i][0]<end) {count++; end = min(end,intervals[i][1]);}
用最少数量的箭引爆气球
贪心选择:从该气球的右边界引爆。只需判断引爆当前气球会不会连带着其他气球即可。注意边界,这个等于的话也会引爆。
划分字母区间
巧妙之处在于,使用一个哈希表记录每个字母出现的最晚下标。再循环中不断更新边界,保证当前区间是之前出现过所有的字母的最远边界。如果边界下标等于i,即可划分区间。
dp总结
dp定义:
核心分析:
递推公式:
(1)基础问题
爬楼梯
核心分析:想要爬到第i层楼梯,那么我只能是从i-2阶楼梯或者i-1阶爬上来的,所以这里有两种方法:最后走一步,最后走两步。
递推公式:dp[i] = dp[i - 1] + dp[i - 2];
使用最小花费爬楼梯
dp定义:爬到第i阶楼梯的最小花费
递推公式:dp[n] = min(dp[n-1]+cost[n-1],dp[n-2]+cost[n-2]);
整数拆分
dp定义:定义dp[i]为整数i拆分出的最大乘积
递推公式:dp[i] = max(dp[i], max((i - j) * j, dp[i - j] * j));
核心分析:dp[i]本身,可能有多次拆分(i - j) * j ,代表只拆分这一次的乘积。dp[i - j] * j,代表由dp[i-j]和j拆分而来得到的乘积
(2)网格二维dp
不同路径1
dp定义:dp[i][j],表示机器人走到(i,j)时候有多少不同路径
核心分析:机器人只能向下走或者向右走,那么他当前的状态肯定是由正上方或者左边移动过来的。注意初始条件,走到第一行或者第一列的任何一个元素,只有一个走法。
递推公式:dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
不同路径2
有障碍物的地方将这些dp置0即可。注意初始条件如果有障碍物之后的路就不能走了
三角形最小路径和
dp定义:dp[i][j]用来储存从上到三角形triangle[i][j]的最小路径
核心分析:每一行第一个状态,只能由上一行第一个元素得到。每一行最后一个状态,只能由上一行最后一个元素得到。其余的状态都可由上一行的两个状态转化而来。
递推公式: dp[i][j] = min(dp[i - 1][j], dp[i - 1][j - 1]) + triangle[i][j];
下降路径最小和2
跟上一题类似,只不过当前状态可以由上一行很多状态转化,需要再添加一个for循环即可。
统计全为 1 的正方形子矩阵
dp定义:用 dp(i,j) 表示以 (i,j) 为右下角,且只包含 1 的正方形的边长最大值。 同时dp[i][j]还可以表示以表示以[i,j]为右下角的正方形数目。
核心分析:如果该位置的值是 1,则 dp(i,j) 的值由其上方、左方和左上方的三个相邻位置的 dp 值决定。具体而言,当前位置的元素值等于三个相邻位置的元素中的最小值加 1。
递推公式: dp[i][j] = min(min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]) + 1;
最大正方形
dp定义:用 dp(i,j) 表示以 (i,j) 为右下角,且只包含 1 的正方形的边长最大值。
核心分析:和上面一样
递推公式:dp[i][j] = min(dp[i - 1][j], min(dp[i][j - 1], dp[i - 1][j - 1])) + 1;
最大子矩阵
(3)背包问题
0-1背包
0-1背包要解决的问题如下:**有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。**下面写出模板:
int bag01(vector<int> weight, vector<int> value, int bagweight)
{int n = weight.size();// 注意!vector<int> dp(bagweight + 1, 0);// 先遍历物品,再遍历背包!!for (int i = 0; i < n; i++) // 遍历物品for (int j = bagweight; j >= weight[i]; j--) // 逆序遍历背包{dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);}return dp[bagweight];
}
需要注意的事项:
1.dp数组的大小为bagweight+1,因为我们会考虑背包重量为0的情况。
2.两层for循环先遍历物品,再遍历背包
3.背包需要逆序遍历,只处理j >= weight[i]的情况。
4.返回的dp下标是dp[bagweight]
0 - 1 背包 是求 给定背包容量 装满背包 的最大价值是多少。 dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
分割等和子集 是求 给定背包容量,能不能装满这个背包。
本题数组中的每个数i表示物品,而nums[i]表示背包的重量。本题的价值最大总和其实也就是背包最大重量。需要注意的是,这里的总和是sum/2。 return dp[tar] == tar;
最后一块石头的重量 II 是求给定背包容量,尽可能装,最多能装多少。
核心分析:如果石头堆能够分为两个子集,使得这两个子集的重量总和相等,那么是不是就不会剩下石头了,如果不能相等,返回大堆和小堆差值即可。 return sum - 2 * dp[tar];
目标和 是求 给定背包容量,装满背包有多少种方法。
相当于从物品中选出等于正数之和的所有可能情况,而正数之和是一个固定值(target+sum)/2。方法的话是累加dp[j] += dp[j - nums[i]];
一和零 是求 给定背包容量(二维),装满背包最多有多少个物品。
相当于将普通的一维dp转化为二维,过程是一样的。 dp[j][k] = max(dp[j][k], dp[j - zeros][k - ones] + 1);
完全背包
零钱兑换1
dp定义:给定背包重量,最少能用dp[j]个物品装满背包。
核心分析:遍历完dp[j],dp[j]仍然等于初始赋值,那么就说明前i个物品不能装满重量为j的背包。需要进行判断
递推公式:dp[j] = min(dp[j],dp[j-coins[i]]+1);
零钱兑换2
dp定义:装满背包有dp[i]种方法
核心分析:从前两个物品中取重量为5的结果就等于从前1个物品中取重量为5的结果加上从前两个物品中取重量为5-2 = 3的结果。dp[0] = 1
递推公式:dp[j] += dp[j - coins[i]];
组合总和4
跟上题类似,但是组合需要调整循环顺序。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
打家劫舍
打家劫舍1
核心分析:小偷有两种偷窃方法:1.偷第i个房间,那么他就不能偷第i-1个房间了。2.不偷第i个房间,那前i-1相当于dp[i-1]
递推公式:dp[i] = max(dp[i-1],dp[i-2]+nums[i-1]);
打家劫舍2
可以在上一题的基础上,分成两种,1.首元素不取,2尾元素不取,之后取两者最大。不过要先添加条件判断。 vector<int> nums1 = vector<int>(nums.begin(), nums.end() - 1); vector<int> nums2 = vector<int>(nums.begin() + 1, nums.end());
打家劫舍3
树形dp,,,
股票问题
我们要求的是获得的最大利润,那么dp[i]就可以代表第i天手中的最大金额。而每一天都有两种状态:持有股票或者不持有股票,而之后我们再将每个元素扩充为两个状态。dp[i][0]表示第i天不持有股票手中的最大金额,dp[i][1]表示第i天持有股票手中的最大金额。 之后这类题目就是不断调整初始条件和递推公式。
买卖股票的最佳时机(单次购买)
// 0表示不持有股票,1表示持有股票
dp[0][0] = 0;
dp[0][1] = -prices[0];
//要么没有行动,要么买/卖出股票
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] + prices[i]);
dp[i][1] = max(dp[i - 1][1], -prices[i]);
买卖股票的最佳时机(可以多次购买)
之前由于只能买一次,所以当要买入的时候dp[i-1][0]肯定为0,而如果可以买多次的话,有可能之前交易后手中有钱了,不为0了。
只需改动上一个代码一个部分 dp[i][1] = max(dp[i-1][0]-prices[i],dp[i-1][1]);
买卖股票的最佳时机(包含手续费)
上面代码减去一个手续费即可,初始化也要减一下。dp[i][1] = max(dp[i - 1][1], dp[i - 1][0] - prices[i] - fee);
买卖股票的最佳时机(一天冷冻期)
持有股票的状态不能由前一天不持有股票的状态转变而来了,而是由前前一天不持有股票的状态转变而来,由于出现了i-2,需要先把dp[0],dp[1]都初始化即可。dp[i][1] = max(dp[i - 1][1], dp[i - 2][0] - prices[i]);
买卖股票的最佳时机(2次购买)
买卖股票的最佳时机(K次购买)
子序列问题
最长递增子序列
最长连续递增序列
最长重复子数组
最长公共子序列
回文子串
.最长回文子串
最长回文子序列
链表总结
基础类型
反转链表
最基础的反转链表,刚开始cur指向空,反转之后作为链表尾部。
ListNode *reverseList(ListNode *head)
{ListNode *cur = nullptr;ListNode *nxt = head;while (nxt){ListNode *temp = nxt->next;nxt->next = cur;cur = nxt;nxt = temp;}head = cur;return head;
}
反转链表2
K个一组反转链表
链表相交
两个链表会相交,则每个链表走a+b+c的路径即可相遇!如果不相交,两指针刚好同时指向空。
while (pA != pB) {pA = pA == nullptr ? headB : pA->next;pB = pB == nullptr ? headA : pB->next;
}
快慢指针
链表的中间节点
需要根据节点数目的奇偶添加条件
while (right!=nullptr&& right->next != nullptr)
环形链表1
注意两点: 1.初始情况下快指针快一些。2.快指针走两步需要判断两种情况。
if (fast == nullptr || fast->next == nullptr) return false;
环形链表2
关键规律:从第一次相遇到环的入口的距离,和从起点到环的入口的距离相同。 先找相遇点在哪,之后搞一个指针从头移动,和相遇点移动同步,走到环入口停止。
单调栈
单调栈题目的关键词是:右边(左边)第一个比当前元素大(小)的元素。
1.右边还是左边的不同是遍历顺序的不同
2.大小的不同是递增栈还是递减栈的不同。
每日温度
while (!st.empty() && temperatures[i] > temperatures[st.top()])
{int num = i - st.top();ans[st.top()] = num;st.pop();
}
st.push(i);
接雨水
柱状图中最大的矩形
二叉树
迭代遍历?
未完待续。。。