データ構造とアルゴリズム
第十回
(2012年12月14日)
文字列照合のアルゴリズム
http://www.sw.it.aoyama.ac.jp/2012/DA/lecture10.html
Martin J. Dürst
© 2008-12 Martin
J. Dürst 青山学院大学
目次
- これからの予定
- 前回のまとめ
- 問題の概要
- 素朴な実装
- Rabin-Karp のアルゴリズム
- Knuth-Morris-Pratt のアルゴリズム
- Boyer-Moore のアルゴリズム
- まとめ
これからの予定
- 12月21日 (金): 11回目の授業
- 1月11日 (金): 12回目の授業
- 1月15日 (火曜日1限、補講):
13回目の授業
- 1月18日 (金): 14回目の授業
- 1月21日~2月2日: 期末試験
補講についての注意
前回のまとめ
- ハッシュ法ではハッシュ関数を使って
- 結果として、辞書の検索、挿入、削除が全て
O(1) で可能
- 激突の場合、チェイン法または開番地法を使用
- Ruby
などのプログラム言語でハッシュは非常に便利なデータ構造
- Ruby 内のハッシュの実装はチェイン法を使用
文字列照合の概要
長い文書の中に短いパターンを見つける
- 長さ n の文字列「文書」(text t)
- 長さ m の文字列「パターン」(pattern
p)
- 文書の中にパターンを探索 (有無・場所・数)
- 場所は普通 shift という
- shift s の t の部分文字列を
ts と書く
- 文字列 t の s 個目の文字を
t[s] と書く
文字列照合の状況
- n と m の関係 (一般には m ≪
n)
- 検索の回数・パターンの数
- 文字の数 (アルファベットの大きさ、b)
- ビット列: b = 2
- 遺伝子: b = 4 (ヌクレオチド) 又は b
≅ 20 (蛋白質)
- 欧米などの文書: b ≅ 26~256
- 東アジア: b ≅ 数千
素朴な実装
- 文書の全ての長さ m
の部分文字列をパターンと比較
- 部分列の数が n-m+1
- 部分列一つをパターンと比較するのは
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 のアルゴリズムの例: ARabinKarp.xls
Rabin-Karp のアルゴリズムの実装: Astringmatch.rb
Knuth-Morris-Pratt のアルゴリズムの概要
- 素朴な実装だと同じ文字が何回も比較対象
- 基本的なアイディア:
今までの比較の知識を活用
- パターン内の比較によって、パターンの移動の距離を事前に算出
Knuth-Morris-Pratt のアルゴリズムの詳細
- 文書の現在地とパターンの現在地
- 一致する間、比較を続く
- パターンの終わりで照合が成功
- 一致しない場合、
- パターンの先頭の場合、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 のアルゴリズムの実装: Astringmatch.rb
Boyer-Moore のアルゴリズムの概要
- 比較はパターンの末尾から
- 比較される文字列の値を考慮
- パターンの移動の間隔を拡大
アイディアの詳細
パターンの移動に二つの「目安」を使用:
- パターンの内部比較
(Knuth-Morris-Pratt の「逆方向版」)
- 不一致の文書の文字のパターン内の最も右の位置
どちらかシフトが大きい方を使用
Boyer-Moore のアルゴリズムの実例
Boyer-Moore のアルゴリズムの計算量
- 最悪の場合、Knuth-Morris-Pratt と同等で
O(n)
- m
がアルファベットの大きさに比べて比較的小さい場合、
多くの場合 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)
宿題: