配列(Array)とは|メモリ配置・性能・静的/動的・多次元の違いと実践的な使い分けガイド

Array とは — 概要

配列(Array)は、同じ種類(あるいは少なくとも同じ扱い方をする)の要素を連続的に並べて格納するデータ構造であり、要素へのインデックス(添字)による直接アクセスを特徴とします。プログラミングにおいて最も基本的かつ重要なデータ構造の一つであり、アルゴリズムやライブラリの基盤となっています。

配列の基本的な特徴

  • 連続したメモリ領域: 多くの実装では要素がメモリ上で連続して格納されるため、インデックス計算で任意要素へ高速にアクセスできる(アドレス = 基底アドレス + インデックス × 要素サイズ)。
  • ランダムアクセス: インデックスが分かれば要素への読み書きが定数時間(O(1))でできるのが典型。
  • 固定長/可変長の差: 言語や実装によっては配列の長さが作成時に固定(例: C, Java の配列)、あるいは実行時に拡張・縮小できる(動的配列、例: C++ の std::vector、Python の list、JavaScript の Array)。
  • 境界チェックの有無: 一部言語(Java、Python 等)は配列アクセス時に範囲チェックを行い例外を投げるが、C言語の生配列は行わずバッファオーバーフローにつながることがある。
  • 多次元配列の表現: 2次元以上は「真の多次元配列(連続領域での行優先/列優先)」か「配列の配列(ジャグ配列)」として実装される場合がある。

メモリ配置と性能(キャッシュ局所性)

配列が連続したメモリ領域に配置されることの最大の利点はCPUキャッシュの効率的利用です。連続した要素を順にアクセスする(線形走査する)場合、キャッシュヒット率が高くなりメモリアクセスが高速化されます。これが配列ベースのアルゴリズムが実装上有利になる主因の一つです。

一方、配列の途中に要素を挿入・削除するときは、後続要素のシフトが必要であり最悪でO(n)のコストがかかります。大量の頻繁な挿入・削除が必要な場合はリンクドリストや別のデータ構造を検討しますが、実務ではメモリ管理やキャッシュ効率の観点から配列ベースの構造(例: 動的配列)を使うことが多いです。

静的配列と動的配列(可変長配列)の違い

  • 静的配列: 作成時にサイズが決まり、変更できない。Cの配列やJavaの配列は典型的な例。固定サイズゆえにオーバーヘッドが少なく、要素の配置も単純。
  • 動的配列(リサイズ可能): 要素が追加されると内部で必要に応じて容量を拡張する。拡張時には新しい大きな領域を確保して既存要素をコピーするという操作が起こるが、増加戦略(倍増など)により平均的には追加操作が「アモルタイズドで」O(1)になる。C++ の std::vector、Java の ArrayList、Python の list がこれに当たる(内部実装や成長戦略は実装に依存)。

多次元配列とジャグ配列

「多次元配列」と聞くと二次元のテーブル状データを想像しますが、実装には大きく二種類あります。

  • 連続領域の多次元配列(行優先・列優先): C言語の int a[M][N]; のように連続領域として格納され、Cは行優先(row-major)、Fortran は列優先(column-major)です。行単位でアクセスする場合は行優先の配置がキャッシュ効率良くなります。
  • ジャグ配列(配列の配列): 各行が独立した配列として確保される形式で、行ごとに長さが異なる(不揃いの行長)ことが可能。Java や多くの高級言語の二次元表現はこちらが基本です。

言語ごとの特性(主な例)

  • C言語: 配列は連続領域に要素を並べた連続記憶。配列名は基底アドレスに暗黙的に変換される場合が多い(ポインタとの関連)。要素アクセスに境界チェックは無く、メモリ安全性はプログラマに依存する。
  • C++: 生配列に加え、std::array(固定長ラップ)や std::vector(動的配列)など標準ライブラリで安全かつ便利に扱えるコンテナがある。std::vector は連続領域を保証する。
  • Java: new で作る配列は固定長。配列アクセス時に範囲外なら ArrayIndexOutOfBoundsException が投げられる。配列はプリミティブ型なら値を、参照型なら参照を格納する。
  • JavaScript: Array オブジェクトは動的で長さ可変、異なる型の要素を混在させることが可能。エンジンは内部で連続領域(packed)や疎なオブジェクト(dictionary)など最適化を行う。
  • Python: list は可変長配列として実装され(CPython の実装上オーバーアロケーションを伴う)、高速なランダムアクセスとアモルタイズド挿入を提供する。数値計算では numpy の ndarray が連続領域で同種の要素を扱い性能的に有利。

アルゴリズム的な観点と計算量

  • ランダムアクセス(indexing): O(1)
  • 末尾への追加(動的配列): アモルタイズドで O(1)。リサイズ時は O(n) のコピーコストが発生する。
  • 先頭/途中への挿入・削除: 平均 O(n)(シフトが必要)
  • 検索(線形探索): O(n)。ソート済み配列なら二分探索で O(log n) の検索が可能。

これらの性質に基づいて、用途に応じて配列を選択します。例えば、頻繁にランダムアクセスが必要であれば配列が最適、頻繁に大小の挿入削除があるなら別構造の検討が必要です。ただし実際の性能はメモリレイアウトやキャッシュ効率の影響も大きく、理論的な計算量だけでなく実装コストやキャッシュ局所性も考慮します。

安全性と落とし穴

  • 境界外アクセス: 言語によっては例外やエラーで検出されるが、Cのようにチェックがない場合はバッファオーバーフロー(セキュリティ脆弱性)につながる。
  • 未初期化メモリ: 生配列を使う場合は要素の初期化を忘れると未定義動作を招く。
  • 参照とコピーの違い: 言語によって配列を代入したときの動作が異なる(参照を共有するのか、要素をコピーするのか)ため、意図しない副作用に注意。
  • 多次元の走査順: 行優先・列優先の違いでアクセス順を誤るとキャッシュミスが増え、性能が大きく低下する。
  • 型と同種性: 高級言語では配列に混在型を入れられるが、数値演算やSIMD最適化を効かせたい場合は同種の要素だけを格納する連続領域(例: numpy.ndarray、C の配列)が望ましい。

実践的な設計上のヒント

  • ランダムアクセスを多用する処理やソート・二分探索などを使うなら配列が有利。
  • サイズ変動が予想される場合は動的配列(std::vector、ArrayList、Python list 等)を使う。事前に容量を reserve/ensure することでリサイズ回数を減らせる。
  • 大きな数値配列で高性能を要求するなら、型が均質な連続配列(C 配列、std::vector、numpy.ndarray)を選ぶ。
  • 多次元データを扱うときは、走査方向(行優先か列優先か)を意識してローカリティを最大化する。
  • 外部入力など信用できない長さ情報を使って配列アクセスを行うときは必ず境界チェックを行う(C/C++ では明示的に行う)。

配列と密接に関係する概念

  • スライス/ビュー: 配列の一部を参照する仕組み。コピーを伴う場合と参照のみで済む場合(ビュー)がある。例: Python のスライスは新しい list を生成するが、numpy のスライスはビューである。
  • バッファ/メモリマップ: 大きな配列データをファイルに対するメモリマップで扱うことで I/O の効率化がはかれる。
  • SIMD・ベクトル化: 同種の連続した数値要素があると、CPU のベクトル命令で処理を並列化しやすく高速化に寄与する。

まとめ

配列はプログラミングにおける基本中の基本であり、ランダムアクセスの高速性、メモリ連続性によるキャッシュメリット、単純なメモリ計算で実装できる点が最大の利点です。一方で挿入・削除やサイズ変更のコスト、言語による境界チェックの有無、そして多次元データの配置と走査順による性能差など、設計上の注意点も多く存在します。用途(高速な読み取り、頻繁な追加・削除、大規模数値計算など)に応じて、静的配列・動的配列・ジャグ配列・専用の数値配列(numpy 等)などを選択することが重要です。

参考文献