41. 缺失的第一个正数
分析
将每个数放到hash
表中, 从小到大枚举每个正整数, 直到找到第一个没找到的正整数
第一步, 将每个数放到hash表中, O(n).
第二步, 枚举最多只会枚举到 n + 1, O(n)
因此整个时间复杂度O(n)
code
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
unordered_set<int> hash;
for (auto &x : nums) hash.insert(x);
int res = 1;
while (hash.count(res)) res ++;
return res;
}
};
进阶
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
if (!n) return 1;
for (auto& x : nums)
if (x != INT_MIN)
x -- ;
for (int i = 0; i < nums.size(); i ++ ) {
while (nums[i] >= 0 && nums[i] < n && nums[i] != nums[nums[i]]) swap(nums[i], nums[nums[i]]);
}
for (int i = 0; i < n; i ++ )
if (nums[i] != i)
return i + 1;
return n + 1;
}
};
42. 接雨水
分析
先将前面柱子高度压入stack中,
到了第4根柱子, 计算当前栈顶元素和上一个元素的高度差,
第2次再加上鼠标所指的第2部分面积, 宽度 = (当前柱子的左边界~栈里往前数下一根柱子的右边界) * 高度(min(栈中下一根柱子的高度, 当前扫描的柱子的高度) - 当前栈中的柱子高度)
然后将第4根柱子加入到栈中, 然后栈中只有两个元素, 阶梯形
总结:
宽度 = (当前元素坐标 和 栈中下一个元素坐标)差
高度 = (当前元素高度 和 栈中下一个元素的高度)差
由于当前第i
个元素, 不需要考虑进res
中进行操作, 因此最后先统计res
, 再stk.push(i)
code
class Solution {
public:
int trap(vector<int>& height) {
stack<int> stk;
int res = 0;
for (int i = 0; i < height.size(); i ++ ){
int last = 0;
while (stk.size() && height[stk.top()] <= height[i]){
// 注意是拿 height[stk.top()] - last
res += (height[stk.top()] - last) * (i - stk.top() - 1); // 统计面积
last = height[stk.top()];
stk.pop();
}
// 注意最后一段区间高度 = height[i] - last, 因为当前栈顶元素高度 > height[i]了
if (stk.size()) res += (i - stk.top() - 1) * (height[i] - last); // 统计最后一部分面积
stk.push(i);
}
return res;
}
};
43. 字符串相乘
分析
高精度乘法 模版题
code
class Solution {
public:
string multiply(string num1, string num2) {
vector<int> A, B;
int n = num1.size(), m = num2.size();
for (int i = n - 1; i >= 0; i --) A.push_back(num1[i] - '0');
for (int i = m - 1; i >= 0; i --) B.push_back(num2[i] - '0');
vector<int> C(n + m);
for (int i = 0; i < n; i ++)
for (int j = 0; j < m; j ++)
C[i + j] += A[i] * B[j];
for (int i = 0, t = 0; i < C.size(); i ++){
t += C[i]; // 这里不能取模, 因为t表示总的和, 下面要需要 / 10 计算进位
C[i] = t % 10;
t /= 10;
}
int k = C.size() - 1;
while (k && !C[k]) k --;
string res;
for (int i = k; i >= 0; i --) res += C[i] + '0';
return res;
}
};
44. 通配符匹配
分析
f[i][j] : s[1 ~ i] 和p[1 ~ j] 是否匹配
要分情况考虑p[j]
是否*
第2种情况p[j]
不是*
的时候, 比较好考虑, 只需要判断s[i] 是不是与p[j]相等, 然后|| f[i - 1][j - 1];
第1种情况, 比较复杂, 因为我们不知道当前*
可以匹配多少个字符(*
可以匹配任意多个字符)
需要枚举下*
匹配多少个字符
- 匹配0个字符 f[i, j - 1]
- 匹配1个字符 f[i - 1, j - 1]
- 匹配k个字符 f[i - k, j - 1]
- …
- 全部匹配 f[0, j - 1]
f
[
i
,
j
]
{
p
[
j
]
是
‘
∗
′
:
f
[
i
,
j
−
1
]
∣
∣
f
[
i
−
1
]
[
j
−
1
]
∣
∣
f
[
i
−
2
]
[
j
−
1
]
∣
∣
.
.
.
∣
∣
f
[
0
,
j
−
1
]
p
[
j
]
不
是
‘
∗
’
:
s
[
i
]
=
=
p
[
j
]
&
&
f
[
i
−
1
]
[
j
−
1
]
f[i, j]\left\{\begin{matrix} p[j]是`*': f[i, j - 1]\quad||\quad f[i - 1][j - 1]\quad||\quad f[i - 2][j - 1]\quad||\quad ...\quad||\quad f[0, j - 1]\\ p[j]不是 `*’: s[i] == p[j] \&\& f[i - 1][j - 1] \\ \end{matrix}\right.
f[i,j]{p[j]是‘∗′: f[i,j−1]∣∣f[i−1][j−1]∣∣f[i−2][j−1]∣∣...∣∣f[0,j−1]p[j]不是‘∗’: s[i]==p[j] && f[i−1][j−1]
由于上面的状态数量
O
(
n
2
)
O(n^2)
O(n2), 转移数量
O
(
n
)
O(n)
O(n), 因此时间复杂度
O
(
n
3
)
O(n^3)
O(n3)
此题优化方式和leetcode10/完全背包问题相似
优化
考虑f[i - 1][j]
的状态去优化
因为当前p[j] == '*'
, 所以f[i - 1][j]
的表达式, 只需要将f[i][j]
表达式中的i
改成i - 1
f
[
i
−
1
]
[
j
]
=
f
[
i
−
1
]
[
j
−
1
]
∣
∣
f
[
i
−
2
]
[
j
−
1
]
∣
∣
.
.
.
.
∣
∣
f
[
0
]
[
j
−
1
]
f[i - 1][j] = f[i - 1][j - 1] || f[i- 2][j - 1] || .... || f[0][j - 1]
f[i−1][j]=f[i−1][j−1] ∣∣ f[i−2][j−1] ∣∣ .... ∣∣ f[0][j−1]
可以发现
f[i][j] = f[i][j - 1] || f[i - 1][j - 1] || f[i - 2][j - 1] || … || f[0, j - 1]
f[i - 1][j] = f[i - 1][j - 1] || f[i- 2][j - 1] || … || f[0][j - 1]
因此f[i][j] = f[i][j - 1] || f[i - 1][j]
code
f[0][0] = true, 因为两个空串是匹配的
f[0][j]是有意义的, 因为第2个字符串 可能很多***, 所有***可以匹配空串, 所以f[0][j] 可能是true
但是f[i][0], 当i > 0的时候, 一定是false
所以遍历的时候i从0开始, j 从1开始(j = 0, 初始化就是false)
class Solution {
public:
bool isMatch(string s, string p) {
int n = s.size(), m = p.size();
s = ' ' + s, p = ' ' + p;
vector<vector<bool>> f(n + 1, vector<bool>(m + 1));
f[0][0] = true;
for (int i = 0; i <= n; i ++ )
for (int j = 1; j <= m; j ++ ){
if (p[j] == '*')
f[i][j] = f[i][j - 1] || i && f[i - 1][j];
else
f[i][j] = (s[i] == p[j] || p[j] == '?') && i && f[i - 1][j - 1];
}
return f[n][m];
}
};
45. 跳跃游戏 II
分析
可以发现贪心是错误的
需要3步,到达终点
而下面的方式, 只用2步可以到达终点
题目本身是一个图论的问题, 0的位置是一个点可以跳2步, 表示有两条边, 相当于给了我们一张图, 求起点到终点的最短距离. 如果用图论的方法去做, 由于每个点需要向后面的点连边, 边数
O
(
n
2
)
O(n^2)
O(n2), 这样的话时间复杂度
O
(
n
2
)
O(n^2)
O(n2), yxc试过, 会超时
所以需要想办法优化一下, 想一下有没有dp的做法, 先不考虑时间的问题, 考虑下怎么做dp
比如说从0这个位置, 从
0
→
1
0 \to 1
0→1, 想要计算的f[i], 其实是从1到终点最少需要多少步数
再从
0
→
2
0 \to 2
0→2, 想要计算的f[i], 还考虑从2到终点最少需要多少步数
从这些所有的选择方案中, 选择最小值
这启发我们可以用f[i]表示 从i到终点的最小步数(也反着表示, 从起点到i的最小步数)
然后我们倒着推一遍, 求f[0]就是答案. 但是这个做法也是O(n^2)的, 最坏的情况下, 需要将后面的所有点枚举一遍, 只要我们每个点的步长足够长, 我们需要将后面的都算一遍, 也会超时
继续优化
看f[i]有没有其他性质, 直觉上讲(估计) f[i]有一定单调性
fi]:从起点到i的最小步数
f[i]递增的话只会递增1, 因为前后两个点是紧挨着的, 并且前面点的步数 >0, 因为到达不了后面那个点, 因此+1步必定可以到达挨着的后面点命题:f[i]是单调的
证明:反证法, 如果f[i]不是单调的, 那么必然存在两个相邻的点, 使得第一个点的值 > 第二个点的值(从起点跳到第一个点的步数 > 从起点跳到第2个点步数). 可以观察后一个点最后一步是从哪个一个点跳过来的, 由于后一个点比上一个点步数要小, 不可能从上一个点跳过来, 必然是从 前面某个点跳过来, 那么 前面某个点的范围如下
这样的话, 就推出了矛盾, 因为从起点到绿色点的步数 <= 红色点, 最多也只会相等, 不可能比起点到红色点的步数大.
f[i]的区间 证明了是一段一段后, 计算后面能跳右边界, 肯定是基于前面的能跳距离的max
每次可以求出一个边界, 求出边界后, [前一段能跳的区间 + 1, 边界] = 前一段能跳的步数 + 1
每段如上述方法赋值
O
(
n
)
O(n)
O(n), 整个线段扫描一遍
O
(
n
)
O(n)
O(n)
code
写代码的时候, 求f[i]的时候, 枚举上一段的变量j
, 当上一段距离跳不到i
的时候, j
一直往后走, 直到j
可以跳到i
为止, 停下来, 停下来的, 第一个找到的j
就是最小步数
f[0] = 0, 因为从起点跳到0的步数,不用跳, 就是0
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
vector<int> f(n);
for (int i = 1, j = 0; i < nums.size(); i ++ ){
while (j + nums[j] < i) j ++;// 扫描前面的j, 找到第一个j, 能够使得 j + nums[j] >= i
// 即表示是最小的j, 能够跳到i
f[i] = f[j] + 1; // 那么从起点到i的步数 就是从起点到j的步数 + 1
}
return f[n - 1];
}
};
46. 全排列
分析
考虑dfs的状态
- 我们需要考虑每个位置上填了什么,
int[]
, 用来表示每个位置 - 我们当前看了第几位,
u
- 当前哪些数已经用过来了
bool st[]
然后要考虑, 每个分支里, 怎么把分支实现出来, 对于当前分支, 枚举下哪些分支没有被用过
code(顺序1 : 枚举每个数, 填哪个位置)
注意leetcode中public:中只能声明vector
, 定义vector大小, 需要在函数中
class Solution {
public:
vector<vector<int>> ans;
vector<int> path;
vector<bool> st;
vector<vector<int>> permute(vector<int>& nums) {
path = vector<int>(nums.size());
st = vector<bool>(nums.size());
dfs(nums, 0);
return ans;
}
void dfs(vector<int>& nums, int u) {
if (u == nums.size()) {
ans.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i ++ ) {
if (st[i] == false) {
path[u] = nums[i];
st[i] = true;
dfs(nums, u + 1);
st[i] = false;
}
}
}
};
code(顺序2 :枚举每个位置填哪个数)
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<bool> st;
vector<vector<int>> permute(vector<int>& nums) {
path = vector<int>(nums.size());
st = vector<bool> (nums.size());
dfs(nums, 0);
return res;
}
void dfs(vector<int>& nums, int u){
if (u == nums.size()){
res.push_back(path);
return;
}
for (int i = 0; i < nums.size(); i ++ ){
if (!st[i]){
path[u] = nums[i];
st[i] = true;
dfs(nums, u + 1);
st[i] = false;
}
}
}
};
47. 全排列 II
分析
dfs顺序问题
有两种dfs顺序,
- 3个位置, 依次枚举每个位置填哪个数
- 枚举每个数, 填写到哪个位置上
如果题目要求我们按照字典序 输出, 我们一定要按照每个位置上填哪个数, 第一个位置上填1行不行, 然后将第1个位置上填1的方案全都找出来, 然后第2个位置上填接着的方案.
如果枚举每个数填到每个位置上, 不一定能保证字典序了, 因为不一定会把第一个位置上最小的数输出
这一题按照第2中顺序
重复元素问题
有重复元素, 比如
1
1
,
1
2
,
2
1^1,1^2, 2
11,12,2,
1
1
1
2
_
1^11^2 \_
1112_
1
2
1
1
_
1^2 1^1 \_
1211_
是同一种方案
重复的原因是因为, 我们将两个1看成不同的1
为了让我们不搜索出重复的元素, 必须限定下, 对于相同的数, 规定下一种顺序
比如
1
1
,
1
2
,
1
3
1^1, 1^2, 1^3
11,12,13, 在搜索的时候, 一定要保证
1
1
_
_
_
,
1
2
_
_
,
_
_
1
3
1^1\_\_\_, 1^2\_\_, \_\_1^3
11___,12__,__13的顺序去搜索(中间可能隔一些数)
这样可以保证对于相同的数来说, 只会枚举一种顺序, 就可以保证唯一了
所以我们发现, 只需要保证他们的相对顺序不变, 就可以了
怎么能够保证所有相同数的相对顺序不变呢?
比如说有这么多
1
1
,
1
2
,
1
3
,
1
4
,
1
5
1^1, 1^2, 1^3, 1^4, 1^5
11,12,13,14,15, 我们需要用第1次没有用过的1, 用相同的数的时候, 一定要保证从前往后挨个用, 不能跳着用
为了方便, 对原数组排序, 这样相同的数就排在一起了, 在枚举的的时候, 如果不是第一个没有用过的数, 直接略过
总而言之, 判断一个数是否是第1次没有用过,是的话就用它, 不是的话就略过
具体操作:
怎么看num[i]
是不是第1次没有用过呢, 如果nums[i - 1] == nums[i]
, 并且nums[i - 1]
没有被用过, 说明我们当前这个数nums[i]
不是第1个没有被用过的数, 就不要用它
nums[i - 1] == nums[i] && !st[i - 1] 那么就直接continue (说明不是第1个没有被用过的数)
code (顺序2)
class Solution {
public:
vector<vector<int>> res;
vector<int> path;
vector<bool> st;
vector<vector<int>> permuteUnique(vector<int>& nums) {
sort(nums.begin(), nums.end());
st = vector<bool> (nums.size());
path = vector<int> (nums.size());
dfs(nums, 0);
return res;
}
void dfs(vector<int>& nums, int u){
if (u == nums.size()){
res.push_back(path);
return ;
}
for (int i = 0; i < nums.size(); i ++ ){
if (!st[i]){
if (i && nums[i] == nums[i - 1] && !st[i - 1]) continue;
st[i] = true;
path[u] = nums[i];
dfs(nums, u + 1);
st[i] = false;
}
}
}
};
48. 旋转图像
分析
矩阵转置, 再对每一层翻转
code
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
int n = matrix.size();
for (int i = 0; i < n; i ++ )
for (int j = 0; j < i; j ++ )
swap(matrix[i][j], matrix[j][i]);
for (int i = 0; i < n; i ++ )
reverse(matrix[i].begin(), matrix[i].end());
}
};
49. 字母异位词分组
分析
建一个hash表, 然后遍历原字符串数组, 用一个变量保存当前字符串的最小表示, 作为hash表的key, 然后将原字符串塞到value里
再遍历一遍hash表, 将value(vector<string>
) 塞到答案中
code
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> hash;
for (auto& str : strs){ // 遍历原字符串数组
string nstr = str; // 新建代表元
sort(nstr.begin(), nstr.end()); // 最小表示
hash[nstr].push_back(str);
}
vector<vector<string>> res;
for (auto& item : hash){
res.push_back(item.second);
}
return res;
}
};
50. Pow(x, n)
快速幂模版题, 注意n可能会是负数INT最大值, abs后可能会爆, 所以用abs(LL(n))
code
class Solution {
public:
double myPow(double x, int n) {
typedef long long LL;
bool is_minus = false;
if (n < 0) is_minus = true;
double res = 1;
for (LL k = abs(LL(n)); k; k >>= 1){
if (k & 1) res *= x;
x *= x;
}
if (is_minus) return 1.0 / res;
return res;
}
};