문자열 검색 및 일치 알고리즘

개요

문자열 일치(검색)는 문자열에 대한 기본 작업입니다. 일치하는 쿼리가 있는 텍스트 문자열 S와 대상 하위 문자열 T가 주어지면 T를 패턴 문자열이라고도 합니다. 패턴 T와 일치하는 텍스트 S의 하위 문자열을 찾고 텍스트에서 하위 문자열의 위치를 ​​반환합니다.

무차별 대입 매칭

무차별 대입 알고리즘(Naive String Matching Algorithm)이라고도 합니다.
기본 아이디어는 문자를 하나씩 비교하는 것입니다.

  • 두 문자열 S와 T의 첫 번째 문자가 동일하면 두 번째 문자를 비교하고 동일하면 계속합니다.
  • 문자 중 하나가 다르면 T 문자열이 한 비트 뒤로 이동하고 S 문자열의 두 번째 문자가 T 문자열의 첫 번째 문자와 다시 비교됩니다.
  • 끝까지 반복해서

성취하다

public static int bf(String text, String pattern) {
    
    
    int m = text.length();
    int n = pattern.length();
    for (int i = 0; i <= m - n; i++) {
    
    
        boolean flag = true;
        for (int j = 0; j < n; j++) {
    
    
            if (text.charAt(i + j) != pattern.charAt(j)) {
    
    
                flag = false;
                break;
            }
        }
        if (flag) {
    
    
            return i;
        }
    }
    return -1;
}

KMP

DEKnuth, JHMorris 및 VRPRatt는 일치에 실패한 후 정보를 사용하여 패턴 문자열과 기본 문자열 간의 일치 횟수를 최소화하여 빠른 일치를 달성하는 향상된 문자열 일치 알고리즘을 발명했습니다. KMP 알고리즘은 텍스트 문자열을 한 번만 검색하면 되며 시간 복잡도는 입니다 O(n).

KMP 알고리즘의 핵심은 다음 배열을 찾는 것입니다. 다음 배열의 길이는 패턴 문자열의 길이입니다. 다음 배열의 각 값은 패턴 문자열에서 현재 문자 앞의 문자열에 있는 동일한 접두사와 접미사의 길이를 나타냅니다.

원칙

폭력적 일치:
문자열 A와 B가 주어지면 B가 A의 하위 문자열인지 확인합니다. 폭력 매칭 방법: A의 첫 번째 문자부터 시작하여 A의 첫 번째 문자와 B의 첫 번째 문자를 비교하여 동일한지 확인하고, 동일하다면 A의 두 번째 문자와 B의 두 번째 문자를 비교합니다. .동일하지 않은 경우 A의 두 번째 문자부터 시작하여 B의 첫 번째 문자와 비교합니다. 등등. 이는 B를 오른쪽으로 한 단계씩 이동하는 것과 같습니다.

무차별 대입 일치는 효율성이 낮습니다. 이는 특히 n-1B의 첫 번째 문자를 성공적으로 일치시킬 수 있지만 마지막 문자가 일치하지 않거나 하위 문자열 B가 긴 경우 단계별 이동에 반영됩니다. KMP는 매칭 테이블의 개념을 활용하여 매칭 효율성을 최적화합니다.


다음과 같이 주어진 문자열 B에 대한 일치 테이블 abcab:

  • 접두사: 마지막 문자를 제외한 모든 연속 조합: a, ab, abc, abca
  • 접미사: 첫 번째 문자를 제외한 모든 연속 조합: bcab, cab, ab, b

일치값: 하위 문자열의 각 문자 조합에 대한 접두사와 접미사를 찾아 동일한 문자 조합이 있는지 비교합니다. 동일한 문자 조합이 몇 개 있습니까? 일치하는 값은 몇 개입니까?

예를 들어, 특정 string 에 대해 abcab일치하는 값 string은 다음과 같습니다 00012.

  • a, 접미사 또는 접미사 없음, 일치 값 = 0
  • ab, 접두사 {a}, 접미사 {b}, 공통 문자 없음, 일치 값 = 0
  • abc, 접두사 {a}{ab}, 접미사 {c}{bc}, 공통 문자 없음, 일치 값 = 0
  • abca, 접두사 {a}{ab}{abc}, 접미사 {a}{ca}{bca}, 일반 문자 {a}, 일치 값=1
  • abcab, 접두사 {a}{ab}{abc}{abca}, 접미사 {b}{ab}{cab}{bcab}, 일반 문자 {ab}, 일치 값 = 2

일치하는 테이블을 기준으로 하나씩 이동할 필요가 없습니다 移动步数 = 成功匹配的位数 - 匹配表里面的匹配值.

성취하다

KMP 알고리즘 구현을 제공합니다.

/**
 * 利用KMP算法求解pattern是否在text中出现过
 *
 * @param text    文本串
 * @param pattern 模式串
 * @return pattern在text中出现,则返回true,否则返回false
 */
public static boolean kmpSearch(String text, String pattern) {
    
    
    // 部分匹配数组
    int[] partMatchTable = kmpNext(pattern);
    // text中的指针
    int i = 0;
    // pattern中的指针
    int j = 0;
    while (i < text.length()) {
    
    
        if (text.charAt(i) == pattern.charAt(j)) {
    
    
            // 字符匹配,则两个指针同时后移
            i++;
            j++;
        } else if (j > 0) {
    
    
            // 字符失配,则利用next数组,移动j指针,避免i指针回退
            j = partMatchTable[j - 1];
        } else {
    
    
            // pattern中的第一个字符就失配
            i++;
        }
        if (j == pattern.length()) {
    
    
            // 搜索成功
            return true;
        }
    }
    return false;
}

private static int[] kmpNext(String pattern) {
    
    
    int[] next = new int[pattern.length()];
    next[0] = 0;
    int j=0;
    for (int i = 1; i < pattern.length(); i++) {
    
    
        while (j > 0 && pattern.charAt(i) != pattern.charAt(j)){
    
    //前后缀相同
            j = next[j - 1];
        }
        if (pattern.charAt(i) == pattern.charAt(j)){
    
    //前后缀不相同
            j++;
        }
        next[i] = j;
    }
    return next;
}

보이어-무어

Boyer-Moore 알고리즘은 실제 응용에서는 KMP 알고리즘보다 더 효율적이며 Linux의 grep 명령을 포함한 다양한 텍스트 편집기의 검색 기능은 모두 Boyer-Moore 알고리즘을 사용한다고 합니다. 이 알고리즘에는 坏字符와 , 두 가지 개념이 있으며 好后缀, 문자열은 뒤에서 앞으로 일치됩니다. 일반적으로 KMP 알고리즘보다 3~5배 빠릅니다.

원칙

텍스트 문자열 S의 길이를 n, 패턴 문자열 T의 길이를 m이라고 가정하면 BM 알고리즘의 주요 특징은 다음과 같습니다.

  • 오른쪽에서 왼쪽으로 비교 및 ​​일치(KMP와 같은 일반 문자열 검색 알고리즘은 왼쪽에서 오른쪽으로 일치).
  • 알고리즘은 전처리 단계와 검색 단계의 두 단계로 나뉩니다.
  • 전처리 단계의 시간 및 공간 복잡도는 모두 O(m+)문자 세트 크기인 256입니다.
  • 검색 단계의 시간 복잡도는 다음과 같습니다 O(mn).
  • 패턴 문자열이 비주기적일 때 최악의 경우 알고리즘은 3n 문자 비교 작업을 수행해야 합니다.
  • 알고리즘은 최상의 경우에 달성됩니다 O(n/m). 즉, 단 n/m한 번의 비교만 필요합니다.

BM 알고리즘은 패턴 문자열을 왼쪽에서 오른쪽으로 이동하고 오른쪽에서 왼쪽으로 비교합니다.

BM 알고리즘의 핵심은 BM(text, pattern)BM 알고리즘이 일치하는 항목이 없을 때 한 번에 두 개 이상의 문자를 건너뛸 수 있다는 것입니다. 즉, 검색된 문자열의 문자를 하나씩 비교할 필요는 없지만 일부 부분을 건너뛰게 됩니다. 일반적으로 검색 키워드가 길수록 알고리즘 속도가 빨라집니다. 효율성은 일치 시도가 실패할 때마다 알고리즘이 이 정보를 사용하여 일치하지 않는 위치를 최대한 많이 제거할 수 있다는 사실에서 비롯됩니다. 즉, 검색할 문자열의 일부 특성을 최대한 활용하여 검색 단계의 속도를 높입니다.

BM 알고리즘에는 잘못된 문자 이동과 좋은 접미사 이동이라는 두 가지 병렬 알고리즘(즉, 두 가지 경험적 전략)이 포함되어 있습니다. 이 두 알고리즘의 목적은 패턴 문자열을 매번 오른쪽으로 최대한 멀리(즉, 최대한 BM()크게) 이동하는 것입니다.

성취하다

/**
 * Boyer-Moore算法是一种基于后缀匹配的模式串匹配算法,后缀匹配就是模式串从右到左开始比较,但模式串的移动还是从左到右的。
 * 字符串匹配的关键就是模式串的如何移动才是最高效的,Boyer-Moore为了做到这点定义了两个规则:坏字符规则和好后缀规则<br>
 * 坏字符规则<bR>
 * 1.如果坏字符没有出现在模式字符中,则直接将模式串移动到坏字符的下一个字符:<br>
 * 2.如果坏字符出现在模式串中,则将模式串最靠近好后缀的坏字符(当然这个实现就有点繁琐)与母串的坏字符对齐:<br>
 * 好后缀规则<bR>
 * 1.模式串中有子串匹配上好后缀,此时移动模式串,让该子串和好后缀对齐即可,如果超过一个子串匹配上好后缀,则选择最靠靠近好后缀的子串对齐。<br>
 * 2.模式串中没有子串匹配上后后缀,此时需要寻找模式串的一个最长前缀,并让该前缀等于好后缀的后缀,寻找到该前缀后,让该前缀和好后缀对齐即可。<br>
 * 3.模式串中没有子串匹配上后后缀,并且在模式串中找不到最长前缀,让该前缀等于好后缀的后缀。此时,直接移动模式到好后缀的下一个字符。<br>
 */
public static List<Integer> bmMatch(String text, String pattern) {
    
    
    List<Integer> matches = new ArrayList<>();
    int m = text.length();
    int n = pattern.length();
    // 生成模式字符串的坏字符移动结果
    Map<Character, Integer> rightMostIndexes = preprocessForBadCharacterShift(pattern);
    // 匹配的节点位置
    int alignedAt = 0;
    // 如果当前节点在可匹配范围内,即当前的A[k]必须在A[0, m-n-1)之间,否则没有必要做匹配
    while (alignedAt + (n - 1) < m) {
    
    
        // 循环模式组,查询模式组是否匹配 从模式串的最后面开始匹配,并逐渐往前匹配
        for (int indexInPattern = n - 1; indexInPattern >= 0; indexInPattern--) {
    
    
            // 1 定义待查询字符串中的当前匹配位置.
            int indexInText = alignedAt + indexInPattern;
            // 2 验证带查询字符串的当前位置是否已经超过最长字符,如果超过,则表示未查询到.
            if (indexInText >= m) {
    
    
                break;
            }
            // 3 获取到带查询字符串和模式字符串中对应的待匹配字符
            char x = text.charAt(indexInText);
            char y = pattern.charAt(indexInPattern);
            // 4 验证结果
            if (x != y) {
    
    
                // 4.1 如果两个字符串不相等,则寻找最坏字符串的结果,生成下次移动的队列位置
                Integer r = rightMostIndexes.get(x);
                if (r == null) {
    
    
                    alignedAt = indexInText + 1;
                } else {
    
    
                    // 当前坏字符串在模式串中存在,则将模式串最靠近好后缀的坏字符与母串的坏字符对齐,shift 实际为模式串总长度
                    int shift = indexInText - (alignedAt + r);
                    alignedAt += shift > 0 ? shift : 1;
                }
                // 退出匹配
                break;
            } else if (indexInPattern == 0) {
    
    
                // 4.2 匹配到的话 并且最终匹配到模式串第一个字符,便是已经找到匹配串,记录下当前的位置
                matches.add(alignedAt);
                alignedAt++;
            }
        }
    }
    return matches;
}

/**
 * 坏字符串
 * 依据待匹配的模式字符串生成一个坏字符串的移动列,该移动列中表明当一个坏字符串出现时,需要移动的位数
 */
private static Map<Character, Integer> preprocessForBadCharacterShift(String pattern) {
    
    
    Map<Character, Integer> map = new HashMap<>();
    for (int i = pattern.length() - 1; i >= 0; i--) {
    
    
        char c = pattern.charAt(i);
        if (!map.containsKey(c)) {
    
    
            map.put(c, i);
        }
    }
    return map;
}

참고: 바이두백과사전

일요일

1990년 Daniel M.Sunday가 제안한 문자열 패턴 일치 알고리즘입니다. 임의의 문자열을 일치시킬 때 다른 일치 알고리즘보다 효율성이 빠릅니다. 평균 시간 복잡도는 이고 O(n), 최악의 경우 시간 복잡도는 입니다 O(n*m).

원칙

Sunday 알고리즘은 KMP 알고리즘과 동일하며 앞에서 뒤로 일치합니다. 일치에 실패하면 일치에 참여한 텍스트 문자열의 마지막 문자 옆에 있는 문자에 초점을 맞추고 해당 문자가 패턴 문자열에 없으면 전체 패턴 문자열이 해당 문자 뒤로 이동됩니다. 해당 문자가 패턴 문자열에 있는 경우 패턴 문자열을 오른쪽으로 이동하여 해당 문자를 정렬합니다.

Sunday 알고리즘은 앞에서 뒤로 일치한다는 점에서 BM 알고리즘과 약간 다르며, 일치에 실패하면 일치에 참여한 주 문자열의 마지막 문자의 다음 문자에 초점을 맞춥니다.

  • 패턴 문자열에 문자가 나타나지 않으면 직접 건너뜁니다. 즉, 이동 자릿수 = 패턴 문자열 길이 + 1입니다.
  • 그렇지 않으면, 이동 숫자의 수 = 패턴 문자열의 길이 - 문자의 가장 오른쪽 위치(0으로 시작) = 패턴 문자열의 문자의 가장 오른쪽 위치에서 꼬리까지의 거리 + 1.

일요일 알고리즘의 예를 들어보세요. 이제 기본 문자열 substring searching에서 패턴 문자열을 찾고 싶다고 가정해 보겠습니다 search.

  • 처음에는 패턴 문자열을 텍스트 문자열의 왼쪽에 정렬합니다.
    여기에 이미지 설명을 삽입하세요.
  • 알고 보니 두 번째 문자에서 불일치가 발견되었는데, 불일치가 있는 경우 일치에 참여한 주 문자열의 마지막 문자 옆에 있는 문자, 즉 굵은 글씨를 주목해 보면 존재하지 않는다. 패턴 문자열 검색, 패턴 문자열이 i직접 i점프 넓은 영역 이후 자릿수를 오른쪽으로 이동 = 일치 문자열 길이 + 1 = 6 + 1 = 7, i 뒤의 문자부터 일치하는 다음 단계 시작 ( 즉, 문자 n):
    여기에 이미지 설명을 삽입하세요.
  • 결과적으로 첫 번째 문자는 일치하지 않습니다.그러다가 메인 문자열에서 마지막으로 일치하는 문자 옆의 문자를 보면 r패턴 문자열에서 마지막에서 세 번째 문자에 나타나므로 패턴 문자열이 오른쪽으로 이동합니다. 3비트(m - 3 = 6 - 3 = r에서 패턴 문자열 끝까지의 거리 + 1 = 2 + 1 =3), 두 가지 정렬 r:
    여기에 이미지 설명을 삽입하세요.
  • 매치가 성공했습니다.

Sunday 알고리즘의 단점: 알고리즘의 핵심은 이동 배열에 의존하고, 이동 배열의 값은 패턴 문자열에 따라 달라지므로 잘못된 이동 배열을 구성하는 패턴 문자열이 있을 수 있습니다.

성취하다

/**
 * sunday 算法
 *
 * @param text    文本串
 * @param pattern 模式串
 * @return 匹配失败返回-1,匹配成功返回文本串的索引(从0开始)
 */
public static int sunday(char[] text, char[] pattern) {
    
    
    int tSize = text.length;
    int pSize = pattern.length;
    int[] move = new int[ASCII_SIZE];
    // 主串参与匹配最末位字符移动到该位需要移动的位数
    for (int i = 0; i < ASCII_SIZE; i++) {
    
    
        move[i] = pSize + 1;
    }
    for (int i = 0; i < pSize; i++) {
    
    
        move[pattern[i]] = pSize - i;
    }
    // 模式串头部在字符串位置
    int s = 0;
    // 模式串已经匹配的长度
    int j;
    // 到达末尾之前
    while (s <= tSize - pSize) {
    
    
        j = 0;
        while (text[s + j] == pattern[j]) {
    
    
            j++;
            if (j >= pSize) {
    
    
                return s;
            }
        }
        s += move[text[s + pSize]];
    }
    return -1;
}

비교됨

이는 단지 예일 뿐이며, 다양한 알고리즘은 다양한 상황에서 일관되지 않게 작동합니다. 검색할 문자열 리터럴 및 패턴 일치 문자열과 관련됩니다.

public static void main(String[] args) {
    
    
    String text = "abcagfacjkackeac";
    String pattern = "ackeac";
    Stopwatch stopwatch = Stopwatch.createStarted();
    int bfRes = bf(text, pattern);
    stopwatch.stop();
    log.info("bf result:{}, take {}ns", bfRes, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.reset();
    stopwatch.start();
    boolean kmpRes = kmpSearch(text, pattern);
    stopwatch.stop();
    log.info("kmp result:{}, take {}ns", kmpRes, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.reset();
    stopwatch.start();
    List<Integer> bmMatch = bmMatch(text, pattern);
    stopwatch.stop();
    log.info("bmMatch result:{}, take {}ns", bmMatch, stopwatch.elapsed(TimeUnit.NANOSECONDS));

    stopwatch.reset();
    stopwatch.start();
    int sunday = sunday(text.toCharArray(), pattern.toCharArray());
    stopwatch.stop();
    log.info("sunday result:{}, take {}ns", sunday, stopwatch.elapsed(TimeUnit.NANOSECONDS));
}

특정 출력 결과:

bf result:10, take 8833ns
kmp result:true, take 4541ns
bmMatch result:[10], take 90500ns
sunday result:10, take 5458ns

테스트 결과는 참고용입니다.

참고

Supongo que te gusta

Origin blog.csdn.net/lonelymanontheway/article/details/128210555
Recomendado
Clasificación