【面试】字符串算法

一、具体

要求:理解知道,一般不要求白板代码书写。

1、暴力求解( O ( M N ) O(MN) O(MN)

// Java
public static int forceSearch(String txt, String pat) {
    
    
    int M = txt.length();
    int N = pat.length();

    for (int i = 0; i <= M - N; i++) {
    
    
        int j;
        for (j = 0; j < N; j++) {
    
    
            if (txt.charAt(i + j) != pat.charAt(j))
                break;
        }
        if (j == N) {
    
    
            return i;
        }
        // 更加聪明? 
        // 1. 预先判断 hash(txt.substring(i, M)) == hash(pat)
        // 2. KMP 
    }
    return -1;
}

2、Rabin-Karp算法

// Java
public final static int D = 256;
public final static int Q = 9997;

static int RabinKarpSerach(String txt, String pat) {
    
    
    int M = pat.length();
    int N = txt.length();
    int i, j;
    int patHash = 0, txtHash = 0;

    for (i = 0; i < M; i++) {
    
    
        patHash = (D * patHash + pat.charAt(i)) % Q;
        txtHash = (D * txtHash + txt.charAt(i)) % Q;
    }
    int highestPow = 1;  // pow(256, M-1)
    for (i = 0; i < M - 1; i++) 
        highestPow = (highestPow * D) % Q;

    for (i = 0; i <= N - M; i++) {
    
     // 枚举起点
        if (patHash == txtHash) {
    
    
            for (j = 0; j < M; j++) {
    
    
                if (txt.charAt(i + j) != pat.charAt(j))
                    break;
            }
            if (j == M)
                return i;
        }
        if (i < N - M) {
    
    
            txtHash = (D * (txtHash - txt.charAt(i) * highestPow) + txt.charAt(i + M)) % Q;
            if (txtHash < 0)
                txtHash += Q;
        }
    }

    return -1;
}

3、KMP

具体细节参考1 & 2 & 3 中的视频 & 文章。

3.1、核心概念

  • 前缀: 除最后一个字符外,一个字符串的全部头部组合;

  • 后缀: 除第一个字符外,一个字符串的全部尾部组合。

    举例: 字符串S = “ABCDAB”, 前缀为[A, AB, ABC, ABCD, ABCDA],后缀为[BCDAB, CDAB, DAB, AB, B],共有元素为"AB",长度为2。

3.2、KMP处理过程

1:prefix table

一个字符串,求所有字串的前后缀的最长公共元素的长度即为所得。
前缀表

将求出来的公共长度整体后移一位,首位置 -1。
过程1
2、字符串匹配

Step 1: 从i=0, j=0开始,逐个对比是否相等。
过程1.1
Step2:当遇到不相等的时候,先看prefix表下面对应数字,然后指针移到pattern字符串对应下标。等价效果pattern字符串整体后移。
process
Step3:移动后的效果如下所示。
result

代码:

// C V2.0
#include<stdio.h>
#include<stdlib.h>
#include<string.h>

void prefix_table(char pattern[], int prefix[], int n) {
    
    
	prefix[0] = 0;
	int len = 0;
	int i = 1;
	while (i<n) {
    
    
		if ( pattern[i]==pattern[len] ) {
    
      // ABA --> ABAB, 最长公共前缀长度由1 --> 2
			len++;
			prefix[i] = len;
			i++;
		}
		else {
    
    
			if (len > 0) {
    
      // 防止越界,
				// ABABCABA --> ABABCABAA。关键步骤,补充理解见下。
				len = prefix[len-1];
			}
			else {
    
      // pattern[i]!=pattern[len] && len==0, 防止死循环 A --> AB, 0 --> -1, 此时i = 0
				prefix[i] = len;
				i++;
			}
		}
	}
}

void move_prefix_table(int prefix[], int n) {
    
    
	int i;
	for (i=n-1; i>0; i--) {
    
    
		prefix[i] = prefix[i-1];
	}
	prefix[0] = -1;
}

void kmp_search(char text[], char pattern[]) {
    
    
	int n = strlen(pattern);
	int m = strlen(text);
	int* prefix = malloc(sizeof(int) * n);
	prefix_table(pattern, prefix, n);
	move_prefix_table(prefix, n);
	
	// text[i]    , len(text)    = m 
	// pattern[i] , len(pattern) = n 
	
	int i = 0;
	int j = 0;
	while (i < m) {
    
    
		if (j==n-1 && text[i] == pattern[j]) {
    
    
			printf("Found pattern at %d\n", i-j);
			j = prefix[j];
		}
		if (text[i]==pattern[j]) {
    
    
			i++; j++;
		}
		else {
    
    
			j = prefix[j];  // 关键过程可见上图
			if (j == -1) {
    
    
				i++; j++;
			}
		}
	}
}
/**/
int main() {
    
    
	/**/
	char pattern[] = "ABABCABAA";
	char text[]    = "ABABABCABAABABABAB";
	kmp_search(text, pattern);

	/*
	char pattern[] = "ABABCABAA";
	int prefix[9];
	int n = 9;
	prefix_table(pattern, prefix, n);
	move_prefix_table(prefix, n);
	int i;
	for (i=0; i<n; i++) {
		printf("%d\n", prefix[i]);
	}
    */
	
	return 0;
}

时间复杂度:O(m+n),最差也可能退化为O(mn),如 {aaaaaaaab, ab}
空间复杂度:O(1)

  • 补充理解:如何快速构建next数组

关键点: pattern字符串自己与自己匹配。
next[i] 定义: P[0]~P[i] 这一段字符串,前缀与后缀的最大公共字串的长度,即使得k-前缀等于k-后缀的最大的k.

关键步骤理解: len = prefix[len-1], why ?(解析来自参考7)

目标:现在 index = 12, 求next数组最后一位的k值,即 next[last] = ? 如何求next[i+1]

  • 已知信息:
  1. 现在字串A与字串B相同,next[last-1] = String_A.length = String_B.length=5.
  2. 前半段中,next[now-1] = {a,b}.length = 2,2是子串A前后缀的最大公共字符串的长度。
  • 分析:
  1. 若 P[now] == P[x],直接在前一位基础上+1,变成5+1=6即可。
  2. 但 P[now] != P[x],那么next[last]=next[now]=0 ? 明显不是。
  • 求next[last] = ? 需要知道 P[0]~P[x-1] 的公共前后缀。

字串A=字串B,P[now] != P[x],即已经知道前后缀最大公共字符–串A or B不满足,我们需要缩短公共字符串,前后缀也需要进一步缩短。

继续分析,下一步迭代(缩短)的前缀一定落在串A里面、后缀一定落在串B里面。串A == 串B,所以就是求串A的前后缀的最大公共长度,即next[now-1]。

now跳转,具体值:now = next[now-1],再比较 p[now] == p[x] ?相等,则 next[last] = now+1; 否则now = next[now-1],继续循环。
result

二、参考

1、KMP字符串匹配算法1
2、KMP字符串匹配算法2
3、字符串匹配的KMP算法
4、字符串匹配之KMP、BoyerMoore、Sunday算法
5、字符串匹配暴力法代码示例
6、Rabin-Karp 代码示例
7、如何更好地理解和掌握 KMP 算法?

猜你喜欢

转载自blog.csdn.net/HeavenDan/article/details/109165811