题目列表:代码随想录 (programmercarl.com)
常用算法
暴力 | O ( n ) O(n) O(n) |
---|---|
二分 | O ( log n ) O(\text{log}n) O(logn) |
二分的前提是数组是有序的,二分法每次能排除一半的元素,时间复杂度为 O ( log n ) O(\text{log}n) O(logn)。
使用二分法是要注意解是否包含在左右边界中,在循环中,区间的定义不能改变(区间的开闭)。
暴力 | O ( n 2 ) O(n^2) O(n2) |
---|---|
二分 | O ( n ) O(n) O(n) |
暴力 | O ( n 2 ) O(n^2) O(n2) |
---|---|
二分 | O ( n ) O(n) O(n) |
理解滑动窗口如何移动、窗口起始位置、如何更新窗口大小。
注意状态更新和数组越界。
常用技巧:虚拟头结点
常见题型:
注意nullptr。
集合 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::set | 红黑树 | 有序 | 否 | 否 | O ( log n ) O(\text{log}n) O(logn) | O ( log n ) O(\text{log}n) O(logn) |
std::multiset | 红黑树 | 有序 | 是 | 否 | O ( log n ) O(\text{log}n) O(logn) | O ( log n ) O(\text{log}n) O(logn) |
std::unordered_set | 哈希表 | 无序 | 否 | 否 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
映射 | 底层实现 | 是否有序 | 数值是否可以重复 | 能否更改数值 | 查询效率 | 增删效率 |
---|---|---|---|---|---|---|
std::map | 红黑树 | key有序 | key不可重复 | key不可修改 | O ( log n ) O(\text{log}n) O(logn) | O ( log n ) O(\text{log}n) O(logn) |
std::multimap | 红黑树 | key有序 | key可重复 | key不可修改 | O ( log n ) O(\text{log}n) O(logn) | O ( log n ) O(\text{log}n) O(logn) |
std::unordered_map | 哈希表 | key无序 | key不可重复 | key不可修改 | O ( 1 ) O(1) O(1) | O ( 1 ) O(1) O(1) |
双指针
模拟
空间复杂度 O ( 1 ) O(1) O(1)解法:
首先扩充数组到每个空格替换成"%20"之后的大小。然后从后向前替换空格,也就是双指针法。
其实很多数组填充类的问题,都可以先预先给数组扩容带填充后的大小,然后在从后向前进行操作。
空间复杂度 O ( 1 ) O(1) O(1)解法:
空间复杂度 O ( 1 ) O(1) O(1)解法:
重要Knuth-Morris-Pratt (KMP) 字符串查找算法
暴力算法: O ( m × n ) O(m\times n) O(m×n)
KMP算法: O ( m + n ) O(m+n) O(m+n)
aabaa
的最长 ~~公共 ~~相同 前后缀为 aa
,长度为2。当s[i]
不匹配时,应该回退到前缀表 next[i-1]
中记录的下标位继续匹配。
void getNext(int* next, const string& s){
// 初始化
int j = -1; // j 指向前缀末尾位置(前缀长度)
next[0] = j; // next[i] 表示 i(包括i)之前最长相等的前后缀长度(其实就是j)
for(int i = 1; i < s.size(); i++) { // i 指向后缀末尾位置,注意i从1开始
while (j >= 0 && s[i] != s[j + 1]) { // 前后缀不相同了
j = next[j]; // 向前回退
}
if (s[i] == s[j + 1]) { // 找到相同的前后缀
j++;
}
next[i] = j; // 将j(前缀的长度)赋给next[i]
}
}
**如何回退?**举例子:
B
和C
不匹配,发生回退,要找到与后缀 ABA
相同部分最长的前缀,其实等同于找到找到左边部分的最长相等前缀,即为n[j-1]
。
int strStr(string haystack, string needle) {
if (needle.size() == 0) {
return 0;
}
int next[needle.size()];
getNext(next, needle);
int j = -1; // // 因为next数组里记录的起始位置为-1
for (int i = 0; i < haystack.size(); i++) { // 注意i就从0开始
while(j >= 0 && haystack[i] != needle[j + 1]) { // 不匹配
j = next[j]; // j 寻找之前匹配的位置
}
if (haystack[i] == needle[j + 1]) { // 匹配,j和i同时向后移动
j++; // i的增加在for循环里
}
if (j == (needle.size() - 1) ) { // 文本串s里出现了模式串t
return (i - needle.size() + 1);
}
}
return -1;
}
思路:构建next
数组,数组长度减去最长相同前后缀的长度相当于是第一个周期的长度,也就是一个周期的长度,如果这个周期可以被整除,就说明整个数组就是这个周期的循环。
erase是 O ( n ) O(n) O(n)的操作,放在for
循环里会导致 O ( n 2 ) O(n^2) O(n2)的复杂度。
std::stack<int, std::vector<int> > third; // 使用vector为底层容器的栈
力扣题目链接
stkIn:队尾元素
stkOut:队首元素
deque实现单调栈。
priority_queue 容器适配器定义了一个元素有序排列的队列。默认队列头部的元素优先级最高。因为它是一个队列,所以只能访问第一个元素,这也意味着优先级最高的元素总是第一个被处理。但是如何定义“优先级”完全取决于我们自己。
priority_queue 模板有 3 个参数,其中两个有默认的参数;第一个参数是存储对象的类型,第二个参数是存储元素的底层容器,第三个参数是函数对象,它定义了一个用来决定元素顺序的断言。因此模板类型是:
template <typename T, typename Container=std::vector<T>, typename Compare=std::less<T>> class priority_queue;
// less 大顶堆
// greater 小顶堆
priority_queue 实例默认有一个 vector 容器。函数对象类型 less 是一个默认的排序断言,定义在头文件 functional中,决定了容器中最大的元素会排在队列前面。functional中定义了 greater,用来作为模板的最后一个参数对元素排序,最小元素会排在队列前面。当然,如果指定模板的最后一个参数,就必须提供另外的两个模板类型参数。
class Solution {
public:
static bool cmp(pair<int, int>& m, pair<int, int>& n) {
return m.second > n.second; // 小顶堆
}
vector<int> topKFrequent(vector<int>& nums, int k) {
unordered_map<int, int> occurrences;
for (auto& v : nums) {
occurrences[v]++;
}
// pair 的第一个元素代表数组的值,第二个元素代表了该值出现的次数
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
for (auto& [num, count] : occurrences) {
if (q.size() == k) {
if (q.top().second < count) {
q.pop();
q.emplace(num, count);
}
} else {
q.emplace(num, count);
}
}
vector<int> ret;
while (!q.empty()) {
ret.emplace_back(q.top().first);
q.pop();
}
return ret;
}
};
提问:
栈里面的元素在内存中是连续分布的么?
一、二叉树的种类
二、二叉树的存储方式
三、二叉树的遍历方式
DFS两种实现
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> stk;
vector<int> res;
if(root != nullptr) stk.emplace(root);
while(!stk.empty()) {
TreeNode* cur = stk.top();
stk.pop();
res.emplace_back(cur->val);
if(cur->right != nullptr) stk.emplace(cur->right);
if(cur->left != nullptr) stk.emplace(cur->left);
}
return res;
}
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> stk;
vector<int> res;
TreeNode* cur = root;
while(cur != nullptr || !stk.empty()) {
if(cur != nullptr) {
stk.emplace(cur);
cur = cur->left;
} else {
cur = stk.top();
stk.pop();
res.emplace_back(cur->val);
cur = cur->right;
}
}
return res;
}
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> stk;
vector<int> res;
if(root == nullptr) return res;
stk.emplace(root);
while(!stk.empty()) {
TreeNode* cur = stk.top();
stk.pop();
res.emplace_back(cur->val);
if(cur->left != nullptr) stk.emplace(cur->left);
if(cur->right != nullptr) stk.emplace(cur->right);
}
reverse(res.begin(), res.end());
return res;
}
BFS实现:
队列
比较leftNode->left
与rightNode->right
、leftNode->right
与rightNode->left
。
类似题目:剑指 Offer 07. 重建二叉树
后序遍历的顺序为:左右中
后序遍历postorder
的最后一个元素为当前根节点root
,在中序遍历搜索root
的索引,可将中序遍历inorder
划分为[leftTree | root | rightTree]
,随即可求出leftTree
和rightTree
的长度,根据子树长度可将后序遍历划分为leftTree | rightTree | root
。由于构建二叉树时确定的是postorder
中的root
位置和子树长度,所以右子树的根节点为root-1
,左子树的根节点为root - 1 - rightLen
。
递归构建二叉树。
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
// inorder 根结点左侧为左子树,右侧为右子树
for(int i = 0; i < inorder.size(); ++ i) {
dict[inorder[i]] = i;
}
return bulid(inorder, postorder, inorder.size() - 1, 0, inorder.size() - 1);
}
private:
unordered_map<int, int> dict;
TreeNode* bulid(vector<int>& inorder, vector<int>& postorder, int i, int l, int r) {
if(l > r) return nullptr;
TreeNode* root = new TreeNode(postorder[i]);
int rootIdx = dict[postorder[i]];
root->left = bulid(inorder, postorder, i - 1 - (r - rootIdx), l, rootIdx - 1); // (r - rootIdx) 为右子树长度
root->right = bulid(inorder, postorder, i - 1, rootIdx + 1, r);
return root;
}
};
同理,前序遍历是构建二叉树时确定的是preorder
中的root
位置和子树长度,所以左子树的根节点为root+1
,左子树的根节点为root + 1 + leftLen
。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == q || root == p || root == NULL) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left != NULL && right != NULL) return root;
if (left == NULL) return right;
return left;
}
};
伪代码:
void backtracking(参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking(路径,选择列表); // 递归
回溯,撤销处理结果
}
}
方法一:回溯
class Solution {
public:
vector<string> findItinerary(vector<vector<string>>& tickets) {
int ticketNum = tickets.size();
if(ticketNum == 1) return tickets[0];
for(vector<string> t : tickets) {
targets[t[0]][t[1]] ++;
}
res.emplace_back("JFK");
recur("JFK", ticketNum);
return res;
}
private:
vector<string> res;
unordered_map<string, map<string,int>> targets;
bool recur(const string& from, int ticketNum) {
if(res.size() == ticketNum + 1) return true;
for(auto& to : targets[from]) { // 必须是引用
if(to.second > 0) {
-- to.second;
res.emplace_back(to.first);
if(recur(to.first, ticketNum)) return true;
res.pop_back();
++ to.second;
}
}
return false;
}
};
方法二:Hierholzer 算法
化简题意:给定一个 n n n个点 m m m 条边的图,要求从指定的顶点出发,经过所有的边恰好一次(可以理解为给定起点的「一笔画」问题),使得路径的字典序最小。
这种「一笔画」问题与欧拉图或者半欧拉图有着紧密的联系,下面给出定义:
如果没有保证至少存在一种合理的路径,我们需要判别这张图是否是欧拉图或者半欧拉图,具体地:
- 对于无向图 G G G, G G G 是欧拉图当且仅当 G G G是连通的且没有奇度顶点。
- 对于无向图 G G G, G G G是半欧拉图当且仅当 G G G是连通的且 G G G中恰有 0 0 0个或 2 2 2 个奇度顶点。
- 对于有向图 G G G, G G G 是欧拉图当且仅当 G G G 的所有顶点属于同一个强连通分量且每个顶点的入度和出度相同。
- 对于有向图 G G G, G G G 是半欧拉图当且仅当
- 如果将 G G G中的所有有向边退化为无向边时,那么 G G G 的所有顶点属于同一个强连通分量;
- 最多只有一个顶点的出度与入度差为 1 1 1;
- 最多只有一个顶点的入度与出度差为 1 1 1;
- 所有其他顶点的入度和出度相同。
Hierholzer 算法用于在连通图中寻找欧拉路径,其流程如下:
注意到只有那个入度与出度差为 1 1 1 的节点会导致死胡同。而该节点必然是最后一个遍历到的节点。我们可以改变入栈的规则,当我们遍历完一个节点所连的所有节点后,我们才将该节点入栈(即逆序入栈)。
对于当前节点而言,从它的每一个非「死胡同」分支出发进行深度优先搜索,都将会搜回到当前节点。而从它的「死胡同」分支出发进行深度优先搜索将不会搜回到当前节点。也就是说当前节点的死胡同分支将会优先于其他非「死胡同」分支入栈。
这样就能保证我们可以「一笔画」地走完所有边,最终的栈中逆序地保存了「一笔画」的结果。我们只要将栈中的内容反转,即可得到答案。
class Solution {
public:
unordered_map<string, priority_queue<string, vector<string>, std::greater<string>>> vec; // string->priority_queue 小顶堆
vector<string> stk;
void dfs(const string& curr) { // 深度优先搜索
while (vec.count(curr) && vec[curr].size() > 0) { // 映射表中存在当前出发点,且目的节点数不为0
string tmp = vec[curr].top(); //字典序大的先入栈,逆序后字典序最小
vec[curr].pop(); // 移除当前边
dfs(move(tmp));
}
stk.emplace_back(curr); // 遇到死胡同入栈
}
vector<string> findItinerary(vector<vector<string>>& tickets) {
for (auto& it : tickets) {
vec[it[0]].emplace(it[1]); // 构建映射表
}
dfs("JFK");
reverse(stk.begin(), stk.end());
return stk;
}
};
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
if (nums.size() <= 1) return nums.size();
int curDiff = 0; // 当前一对差值
int preDiff = 0; // 前一对差值
int result = 1; // 记录峰值个数,序列默认序列最右边有一个峰值
for (int i = 0; i < nums.size() - 1; i++) {
curDiff = nums[i + 1] - nums[i];
// 出现峰值
if ((curDiff > 0 && preDiff <= 0) || (preDiff >= 0 && curDiff < 0)) {
result++;
preDiff = curDiff;
}
}
return result;
}
};
class Solution {
public:
int jump(vector<int>& nums) {
int n = nums.size();
if(n == 1) return 0;
int step = 1;
int rightmost = nums[0];
int nextStepMost = 0;
if(rightmost >= n - 1) return step;
for(int i = 1; i < n; i++) {
nextStepMost = max(nextStepMost, i + nums[i]);
if(nextStepMost >= n - 1) return ++ step;
if(rightmost <= i) {
++ step;
rightmost = nextStepMost;
}
}
return step;
}
};
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int result = 0;
int minPrice = prices[0]; // 记录最低价格
for (int i = 1; i < prices.size(); i++) {
// 情况二:相当于买入
if (prices[i] < minPrice) minPrice = prices[i];
// 情况三:保持原有状态(因为此时买则不便宜,卖则亏本)
if (prices[i] >= minPrice && prices[i] <= minPrice + fee) {
continue;
}
// 计算利润,可能有多次计算利润,最后一次计算利润才是真正意义的卖出
if (prices[i] > minPrice + fee) {
result += prices[i] - minPrice - fee;
minPrice = prices[i] - fee; // 情况一,这一步很关键
}
}
return result;
}
};
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
// 0 无覆盖; 1:存在摄像头; 2: 不需要摄像头(被覆盖或空子树)
if (cur == NULL) return 2; // 空子树
int left = traversal(cur->left); // 左
int right = traversal(cur->right); // 右
if (left == 2 && right == 2) return 0; // 左右子树为空 或者 左右子树被覆盖但是没有相机
else if (left == 0 || right == 0) { //
result++; //左右子树中有一个未被覆盖
return 1; //安装摄像头
} else return 2; // 左右子树均被覆盖,且至少存在一个摄像头
}
public:
int minCameraCover(TreeNode* root) {
result = 0;
if (traversal(root) == 0) { // root 无覆盖
result++;
}
return result;
}
};
通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。