概要
文字列マッチング (検索) は文字列に対する基本的な操作です。一致するクエリを持つテキスト文字列 S とターゲット部分文字列 T が与えられた場合、T はパターン文字列とも呼ばれます。パターン T に一致するテキスト S 内の部分文字列を検索し、テキスト内の部分文字列の位置を返します。
ブルートフォースマッチング
ブルート フォース アルゴリズム。単純な文字列マッチング アルゴリズムとも呼ばれます。
基本的な考え方は、文字を 1 つずつ比較することです。
- 2 つの文字列 S と T の最初の文字が同じである場合は、2 番目の文字を比較し、同じである場合は続行します。
- いずれかの文字が異なる場合、T 文字列は 1 ビット後方にシフトされ、S 文字列の 2 番目の文字が 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 アルゴリズムはテキスト文字列を 1 回検索するだけで済み、その時間計算量は ですO(n)
。
KMP アルゴリズムの鍵は、次の配列を見つけることです。次の配列の長さはパターン文字列の長さです。次の配列の各値は、パターン文字列の現在の文字の前にある文字列内の同じプレフィックスとサフィックスの長さを表します。
原理
暴力的なマッチング:
文字列 A と B が与えられた場合、B が A の部分文字列であるかどうかを判断します。暴力的なマッチング方法: A の最初の文字から開始して、A の最初の文字と B の最初の文字を比較し、それらが同じであるかどうかを確認し、同じであれば、A の 2 番目の文字と B の 2 番目の文字を比較します。異なる場合は、A の 2 番目の文字から開始して、B の最初の文字と比較します。等々。これは、B を 1 つずつ右に移動することと同じです。
n-1
ブルート フォース マッチングは効率が低く、特にB の最初の文字は正常に一致するが、最後の文字が一致しない場合、または部分文字列 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 アルゴリズムを使用していると言われています。このアルゴリズムには坏字符
と の2 つの概念があり好后缀
、文字列は後ろから前に照合されます。一般に、KMP アルゴリズムより 3 ~ 5 倍高速です。
原理
テキスト文字列 S の長さを n、パターン文字列 T の長さを m とすると、BM アルゴリズムの主な特徴は次のとおりです。
- 右から左に比較および一致します (KMP などの一般的な文字列検索アルゴリズムは左から右に一致します)。
- アルゴリズムは、前処理段階と検索段階の 2 つの段階に分かれています。
- 前処理段階の時間と空間の複雑さは両方とも です
O(m+)
。これは文字セットのサイズで、通常は 256 です。 - 検索フェーズの時間計算量は次のとおりです
O(mn)
。 - パターン文字列が非周期的である場合、最悪の場合、アルゴリズムは 3n 文字の比較演算を実行する必要があります。
- このアルゴリズムは最良の場合
O(n/m)
、つまりn/m
1 回の比較のみが必要な場合に達成されます。
BM アルゴリズムは、パターン文字列を左から右に移動し、右から左に比較します。
BM アルゴリズムの本質は、BM(text, pattern)
一致しない場合に BM アルゴリズムが一度に複数の文字をスキップできることです。つまり、検索文字列内の文字を 1 つずつ比較する必要はありませんが、一部の部分はスキップされます。一般に、検索キーワードが長いほど、アルゴリズムは高速になります。その効率は、マッチング試行が失敗するたびに、アルゴリズムがこの情報を使用して可能な限り多くの不一致位置を排除できるという事実から来ています。つまり、検索対象の文字列のいくつかの特性を最大限に活用して、検索手順を高速化します。
BM アルゴリズムには、不良文字シフトと良好なサフィックス シフトという 2 つの並列アルゴリズム (つまり、2 つのヒューリスティック戦略) が含まれています。これら 2 つのアルゴリズムの目的は、パターン文字列を毎回できるだけ右に (つまり、できるだけ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)
。
原理
日曜日のアルゴリズムは KMP アルゴリズムと同じで、前から後ろに照合します。一致に失敗した場合は、一致に関与するテキスト文字列の最後の文字の次の文字に注目します。その文字がパターン文字列にない場合は、パターン文字列全体がその文字の後に移動されます。文字がパターン文字列内にある場合は、パターン文字列を右にシフトして、対応する文字を揃えます。
Sunday アルゴリズムは、前から後ろに照合するという点で BM アルゴリズムとは少し異なります。照合が失敗した場合、照合に参加したメイン文字列の最後の文字の次の文字に焦点を当てます。
- 文字がパターン文字列に現れない場合、その文字は直接スキップされます。つまり、移動桁数 = パターン文字列の長さ + 1。
- それ以外の場合、シフト桁数 = パターン文字列の長さ - 文字の右端の位置 (0 から始まる) = パターン文字列の文字の右端から末尾までの距離 + 1。
日曜日のアルゴリズムの例を示します。ここで、メイン文字列substring searching
内のパターン文字列を見つけたいとしますsearch
。
- 最初に、パターン文字列をテキスト文字列の左側に配置します。
- 2 番目の文字で不一致が見つかったことがわかります。不一致がある場合は、一致に関与するメイン文字列の最後の文字の次の文字、つまり太字に注目します。パターン文字列の検索、パターン文字列は
i
直接i
ジャンプします。大きな領域の後で、桁数を右に移動 = 一致文字列長 + 1 = 6 + 1 = 7 し、次の一致ステップを i( の後の文字から開始します)つまり、文字 n):
- 結果として、最初の文字は一致しません。次に、メイン文字列で最後に一致した文字の次の文字を調べます。この文字は
r
パターン文字列の最後から 3 番目の文字にあるため、パターン文字列は次のように右に移動されます。 3 ビット (m - 3 = 6 - 3 = r からパターン文字列の終わりまでの距離 + 1 = 2 + 1 =3)、次の 2 つを整列させます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
テスト結果は参考用です。