Upload
hiro-h
View
503
Download
3
Embed Size (px)
Citation preview
Sapporo.cpp 第8回勉強会(2014.12.27)
その文字列検索、std::string::findだけで
大丈夫ですか?
H.HiroTwitter: @h_hiro_
http://hhiro.net/about/
自己紹介
H.Hiro●情報系の研究員やってます
●趣味でもプログラム書いてます●でも最近は趣味ではあまりプログラム書けてないのです
告知
第35回 北海道開発オフ●みんなで集まって、だけど思い思いに開発したり勉強したり
●でもはかどるんです●1月17日(土) 9:00~16:00http://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のようにシンプルに検索してもいいけど●パターンを前処理●テキストを前処理も必要に応じて使おう!