indexOfを使って文字列中の単語の出現箇所をすべて見つける

indexOfを使用して文字列内の単語のすべての出現を検索する

1. 概要

大きなテキスト文字列で文字のパターンまたは単語を検索する雑用は、さまざまなフィールドで行われます。 たとえば、バイオインフォマティクスでは、染色体でDNAスニペットを見つける必要がある場合があります。

メディアでは、編集者が大量のテキストで特定のフレーズを見つけます。 データ監視は、データに埋め込まれた疑わしい単語を探すことで詐欺やスパムを検出します。

どのような状況でも、検索は非常によく知られており、面倒な作業であるため、一般にthe “Needle in a Haystack Problem”と呼ばれます。 このチュートリアルでは、JavaStringクラスのindexOf(String str, int fromIndex) メソッドを使用して、文字列内の単語のすべての出現箇所を検索する簡単なアルゴリズムを示します。

2. シンプルなアルゴリズム

より大きなテキスト内の単語の出現を単純にカウントする代わりに、このアルゴリズムはテキスト内の特定の単語が存在するすべての場所を見つけて識別します。 この問題に対する私たちのアプローチは短くてシンプルなので、次のようになります。

  1. 検索will find the word even within words in the text。 したがって、「able」という単語を検索すると、「comfortable」と「tablet」で検索されます。

  2. 検索will be case-insensitive

  3. アルゴリズムis based on the naïve string search approach。 つまり、単語とテキスト文字列の文字の性質についてはナイーブなので、ブルートフォースを使用して、検索単語のインスタンスについてテキストのすべての場所をチェックします。

2.1. 実装

検索のパラメータを定義したので、簡単な解決策を書いてみましょう。

public class WordIndexer {

    public List findWord(String textString, String word) {
        List indexes = new ArrayList();
        String lowerCaseTextString = textString.toLowerCase();
        String lowerCaseWord = word.toLowerCase();

        int index = 0;
        while(index != -1){
            index = lowerCaseTextString.indexOf(lowerCaseWord, index);
            if (index != -1) {
                indexes.add(index);
                index++;
            }
        }
        return indexes;
    }
}

2.2. ソリューションのテスト

アルゴリズムをテストするために、シェイクスピアのハムレットの有名な一節のスニペットを使用して、5回出現する「または」という単語を検索します。

@Test
public void givenWord_whenSearching_thenFindAllIndexedLocations() {
    String theString;
    WordIndexer wordIndexer = new WordIndexer();

    theString = "To be, or not to be: that is the question: "
      + "Whether 'tis nobler in the mind to suffer "
      + "The slings and arrows of outrageous fortune, "
      + "Or to take arms against a sea of troubles, "
      + "And by opposing end them? To die: to sleep; "
      + "No more; and by a sleep to say we end "
      + "The heart-ache and the thousand natural shocks "
      + "That flesh is heir to, 'tis a consummation "
      + "Devoutly to be wish'd. To die, to sleep; "
      + "To sleep: perchance to dream: ay, there's the rub: "
      + "For in that sleep of death what dreams may come,";

    List expectedResult = Arrays.asList(7, 122, 130, 221, 438);
    List actualResult = wordIndexer.findWord(theString, "or");
    assertEquals(expectedResult, actualResult);
}

テストを実行すると、期待される結果が得られます。 Searching for “or” yields five instances embedded in various ways in the text string:

index of 7, in "or"
index of 122, in "fortune"
index of 130, in "Or
index of 221, in "more"
index of 438, in "For"

数学的には、アルゴリズムにはO(m*(n-m))のBig-O表記があります。ここで、mは単語の長さ、nはテキスト文字列の長さです。 このアプローチは、数千文字のhaystackテキスト文字列に適している場合がありますが、数十億の文字がある場合は非常に遅くなります。

3. 改善されたアルゴリズム

上記の簡単な例は、テキスト文字列内の特定の単語を検索するための単純なアプローチを示しています。 そのため、すべての検索語およびテキストに対して機能します。

検索ワードに「aaa」などの繰り返しパターンの文字が含まれていないことが事前にわかっている場合は、もう少し効率的なアルゴリズムを作成できます。

この場合、バックアップを実行してテキスト文字列内のすべての場所を潜在的な開始場所として再確認することを安全に回避できます。 indexOf( )メソッドを呼び出した後、見つかった最新のオカレンスの終了直後の場所にスライドします。 この単純な調整により、O(n)の最良のシナリオが得られます。

以前のfindWord( )メソッドのこの拡張バージョンを見てみましょう。

public List findWordUpgrade(String textString, String word) {
    List indexes = new ArrayList();
    StringBuilder output = new StringBuilder();
    String lowerCaseTextString = textString.toLowerCase();
    String lowerCaseWord = word.toLowerCase();
    int wordLength = 0;

    int index = 0;
    while(index != -1){
        index = lowerCaseTextString.indexOf(lowerCaseWord, index + wordLength);  // Slight improvement
        if (index != -1) {
            indexes.add(index);
        }
        wordLength = word.length();
    }
    return indexes;
}

4. 結論

このチュートリアルでは、大文字と小文字を区別しない検索アルゴリズムを提示して、より大きなテキスト文字列内の単語のすべてのバリエーションを見つけました。 ただし、JavaStringクラスのindexOf()メソッドは本質的に大文字と小文字を区別し、たとえば「Bob」と「bob」を区別できるという事実を隠さないでください。

全体として、indexOf()は、部分文字列操作のコーディングを行わずに、テキスト文字列に埋め込まれている文字シーケンスを見つけるための便利な方法です。

いつものように、この例の完全なコードベースはover on GitHubです。