LeetCode Hot100 简明题解
温故而知新
哈希
1. 两数之和
枚举-查询-添加
for(int i = 0; i < n; i++){
if(map.contains(target - nums[i])){
return {map[target - nums[i]], i};
}
map[nums[i]] = i;
}
给你一个字符串数组,请你将 字母异位词 组合在一起。可以按任意顺序返回结果列表。
枚举每个字符串,排序后作为同构词的Key
给定一个未排序的整数数组 nums ,找出数字连续的最长序列(不要求序列元素在原数组中连续)的长度。
int longestConsecutive(vector<int>& nums) {
unordered_set<int> set(nums.begin(), nums.end());
int ans = 0;
for(auto v : set){
if(set.contains(v - 1)) continue;
int u = v;
while(set.contains(u)) u++;
ans = max(ans, u - v);
}
return ans;
}
双指针
283. 移动零
学吧
void moveZeroes(vector<int>& nums) {
int L = 0;
for(auto& v : nums) if(v) swap(v, nums[L++]);
}
11. 盛最多水的容器
通解:高度排序,枚举,更新左右最远可达位置,计算更新ans
双指针做法:
int maxArea(vector<int>& height) {
int ans = 0, L = 0, R = height.size() - 1;
while(L < R){
ans = max(ans, min(height[L], height[R]) * (R - L));
height[L] < height[R] ? L++ : R--;
}
return ans;
}
vector<vector<int>> threeSum(vector<int>& nums) {
int n = nums.size();
sort(nums.begin(), nums.end());
vector<vector<int>> ans;
for(int i = 0; i < n; i++){
if(i > 0 && nums[i] == nums[i - 1]) continue; // i去重
int k = n - 1; //j k同维
for(int j = i + 1; j < n; j++)
{
if(j > i + 1 && nums[j] == nums[j - 1]) continue; // j去重
while(k > j && (k < n - 1 && nums[k] == nums[k + 1])) k--; // k去重
while(k > j && (nums[i] + nums[j] + nums[k] > 0)) k--; // > 0 的排除
//合并写法
//while(k > j && ((nums[i] + nums[j] + nums[k] > 0) || (k < n - 1 && nums[k] == nums[k + 1]))) k--;
if(k > j && nums[i] + nums[j] + nums[k] == 0) ans.push_back({nums[i], nums[j], nums[k]});
}
}
return ans;
}
42. 接雨水
计算的方式是 每个格子竖直方向最多接多高的雨水,即两侧最大值的较小者-当前格子高度,有点类似积分。
前后缀分解
int trap(vector<int>& height) {
int ans = 0, n = height.size();
vector<int> left(n), right(n);
left[0] = height[0], right[n - 1] = height[n - 1];
for(int i = 1; i < n; i++) left[i] = max(left[i - 1], height[i]);
for(int i = n - 2; i >= 0; i--) right[i] = max(right[i + 1], height[i]);
for(int i = 1; i < n - 1; i++) ans += min(left[i], right[i]) - height[i];
return ans;
}
同向双指针
int trap(vector<int>& height) {
int ans = 0, L = 0, R = height.size() - 1, mxL = 0, mxR = 0;
while(L < R){
mxL = max(mxL, height[L]);
mxR = max(mxR, height[R]);
ans += mxL < mxR ? mxL - height[L++] : mxR - height[R--];
}
return ans;
}
另一种计算方式是,累加每个水平方向的连续块
单调栈
int trap(vector<int>& height) {
int ans = 0, n = height.size();
stack<int> st;
for(int i = 0; i < n; i++){
int mxR = height[i];
while(!st.empty() && mxR >= height[st.top()]){
int bottom = height[st.top()];
st.pop();
if(!st.empty()){
int mxL = height[st.top()];
//(i - st.top() - 1) 水平方向宽度
ans += (min(mxL, mxR) - bottom) * (i - st.top() - 1);
}
}
st.push(i);
}
return ans;
}
滑动窗口
- 枚举左端点
int lengthOfLongestSubstring(string s) {
int ans = 0, n = s.size(), vis[128];
for(int i = 0, j = 0; i < n; i++){
while(j < n && !vis[s[j]]) vis[s[j++]]++;
ans = max(ans, j - i);
vis[s[i]]--;
}
return ans;
}
- 枚举右端点
int lengthOfLongestSubstring(string s) {
int ans = 0, n = s.size(), vis[128];
for(int i = 0, j = 0; j < n; j++){
vis[s[j]]++;
while(vis[s[j]] > 1) vis[s[i++]]--;
ans = max(ans, j - i + 1);
}
return ans;
}
给定两个字符串 s 和 p,找到 s 中所有 p 的异位词的子串,返回这些子串的起始索引。不考虑答案输出的顺序。
不要
vector<int> findAnagrams(string s, string p) {
int n = s.size(), m = p.size();
vector<int> ans;
int count[26]{0};
for(int i = 0; i < m; i++) count[p[i] - 'a']++;
int diff = 0;
for(int k = 0; k < 26; k++) if(count[k] > 0) diff++;
for(int i = 0; i < n; i++){
int v = s[i] - 'a';
if(count[v] == 0) diff++;
count[v]--;
if(count[v] == 0) diff--;
if(i >= m - 1){
int L = i - m + 1;
if(diff == 0) ans.push_back(L);
int u = s[L] - 'a';
if(count[u] == 0) diff++;
count[u]++;
if(count[u] == 0) diff--;
}
}
return ans;
}
int subarraySum(vector<int>& nums, int k) {
int ans = 0, sum = 0;
unordered_map<int, int> count;
count[0] = 1;
for(auto v : nums){
sum += v;
ans += count[sum - k]; //前缀和 sum-pre=k ->pre=sum-k
count[sum]++;
}
return ans;
}
239. 滑动窗口最大值
单调队列
队列保持递减(单调不增),则左侧第一个即当前结果,左侧出k窗口的退出。
vector<int> maxSlidingWindow(vector<int>& nums, int k) {
int n = nums.size();
deque<int> deq;
vector<int> ans; //size = n - k + 1;
for(int i = 0; i < n; i++){
int v = nums[i];
while(!deq.empty() && v >= nums[deq.back()]) deq.pop_back();
deq.push_back(i);
if(i >= k - 1){
ans.push_back(nums[deq.front()]);
if(deq.front() == i - k + 1) deq.pop_front();
}
}
return ans;
}
给你一个字符串 s 、一个字符串 t 。返回 s 中涵盖 t 所有字符的最小子串。如果 s 中不存在涵盖 t 所有字符的子串,则返回空字符串 "" 。
核心思路同 438. 找到字符串中所有字母异位词
string minWindow(string s, string t) {
int n = s.size(), m = t.size();
int count[128];
for(auto v : t) count[v]++;
int diff = 0;
for(auto v : count) if(v) diff++;
int L = -1, R = n;
for(int i = 0, j = 0; i < n; i++){
while(j < n && diff){
count[s[j]]--;
if(count[s[j]] == 0) diff--;
j++;
}
if(diff > 0) break;
if(j - i < R - L){
L = i; R = j;
}
if(count[s[i]] == 0) diff++;
count[s[i]]++;
}
return L == -1 ? "" : s.substr(L, R - L);
}
数组
int maxSubArray(vector<int>& nums) {
int ans = INT_MIN, sum = 0; //有负值 ans起始MIN
for(auto v : nums){
sum += v;
ans = max(ans, sum);
if(sum < 0) sum = 0;
}
return ans;
}
vector<vector<int>> merge(vector<vector<int>>& intervals) {
vector<vector<int>> ans;
sort(intervals.begin(), intervals.end());
for(auto v : intervals){
if(ans.empty() || v[0] > ans.back()[1]) ans.push_back(v);
else ans.back()[1] = max(ans.back()[1], v[1]); //取最大值
}
return ans;
}
189. 轮转数组
重点是空间复杂度
模拟复制
nums2[(i + k) % n] = nums[i];
gcd
void rotate(vector<int>& nums, int k) {
int n = nums.size();
int gcd = __gcd(n, k);
for(int p = 0; p < gcd; p++){
int i = p, t = n / gcd, left = nums[p];
while(t-- > 0){
int j = (i + k) % n;
swap(nums[j], left);
i = j;
}
}
}
翻转数组
void rotate(vector<int>& nums, int k) {
int n = nums.size();
auto reverse = [&](int L, int R)->void{ while(L < R) swap(nums[L++], nums[R--]); };
reverse(0, n - 1);
reverse(0, k % n - 1);
reverse(k % n, n - 1);
}
238. 除自身以外数组的乘积
前后缀分解
41. 缺失的第一个正数
置换
int firstMissingPositive(vector<int>& nums) {
int n = nums.size();
for(int i = 0; i < n; i++){
while(nums[i] > 0 && nums[i] <= n && nums[nums[i] - 1] != nums[i])
swap(nums[nums[i] - 1], nums[i]);
}
for(int i = 0; i < n; i++) if(nums[i] != i + 1) return i + 1;
return n + 1;
}
矩阵
给定一个 m x n 的矩阵,如果一个元素为 0 ,则将其所在行和列的所有元素都设为 0 。请使用 原地 算法。
感觉并不是什么很好的题目,这个要求的核心思想在于:
用矩阵的第一行和第一列存储每一列和每一行是否有0。
先记录原本第一行第一列是否有0,然后先二维枚举一遍,把行列是否有0的信息存到第一行第一列里。
如果某一行出现了0,则第一列的该行本身就会变为0,所以数据丢失也不会有问题。
最后处理第一行第一列是否需要置零。
vector<int> spiralOrder(vector<vector<int>>& matrix) {
int n = matrix.size(), m = matrix[0].size();
vector<int> ans;
int top = 0, bottom = n - 1, left = 0, right = m - 1;
int x = 0, y = 0;
while(true){
while(y <= right) ans.push_back(matrix[x][y++]);
if(top == bottom) break;
y--; x++; top++;
while(x <= bottom) ans.push_back(matrix[x++][y]);
if(left == right) break;
x--; y--; right--;
while(y >= left) ans.push_back(matrix[x][y--]);
if(top == bottom) break;
y++; x--; bottom--;
while(x >= top) ans.push_back(matrix[x--][y]);
if(left == right) break;
x++; y++; left++;
}
return ans;
}
顺时针旋转90度=先主对角线翻转再左右翻转=先上下翻转再主对角线翻转
240. 搜索二维矩阵 II
右上角折线搜索 本质是以右上(或左下)为根的BST
bool searchMatrix(vector<vector<int>>& matrix, int target) {
int n = matrix.size(), m = matrix[0].size();
int x = 0, y = m - 1;
while(x < n && y >= 0){
int v = matrix[x][y];
if(v == target) return true;
if(v < target) x++;
else y--;
}
return false;
}
链表
160. 相交链表
空间
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto pa = headA, pb = headB;
while(pa != pb){
//每次走一步 走到当前链表的末尾之后的空节点
pa = pa ? pa->next : headB;
pb = pb ? pb->next : headA;
}
return pa;
}
ListNode* reverseList(ListNode* head) {
ListNode* pre = nullptr;
while(head != nullptr){
//循环置换 (head, head.next, pre)=(head.next, pre, head);
/*
auto pt = head;
head = head->next;
pt->next = pre;
pre = pt;
*/
/*
auto pt = head->next;
head->next = pre;
pre = head;
head = pt;
*/
auto pt = pre;
pre = head;
head = head->next;
pre->next = pt;
}
return pre;
}
234. 回文链表
递归写法
bool isPalindrome(ListNode* head) {
auto left = head;
auto dfs = [&](auto&& self, ListNode* cur)->bool {
if(cur == nullptr) return true;
if(!self(self, cur->next)) return false;
if(left->val != cur->val) return false;
left = left->next;
return true;
};
return dfs(dfs, head);
}
空间复杂度
141. 环形链表
快慢指针
bool hasCycle(ListNode *head) {
auto slow = head, fast = head;
while(slow != nullptr && fast != nullptr && fast->next != nullptr){
slow = slow->next;
fast = fast->next->next;
if(slow == fast) return true;
}
return false;
}
ListNode *detectCycle(ListNode *head) {
auto slow = head, fast = head;
while(true){
if(fast == nullptr || fast->next == nullptr) return nullptr;
slow = slow->next;
fast = fast->next->next;
if(slow == fast) break;
}
slow = head;
while(slow != fast) slow = slow->next, fast = fast->next;
return slow;
}
21. 合并两个有序链表
递归写法
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(list1 == nullptr) return list2;
if(list2 == nullptr) return list1;
if(list1->val <= list2->val){
list1->next = mergeTwoLists(list1->next, list2);
return list1;
}
else{
list2->next = mergeTwoLists(list2->next, list1);
return list2;
}
}
ListNode* addTwoNumbers(ListNode* l1, ListNode* l2) {
int add = 0;
ListNode* head = new ListNode();
auto node = head;
while(l1 || l2){
int total = (l1 ? l1->val : 0) + (l2 ? l2->val : 0) + add;
node->next = new ListNode(total % 10);
add = total / 10;
if(l1) l1 = l1->next;
if(l2) l2 = l2->next;
node = node->next;
}
if(add > 0) node->next = new ListNode(add); //重要
return head->next;
}
19. 删除链表的倒数第 N 个结点
头结点可能会为空 使用dummy节点方便。
ListNode* removeNthFromEnd(ListNode* head, int n) {
auto dummy = new ListNode(0, head);
auto slow = dummy, fast = dummy;
while(n--) fast = fast->next;
while(fast->next) slow = slow->next, fast = fast->next;
slow->next = slow->next->next;
return dummy->next;
}
24. 两两交换链表中的节点
递归做法
ListNode* swapPairs(ListNode* head) {
if(!head || !head->next) return head;
auto res = head->next;
head->next = swapPairs(head->next->next);
res->next = head;
return res;
}
ListNode* reverseKGroup(ListNode* head, int k) {
ListNode dummy;
int count = 0;
auto Check = [&](ListNode* cur)->bool {
auto pt = cur;
int count = k;
while(pt && count){
pt = pt->next;
count--;
}
return count == 0;
};
auto Reverse = [&](ListNode* cur)->vector<ListNode*> {
count = 0;
ListNode* pre = nullptr;
ListNode* next = nullptr;
vector<ListNode*> res(2);
while(cur && count < k){
next = cur->next;
auto tmp = cur;
cur = cur->next;
tmp->next = pre;
pre = tmp;
count++;
}
return {pre, next};
};
auto pre = &dummy;
while(Check(head)){
auto tmp = head;
auto res = Reverse(head);
pre->next = res[0];
head = res[1];
pre = tmp;
}
pre->next = head;
return dummy.next;
}
unordered_map<Node*, Node*> cachedNode;
Node* copyRandomList(Node* head) {
if (head == nullptr) {
return nullptr;
}
if (!cachedNode.count(head)) {
Node* headNew = new Node(head->val);
cachedNode[head] = headNew;
headNew->next = copyRandomList(head->next);
headNew->random = copyRandomList(head->random);
}
return cachedNode[head];
}
另外有一种修改原链表的处理方式,逐个插入
148. 排序链表
归并排序
分治
迭代
23. 合并 K 个升序链表
标准优先队列做法。
注意C++优先队列自定义元素类型,自定义比较函数写法。
特别注意less对应大根堆,greater对应小根堆。
struct compare{
bool operator() (const ListNode* A, const ListNode* B){
return A->val > B->val;
}
};
ListNode* mergeKLists(vector<ListNode*>& lists) {
ListNode dummy;
priority_queue<ListNode*, vector<ListNode*>, compare> pq;
for(auto node : lists) if(node) pq.push(node);
auto node = &dummy;
while(!pq.empty()){
auto top = pq.top(); pq.pop();
node->next = new ListNode(top->val);
node = node->next;
if(top->next) pq.push(top->next);
}
return dummy.next;
}
分治
146. LRU 缓存
哈希表+双向链表
二叉树
94. 二叉树的中序遍历
递归
迭代
vector<int> inorderTraversal(TreeNode* root) {
vector<int> ans;
stack<TreeNode*> st;
while(!st.empty() || root != nullptr){
while(root != nullptr){
st.push(root);
root = root->left;
}
root = st.top();
st.pop();
ans.push_back(root->val);
root = root->right;
}
return ans;
}
Morris遍历
104. 二叉树的最大深度
自底向上
int maxDepth(TreeNode* root) {
if(root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right)) + 1;
}
自顶向下
int maxDepth(TreeNode* root) {
int ans = 0;
auto dfs = [&](auto&& self, TreeNode* node, int depth)->void{
if(node == nullptr){
ans = max(ans, depth);
return;
}
self(self, node->left, depth + 1);
self(self, node->right, depth + 1);
};
dfs(dfs, root, 0);
return ans;
}
226. 翻转二叉树
自底向上
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr) return root;
auto left = invertTree(root->left);
auto right = invertTree(root->right);
root->left = right;
root->right = left;
return root;
}
自顶向下
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr) return root;
swap(root->left, root->right);
invertTree(root->left);
invertTree(root->right);
return root;
}
bool isSymmetric(TreeNode* root) {
auto dfs = [&](auto&& self, TreeNode* L, TreeNode* R)->bool {
if(L == nullptr && R == nullptr) return true;
if(L == nullptr || R == nullptr) return false;
if(L->val != R->val) return false;
return self(self, L->right, R->left) && self(self, L->left, R->right);
};
return dfs(dfs, root->left, root->right);
}
543. 二叉树的直径
树形dp
int diameterOfBinaryTree(TreeNode* root) {
int ans = 0;
auto dfs = [&](auto&& self, TreeNode* node)->int {
if(node == nullptr) return 0;
int L = self(self, node->left);
int R = self(self, node->right);
ans = max(ans, L + R);
return max(L, R) + 1;
};
dfs(dfs, root);
return ans;
}
另一种是两次dfs/bfs求解
102. 二叉树的层序遍历
BFS基本应用
108. 将有序数组转换为二叉搜索树
TreeNode* sortedArrayToBST(vector<int>& nums) {
auto dfs = [&](auto&& self, int L, int R)->TreeNode* {
if(L > R) return nullptr;
int mid = L + (R - L) / 2;
TreeNode* node = new TreeNode(nums[mid]);
node->left = self(self, L, mid - 1);
node->right = self(self, mid + 1, R);
return node;
};
return dfs(dfs, 0, nums.size() - 1);
}
bool isValidBST(TreeNode* root) {
auto dfs = [&](auto&& self, TreeNode* node, i64 minVal, i64 maxVal)->bool{
if(node == nullptr) return true;
if(node->val >= maxVal || node->val <= minVal) return false;
return self(self, node->left, minVal, min(maxVal, (i64)node->val))
&& self(self, node->right, max(minVal, (i64)node->val), maxVal);
};
return dfs(dfs, root, LONG_MIN, LONG_MAX);
}
另一种可以中序遍历 检测是否有序
230. 二叉搜索树中第 K 小的元素
中序遍历二叉搜索树的结果是有序列表
int kthSmallest(TreeNode* root, int k) {
int ans = 0;
auto dfs = [&](auto&& self, TreeNode* node){
if(!node) return;
self(self, node->left);
if(--k == 0) ans = node->val;
self(self, node->right);
};
dfs(dfs, root);
return ans;
}
记录子树size可以快速多次查询
如果有增删操作 需要用平衡树
199. 二叉树的右视图
vector<int> rightSideView(TreeNode* root) {
vector<int> ans;
auto dfs = [&](this auto&& dfs, TreeNode* node, int depth) -> void {
if (node == nullptr) {
return;
}
if (depth == ans.size()) { // 这个深度首次遇到
ans.push_back(node->val);
}
dfs(node->right, depth + 1); // 先递归右子树,保证首次遇到的一定是最右边的节点
dfs(node->left, depth + 1);
};
dfs(root, 0);
return ans;
}
void flatten(TreeNode* root) {
auto dfs = [&](auto&& self, TreeNode* node)->TreeNode* {
auto left = node->left;
auto right = node->right;
node->left = nullptr;
if(left){
node->right = left;
node = self(self, left);
}
if(right){
node->right = right;
node = self(self, right);
}
return node;
};
if(root) dfs(dfs, root);
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
auto dfs = [&](auto&& self, int L1, int R1, int L2, int R2)->TreeNode* {
if(L1 > R1) return nullptr;
auto node = new TreeNode(preorder[L1]);
int t = -1;
for(int t = L2; t <= R2; t++) if(inorder[t] == preorder[L1]) break;
node->left = self(self, L1 + 1, L1 + t - L2, L2, t - 1); //取相同长度 t - L2 + 1
node->right = self(self, L1 + t - L2 + 1, R1, t + 1, R2);
return node;
};
return dfs(dfs, 0, preorder.size() - 1, 0, inorder.size() - 1);
}
int pathSum(TreeNode* root, int targetSum) {
unordered_map<long long, int> dict{{0, 1}};
int ans = 0;
auto dfs = [&](auto&& self, TreeNode* node, long long sum)->void{
sum += node->val;
ans += dict[sum - targetSum];
dict[sum]++;
if(node->left) self(self, node->left, sum);
if(node->right) self(self, node->right, sum);
dict[sum]--;
sum -= node->val;
};
if(root) dfs(dfs, root, 0);
return ans;
}
236. 二叉树的最近公共祖先
一个很巧妙的写法
//fa表示父节点映射
auto p2 = p, q2 = q;
while(p2 != q2){
p2 = fa[p2];
if(p2 == nullptr) p2 = q;
q2 = fa[q2];
if(q2 == nullptr) q2 = p;
}
return p2;
纯递归写法
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root == nullptr || root == p || root == q) return root;
auto left = lowestCommonAncestor(root->left, p, q);
auto right = lowestCommonAncestor(root->right, p, q);
return left && right ? root : left ? left : right;
}
124. 二叉树中的最大路径和
简单树形dp
int maxPathSum(TreeNode* root) {
int ans = INT_MIN;
auto dfs = [&](auto&& self, TreeNode* node)->int{
int left = 0, right = 0;
if(node->left) left = self(self, node->left);
if(node->right) right = self(self, node->right);
ans = max(ans, node->val + left + right);
return max(0, node->val + max(left, right));
};
dfs(dfs, root);
return ans;
}
图论
200. 岛屿数量
矩阵DFS基本
int POS_X[4] = {-1, 1, 0, 0};
int POS_Y[4] = {0, 0, -1, 1};
int numIslands(vector<vector<char>>& grid) {
int ans = 0;
int n = grid.size(), m = grid[0].size();
auto dfs = [&](auto&& self, int x, int y)->void {
grid[x][y] = '0';
for(int k = 0; k < 4; k++){
int x2 = x + POS_X[k];
int y2 = y + POS_Y[k];
if(x2 >= 0 && x2 < n && y2 >= 0 && y2 < m && grid[x2][y2] == '1'){
self(self, x2, y2);
}
}
};
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(grid[i][j] == '1'){
dfs(dfs, i, j);
ans++;
}
}
}
return ans;
}
994. 腐烂的橘子
多源BFS
int POS_X[4] = {-1, 1, 0, 0};
int POS_Y[4] = {0, 0, -1, 1};
int orangesRotting(vector<vector<int>>& grid) {
int n = grid.size(), m = grid[0].size();
int total = 0;
queue<int*> que;
for(int i = 0; i < n; i++){
for(int j = 0; j < m; j++){
if(grid[i][j] == 1) total++;
else if(grid[i][j] == 2) que.push(new int[2]{i, j});
}
}
if(!total) return 0; //小心
int ans = 0;
while(!que.empty()){
int count = que.size();
while(count--){
auto v = que.front(); que.pop();
for(int k = 0; k < 4; k++){
int x = v[0] + POS_X[k];
int y = v[1] + POS_Y[k];
if(x >= 0 && x < n && y >= 0 && y < m && grid[x][y] == 1){
que.push(new int[2]{x, y});
total--;
grid[x][y] = 0;
}
}
}
ans++;
}
return total ? -1 : ans - 1;
}
207. 课程表
拓扑排序/判环
bool canFinish(int numCourses, vector<vector<int>>& prerequisites) {
vector<vector<int>> g(numCourses);
vector<int> deg(numCourses);
for(auto& e : prerequisites){
g[e[1]].push_back(e[0]);
deg[e[0]]++;
}
queue<int> que;
for(int i = 0; i < numCourses; i++) if(!deg[i]) que.push(i);
while(!que.empty()){
numCourses--;
int u = que.front(); que.pop();
for(auto v : g[u]){
deg[v]--;
if(!deg[v]) que.push(v);
}
}
return numCourses == 0;
}
208. 实现 Trie (前缀树)
Node节点的属性包括:
后续节点的引用vector<Node*>
当前节点是否是结束节点。
回溯
46. 全排列
暴力选择
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ans;
vector<int> p;
vector<int> vis(n);
auto dfs = [&](auto&& self, int cur)->void{
if(cur == n) ans.push_back(p);
for(int i = 0; i < n; i++){
if(!vis[i]){
p.push_back(nums[i]);
vis[i] = 1;
self(self, cur + 1);
vis[i] = 0;
p.pop_back();
}
}
};
dfs(dfs, 0);
return ans;
}
获取下一个排列
bool NextPermutation(vector<int>& p){
int n = p.size(), t = -1;
for (int i = 0; i < n - 1; i++) if (p[i + 1] - p[i] > 0) t = i;
if (t == -1) return false;
for (int i = n - 1; ; i--)
{
if (p[i] > p[t])
{
swap(p[i], p[t]);
break;
}
}
sort(p.begin() + t + 1, p.end());
return true;
}
vector<vector<int>> permute(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ans;
vector<int> index(n);
for(int i = 0; i < n; i++) index[i] = i;
do{
vector<int> p;
for(auto v : index) p.push_back(nums[v]);
ans.push_back(p);
}
while(NextPermutation(index));
return ans;
}
vector<vector<int>> subsets(vector<int>& nums) {
int n = nums.size();
vector<vector<int>> ans(1 << n);
for(int mask = 0; mask < (1 << n); mask++){
for(int i = 0; i < n; i++) {
if((mask >> i) & 1) ans[mask].push_back(nums[i]);
}
}
return ans;
}
17. 电话号码的字母组合
就是回溯
vector<string> letterCombinations(string digits) {
vector<vector<char>> map = {
{'a', 'b', 'c'},
{'d', 'e', 'f'},
{'g', 'h', 'i'},
{'j', 'k', 'l'},
{'m', 'n', 'o'},
{'p', 'q', 'r', 's'},
{'t', 'u', 'v'},
{'w', 'x', 'y', 'z'}
};
vector<string> ans;
if(digits == "") return ans;
string s;
auto dfs = [&](auto&& self, int cur)->void{
if(cur == digits.size()){
ans.push_back(s);
return;
}
auto vec = map[digits[cur] - '2'];
for(auto v : vec){
s.push_back(v);
self(self, cur + 1);
s.pop_back();
}
};
dfs(dfs, 0);
return ans;
}
选与不选
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> p;
auto dfs = [&](auto&& self, int cur, int sum)->void{
if(cur == candidates.size()){
if(sum == target){
ans.push_back(p);
}
return;
}
self(self, cur + 1, sum);
int v = candidates[cur];
if(target - sum >= v){
p.push_back(v);
self(self, cur, sum + v);
p.pop_back();
}
};
dfs(dfs, 0, 0);
return ans;
}
枚举选哪一个
vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
vector<vector<int>> ans;
vector<int> p;
auto dfs = [&](auto&& self, int cur, int sum)->void{
if(sum == target){
ans.push_back(p);
return;
}
for(int i = cur; i < candidates.size(); i++){
int v = candidates[i];
if(sum + v <= target){
p.push_back(v);
self(self, i, sum + v);
p.pop_back();
}
}
};
dfs(dfs, 0, 0);
return ans;
}
22. 括号生成
枚举子集 略
选左括号还是右括号
vector<string> generateParenthesis(int n) {
int m = n * 2;
int lc = 0;
string s;
vector<string> ans;
auto dfs = [&](auto&& self, int i)->void{
if(i == m){
if(lc == 0) ans.push_back(s);
return;
}
lc++; s.push_back('(');
self(self, i + 1);
s.pop_back(); lc--;
if(lc > 0){
lc--; s.push_back(')');
self(self, i + 1);
s.pop_back(); lc++;
}
};
dfs(dfs, 0);
return ans;
}
给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
这个凭啥进top100
对 凭啥进top100
vector<vector<string>> partition(string s) {
vector<vector<string>> ans;
vector<string> p;
auto check = [&](int L, int R)->bool{
while(L < R){
if(s[L] != s[R]) return false;
L++; R--;
}
return true;
};
auto dfs = [&](auto&& self, int i)->void{
if(i == s.size()) {
ans.push_back(p);
return;
}
for(int j = i; j < s.size(); j++){
if(check(i, j)){
p.push_back(s.substr(i, j - i + 1));
self(self, j + 1);
p.pop_back();
}
}
};
dfs(dfs, 0);
return ans;
}
二分
int searchInsert(vector<int>& nums, int target) {
int low = 0, high = nums.size() - 1;
while(low <= high){
int mid = low + (high - low) / 2;
if(nums[mid] < target) low = mid + 1;
else high = mid - 1;
}
return low; //low最终是>=target的第一个索引
}
74. 搜索二维矩阵
两次二分
一次二分
右上角搜索
int search(vector<int>& nums, int target) {
int n = nums.size();
int low = 0, high = n - 1;
while(low <= high){
int mid = low + (high - low) / 2;
int v = nums[mid];
if(v == target) return mid; //特殊
if(v < target){
if(v > nums[n - 1]) low = mid + 1;
else {
if(target > nums[n - 1]) high = mid - 1;
else low = mid + 1;
}
}
else{
if(v < nums[n - 1]) high = mid - 1;
else{
if(target > nums[n - 1]) high = mid - 1;
else low = mid + 1;
}
}
}
return (low < n && nums[low] == target) ? low : -1;
}
153. 寻找旋转排序数组中的最小值
闭区间的意义
int findMin(vector<int>& nums) {
int low = 0, high = nums.size() - 1;
while(low < high){
int mid = low + (high - low) / 2;
if(nums[mid] > nums[high]) low = mid + 1;
else high = mid;
}
return nums[low];
}
新手梦魇
double findMedianSortedArrays(vector<int>& nums1, vector<int>& nums2) {
if(nums1.size() > nums2.size()) swap(nums1, nums2);
int n = nums1.size(), m = nums2.size();
int low = 0, high = n;
while(low <= high){
int mid = low + (high - low) / 2;
int j = (n + m - 1) / 2 - mid;
if(mid == 0 || j == m - 1 || nums1[mid - 1] <= nums2[j + 1]) low = mid + 1;
else high = mid - 1;
}
int L1 = high, L2 = (n + m - 1) / 2 - L1;
int maxL = max(L1 == 0 ? INT_MIN : nums1[L1 - 1], L2 == -1 ? INT_MIN : nums2[L2]);
int minR = L1 == n ? (L2 == m - 1 ? INT_MAX : nums2[L2 + 1]) : min(nums1[L1], L2 == m - 1 ? INT_MAX : nums2[L2 + 1]);
return (n + m) % 2 == 0 ? (maxL + minR) / 2.0 : maxL;
}
栈
bool isValid(string s) {
unordered_map<char, char> dict{{')', '('}, {']', '['}, {'}', '{'}};
stack<char> st;
for(auto v : s){
if(dict.contains(v)){
if(st.empty() || st.top() != dict[v]) return false;
st.pop();
}
else st.push(v);
}
return st.empty();
}
155. 最小栈
前缀最小值
class MinStack {
public:
stack<pair<int, int>> st;
MinStack() {
st.push({0, INT_MAX});
}
void push(int val) {
st.push({val, min(st.top().second, val)});
}
void pop() {
st.pop();
}
int top() {
return st.top().first;
}
int getMin() {
return st.top().second;
}
};
394. 字符串解码
栈或递归 有些麻烦
//递归写法
string decodeString(string s) {
auto dfs = [&](auto&& self, int& i)->string {
string res;
while(i < s.size()){ //s可能是多个词组并列组成
while(i < s.size() && s[i] >= 'a' && s[i] <= 'z') res.push_back(s[i++]);
if(s[i] == ']'){ //递归出口 上层词组的末尾
i++;
return res;
}
//3[abc]
int count = 0;
while(s[i] >= '0' && s[i] <= '9') count = count * 10 + s[i++] - '0';
string child = self(self, ++i); //由于可以嵌套 这里必然是递归调用
for(int k = 0; k < count; k++) res += child;
}
return res;
};
int i = 0;
return dfs(dfs, i);
}
739. 每日温度
单调栈
vector<int> dailyTemperatures(vector<int>& temperatures) {
int n = temperatures.size();
vector<int> ans(n);
stack<int> st;
for(int i = 0; i < n; i++){
int v = temperatures[i];
while(st.size() && temperatures[st.top()] < v){
ans[st.top()] = i - st.top();
st.pop();
}
st.push(i);
}
return ans;
}
84. 柱状图中最大的矩形
单调栈
可以遍历两次,可以倒序枚举
int largestRectangleArea(vector<int>& heights) {
int n = heights.size();
vector<int> mxL(n), mxR(n, n - 1);
stack<int> st; //递增栈。 如果是递减栈 则较小的值完全可以取代左侧的较大值。
//计算每个位置i的向左最远位置和向右最远位置
for(int i = 0; i < n; i++){
int v = heights[i];
while(!st.empty() && heights[st.top()] > v){
mxR[st.top()] = i - 1; //出栈时,栈顶的极右值就是i - 1
st.pop();
}
mxL[i] = st.empty() ? 0 : st.top() + 1; //出栈结束后 当前i的极左值就是栈顶 + 1
st.push(i);
}
int ans = 0;
for(int i = 0; i < n; i++){
ans = max(ans, (mxR[i] - mxL[i] + 1) * heights[i]);
}
return ans;
}
堆
215. 数组中的第K个最大元素
347. 前 K 个高频元素
-快速选择
-堆的实现
295. 数据流的中位数
对顶堆的基本实现,切记每次添加数据 必须对两堆做一次平衡
class MedianFinder {
public:
//最小堆 存储较大的一半 + 1
priority_queue<double, vector<double>, greater<double>> pqL;
//最大堆 存储较小的一半 - 1
priority_queue<double> pqR;
MedianFinder() {
}
void addNum(int num) {
//放入最小堆 然后把最小堆的最小值 放到最大堆
pqL.push(num); pqR.push(pqL.top()); pqL.pop();
//如果最大堆个数超过最小堆 把最大堆的最大值 放回最小堆
if(pqR.size() > pqL.size()){
pqL.push(pqR.top()); pqR.pop();
}
}
double findMedian() {
return pqL.size() == pqR.size() ? (pqL.top() + pqR.top()) / 2 : pqL.top();
}
};
贪心
int maxProfit(vector<int>& prices) {
int ans = 0, minLeft = prices[0];
for(auto v : prices){
ans = max(ans, v - minLeft);
minLeft = min(minLeft, v);
}
return ans;
}
55. 跳跃游戏
bool canJump(vector<int>& nums) {
int n = nums.size();
int mx = 0;
for(int i = 0; i < n; i++){
if(i > mx) return false; //关键
mx = max(mx, i + nums[i]);
}
return true;
}
给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段,同一字母最多出现在一个片段中。
记录每个字母出现的最右端位置
动态规划
70. 爬楼梯
多解
给定一个非负整数 numRows,生成「杨辉三角」的前 numRows 行。
198. 打家劫舍
a表示不选当前元素的最大值,b表选择当前元素的最大值
int rob(vector<int>& nums) {
int a = 0, b = 0;
for(auto v : nums){
int c = a + v;
a = max(a, b);
b = c;
}
return max(a, b);
}
a表示不选当前元素的最大值,b表示选与不选的最大值
int rob(vector<int>& nums) {
int a = 0, b = 0;
for(auto v : nums){
int c = max(a + v, b);
a = b;
b = c;
}
return b;
}
const int MX = INT_MAX / 2;
int numSquares(int n) {
vector<int> dp(n + 1, MX);
dp[0] = 0;
for(int i = 1; i <= n; i++){
for(int j = 1; j <= i / j; j++){
dp[i] = min(dp[i], dp[i - j * j] + 1);
}
}
return dp[n];
}
int coinChange(vector<int>& coins, int amount) {
vector<int> dp(amount + 1, amount + 1);
dp[0] = 0;
for(int i = 1; i <= amount; i++){
for(auto v : coins){
if(i >= v) dp[i] = min(dp[i], dp[i - v] + 1);
}
}
return dp[amount] > amount ? -1 : dp[amount];
}
139. 单词拆分
复杂度是
bool wordBreak(string s, vector<string>& wordDict) {
int n = s.size();
unordered_set<string> set(wordDict.begin(), wordDict.end());
vector<int> dp(n + 1, 0);
dp[0] = 1;
for(int i = 1; i <= n; i++){
for(int j = max(0, i - 20); j < i; j++){
auto sub = s.substr(j, i - j);
if(set.contains(sub)) dp[i] |= dp[j];
}
}
return dp[n];
}
int lengthOfLIS(vector<int>& nums) {
int n = nums.size();
vector<int> dp;
for(auto v : nums){
if(dp.size() == 0 || dp.back() < v) dp.push_back(v);
else *lower_bound(dp.begin(), dp.end(), v) = v;
}
return dp.size();
}
int maxProduct(vector<int>& nums) {
int ans = INT_MIN;
int pst = 1, neg = 1;
for(auto v : nums){
int tmp = pst;
pst = max({pst * v, neg * v, v});
neg = min({tmp * v, neg * v, v});
ans = max(ans, pst);
}
return ans;
}
416. 分割等和子集
标准01背包
bool canPartition(vector<int>& nums) {
int n = nums.size();
int sum = 0;
for(auto v : nums) sum += v;
if(sum % 2) return false;
sum /= 2;
vector<int> dp(sum + 1, 0);
dp[0] = 1;
for(auto v : nums){
for(int i = sum; i >= v; i--){
dp[i] |= dp[i - v];
}
}
return dp[sum];
}
- 动态规划
- 栈
int longestValidParentheses(string s) {
int n = s.size();
stack<int> st;
int ans = 0;
int start = -1;
for(int i = 0; i < n; i++){
if(s[i] == ')'){
if(!st.empty()) st.pop();
else start = i;
ans = max(ans, i - (st.empty() ? start : st.top()));
}
else st.push(i);
}
return ans;
}
- 两次遍历
62. 不同路径
标准二维dp
可以滚动数组
另组合数学解法
using i64 = long long;
i64 p = 2e9 + 11; //注意题目范围是 <=2e9 所有答案必须选择超过2e9的质数,同时小心爆int64 故选择最小的>=2e9的质数
i64 power(i64 x, i64 y, i64 p){
i64 r = 1;
while(y > 0){
if(y & 1) r = r * x % p;
y >>= 1;
x = x * x % p;
}
return r;
}
int uniquePaths(int m, int n) {
vector<i64> f(m + n, 1);
for(int i = 1; i < m + n; i++) f[i] = f[i - 1] * i % p;
return f[m + n - 2] * power(f[m - 1], p - 2, p) % p * power(f[n - 1], p - 2, p) % p;
}
64. 最小路径和
同上dp
int minPathSum(vector<vector<int>>& grid) {
int n = grid.size(), m = grid[0].size();
vector<int> dp(m);
for(int i = 0; i < m; i++) dp[i] = i == 0 ? grid[0][0] : dp[i - 1] + grid[0][i];
for(int i = 1; i < n; i++){
dp[0] += grid[i][0];
for(int j = 1; j < m; j++){
dp[j] = min(dp[j - 1], dp[j]) + grid[i][j];
}
}
return dp[m - 1];
}
- 中心扩展
解
string longestPalindrome(string s) {
int n = s.size();
int mx = 0, left = 0;
auto dfs = [&](int L, int R)->int {
while(L >= 0 && R < n && s[L] == s[R]){
L--; R++;
}
return R - L - 1;
};
for(int i = 0; i < n; i++){
//奇数中心
int odd = dfs(i, i);
if(odd > mx){
mx = odd;
left = i - odd / 2;
}
//偶数中心
int even = dfs(i, i + 1);
if(even >= mx){
mx = even;
left = i - even / 2 + 1;
}
}
return s.substr(left, mx);
}
- Manacher
1143. 最长公共子序列
dp[i][j]与dp[i - 1][j - 1] dp[i - 1][j] dp[i][j - 1]有关
int longestCommonSubsequence(string text1, string text2) {
int n = text1.size(), m = text2.size();
vector<int> dp(m + 1);
for(auto v : text1){
int lt = 0; //"左上角"
for(int i = 1; i <= m; i++){
int tmp = dp[i];
dp[i] = v == text2[i - 1] ? lt + 1 : max(dp[i], dp[i - 1]);
lt = tmp;
}
}
return dp[m];
}
int minDistance(string word1, string word2) {
int n = word1.size(), m = word2.size();
vector<int> dp(m + 1);
for(int i = 0; i <= n; i++){
int lt = 0;
for(int j = 0; j <= m; j++){
int tmp = dp[j];
int cur = INT_MAX / 2;
if(i > 0) cur = min(cur, dp[j] + 1);
if(j > 0) cur = min(cur, dp[j - 1] + 1);
if(i > 0 && j > 0) cur = min(cur, lt + (word1[i - 1] == word2[j - 1] ? 0 : 1));
if(i > 0 || j > 0) dp[j] = cur, lt = tmp;
}
}
return dp[m];
}
技巧
int singleNumber(vector<int>& nums) {
int ans = 0;
for(auto v : nums) ans ^= v;
return ans;
}
int majorityElement(vector<int>& nums) {
int ans = 0, count = 0;
for(auto v : nums){
if(v == ans) count++;
else if(--count < 0) ans = v, count = 1;
}
return ans;
}
75. 颜色分类
两遍扫描
void sortColors(vector<int>& nums) {
int n = nums.size(), pt = 0;
for(int i = 0; i < n; i++){
if(nums[i] == 0){
swap(nums[pt], nums[i]);
pt++;
}
}
pt = n - 1;
for(int i = n - 1; i >= 0; i--){
if(nums[i] == 2){
swap(nums[pt], nums[i]);
pt--;
}
}
}
一遍扫描
void sortColors(vector<int>& nums) {
int n = nums.size(), L = 0, R = n - 1;
for(int i = 0; i <= R; i++){
if(nums[i] == 0) swap(nums[L++], nums[i]);
else if(nums[i] == 2){
swap(nums[R--], nums[i]);
if(nums[i] == 0 && i == L) L++;
else i--;
}
}
}
31. 下一个排列
标准实现
bool NextPermutation(vector<int>& p)
{
int n = p.size(), t = -1;
//找最后一组相邻递增对 如[2,1,3,5,4]里的[3,5]
for (int i = 0; i < n - 1; i++) if (p[i + 1] - p[i] > 0) t = i;
//没找到说明是递减排列 如[5,4,3,2,1]
if (t == -1) return false;
for (int i = n - 1; ; i--)
{
if (p[i] > p[t])
{
//上面找到[3,5]后实际交换的并不一定是[3,5]
//而是3后面最后一个比3大的数字 即4
swap(p[i], p[t]);
break;
}
}
//交换[3,4]后变成[2,1,4,5,3] 由于3 4交换已经增大了,要找4开头的最小排列
//故还需要对[t+1,n-1]排序
sort(p.begin() + t + 1, p.end());
return true;
}
void nextPermutation(vector<int>& nums) {
if(!NextPermutation(nums)){
for(int i = 0; i < nums.size() / 2; i++) swap(nums[i], nums[nums.size() - i - 1]);
}
}
287. 寻找重复数
哈希
二分
二进制分解
快慢指针判环
int findDuplicate(vector<int>& nums) {
int slow = 0, fast = 0;
do {
slow = nums[slow];
fast = nums[nums[fast]];
} while (slow != fast);
slow = 0;
while (slow != fast) {
slow = nums[slow];
fast = nums[fast];
}
return slow;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具