字符串相关算法题学习(字符串匹配KMP,后缀数组...)

一、基础题

1.1 字符串的包含问题

字符串B是否包含字符串A的全部字符
判断字符串A中的字符是否全部出现在字符串B中

public class StrContain {
    public static void main(String[] args) {

    }

    public static boolean check(String s1, String s2) {
    	// 遍历s1 如果s1中有元素不在s2中 直接返回false
    	// 遍历完没有返回false则说明符合条件
        for (int i = 0; i < s1.length(); i++) {
            char a = s1.charAt(i);
            if (s2.indexOf(a) == -1) {
                return false; 
            }
        }
        return true;
    }

    // 优化:对s2排序 对s1进行二分查找
    public static boolean check1(String s1, String s2) {
        char[] s2_arr = s2.toCharArray();
        Arrays.sort(s2_arr); // 排序
        for (int i = 0; i < s1.length(); i++) {
            char a = s1.charAt(i);
            int index = Arrays.binarySearch(s2_arr, a); // 二分查找
            if (index < 0) {
                return false;
            }
        }
        return true;
    }
}

1.2 字符串有无重复字符

public class RepetitionStr {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine();  // 可读取空格
        System.out.println(findSameS(s));
    }

    public static boolean findSameS(String s) {
        if (s.length() == 0) {
            return true;
        }
        // 对于ASC码字符开辟128的辅助空间
        int[] arr = new int[128];
        for (int i = 0; i < s.length(); i++) {
            int c = s.charAt(i);
            if (arr[c] > 0) {
                return false; // 有重复
            }else {
                arr[c]++;
            }
        }
        return true; // 没有
    }
}

1.3 巧妙反转字符串

请实现一个算法,翻转一个给定的字符串
测试样例:
“This is nowcoder”
返回"redocwon si sihT"

public class ReverseStr {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine();
        System.out.println(reverseS(s));
        System.out.println(reverseS1(s));
    }

    public static String reverseS1(String s) {
        int len = s.length();
        char[] charArr = new char[len];
        for (int i = 0; i < len; i++) {
            charArr[i] = s.charAt(len - 1 - i);
        }
        return new String(charArr);
    }

    // 巧用API
    public static String reverseS(String s) {
        StringBuilder sb = new StringBuilder(s);
        return sb.reverse().toString();
    }
}

1.4 变形词问题

给定两个字符串,请编写程序,确定其中一个字符串的字符重新排列后,能否变成另一个字符串。
这里规定大小写为不同字符,且考虑字符串中的空格。给定一个string A和一个string B,请返回一个bool,代表两串是否重新排列后可相同。保证两串的长度都小于等于5000.
测试样例:
“Here you are”,“Are you here”
返回false

public class ChangeWord {
    public static void main(String[] args) {
        String A = "Here you are";
        String B = "are you Here";
        System.out.println(checkSame(A, B));
    }

    // 巧用API
    public static boolean checkSam(String A, String B) {
        if (A.length() != B.length()) {
            return false;
        }
        char[] arr1 = A.toCharArray();
        char[] arr2 = B.toCharArray();
        Arrays.sort(arr1);
        Arrays.sort(arr2);
        return Arrays.equals(arr1, arr2);
    }

    // 开辟128数组计数 扫描A+1,扫描B-1,如果出现负数则返回false
    // 在遍历一次数组,如果有等于1的则返回false
    public static boolean checkSame(String A, String B) {
        if (A.length() != B.length()) {
            return false; // 长度不相等
        }
        int[] arr = new int[128];
        for (int i = 0; i < A.length(); i++) {
            int c = A.charAt(i);
            arr[c]++;
        }
        for (int i = 0; i < B.length(); i++) {
            int c = B.charAt(i);
            arr[c]--;
            if (arr[c] < 0) { // 说明B中某一个字符比A多
                return false;
            }
        }
        for (int i = 0; i < arr.length; i++) { // 说明A中某一个字符比B多
            if (arr[i] == 1) {
                return false;
            }
        }
        return true;
    }
}

1.5 替换字符串中的空格

请编写一个方法,将字符串中的空格全部替换为“%20”。假定该字符串有足够的空间存放新增的字符,并且知道字符串的真实长度(小于等于1000),同时保证字符串由大小写的英文字母组成。
给定一个string iniString为原始串,以及串的长度 int len,返回替换后的string
测试样例:
“Mr John Smith”,13
返回"Mr%20John%20Smith"

public class ReplaceNull {
    public static void main(String[] args) {
        String s = "Mr John Smith";
        System.out.println(replace(s, s.length()));
        System.out.println(replaceSpace(s, s.length()));
        System.out.println(replace("Mr John Smith0000000000".toCharArray(),13));
    }

    // 巧用API 正则表达式
    public static String replaceSpace(String s, int len) {
        return s.replaceAll("\\s", "%20");
    }

    // 1
    public static String replace(String s, int len) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < len; i++) {
            if (s.charAt(i) == ' ') {
                sb.append("%20");
            } else {
                sb.append(s.charAt(i));
            }
        }
        return sb.toString();
    }

    // 2
    public static String replace(char[] iniString, int len) {
        int count = len; // 记录空格 扩充数组 空格占一个字符 %20占3个字符
        for (int i = 0; i < iniString.length; i++) {
            if (iniString[i] == ' ') {
                count += 2;
            }
        }
        int p1 = len - 1; // 原数组长度-1
        int p2 = count - 1; // 变形后数组长度-1
        while (p1 >= 0) {
            if (iniString[p1] == ' ') {
                iniString[p2--] = '0';
                iniString[p2--] = '2';
                iniString[p2--] = '%';
            } else {
                iniString[p2--] = iniString[p1];
            }
            p1--;
        }
        return new String(iniString, 0, count);
    }
}

1.6 压缩字符串

利用字符串重复出现的次数,编写一个方法,实现基本的字符串压缩功能。
比如,字符串“aabcccccaaa”经压缩会变成“a2b1c5a3”。若压缩后的字符串没有变短,则返回原先的字符串。给定一个string iniString为待压缩的串(长度小于等于10000),保证串内字符均由大小写英文字母组成,返回一个string,为所求的压缩后或未变化的串。
测试样例:
“aabcccccaaa”
返回:“a2b1c5a3”

public class ReduceStr {
    public static void main(String[] args) {
        String s = "aabcccccaabd";
        System.out.println(zipS(s));
    }

    public static String zipS(String s) {
        int count = 1; // 累计出现次数
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < s.length() - 1; i++) {
            while (i < s.length() - 1 && s.charAt(i) == s.charAt(i + 1)) {
                count++;
                i++;
            }
            sb.append(s.charAt(i)).append(count);
            count = 1;
        }
        if (s.charAt(s.length() - 1) != s.charAt(s.length() - 2)) {
            sb.append(s.charAt(s.length()-1)).append(1);
        }
        return sb.toString();
    }
}

1.7 判断两字符集是否相同

public class SameStr {
    public static void main(String[] args) {

    }

    // 有限制,使用hash映射
    public static boolean check2(String s1, String s2) {
        HashMap<Character, Integer> map = new HashMap<>();
        // 扫描s1
        for (int i = 0; i < s1.length(); i++) {
            char c = s1.charAt(i);
            if (map.get(c) == null) { // 没有键
                map.put(c, 1);
            }
        }
        // 扫描s2
        for (int i = 0; i < s2.length(); i++) {
            char c = s2.charAt(i);
            if (map.get(c) == null) {
                return false;
            }
        }
        return true;
    }

    // 没有限制为ASCII,只需要开辟256的辅助空间
    public static boolean check(String s1, String s2) {
        int[] help = new int[256];
        // 扫描s1
        for (int i = 0; i < s1.length(); i++) {
            char c = s1.charAt(i);
            if (help[c] == 0) {
                help[c] = 1;
            }
        }
        // 扫描s2
        for (int i = 0; i < s2.length(); i++) {
            char c = s2.charAt(i);
            if (help[c] == 0) { // 说明s1中无s2
                return false;
            }
        }
        return true;
    }
}

1.8 旋转词

给定两个字符串s1和s2,要求判定s2是否能够被通过s1做循环移位得到的字符串包含。例如,给定s1=AABCD和s2=CDAA,返回true;给定s1=ABCD和s2=ACBD,返回false

public class RotateWord {
    public static void main(String[] args) {
        String s1 = "ABCD";
        String s2 = "ACBD";
        System.out.println(isRotate(s1, s2));
    }

    // 判断s1+s1是否包含s2
    public static boolean isRotate(String s1, String s2) {
        if (s1.length() < s2.length()) {
            return false;
        }
        // 自己和自己拼起来
        StringBuilder sb = new StringBuilder(s1).append(s1);
        // 判断s2是否在sb中
        return sb.toString().contains(s2);
    }
}

1.9 将字符串按单词翻转

将字符串按单词翻转,如here you are翻转成are you here

public class ReverseWords {
    public static void main(String[] args) {
        String s = "here you are";
        System.out.println(WordReverse(s));
    }

    public static String WordReverse(String s) {
        String[] s1 = s.split(" ");
        StringBuilder sb = new StringBuilder();
        for (int i = s1.length - 1; i >= 0; i--) {
            sb.append(s1[i]).append(" ");
        }
        //去掉最后一个空格
        return sb.deleteCharAt(sb.length() - 1).toString();
    }
}

1.10 神奇的回文串

acbca
aaaaa
abcba

问题描述:1221是一个非常特殊的数,它从左边读和从右边读是一样的,编程求所有这样的四位十进制数。
输出格式:按从小到大的顺序输出满足条件的四位十进制数

public class PalindromeStr {
    public static void main(String[] args) {
        System.out.println(isPalindrome(""));
        palindromeNumber();
    }

    public static boolean isPalindrome(String s) {
        if (s.isEmpty()) {
            return true;
        }
        return s.equals(new StringBuilder(s).reverse().toString());
    }

    public static void palindromeNumber() {
        for (int i = 1; i < 10; i++) {
            for (int j = 0; j < 9; j++) {
                //千位和个位相同,百位和十位相同
                System.out.println(i * 1000 + j * 100 + j * 10 + i);
            }
        }
    }
}

二、尺取法

尺取法:顾名思义,像尺子一样取一段,借用挑战书上面的话说,尺取法通常是对数组保存一对下标,即所选取的区间的左右端点,然后根据实际情况不断地推进区间左右端点以得出答案。尺取法比直接暴力枚举区间效率高很多,尤其是数据量大的时候,所以说尺取法是一种高效的枚举区间的方法,是一种技巧,一般用于求取有一定限制的区间个数或最短的区间等等。当然任何技巧都存在其不足的地方,有些情况下尺取法不可行,无法得出正确答案,所以要先判断是否可以使用尺取法再进行计算。

2.1 hiho字符串

描述:如果一个字符串恰好包含2个’h’、1个’i’,和1个’o’,我们就称这个字符串是hiho字符串
例如:“olhateher”、"hugeInputhugeoutput"都是hiho字符串
现在给定一个只包含小写字母的字符串S,小Hi想知道S的所有子串中,最短的hiho字符串是哪个
输入:
字符串S
对于80%的数据,S的长度不超过1000
对于100%的数据,S的长度不超过100000
输出:
找到S的所有子串中,最短的hiho字符串是哪个,输出该子串的长度。如果S的子串中没有hiho字符串,输出-1
样例输入:
happyhahaiohell
样例输出:
5

import java.util.Scanner;

public class DeterminingTheFeet {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String s = sc.nextLine();
        char[] w = s.toCharArray();
        solve(w);
    }

    public static void solve(char[] w) {
        int min = Integer.MAX_VALUE; // 更新最小值
        int j = -1; // 定义是否为第一次
        for (int i = 0; i < w.length; i++) {
            char c = w[i];
            if (check(c)) { // 判断是否是 h、i、o 其中任意一个字符 不是则跳过
                if (j == -1) { // j的第一次定位
                    j = i + 1;
                }
                while (j < w.length) {
                    char c2 = w[j];
                    if (check(c2) && containsAll(w, i, j)) { // 全部囊括
                        if (check(w, i, j) && j - i + 1 < min) { // 满足条件 更新min
                            min = j - i + 1;
                        }
                        break;
                    }
                    j++; // j继续扫描
                }
            }
        }
        System.out.println(min == Integer.MAX_VALUE ? -1 : min);
    }

    // 判断从i->j 是否恰好包含2个h 1个i 1个o
    public static boolean check(char[] w, int i, int j) {
        int c1 = 0;
        int c2 = 0;
        int c3 = 0;
        for (int k = i; k <= j; k++) {
            if (w[k] == 'h') c1++;
            if (w[k] == 'i') c2++;
            if (w[k] == 'o') c3++;
        }
        return c1 == 2 && c2 == 1 && c3 == 1;
    }

    // 判断从i->j 是否包含 超过2个h 1个i 1个o
    public static boolean containsAll(char[] w, int i, int j) {
        int c1 = 0;
        int c2 = 0;
        int c3 = 0;
        for (int k = i; k <= j; k++) {
            if (w[k] == 'h') c1++;
            if (w[k] == 'i') c2++;
            if (w[k] == 'o') c3++;
        }
        return c1 >= 2 && c2 >= 1 && c3 >= 1;
    }

    // 判断是否是 h i o 其中任意一个字符
    public static boolean check(char c) {
        return c == 'h' || c == 'i' || c == 'o';
    }
}

2.2 最短摘要的生成

尺取法例题(左右指针一直交替往右扫描更新最短距离)
给定一段产品的英文描述,包含M个英文单词,每个英文单词以空格分隔,无其他标点符号;再给定N个英文单词关键词,请说明思路并编程实现方法
目标是找出此产品描述中包含N个关键字(每个关键字至少出现一次)的长度最短的子串,作为产品输出。

import java.util.Arrays;

public class Shortest {
    public static void main(String[] args) {
        // 文章
        String[] s = {"a", "b", "c", "d", "h", "e", "f", "c", "c", "d", "e", "f", "d", "c"};
        // 在文章中找出包含"c" "e"的最小一段
        solve(s, new String[]{"c", "e"});
    }

    public static void solve(String[] w, String[] keys) {
        int begin = -1; // 记录开始位置
        int end = -1; // 记录结束位置
        int j = -1; // 记录上一次囊括了所有关键字的右边界
        int len = Integer.MAX_VALUE; // 最大int数 记录最短文章长度
        int[] keyFound = new int[keys.length]; // 辅助数组做标记
        for (int i = 0; i < w.length; i++) {
            Arrays.fill(keyFound, 0); // 先全部填充为0
            // 如果i位置是关键字 求以i开头包含所有关键字的序列
            String word1 = w[i];
            int index = indexOf(keys, word1); // 返回包含word1处的索引 没有则返回-1
            if (-1 == index) { // 不是关键字 跳过
                continue;
            } else { // 是关键字 在辅助数组 对应索引位置做标记
                keyFound[index] = 1; // 标记
            }
            if (j == -1) { // j第一次定位
                j = i + 1;
            }
            for (; j < w.length; j++) {
                String word2 = w[j]; // 当前文章单词
                int index1 = indexOf(keys, word2); // 判断是否为关键字
                if (index1 == -1 || keyFound[index1] == 1) { // 不是关键字或关键字重复
                    continue;
                } else { // 找到没有发现过的关键字
                    keyFound[index1] = 1; // 标记一下
                    if (sum(keyFound) == keys.length) { // 辅助数组长度等于关键字长度,全部找齐
                        if (j - i + 1 < len) { // 更新
                            len = j - i + 1;
                            begin = i;
                            end = j;
                        }
                        break;
                    }
                }
            }
        }
        print(w, begin, end);
    }

    // 在关键字数组中查找word 找到则返回单词所在的索引 没有则返回-1
    public static int indexOf(String[] q, String word1) {
        for (int i = 0; i < q.length; i++) {
            if (q[i].equals(word1)) {
                return i;
            }
        }
        return -1;
    }

    // 标记数组求和
    public static int sum(int[] keyFound) {
        int sum = 0;
        for (int e : keyFound) {
            sum += e;
        }
        return sum;
    }

    // 输出
    static void print(String[] w, int begin, int end) {
        System.out.println(begin + " " + end);
        for (int i = begin; i <= end; i++) {
            System.out.println(w[i] + " ");
        }
        System.out.println();
    }
}

三、字符串匹配

3.1 暴力匹配字符串

public class Brute_Froce {
    public static void main(String[] args) {
        String s1 = "abcdefg";
        String s2 = "def";
        System.out.println(match(s1, s2));
    }

    /**
     * @param s1 原字符串
     * @param s2 匹配的串
     */
    static int match(String s1, String s2) {
        if (s2.length() > s1.length()) { // 匹配的串比原来的串要长 不满足条件
            return -1;
        }
        int l = 0; // 指向s1
        int r = 0; // 指向s2
        while (l < s1.length()) {
            if (s1.charAt(l) == s2.charAt(r)) { // 相等则同时往右走
                l++;
                r++;
            } else { // 不相等原串回到初始匹配的位置+1 匹配串回到首位
                l = l - r + 1;
                r = 0;
            }
            if (r == s2.length()) { // 匹配串来到最后一位 匹配成功
                return l - r;
            }
        }
        return -1;
    }
}

3.2 哈希匹配字符串

hash->滚动hash
hash§–O(n)
hash(s)–O(m*n)——>O(m+n)
求hash
31进制:C0 * 31 ^ 2+C1 * 31 ^ 1+C2 * 31 ^ 0(3个字符) --> ((C0+0) * 31+C1) * 31+C2

public class PabinKarp {

    final static long seed = 31;

    public static void main(String[] args) {
        String s1 = "abcdefg";
        String s2 = "def";
        match(s1, s2);
    }

    /**
     * @param s 原串
     * @param p 匹配串
     */
    static void match(String s, String p) {
        long hash_p = hash(p); // p的hash值
        /*
        // 普通hash匹配
        int p_len = p.length(); // 匹配串的长度 每次截取当前i+匹配串长度
        for (int i = 0; i + p_len < s.length(); i++) {
            long hash_i = hash(s.substring(i, i + p_len)); // 截取子串
            if (hash_i == hash_p) { // 判断子串的hash和匹配串是否相等
                System.out.println("match:" + i);
            }
        }*/

        // 滚动hash匹配
        long[] hash_s = hash(s, p.length());
        for (int i = 0; i < hash_s.length; i++) {
            if (hash_p == hash_s[i]) {
                System.out.println("match:" + i);
            }
        }
    }

    // 普通求hash
    // 公式:C0 * 31^2 + C1 * 31^1 + C2 * 31^0(3个字符) --> ((C0+0) * 31 + C1) * 31 + C2
    static long hash(String s) {
        long h = 0;
        for (int i = 0; i < s.length(); i++) {
            h = seed * h + s.charAt(i);
        }
        return h % Long.MAX_VALUE;
    }


    /**
     * 滚动hash
     * 用滚动的方法求出s中长度为n的每个子串的hash,组成一个hash数组
     * 加一个新的,减去最前面那个
     */
    static long[] hash(String s, int n) {
        long[] res = new long[s.length() - n + 1]; // 有多少个子串 就开辟多大的数组
        // 前n个字符的hash
        res[0] = hash(s.substring(0, n)); // 第一个子串
        for (int i = n; i < s.length(); i++) { // 计算除第一个外 每一个子串
            char newChar = s.charAt(i); // 新增加的一个
            char oldChar = s.charAt(i - n); // 最前面的一个
            // 前n个字符的hash*seed-前n字符的第一字符*seed的n次方
            // (C0 * 31 + C1) * 31 + C2
            long v = (long) ((res[i - n] * seed + newChar - Math.pow(seed, n) * oldChar) % Long.MAX_VALUE);
            res[i - n + 1] = v;
        }
        return res;
    }
}

3.3 KMP匹配字符串

通常我们在对字符串进行匹配时通常都采用的是嵌套循环的方式,这种方式虽然利于理解但是代码的时间复杂度实在太高了,每次都会执行很多次相同的代码,如果我们想要降低它的时间复杂度我们应该怎么操作?

我们首先要知道为什么常用的嵌套式的循环的时间复杂度高,是因为我们在对字符串进行回溯的时候,主串和子串都要进行回溯,而且回溯的距离非常短,导致代码运行的慢,那么想要提高代码的运行效率我们应该怎么来对回溯的位置进行确定呢?这里就要首先了解了解字符串的最长相等前缀和后缀。

字符串的最大相等前后缀:
关于字符串最大相等前后缀我先举个例子:
字符串:abcdab
前缀的集合:{a, ab, abc, abcd, abcda}
后缀的集合:{b, ab, dab, cdab, bcdab}
那么将其一一进行匹配就可以知道最长相等前后缀就是ab。

在例如字符串:abcabfabcab中最长相等前后缀是什么:这下就能看出来了,就是abcab。

理解了最大相等前后缀我们对该字符串的回溯位置就大概解决了,当你在前缀位置时不匹配了,我们就可以直接跳到后缀位置再进行匹配,反之也可以,因为它们是相匹配的子字符串的。

next数组:
这是KMP算法中最重要的

next数组,它的作用有两个:
1.next[i]的值表示下标为i的字符前的字符串最长相等前后缀的长度。
2.表示该处字符不匹配时应该回溯到的字符的下标。

字符串:abcabcmn,要求得该字符串的next数组我们就的把该字符串逐一拆开,求得它的子串的最长相等前后缀,拆开可以得到:{a, ab, abc, abca, abcab, abcabc, abcabcm, abcabcmn},对其分别求最长相等前后缀可得出next数组为:next[0] = -1(前面没有字符串单独处理)

abcabcmn
next[0]next[1]next[2]next[3]next[4]next[5]next[6]next[7]
-10001230

next数组有这两个作用的源头是:之前提到的字符串的最长相等前后缀。

图解KMP算法:

现在我们先看一个图:第一个长条代表主串,第二个长条代表子串。红色部分代表两串中已匹配的部分,绿色和蓝色部分分别代表主串和子串中不匹配的字符。
再具体一些:这个图代表主串"abcabeabcabcmn"和子串"abcabcmn"。

在这里插入图片描述
现在发现了不匹配的地方,根据KMP的思想我们要将子串向后移动,现在解决要移动多少的问题。
之前提到的最长相等前后缀的概念有用处了。因为红色部分也会有最长相等前后缀。如下图:
在这里插入图片描述
灰色部分就是红色部分字符串的最长相等前后缀,我们子串移动的结果就是让子串的红色部分最长相等前缀和主串红色部分最长相等后缀对齐。
在这里插入图片描述
这一步弄懂了,KMP算法的精髓就差不多掌握了。接下来的流程就是一个循环过程了。事实上,每一个字符前的字符串都有最长相等前后缀,而且最长相等前后缀的长度是我们移位的关键,所以我们单独用一个next数组存储子串的最长相等前后缀的长度。而且**next数组的数值只与子串本身有关。**因为你前几项已经匹配成功了,所以子串的前几项的最长相等前后缀和已经匹配部分的主串的最长相等前后缀相同了。

KMP算法的时间复杂度:

KMP算法中多了一个求next数组的过程,多消耗了一点点空间。我们设主串s长度为n,子串t的长度为m。求next数组时时间复杂度为O(m),因后面匹配中主串不回溯,比较次数可记为n,所以KMP算法的总时间复杂度为O(m+n),空间复杂度记为O(m)。相比于朴素的模式匹配时间复杂度O(m*n),KMP算法提速是非常大的,这一点点空间消耗换得极高的时间提速是非常有意义的,这种思想也是很重要的。

解释next数组构造过程中的回溯问题:

举个例子:下面的长条代表子串,红色部分代表当前匹配上的最长相等前后缀,蓝色部分代表t.data[j]。

在这里插入图片描述
在这里插入图片描述

public class Kmp {
    public static void main(String[] args) {
        String str1 = "BBC ABCDAB ABCDABCDABDE";
        String str2 = "ABCDABD";
        System.out.println(Arrays.toString(SNext(str2))); // 输出next数组
        System.out.println(index(str1, str2)); // 输出结果
    }

    static int index(String s, String p) {
        if (s.length() == 0 || p.length() == 0) return -1;
        if (p.length() > s.length()) return -1;
        int[] next = SNext(p); // 匹配数组
        int i = 0; // s位置
        int j = 0; // p位置
        int sLen = s.length();
        int pLen = p.length();
        while (i < sLen) {
            // 如果j=-1,或者当前字符匹配成功(即是s[i]==s[j]),都另i++,j++
            // j=-1,因为next[0]=-1,说明p的第一位和i这个位置无法匹配,这时i,j都增加1,i移位,j从0开始
            if (j == -1 || s.charAt(i) == p.charAt(j)) {
                i++;
                j++;
            } else {
                // 如果j!=-1,且当前字符匹配失败(即s[i]!=p[j]),则令i不变,j=next[j]
                // next[j]即为j所对应的next值
                j = next[j];
            }
            if (j == pLen) {
                return i - j;
            }
        }
        return -1;
    }

    /**
     * 如果p[j]==p[k],next[j+1]=k+1或者k<0,next[j+1]=k+1;j++,k++
     * 否则,k继续回溯,直到满足p[j]==p[k]或者k<0
     * 记录的是前n-1个
     *
     * @param s
     * @return
     */
    static int[] SNext(String s) {
        int[] next = new int[s.length()]; // next数组长度与字符串相同
        char[] p = s.toCharArray(); // 转为字符数组
        next[0] = -1; // 第一位设置为-1
        if (s.length() == 1) {
            return next;
        }
        next[1] = 0; // 第二位初始化为0
        int j = 1; // 从第一位开始扫描
        int k = next[j];
        while (j < p.length - 1) { // 最后一位不计算
            if (k < 0 || p[j] == p[k]) {
                next[++j] = ++k;
            } else {
                k = next[k]; // 回溯
            }
        }
        return next;
    }
}

四、next数组&后缀数组&高度数组

4.1 next数组例题

前缀周期性
aaa
abab
abcabc next[6]=3
abcabcabc next[9]=6 j=9,k=6
j%(j-k)=0
次数:j/(j-k)

Input:
3
aaa
12
aabaabaabaab
0

Output:周期串索引->出现次数
Test case #1
2 2
3 3
Test case #2
2 2
6 2
9 3
12 4

public class NextArr {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        ArrayList<String> list = new ArrayList();
        while (true) {
            int n = sc.nextInt();
            if (n == 0) {
                break;
            }
            String s = sc.next();
            list.add(s);
        }
        for (int j = 0; j < list.size(); j++) {
            String s = list.get(j);
            int[] next = SNext(s);
            System.out.println(Arrays.toString(next));
            System.out.println("Tset case #" + (j + 1));
            for (int i = 2; i < next.length; i++) {
                int k = next[i];
                int t = i - k;
                if (i % t == 0 && i / t > 1) {
                    System.out.println(i + " " + i / t);
                }
            }
        }
    }

    static int[] SNext(String s) {
        int[] next = new int[s.length() + 1]; // next数组长度与字符串相同
        char[] p = s.toCharArray(); // 转为字符数组
        next[0] = -1; // 第一位设置为-1
        if (s.length() == 1) {
            return next;
        }
        next[1] = 0; // 第二位初始化为0
        int j = 1; // 从第一位开始扫描
        int k = next[j];
        while (j < p.length) { // 最后一位不计算
            if (k < 0 || p[j] == p[k]) {
                next[++j] = ++k;
            } else {
                k = next[k]; // 回溯
            }
        }
        return next;
    }
}

4.2 后缀数组

后缀数组:就是串的所有后缀子串按字典序排序后,在数组中记录后缀的起始下标后缀数组就是:排名和原下标的映射sa[0]=5,起始下标的后缀在所有后缀中字典序最小

rank(排名数组)数组:给定后缀的小标,返回其字典序,rk[5]=0;rank[sa[i]]=i;

ABABABABB
ABABABABB ~ 0
 BABABABB ~ 1
  ABABABB ~ 2
   BABABB ~ 3
    ABABB ~ 4
	 BABB ~ 5
	  ABB ~ 6
	   BB ~ 7
	    B ~ 8

sa=[“ABABABABB~ 0”,“ABABABB~ 2”,“ABABB~ 4”,“ABB~ 6”,“B~8”,
“BABABABB~ 1”,“BABABB~ 3”,“BABB~ 5”,“BB~7”]

sa[0]=0,sa[1]=2,sa[2]=4,sa[3]=6,sa[4]=8,sa[5]=1,sa[6]=3,sa[7]=5,as[8]=7

rk[0]=0,rk[1]=5,rk[2]=1,rk[3]=6,rk[4]=2,rk[5]=7,rk[6]=3,rk[7]=8,rk[8]=4

子串:一定是某个后缀的前缀
查找子串,对后缀数组用二分查找

后缀数组有什么用:匹配

怎么求后缀数组:

  • 把所有后缀数组放入数组,Arrays.sort(); // nlog(n)
  • 倍增法
    • k=1,一个字符,排序,得到sa,rk
    • k=2,利用上一轮的rk快速比较两个后缀
    • k=4
    • k=8
public class Suffix {
    public static void main(String[] args) {
        String s = "ABABABABB";
        String p = "BABB";
        match(s, p);
    }

    /**
     * 字符串匹配
     *
     * @param s1 原串
     * @param s2 匹配串
     */
    public static void match(String s1, String s2) {
        Suff[] sa = getSa(s1); // 后缀数组
        int l = 0;
        int r = s1.length() - 1;
        // 二分查找
        while (l <= r) {
            int mid = l + ((r - l) >> 1);
            Suff midSuff = sa[mid]; // 居中的后缀
            String suffStr = midSuff.s; // 居中后缀的字符串
            int compareRes;
            // 将后缀和匹配串比较O(n)
            if (suffStr.length() >= s2.length()) {
                compareRes = suffStr.substring(0, s2.length()).compareTo(s2);
            } else {
                compareRes = suffStr.compareTo(s2);
            }
            if (compareRes == 0) {
                System.out.println(midSuff.index);
                break;
            } else if (compareRes < 0) {
                l = mid + 1;
            } else {
                r = mid - 1;
            }
        }
    }

    /**
     * 获取后缀数组
     *
     * @param s 后缀数组原字符串
     * @return 后缀数组
     */
    public static Suff[] getSa(String s) {
        int strLen = s.length();
        // sa是排名到下标的映射,即sa[i]==k为排名为i的后缀是从k开始的
        Suff[] suffixArray = new Suff[strLen];
        for (int i = 0; i < strLen; i++) {
            String suffI = s.substring(i);
            suffixArray[i] = new Suff(suffI, i);
        }
        // 排序
        Arrays.sort(suffixArray);
        return suffixArray;
    }
}


class Suff implements Comparable<Suff> {
    String s;
    int index;

    public Suff(String s, int index) {
        this.s = s;
        this.index = index;
    }

    public int compareTo(Suff o2){
        return this.s.compareTo(o2.s);
    }

    @Override
    public String toString() {
        return "Suff{" +
                "s='" + s + '\'' +
                ", index=" + index +
                '}';
    }
}

4.3 高度数组

在后缀数组的基础上,两两比较公共子串
ABABABABB~0  height[0]=0
  ABABABB~6  height[1]=6
	ABABB~4		...
	  ABB~2
	    B~0
 BABABABB~1
   BABABB~5
	 BABB~4
	   BB~1
height[i] = lcp(sa[i],sa[i-1]); //lcp:最长公共子串
height[rk[i]] --> height[rk[i+1]]
height[rk[i+1]] >= height[rk[i]]-1
public class HeightArr {
    public static void main(String[] args) {
        String s="ABCABC";
        int[] heigth = getHeigth(s, getSa(s));
        System.out.println(Arrays.toString(heigth));
    }

    public static int[] getHeigth(String src, Suff[] sa) {
        int strLength = src.length();
        int[] rk = new int[strLength];
        // 将rank表示为不重复的排名即0~n-1
        for (int i = 0; i < strLength; i++) {
            rk[sa[i].index] = i;
        }
        int[] height = new int[strLength];
	/*
	如果已经知道后缀数组中i与i+1的lcp为h,那么i代表的字符串与i+1代表的字符串去掉首字母后的lcp为h-1
	根据这个我们可以发现,如果知道i与后缀数组中在它后一个的lcp为k,
	那么它去掉首字母后的字符串与其在后缀数组中的后一个的lcp大于等于k-1
	例如对于字符串abcefabc,我们知道abcefabc与abc的lcp为3,那么bcefabc的lcp大于等于3-1;
	利用这一点就可以O(n)求出高度数组
	*/
        int k = 0;
        for (int i = 0; i < strLength; i++) {
            int rankOfSuffI = rk[i]; // i后缀的排名
            if (rankOfSuffI == 0) {
                height[0] = 0;
                continue;
            }
            int rankOfPre = rankOfSuffI - 1;
            int j = sa[rankOfPre].index; // j是i串字典序靠前的串的下标
            if (k > 0) k--;
            for (; j + k < strLength && i + k < strLength; k++) {
                if (src.charAt(j + k) != src.charAt(i + k)) {
                    break;
                }
            }
            height[rankOfSuffI] = k;
        }
        return height;
    }

    /**
     * 获取后缀数组
     *
     * @param s 后缀数组原字符串
     * @return 后缀数组
     */
    public static Suff[] getSa(String s) {
        int strLen = s.length();
        //sa是排名到下标的映射,即sa[i]==k为排名为i的后缀是从k开始的
        Suff[] suffixArray = new Suff[strLen];
        for (int i = 0; i < strLen; i++) {
            String suffI = s.substring(i);
            suffixArray[i] = new Suff(suffI, i);
        }
        //排序
        Arrays.sort(suffixArray);
        return suffixArray;
    }
}

4.4 后缀数组应用

最长重复子串(可重叠或者说可交叉)(leetcode)

abcdbcde bcd 3
123232323 232323 6

–>高度数组的最大值

public class HeightArr {
    public static void main(String[] args) {
        String s = "ABCABC";
        int[] heigth = getHeight(s, getSa(s));
        System.out.println(Arrays.toString(heigth));

        System.out.println(MaxRepeatSubString("123232323"));
    }

    public static int MaxRepeatSubString(String s) {
        Suff[] sa = getSa(s);
        int[] height = getHeight(s, sa);
        int maxHeight = 0;
        int maxIndex = -1;
        for (int i = 0; i < height.length; i++) {
            if (height[i] > maxHeight) {
                maxHeight = height[i];
                maxIndex = i;
            }
        }
        int index = sa[maxIndex].index;
        System.out.println(s.substring(index, index + maxHeight));
        return maxHeight;
    }

    public static int[] getHeight(String src, Suff[] sa) {
        int strLength = src.length();
        int[] rk = new int[strLength];
        //将rank表示为不重复的排名即0~n-1
        for (int i = 0; i < strLength; i++) {
            rk[sa[i].index] = i;
        }
        int[] height = new int[strLength];
	/*
	如果已经知道后缀数组中i与i+1的lcp为h,那么i代表的字符串与i+1代表的字符串去掉首字母后的lcp为h-1
	根据这个我们可以发现,如果知道i与后缀数组中在它后一个的lcp为k,
	那么它去掉首字母后的字符串与其在后缀数组中的后一个的lcp大于等于k-1
	例如对于字符串abcefabc,我们知道abcefabc与abc的lcp为3,那么bcefabc的lcp大于等于3-1;
	利用这一点就可以O(n)求出高度数组
	*/
        int k = 0;
        for (int i = 0; i < strLength; i++) {
            int rankOfSuffI = rk[i];//i后缀的排名
            if (rankOfSuffI == 0) {
                height[0] = 0;
                continue;
            }
            int rankOfPre = rankOfSuffI - 1;
            int j = sa[rankOfPre].index;//j是i串字典序靠前的串的下标
            if (k > 0) k--;
            for (; j + k < strLength && i + k < strLength; k++) {
                if (src.charAt(j + k) != src.charAt(i + k)) {
                    break;
                }
            }
            height[rankOfSuffI] = k;
        }
        return height;
    }

    /**
     * 获取后缀数组
     *
     * @param s 后缀数组原字符串
     * @return 后缀数组
     */
    public static Suff[] getSa(String s) {
        int strLen = s.length();
        //sa是排名到下标的映射,即sa[i]==k为排名为i的后缀是从k开始的
        Suff[] suffixArray = new Suff[strLen];
        for (int i = 0; i < strLen; i++) {
            String suffI = s.substring(i);
            suffixArray[i] = new Suff(suffI, i);
        }
        //排序
        Arrays.sort(suffixArray);
        return suffixArray;
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
字符串匹配算法是一种用来查找一个字符串(即目标串)在另一个字符串(即模式串)中的出现位置的算法。其中,KMP算法是一种比较常用的字符串匹配算法KMP算法的核心思想是通过利用模式串中已经匹配过的信息,来尽量减少目标串和模式串的比较次数,从而提高匹配效率。它利用一个最长公共前缀和最长公共后缀数组,记录模式串中已经匹配成功的前缀和后缀的长度。通过根据这些信息来移动模式串的位置,避免不必要的比较。 而字符串哈希算法是一种将字符串映射为一个较短的固定长度的数值的算法。通过对字符串的每个字符进行一系列运算,如求幂、取模等,最终得到一个哈希值。这个哈希值可以代表该字符串的特征,不同字符串的哈希值一般不会相同。 字符串哈希算法的主要作用是将字符串转化为一个定长的数字,方便在数据结构中进行比较和存储。在字符串匹配中,使用哈希算法可以将目标串和模式串转换为哈希值,然后比较哈希值是否相等来判断是否匹配。由于比较哈希值的时间复杂度较低,使用字符串哈希算法可以提高匹配效率。 总的来说,字符串匹配算法字符串哈希算法都是用来处理字符串匹配的问KMP算法通过利用已知信息来减少比较次数,提高匹配效率;而字符串哈希算法则是将字符串转化为哈希值,便于进行比较和存储。两者都在一定程度上提高了字符串匹配的效率。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明仔爱编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值