(このコンテンツはメールマガジンの STL & iostream 入門に手を加えたものです。「 STL と iostream が使えるかのチェック」等はメールマガジンの方のページをご覧ください)
|
set と multiset ( #22 ) |
今回はコンテナクラスの std::set と std::multiset を紹介します。
このふたつのクラスと、次次回紹介する std::map と std::multimap はこれまでのコンテナクラスとは値の格納方法が違います。この4つのクラスは「2分木」という方法で値を格納します。また、 std::set, std::multiset, std::map, std::multimap の4つのコンテナクラスを「連想コンテナ」と呼びます。 std::vector などは「1列に並んだデータ」をイメージするものでした。 std::set などは「2分木」という規則に則ってデータを格納します。これは「ひとつの値がふたつの枝を持ち、右側の枝に大きい値、左側の枝に小さな値を継なげる形で大きな木を形成する」形式です。つまり「値を挿入する段階で、順序づけによるソートを行う」ということです。 まあとりあえず使ってみましょう。 //////////////////////////////////////////////////////////////// // std::set の使用例。 #pragma warning( disable : 4786 ) // VC での警告を取り除きます。 #include <stdio.h> #include <set> void Use_set() { std::set< int > cSet; cSet.insert( 100 ); cSet.insert( 200 ); cSet.insert( 300 ); std::set< int >::iterator cIter = cSet.find( 200 ); if( cIter != cSet.end() ) printf( "%d\n", *cIter ); } // 結果 200 ///////////////////////////// std::set には push_back() などのメンバ関数がありません。2分木への挿入は必ず大小関係によって「どの枝に挿入するか」が決まるので、位置を指定できないからです。挿入は std::set::insert() を使って値を直接渡します。 std::set が最も役立つのは「検索」です。 std::set は挿入時にすでにソートされているので std::set::find() を使って検索すればアルゴリズムの std::find() よりもずっと速く検索できます。 もうひとつ例を見てみましょう。今度は std::set::lower_bound() と std::set::upper_bound() を使ってみます。 //////////////////////////////////////////////////////////////// // std::set::lower_bound() の使用例。 #pragma warning( disable : 4786 ) // VC での警告を取り除きます。 #include <stdio.h> #include <set> void Use_bound() { std::set< int > cSet; cSet.insert( 500 ); cSet.insert( 200 ); cSet.insert( 300 ); cSet.insert( 100 ); cSet.insert( 400 ); std::set< int >::iterator cIBegin = cSet.lower_bound( 150 ); // 下限を取得。 std::set< int >::iterator cIEnd = cSet.upper_bound( 450 ); // 上限を取得。 for ( std::set< int >::iterator cIter = cIBegin ; cIter != cIEnd ; ++cIter ) printf( "%d, ", *cIter ); } // 結果 200, 300, 400, ///////////////////////////// std::set 内の各要素はソートされた形で格納されています。この中から「ある範囲」を返すメンバ関数が std::set::lower_bound() と std::set::upper_bound() です。このふたつのメンバ関数を呼び出すことで、上の例なら 150 から 450 の間の値を囲むようにイテレーターを取得することができます( std::set::upper_bound() は例によって「ひとつ先の要素」を指します)。 その前の「値の挿入」はバラバラにしていますが、各値は挿入時にはソートされています。なので、この「特定の値の範囲」の中をイテレーターで走査すれば、小さい順に並んだ各要素を順に取得することができます。 std::set のこの性質から、「高速な検索」や「データを順序立てて並べたい」場合に役立つことが分かると思います。 |
|
std::set とほとんど同じ機能を持つコンテナクラスに std::multiset があります。このふたつの違いは「 std::set は同じ要素を複数持てないが、 std::multiset は複数持てる」という点です。たとえば
std::set< int > cSet; cSet.insert( 100 ); cSet.insert( 100 ); としても 100 という要素は1個だけしか挿入されません。ですが std::multiset< int > cMSet; cMSet.insert( 100 ); cMSet.insert( 100 ); なら 100 という要素がふたつ挿入されます。これは今度紹介する std::map と std::multimap の場合には大きい意味を持つのですが、 std::set の場合には「値の重複」のメリットはあまりないと思います。 |
連想コンテナとプレディケート ( #23 ) |
std::set などの連想コンテナには、そのままだと使えないクラスがあります。例えば次の CInt のようなクラスとか。
//////////////////////////////////////////////////////////////// // CInt の間違った使用例。 #include <stdio.h> #include <set> class CInt { public: int m_i; CInt( int p_i ) : m_i( p_i ) {} }; void Use_CInt_fail() { std::set< CInt > cSet; cSet.insert( CInt( 100 ) ); // コンパイルエラー。 } ///////////////////////////// 連想コンテナは、ソートをするため要素の挿入時に「値の比較」を行います。値の比較は、デフォルトでは < 演算子で行うため、 CInt には < 演算子が必要ということになります。 演算子のオーバーロードができない場合には、 #09 で紹介したプレディケートを作成して、連想コンテナと一緒に使用します。 //////////////////////////////////////////////////////////////// // CPredForSet の使用例。 #include <stdio.h> #include <set> class CPredForSet : public std::binary_function< CInt, CInt, bool > { public: bool operator()( const CInt &p_rcIntL, const CInt &p_rcIntR ) const { return p_rcIntL.m_i < p_rcIntR.m_i; } }; // 使用例。 void Use_CPredForSet() { std::set< CInt, CPredForSet > cSet; cSet.insert( CInt( 100 ) ); cSet.insert( CInt( 200 ) ); cSet.insert( CInt( 300 ) ); std::set< CInt, CPredForSet >::iterator cIter = cSet.find( CInt( 200 ) ); if( cIter != cSet.end() ) printf( "%d\n", ( *cIter ).m_i ); } // 結果 200 ///////////////////////////// std::set はテンプレート第2引数に「比較用プレディケート」を受け取ります。デフォルトは std::less というクラスで、このプレディケートはただ < で比較するというものです。そのため、 CInt のようなクラスでは比較できません。 代わりに CPredForSet というクラスを作ってプレディケートとして使用します。この中の operator()() は、ふたつの引数のメンバ変数を < 演算子で比較します。連想コンテナ用のプレディケートは、必ず < や > っぽい比較をする必要があります。 <= のように「値の一致」という結果を返さないプレディケートでないと、2分木がうまく作れないからです。 このプレディケートさえ作ってしまえば、 CInt も連想コンテナに使用できます。ちなみに > 演算子のものを作れば逆にソートできます。 STL でも std::greater() なんてものが用意されています。この辺はアルゴリズムの std::sort() などにも応用できる部分なので試してみてください。 連想コンテナに限らず、 STL のコンテナにクラスが使えるかどうかは「必要な演算子が用意されているかどうか」に掛かっています。関数オブジェクトでカバーできるのはこの「連想コンテナの比較」くらいなので、それほどコンテナに使用できるクラスは多くないかもしれません。 |
map と multimap ( #24 ) |
まず std::pair というクラス(正確には構造体)を紹介します。
//////////////////////////////////////////////////////////////// // std::pair の使用例。 #pragma warning( disable : 4786 ) // VC での警告を取り除きます。 #include <stdio.h> #include <map> void Use_pair() { std::pair< int, double > cPair( 2, 1.414 ); printf( "%d, %f\n", cPair.first, cPair.second ); } // 結果 2, 1.414000 ///////////////////////////// std::pair は「ふたつのデータを格納するクラス」です。各データは std::pair::first と std::pair::second のメンバ変数に入っています。標準 C++ ライブラリでは「ふたつの値をセットにして使う」ことが多いので、こういったクラスを用意してあるのです。 さて本題に入りましょう。今回は std::map と std::multimap を紹介します。これらは前回の std::set 、 std::multiset と似た機能を持っています。唯一違うのは「ふたつの値をセットにして格納する」という点です。 例を見てみましょう。 //////////////////////////////////////////////////////////////// // std::map の使用例。 #pragma warning( disable : 4786 ) // VC での警告を取り除きます。 #include <stdio.h> #include <map> void Use_map() { typedef std::pair< int, double > type_id; std::map< int, double > cMap; cMap.insert( type_id( 2, 1.414 ) ); cMap.insert( type_id( 3, 1.732 ) ); cMap.insert( type_id( 5, 2.236 ) ); std::map< int, double >::iterator cIter = cMap.find( 3 ); if( cIter != cMap.end() ) printf( "%d, %f\n", ( *cIter ).first, ( *cIter ).second ); } // 結果 3, 1.732000 ///////////////////////////// std::map はふたつの値をセットにして格納します。このとき、先ほどの std::pair を使います。 std::pair のうち、 first を「キー」、 second を「値」といいます。 std::map も2分木を使ってソートしつつ挿入しますが、ソートの対象は「キー」のみです。「値」に関してはソートされません。 同じく std::map::find() を使った検索も、キーに対してのみ行われます。 std::map が「辞書」と呼ばれるのはこのためです、キーは英単語、値はその訳といったところになります。辞書の中ではキーによってソートされているわけです。 std::map のイテレーターが指す要素は std::pair 型なので、 first と second を使って値を取得する必要があります。 このように std::map::find() などは「キーに対して」のみ実行されます。「値に対して」行う場合には、端からひとつずつ調べていく必要があります。 std::multiset のように、 std::map の「キー重複版」 std::multimap も用意されています。 std::map は「キーの重複」が許されていません(逆に言うと「値の重複」はできます)。「キーの重複」をしたい場合には代わりに std::multimap を使用します。 このとき注意しなければならないのは「同じキーでも値は違う」という点です。人名辞典で「田中」の項がふたつあれば、それは当然別々の人でしょう。 std::multimap::find() を使用した場合には「田中 A 」しか取得できないので「田中 B 」は永遠に見つからない状態になってしまいます。 そこで使うのが std::multimap::equal_range() です。 //////////////////////////////////////////////////////////////// // std::equal_range の使用例。 #pragma warning( disable : 4786 ) // VC での警告を取り除きます。 #include <stdio.h> #include <map> void Use_equal_range() { typedef std::pair< int, int > type_ii; typedef std::multimap< int, int >::iterator type_MMapIter; std::multimap< int, int > cMMap; cMMap.insert( type_ii( 100, 10 ) ); cMMap.insert( type_ii( 250, 50 ) ); cMMap.insert( type_ii( 100, -10 ) ); std::pair< type_MMapIter, type_MMapIter > cIterPair = cMMap.equal_range( 100 ); for ( type_MMapIter cIter = cIterPair.first ; cIter != cIterPair.second ; ++cIter ) printf( "%d, ", ( *cIter ).second ); } // 結果 10, -10, ///////////////////////////// std::multimap::equal_range() は引数がキーになっている範囲をふたつのイテレーターとして返します。 std::multimap 内はキーでソートされているので、同じキーならかならず並んでいます。その並ぶ範囲を返します。 ふたつのイテレーターは std::pair にひとつずつ入っています。このように std::pair は std::map 専用というわけではないのです(ちなみに std::multiset::equal_range() も std::pair でふたつのイテレーターを返します)。このふたつのイテレーターの間を走査すれば、特定のキーが持つすべての値を取得することができます。 このように、 std::set std::map std::multiset std::multimap の4つの連想コンテナは、自分が持っているメンバ関数を多用します。そのため、アルゴリズムなどとちょっと連携が取りにくいかもしれません。でも、 equal_range() のようにたいがいのメンバ関数はイテレーターを返します。これをうまく使って、アルゴリズムに組み込むようにしましょう。 |
|
|
(C)KAB-studio 2001 ALL RIGHTS RESERVED. |