KMP法とBM法は、検索ワードの方に前処理をしました。文字列の方に前処理をすることも考えられそうです。文字列に前処理をするアルゴリズムの1つがN-gram Indexです。
本の中で特定のキーワードを探すとき、皆さんはどうしますか。1ページ目から全部探していくのは大変です。これは力任せ法です。普通は索引を使うと思います。N-gram Indexは、まさに索引をアルゴリズムにしたものだとイメージするとよいでしょう。
まず、最初に文字列の中のすべての文字について、文字ごとに出現位置を計算します。複数回出現する文字も多数あると思いますので、出現位置はリスト構造など複数保持できるようにします。
例えば、「First Fly Filter File」なら以下のような索引ができます。
文字 | 出現位置 | |
---|---|---|
F | 0、6、10、17 | |
i | 1、11、18 | |
r | 2、15 | |
s | 3 | |
t | 4、13 | |
空白 | 5、9、16 | |
l | 7、12 | |
y | 8 | |
e | 14、19 | |
検索するときには、検索ワードの先頭の文字に対応する索引を調べます。その位置から1文字ずつ一致するかどうかを調べます。不一致だったら、次の索引の位置から再度調べます。すべて調べて不一致だったら検索失敗です。
索引は1文字に限る必要はありません。複数文字で索引を作る方法も考えられます。例えば、2文字で先ほどの文字列の索引を作ると以下のようになります。
文字 | 出現位置 | |
---|---|---|
Fi | 0、10、17 | |
ir | 1 | |
st | 2 | |
・・・ | ・・・ | |
3文字以上でも同様です。1文字の場合をUnigram、2文字の場合をBigram、3文字の場合をTrigramと呼びます。
文字数が多くなれば多くなるほど、文字の組み合わせが増えるので、索引に必要なメモリ領域が増えていきます。その半面、検索するときには、索引の時点でより多くマッチするかどうか調べられるので、素早く検索できる可能性が高くなるでしょう。
では、N-gram Indexを実装しましょう。先ほどのプログラムの37行目「末尾3」というコメントの次行以降に下記のプログラムを追加してください。
<script type="text/javascript"> function getNGramHash(text, n) { var hash = []; for (var i = 0; i < text.length+1-n; i++) { var index = text.substring(i, i+n); if (hash[index] == undefined) { hash[index] = []; } hash[index].push(i); } return hash; } function UnigramIndex(text_id) { var n = 1; var text = getStringById(text_id); var index_hash = getNGramHash(text, n); this.find = function(str) { var index_str = str.substring(0,n); var index_item = index_hash[index_str]; for (var i = 0; i < index_item.length; i++) { var index = index_item[i]; var isFound = true; for(var j = 0; j < str.length; j++) { if(str.charAt(j) != text.charAt(index + j)) { isFound = false; break; } } if (isFound) { return index; } } } } var unigram_index_hash = []; function getUnigramIndex(text_id) { return unigram_index_hash[text_id]; } function setUnigramIndex(text_id) { unigram_index_hash[text_id] = new UnigramIndex(text_id); } window.onload = function() { setUnigramIndex('area1'); } function findTextAreaUnigram(text_id, search_id) { var text = getStringById(text_id); var unigram_index = getUnigramIndex(text_id); var search = getStringById(search_id); alert(unigram_index.find(search)); } </script> <input type="button" onClick="findTextAreaUnigram('area1','search');" value="findUnigram">
プログラムを解説します。
2〜10行目
インデックスを作成する関数です。引数nには、何文字でインデックスを作るかを指定します。
4行目
文字列の各文字についてループします。
5行目
文字列からn文字分取得します。
6、7行目
インデックスに文字の位置を格納します。複数値が入るよう、配列にしてpushで値を追加します。
12〜32行目
Unigram Indexのクラスです。特定の文字列1つについてUnigramIndexのインスタンスが1つできます。
text_idを指定してnewすると、インデックスを作成して検索する準備を行います。その後find関数で検索を実行します。
13行目
nはインデックスの文字数です。Unigramなのでn=1です。
14行目
文字列を取得します。
15行目
インデックスを作成します。
17〜31行目
検索する関数です。
18行目
検索ワードからn文字分取得します。
19行目
取得した部分文字列に対応するインデックスを取得します。配列になっています。
20行目
取得したインデックスそれぞれについてループします。
23行目
インデックスの位置から検索ワードと文字列が一致するか1文字ずつ調べます。
29行目
ここでisFoundがtrueのままだったら、全部一致したということでindexの値を返します。
34行目
UnigramIndexオブジェクトを格納する変数です。特定の文字列について、検索するたびにnewすると毎回インデックスを作成することになります。1度インデックスを作成したら、何度作成し直しても同じものになるので、UnigramIndexのインスタンスを保存しておいて使い回します。
35〜37行目
text_idに対応するUnigramIndexオブジェクトを取得する関数です。
39〜41行目
UnigramIndexのインスタンスを生成した保存する関数です。
43〜45行目
ページを表示したときに「area1」に対応するUnigramIndexインスタンスを生成します。
47〜53行目
ボタンを押したときに実行する関数です。
流れが複雑になっていますが、まとめると次のようになります。
以上で文字列探索は終わりです。次回は圧縮アルゴリズムを紹介します。
Copyright © ITmedia, Inc. All Rights Reserved.