目录
1.两数之和
暴力:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
for (int i = 0; i < nums.size(); i ++)
{
for (int j = i + 1; j < nums.size(); j ++)
{
if (nums[i] + nums[j] == target)
{
return {i, j};
}
}
}
return {};
}
};
哈希表:
class Solution {
public:
vector<int> twoSum(vector<int>& nums, int target) {
unordered_map<int, int> hash;
set<int> judge;
for (int i = 0; i < nums.size(); i ++)
{
if (judge.find(target - nums[i]) != judge.end())
{
return {hash[target - nums[i]], i};
}
judge.insert(nums[i]);
hash[nums[i]] = i;
}
return {};
}
};
2.字母异位词分组
思路:
无论单词中字母如何组合,排序后的单词是一定的,用排序后的单词作为这一组单词的代表
以代表分组
排序:
class Solution {
public:
vector<vector<string>> groupAnagrams(vector<string>& strs) {
unordered_map<string, vector<string>> mp;
vector<vector<string>> res;
for (int i = 0; i < strs.size(); i ++)
{
string x = strs[i];
sort(x.begin(), x.end());
mp[x].push_back(strs[i]);
}
for (auto it = mp.begin(); it != mp.end(); it ++) res.push_back(it->second);
return res;
}
};
3.最长连续序列
class Solution {
public:
int longestConsecutive(vector<int>& nums) {
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
int a[100010];
int len = 0;
for (int i = 0; i < nums.size(); i ++) a[len ++] = nums[i];
if (nums.size() == 0) return 0;
if (nums.size() == 1) return 1;
map<int, int> cnt;
int max = 0, d = 0;
for (int i = 1, j = 0; i < len; i ++, j ++)
{
if (a[i] - a[j] != d)
{
d = a[i] - a[j];
cnt.clear();
}
cnt[d] ++;
if (cnt[d] > max && d == 1) max = cnt[d];
}
return max + 1 ;
}
};
4.移动零
class Solution {
public:
void moveZeroes(vector<int>& nums) {
int cnt = 0;
for (int i = 0; i < nums.size(); i ++)
if (nums[i]) nums[cnt ++] = nums[i];
for (int i = cnt; i < nums.size(); i ++) nums[i] = 0;
}
};
5.盛最多水的容器
思路(双指针+贪心):
1.p和q两个指针分别指向数组的两端
2.指针指向的数值较小的移动
--->为什么数值较小的移动?
答:无论移动p,q哪个指针,移动后宽度w是固定的,是个定值,那么最优解的更新关键看高度,如果移动较大的数值,那么高度h只有可能变小或者不变,如果移动较小的数值,高度h可能变小或者不变或者变大,综合来看,移动较小的更有可能得到更大的容量
class Solution {
public:
int maxArea(vector<int>& height) {
int ans = 0;
int p = 0, q = height.size() - 1;
while (p != q)
{
int w = q - p;
int h = min(height[p], height[q]);
ans = max(ans, h * w);
if (height[p] < height[q]) p ++;
else q --;
}
return ans;
}
};
6.三数之和
思路:
将求解三数之和变为求解两数之和
用哈希表对结果判重
class Solution {
public:
vector<vector<int>> threeSum(vector<int>& nums) {
sort(nums.begin(), nums.end());
set<pair<int, pair<int, int>>> judge;
vector<vector<int>> res;
unordered_map<int, int> cnt;
for (int i = 0; i < nums.size(); i ++) cnt[nums[i]] ++;
int len = nums.size();
for (int i = 0; i < len; i ++)
{
if (i > 0 && nums[i] == nums[i - 1]) continue;
for (int j = i + 1; j < len; j ++)
{
int a = nums[i], b = nums[j];
int c = 0 - a - b;
if (c < nums[0] || c > nums[len - 1]) break;
cnt[a] --;
cnt[b] --;
if (cnt[c] >= 1 && judge.find({a, {b, c}}) == judge.end())
{
vector<int> t;
t.push_back(a);
t.push_back(b);
t.push_back(c);
res.push_back(t);
//放入判重哈希表中
judge.insert({a, {b, c}});
judge.insert({a, {c, b}});
judge.insert({b, {a, c}});
judge.insert({b, {c, a}});
judge.insert({c, {b, a}});
judge.insert({c, {a, b}});
}
cnt[a] ++;
cnt[b] ++;
}
}
return res;
}
};
7.接雨水
思路(动态规划):
1.预处理出对于每一个高度从右向左看,以及从左向右看的最大高度
2.对于每一个高度,根据其左右两端的最大高度的较小值,算出当前位置可以盛放的雨水
class Solution {
public:
int trap(vector<int>& height) {
int l[101000], r[101000];
int len = height.size();
memset(l, 0, sizeof l);
memset(r, 0, sizeof r);
l[0] = height[0];
for (int i = 1, j = len - 1; i < len; i ++, j --)
{
l[i] = max(l[i - 1], height[i]);
r[j] = max(r[j + 1], height[j]);
}
r[0] = max(r[1], height[0]);
int sum = 0;
for (int i = 1; i < len; i ++)
{
int h = min(l[i - 1], r[i + 1]);
sum += max(0, h - height[i]);
}
return sum;
}
};
8.无重复字符的最长子串
code1:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
//判断是否出现过
set<char> hash;
int res = 0; //返回最终答案
int len = s.size();
int p = 0, q = p;
while (p < len)
{
while (q < len && !hash.count(s[q]))
{
hash.insert(s[q]);
q ++;
}
res = max(res, q - p);
hash.erase(s[p]);
p ++;
}
return res;
}
};
code2:
class Solution {
public:
int lengthOfLongestSubstring(string s) {
int len = s.size();
map<char, int> cnt;
int res = 0;
for (int i = 0; i < len; i ++)
{
cnt.clear();
cnt[s[i]] ++;
int j = i + 1;
for (;j < len; j ++)
{
cnt[s[j]] ++;
if (cnt[s[j]] == 2) break;
}
res = max(res, j - i);
}
return res;
}
};
9.找到字符串中所有字母异位词
思路(滑动窗口):
在s串中维护长度为p串长度的窗口
当窗口内的内容与p串相同时,找到其对应起点
class Solution {
public:
vector<int> findAnagrams(string s, string p) {
vector<int> res;
int ls = s.size(), lp = p.size();
if (ls < lp) return {};
vector<int> scount(26);
vector<int> pcount(26);
for (int i = 0; i < lp; i ++)
{
scount[s[i] - 'a'] ++;
pcount[p[i] - 'a'] ++;
}
if (scount == pcount) res.push_back(0);
for (int i = 0; i < ls - lp; i ++)
{
scount[s[i] - 'a'] --;
scount[s[i + lp] - 'a'] ++;
if (scount == pcount)
{
res.push_back(i + 1);
}
}
return res;
}
};
10.和为k的子数组
思路(前缀和 + 哈希表优化):
在数组中求某一段区间的和,很容易能想到用前缀和算法
但是暴力枚举每一个端点,时间复杂度会比较高,很容易会超时
用哈希表对算法进行优化能大大降低时间复杂度
class Solution {
public:
int subarraySum(vector<int>& nums, int k) {
//前缀和数组
int pre[200010];
//哈希表存储 pre[j - 1]
unordered_map<int, int> hash;
hash[0] = 1;
int len = nums.size(), cnt = 0;
for (int i = 1; i <= len; i ++)
{
pre[i] = pre[i - 1] + nums[i - 1];
if (hash[pre[i] - k] > 0) cnt += hash[pre[i] - k];
hash[pre[i]] ++;
}
return cnt;
}
};
11.滑动窗口最大值
思路(单调队列):
1. 维护一个窗口
2. 队列的队头和队尾元素与原数组中的下标存在映射关系
3.当队列需要移动时(即当前遍历到的下标与队头对应的下标超出了滑动窗口的长度时),需要移动队列,即让队头元素弹出
4.队头所存储的元素为当前最优解,当每次需要给队列中加入新元素时,则要判断是否需要从队列中弹出元素(从队尾弹出)
5.什么时候弹出元素:当队列中含有元素,且当前元素比队尾元素更大且距离更近,则说明当前元素比队尾更优
6.把新元素从队尾入队
class Solution {
public:
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
vector<int> res;
int q[100010], hh = 0, tt = -1;
int len = nums.size();
for (int i = 0; i < len; i ++)
{
//当前滑动窗口大小已满 需要移动队头
if (i - q[hh] + 1 > k && hh <= tt) hh ++;
//插入新元素时需要判断 是否需要弹出队尾元素
while (hh <= tt && nums[i] >= nums[q[tt]]) tt --;
//向队列中插入新元素
q[++ tt] = i;
if (i + 1 >= k) res.push_back(nums[q[hh]]);
}
return res;
}
};
12.最小覆盖子串
思路(双指针 + 滑动窗口):
1.维护一个滑动窗口,滑动窗口里面存储的是当前所遍历到的且在模板串中字母,左右指针表示边界
2.当滑动窗口内不包含模板串所有的字母时,移动右指针
3.当滑动窗口中包含模板串所有的字母时,考虑更新最小窗口大小以及起点位置,移动左指针
class Solution {
public:
unordered_map<char, int> window, flag;
//判断滑动窗口是否包含模板串所有字母
bool check() {
// if (window.size() < flag.size()) return false;
for (auto it : flag) {
if (window[it.first] < it.second) return false;
}
return true;
}
string minWindow(string s, string t) {
for (auto x : t) flag[x] ++;
int len = s.size();
int l = 0, r = -1; //左右指针维护滑动窗口
int min_len = 0x3f3f3f3f, idx = -1;
while (r < len){
//移动右指针:当在模板串中可以找到当前要放入滑动窗口的字母时
if (flag.find(s[++ r]) != flag.end()) window[s[r]] ++;
//如果当前滑动窗口内包含所有模板串所含字母
while (check() && r >= l) {
//考虑更新窗口大小和起点位置
if (min_len > r - l + 1) {
min_len = r - l + 1;
idx = l;
}
//移动左指针:若要弹出的字母在滑动窗口中 则需弹出
if (window.find(s[l]) != window.end()) window[s[l]] --;
l ++;
}
}
if (idx == -1) return "";
return s.substr(idx, min_len);
}
};
13.最大子数组和
思路(动态规划):
class Solution {
public:
int maxSubArray(vector<int>& nums) {
int len = nums.size();
int f[100010];
int res = nums[0];
f[0] = nums[0];
for (int i = 1; i < len; i ++)
{
f[i] = max(f[i - 1] + nums[i], nums[i]);
res = max(res, f[i]);
}
return res;
}
};
14.合并区间
思路:
1.将区间按左端点排序
2.l,r记录当前合并区间的左右端点
3.判断当前遍历到的区间的左端点是否可以合并到当前区间:
1>可以合并,考虑更新当前合并区间的右端点
2> 不可以合并,更新l,r
class Solution {
public:
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> res;
int cnt = 0;
int len = intervals.size();
sort(intervals.begin(), intervals.end());
int l = intervals[0][0], r = intervals[0][1];
for (int i = 1; i < len; i ++)
{
int p = intervals[i][0], q = intervals[i][1];
if (p <= r) r = max(r, q);
else
{
res.push_back({l, r});
l = p;
r = q;
}
}
res.push_back({l, r});
return res;
}
};
15.轮转数组
class Solution {
public:
void rotate(vector<int>& nums, int k) {
vector<int> copy(nums);
int len = nums.size();
for (int i = 0; i < len; i ++)
{
int x = (i + k) % len;
nums[x] = copy[i];
}
}
};
16.除自身以外数组的乘积
class Solution {
public:
vector<int> productExceptSelf(vector<int>& nums) {
vector<int> res;
int len = nums.size();
int l[100010], r[100010];
l[0] = 1;
r[len - 1] = 1;
for (int i = 1, j = len - 2; i < len; i ++, j --)
{
l[i] = l[i - 1] * nums[i - 1];
r[j] = r[j + 1] * nums[j + 1];
}
for (int i = 0; i < len; i ++) res.push_back(l[i] * r[i]);
return res;
}
};
17.缺失的第一个正数
class Solution {
public:
int firstMissingPositive(vector<int>& nums) {
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
int len = nums.size();
int cnt = 1;
int p = 0;
if (nums[len - 1] <= 0) return 1;
while (p < len)
{
while (p < len && nums[p] <= 0) p ++;
if (nums[p ++] == cnt) cnt ++;
else break;
}
return cnt;
}
};
17.矩阵置零
class Solution {
public:
void setZeroes(vector<vector<int>>& matrix) {
int len = matrix.size();
int wid = matrix[0].size();
vector<vector<int>> cp(matrix);
set<int> len_zero, wid_zero;
for (int i = 0; i < len; i ++)
{
for (int j = 0; j < wid; j ++)
{
if (cp[i][j] == 0)
{
if (len_zero.find(i) == len_zero.end())
{
for (int k = 0; k < wid; k ++) matrix[i][k] = 0;
len_zero.insert(i);
}
if (wid_zero.find(j) == wid_zero.end())
{
for (int k = 0; k < len; k ++) matrix[k][j] = 0;
wid_zero.insert(j);
}
}
}
}
}
};
18.螺旋矩阵
思路:
每遍历一行或一列就更新一下对应边界
class Solution {
public:
vector<int> spiralOrder(vector<vector<int>>& matrix) {
vector<int> res;
if (matrix.empty()) return res;
int u = 0;
int d = matrix.size() - 1;
int l = 0;
int r = matrix[0].size() - 1;
while (1)
{
//从左向右遍历
for (int i = l; i <= r; i ++) res.push_back(matrix[u][i]);
if (++ u > d) break; //出边界
//从上向下遍历
for (int i = u; i <= d; i ++) res.push_back(matrix[i][r]);
if (-- r < l) break; //出边界
//从右向左遍历
for (int i = r; i >= l; i --) res.push_back(matrix[d][i]);
if (-- d < u) break; //出边界
//从下向上遍历
for (int i = d; i >= u; i --) res.push_back(matrix[i][l]);
if (++ l > r) break; //出边界
}
return res;
}
};
19.旋转图像
class Solution {
public:
void rotate(vector<vector<int>>& matrix) {
vector<vector<int>> cp(matrix);
int n = matrix.size();
for (int i = 0; i < n; i ++)
for (int j = n - 1; j >= 0; j --)
matrix[i][n - j - 1] = cp[j][i];
}
};
20.搜索二维矩阵II
思路(二分查找):
因为每一行都是从小到大排序的,所以可以用二分查找
下面的博文里面有二分查找的相关总结:
class Solution {
public:
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int len = matrix.size();
int wid = matrix[0].size();
for (int i = 0; i < len; i ++)
{
//二分查找
int l = 0, r = wid - 1;
while (l < r)
{
int mid = (l + r) / 2;
if (matrix[i][mid] >= target) r = mid;
else l = mid + 1;
}
if (matrix[i][l] == target) return true;
}
return false;
}
};
21.岛屿数量
思路(dfs):
分别向四个方向遍历,看哪个方向可达,标注已到达的坐标
class Solution {
public:
int h, w;
int dx[5] = {0, 0, 1, -1};
int dy[5] = {1, -1, 0, 0};
void dfs(int x, int y, vector<vector<char>>& grid, vector<vector<bool>>& used) {
if (x < 0 || x >= h || y < 0 || y >= w) return;
if (grid[x][y] == '0') return;
for (int i = 0; i < 4; i ++)
{
int a = x + dx[i], b = y + dy[i];
if (a < 0 || a >= h || b < 0 || b >= w ) continue;
if (grid[a][b] == '1' && !used[a][b])
{
used[a][b] = true;
dfs(a, b, grid, used);
}
}
}
int numIslands(vector<vector<char>>& grid) {
h = grid.size();
w = grid[0].size();
vector<vector<bool>> used = vector<vector<bool>>(h, vector<bool>(w, false));
int ans = 0;
for (int i = 0; i < h; i ++)
{
for (int j = 0; j < w; j ++)
{
if (!used[i][j] && grid[i][j] == '1')
{
ans ++;
dfs(i, j, grid, used);
}
}
}
return ans;
}
};
22.腐烂的橘子
注意:每分钟,所有腐烂的橘子都会向四周腐烂
思路(bfs):
1. 队列中存储当前腐烂的橘子
2. 取出当前队列中所有腐烂的橘子
3. 向四周腐烂,若新鲜的橘子被腐烂,新鲜的橘子减一,并把其加入腐烂橘子的队列当中去
class Solution {
public:
int h, w;
int fresh = 0, ans = 0;
int dx[5] = {1 , -1, 0, 0};
int dy[5] = {0, 0, 1, -1};
int orangesRotting(vector<vector<int>>& grid) {
h = grid.size();
w = grid[0].size();
queue<pair<int, int>> q; //存储腐烂的橘子的坐标
for (int i = 0; i < h; i ++)
{
for (int j = 0; j < w; j ++)
{
if (grid[i][j] == 1) fresh ++;
else if (grid[i][j] == 2) q.push({i, j});
}
}
while (!q.empty())
{
int len = q.size();
bool flag = false;
//取出当前队列中所有的腐烂橘子,向下一层腐烂
for (int i = 0; i < len; i ++)
{
auto hh = q.front();
q.pop();
int x = hh.first, y = hh.second;
for (int d = 0; d < 4; d ++)
{
int a = x + dx[d], b = y + dy[d];
if (a < 0 || a >= h || b < 0 || b >= w) continue;
if (grid[a][b] == 1)
{
grid[a][b] = 2;
q.push({a, b});
fresh --;
flag = true;
}
}
}
if (flag) ans ++;
}
if (fresh > 0) ans = -1;
return ans;
}
};
23.拓扑排序
思路(topsort):
1. 明确这是一个有向图,并且y一定在x前面,这满足拓扑序列(有向无环图)的条件:
(1)每个顶点出现且只出现一次。
(2)若存在一条从顶点 A 到顶点 B 的路径,那么在序列中顶点 A 出现在顶点 B 的前面。
2. 存储每一个节点的入度,入度为0的点为起点(入度:以当前节点为终点的边的个数)
3. 每次都选取入度为 0 的点加入拓扑队列中,再删除与这一点连接的所有边。
4. 邻接表存储边
topsort板子:
bool topsort(int numCourses)
{
for (int i = 0; i < numCourses; i ++)
if (d[i] == 0) q[++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (d[j] != 0) d[j] --;
if (d[j] == 0) q[++ tt] = j;
}
}
if (tt == numCourses - 1) return true;
return false;
}
图示模拟:
无环:
有环(不能构成拓扑序):
class Solution {
public:
//邻接表
int n;
int h[100010], e[100010], ne[100010], idx = 0;
int q[100010], d[100010], hh = 0, tt = -1;
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
bool topsort(int numCourses)
{
for (int i = 0; i < numCourses; i ++)
if (d[i] == 0) q[++ tt] = i;
while (hh <= tt)
{
int t = q[hh ++];
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i];
if (d[j] != 0) d[j] --;
if (d[j] == 0) q[++ tt] = j;
}
}
if (tt == numCourses - 1) return true;
return false;
}
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
if (prerequisites.empty()) return true;
memset(h, -1, sizeof h);
n = prerequisites.size();
set<pair<int, int>> used;
for (int i = 0; i < n; i ++)
{
int x = prerequisites[i][0], y = prerequisites[i][1];
if (x == y) return false;
if (used.find({x, y}) != used.end()) return false;
used.insert({y, x});
add(y, x);
d[x] ++;
}
if (topsort(numCourses)) return true;
return false;
}
};