C++入門|重複をなくすset

同じ流派は二度登録されない。setを使えば、重複を消して、純粋なデータの集まりだけを管理できます。

STLには、vector、list、mapのほかにも、データ管理に役立つ便利なクラスが用意されています。
今回学ぶsetは、その中でも「重複をなくす」ことが得意なクラスです。

setは集合を扱うためのSTLコンテナです。
集合とは、いくつかの要素をひとまとまりにしたものです。

ただし、setには大きな特徴があります。
それは、同じ値を複数回登録しても、同じ値は1つしか残らないという点です。

たとえば、修行場で戦士たちが使える流派を登録するとします。
亀仙流、神流、神流、界王流のように、神流を2回登録しようとしても、setの中では神流は1つだけになります。

ドラゴンボール風にたとえるなら、setは神殿にある流派登録簿です。
同じ流派を何度も書き込もうとしても、登録簿には1回分だけが残ります。
そのため、「どの流派が存在しているのか」を純粋に調べたいときにとても便利です。

vectorやlistは、同じ値を何度でも入れられます。
一方、setは同じ値を重複して持ちません。

コンテナ同じ値の登録向いている使い方
vectorできる入力順や番号付きのデータを管理する
listできる並び替えや途中の挿入、削除を扱う
mapキーと値を対応付ける名前から得点などを取り出す
set同じ値は1つだけ重複をなくして要素の種類を調べる

setとは何か

setは、同じ値を重複して登録しないSTLコンテナです。

vectorやlistでは、同じ値を何回でも追加できます。
たとえば、vectorに神流を2回追加すれば、神流は2回分保存されます。

しかし、setではそうなりません。
神流を1回登録しても、2回登録しても、setの中に残る神流は1つだけです。

この性質は、データの種類だけを知りたいときに役立ちます。

たとえば、修行ログに次のような流派名が並んでいたとします。

亀仙流
神流
神流
界王流

ここで知りたいのが「全部でどの流派が登場したか」なら、神流が2回ある必要はありません。
setを使えば、重複を取り除き、次のような集まりとして扱えます。

亀仙流
神流
界王流

このように、setは「同じものを1つにまとめる」ための道具として使えます。

図:setは同じ値を1つだけ登録する

この図が示していること

この図では、setに同じ値を登録しようとしても、最終的には1つだけ残ることを表しています。

神流を2回登録しようとしても、setの中では神流は1つだけになります。
setは、データの重複をなくして、どの要素が存在するのかを確認したいときに便利です。

setを使う準備

setを使うには、set用のヘッダーをインクルードします。

#include <set>

また、文字列を扱うためにstring、画面表示のためにiostreamも使います。

#include <iostream>
#include <string>
#include <set>

今回のサンプルでは、標準名前空間を使えるようにします。

using namespace std;

setの基本的な宣言は、次の形です。

set<型名> インスタンス名;

たとえば、文字列を登録するsetを作る場合は、次のように書きます。

set<string> styles;

このstylesには、string型の値を登録できます。
ここでは、修行流派名を文字列として登録していきます。

宣言意味
set stylesstring型の値を重複なしで管理するset
set powersint型の値を重複なしで管理するset

setを使ったサンプルプログラム

それでは、setを使ったサンプルを見ていきましょう。

ここでは、修行場で確認された流派名を登録します。
同じ流派名を複数回登録しようとしても、setの中では1つだけになります。

なお、setは要素を一定の順序で管理します。
そのため、表示される順番は登録した順番とは限りません。

プロジェクト/ファイル名: Chap6_09/main.cpp

#include <iostream>
#include <string>
#include <set>

using namespace std;

int main(int argc, char** argv) {
    // 流派名を重複なしで管理するset
    set<string> styles;

    // 流派名を登録する
    styles.insert("Kame");
    styles.insert("Kami");
    styles.insert("Kami");   // 同じ流派名をもう一度登録しようとする
    styles.insert("Kai");

    // 登録されている流派名を表示する
    set<string>::iterator it;

    for (it = styles.begin(); it != styles.end(); it++) {
        cout << *it << endl;
    }

    // KaiとTsuruがset内に存在するか調べる
    string checkStyles[] = { "Kai", "Tsuru" };

    for (int i = 0; i < 2; i++) {
        it = styles.find(checkStyles[i]);

        if (it == styles.end()) {
            // データがset内に存在しない場合
            cout << checkStyles[i] << " is not in a set." << endl;
        }
        else {
            // データがset内に存在する場合
            cout << checkStyles[i] << " is in a set." << endl;
        }
    }

    // プログラムが正常に終わったことを示す
    return 0;
}

実行結果

Kai
Kame
Kami
Kai is in a set.
Tsuru is not in a set.

このプログラムでは、stylesというsetを使っています。

stylesには、Kame、Kami、Kami、Kaiの順に登録しています。
ただし、Kamiは2回登録しようとしています。

setでは同じ値を重複して登録できないため、最終的にstylesの中にあるKamiは1つだけです。

そのため、表示される要素はKai、Kame、Kamiの3つになります。
登録しようとした値は4回分ありますが、実際に残る種類は3つです。

insertで値を登録する

setに値を登録するときは、insertを使います。

styles.insert("Kame");
styles.insert("Kami");
styles.insert("Kami");
styles.insert("Kai");

この処理では、最初にKameを登録します。
次にKamiを登録します。
さらにもう一度Kamiを登録しようとします。
最後にKaiを登録します。

しかし、setでは同じ値は1つしか持てません。

そのため、2回目のKamiは新しい要素としては追加されません。
結果として、stylesの中身は次のようになります。

登録しようとした値setの中身
KameKame
KamiKame、Kami
KamiKame、Kami
KaiKai、Kame、Kami

ここで注目したいのは、同じKamiを登録しようとしても、setの中身が増えていないことです。

setは、重複を許さない集合です。
同じ流派名を何度も登録しても、その流派名は1つだけ残ります。

ドラゴンボール風に言えば、神流の戦士が何度も同じ流派名を登録しようとしても、神殿の流派登録簿には神流という項目が1つだけ残るイメージです。

図:insertしても同じ値は増えない

この図が示していること

この図では、insertを使って値を登録しても、同じ値は重複して増えないことを表しています。

Kamiを2回登録しようとしても、setの中身にはKamiが1つだけ残ります。
setは、同じデータを自動的に1つにまとめるため、重複をなくしたい場面に向いています。

setの要素を表示する

setに登録されている要素を表示するときは、イテレータを使います。

set<string>::iterator it;

itは、setの要素を指すためのイテレータです。

beginで先頭を指し、endに到達するまで進めます。

for (it = styles.begin(); it != styles.end(); it++) {
    cout << *it << endl;
}

このfor文では、stylesの中の要素を順番に取り出して表示しています。

*itは、イテレータitが現在指している要素の値を表します。

実行結果では、次のように表示されます。

Kai
Kame
Kami

ここで、登録した順番とは違う順番で表示されていることに気づくかもしれません。

登録した順番は、Kame、Kami、Kami、Kaiでした。
しかし、表示ではKai、Kame、Kamiとなっています。

setは、要素を重複なしで管理するだけでなく、通常は一定の順序で要素を管理します。
そのため、登録順そのままに表示されるとは限りません。

この点は、vectorやlistとの大きな違いです。

setとvectorの違い

setとvectorは、どちらも複数のデータを管理できます。
ただし、目的が違います。

vectorは、入力された順番を大切にしてデータを並べるのに向いています。
同じ値もそのまま保存できます。

一方、setは、同じ値を1つだけ残すことに向いています。
入力順をそのまま保つことよりも、要素の重複をなくして管理することが大切な場面で使います。

比較項目vectorset
同じ値の登録できる1つだけ残る
順番追加した順番で扱いやすい登録順とは限らない
添字アクセスできるできない
主な目的順番付きでデータを保存する重複なしで要素を管理する

たとえば、修行参加者の入力履歴をそのまま残したいならvectorが向いています。
どの流派が登場したかだけを知りたいならsetが向いています。

ドラゴンボール風に言えば、vectorは修行参加の記録ログです。
同じ流派が何回出てきても、そのまま記録します。

setは流派の種類一覧です。
同じ流派が何回出てきても、その流派は1つだけ表示します。

findでデータの有無を調べる

setでは、ある値が登録されているかどうかを調べるときにfindを使います。

今回のプログラムでは、KaiとTsuruがstylesの中に存在するかを調べています。

string checkStyles[] = { "Kai", "Tsuru" };

for文で配列の中身を順番に調べます。

for (int i = 0; i < 2; i++) {
    it = styles.find(checkStyles[i]);

    if (it == styles.end()) {
        cout << checkStyles[i] << " is not in a set." << endl;
    }
    else {
        cout << checkStyles[i] << " is in a set." << endl;
    }
}

findは、指定した値をsetの中から探します。

見つかった場合は、その要素を指すイテレータを返します。
見つからなかった場合は、endと同じイテレータを返します。

そのため、it == styles.end() であれば、探した値はsetの中に存在しません。
it != styles.end() であれば、探した値はsetの中に存在します。

今回のstylesには、Kai、Kame、Kamiが登録されています。
そのため、Kaiは見つかります。
しかし、Tsuruは登録されていないため見つかりません。

実行結果は次のようになります。

Kai is in a set.
Tsuru is not in a set.

図:findでset内の存在確認をする

この図が示していること

この図では、findによってsetの中に値が存在するかを調べる流れを表しています。

Kaiはstylesの中にあるため、findで見つかります。
Tsuruはstylesの中にないため、findの結果はendになります。

findの戻り値をendと比較することで、値が存在するかどうかを判断できます。

endは見つからなかったことを表す目印にもなる

STLでは、endは最後の要素そのものではなく、最後の次の位置を表すイテレータです。

setのfindでは、探した値が見つからなかった場合にendが返されます。

it = styles.find("Tsuru");

stylesの中にTsuruがなければ、itはstyles.end()と同じになります。

そのため、次の条件で存在しないことを判定できます。

if (it == styles.end()) {
    cout << "見つかりませんでした。" << endl;
}

逆に、見つかった場合はendではありません。

if (it != styles.end()) {
    cout << "見つかりました。" << endl;
}

この考え方は、setだけでなくmapなどでもよく使われます。
findで探し、endかどうかを確認する流れは、STLでとても大切です。

setの主なメンバ関数

setには、集合を管理するための便利なメンバ関数があります。

メンバ関数役割
insert要素を登録する
find指定した値を探す
erase指定した要素を削除する
clearすべての要素を消去する
emptysetが空かどうかを調べる
size登録されている要素数を取得する

insertは、setに値を登録します。
同じ値がすでに登録されている場合、その値は増えません。

findは、指定した値がsetの中に存在するかを調べます。

eraseは、指定した値を削除します。
たとえば、Kameを削除したい場合は次のように書けます。

styles.erase("Kame");

clearは、setの中身をすべて消します。

styles.clear();
styles.clear();

emptyは、setが空かどうかを調べます。

if (styles.empty()) {
    cout << "流派登録簿は空です。" << endl;
}

sizeは、setに登録されている要素数を取得します。

cout << styles.size() << endl;

setは、重複なしで要素を管理するだけでなく、存在確認や削除、全削除、要素数の確認もできます。

setを使いたい場面

setは、重複をなくしたい場面でとても役立ちます。

たとえば、修行ログに登場した流派名を集める場合を考えます。
同じ流派が何度も出てきても、最終的に知りたいのが流派の種類だけなら、setがぴったりです。

また、ある値が登録済みかどうかを調べたいときにも便利です。
findを使えば、目的の値がsetの中にあるかどうかを確認できます。

使いたい場面setが向いている理由
重複をなくしたい同じ値を1つだけ管理できる
種類だけを知りたい何が存在するかを確認しやすい
登録済みか調べたいfindで存在確認できる
一覧を整理したい重複なしの一覧を作れる

ドラゴンボール風に言えば、setは流派の種類を管理する神殿の登録簿です。
何人もの戦士が神流を使っていても、流派の種類としては神流が1つあれば十分です。

setで身につけたい感覚

setは、同じ値を重複して持たないSTLコンテナです。

vectorやlistは、同じ値を何回でも入れられます。
しかしsetでは、同じ値は1つだけです。

今回のサンプルでは、Kamiを2回登録しようとしました。

styles.insert("Kami");
styles.insert("Kami");

しかし、setの中にKamiは1つだけ残りました。

この性質により、setは重複を取り除きたいときにとても便利です。

さらに、findを使えば、特定の値が存在するかどうかを調べられます。
見つかった場合はその要素を指すイテレータが返り、見つからなかった場合はendが返ります。

setを使うときは、次の感覚を持っておくと理解しやすくなります。

考え方内容
同じ値は1つだけ重複登録されない
追加はinsert登録したい値をsetへ入れる
検索はfind指定した値があるか調べる
見つからないとend存在しないことを判定できる
登録順とは限らないsetは一定の順序で要素を管理する

setは、単なるデータの入れ物ではなく、重複しない集合を作るためのクラスです。

修行場に登場した流派、使われた技の種類、集めたアイテム名、参加した戦士名など、同じものを1つにまとめたいときに活躍します。

重複をなくして、どんな要素が存在するのかを整理する。
それが、setを使う大きな目的です。