算法训练营【3】蓄水池算法+KMP算法及其扩展

蓄水池算法+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. 树的子结构

剑指 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]

猜你喜欢

转载自blog.csdn.net/qq_41852212/article/details/121294706