推荐看👉 OI Wiki
算法部分
位图(bitmap)
通过一个bit位来表示某个元素对应的值或者状态,其中的key就是对应元素本身,value对应0或1,我们知道8个bit可以组成一个Byte,所以bitmap本身会极大的节省储存空间。
字典(map)
-
字典有什么特点呢?
- 字典的主要特点是一一对应的关系.
- 比如保存一个人的信息, 在合适的情况下取出这些信息.
- 使用数组的方式: [18, “Coderwhy”, 1.88]. 可以通过下标值取出信息.
- 使用字典的方式: {“age” : 18, “name” : “Coderwhy”, “height”: 1.88}. 可以通过key取出value
kmp算法
kmp算法也就是字符串匹配算法。
比如:
在string str = “abcababc
ba"中匹配
string str2 = “ababc
“字符串
最长公共前后缀:要匹配的字符串生成的数组
从一个字符开始到整个字符串,默认每行的公共前后缀最大值为全部字母数,然后判断是否符合其数字,如果不符合减一,一直判断到0结束。一行元素为1的数字为0,最后一行不统计
0 a
0 a b
1 a b a
2 a b a b
0 a b a b c
得出的公共前后缀为
0 | 0 | 1 | 2 | 0 |
然后整体向后移动一位,原最后一位被覆盖,下标为0的位置设为-1,最后的公共前后缀为
-1 |
0 | 0 | 1 | 2 |
kmp匹配过程
abcababc
ba
ababc
如果匹配到不相等元素,则通过next数组(P)决定next数组移动的位置,例如上图next数组(P)下标为2的元素与T字符串不相等,则看一下P数组下标2的next数组的值为0,则把next数组下标为0的位置移动到不匹配的地方
如果next数组的值为-1则next数组整体右移,或找到T字符串与子字符串第一个元素(如果找到的话),然后把P移动到此位置(T的剩余的元素大于等于子字符串元素个数);
代码
#include <cstring>
#include <iostream>
#include <string>
// 计算最长公共前后缀
void prefix_table(char pattern[], int prefix[], int n) {
prefix[0] = 0;
int len = 0;
int i = 1;
while (i < n) {
if (pattern[i] == pattern[len]) {
len++;
prefix[i] = len;
i++;
} else {
if (len > 0) {
len = prefix[len - 1];
} else {
prefix[i] = len;
i++;
}
}
}
}
// 计算next数组
void move_prefix_table(int table[], int n) {
for (int i = n - 1; i > 0; --i) {
table[i] = table[i - 1];
}
table[0] = -1;
}
// kmp算法
void kmp_search(char text[], char pattern[]) {
int n = strlen(pattern);
int m = strlen(text);
int *prefix = new int[n];
prefix_table(pattern, prefix, n);
move_prefix_table(prefix, n);
int i = 0;
int j = 0;
while (i < m) {
if (j == n - 1 && text[i] == pattern[j]) {
std::cout << "Found pattern at " << i - j << '\n';
j = prefix[j];
}
if (text[i] == pattern[j]) {
i++;
j++;
} else {
j = prefix[j];
if (j == -1) {
i++;
j++;
}
}
}
delete[] prefix;
}
int main(void) {
char pattern[] = "ABABCABAA";
char text[] = "ABABABCABAABABABAAB";
kmp_search(text, pattern);
return 0;
}
双指针
同向双指针
判断一个链表是否有环
创建两个指针ptr1,ptr2
,两个指针同时指向链表头结点。ptr1每次向后移动一个结点,ptr2每次移动2个结点,如果链表有环他们会指向同一个结点
class Solution {
public:
/**
* @param circles: The value of 6 points on n rings
* @return: Whether there are two same circles
*/
bool samecircle(Node head) {
// write your code here
Node ptr2 = head;
Node ptr2 = head;
while (p1 != nullptr && p2 != nullptr)
{
ptr1 = p1.next;
ptr2 = p2.next.next;
if (ptr1 == ptr2) // 结点相遇
return true;
}
return false;
}
};
判断环长
第一次相遇代表有环,第二次相遇代表两个指针发生了套环,所以:
环长 = 速度差 * 移动次数
判断入环点
慢指针ptr1每次走一步,所以走的距离是:
d1 = D + X * (S1 + S2) + S1
快指针ptr2走的距离是:
d2 = D + N * (S1 + S2) + S1
快指针速度是慢指针2倍。 即:
2 * d1 = d2
整理后的公式为
D = (n - 2X - 1)(S1 + S2) + S2
假设n - 2X -1
的值为0,则D = S2
;,那么我们就可以在首次相遇点的时候,定义一个指针指向链表的起点,一个指针指向首次相遇点,然后两个指针每次前进1步,当两个指针相遇的时候就是链表的入环点。
例子:
class Solution {
public:
/**
* @param circles: The value of 6 points on n rings
* @return: Whether there are two same circles
*/
Node find_node(Node head) {
// write your code here
Node ptr1 = head;
Node ptr2 = head;
Node ptr3 = nullptr;
Node ptr4 = nullptr;
while (p1 != nullptr && p2 != nullptr) {
ptr1 = p1.next;
ptr2 = p2.next.next;
if (ptr1 == ptr2) // 结点相遇
ptr3 = head;
ptr4 = ptr1;
break;
}
if (ptr3 != nullptr && != ptr4 != nullptr) {
while(ptr3 != ptr4) {
ptr3 = ptr3.next;
ptr4 = ptr4.next;
}
return ptr4;
}
return nullptr;
}
};
相向双指针
二分法
- 又称折半搜索,期望时间复杂度为O(log2n),最差为O(log2(n + 1))
二分法前提是数据已经有序
在A[0] … A[n]中搜索K。
步骤: \1. 令low = 0, high = n - 1,初始的查找区域为[low, high]. \2. 取low和high的中间值mid = (low+high)/2。 \3. 如果A[mid] = K,则返回mid, 如果不等,则重新确定查找区间。 \4. 当low > high 时,则表示区间已经失效,如果还未找到,则表示数组中不包含K的值,返回-1。
template<class T>
int binary_search(vector<T> &A, T K) {
int low = 0;
int high = A.size() - 1;
while( low < high ) {
int mid = (low + high)/2;
if( A[mid] < K )
low = mid + 1;
else if( A[mid] > k )
high = mid - 1;
else
return mid;
}
return -1; /*返回-1表示数组不存在K的值*/
}
二分答案
二分答案
与二分查找
其实是不一样的
二分答案
: 即对你要求的答案进行二分二分查找
: 对一个已知的有序数据集上进行二分的查找
基础算法·二分答案 - Potassium - 博客园 (cnblogs.com)
分治法
分治分治,即分而治之。分治,就是把一个复杂的问题分成两个或更多的相同或相似的子问题,再把子问题分成更小的子问题……直到最后子问题可以简单的直接求解,原问题的解即子问题的解的合并。这个技巧是很多高效算法的基础,如排序算法(快速排序,归并排序),傅立叶变换(快速傅立叶变换)……直接说就是将一个难以直接解决的大问题,分割成一些规模比较小的相同的小问题,以便各个击破,分而治之。
分治法所能解决的问题一般具有以下几个特征:
\1) 该问题的规模缩小到一定的程度就可以容易地解决
\2) 该问题可以分解为若干个规模较小的相同问题,即该问题具有最优子结构性质。
\3) 利用该问题分解出的子问题的解可以合并为该问题的解;
\4) 该问题所分解出的各个子问题是相互独立的,即子问题之间不包含公共的子子问题。
第一条: 特征是绝大多数问题都可以满足的,因为问题的计算复杂性一般是随着问题规模的增加而增加;
第二条: 特征是应用分治法的前提它也是大多数问题可以满足的,此特征反映了递归思想的应用;、
第三条: 特征是关键,能否利用分治法完全取决于问题是否具有第三条特征,如果具备了第一条和第二条特征,而不具备第三条特征,则可以考虑用贪心法或动态规划法。
第四条: 特征涉及到分治法的效率,如果各子问题是不独立的则分治法要做许多不必要的工作,重复地解公共的子问题,此时虽然可用分治法,但一般用动态规划法较好。
经典问题:
二分查找
棋盘覆盖
汉诺塔问题
归并排序/合并排序
快速排序
宽度优先搜索(BFS)
它并不考虑结果的可能位置,彻底地搜索整张图,直到找到结果为止。
基本过程,BFS 是从根节点开始,沿着树(图)的宽度遍历树(图)的节点。如果所有节点均被访问,则算法中止。一般用队列数据结构来辅助实现 BFS 算法。
广度优先搜索原理与实践 - huansky - 博客园 (cnblogs.com)
private Map<String, Boolean> status = new HashMap<String, Boolean>();
private Queue<String> queue = new LinkedList<String>();
public void BFSSearch(String startPoint) {
//1.把起始点放入queue;
queue.add(startPoint);
status.put(startPoint, false);
bfsLoop();
}
private void bfsLoop() {
while(!queue.isEmpty()) {
// 1) 从queue中取出队列头的点;更新状态为已经遍历。
String currentQueueHeader = queue.poll(); //出队
status.put(currentQueueHeader, true);
System.out.println(currentQueueHeader);
// 2) 找出与此点邻接的且尚未遍历的点,进行标记,然后全部放入queue中。
List<String> neighborPoints = graph.get(currentQueueHeader);
for (String poinit : neighborPoints) {
if (!status.getOrDefault(poinit, false)) { //未被遍历
if (queue.contains(poinit)) continue;
queue.add(poinit);
status.put(poinit, false);
}
}
}
}
拓扑排序法
- 拓扑排序指的是将有向无环图(又称“DAG”图)中的顶点按照图中指定的先后顺序进行排序。
算法:拓扑排序 - 子烁爱学习 - 博客园 (cnblogs.com)
深度优先搜索/回溯法 (DFS)
深度优先搜索(缩写DFS)有点类似广度优先搜索,也是对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,如果发现不能到达目标解,那就返回到上一个节点,然后从另一条路开始走到底,这种尽量往深处走的概念即是深度优先的概念。
/**
* DFS核心伪代码
* 前置条件是visit数组全部设置成false
* @param n 当前开始搜索的节点
* @param d 当前到达的深度,也即是路径长度
* @return 是否有解
*/
bool DFS(Node n, int d){
if (isEnd(n, d)){//路径长度为返回true,表示此次搜索有解
return true;
}
for (Node nextNode in n){//遍历跟节点n相邻的节点nextNode,
if (!visit[nextNode]){//未访问过的节点才能继续搜索
//例如搜索到V1了,那么V1要设置成已访问
visit[nextNode] = true;
//接下来要从V1开始继续访问了,路径长度当然要加
if (DFS(nextNode, d+1)){//如果搜索出有解
//例如到了V6,找到解了,你必须一层一层递归的告诉上层已经找到解
return true;
}
//重新设置成未访问,因为它有可能出现在下一次搜索的别的路径中
visit[nextNode] = false;
}
//到这里,发现本次搜索还没找到解,那就要从当前节点的下一个节点开始搜索。
}
return false;//本次搜索无解
}
动态规划
它将问题分成小问题,并先着手解决这些小问题
背包型DP
假设有三种商品,分别是小汽车1吨,卡车3吨,起重机4吨.价格分别是:3000,4000,6000。
现在只能卖总共4吨的商品,怎么卖商品售价最高?
所有的组合为:
组合 | 价值 |
---|---|
无 | 0 |
小汽车 | 3000 |
卡车 | 4000 |
起重机 | 6000 |
小汽车和起重机 | 重量过大 |
小汽车和卡车 | 7000 |
卡车和起重机 | 重量过大 |
小汽车,卡车,起重机 | 重量过大 |
可以看到随着商品种类增加组合也在飞速增长,时间复杂度为O(2^n),太慢了
动态规划算法可以用网格描述
填充这些表格最后就是背包问题的解法。
当在小汽车一行时,其他种类的商品选不了,所以都是3000
当加入卡车时1-2吨放不下3吨的卡车,3吨可以放下卡车,所以为4000,4吨可以放下一个卡车和一个小汽车,共7000
加入起重机时,1-2背包只能装小汽车,3吨可以装一辆卡车,4吨可以装起重机,但是价格低于7000所以不选起重机的价格,定位卡车加小汽车,价格为7000.
坐标型DP
又被称为网格型动态规划
一个网格有m行n列,一个小动物从(0, 0)出发,每一步可以向下或向右走一步,最总到达(m - 1, n - 1)处
- 最简单的动态规划类型
- 给定一个序列或者网格
- 需要找到序列中某个/些子序列或网格中的某条路径
- 某种性质最大/最小
- 计数
- 存在性
- 动态规划方程 f[i] 中的下标i表示以ai为结尾的满足条件的子序列的性质,f[i][j] 中下标 i , j 表示以格子( i , j )为结尾的满足条件的路径的性质
- 最大值/最小值
- 个数
- 存在性
- 坐标型动态规划的初始条件f[0]就是指以a0为结尾的子序列的性质
class Solution {
public int uniquePathsWithObstacles(int[][] obstacleGrid) {
if(obstacleGrid==null||obstacleGrid.length==0){
return 0;
}
int [] [] dp = new int[obstacleGrid.length][obstacleGrid[0].length];
for(int i=0;i<obstacleGrid.length;i++){
for(int j=0;j<obstacleGrid[i].length;j++){
if(obstacleGrid[i][j]==1){//有障碍
dp[i][j]=0;
continue;
}else{
if(i == 0 && j == 0) dp[i][j]=1;//没有障碍起点
else if(i==0) dp[i][j]=dp[i][j-1];//在第一行,上一题在没有障碍情况下默认是1,但是有障碍就取决于该行前面是否有障碍了
else if(j==0) dp[i][j]=dp[i-1][j];//第一列,没有障碍默认是1,有障碍就取决于该列前面是否有障碍了
else{
dp[i][j]=dp[i-1][j]+dp[i][j-1];
}
}
}
}
return dp[obstacleGrid.length-1][obstacleGrid[0].length-1];
}
}
序列型DP
双序列型DP
划分型DP
常见类型:
1. 给定长度为N的序列或字符串,要求划分成若干段
- 段数不限,或指定K段
- 每一段满足一定的性质
2. 做法
- 类似于序列型动态规划,但是通常要加上段数信息
- 一般用`f[i][j]`记录前i个元素(元素0~i-1)分成 j 段的性质,如最小代价
记忆化搜索
区间型DP
状态压缩DP
博弈型DP
匹配型DP
数位 DP
树形DP
插头 DP
概率 DP
动态 DP
排序算法
外排序算法
排序之外部排序 - Judy518 - 博客园 (cnblogs.com)
快速排序算法
欧拉路径
什么是欧拉路径?欧拉路径就是一条能够不重不漏地经过图上的每一条边的路径,即小学奥数中的一笔画问题。而若这条路径的起点和终点相同,则将这条路径称为欧拉回路。
欧拉路径详解 - TEoS - 博客园 (cnblogs.com)
模拟法
模拟算法(C++)_1只小弛的博客-CSDN博客_c++模拟
C++算法:模拟 - 无咕 - 博客园 (cnblogs.com)
扫描线算法
扫描线是一种用于求矩阵面积并或者周长并的算法,可以使用
线段树
来优化。假设给定了平面上若干个可能相交的矩阵,需要求出它们的面积并(面积之和减去相交部分)或者周长并(外轮廓的长度)。我们可以虚拟出一条按顺序扫描整个平面的线段,通过对平行或垂直于 [Math Processing Error]x 轴的线段进行处理得到答案。
扫描线 - Ling_Lover - 博客园 (cnblogs.com)
枚举法
枚举法 - Huise.J - 博客园 (cnblogs.com)
最短路径
看完就懂了!一篇搞定图论最短路径问题 - thousfeet - 博客园 (cnblogs.com)
贪心法
贪心算法原理及其应用 - vcjmhg - 博客园 (cnblogs.com)
最小生成树
最小生成树 - SeanOcean - 博客园 (cnblogs.com)
狄克斯特拉算法
Dijkstra算法(一)之 C语言详解 - 如果天空不死 - 博客园 (cnblogs.com)
近似算法
算法课堂笔记6—近似算法 - f91og - 博客园 (cnblogs.com)
高精度计算
LRU算法
缓存淘汰算法
长期不被使用的数据,在未来被用到的几率也不大。如果缓存到达了预设值就要删除一些内容,给新的内容腾位置
如何实现LRU算法? - murphy_gb - 博客园 (cnblogs.com)
A星寻路算法
A星寻路算法 - szmtjs10 - 博客园 (cnblogs.com)