データ構造とアルゴリズム
第十一回
(2014年12月5日)
文字列照合のアルゴリズム
http://www.sw.it.aoyama.ac.jp/2014/DA/lecture11.html
Martin J. Dürst
© 2009-14 Martin
J. Dürst 青山学院大学
目次
- 前回の残り・まとめ
- 文字列照合の概要
- 素朴な実装
- Rabin-Karp のアルゴリズム
- Knuth-Morris-Pratt のアルゴリズム
- Boyer-Moore のアルゴリズム
- まとめ
前回の残り
前回のまとめ
- ハッシュ法ではハッシュ関数を使用
- 結果として、辞書の検索、挿入、削除が全て
O(1) で可能
- 激突の対応はチェイン法や開番地法
- Ruby
などのプログラム言語でハッシュは非常に便利なデータ構造
- Ruby 内のハッシュの実装はチェイン法を使用
体験のチャンス
- 視線追跡装置と研究実験の体験
- 新機能の実験への貢献
- O棟5階 O-527号室、今日14:00から順次可能
- ふるってご参加ください
文字列照合の概要
長い文書の中に短いパターンを発見
- 長さ n の文字列「文書」(text t)
- 長さ m の文字列「パターン」(pattern
p)
- 文書の中にパターンを探索 (有無・場所・数)
- 場所は普通 shift という
- shift s の t の部分文字列を
ts と書く
- 文字列 t の s 個目の文字を
t[s] と書く
背景: 様々なプログラム言語の文字列照合アルゴリズム
...paternpattnetrnternapatternatnetnpttepneretanetpat...
文字列照合の状況
- n と m の関係 (一般には m ≪
n)
- 検索の回数・パターンの数
- 文字の数 (アルファベットの大きさ、b)
- ビット列: b = 2
- 遺伝子: b = 4 (ヌクレオチド) 又は b
≅ 20 (蛋白質)
- 欧米などの文書: b ≅ 26~256
- 東アジア: b ≅ 数千
素朴な実装
- 文書内の全ての長さ m
の部分文字列をパターンと比較
- 部分列の数が n-m+1 = O(n)
- 部分列一つをパターンと比較するのは O(m)
- 超単純な実装は必ず O(n) · O(m) = O(nm)
- 比較を速い段階で打ち切ると:
- 一般の場合、計算量は O(n) に近い
- 最悪の場合、計算量は O(nm)
実例: t = aaa....aaaab, p = aa..ab
Rabin-Karp のアルゴリズムの概要
- ハッシュ関数を使用
- 文書の全ての長さ m
の部分列のハッシュ値を計算
- パターンのハッシュ値と比較
- ハッシュ値が一致の場合、実体で確認
- 単純な実装は O(nm)
- 工夫によって改善可能
ハッシュ関数の選択
- hf(ts+1) を
hf(ts)
から簡単に計算可能なハッシュ関数を採用
- パターンのハッシュ値は hf (p) =
(p[0]·bm-1+
p[1]·bm-2+...+p[m-2]·b1+p[m-1]·b0)
mod d
(b はアルファベットの基数、d
は適切に選んだ法)
- 文書内の候補のハッシュ値は
hf (ts) =
(t[s]·bm-1+t[s+1]·bm-2+...+t[s+m-2]·b1
+t[s+m-1]·b0)
mod d
hf (ts+1)
=
(t[s+1]·bm-1+t[s+2]·bm-2+...
+t[s+m-1]·b1+t[s+m]·b0)
mod d
ハッシュ関数の高速化
- mod 演算の性質 (情報数学 I の合同算術を参照)
の利用で
hf (ts+1) = ((hf
(ts) - t[s]·bm-1) ·
b + t[s+m]) mod d
= ((hf (ts) -
t[s]·(bm-1 mod
d)) · b + t[s+m])
mod d
- hf(ts+1) は
hf(ts) から O(1) で計算可能のため、全体は
O(m+n)
Rabin-Karp のアルゴリズムの実例
パターン: 081205
文書: 28498608120598743297
(手作業の場合、九去法が便利)
Excell による Rabin-Karp のアルゴリズムの例: BRabinKarp.xls
Rabin-Karp のアルゴリズムの実装: Bstringmatch.rb
Knuth-Morris-Pratt のアルゴリズムの概要
- 素朴な実装だと同じ文字が何回も比較対象
- 基本的なアイディア:
今までの比較の知識を活用
- パターン内の比較によって、パターンの移動の距離を事前に算出
Knuth-Morris-Pratt のアルゴリズムの詳細
- 文書の現在地とパターンの現在地を比較
- 一致する間、s を変更無に比較を継続
- パターンの最後まで一致の場合、照合が成功
- 一致しない場合、
- パターンの先頭の場合、s
を一つ増やす
- それ以外、パターンの現在地を事前計算通り逆戻し
(パターンの移動、文書内の現在地はそのまま)
Knuth-Morris-Pratt のアルゴリズムの計算量
- 比較の結果、次の二つの内一つが起きる:
- パターンを (一文字以上) 右へ移動
(最大 n-m 回)
- 比較の対象を一文字右へ移動
(最大 n 回)
- 合計の回数はおよそ 2n で、計算量は O(n)
- 準備以外、計算量は m に依存しない
- 利点: 文字列を完全に左から右へ探索
Knuth-Morris-Pratt のアルゴリズムの事前計算
- すべての x において
- p[x] が一致しない場合
- p[0] ... p[x-1] (長さ x)
が既に一致
- 一致する部分の一致する最長の真の prefix と suffix
の長さは
- パターンの次の現在地と一致
- 更に、次の現在地でパターンの文字が同じでしたら結果も同じなので省略可能
- パターンの先頭のことを -1 で表現
Knuth-Morris-Pratt のアルゴリズムの実装: Bstringmatch.rb
Boyer-Moore のアルゴリズムの概要
- 比較はパターンの末尾から
- 比較される文書の文字の値を考慮
- パターンの移動の間隔を拡大
- 具体例:
パターンの最後の文字が文書と一致しない、かつ文書の文字がパターンに出現しない場合、パターンが一気に
m 文字移動可能
アイディアの詳細
パターンの移動に二つの「目安」を使用:
- パターンの内部比較
(Knuth-Morris-Pratt の「逆方向版」)
- 不一致の文書の文字のパターン内の最も右の位置
どちらかシフトが大きい方を使用
Boyer-Moore のアルゴリズムの実例
Boyer-Moore のアルゴリズムの計算量
- 最悪の場合、Knuth-Morris-Pratt と同等で O(n)
- m が b に比べ比較的小さい場合、
多くの場合 O(n/m)
文字列照合と文字コード
- 文字コードによって文字の数が多い
- 実装は文字ごとよりもバイトごとが簡単
- 一部の文字コードではバイトごとの実装が不可能
- 可能: UTF-8
- 不可能:
iso-2022-jp
(JIS), Shift_JIS
(SJIS), EUC-JP
(EUC)
文字コードごとのバイトパターン
- UTF-8: 0xxxxxxx, 110xxxxx 10xxxxxx, 1110xxxx 10xxxxxx 10xxxxxx, 11110xxx
10xxxxxx 10xxxxxx 10xxxxxx
- EUC-JP: 0xxxxxxx, 1xxxxxxx 1xxxxxxx
- Shift_JIS: 0xxxxxxx, 1xxxxxxx xxxxxxxx
- iso-2022-jp: 0xxxxxxx, 0xxxxxxx 0xxxxxxx
来年度への展望
- プログラムなどの字句解析は高度な文字列照合
- パターンは固定の文字列いがいも存在
- 有限オートマトンが使用可能
- 正規表現で指定可能
- これは
3年前期の「言語理論とコンパイラ」のテーマ
- Ruby
など多くのプログラム言語でも正規表現が使用可能
まとめ
- 文字列照合の単純な実装は最悪で
O(nm)
- Rabin-Karp のアルゴリズムは O(n)
で、ハッシュ関数の使用で2次元にも使用可能
- Knuth-Morris-Pratt のアルゴリズムは O(n)
で、入力の順にしか文書を見ない
- Boyer-Moore のアルゴリズムは多くの場合
O(n/m)
宿題: