更新中……
- 两数相加
- 盛水最多的容器
- 电话号码的字母组合
- 删除链表的倒数第 N 个结点
- 字母异位词分组
- 寻找两个正序数组的中位数
- 合并区间
- 不同路径(与最小路径和类似)
- 正则表达式匹配
- 颜色分类
- 单词搜索
- 只出现一次的数字
- 合并K个升序链表
- 接雨水
- 移除元素
- 最长有效括号
- 不同的二叉搜索树
- 验证二叉搜索树
- 对称二叉树
- 从前序与中序遍历序列构造二叉树
- 最长连续序列
- 排序链表
- 乘积最大子数组
- 编辑距离
- 最小栈
- 最小覆盖子串
- 计算右侧小于当前元素的个数
- 柱状图中最大的矩形
- 最大矩形
- 打家劫舍III
- 目标和
- 删除无效的括号
- 完全平方数
- 合并二叉树
- 除法求值
- 根据身高重建队列
- 二叉树的直径
- 路径总和III
- 回文链表
- 字符串解码
- 最短无序连续子数组
- 前k个高频元素
- 任务调度器
- 滑动窗口最大值
- 比特位计数
- 实现Trie(前缀树)
- 除自身以外数组的乘积
- 三数之和
- 找到字符串中所有字母异位词
- 移动零
- 最佳买卖股票时机含冷冻期
- 打家劫舍
- 最大正方形
- 分割等和子集
- 回文子串(的个数)
- 戳气球
- 下一个排列
- 环形链表
- 全排列与全排列II(含重复元素去重)
- 组合数
- 路径总和II
- 子集I&&子集II
- 划分为k个相等子集
两数相加
-
个人思路:将链表1转换为num1,链表2转换为num2,两数相加后再转换回链表。存在的问题:链表长度超过long long数字范围,无法做加法
-
题解:此题需要模拟加法
-
需要注意:
1. 进位的问题,最后一位进位的问题
2. 链表的遍历,新建链表,返回链表头结点ListNode* head = nullptr; ListNode* node = nullptr; if(head == nullptr){ head = node = new ListNode(cur); } else{ node->next = new ListNode(cur); node = node->next; } return head
-
反思:
1. 需要考虑数字的可表示范围
4. 注意链表的构造,并返回头结点的方法
盛水最多的容器
-
个人思路:想到了接雨水,想到了按行、按列遍历,但还是没有明确思路
-
题解:双指针。
1. 使用双指针卡出容器的两个边界, 计算当前的盛水量(当前盛水量 = 容器两侧的最小值 * 宽度)
2. 移动较小的指针,重复步骤1,直到指针重合为什么是移动较小的指针,因为移动指针会让宽度减小,移动较大的指针只会让盛水量减小,因此只有移动较小的指针才可能使盛水量增加
(二周目卡到正确性这里) -
注意:无
-
反思:
1. 编码没有难度
2. 思路比较巧妙,还需要多做题
电话号码的字母组合
- 个人思路:hash+回溯。遍历字符串,对每个数字对应的字母进行排列组合
- 题解:
1. 递归参数:题目字符串的索引
2. 递归出口:索引超出字符串,return
3. 递归体:遍历字符对应的3~4个字母,进行排列组合 - 注意:
1. 要明确递归是在遍历什么的
2. 递归体是在遍历什么的
3. 代码就会比较好写 - 反思:
1. 代码写的还是比较顺利的,对回溯有了更明确的认识
删除链表的倒数第 N 个结点
- 个人思路:只想到了两次遍历
- 题解:快慢指针,快指针比慢指针先行n步,即快慢指针之间相差n,当快指针为null时,慢指针指向待删节点
- 注意:删除节点为头结点时的特殊情况
- 反思:
1. 思路还是不够打开
2. 双指针(左右指针、快慢指针)对于自己比较难想到’
字母异位词分组
-
个人思路:观察到相同字母的单词,排序后的结果是相同的,因此对排序后结果相同的单词放在一个子集中即可
-
题解:与个人思路一致
-
注意:
1. 二维vecotr在未声明行数时,不能通过索引插入。要么提前声明行数,要么用一个空vector插入
2. 可以用map<string, vector >避免上述情况vector<vector<string> > ans; vector<string> temp; ans.push_back(temp); mp[word] = index; ans[index].push_back(strs[i]); index++
-
反思:
1. string同样可以使用sort()
2. 二维vector的遍历要格外注意是否已经存在这一行
3. 做题时要牢记,写注释 map中key-value含义,
4. 多尝试不同的STL容器解决该问题
寻找两个正序数组的中位数
- 个人思路:是利用归并排序,合并过程中找到中位数,时间复杂度为O(m + n)。要注意序列长度为奇数和偶数,对应的中位数不同
- 题解:要求时间复杂度为O(log(m+n)),采用找第k小元素的思路。中位数索引k = (m + n)/2,在两个有序数组中寻找k/2的数字,每次将较小元素前k/2个数字排除掉
- 注意:
1. 序列为奇数,中位数是第 len/2 + 1个数字,对应的索引(从0开始): len/2,例:[2,4,5],第2个数字为中位数,下标为1
2. 序列为偶数,中位数是第 len/2 和 len/2 + 1两个数字,对应的索引(从0开始):len/2 - 1 和 len/2,例[4,5,6,7],第2个和第3个数字为中位数,下标为1和2
3. 其实还是索引和对应的第x个数字之间相差1,写代码时建议提前明确,写着写着容易犯晕 - 反思:
1. 看了题解思路,未复现题解代码
合并区间
- 个人思路:个人思路没有注意到排序(经常被乱序坑),暴力解法。用map的key表示范围内的数字,value表示当前数字对应的范围,如:map[2] = [1,6],即2对应的范围区间是[1,6]
如果旧区间和新区间的范围重叠,start取二者的最小值,end取二者的最大值,将范围内所有数字更新为合并后的区间
遍历map,将所有范围存入set去重
set的元素丢入二维vector中
(随即发现,当前和并的区间会和已经合并后的区间重合,需要提前排序) - 题解:提前排序,用当前范围的左边界与ans中最后一个范围的右边界作比较,如果当前的左边界要比之前的最远右边界还大,说明区间不重合,否则说明重合
- 注意:
1. set的key值是pair时,没办法用unordered_set,因为C++中没有给pair做Hash的函数
2. 格外需要多想想提前排序 - 反思:
1. 提前排序没有想到,尤其是对大小关系比较依赖的题目
2. 暴力思想还是太重了,容器无脑堆叠
不同路径(与最小路径和类似)
- 个人思路:回溯法,到达右下角为递归出口,此时为一条完整路径,计数加一。递归体:向下和向右两个方向行进。剪枝:超出边界,已经访问过
int cnt;
void dfs(int m, int n, int x, int y){
if(x == m - 1 && y == n - 1){
cnt++;
return;
}
for(int i = 0; i < 2; ++i){
int tx = x + dir[i][0];
int ty = y + dir[i][1];
if(tx >= 0 && tx <= m - 1 && ty >= 0 && ty <= n - 1 && vis[tx][ty] == false){
vis[tx][ty] = true;
cnt++;
dfs(m, n, tx, ty);
cnt--;
vis[tx][ty] = false;
}
}
}
- 题解:因为本题m,n取值为100,回溯会超时,因此本题采用动态规划。dp[i][j]表示到达坐标(i,j)的不同路径个数为dp[i][j];初始化第一行和第一列全为1(表示只有一种路径可以到达);其他的点(i,j),肯定是由前一个点向下、向右行进过来的,因此dp[i][j] = dp[i-1][j] + dp[i][j-1]
for(int i = 0; i < m; ++i){
dp[i][0] = 1;
}
for(int j = 0; j < n; ++j){
dp[0][j] = 1;
}
for(int i = 1; i < m; ++i){
for(int j = 1; j < n; ++j){
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m -1][n - 1];
- 注意:注意对dp数组的初始化,和初始化的含义,为什么要把这些初始化为1
- 反思:
1. 注意m,n取值为100,回溯一定会超时
正则表达式匹配
- 个人思路:模拟,对p的取值分类讨论,过了75%
1. p取字母
2. p取 .
3. p取 * - 题解:题解居然是动态规划。dp[i][j]表示字符串s前i位与字符串p前j位能否匹配上,从p[j]的取值来考虑,p[j]可取字母、‘.’、'* '。当p[j]==s[i]时,dp[i][j] = dp[i-1][j-1],再从p取.和考虑:p[j] == “.” : dp[i][j] = dp[i-1][j-1];当p取时,分为两种情况,* 前的字母取0个(和s[j]没有匹配上),或取任意个(和s[j]匹配上了,或者*前的字母为万能符.)
- 注意:模拟时没有考虑到的特殊场景
1. *表示任意个,可以是0个
2. *前为.的特例
3. s没匹配完,但p已经匹配完了,匹配失败 - 反思:
1. 压根没有往动态规划上考虑
2. 一些特殊场景处理不到位
颜色分类
- 个人思路:直接对数组快速排序,没有利用数组中只有三种元素0,1,2的条件
- 题解:双指针p0和p1,p0表示待插入0元素的末尾索引,p1表示待插入1元素的末尾索引。遍历数组,遇到0插入到p0位置,遇到1插入到p1位置。
- 注意:在遇到0时,如果已经排序了一部分的0和1,则此时数组中若干个0后面已经有了若干个1,一定会发生将1交换出去的情况,此时要多进行一次交换,把交换出去的1再交换回来
int ptr0 = 0;//指向待插入元素0的位置
int ptr1 = 0;//指向待插入元素1的位置
for(int i = 0; i < nums.size(); ++i){
if(nums[i] == 0){
swap(nums[i], nums[ptr0]);
if(ptr1 > ptr0){
//说明0后面已经有排好序的1元素,经过上面的交换后,会把已经排好序的1元素交换出去
swap(nums[i], nums[ptr1]);//因此需要再交换一次1元素
}
//交换0后,0指针和1指针均增加
ptr0++;
ptr1++;
}
else if(nums[i] == 1){
swap(nums[i], nums[ptr1]);
ptr1++;//1指针后移
}
}
二周目双指针,再次明确指针含义和指针移动的操作,操作有些复杂需要提前想好指针移动的思路
- 反思:
1. 对题目条件利用不充分
2. 在处理0后面有若干个1的情况,即二次交换,处理的不够到位,逻辑不够清晰
单词搜索
- 个人思路:典型回溯法,个人思路是逐步完善的过程。
主要思路是搜索出来所有单词情况,和模式单词比对,判断是否能匹配上- 最初个人思路是认为只能向下和向右走,只枚举了两个方向
- 后来发现可以向上向左走,修改为4个方向
- 又发现并不要求从左上角出发,可以从任意点开始,因此又增加了遍历board的双重for循环
- 依照自己的思路,把现在组合出来的单词记为cur,当cur长度大于word时做剪枝,当cur和word一样时,标记为找到了。这样的操作仍然超时,而且也想不出能做的剪枝策略了
- 题解:不依靠当前组合出来的单词cur做判断,而是在board中每走一步,就和word[index]做比对,只有匹配上的才能继续搜索,否则就剪枝,这个回溯思路效率大大提高
- 注意:
1. 回溯过程中可以走的方向是两个还是四个
2. 回溯的起始位置是固定的还是不固定的
3. 对回溯中的所有参数都思考一下是不是可以剪枝 - 反思:
1. 回溯的角度有很多,不同的回溯角度效率不同。还是需要多思考多练习
比如此题,个人的回溯思路是先组合出所有的单词,再看是否和目标词匹配上;题解的思路是,只有当前字符和目标词的字符匹配上,才可以进行下一步搜索
只出现一次的数字
题目联想到了剑指offer中,出现半数以上的元素,PK的方法(摩尔投票)解决
链接
- 个人思路:先对数组排序,线性遍历数组,从0开始,步长为2,使用当前元素与下一个元素进行比对,不同(或是数组最后一个元素)则当前元素即为只出现一次的数字。
- 题解:要求线性时间和常数空间,利用异或运算的特性:
- 任何数和 00 做异或运算,结果仍然是原来的数,即 a ⊕ 0 = a a \oplus 0=a a⊕0=a。
- 任何数和其自身做异或运算,结果是 00,即 a ⊕ a = 0 a \oplus a=0 a⊕a=0
- 异或运算满足交换律和结合律,即 a ⊕ b ⊕ a = b ⊕ a ⊕ a = b ⊕ ( a ⊕ a ) = b ⊕ 0 = b a \oplus b \oplus a=b \oplus a \oplus a=b \oplus (a \oplus a)=b \oplus0=b a⊕b⊕a=b⊕a⊕a=b⊕(a⊕a)=b⊕0=b
- 注意:异或运算是将数字转换为二进制,逐位进行异或运算,并不是对十进制数字直接进行异或的
- 反思:个人只想着线性遍历数组,忘记前面的排序O(nlogn),
合并K个升序链表
- 个人思路:
1. 顺序合并,先合并前两个链表,合并结果再与第三个合并……以此类推
2. 看到优先队列的提示,思考的是:先把k个链表的头结点放入优先队列中,取出堆顶元素,把堆顶元素对应的链表指针后移一位。自己纠结的问题是,怎么确定堆顶元素和链表之间的映射关系,这是错误的思考方向 - 题解:
1. 优先队列,遍历所有链表,把每个元素丢到优先队列中,每次取出堆顶元素构建新链表(主要思想是先排序再建表,也可以把所有元素丢到数组中,sort排序后再建表)
2. 分治法
分:把k个链表divide,直到分到最细粒度,也就是只有自己一个链表为止
合:把k个链表两两向上合并,直到合成一个链表
过程:每次合并调用“合并两个有序链表”的代码
ListNode* divide(vector<ListNode*>& lists, int left, int right){
if(left == right){
return lists[left];
}
if(left > right){
return nullptr;
}
int mid = (left + right) / 2;
return mergeTwoLists(divide(lists, left, mid), divide(lists, mid + 1, right));
}
- 注意:
1. 构建链表,返回头结点的代码,head不动,不断让node后移ListNode* head = nullptr; ListNode* node = nullptr; if(head == nullptr){ head = node = new ListNode(cur); } else{ node->next = new ListNode(cur); node = node->next; } return head
- 分治思想,先分到最细粒度,再合并,大规模的问题简单化
- 反思:
1. 优先队列的错误思路,是自己想得过于复杂,陷入了归并排序的思路中
接雨水
- 个人思路:个人无思路。。。
- 题解:
1. 按列求,遍历所有位置,找到当前位置左右两侧的最高的高墙,只有两侧的高墙都比当前位置高,才能盛雨水
2. 动态规划。根据思路1(按列),每次都要往左右两侧遍历找出最高的高墙。考虑使用动态规划的状态转移方程优化对于 max_left我们其实可以这样求。
max_left [i] = Max(max_left [i-1],height[i-1])。它前边的墙的左边的最高高度和它前边的墙的高度选一个较大的,就是当前列左边最高的墙了。
二周目,想到了暴力解法,按列遍历,找到当前位置左右两侧最高的墙。动规提前找出每个位置左右两侧最高的墙
int dpMaxLeft[20005];//dpMaxLeft[i]表示i位置左侧的高墙高度为dpMaxLeft[i]
int dpMaxRight[20005];//dpMaxRight[i]表示i位置右侧的高墙高度为dpMaxRight[i]
//初始化dp数组
dpMaxLeft[0] = height[0];
dpMaxRight[n] = height[n];
for(int i = 1; i < n; ++i){
//状态转移左侧高墙
dpMaxLeft[i] = max(dpMaxLeft[i - 1], height[i - 1]);
}
for(int i = n - 1; i >= 0; --i){
//状态转移右侧高墙
dpMaxRight[i] = max(dpMaxRight[i + 1], height[i + 1]);
}
- 注意:
1. 动态规划思路是对按列方法的优化。按列方法会超时
2. 只有两侧的高墙都比当前位置高,才能盛雨水 - 反思:
1. 要学会思考状态转移的过程,思考当前状态是从前面的哪个转移过来的
移除元素
链接
题目要求时间复杂度O(N),空间复杂度O(1),即不开辟额外空间
- 题解:快慢双指针,双指针初始化为下标0,快指针的作用:遍历整个数组;慢指针的作用:确定满足条件的数据位置,实现O(1)空间复杂度
class Solution {
public:
int removeElement(vector<int>& nums, int val) {
if(nums.size() == 0){
return 0;
}
int slow = 0;
for(int quick = 0; quick < nums.size(); ++quick){
if(nums[quick] != val){
nums[slow] = nums[quick];
slow++;
}
}
return slow;
}
};
- 二周目个人思路:左右指针指向数组首尾,如果左指针元素== val,与右指针交换。如果右指针元素==val,移除尾部元素,右指针重新指向数组尾部
int removeElement(vector<int>& nums, int val) {
int left = 0;
int right = nums.size() - 1;
while(left <= right){
if(nums[left] == val){
//左指针元素==val,与右指针元素相等
swap(nums[left], nums[right]);//交换
}
else{
left++;
}
if(nums[right] == val){
//右指针元素==val,pop
nums.pop_back();
right = nums.size() - 1;
}
}
return nums.size(); //返回的是移除后,数组遍历的长度,可以不pop_back()
}
- 注意:
1. 该快慢指针高效复制数组的方法可以抽象为:将原数组中满足条件的数抽取出来,并构成新数组
2. 返回的int含义:遍历数组的长度,可以不删除数组元素
3. 这里提到条件不限于数组中元素与目标元素相等,还可以是下标是质数or非质数…… - 反思:
1. 这个快慢指针的高效复制数组的方法值得琢磨
最长有效括号
- 个人思路:用栈模拟,栈中存放的是括号。——联想到有效括号的题目
for(int i = 0; i < s.size(); ++i){
if(s[i] == '('){
//左括号
st.push(s[i]);
}
else{
//右括号
if(st.empty()){
//栈为空
st.push(s[i]);
}
else{
//栈不空
if(st.top() == '('){
//栈顶元素是左括号,说明匹配上了
st.pop();//左括号出栈
tempans += 2;
ismatch = true;
}
else{
//没有匹配上
st.push(s[i]);
}
}
}
- 题解:栈中存储的是索引,通过计算当前索引-出栈后的栈顶元素索引,即可求得连续最长有效括号。栈中参照元素“-1”的作用,处理出栈后栈为空的情况
stack<int> st;
st.push(-1);
int ans = 0, temp = 0;
for(int i = 0; i < s.size(); ++i){
int topindex = st.top();
if(topindex == -1){
st.push(i);
}
else{
if(s[i] == ')' && s[st.top()] == '('){
//左右括号匹配上了
st.pop();
temp