C++入門|6章の練習問題

修行データを集計し、修行コードを検索する。6章で身につけたSTLの力を、2つの実戦課題で確かめよう。

6章では、テンプレートとSTLを使ったデータ管理について学んできました。

vectorでは、要素数が最初から決まっていないデータを順番に保存しました。
listでは、イテレータを使って要素をたどり、途中への挿入や削除を行いました。

mapでは、キーと値を対応付ける連想配列を作りました。
setでは、同じ値を重複させずに管理しました。

さらに、stackとqueueでは、データを登録した順番と取り出す順番の違いを確認しました。

今回の練習問題では、その中でも特によく使うvectorとmapを取り上げます。

最初の問題では、入力された修行ポイントをvectorへ保存し、合計、最大値、最小値を求めます。

次の問題では、英語の修行コードと日本語の修行名をmapへ登録し、入力された英語から対応する日本語を検索します。

ドラゴンボール風にたとえると、1つ目は神殿に集められた修行結果を集計する課題です。
2つ目は、英語で登録された修行コードを検索して、日本語の修行内容を表示する課題です。

どちらも、ただプログラムを入力するだけではなく、処理の順番を自分で考えることが大切です。

練習問題主に確認する機能
練習問題6-1vector、push_back、イテレータ、最大値、最小値
練習問題6-2map、キーと値、find、end、second

図:6章の練習問題で確認する内容

この図が示していること

この図では、2つの練習問題で確認する内容を表しています。

練習問題6-1では、vectorへ保存された複数の数値から、合計、最大値、最小値を求めます。

練習問題6-2では、mapに登録された英語と日本語の対応から、入力されたキーをfindで検索します。

6章で学んだデータの保存、走査、集計、検索を、2つの課題を通して確認できます。

練習問題6-1:修行ポイントの合計・最大値・最小値

問題

キーボードから正の整数を繰り返し入力するプログラムを作りなさい。

入力された正の整数は、戦士が修行で獲得した修行ポイントとしてvectorへ保存します。

0以下の数値が入力されたら入力を終了し、次の情報を表示してください。

入力された修行ポイントの一覧を、入力された順番で表示します。
すべての修行ポイントの合計を表示します。
入力された修行ポイントの最大値を表示します。
入力された修行ポイントの最小値を表示します。

入力された正の整数が1つもない場合は、修行データが登録されていないことを表示してプログラムを終了してください。

期待される実行結果の例

修行ポイントを入力:5
修行ポイントを入力:2
修行ポイントを入力:4
修行ポイントを入力:3
修行ポイントを入力:1
修行ポイントを入力:-1

修行ポイント一覧:5 2 4 3 1
合計:15
最大値:5
最小値:1

正の整数が1つも入力されなかった場合は、次のようになります。

修行ポイントを入力:0
修行データが登録されていません。

プログラムを作成するヒント

入力される数値の個数は、プログラムを実行するまで分かりません。
そのため、要素数を最初に決める配列ではなくvectorを使うと作りやすくなります。

入力された数値が正の整数なら、push_backでvectorの末尾へ追加します。

trainingPoints.push_back(point);

0以下の数値が入力された場合は、breakで入力ループを終了します。

入力が終わったら、emptyを使ってvectorが空かどうかを確認します。

if (trainingPoints.empty()) {

最大値と最小値の初期値には、vectorの先頭要素を使用できます。

int maxPoint = trainingPoints[0];
int minPoint = trainingPoints[0];

そのあと、イテレータを使って全要素を順番に調べます。

現在の値がmaxPointより大きければ、maxPointを更新します。

if (*itr > maxPoint) {
    maxPoint = *itr;
}

現在の値がminPointより小さければ、minPointを更新します。

if (*itr < minPoint) {
    minPoint = *itr;
}

同じループの中で、値の表示と合計の計算も行えます。

プログラムの解答例

プロジェクト名/ファイル名: Prac6_1/training_point_report.cpp

#include <iostream>
#include <vector>

using namespace std;

int main(int argc, char** argv) {
    // 入力された修行ポイントを保存するvector
    vector<int> trainingPoints;

    // 0以下の数値が入力されるまで繰り返す
    while (true) {
        int point;

        cout << "修行ポイントを入力:";
        cin >> point;

        if (point > 0) {
            // 正の整数をvectorの末尾へ追加する
            trainingPoints.push_back(point);
        }
        else {
            // 0以下が入力されたら入力を終了する
            break;
        }
    }

    // 正の整数が1つも入力されなかった場合
    if (trainingPoints.empty()) {
        cout << "修行データが登録されていません。" << endl;
        return 0;
    }

    // 合計値を初期化する
    int sum = 0;

    // 最大値と最小値を先頭要素で初期化する
    int maxPoint = trainingPoints[0];
    int minPoint = trainingPoints[0];

    // vectorの全要素を調べるためのイテレータ
    vector<int>::iterator itr;

    cout << endl;
    cout << "修行ポイント一覧:";

    // 値を表示しながら合計・最大値・最小値を求める
    for (itr = trainingPoints.begin();
         itr != trainingPoints.end();
         itr++) {

        cout << *itr << " ";

        // 合計値へ加算する
        sum += *itr;

        // 現在の値が大きければ最大値を更新する
        if (*itr > maxPoint) {
            maxPoint = *itr;
        }

        // 現在の値が小さければ最小値を更新する
        if (*itr < minPoint) {
            minPoint = *itr;
        }
    }

    cout << endl;
    cout << "合計:" << sum << endl;
    cout << "最大値:" << maxPoint << endl;
    cout << "最小値:" << minPoint << endl;

    return 0;
}

解説

vectorの用意

最初に、修行ポイントを保存するvectorを宣言しています。

vector<int> trainingPoints;

このvectorには、int型の値を保存できます。

入力される修行ポイントの個数は決まっていません。
そのため、入力されるたびに要素を増やせるvectorが向いています。

ドラゴンボール風に考えると、trainingPointsは神殿に設置された修行記録ボードです。
戦士がポイントを報告するたびに、新しい記録欄が後ろへ追加されます。

入力を繰り返す処理

入力にはwhileを使っています。

while (true) {

whileの条件がtrueなので、そのままでは何度でも繰り返します。

ループの中で数値を入力し、正の整数ならvectorへ追加します。

if (point > 0) {
    trainingPoints.push_back(point);
}

0以下の数値が入力された場合は、breakによってループを終了します。

else {
    break;
}

入力された0以下の数値は、終了を知らせるための値なので、vectorには保存しません。

入力値処理
5vectorへ追加
2vectorへ追加
4vectorへ追加
3vectorへ追加
1vectorへ追加
-1入力ループを終了

emptyでデータの有無を確認する

入力が終わったあと、vectorが空かどうかを確認しています。

if (trainingPoints.empty()) {

emptyは、vectorに要素が1つもなければtrueを返します。

最大値と最小値を求めるためには、比較の基準になる値が必要です。
vectorが空なのにtrainingPoints[0]へアクセスすると、存在しない要素を参照してしまいます。

そのため、先にemptyで確認し、空だった場合はメッセージを表示して終了しています。

cout << "修行データが登録されていません。" << endl;
return 0;

最大値と最小値の初期化

最大値と最小値は、vectorの先頭要素で初期化しています。

int maxPoint = trainingPoints[0];
int minPoint = trainingPoints[0];

たとえば、最初の値が5なら、比較を始める前の最大値と最小値はどちらも5です。

そのあと、すべての値と比較しながら更新します。

最大値を0で初期化する方法も考えられますが、今回のように正の整数だけを扱う場合に限られます。

先頭要素で初期化すれば、入力される数値の範囲が変わった場合にも対応しやすくなります。

イテレータで全要素をたどる

vectorに保存された値は、イテレータを使って順番に処理しています。

vector<int>::iterator itr;

ループでは、beginから始めてendへ到達するまで進みます。

for (itr = trainingPoints.begin();
     itr != trainingPoints.end();
     itr++) {

beginは先頭要素を指します。
endは最後の要素ではなく、最後の次の位置を表します。

現在の要素の値は、*itrで取り出せます。

cout << *itr << " ";

合計値を求める

合計値を保存するsumは、最初に0で初期化しています。

int sum = 0;

イテレータが指している値を、ループのたびにsumへ加算します。

sum += *itr;

入力された値が5、2、4、3、1なら、sumは次のように変化します。

調べる値sumの変化
50 → 5
25 → 7
47 → 11
311 → 14
114 → 15

最大値を求める

現在の値がmaxPointより大きい場合は、最大値を更新します。

if (*itr > maxPoint) {
    maxPoint = *itr;
}

入力された値が5、2、4、3、1の場合、maxPointの初期値は5です。

2、4、3、1はすべて5より小さいため、maxPointは最後まで5のままです。

たとえば、途中に8が含まれていれば、8を調べた時点でmaxPointは8へ更新されます。

最小値を求める

現在の値がminPointより小さい場合は、最小値を更新します。

if (*itr < minPoint) {
    minPoint = *itr;
}

最初のminPointは5です。

2を調べると、2は5より小さいのでminPointが2になります。
4を調べても、4は2より大きいため更新しません。
1を調べると、1は2より小さいのでminPointが1になります。

調べる値maxPointminPoint
初期値555
555
252
452
352
151

図:合計・最大値・最小値を更新する流れ

この図が示していること

この図では、イテレータでvectorの全要素を順番に調べながら、合計、最大値、最小値を同時に更新する流れを表しています。

1つのループの中で、値の表示、合計への加算、最大値の比較、最小値の比較を行っています。

データを何度も最初から調べ直すのではなく、1回の走査で必要な集計を行えることが分かります。

練習問題6-2:英語の修行コードを日本語へ変換する

問題

コンソールから英語の修行コードを入力すると、対応する日本語の修行名を表示するプログラムを作りなさい。

英語と日本語の対応にはmapを使用してください。

英語の修行コードと日本語の修行名は、次のように対応させます。

英語日本語
aura気力集中
guard防御鍛錬
heal回復修行

登録されている英語が入力された場合は、英語と対応する日本語を表示します。

登録されていない英語が入力された場合は、変換できないことを表示してプログラムを終了します。

期待される実行結果の例1

英語の修行コードを入力:aura
auraは日本語では気力集中です。

期待される実行結果の例2

英語の修行コードを入力:attack
変換できません。

プログラムを作成するヒント

英語と日本語は、map<string, string>を使って対応付けられます。

map<string, string> trainingNames;

英語をキーとして、日本語の修行名を値に登録します。

trainingNames["aura"] = "気力集中";

入力された英語がmapに存在するかどうかを調べるには、findを使います。

itr = trainingNames.find(englishName);

findの結果がendと異なっていれば、キーが見つかっています。

if (itr != trainingNames.end()) {

見つかった要素のキーはitr->first、値はitr->secondで取り出せます。

見つからなかった場合は、findの結果がendと同じになります。

プログラムの解答例

プロジェクト名/ファイル名: Prac6_2/training_name_dictionary.cpp

#include <iostream>
#include <string>
#include <map>

using namespace std;

int main(int argc, char** argv) {
    // 英語の修行コードと日本語の修行名を管理するmap
    map<string, string> trainingNames;

    // 英語をキー、日本語を値として登録する
    trainingNames["aura"] = "気力集中";
    trainingNames["guard"] = "防御鍛錬";
    trainingNames["heal"] = "回復修行";

    // 入力された英語を保存する変数
    string englishName;

    cout << "英語の修行コードを入力:";
    cin >> englishName;

    // 入力されたキーを検索する
    map<string, string>::iterator itr;
    itr = trainingNames.find(englishName);

    // 検索結果を判定する
    if (itr != trainingNames.end()) {
        // キーが見つかった場合
        cout << itr->first << "は日本語では"
             << itr->second << "です。" << endl;
    }
    else {
        // キーが見つからなかった場合
        cout << "変換できません。" << endl;
    }

    return 0;
}

解説

mapの宣言

最初に、英語の修行コードと日本語の修行名を管理するmapを宣言しています。

map<string, string> trainingNames;

mapの基本形は次のようになります。

map<キーの型, 値の型> 変数名;

今回のプログラムでは、キーと値の両方が文字列なので、どちらにもstringを指定しています。

mapの部分内容
1つ目のstring英語の修行コード
2つ目のstring日本語の修行名
trainingNamesmapの変数名

キーと値の登録

英語と日本語の対応は、次の処理で登録しています。

trainingNames["aura"] = "気力集中";
trainingNames["guard"] = "防御鍛錬";
trainingNames["heal"] = "回復修行";

角括弧の中がキーで、右辺が値です。

trainingNames["aura"] = "気力集中";では、auraというキーに気力集中という値を対応させています。

登録後のmapは、次のような状態になります。

aura  → 気力集中
guard → 防御鍛錬
heal  → 回復修行

ドラゴンボール風に考えると、英語の修行コードを日本語の修行内容へ変換する、神殿の修行辞書です。

英語の修行コードを入力する

入力された英語は、string型のenglishNameへ保存します。

string englishName;

cinでキーボードから入力を受け取ります。

cin >> englishName;

auraと入力すれば、englishNameにはauraが入ります。

登録されているキーは小文字なので、AuraやAURAはauraとは別の文字列として扱われます。

入力結果
aura見つかる
Aura見つからない
AURA見つからない

findでキーを検索する

入力されたキーは、findを使って検索します。

map<string, string>::iterator itr;
itr = trainingNames.find(englishName);

findは、指定したキーがmapの中にあるかを探します。

キーが見つかった場合は、その要素を指すイテレータを返します。

キーが見つからなかった場合は、trainingNames.end()と同じイテレータを返します。

入力されたキーfindの結果
auraauraの要素を指す
guardguardの要素を指す
healhealの要素を指す
attackendと同じになる

endと比較する

キーが見つかったかどうかは、イテレータをendと比較して判定します。

if (itr != trainingNames.end()) {

itrがendと異なっていれば、キーが見つかっています。

itrがendと同じなら、キーは登録されていません。

else {
    cout << "変換できません。" << endl;
}

endは最後の要素ではありません。
mapの最後の要素よりもさらに先にある、終端を示す位置です。

findは、検索したキーが見つからなかったときに、このendを返します。

firstとsecondでキーと値を取り出す

mapの要素は、キーと値の組み合わせです。

イテレータからキーを取り出すにはfirstを使います。

itr->first

値を取り出すにはsecondを使います。

itr->second

auraが見つかった場合は、次のようになります。

書き方内容
itr->firstaura
itr->second気力集中

表示処理では、firstとsecondを組み合わせています。

cout << itr->first << "は日本語では"
     << itr->second << "です。" << endl;

そのため、auraを入力すると次のように表示されます。

auraは日本語では気力集中です。

角括弧ではなくfindを使う理由

mapでは、次のように角括弧を使って値を取得することもできます。

trainingNames[englishName]

ただし、登録されていないキーを角括弧で指定すると、そのキーがmapへ新しく追加されます。

たとえば、attackが登録されていない状態で次の処理を行うと、attackというキーが追加されます。

trainingNames["attack"];

今回の目的は、入力されたキーが存在するかどうかを確認することです。
新しいキーを追加する必要はありません。

そのため、意図しないデータの追加を防げるfindを使っています。

方法登録されていないキーを指定した場合
角括弧新しいキーが追加される
find新しいキーは追加されず、endが返る

図:findで修行コードを検索する流れ

この図が示していること

この図では、入力された修行コードをfindで検索し、見つかった場合と見つからなかった場合に処理が分かれる流れを表しています。

auraはmapに登録されているため、気力集中が表示されます。

attackは登録されていないため、findの結果がendと同じになり、変換できないことが表示されます。

また、findによる検索では、存在しないキーがmapへ勝手に追加されないことも分かります。

2つの練習問題で確認した内容

練習問題6-1では、入力される数が決まっていない修行ポイントをvectorへ保存しました。

push_backで値を追加し、emptyでデータの有無を確認しました。
そのあと、イテレータを使って全要素をたどり、合計、最大値、最小値を求めました。

練習問題6-2では、英語の修行コードと日本語の修行名をmapで対応付けました。

入力されたキーをfindで検索し、endと比較して存在を判定しました。
見つかった場合はfirstとsecondを使って、キーと値を取り出しました。

処理使用したSTLの機能
要素数が決まっていない値の保存vector
vectorの末尾へ追加push_back
全要素を順番に調べるイテレータ、begin、end
データが空か確認empty
英語と日本語を対応付けるmap
キーを検索するfind
検索失敗を判定するend
mapのキーを取得するfirst
mapの値を取得するsecond

ドラゴンボール風にたとえると、vectorは修行結果を順番にためる記録ボードです。
mapは修行コードから修行内容を探す検索装置です。

同じSTLでも、vectorとmapでは得意な処理が違います。

順番にデータを保存して集計したいならvectorが向いています。
名前やコードから対応する値を探したいならmapが向いています。

このように、扱うデータと処理の目的に合わせてコンテナを選べるようになると、C++でのデータ管理がさらに分かりやすくなります。