1.KMP算法
问题描述:给定一个主串(以 S 代替)和模式串(以 P 代替),要求找出 P 在 S 中出现的位置,此即串的模式匹配问题。java中的String的indexOf方法可以直接返回位置,但是它的方法基本是基于KMP算法的。
如果用传统的暴力循环,时间效率肯定非常慢,所以我们可以使用KMP算法
算法流程:
(1)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G0aBuPXp-1630226701641)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758155634.png)]
首先,主串"BBC ABCDAB ABCDABCDABDE"的第一个字符与模式串"ABCDABD"的第一个字符,进行比较。因为 B 与 A 不匹配,所以模式串后移一位。
(2)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WAjksCZG-1630226701644)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758195521.png)]
因为 B 与 A 又不匹配,模式串再往后移。
(3)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xkTFlQ5t-1630226701645)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758329150.png)]
就这样,直到主串有一个字符,与模式串的第一个字符相同为止,如上图,然后慢慢匹配,发现了有一个位置D匹配了一个空,则没有匹配上。
(4)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xv6XmuF1-1630226701647)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758421873.png)]
一个基本事实是,当空格与 D 不匹配时,你其实是已经知道前面六个字符是"ABCDAB"。KMP 算法的想法是,设法利用这个已知信息,不要把"搜索位置"移回已经比较过的位置,而是继续把它向后移,这样就提高了效率。
怎么做到这一点呢?可以针对模式串,设置一个跳转数组int next[]
,这个数组是怎么计算出来的,后面再介绍,这里只要会用就可以了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZBn9dV6G-1630226701648)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758516245.png)]
已知空格与 D 不匹配时,前面六个字符"ABCDAB"是匹配的。根据跳转数组可知,不匹配处 D 的 next 值为 2,因此接下来从模式串下标为 2 的位置开始匹配。 然后发现还是不匹配,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KxqP9tMy-1630226701649)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758575443.png)]
由于C 处的 next 值为 0,因此接下来模式串从下标为 0 处开始匹配,然后发现还是不匹配,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LYRFJvCZ-1630226701650)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758614123.png)]
由于A的next 值为 - 1,表示模式串的第一个字符就不匹配,那么把主串的i位置直接往后移一位,然后挨个挨个比较,如下图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-taurVBqp-1630226701651)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758773295.png)]
又发现 C 与 D 不匹配。于是,下一步从D的next[7]=2,下标为 2 的地方,也就是子串为C的位置开始,然后发现C与主串的C一样了,然后两个指针同时移动,子串的指针走到末尾了,说明匹配成功。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40wqcuGe-1630226701651)(KMP、马拉车、滑动窗口、图、并查集.assets/1618758971210.png)]
那么 next 数组是如何求出的?next数组含义:最长前缀和最长后缀的最大匹配长度
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MuMEWaKR-1630226701652)(KMP、马拉车、滑动窗口、图、并查集.assets/1618759264532.png)]
- i = 0,对于模式串的首字符,我们统一为
next[0] = -1
; - i = 1,前面的字符串为
A
,其最长相同前后缀长度为 0,即next[1] = 0
; - i = 2,前面的字符串为
AB
,其最长相同前后缀长度为 0,即next[2] = 0
; - i = 3,前面的字符串为
ABC
,其最长相同前后缀长度为 0,即next[3] = 0
; - i = 4,前面的字符串为
ABCD
,其最长相同前后缀长度为 0,即next[4] = 0
; - i = 5,前面的字符串为
ABCDA
,其最长相同前后缀为A
,即next[5] = 1
; - i = 6,前面的字符串为
ABCDAB
,其最长相同前后缀为AB
,即next[6] = 2
; - i = 7,前面的字符串为
ABCDABD
,其最长相同前后缀长度为 0,即next[7] = 0
。
那么,为什么根据最长相同前后缀的长度就可以实现在不匹配情况下的跳转呢?举个代表性的例子:假如i = 6
时不匹配,此时我们是知道其位置前的字符串为ABCDAB
,仔细观察这个字符串,首尾都有一个AB
,既然在i = 6
处的 D 不匹配,我们为何不直接把i = 2
处的 C 拿过来继续比较呢,因为都有一个AB
啊,而这个AB
就是ABCDAB
的最长相同前后缀,其长度 2 正好是跳转的下标位置。求next数组如下代码:
//next数组含义:最长前缀和最长后缀的匹配长度
public static int[] getNextArray(char[] ms) {
if (ms.length == 1) return new int[] {
-1 };
int[] next = new int[ms.length];
next[0] = -1; //规定 0位置 -1
next[1] = 0;//规定 1位置 0
int i = 2;
int cn = 0; //拿哪个位置的字符和i-1比,也是当前使用的信息
while (i < next.length) {
//如果i-1之前的那个字符 == 当前位置 ms[cn]
if (ms[i - 1] == ms[cn]) {
next[i++] = ++cn;
} else if (cn > 0) {
//如果不一样就继续往前跳
cn = next[cn];
} else {
//跳到最前面都没一样 那么就=0
next[i++] = 0;
}
}
return next;
}
public static int getIndexOf(String s, String m) {
if (s == null || m == null || m.length() < 1 || s.length() < m.length())
return -1;
char[] str1 = s.toCharArray();
char[] str2 = m.toCharArray();
int i1 = 0; //在str1中比对的位置
int i2 = 0;//在str2中比对的位置
int[] next = getNextArray(str2); //对str2求next数组
while (i1 < str1.length && i2 < str2.length) {
//比对的位置两者的值一样,那就都++
if (str1[i1] == str2[i2]) {
i1++;
i2++;
}
//等价于i2 == 0 next[i2] == -1,代表str2已经到0位置都str1[i1] != str2[i2],str2没办法再往前跳,那就str1换一个位置和str2[0]匹配,i1++
else if (next[i2] == -1)
i1++;
else
//str2还可以往前跳(来到最长前后缀的下一个位置 ),i1固定不动
i2 = next[i2];
}
//while循环结束了,i2 == str2.length,那返回str2在str1中开始的位置就是i1 - i2
return i2 == str2.length ? i1 - i2 : -1;
}
2.宽度优先遍历(BFS)
思想:跟层序遍历一样,利用队列实现,但是有可能是有环的那种无向图,为了保证每个节点被遍历一次,需要一个HashSet的结构。从源节点开始依次按照宽度进队列,然后弹出 ,每弹出一个点,把该节点所有没有进过队列的邻接点放入队列 ,直到队列变空。
public static void bfs(Node node) {
if (node == null)
return;
Queue<Node> queue = new LinkedList<>();
HashSet<Node> set = new HashSet<>();
queue.add(node);
set.add(node);
while (!queue.isEmpty()) {
Node cur = queue.poll();//获得当前节点
System.out.println(cur.value);//处理节点
for (Node next : cur.nexts) //遍历这个节点的所有关联的节点
if (!set.contains(next)) {//当set表中有节点时候,就不能再放进去了,保证每个节点遍历一次
set.add(next);
queue.add(next);
}
}
}
3.广度优先遍历(DFS)
思想:利用栈实现,从源节点开始把节点按照深度放入栈,然后弹出,每弹出一个点,把该节点和下一个没有进过栈的邻接点放入栈 ,直到栈变空
public static void dfs(Node node) {
if (node == null)
return;
Stack<Node> stack = new Stack<>();
HashSet<Node> set = new HashSet<>();
stack.add(node);
set.add(node);
while (!stack.isEmpty()) {
Node cur = stack.pop();//当前节点
for (Node next : cur.nexts) {
if (!set.contains(next)) {
//如果set中没有包含下一个节点
stack.push(cur);//则先压入当前节点,再压入下一个节点
stack.push(next);
set.add(next);//再把下一个节点放到set中
System.out.println(next.value);//处理节点
break;
}
}
}
}
4.并查集
参考资源:并查集,是一种判断“远房亲戚”或者岛屿的算法。打个比方:你身边的某个“朋友”,很有可能就是你父亲的母亲的姑妈的大姨的哥哥的表妹的孙子的女儿的父亲的孙子。如果给定这么一张“家谱”(无向图),如何判断两个顶点是不是“亲戚”呢?用人话说,就是判断一个图中两个点是否联通(两个顶点相互联通则为亲戚)。
并查集是专门用来解决这样的问题的,和搜索不同,并查集在构建图的时候同时就标记出了哪个“人”属于哪个“团伙”(一团伙中的点两两联通)。
public class 并查集的实现 {
//首先设计一种结构,就是把元素封装成一个节点,仅仅是把value包裹一层
public static class Element<V>{
public V value;
public Element(V value){
this.value = value;
}
}
//并查集类
public static class UnionFindSet<V>{
public HashMap<V, Element<V>> elementMap;// 把每个元素封装成一个节点
public HashMap<Element<V>, Element<V>> fatherMap;// 保存该节点的父节点
public HashMap<Element<V>, Integer> sizeMap;// 获得该节点下面有多少子节点(包括自己)
//并查集的构造函数 在初始化的时候要求用户把所有的样本给你
public UnionFindSet(List<V> list){
elementMap = new HashMap<>();
fatherMap = new HashMap<>();
sizeMap = new HashMap<>();