前言
Carl哥在代码随想录的总结让人受益匪浅,仅基于个人理解的角度做部分总结,部分内容引用原文中的概念,所以建议配合原链接一同阅读。不定期完善。
原链接如下:《代码随想录》
3.1-5.12一刷
一、数组
Tips:
- 看到有序数组—>尝试二分法
- 看到有序数组 & 需要构造有序数组 / 剔除元素—>尝试双指针
- 滑动窗口较难,多练
方法 | 复杂度 |
---|---|
二分法 | 暴力:
O
(
n
)
O(n)
O(n) 二分法: O ( l o g n ) O(logn) 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) |
关于二分法:
while
什么时候<
,什么时候<=
?
left = mid +1 , right = mid-1这叫左闭右闭,表示每次搜索的区间都在包括left和right的范围内;如果出现了right = mid,这就叫左闭右开。只有左闭右闭用<=
,因为left == right后的下一步一定能跳出while,但左闭右开用<
,因为左闭右开有个特点:left最终一定会==right,如<=
则有无限循环的可能。
左闭右闭 | <= |
---|---|
左闭右开 | < |
- 对二分搜索和二分边界搜索做一个总结
类型 | 特点 |
---|---|
二分搜索 | 用 <= ,对称即可while外 return -1 ,因为如果存在的话一定在循环内已经return -1 了 |
搜索左右边界 | 可以左闭右开< ,也可以左闭右闭<= ,左闭右闭记得检查越界通常左闭右闭跳出循环后,两指针正好差一位: right == left-1 ,在查找左边界中返回left ,反之返回right |
检查左侧出界情况:
if (left >= nums.size()||nums[left] != target)//出界或不存在
return -1;
二、哈希表
集合 | 映射 | 查询 / 增删效率 | |
---|---|---|---|
红黑树 | set multiset | map multimap | O(logn) |
哈希表 | unordered_set | unordered_map | O(1) |
什么时候用数组:
包含有限的连续个元素(如小写字母),可以记录频率。
什么时候用set:
没有限制数值的大小,或者哈希值很分散、跨度很大,造成空间浪费。Ps:set用insert插入
什么时候用map:
前两者有局限,因为可能元素少,哈希值太大,空间浪费。另外set的元素只能是key,而[两数之和]不仅要检查元素大小还要返回下标,所以set不能用
map
是一种<key,value>结构, 两数之和中,用key保存数值,用value保存数值所在下标:map[ nums[i] ] = i
,同时也可以写成mp.insert(pair<int,int>(nums[i],i))
默认情况下:
- map排序是按key排的
- map自带的
find
函数也是按key查找的。如果要按value查找,参考《map按value值查找——find_if的使用》 - 直接将vector转成unordered_set:
unordered_set<int> st(nums.begin(), nums.end());
- 如果要对
value
排序,只能先转成vector
(pair类型)再排序,因map只能对key
排序。
给频率排序:
bool static cmp (const pair<int, int>& a, const pair<int, int>& b) {
return a.second > b.second; // 按照频率从大到小排序
}
vector<pair<int, int>> vec(map.begin(), map.end());
sort(vec.begin(), vec.end(), cmp); // 给频率排个序
三、字符串
- 基础:反转字符串—>双指针(原理)
- 替换元素: 数组
填充、替换
类的问题(扩大),往往双指针后–>前进行遍历,有时需要reserve
至带填充后的大小 - 移除元素:数组
移除
类问题(缩小),可以双指针前–>后遍历,最后erase
一次即可 - 可以考虑先整体反转、后局部反转
- KMP:重复元素------直接算给定s的next数组(字符串长度)-(next最后一位)可以被(字符串长度)整除,则为重复子字符串,切记先判断next最后一位不为0
KMP
KMP是一种字符串匹配算法,还有Boyer-Moore 算法、Sunday 算法等
复杂度:O(m+n)
作用:在一个串中查找是否出现过另一个串
精髓:当出现字符串不匹配时,可以记录一部分之前已经匹配的文本内容,利用这些信息避免从头再去做匹配
前缀:所有以首字符开头的连续子串(不包含最后一个字符)
后缀:以此类推
最长公共(相等)前后缀:字符串中最长的相同前后缀
|
↓
前缀表(prefix table)
:以每一位结尾的字符串,分别求最长公共前后缀。 解决了模式串与主串不匹配的时候,模式串应该从哪里开始重新匹配
next数组:有的把前缀表整体-1,有的把前缀表右移一位,建议直接拿前缀表当next数组。(只不过代表匹配错时处理冲突的方式不一样)
|
↓
实现next数组过程:
- 初始化
j
前缀末尾 (同时也代表着包括i
的i
之前的字串的最长前后缀)
i
后缀末尾 - 处理前后缀不相同:
while
和之后匹配字符串相同操作,j
移动到next[j-1]
直到相同为止。next[j-1] == n
的含义是:包括j-1
在内的往前n个字符(后缀)和最开头的n个字符(前缀)是相同的,如果j
不匹配不必从0开始,到第n+1个位置(下标n
)去试试看呢?如果成功相同的话,岂不是直接可以把前面n
个都加上了吗?此时 - 处理前后缀相同:
for
- 更新next数组
void getNext(int* next, const string& s) {
int j = 0;
next[0] = 0;
for(int i = 1; i < s.size(); i++) {
while (j > 0 && s[i] != s[j])//不满足时一直回退
j = next[j - 1];
if (s[i] == s[j])//满足时+1
j++;
next[i] = j;//更新next数组
}
}
四、栈和队列
栈和队列的默认底层容器是deque
。因为底层容器可插拔(可以控制使用哪种容器来实现,vector、list都可以),所以栈和队列是容器适配器
定义使用vector为底层容器的栈
stack<int, vector<int> > third;
双端队列deque
用front()返回第一个元素,back()返回尾元素。pop()和push()要加_front/_back。
优先队列priority_queue
没有front(),只能通过top()访问首元素。set的排列是默认从小到大,而priority_queue默认从大到小;
pair:
将两个数据(经常为不同数据类型)组合成一组数据。pair的实现是一个结构体,主要的两个成员变量是first、second。
插入pair<>
可以用q.push({i,j});
也可以用q.emplace(i,j);
,emplace可能略好一点,具体区别参考:《push 和 emplace区别》、《C++ 之 pair用法》
建立一个优先队列时候默认大顶堆
priority_queue<int> r;
如果要建立小顶堆:
priority_queue <int,vector<int>,greater<int> > p;
对于pair类型自定义cmp函数:
priority_queue<pair<int, int>, vector<pair<int, int>>, decltype(&cmp)> q(cmp);
辨析: 用pair的话,指向元素的第一个值用m.first;map中用指针iter->first表示。
结构 | pair | map |
---|---|---|
访问成员 | m.first | iter->first |
原理 | pair有两个成员,. 访问成员 | 箭头运算符-> 相当于解引用 + 成员访问所以it->mem和(*it).mem意思相同 |
五、二叉树
树 | 定义 |
---|---|
满二叉树 | 没有度为1的结点 |
完全二叉树 | 最下面一层集中在左边位置或填满 |
二叉搜索树(BST) | 左子树所有结点均小于根节点(或空),右均大(或空) |
平衡二叉搜索树(AVL) | 首先是一颗二叉搜索树,其次每个结点左右孩子高度差不超过1 |
递归遍历:
- 确定递归函数的参数和返回值
- 确定终止条件
- 确定单层递归的逻辑
dfs
前序:先空树判断,再放入root,
while(stack非空时)
{
取栈顶值;
放入右孩子;
放入左孩子;
}//结果顺序:中左右
后序:调整左右孩子入栈顺序,再反转结果数组->左右中
bfs
层序遍历:
while(queue非空时){
vec<int>temp;
for(queue.size()){
把queue单层元素加入temp
左右分别加入queue}
temp加入ans;
}return ans;
问题分类:
- 涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定
前序
,都是先构造中节点。 - 求普通二叉树的属性,一般是
后序
,一般要通过递归函数的返回值做计算。 - 求二叉搜索树的属性,一定是
中序
了,要不白瞎了有序性了。
哈希表、AVL和红黑树比较
如果要设计一个查找、插入、删除都很快的数据结构
哈希表(x ) | 二叉搜索树/红黑树(√ ) | 跳表 | |
---|---|---|---|
优点 | 插存删都是 O ( 1 ) O(1) O(1) | 插存删都是 O ( l o g n ) O(logn) O(logn),稳定且有序 | 插存删都是 O ( l o g n ) O(logn) O(logn),且删除简单,胜在手写简单(Redis中的有序集合基于跳表) |
缺点 | 1.无序存储,输出有序数据非常麻烦 2.因为有哈希冲突所以性能不稳定,另外虽然是 O ( 1 ) O(1) O(1),但最坏情况比 O ( l o g n ) O(logn) O(logn)更差 3.构造复杂,比如如何设计和解决冲突 | 红黑树手写实现比较复杂 | 性能不如红黑树 |
AVL和红黑树进一步比较
平衡二叉搜索树 | 红黑树 | |
---|---|---|
特点 | 严格保证平衡 | 只有深度差大于2时才平衡,保持相对平衡 |
查询复杂度 | O ( l o g n ) O(logn) O(logn),因为绝对平衡所以略高于红黑树 | O ( l o g n ) O(logn) O(logn) |
插入删除复杂度 | O ( l o g n ) O(logn) O(logn) | O ( l o g n ) O(logn) O(logn) |
插入失衡,复横复杂度 | 最多两次旋转, O ( 1 ) O(1) O(1) | 最多两次旋转, O ( 1 ) O(1) O(1) |
删除失衡,复横复杂度 | O ( l o g n ) O(logn) O(logn) | 最多三次旋转, O ( 1 ) O(1) O(1) |
优点 | 查询效率略高 | 除了查询略低,其余的效率都更高,且维护代价小;是一种折中 |
构造二叉树(根据层序遍历的数组构造)
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
//构建二叉树
TreeNode* construct(vector<int>vec) {
vector<TreeNode*>tree(vec.size(), nullptr);//初始化null指针
for (int i = 0; i < vec.size(); i++) {
TreeNode* tmp = nullptr;
if (vec[i] != -1)tmp = new TreeNode(vec[i]);
tree[i] = tmp;
}
for (int i = 0; 2 * i + 2 < vec.size(); i++) {
if (!tree[i])continue;
tree[i]->left = tree[2 * i + 1];
tree[i]->right = tree[2 * i + 2];
}
return tree[0];
}
// 层序打印打印二叉树
void print_binary_tree(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
while (!que.empty()) {
int size = que.size();
vector<int> vec;
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if (node != NULL) {
vec.push_back(node->val);
que.push(node->left);
que.push(node->right);
}
// 这里的处理逻辑是为了把null节点打印出来,用-1 表示null
else vec.push_back(-1);
}
result.push_back(vec);
}
for (int i = 0; i < result.size(); i++) {
for (int j = 0; j < result[i].size(); j++) {
cout << result[i][j] << " ";
}
cout << endl;
}
}
int main() {
// 注意本代码没有考虑输入异常数据的情况
// 用 -1 来表示null
vector<int> vec = { 4,1,6,0,2,5,7,-1,-1,-1,3,-1,-1,-1,8 };
cout << vec.size() << endl;
TreeNode* root = construct(vec);
print_binary_tree(root);
}
构造二叉树(根据无向边界构造)
来源: 9.4网易笔试100、100、89、第四题没写完
第一行输入一个正整数n,代表节点的数量(1-n)
接下来的n-1行,每行输入两个正整数u和v,代表点u和点v有一条边相连
3
1 2 3
1 2
1 3
struct TreeNode {
int val;
TreeNode* left;
TreeNode* right;
TreeNode() : val(0), left(nullptr), right(nullptr) {}
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
//============================
int n = 3;
vector<vector<int>> edge;
int temp1, temp2;
for (int i = 0; i < n - 1; i++) {
cin >> temp1 >> temp2;
edge.push_back({ temp1,temp2 });
}
//记得先把边排排序
//如果需要的话,对每个edge[i]先排个序也不是不行
sort(edge.begin(), edge.end(), [](vector<int>& lhs, vector<int>& rhs) {
if (lhs[0] == rhs[0]) {
return lhs[1] < rhs[1];
}
return lhs[0] < rhs[0];
});
//注释的含义:可以直接通过mp[i]找到第i个结点的数据结构
unordered_map<int, TreeNode*> mp;
TreeNode* root = new TreeNode(1);
mp[1] = root;
for (int i = 0; i < edge.size(); i++) {
TreeNode* cur = mp[edge[i][0]];
if (cur->left == nullptr) {
cur->left = new TreeNode(edge[i][1]);
mp[edge[i][1]] = cur->left;
}
else {
cur->right = new TreeNode(edge[i][1]);
mp[edge[i][1]] = cur->right;
}
}
六、回溯
回溯步骤:
- 回溯函数模板返回值以及参数
- 回溯函数终止条件
- 回溯函数遍历过程
回溯算法模板:
void backtracking (参数) {
if (终止条件) {
存放结果;
return;
}
for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
处理节点;
backtracking (路径,选择列表); // 递归
回溯,撤销处理结果
}
}
三种去重办法:
1. used数组去重
if (i > 0 && candidates[i] == candidates[i - 1] && used[i - 1] == false)continue;
2. 直接利用cur来去重
if (i > startIndex && candidates[i] == candidates[i - 1])continue;
3. set去重
对于递增子序列问题(顺序固定,不能排序)适用,递增子序列一定要set给同层去重,
例如输入1,2,3,4,1,1,1时[1,1]和[1,1,1]会重复出现,递增子序列问题不需排序。
为什么子集用set去重的时候则一定要排序呢?因为先排序决定好了各个子集的有序性,也就保证了唯一性,否则在输入1,2,3,4,1,1,1时会出现[1,2,4]和[2,4,1]这两种相同情况。
三种剪枝方法:
1. for循环中缩小范围,对不满足k个数的可能性排除
for (int i = startIndex; i <= n - (k - path.size()) + 1; i++)
2. 终止条件中,总和大于规定的总和后直接返回
if (sum > targetSum) { // 剪枝操作
return;
}
3.综合以上两项,如果已经知道下一层会大于sum,
干脆连下一层递归都不要进入,减少了进入递归并判断的时间
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++)
如果只需要找到一个结果而不是所有的,那么返回值用bool
而不是void,分别在函数定义、终止、递归和结尾更改。
在二叉树-路径总和中提到:
- 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就
不要
返回值 - 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就
需要
返回值 - 如果要搜索其中一条符合条件的路径,那么递归一定
需要
返回值,因为遇到符合条件的路径了就要及时返回,往往是bool
什么时候可以剪枝?
—— 非子集问题时,才可以剪枝,因为子集要遍历整棵树,其他的不用。
什么时候可以不用cur来控制起始位置?
—— 如果是一个集合来求组合的话,就需要cur(如组合问题);
—— 如果是多个集合取组合,各个集合之间相互不影响,那么就不用cur(如电话号码)。
什么时候可以不写终止条件?
—— 终止条件里只有return的时候可以不写,因为越界后自动访问空函数了。
七、贪心
如果找到局部最优,然后推出整体最优,那么就是贪心。
两个维度权衡问题: 正确选择遍历顺序
发糖果一题中,先从前到后,逢后面分数高的就多发一个,再从后往前,逢分数高的就多发一个,记得和自己本身数字作比较,取最大值。
根据身高重建队列中,先确定一个维度,再确定另一个维度。
先按身高排序,再按k来插入。
重叠区间问题:
最少数量的箭引爆气球: 初始化答案为1支(若不为空)。按照气球起始位置排序,重叠区间右边界最小值之前一定需要一个弓箭。若不重叠,res++;重叠则更新最小右边界。
无重叠区间: 按右边界排序,记录非交叉区间个数,再用总数减去非交叉区间个数。
优先选右边界最小的区间,把包含这一坐标的其他区间都去掉,再数下一个右边界最小的。
(注意相邻时怎么判断)也可以用弓箭题中总气球减去弓箭数量即可AC。
八、动态规划
动态规划五部曲
- 确定DP数组及其下标含义
- 确定递推公式
- 如何初始化
- 确定遍历顺序
- 举例推导
0-1背包问题:
一维数组遍历0-1背包的时候,外层物品,内层背包要用倒序
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
不可以先遍历背包,必须先遍历物品。
完全背包问题:
每个物品都有无限个
区别是内外层均从小到大
遍历,而且内外层循环可以互换
!
如果求组合数
就是外层for循环遍历物品,内层for遍历背包。
如果求排列数
就是外层for遍历背包,内层for循环遍历物品。
求排列组合类的转移公式:
dp[j] += dp[j - nums[i]]
多重背包问题:
很少。先把数量展开,再视为一个0-1背包问题即可
经典问题:
打家劫舍
买卖股票的最佳时机
九、图论
dfs
- 求给定的
起点
到给定的终点
的所有路径 / 最短路径(加权) - 如果是加权图,就用邻接矩阵表示边的关系,
edge[i][j] = len
bfs
- 求给定
起点
到给定终点
的最短路径(无权)
Floyd-Warshall
- 多源最短路径: 任意两个点之间的最短路径
Dijkstra
- 单源最短路径: 指定起点,到所有点的最短路径
Floyd | Dijkstra | Bellman-Ford | |
---|---|---|---|
时间复杂度 | O ( N 3 ) O(N3) O(N3) | O ( ( M + N ) l o g N ) O((M+N)logN) O((M+N)logN) | O ( N M ) O(NM) O(NM) |
适用情况 | 稠密图 | 稠密图 | 稀疏图 |
解决负权边 | 可以 | 不行 | 可以 |
判定是否存在负权回路 | 不行 | 不行 | 可以 |
最小生成树:求连接所有点的最小花费 (加权无向图)
Kruskal:按照权值从小到大排序,每次选 权值最小 && 不在同一集合里的边,直到n-1
为止
Prim:从任一顶点开始,先存储成树,再枚举每一个树顶点到每一个非树顶点所有的边,找到最短边并添加顶点,直到n个顶点为止
《LeetCode 1135. 最低成本联通所有城市(最小生成树+排序+并查集)》
《leetcode 1135. 最低成本联通所有城市(C++)》
专题:《最小生成树知识点题库》
九、单调栈
什么时候用单调栈?
——通常是一维数组,要寻找任一个元素的右边或者左边第一个比自己大或者小的元素的位置,此时我们就要想到可以用单调栈了。
就接雨水
举例,各方法复杂度
方法 | 复杂度 |
---|---|
暴力 | 时间:
O
(
n
2
)
O(n^2)
O(n2) 空间: O ( 1 ) O(1) O(1) 超时 |
单调栈 | 时间:
O
(
n
)
O(n)
O(n) 空间: O ( n ) O(n) O(n) |
动态规划 | 时间:
O
(
n
)
O(n)
O(n) 空间: O ( n ) O(n) O(n) |
双指针 | 时间:
O
(
n
)
O(n)
O(n) 空间: O ( 1 ) O(1) O(1) |
本质是空间换时间。
栈里存放的是下标,以单调递增栈为例(从栈顶到栈底递增):
栈为空 || 当前遍历元素A[i]小于或等于栈顶元素A[st.top()]
栈不为空 && 当前遍历元素A[i]大于栈顶元素A[st.top()]
十、排序
分类 | 简单算法 | 改进算法 |
---|---|---|
交换 | 冒泡 | 快速 |
插入 | 直接插入 | 希尔 |
选择 | 简单选择 | 堆 |
归并 | 归并 |
排序方法 | 平均 | 最好 | 最坏 | 空间 | 稳定 | 备注 |
---|---|---|---|---|---|---|
冒泡排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | 1 | 稳定 | |
简单选择排序 | O ( n 2 ) O(n^2) O(n2) | 同 | 同 | 1 | 稳定 | 略优于冒泡i |
直接插入排序 | O ( n 2 ) O(n^2) O(n2) | O ( n ) O(n) O(n) | O ( n 2 ) O(n^2) O(n2) | 1 | 稳定 | 略优于前二者 |
希尔排序 | O ( n l o g n ) O(nlogn) O(nlogn)~ O ( n 2 ) O(n^2) O(n2) | O ( n 1 . 3 ) O(n^1.3) O(n1.3) | O ( n 2 ) O(n^2) O(n2) | 1 | ||
堆排序 | O ( n l o g n ) O(nlogn) O(nlogn) | 同 | 同 | 1 | 不适用个数少 | |
归并排序 | O ( n l o g n ) O(nlogn) O(nlogn) | 同 | 同 | O ( n ) O(n) O(n) | 稳定 | |
快速排序 | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n l o g n ) O(nlogn) O(nlogn) | O ( n 2 ) O(n^2) O(n2) | O ( l o g n ) O(logn) O(logn)~ O ( n ) O(n) O(n) |
最好情况: 基本有序时,用简单算法即可
最坏情况: 堆 / 归并
个数少: 用简单算法即可;个数多: 用改进算法
十一、并查集
不考虑rank(加权标记法)的情况:
const int n = 1005; // 节点数量3 到 1000
int pre[1005];
// 并查集初始化
void init() {
for (int i = 0; i < n; ++i) {
pre[i] = i; //等于自身
}
}
// 并查集里寻根的过程
int find(int u) {
return u == pre[u] ? u : pre[u] = find(pre[u]);
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
int root_u = find(u);
int root_v = find(v);
if (root_u == root_v) return;
pre[root_v] = root_u;
}
// 判断 u 和 v是否找到同一个根
bool same(int u, int v) {
return find(u) == find(v); //一定得是根
}
考虑rank
(加权标记法)的情况:
只有1.初始化 和 2.连接的操作不同
const int n = 1005; // 节点数量3 到 1000
int pre[1005];
int rank[1005];
// 初始化
void init() {
for (int i = 0; i < n; ++i) {
pre[i] = i;
rank[i] = 1;//深度为1
}
}
// 将v->u 这条边加入并查集
void join(int u, int v) {
int root_u = find(u);
int root_v = find(v);
if (root_u == root_v) return;
if(rank[root_x] > rank[root_y])pre[y] = x; //x更深
else{ //相等或y深时都插入y
if(rank[root_x] == rank[root_y])rank[root_y]++; //只有相等的时候才需要把更深的++
pre[x] = y; //相等或y深时都执行的操作
}
}
// 寻根(同)
//int find(int u) {
// return u == pre[u] ? u : pre[u] = find(pre[u]);
//}
// 判断同根(同)
//bool same(int u, int v) {
// return find(u) == find(v); //一定得是根
//}
十二、前缀和
前缀和简单题:《寻找数组的中心下标》
此外,前缀和
通常和哈希表
进行搭配,哈希表中别忘了哨兵:求个数就存个数,哨兵存(0,1) 求长度就存下标,哨兵存(0,-1)
求长度
求个数,所以存长度,哨兵mp[0] = -1
,找到了就更新/判断长度,没找到就存储下标
《连续的子数组和》:是否存在1.至少两个元素 2.总和为k的倍数 的子数组?
因为要判断长度是否大于等于2,所以存下标
转化条件:
子数组总和为k的倍数 (S[j]-S[i-1] == n*k) -----> S[j]%k == S[i-1]%k 即模相同
存储的是 <S[i]%k , i>
《连续数组》:求和为0的最长长度
转化条件:
子数组总和为0 (S[j]-S[i-1] == 0) -----> S[j] == S[i-1]
存储的是 <S[i] , i>
求个数
求个数,所以存个数,哨兵mp[0] = 1
;找到了就对该key的value添加至count,没找到就value++
《和为K的子数组》:求和为k的子数组的个数
转化条件:
子数组总和为k (S[j]-S[i-1] == k) -----> S[j] - k == S[i-1]
存储的是 <S[i] , count> , 找的时候用 find(S[j] - k),存的时候用S[j]
转化条件:
子数组总和为k的倍数 (S[j]-S[i-1] == n*k) -----> S[j]%k == S[i-1]%k 即模相同
因为这次用例有负数,所以存储的是 <(S[i]%k+k)%k , count> ,找的时候用 find((S[i]%k+k)%k),存的时候也一样
十三、容器接口
函数 | 容器 | 其他 |
---|---|---|
push | stack &queue 栈和队列priority_queue 优先队列 | 栈、优先队列只有top() 队列有 front() 和back() |
push_front/back | vector 数组deque 双端队列 | front() ,back() |
insert | set 集合、map 映射list 链表 |
十四、模板
输入练习:获取1,2,3,4,5,6
中的数字
string s = "1.2.0123.3";//注意:这里开头的0不被记录
vector<int>v;//提取数字的数组
string tmp;
for (auto i : s) {
if (i >= '0' && i <= '9')tmp += i;
else if (i == '.') {//这里是分隔的符号
v.push_back(stoi(tmp));
tmp.clear();
}
}
v.push_back(stoi(tmp));
tmp.clear();
string s = "1,2,3,4,5";
vector<int>nums;
int p1 = 0, p2 = 0;
for (; p2 < s.size(); p2++)
if (s[p2] == ',') {
nums.push_back(stoi(s.substr(p1, p2 - p1)));
p1 = p2 + 1;
}
else if (p2 == s.size() - 1) {
nums.push_back(stoi(s.substr(p1, p2 - p1 + 1)));
}
题干直接输入数组
int n; cin>>n;
vector<int> vec(n, 0);
for (int i = 0; i < n; i++) {
cin >> vec[i];
}
进制转换
十进制转二进制:
string s;
while (n) {
s += to_string(n % 2);
n /= 2;
}
reverse(s.begin(), s.end());
二进制转十进制:
```cpp
string n; //二进制字符串
int sum = 0;//最终转化的十进制数
for (int i = 0; i < n.size(); i++){
if (n[i] == '1'){
int j = pow(2, n.size() - i - 1);
sum += j;
}
}
十五、技巧
各算法使用情况
- 动态规划: 求方案数、
常见越界原因:
1.链表中指针如果一次前进两格,while(p->next && p->next->next)
2.for循环时只要里面有对[i-1]
或[i+1]
的操作,就要判断i>0
或i<size-1
3.栈与队列返回top
或front/back
时之前一定要判断非空
求a
除以b
的结果,要求结果向上取整:
int ans = (a+b-1)/b;
对float变量a的百分位进行四舍五入
《四舍五入》
a = (int)(a*100+0.5)/(double)100;
取模不为负
int mod = (i % k + k) % k
访问tuple成员
《tuple容器》
tuple<size_t, size_t, size_t> iteam
auto book = get<0>(item); //返回item第一个成员
auto cnt = get<1>(item); //返回item第二个成员
二维数组转一维数组
for (int i = 0; i < row; i++) {
for (int j = 0; j < col-1; j++) {
cout << m[i][j];
}
}
for (int i = 0; i < row; i++) {
for (int j = 0; j < col; j++) {
cout << n[i * col + j];
}
}
获取[0,1,…n]数列里的中位数
int mid;
如果要求 n为偶数时mid正好指向一半的数量 || n为奇数时指向中间数以左(不包括中间数):
mid = (n/2)-1;
如果要求 n为偶数时mid正好指向一半的数量 || n为奇数时指向中间数:
mid = (n-1)/2;