常用算法汇总
回想起大学学过的算法,可谓数不尽数。但是有些算法总是让我又爱又恨。比如KMP、再比如Dijkstra最短路径,这些算法其实都很精彩,奈何我一学就忘 ,每次忘记了都要花费大量的时间去看教程,比如知乎上面的各种算法。一看就是半天,所以我打算开一个帖子博文,专门记录那些「稍微有些复杂」、「不好理解容易忘记」、「经常会考到」的算法吧!
KMP算法
SE高级数据结构里面讲到的,核心在于j = next[j]
1)KMP核心思想
1 2 3 4 index 0 1 2 3 4 5 6 目标串(ss) a b a b a b c | | | | 模式串(mm) a b a b c
如上图,匹配两个字符串abababc
和ababc
。定义:我们要在这个目标串(ss)里面找我们想要的模式串(mm),建议记一下缩写(目标串ss,模式串mm),
先比较发现index 0-3的字母都一样,都是abab
。index为4的时候就不一样了,这时候怎么办?我悄悄的把模式串往后移动2位 ,可以发现,除开被移走的部分,已经匹配的区域还是一样匹配。
1 2 3 4 index 0 1 2 3 4 5 6 目标串(ss) a b a b a b c | | 模式串(mm) a b a b c
进而我们发现后面的4-6的也一样,匹配成功。KMP的核心思想就是:当发现不匹配的时候,向后挪动「模式串」若干位 ,然后继续匹配,跑完为止。问题:
两问题本质一致,原因很简单,对于模式串的mm[0]-mm[3]
,也就是abab,从mm[0]往后数2两位,与mm[3]往前数两位一样,这就是为什么挪动两位 ,挪了还一样原因
1 2 3 4 5 6 模式串(mm) a b a b c / / / / / / / / 模式串(mm) a b a b c
补充:有人说那我从mm[0]往后数4两位,与mm[3]往前数4位也一样,我这里规定:如果出现这种情况,以最小 的为准!所以还是以2为准。我们记next[4] = 2。含义:
4:mm串从0-3的字符串,记为str
2:str从头部往后数2个的字符串str1,到str后部往前数2个str2,str1==str2。
上面所述,如果比2小,比0大,不可能出现str1==str2
核心思想点到为止,再纠结自己拿笔画图、看其他资料。我只觉得如果死纠结只会让自己一天都沉迷在KMP怎么写的原理里面。(所以劝大家不要纠结)
2)KMP算法
要写对KMP,只需要知道几个关键东西
首先要写对buildNext数组的过程
然后要写对匹配的原理
这里顺带提一嘴buildNext
函数,记住两个:
目标串不带通配符,模式串带-1作为index的通配符
i
永远对应目标串,j
永远对应模式串
优先判断:j < 0
代表通配
错配时:j = next[j]
初始化:next[i] = j
匹配:先ij
自增,再考虑next[i] = j
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 vector<int > buildNext (string mm) { int i = 0 ; int j = -1 ; int mmSize = mm.size (); vector<int > next = vector (mm.size (), 0 ); next[i] = j; while (i + 1 < mmSize){ if (j < 0 || mm[i] == mm[j]){ i++; j++; next[i] = j; } else { j = next[j]; } } return next; }
再来说KMP的算法
初始化时i = j = 0
错配j = next[j]
最终,如果j没有跑满,那就是不匹配,如果匹配,下标为i-j
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int KMP (string ss, string mm) { int i = 0 ; int j = 0 ; int ssSize = ss.size (); int mmSize = mm.size (); vector<int > next = buildNext (mm); while (i < ssSize && j < mmSize){ if (j < 0 || ss[i] == mm[j]){ i++;j++; } else { j = next[j]; } } int index = i - j; if (j < (int )mm.size ()) return -1 ; return i - j; }
Dijsktra算法
Dijsktra是一个最短路径的算法。有两个版本的实现!我这里给出两个模版,都返回一个数组,代表从src
出发,到所有节点的最短距离。
1)版本一
版本一比较好理解,推荐记忆这个!
我这里只用最简单的语言描述这个算法,具体证明感兴趣者研究。算法步骤:
g r a p h [ i ] [ j ] graph[i][j] g r a p h [ i ] [ j ] 表示从 i → j i \rightarrow j i → j 的距离,地图数据,s r c src src 代表出发的节点编号
维护一个dist
数组,记录src
出发到各个节点的距离,初始化inf
表示无穷远
维护一个used
数组,记录是否曾经到过这里,初始化false
,全部没有走过
从出发点到出发点距离显然是 0 0 0 ,所以dist[src] = 0
遍历n n n 次(n n n 是节点的数量):
每次在dist
数组里面找到dist
数组里面,之前没走过的 ,且值最小的下标 x x x :即当前距离src
最小的节点是 x x x
找到后标记u s e d [ x ] = t r u e used[x] = true u se d [ x ] = t r u e ,这个节点已经走过啦!
考虑从 x x x 节点出发可以到达的地方,用 y y y 遍历并更新 d i s t [ ] dist[] d i s t [ ] 数组,d i s t [ y ] = m i n ( d i s t [ y ] , d i s t [ x ] + g r a p h [ x ] [ y ] ) dist[y] = min(dist[y], dist[x] + graph[x][y]) d i s t [ y ] = min ( d i s t [ y ] , d i s t [ x ] + g r a p h [ x ] [ y ])
(解释:从 s r c → y src \rightarrow y src → y 的最短路径,如果发现路过 s r c → x → y src \rightarrow x \rightarrow y src → x → y 更短,就更新)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 vector<int > dijkstra_v1 (vector<vector<int >>& graph, int src) { const int inf = INT_MAX / 2 ; const int n = graph.size (); vector<int > dist (n, inf) ; dist[src] = 0 ; vector<bool > used (n, false ) ; for (int i = 0 ; i < n; ++i) { int x = -1 ; for (int y = 0 ; y < n; ++y) { if (!used[y] && (x == -1 || dist[y] < dist[x])) { x = y; } } used[x] = true ; for (int y = 0 ; y < n; ++y) { dist[y] = min (dist[y], dist[x] + graph[x][y]); } } return dist; }
2)版本二
第二个版本是用的优先级队列实现的。因为前面我每次都要遍历一次数组,才能找到数组 d i s t [ x ] dist[x] d i s t [ x ] 中值最小的:即之前没走过的 ,且 d i s t [ x ] dist[x] d i s t [ x ] 值最小的下标 x x x 。
所以我可以用一个pair<int, int>
的优先级队列存储,pair的第一个元素是节点 i i i 的 d i s t dist d i s t 距离,第二个元素是节点的下标 i i i 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 vector<int > dijkstra_v2 (vector<vector<int >>& graph, int src) { const int inf = INT_MAX / 2 ; const int n = graph.size (); vector<int > dist (n, inf) ; dist[src] = 0 ; priority_queue<pair<int , int >, vector<pair<int , int >>, greater<>> q; q.push ({0 , src}); while (!q.empty ()) { auto p = q.top (); q.pop (); int distance = p.first; int curNodeID = p.second; if (dist[curNodeID] < distance){ continue ; } for (int next = 0 ; next < graph[curNodeID].size (); next++){ if (graph[curNodeID][next] == inf) continue ; int newDistance = dist[curNodeID] + graph[curNodeID][next]; if (newDistance < dist[next]){ dist[next] = newDistance; q.push ({newDistance, next}); } } } return dist; }
Floyd算法
Floyd算法主要用来解决复杂度为 O ( n 3 ) O(n3) O ( n 3 ) 的「多源最短路径」问题
1)写法
函数如下,简称三for循环算法,但是需要注意的是三个for的顺序 。依次枚举中转点 ->枚举起点 ->枚举终点 ->松弛操作。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 vector<vector<int >> floyd (vector<vector<int >>& graph) { int num = graph.size (); const int inf = INT_MAX / 2 ; vector<vector<int >> dist = graph; for (int p = 0 ; p < num; p++) { for (int i = 0 ; i < num; i++) { for (int j = 0 ; j < num; j++) { dist[i][j] = min (dist[i][j], dist[i][p] + dist[p][j]); } } } return dist; }
2)原理
Floyd算法本质上是DP,即对于每个(可能的)新增的节点p p p ,来更新(可能的)节点 i i i 到 j j j 的最短距离。为什么新增节点 p p p 的循环要放在最外层呢?这是由其算法本身所决定的,其每一步求出任意一对顶点之间仅通过中间节点1 , 2 , … , k 1,2,…,k 1 , 2 , … , k 的最短距离,当1 , 2 , … , k 1,2,…,k 1 , 2 , … , k 扩展到1 , 2 , … , k , k + 1 1,2,…,k,k+1 1 , 2 , … , k , k + 1 时候,我们需要计算路过 k + 1 k+1 k + 1 的最短距离,以此类推。
假设有下图:
graph LR;
1((节点1))-->|1|4((节点4));
4-->|3|2((节点2));
4-->|1|3((节点3));
3-->|1|2((节点2));
对应的数组:
1 map [1 ][4 ] = 1 ; map[4 ][2 ] = 3 ; map[4 ][3 ] = 1 ; map[3 ][2 ] = 1 ;
如果我们按 p p p 循环在最内层来计算,那么1,2节点的最短路径为min(map[1][2], map[1][3]+map[3][2],map[1][4]+map[4][2]) = 4;
,此后是不会更新的 1 → 2 1 \rightarrow 2 1 → 2 的最短路径
实际上min(map[1][2]) = map[1][4] + map[4][3] + map[3][2] = 3;