文章目录
蓄水池算法+KMP算法
蓄水池算法
解决的问题:
假设有一个源源吐出不同球的机器,
只有装下10个球的袋子,每一个吐出的球,要么放入袋子,要么永远扔掉
如何做到机器吐出每一个球之后,所有吐出的球都等概率被放进袋子里
应用:抽奖
假设有个随机函数f(n),可以返回1-n之间的数
1-10号直接进袋子
10号以后的第k号球,使用随机函数f(k),(相当于是 10/k )
返回的如果是大于10的数字,那就不入袋,如果是10以内的,则随机选一个袋内的球扔出去,然后将新的球装入袋内。
public static class RandomBox {
private int[] bag;
private int N;
private int count;
public RandomBox(int capacity) {
bag = new int[capacity];
N = capacity;
count = 0;
}
private int rand(int max) {
return (int) (Math.random() * max) + 1;
}
public void add(int num) {
count++;
if (count <= N) {
bag[count - 1] = num;
} else {
if (rand(count) <= N) {
bag[rand(N) - 1] = num;
}
}
}
public int[] choices() {
int[] ans = new int[N];
for (int i = 0; i < N; i++) {
ans[i] = bag[i];
}
return ans;
}
}
KMP算法
解决的问题:快速匹配两个相同结构的子串是否相等
假设字符串str长度为N,字符串match长度为M,M <= N
想确定str中是否有某个子串是等于match的。
可以通过KMP算法,使得时间复杂度O(N)
暴力解决
以str 每个元素为开头遍历,如果与match中的字符相等,则依次往下
API解法
public class Main {
public static void main(String[] args) {
String s1="123456qwe";
String s2="456q";
System.out.println(s1.indexOf(s2));
}
}
kmp算法解决
前置知识
前缀字串和后缀字串
从第一个字符开始的字符到当前元素之前的字串(不包含它自己)叫前缀字串
从当前字符的前一个字符开始的,往前到第二个字符的字串叫后缀字串
且前缀字串和后缀字串最大长度为n-1,即字串不能是字符串本身
aaabba
对于第一个元素,他是第一个,则无前缀和无后缀
对于第二个,他是a,他的前缀和后缀都是a,但a又是它字符串的本身,所以前后缀最长相等长度为0
对于第三个,前缀最大为a,后缀最大为a,则前后缀最长相等长度为1
对于第四个,他前缀最大为aa,后缀最大为aa,则前后缀最长相等长度为2
以此类推
一个例子
假设str 为
aabbaabxabbaae
需要匹配的match为
aabbaae
假设用暴力解
则匹配到(加粗的代表正在匹配的位置)
aabbaab
aabbaae
时就会不匹配,从而再从str的第二个字符开始匹配
但其实不用返回到第二个字符开始,对于此例,match字符前缀 aa 和后缀aa 相等,那直接从前后相等的地方开始匹配就好了
aabbaabxabbaae
aabbaae
对于e,他的前后缀最长先等的长度是2,则从2位置的下一个开始与原字符匹配,两个b相等,同时往后
aabbaabxabbaae
aabbaae
x与b不相等,这个b的最长前后缀相等为0,即代表b前面的字符没有前后缀相等的子字符串。match字符串跳回到最开始的位置进行匹配
aabbaabxabbaae
aabbaae
x和a不想等,a已经是第一个字符了,第一个字符都不想等,则无法继续匹配,str字符串位置往后移一位再进行匹配。
对于match中的m字符每个,都需要求他的最长前后缀子串相等的长度,可以先求出来,放到数组中,可以直接通过数组【下标】来取值。
定义 next数组,它的值记录的是前面子串的最长前后缀子串相等的长度,
next数组的元素 表示如果在match 中失败的话,下一步开始尝试的位置是哪个位置
//匹配过程
public static int getIndexOf(String s1, String s2) {
if (s1 == null || s2 == null || s2.length() < 1 || s1.length() < s2.length()) {
return -1;
}
char[] str = s1.toCharArray();
char[] match = s2.toCharArray();
int x = 0;
int y = 0;
// O(M) m <= n
int[] next = getNextArray(match);
// O(N)
while (x < str.length && y < match.length) {
if (str[x] == match[y]) {
x++;
y++;
} else if (next[y] == -1) {
// y == 0 str往后一个位置
x++;
} else {
//match无法继续匹配,回到当前字符前面字串的最大相等位置的下一个位置继续匹配
y = next[y];
}
}
// y 越界 返回 -1
// x 越界。y 越界 最后一段匹配了
//x 没越界,y越界了, str 中有一部分是匹配到了match。也就是在 x-y位置
return y == match.length ? x - y : -1;
}
//求next数组过程
public static int[] getNextArray(char[] match) {
if (match.length == 1) {
return new int[] {
-1 };
}
int[] next = new int[match.length];
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪个位置上求next数组的值
int cn = 0; // 当前是哪个位置的值再和i-1位置的字符比较
while (i < next.length) {
// 当前位置的前一个位置的字符 如果与他记录的 next数组中元素位置的字符相等,那这个位置的next元素为cn加1
if (match[i - 1] == match[cn]) {
// 配成功的时候
next[i] = cn+1;
i++;
cn++;
} else if (cn > 0) {
// 不断的找上个位置,若回退到字符串开头。0 ,则代表没有前后缀公共长度为0
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
kmp回退原理
对于上图,str匹配到最后一个c时,发现了match最后一个字符匹配不上,
match回退到match第一个c的位置继续匹配
为什么这样回退一定保证第一个c位置之前的字符是能够与str字符串匹配上的?
对于t而言,它的最长前后缀相等和为5,即aabaa
而匹配到t位置之前,一定是与str都匹配上了,要不然也到不了t的位置
所以match中t的后缀aabaa,一定是在str中有的,即【1】=【2】
回退到c的位置是因为t最长前后缀相等和为5,代表前缀和后缀相等的最大长度为5 ,即 【2】=【3】
等量代换过来就是【1】=【2】=【3】
所以match从t回退到c,本质上是比较以 j 开头比较 i 开头的字符串
如果相等,继续下一个,如果不等则回退到当前元素的前后缀最大相等长度的下一个位置
直到match 回退到第一个字符,则表示已经没有前缀可以使用了,所以要str往后移动,重新开始匹配
时间复杂度求解思路
对于x(str 的遍历指针),y(match的遍历指针)
x-y 最大的情况为y为0,x为n
x(max:n) x-y(max:n)
第一个分支 str[x] == match[y]
x++;y++;
x ⬆️ y⬆️
第二个分支 next[y] == -1
x++;
x ⬆️ y不变
第三个分支 y = next[y];
x 不变 y⬇️
整体过程中,x最大为n,x-y最大为n,整体为2n,所以三个分支发生的次数不超过2n
即kmp过程中的时间复杂度为O(N)
应用:判断是否互为旋转词
例如Str1=“123456”,对于Str1的旋转词,字符串本身也是其旋转词,Str1="123456"的旋转词为,“123456”,“234561”,“345612”,“456123”,“561234”,“612345”。给定Str1和Str2,那么判断这个两个字符串是否互为旋转词?是返回true,不是返回false
暴力解法思路:把str1的所有旋转词都列出来,看str2是否在这些旋转词中。挨个便利str1,循环数组的方式,和str2挨个比对。O(N*N)
KMP解法:str1拼接str1得到str’,“123456123456”,我们看str2是否是str’的子串
因为字符拼起来已经囊括了所有的旋转词
应用:剑指 Offer 26. 树的子结构
暴力方法,以a树的每个节点去匹配b树的头节点,如果能匹配到,则继续匹配左右子树,直到 同时匹配完。
假设a树n个节点,b树m个节点,最坏情况为需要遍历a树每个节点,则时间复杂度为O(M*N)
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B==null){
return false;
}
if(A==null){
return false;
}
if(compare(A,B)){
return true;
}
return isSubStructure(A.left,B) || isSubStructure(A.right,B);
}
public boolean compare(TreeNode A, TreeNode B){
if( B==null){
return true;
}if(A==null ){
return false;
}
return A.val==B.val && compare(A.left,B.left) && compare(A.right,B.right);
}
先序序列化这两个树,然后kmp算法对比
时间复杂度O(N)
class Solution {
public boolean isEqual(String s1,String s2){
if(s1==null&&s2==null){
return true;
}else{
if(s1==null||s2==null){
return false;
}else{
return s1.equals(s2);
}
}
}
//匹配过程
public int getIndexOf(String[] s1, String[] s2) {
if (s1 == null || s2 == null || s2.length < 1 || s1.length < s2.length) {
return -1;
}
int x = 0;
int y = 0;
// O(M) m <= n
int[] next = getNextArray(s2);
// O(N)
while (x < s1.length && y < s2.length) {
if (isEqual(s1[x],s2[y])) {
x++;
y++;
} else if (next[y] == -1) {
// y == 0
x++;
} else {
y = next[y];
}
}
return y == s2.length ? x - y : -1;
}
//求next数组过程
public int[] getNextArray(String[] str2) {
if (str2.length == 1) {
return new int[] {
-1 };
}
int[] next = new int[str2.length];
next[0] = -1;
next[1] = 0;
int i = 2; // 目前在哪个位置上求next数组的值
int cn = 0; // 当前是哪个位置的值再和i-1位置的字符比较
while (i < next.length) {
if (isEqual(str2[i - 1],str2[cn])) {
// 配成功的时候
next[i++] = ++cn;
} else if (cn > 0) {
cn = next[cn];
} else {
next[i++] = 0;
}
}
return next;
}
public boolean isSubStructure(TreeNode A, TreeNode B) {
if(B==null || A==null )return false;
List<String> list1 = pre(A);
List<String> list2 = pre(B);
String[] str1 = new String[list1.size()];
for(int i=0;i<list1.size();i++){
str1[i]=list1.get(i);
}
String[] str2 = new String[list2.size()];
for(int i=0;i<list2.size();i++){
str2[i]=list2.get(i);
}
return getIndexOf(str1,str2)!=-1;
}
public List<String> pre(TreeNode node){
List<String> list = new ArrayList<>();
pres(list,node);
return list;
}
public void pres(List<String> list,TreeNode node){
if(node==null){
list.add(null);
}else{
list.add(String.valueOf(node.val));
pres(list,node.left);
pres(list,node.right);
}
}
}
最后卡一个用例,个人认为这组用例应该输出false,而不是true
[10,12,6,8,3,11] [10,12,6,8]