Sapporo.cpp 第8回勉強会(2014.12.27)
その文字列検索、
std::string::findだけで
大丈夫ですか?
H.Hiro
Twitter: @h_hiro_
http://hhiro.net/about/
自己紹介
H.Hiro
●
情報系の研究員
やってます
●
趣味でもプログラム書いてます
●
でも最近は趣味ではあまり
プログラム書けてないのです
告知
第35回 北海道開発オフ
●
みんなで集まって、だけど
思い思いに開発したり勉強したり
●
でもはかどるんです
●
1月17日(土) 9:00~16:00
http://devdo.doorkeeper.jp/
よろしく
お願いします
今回話す内容
文字列を
検索する
C++ Advent Calendar 2014に
書いた記事の拡大版です
http://qiita.com/h_hiro_/
items/dcad2e2eddcb42671d9d
具体的には
BANNANABANANAN
BANANApattern:
text:
"テキスト"から"パターン"が
出現する場所を見つけたい
具体的には
BANNANABANANAN
BANANApattern:
text:
今の場合だとここが出現位置。
(0起点での)7文字目から
始まる場所
文字列データに
対する
最も基本的な
処理の一つ
今回のテーマ
●
C++には、標準で
std::stringにfind関数があって
文字列検索が行える
●
ただ、それは非常に素朴な方法
●
文字数が増えても、(ある程度)
高速に検索したい
実際、
文字数が増えると
「高速に検索できる」
ことの価値が上がる
Web検索エンジンは
その最たる例
今回は、そんな
バリバリの実装の話は
しませんが
何に注目して
高速化を図って
いるのかという
アイデアを紹介します
予告しておくと
(1) パターン前処理型
(2) 索引型
では、最初に
基本となる検索
基本的な
文字列検索
std::string::find
std::string::findの使い方
std::string text =
"BANNANABANANAN";
std::string pattern =
"BANANA";
text.find(pattern);
// "7"を返す
std::string::findの検索手順
BANNANABANANAN
BANANApattern:
text:
まず、パターンを左端に合わせて
std::string::findの検索手順
BANNANABANANAN
BANANApattern:
text:
まず、パターンを左端に合わせて
パターンの末尾まで一致して
いるか調べる
std::string::findの検索手順
BANNANABANANAN
BANANApattern:
text:
一致していない文字が一つでも
あれば、左端を一つずらし
std::string::findの検索手順
BANNANABANANAN
BANANApattern:
text:
一致していない文字が一つでも
あれば、左端を一つずらし
同様に調べていく
std::string::findの検索手順
BANNANABANANAN
BANANApattern:
text:
全部一致している箇所が
見つかったら、それを結果として
出力する
まとめるとこんな具合になる
text BANNANABANANAN
pattern BANA
B
B ※赤文字:
B 間違っていた文字
B
B
B
BANANA
まとめるとこんな具合になる
text BANNANABANANAN
pattern BANA
B
B
B
B
B
B
BANANA
判定する起点(左端)が
1文字ずつ動いている
まとめるとこんな具合になる
text BANNANABANANAN
pattern BANA
B
B
B
B
B
B
BANANA
→もっと多い文字数
動かせるか?
判定する起点(左端)が
1文字ずつ動いている
高速化の手段(1)
パターンを前処理する
代表的なものが
二つあるので
うち一つを紹介します
前処理つきの検索(Knuth-Morris-Pratt)
text BANNANABANANAN
pattern BANANA
まず、パターンを全部見て、
パターンの先頭から■文字が
パターンの他の位置にも出現するか調べる
●
BANANA → ■にかかわらず出現しない
●
CACAO
→ ■が1か2なら、3文字目に出現する
→ ■が3以上なら、出現しない
前処理つきの検索(Knuth-Morris-Pratt)
text BANNANABANANAN
pattern BANA
さて、さっきと同様
“A”が違っていたことが
わかったときに
前処理つきの検索(Knuth-Morris-Pratt)
text BANNANABANANAN
pattern BANA
B
さっきの例では
左端を一つずらして
検索を再開していたのだが
前処理つきの検索(Knuth-Morris-Pratt)
text BANNANABANANAN
pattern BANA
B
パターン中に“B”が先頭以外には
ないことを事前に調べていれば、
次に調べ始める場所は、ここまで動かせる。
→左端を1文字よりも大きく動かせた!
前処理つきの検索(Knuth-Morris-Pratt)
text BANNANABANANAN
pattern BANA
B
B
B
B
BANANA
パターンを前処理する検索
Knuth-Morris-Pratt
●
パターンの先頭と同じ文字列が、パターンの
別の位置に出現するかを利用
例:BANBAABAN
●
最悪時間計算量は低いが、実用上はBMがより高速
Boyer-Moore
●
パターンとテキストの文字が一致していなかった
とき、パターンをテキスト側の文字に合わせる
●
詳しくはQiitaの記事を
使ってみる
これらの検索アルゴリズムは
Boostに入っている
●
boost::algorithm::knuth_morris_pratt
(パターンを前処理した結果のクラス)とか
boost::algorithm::knuth_morris_pratt_search
(単に検索を1回行うための関数)とか
●
ここにコード貼っても長くなりすぎるので
Qiitaの記事中のサンプルをご覧ください
注意点(1)
パターンを
時間をかけて
前処理するのだから
パターンがある程度
長いときに効果を発揮する
●
逆に、短いときは逆効果だったり
●
パターンの長さが100くらいだと
単にfindしたほうが速かった
http://qiita.com/h_hiro_/items/
dcad2e2eddcb42671d9d
#%E5%AE%9F%E9%A8%93
注意点(2)
ここまで
パターンを前処理して
がんばって
きたわけだけど
どちらにせよ
計算時間を決める
最大の要素が
どちらにせよ
計算時間を決める
最大の要素が
テキストの大きさ
どちらにせよ
計算時間を決める
最大の要素が
テキストの大きさ
→大規模DBには厳しい
それなら、
前処理が必要なのは
それなら、
前処理が必要なのは
パターンよりもむしろ
テキストだ!
高速化の手段(2)
索引を付与する
(テキストを前処理)
索引の方式1:
単語ごとに保存して候補を絞り込む
1.C++11がようやく出た。
2.C++11が出たと思ったらもうC++14が出る。
3.C++17はすぐ出るんだろうか。
単語 出現した文章のID
C++ 1, 2, 3
出た 1, 2
出る 2, 3
単語 出現した文章のID
11 1, 2
14 2
17 3
“inverted index” (転置インデックス)と呼ばれる
索引の方式1:
単語ごとに保存して候補を絞り込む
1.C++11がようやく出た。
2.C++11が出たと思ったらもうC++14が出る。
3.C++17はすぐ出るんだろうか。
単語 出現した文章のID
C++ 1, 2, 3
出た 1, 2
出る 2, 3
単語 出現した文章のID
11 1, 2
14 2
17 3
「C++11が出た」を検索する場合、
索引の方式1:
単語ごとに保存して候補を絞り込む
1.C++11がようやく出た。
2.C++11が出たと思ったらもうC++14が出る。
3.C++17はすぐ出るんだろうか。
単語 出現した文章のID
C++ 1, 2, 3
出た 1, 2
出る 2, 3
単語 出現した文章のID
11 1, 2
14 2
17 3
IDだけに注目すると、3は候補から外れることがわかる!
索引の方式1:
単語ごとに保存する
利点:
単語単位に区切っているので
意図した結果が出やすい
欠点:
単語の区切りに沿わないものを
抽出できない
欠点:
単語の区切りに沿わないものを
抽出できない
→対応したければ
 「すべての部分文字列」を
 索引に格納するようにする
索引の方式2:
すべての部分文字列を保存する
C++11が出たと思ったらもうC++14が出る。
(1文字目が起点の部分文字列)
“C”, “C+”, “C++”, “C++1”, “C++11”, ...
(2文字目が起点の部分文字列)
“+”, “++”, “++1”, “++11”, “++11が”, ...
:
メモリ
使いすぎない?
実際は
かなり節約
できます。
索引の方式2:
すべての部分文字列を保存する
1 2 3 4 5 6
P E O P L E
P
E
O
P
L
E
E
L
E
O
P
L
O
P
L
Suffix tree:
●
完全に木構造ですべての
部分文字列を格納
●
検索は超高速(木を順に
辿るだけ)
●
ただしメモリはものすごく
食う(ポインタを
文字数×5以上は使う)
E
E
索引の方式2:
すべての部分文字列を保存する
1 2 3 4 5 6
P E O P L E Suffix array:
●
辞書順で並べて
左端の配列だけ保存
●
容量は小さめ(文字列長×
ポインタサイズ)
●
ただし、suffix treeに
比べると検索のオーバー
ヘッドが大きい
6 E
2 E O P L E
5 L E
3 O P L E
1 P E O P L E
4 P L E
注意点
前半(パターンの前処理)の
ときに言ったこと
パターンを時間をかけて
前処理するのだから
●
パターンがある程度長いときに
効果を発揮する
●
逆に、短いときは逆効果だったり
テキストの前処理だと
テキストを時間をかけて
前処理するのだから
●
テキストがある程度長いときに
効果を発揮する
●
逆に、短いときは逆効果だったり
テキストは、パターンに比べると
とてつもなく長いことも多い
(データベース使って
文書を格納してるとか)
↓
前処理の時間が
ばかにならない!
●
テキストが頻繁に更新される
場合にはあまり向かない
(テキストエディタ内の検索など)
●
索引を作るとすれば、相応の
計算量が必要
●
それ以上に検索の高速化の
意義がある応用に使われる
(文書DB検索など)
おわりに
普段はstd::string::findのように
シンプルに検索してもいいけど
●
パターンを前処理
●
テキストを前処理
も必要に応じて使おう!

その文字列検索、std::string::findだけで大丈夫ですか?【Sapporo.cpp 第8回勉強会(2014.12.27)】