문자열 일치 문제
1. 문자열 매칭 문제
- 문자열 S와 문자열 T가 주어지면 S에서 T의 위치를 쿼리하는 것은 문자열 일치 문제입니다.
2. 솔루션
2.1 폭력 매칭 알고리즘
2.1.1 알고리즘 단계
- 이중 포인터를 사용하세요. 포인터 i는 순회를 위해 S 문자열을 가리키고, 포인터 j는 순회를 위해 T를 가리킵니다.
- 특정 순간에 S[I]!=T[j]이면 현재 하위 문자열이 일치하지 않으며 일치를 다시 시작해야 함을 의미합니다. 즉, i=i-j+1(새로운 시작 위치) S의 매칭 연산, j=0 (T는 처음부터 매칭);
- j=T.length이면 일치가 성공한 것입니다.
2.1.2 코드 구현
package com.northsmile.string;
/**
* @author NorthSmile
* @version 1.0
* @date 2023/8/22&1:20
* 暴力匹配算法解决字符串匹配问题
*/
public class StrMatch {
public static void main(String[] args) {
String s="abdbcabcdef";
String t="abc";
System.out.println(match(s,t));
}
public static int match(String s,String t){
if (t.length()>s.length()){
return -1;
}
if (s.equals(t)){
return 0;
}
int i=0,j=0;
while (i<s.length()&&j<t.length()){
if (s.charAt(i)==t.charAt(j)){
i++;
j++;
}else{
i=i-j+1;
j=0;
}
}
return j==t.length()?i-j:-1;
}
}
2.2 KMP 알고리즘
- 폭력 매칭 알고리즘의 단점: 매칭 과정에서 매칭이 실패하면 문자열은 현재 매칭 시작점 + 1을 새로운 시작점으로 사용해야 하며, 문자열과 패턴 문자열의 길이가 길면 성능이 상대적으로 낮습니다.
- 문자열 일치에 KMP 알고리즘을 사용하고 문자열의 접두어와 접미어 중 가장 긴 공통 하위 문자열을 사용하여 불필요한 잘못된 일치 작업을 줄여 일치 속도를 향상시킬 수 있습니다.
2.2.1 알고리즘 단계
2.2.2 다음 배열 계산
- 각 위치에 해당하는 접두사와 접미사의 최대 공통 길이를 계산하고 최대 공통 길이 표를 얻습니다.
- 최대 길이를 오른쪽으로 1비트 이동하고 첫 번째 위치를 -1로 채웁니다(첫 번째 위치에서 시작하는 데 0이 필요한 경우 이 항목의 다음 배열 요소에 1을 추가하면 됩니다).
2.2.2 코드 구현
package com.northsmile.string;
import java.util.Arrays;
/**
* @author NorthSmile
* @version 1.0
* @date 2023/8/22&1:20
* KMP算法
* 目的:i不回退,j回退到特定的位置
*/
public class KMP{
public static void main(String[] args) {
String str="abdbcabcdef";
String pattern="abc";
// String pattern="abcababcabc";
// String str="BBC ABCDAB ABCDABCDABDE";
// String pattern="ABCDABD";
// String pattern="AAAB";
System.out.println(Arrays.toString(calNext(pattern)));
System.out.println(match(str,pattern,0));
}
/**
* 从str的pos位置查找pattern
* @param str
* @param pattern
* @param pos
* @return
*/
public static int match(String str,String pattern,int pos){
if (str==null||pattern==null){
return -1;
}
if (pattern.length()>str.length()){
return -1;
}
if (pos<0||pos>=pattern.length()){
return -1;
}
if (str.equals(pattern)){
return 0;
}
int[] next=calNext(pattern);
// i指向文本串,j指向模式串
int i=pos,j=0;
while (i<str.length()&&j<pattern.length()){
// j=-1表示两个串第一个字符就不匹配
if ((j==-1)||str.charAt(i)==pattern.charAt(j)){
i++;
j++;
}else{
// 回退j
j = next[j];
}
}
return j==pattern.length()?i-j:-1;
}
// 字符串对应next数组的计算
public static int[] calNext(String str){
int n=str.length();
int[] next=new int[n];
next[0]=-1;
next[1]=0;
for (int i=2,k=next[1];i<n;i++){
// k=-1表示前缀和后缀没有公共串
if (k==-1||str.charAt(i-1)==str.charAt(k)){
next[i]=k+1;
k=next[i];
}else{
k=next[k];
i--;
}
}
return next;
}
}
3. 실제 질문
3.1 Lituo 28. 문자열에서 첫 번째 일치 항목의 첨자를 찾으세요.
class Solution {
public int strStr(String haystack, String needle) {
return kmp(haystack,needle);
}
public int kmp(String str, String pattern){
if(str==null||pattern==null){
return -1;
}
if(str.length()==0||pattern.length()==0){
return -1;
}
if(pattern.length()>str.length()){
return -1;
}
if(pattern.equals(str)){
return 0;
}
// 计算模式串的next数组
int[] next=getNext(pattern);
// 匹配查找
int i=0,j=0;
while(i<str.length()&&j<pattern.length()){
if(j==-1||str.charAt(i)==pattern.charAt(j)){
i++;
j++;
}else{
// j回退
j=next[j];
}
}
return j==pattern.length()?i-j:-1;
}
public int[] getNext(String str){
int n=str.length();
if(n==1){
return new int[]{
-1};
}
if(n==2){
return new int[]{
-1,0};
}
int[] next=new int[n];
next[0]=-1;
next[1]=0;
// k用于记录i-1位置需要回退的位置
int i=2,k=0;
while(i<n){
if(k==-1||str.charAt(i-1)==str.charAt(k)){
next[i]=k+1;
k++;
i++;
}else{
// k回退
k=next[k];
}
}
return next;
}
}
3.2 Leetour 459. 반복되는 하위 문자열
class Solution {
// 字符串匹配
public boolean repeatedSubstringPattern(String s) {
return kmp(s+s,s,1)!=s.length();
}
public int kmp(String str, String pattern,int pos){
if(str==null||pattern==null){
return -1;
}
if(str.length()==0||pattern.length()==0){
return -1;
}
if(pattern.length()>str.length()){
return -1;
}
if(pattern.equals(str)){
return 0;
}
// 计算模式串的next数组
int[] next=getNext(pattern);
// 匹配查找
int i=pos,j=0;
while(i<str.length()&&j<pattern.length()){
if(j==-1||str.charAt(i)==pattern.charAt(j)){
i++;
j++;
}else{
// j回退
j=next[j];
}
}
return j==pattern.length()?i-j:-1;
}
public int[] getNext(String str){
int n=str.length();
if(n==1){
return new int[]{
-1};
}
if(n==2){
return new int[]{
-1,0};
}
int[] next=new int[n];
next[0]=-1;
next[1]=0;
// k用于记录i-1位置需要回退的位置
int i=2,k=0;
while(i<n){
if(k==-1||str.charAt(i-1)==str.charAt(k)){
next[i]=k+1;
k++;
i++;
}else{
// k回退
k=next[k];
}
}
return next;
}
}
3.3 NC149 kmp 알고리즘
import java.util.*;
public class Solution {
/**
* 代码中的类名、方法名、参数名已经指定,请勿修改,直接返回方法规定的值即可
*
* 计算模板串S在文本串T中出现了多少次
* @param S string字符串 模板串
* @param T string字符串 文本串
* @return int整型
*/
static int count=0;
public int kmp (String S, String T) {
kmp(T,S,0);
return count;
}
public void kmp (String s, String p,int pos) {
if(s==null||p==null){
return;
}
if(s.length()==0||p.length()==0){
return;
}
if(pos<0||pos>=s.length()){
return;
}
int[] next=getNext(p);
int i=pos,j=0;
while(i<s.length()&&j<p.length()){
if(j==-1||s.charAt(i)==p.charAt(j)){
i++;
j++;
}else{
j=next[j];
}
if(j==p.length()){
count++;
j=next[j];
}
}
}
public int[] getNext(String s){
int n=s.length();
if(n==1){
return new int[]{
-1};
}
if(n==2){
return new int[]{
-1,0};
}
int[] next=new int[n+1];
next[0]=-1;
next[1]=0;
int i=2,k=0;
while(i<=n){
if(k==-1||s.charAt(i-1)==s.charAt(k)){
next[i]=k+1;
k++;
i++;
}else{
k=next[k];
}
}
return next;
}
}
3.4 KMP 알고리즘
import java.util.*;
// 注意类名必须为 Main, 不要有任何 package xxx 信息
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
// 注意 hasNext 和 hasNextLine 的区别
while (in.hasNextLine()) {
// 注意 while 处理多个 case
String str = in.nextLine();
String match = in.nextLine();
List<Integer> ans=kmp(str,match,0);
if(ans.size()==0){
System.out.println(-1);
return;
}
for(int i=0;i<ans.size();i++){
System.out.print(ans.get(i));
if(i!=ans.size()-1){
System.out.print(" ");
}else{
System.out.println();
}
}
}
}
public static List<Integer> kmp (String s, String p,int pos) {
List<Integer> ans=new ArrayList<>();
if(s==null||p==null){
return ans;
}
if(s.length()==0||p.length()==0){
return ans;
}
if(pos<0||pos>=s.length()){
return ans;
}
int[] next=getNext(p);
int i=pos,j=0;
while(i<s.length()&&j<p.length()){
if(j==-1||s.charAt(i)==p.charAt(j)){
i++;
j++;
}else{
j=next[j];
}
if(j==p.length()){
ans.add(i-j);
j=next[j];
}
}
return ans;
}
public static int[] getNext(String s){
int n=s.length();
if(n==1){
return new int[]{
-1,0};
}
int[] next=new int[n+1];
next[0]=-1;
next[1]=0;
int i=2,k=0;
while(i<=n){
if(k==-1||s.charAt(i-1)==s.charAt(k)){
next[i]=k+1;
k++;
i++;
}else{
k=next[k];
}
}
return next;
}
}
https://www.zhihu.com/question/21923021/answer/769606119 링크를 참조하세요 . 이 블로거는 다음 배열 계산에 대해 매우 명확합니다!