C++の連想配列とは、キーと値をセットで管理するデータ構造のことです。
通常の配列では、0番目、1番目、2番目のように番号を使って値を取り出します。
一方、連想配列では、文字列や数値などの「キー」を使って値を取り出します。
たとえば、「商品名」と「価格」、「名前」と「点数」、「ユーザーID」と「ユーザー情報」のように、何かと何かを対応させて管理したいときに便利です。
C++では、連想配列として主に次のようなコンテナが使われます。
C++で使う代表的な連想配列
std::map
std::map は、キーと値のペアを管理する代表的な連想コンテナです。
大きな特徴は、キーが自動的に並べ替えられることです。
標準では、キーは昇順に並びます。
たとえば、数値をキーにした場合は小さい順に並び、文字列をキーにした場合は辞書順に近い順番で管理されます。
そのため、キーの順番に沿ってデータを表示したい場合や、範囲検索をしたい場合に向いています。
std::unordered_map
std::unordered_map も、キーと値のペアを管理する連想配列の一種です。
ただし、std::map とは違い、キーの順序は保証されません。
その代わり、ハッシュテーブルという仕組みを使うため、平均的には高速に検索できます。
順番を気にせず、キーから値をすばやく探したい場合に向いています。
たとえば、単語の出現回数を数える処理や、ユーザーIDからユーザー情報を検索する処理などでよく使われます。
std::mapとstd::unordered_mapの違い
キーの並び順が違う
std::map は、標準ではキーが昇順に並びます。
一方、std::unordered_map は、キーの順序を保証しません。
登録した順番でもなく、キーの昇順でもありません。実行環境や内部状態によって、表示される順番が変わることがあります。
そのため、順番が重要な場合は std::map、順番が不要な場合は std::unordered_map を選ぶとよいです。
検索速度が違う
std::map は、通常、平衡二分探索木という仕組みで実装されます。
そのため、検索・追加・削除はおおむね対数時間で行われます。
一方、std::unordered_map はハッシュテーブルを使うため、平均的には定数時間で検索できます。
ただし、std::unordered_map が常に std::map より速いとは限りません。
ハッシュの衝突が多い場合や、キーの型によっては、期待したほど速くならないこともあります。
キーに必要な条件が違う
std::map では、キー同士の順序を決められる必要があります。
つまり、どちらのキーが小さいか、大きいかを判断できる必要があります。
一方、std::unordered_map では、キーをハッシュ化できることと、キー同士が等しいかどうかを比較できることが必要です。
整数や文字列などの基本的な型であれば、通常は特別な準備なしに使えます。
連想配列の基本的な考え方
キーと値をセットで管理する
連想配列では、データを「キー」と「値」の組み合わせで管理します。
たとえば、商品価格を管理する場合は、商品名がキー、価格が値になります。
学生の点数を管理する場合は、学生名がキー、点数が値になります。
このように、キーを使って目的の値を取り出せるのが連想配列の大きな特徴です。
キーは重複できない
std::map と std::unordered_map では、同じキーを複数持つことはできません。
同じキーに対して新しい値を設定すると、既存の値が上書きされます。
たとえば、ある名前に対して最初に80点を登録し、そのあと同じ名前に90点を登録した場合、最終的には90点として扱われます。
同じキーに複数の値を持たせたい場合は、std::multimap を使う方法や、値の側に複数のデータを持たせる方法があります。
std::mapの特徴
キーが自動的に並ぶ
std::map の最大の特徴は、キーが自動的に並ぶことです。
標準では昇順に並びます。数値なら小さい順、文字列なら辞書順に近い順番になります。
そのため、登録した順番とは異なる順番で表示されることがあります。
キーの順番に意味があるデータを扱う場合には、std::map が便利です。
範囲検索に向いている
std::map はキーが並んで管理されているため、範囲検索に向いています。
たとえば、ある日付以降の予定を取り出したい場合や、あるID以上のデータを探したい場合などに使いやすいです。
順番を活かした処理をしたい場合は、std::unordered_map よりも std::map のほうが適しています。
検索・追加・削除は安定している
std::map は、検索・追加・削除の計算量が安定しています。
大量のデータを扱う場合でも、データ数に応じて比較的予測しやすい性能になります。
ただし、平均的な検索速度だけを見ると、std::unordered_map のほうが速い場合もあります。
std::unordered_mapの特徴
キーの順番は保証されない
std::unordered_map では、キーの順番は保証されません。
登録した順番に並ぶわけでもなく、キーの昇順に並ぶわけでもありません。
そのため、表示順や処理順に依存するコードを書くべきではありません。
順番が必要な場合は、std::map を使うか、別途並べ替えを行う必要があります。
平均的に高速な検索ができる
std::unordered_map は、ハッシュテーブルを使ってデータを管理します。
そのため、平均的には非常に高速にキーを検索できます。
特に、順序を気にせず、キーから値をすばやく取り出したい場合に向いています。
ただし、ハッシュの衝突が多い場合など、最悪の場合は検索が遅くなることもあります。
集計処理と相性がよい
std::unordered_map は、単語数や投票数などを数える処理と相性がよいです。
たとえば、文章中に出てくる単語の回数を数えたり、アンケート結果を集計したりする場面でよく使われます。
キーの順番が不要で、単純に「何が何回出たか」を管理したい場合には便利です。
値の追加と更新
新しいキーを追加できる
連想配列では、新しいキーに値を設定することで、データを追加できます。
たとえば、まだ登録されていない商品名に価格を設定すれば、その商品名と価格の組み合わせが新しく追加されます。
既存のキーは上書きされる
すでに存在するキーに対して新しい値を設定すると、以前の値は上書きされます。
これは便利な一方で、意図せず値を変更してしまう可能性もあります。
同じキーに新しい値を設定する場合は、「追加」ではなく「更新」になることを意識しておくとよいです。
値の取得
キーを指定して値を取り出す
連想配列では、キーを指定することで対応する値を取得できます。
たとえば、商品名を指定して価格を取得したり、名前を指定して点数を取得したりできます。
このように、意味のあるキーを使って値を取り出せるため、通常の配列よりも直感的にデータを扱える場面があります。
存在しないキーに注意する
C++の連想配列では、存在しないキーに対してアクセスしたときの挙動に注意が必要です。
特に [] を使って存在しないキーにアクセスすると、そのキーが新しく追加されます。
値の型が整数であれば、初期値として0が入ります。
文字列であれば空文字列、配列のようなコンテナであれば空の状態になります。
これは便利な場合もありますが、存在確認だけをしたい場面では意図しないデータ追加につながることがあります。
キーの存在確認
findを使う方法
キーが存在するか確認する代表的な方法が find です。
find は、指定したキーが見つかればその要素を指す情報を返します。
見つからなかった場合は、末尾を表す情報を返します。
存在確認と値の取得を安全に行いたい場合は、find を使う方法がよく使われます。
countを使う方法
count を使って、指定したキーが存在するかどうかを確認することもできます。
std::map や std::unordered_map では同じキーを複数持てないため、結果は必ず0または1です。
0なら存在しない、1なら存在するという意味になります。
ただし、std::multimap や std::unordered_multimap のように同じキーを複数持てるコンテナでは、2以上になることもあります。
containsを使う方法
C++20以降では、contains を使ってキーの存在確認ができます。
contains は名前のとおり、指定したキーを含んでいるかどうかを調べるための機能です。
読みやすく、直感的に理解しやすいため、C++20以降を使える環境では便利です。
ただし、C++17以前では使えないため、その場合は find や count を使います。
要素の削除
キーを指定して削除できる
連想配列では、キーを指定して要素を削除できます。
たとえば、特定の商品情報を削除したい場合や、登録済みのユーザー情報を削除したい場合に使います。
キーが存在すれば、そのキーと値のペアが削除されます。
存在しないキーを削除しても問題になりにくい
存在しないキーを削除しようとしても、多くの場合はエラーにはなりません。
単に削除される要素がないだけです。
そのため、削除前に必ず存在確認をしなければならないわけではありません。
全要素の走査
すべてのキーと値を取り出せる
連想配列に入っているすべての要素を順番に取り出すこともできます。
このとき、std::map ではキーの順番に沿って要素が取り出されます。
一方、std::unordered_map では順番が保証されないため、どの順番で取り出されるかに依存してはいけません。
キーと値を分けて扱える
連想配列の各要素は、キーと値のペアとして管理されています。
そのため、全要素を処理するときは、キーと値をそれぞれ取り出して利用できます。
たとえば、一覧表示ではキーを名前、値を点数として表示できます。
集計処理では、キーを項目名、値を回数として扱えます。
std::mapを使うべき場面
キー順にデータを扱いたい場合
std::map は、キーが標準で昇順に並ぶため、キー順にデータを扱いたい場面に向いています。
たとえば、ID順にユーザー一覧を表示したい場合や、日付順に予定を管理したい場合に便利です。
範囲検索をしたい場合
ある範囲に含まれるキーを探したい場合にも、std::map は向いています。
たとえば、一定期間内のデータを取り出す、指定した数値以上のデータを探す、といった処理です。
キーが整列されていることを活かしたい場合は、std::map を選ぶとよいです。
表示順を安定させたい場合
データを出力するときに順番を安定させたい場合にも、std::map は便利です。
std::unordered_map では表示順が変わる可能性がありますが、std::map ならキーの順番で表示されます。
ログ出力やレポート作成など、見た目の順番が大切な場面では std::map が適しています。
std::unordered_mapを使うべき場面
順番が不要な場合
キーの順番を気にしない場合は、std::unordered_map が候補になります。
たとえば、単にキーから値を探せればよい場合や、表示順に意味がない場合です。
高速な検索を期待したい場合
std::unordered_map は、平均的に高速な検索が期待できます。
そのため、大量のデータからキーを使って値を探す処理に向いています。
ただし、常に std::map より速いわけではありません。
データ量、キーの型、ハッシュの状態によって性能は変わります。
カウント処理をしたい場合
単語の出現回数、投票数、アクセス回数などを数える処理では、std::unordered_map がよく使われます。
キーに対象となる単語や項目を指定し、値に回数を入れることで、簡単に集計できます。
multimapとは
同じキーを複数持てるコンテナ
std::multimap は、同じキーを複数持てる連想コンテナです。
通常の std::map では、同じキーを1つしか持てません。
同じキーに値を設定すると、既存の値が上書きされます。
一方、std::multimap では、同じキーに対して複数の値を登録できます。
使いどころ
同じ名前に複数の記録を紐づけたい場合や、同じカテゴリに複数の商品を登録したい場合などに使えます。
ただし、実際には、値の側に複数のデータを持たせる方法のほうが扱いやすいこともあります。
たとえば、1つのキーに対してリストのようなデータを持たせることで、同じキーに複数の値を関連づけられます。
unordered_multimapとは
順序なしで同じキーを複数持てる
std::unordered_multimap は、std::unordered_map のように順序を保証せず、さらに同じキーを複数持てるコンテナです。
ただし、初心者が最初に覚えるべき優先度は高くありません。
まずは、std::map と std::unordered_map の違いを理解することが大切です。
初心者はmapとunordered_mapを優先して理解する
C++の連想配列を学び始めた段階では、まず次の2つを理解すれば十分です。
std::map は、キー順に管理したいときに使います。
std::unordered_map は、順序が不要で高速な検索を期待したいときに使います。
この2つを使い分けられるようになれば、多くの場面に対応できます。
連想配列で注意すべきポイント
存在しないキーにアクセスすると追加される場合がある
C++の連想配列では、存在しないキーに対して [] でアクセスすると、そのキーが新しく追加されます。
これは、カウント処理などでは便利です。
たとえば、まだ登場していない単語の回数を0から始めて、そのまま増やしていくような処理では役立ちます。
しかし、単に存在確認をしたいだけの場合には注意が必要です。
意図せずキーが追加され、データの中身が変わってしまう可能性があります。
insertは既存のキーを上書きしない
要素の追加には insert も使えます。
ただし、insert は、同じキーがすでに存在する場合、基本的に値を上書きしません。
つまり、「存在しない場合だけ追加したい」ときに向いています。
一方、既存のキーがあれば値を更新したい場合は、別の方法を使う必要があります。
atは存在しないキーを追加しない
値を取得する方法として at もあります。
at は、存在しないキーを勝手に追加しません。
その代わり、指定したキーが存在しない場合は例外が発生します。
そのため、存在することが分かっているキーを読み取る場合や、存在確認をしたあとに使う場合に適しています。
constな連想配列では角括弧でアクセスできない
読み取り専用の連想配列では、[] によるアクセスが使えない場合があります。
これは、[] が存在しないキーを追加する可能性を持っているためです。
読み取り専用のデータから値を取り出す場合は、at や find を使うのが適しています。
実務や学習で意識したい書き方
using namespace stdは学習用なら便利
初心者向けのサンプルでは、using namespace std が使われることがあります。
これはコードを短く書けるため、学習段階では分かりやすいというメリットがあります。
ただし、実務や大きなプログラムでは、名前の衝突を避けるために std:: を付けて書くことも多いです。
学習用と実務用では、書き方の方針が少し違うことを理解しておくとよいです。
endlは必須ではない
出力の改行には endl が使われることがあります。
ただし、endl は単に改行するだけでなく、出力バッファをフラッシュする働きもあります。
そのため、単に改行したいだけなら、別の改行方法が使われることもあります。
初心者向けの説明では endl でも問題ありませんが、実用的なコードでは状況に応じて使い分けられます。
日本語文字列は環境に注意する
C++で日本語の文字列をキーにすることもできます。
ただし、ソースファイルの文字コードや実行環境、ターミナルの設定によっては文字化けすることがあります。
連想配列の仕組み自体とは別の問題ですが、日本語を扱う場合は文字コードにも注意が必要です。
連想配列の活用例
商品名と価格を管理する
連想配列は、商品名と価格を対応させるような処理に向いています。
商品名をキー、価格を値として管理すれば、商品名から価格を簡単に調べられます。
ECサイトの商品管理や、簡単な料金表のようなデータに応用できます。
名前と点数を管理する
学生の名前と点数を管理する場合にも連想配列は便利です。
名前をキー、点数を値にすれば、特定の学生の点数をすぐに取り出せます。
ただし、同姓同名のようにキーが重複する可能性がある場合は、名前だけをキーにするのではなく、IDなど一意に識別できる情報をキーにしたほうが安全です。
単語の出現回数を数える
文章中の単語が何回出てきたかを数える処理は、連想配列の代表的な活用例です。
単語をキー、出現回数を値として管理すれば、単語が登場するたびに回数を増やしていけます。
このようなカウント処理では、std::unordered_map がよく使われます。
ユーザーIDから情報を取得する
ユーザーIDをキーにして、ユーザー名やプロフィール情報を管理することもできます。
IDは一意であることが多いため、連想配列のキーとして使いやすいです。
順番が不要で、IDから情報を高速に取り出したい場合は、std::unordered_map が向いています。
mapとunordered_mapの選び方
順番が必要ならstd::map
キーの順番に意味がある場合は、std::map を選ぶとよいです。
たとえば、日付順、ID順、名前順などでデータを扱いたい場合です。
また、範囲検索をしたい場合にも std::map は適しています。
順番が不要ならstd::unordered_map
順番が不要で、キーから値をすばやく取り出したい場合は、std::unordered_map が候補になります。
特に、集計処理や検索処理ではよく使われます。
ただし、表示順に依存する処理には向いていません。
迷ったときの考え方
初心者のうちは、次のように考えると分かりやすいです。
キーの順番を使いたいなら std::map。
順番が不要で、検索の速さを期待したいなら std::unordered_map。
この基準で選べば、多くの基本的なケースに対応できます。
C++の連想配列を理解するうえで重要なポイント
通常の配列とは仕組みが違う
連想配列は「キーで値を取り出せる配列」のように説明されることがあります。
この表現は分かりやすいですが、厳密には通常の配列とは仕組みが異なります。
通常の配列は、要素が連続したメモリ上に並び、番号でアクセスします。
一方、std::map や std::unordered_map は、キーを使って値を探すための専用の構造を持っています。
そのため、通常の配列と同じものではなく、キーと値の対応関係を管理するコンテナとして理解するとよいです。
キーは一意であることが基本
std::map と std::unordered_map では、キーは一意です。
同じキーを複数登録することはできません。
そのため、キーには重複しにくい情報を使うのが基本です。
ユーザー名や商品名のように重複する可能性があるものよりも、ユーザーIDや商品コードのように一意に決まるもののほうがキーに向いています。
値にはさまざまな型を使える
連想配列の値には、整数や文字列だけでなく、複数のデータをまとめたものも使えます。
たとえば、1つのキーに対して複数の点数を持たせたり、ユーザー情報のまとまりを持たせたりできます。
このように、値の型を工夫することで、より複雑なデータも管理できます。
まとめ
C++の連想配列は、キーと値を対応させて管理するための便利なコンテナです。
代表的なものには、std::map と std::unordered_map があります。
std::map は、標準ではキーが昇順に並ぶため、キー順にデータを扱いたい場合に向いています。
日付順、ID順、名前順などで処理したいときに便利です。
一方、std::unordered_map は、キーの順番を保証しない代わりに、平均的に高速な検索が期待できます。
順番が不要で、キーから値をすばやく取り出したい場合に向いています。
連想配列を使うときに特に注意したいのは、存在しないキーへのアクセスです。
[] を使って存在しないキーにアクセスすると、そのキーが新しく追加され、値は初期化されます。
そのため、存在確認だけをしたい場合は、find、count、C++20以降であれば contains を使うと安全です。
基本的な選び方としては、キー順に扱いたいなら std::map、順番が不要で高速な検索を期待したいなら std::unordered_map と覚えるとよいでしょう。
以上、C++の連想配列についてでした。
最後までお読みいただき、ありがとうございました。
