配列と連結リストの違い:読み出し・挿入と削除・メモリの並び・キャッシュ・計算量を比較

配列は、同じ型の要素をメモリ上に隙間なく連続して並べ、要素番号(添字)から位置を計算して、どの要素も一定の時間で読み書きできるデータ構造です。連結リストは、各要素が値と次の要素の位置を指すポインタを持ち、それをたどってつながるデータ構造で、要素はメモリ上の離れた場所に置けます。配列は読み出しが速い代わりに、途中への挿入や削除では後ろの要素をずらす必要があります。連結リストは、位置が分かっていればポインタの付け替えだけで挿入や削除ができる代わりに、i番目の要素を読むには先頭から順にたどる必要があります。番号で読むことが多いなら配列、決まった位置での挿入や削除が多いなら連結リスト、というのが基本の使い分けです。

項目配列連結リスト
メモリ上の並び方要素が、連続した領域に隙間なく並ぶ要素はばらばらの場所に置かれ、ポインタでつながる
i番目の要素の読み出しO(1)。先頭の番地 + i × 要素の大きさ で位置を計算できるO(n)。先頭から i 回ポインタをたどる
値の探索整列していなければ O(n) の線形探索。整列済みなら、二分探索で O(log n)整列していても O(n)。途中の要素へ一度に移れないので、二分探索は使えない
途中への挿入・削除O(n)。後ろの要素を1つずつずらす挿入する位置(直前の要素)が分かっていれば O(1)。ポインタの付け替えだけで済む
末尾への追加空きがあれば O(1)。可変長配列で容量を2倍ずつ増やす方式では、ときどき全体をコピーするが、平均すると O(1)(償却)末尾の要素を指すポインタを持っていれば O(1)
大きさの変更固定長の配列は、作るときに大きさを決める。広げるには、新しい領域を確保してコピーする要素ごとに領域を確保するので、必要な分だけ増やせる
使うメモリ要素の分だけで済む(可変長配列では、予備の容量を持つ)要素ごとに、ポインタの分が余計にかかる(双方向リストなら2つ分)
キャッシュの効きやすさ効きやすい。連続して並ぶので、順に読むとキャッシュやプリフェッチがよく働く効きにくい。要素が散らばりやすいので、同じ O(n) の走査でも、配列より大幅に遅くなることがある
主な用途添字で読み書きする表、行列や画像、ソートや二分探索の対象、スタックやキューの実装挿入や削除が頻繁な列、ハッシュテーブルのチェイン法、空き領域の管理

数値は代表的な目安で、世代や製品によって異なります。

どんなときに使うか

配列
要素を番号で読み書きしたり、先頭から順に読んだりすることが多いときに向きます。キャッシュがよく効くので、今のコンピュータでは、多くの場面で配列(可変長配列を含む)の方が速くなります。
連結リスト
途中への挿入や削除が多く、その位置をすでにポインタで持っているときに向きます。要素を動かさずにつなぎ替えられるので、要素の番地が変わっては困るときにも使います。

よくある質問

計算量では連結リストの挿入が O(1) なのに、実際には配列の方が速いことがあるのはなぜですか?

理由は2つあります。1つは、挿入する位置を先頭から探すなら、その探索に O(n) かかり、挿入が O(1) でも全体では配列と同じ O(n) になることです。もう1つは、配列の要素をずらす処理は連続したメモリのコピーで、キャッシュがよく効いて非常に速い一方、連結リストのたどる処理は、要素ごとに離れた番地を読むので、キャッシュに当たりにくいことです。計算量は、データが多くなったときの増え方を表すもので、実際の速さは測って確かめます。

単方向リストと双方向リストは、どう違いますか?

単方向リストは、各要素が次の要素へのポインタだけを持ち、先頭から後ろへしかたどれません。双方向リストは、前の要素へのポインタも持つので、後ろから前へもたどれ、ある要素へのポインタさえあれば、その要素を O(1) で削除できます。その代わり、要素ごとにポインタが1つ増え、付け替えるポインタも増えます。

可変長配列(動的配列)とは何ですか?

要素が増えて容量が足りなくなったら、より大きな領域を確保して全体をコピーし、大きさを変えられるようにした配列です。容量を2倍ずつ増やすと、コピーの総数は要素数の約1倍で済むので、末尾への追加は平均すると O(1) になります。番号での読み出しは、普通の配列と同じく O(1) です。

理解度チェック

配列と比べたときの連結リスト(各要素が次の要素の位置を指すポインタを持つリスト)の特徴として正しいものはどれか。

  1. A挿入する位置の直前の要素が分かっていれば、ほかの要素を動かさずにポインタの付け替えだけで挿入できるが、i 番目の要素を読むには先頭から順にたどる必要がある
  2. Bi 番目の要素の位置を計算で求められるので、どの要素も一定の時間 O(1) で読み出せる
  3. C要素がメモリ上に隙間なく連続して並ぶので、順に読むときにキャッシュやプリフェッチが効きやすい
答えと解説を見る

正解:A. 挿入する位置の直前の要素が分かっていれば、ほかの要素を動かさずにポインタの付け替えだけで挿入できるが、i 番目の要素を読むには先頭から順にたどる必要がある

連結リストでは、挿入や削除の位置が分かっていれば、前後のポインタを書き換えるだけで O(1) で済みます。一方、i 番目の要素を読むには先頭から i 回ポインタをたどる必要があり、O(n) かかります。配列は先頭の番地と要素の大きさから i 番目の番地を計算できるので O(1) で読めますが、途中への挿入には後ろの要素をずらす O(n) の手間がかかります。また、配列は要素が連続して並ぶためキャッシュやプリフェッチが効きやすく、メモリ上に散らばりやすい連結リストは、同じ O(n) の走査でも大幅に遅くなることがあります。

可変長配列で、容量が満杯のときに要素を追加すると、2倍の容量の領域を新たに確保して全要素をコピーする。容量1の空の配列に要素を1つずつ1,000個追加したとき、コピーされる要素の総数はいくつか。

  1. A10
  2. B1,023
  3. C499,500
答えと解説を見る

正解:B. 1,023

容量は1、2、4、…、512と満杯になるたびに2倍になり、そのたびにその時点の要素をコピーするので、コピーの総数は 1 + 2 + 4 + … + 512 = 1,023 です。1,000個の追加に対してコピーは要素数の約1倍なので、追加1回あたりの手間は平均すると一定、つまり償却 O(1) になります。容量を1ずつしか増やさないと、コピーは 1 + 2 + … + 999 = 499,500 となり O(n²) です。10は領域を確保し直した回数で、コピーされる要素の数ではありません。

ハッシュテーブルの衝突を処理する方法として、チェイン法(同じ位置の要素を連結リストでつなぐ)とオープンアドレス法(表の中の空いている別の位置を探す)を比べた説明として正しいものはどれか。負荷率は、格納した要素数を表の大きさで割った値とする。

  1. Aチェイン法では負荷率が1を超えても格納できるが、オープンアドレス法では表の大きさを超えて格納できず、負荷率が1に近づくと探索の回数が急に増える
  2. Bオープンアドレス法では衝突が起こらないので、負荷率によらず探索は1回で済む
  3. Cチェイン法では、負荷率がいくら高くなっても探索の手間は変わらない
答えと解説を見る

正解:A. チェイン法では負荷率が1を超えても格納できるが、オープンアドレス法では表の大きさを超えて格納できず、負荷率が1に近づくと探索の回数が急に増える

チェイン法は同じ位置の要素をリストにつなぐので、要素数が表の大きさを超えても格納できますが、平均のリストの長さは負荷率に比例して伸び、探索の手間も増えます。オープンアドレス法は表の中だけに格納するので要素数は表の大きさまでで、負荷率が1に近づくと空きを探す回数が急に増えます。どちらの方法でも、負荷率が決めた値(例えば0.5〜0.75程度)を超えたら表を大きくして全要素を入れ直す(リハッシュ)のが普通です。オープンアドレス法でも衝突は起こり、空いた位置を探すことで対処しています。

  1. 要素数が多く、負荷率を0.5程度に保ったハッシュテーブルで、線形探査のオープンアドレス法が、チェイン法(連結リストでつなぐ方法)より速くなりやすい理由として最も適切なものはどれか。
  2. 配列の中央の要素と比べて、探す範囲を半分ずつに絞っていく二分探索を使うための前提条件として正しいものはどれか。

データ構造とアルゴリズムの練習問題へ

関連する用語の解説

配列
同じ型の値を並べて記憶し、要素番号(添字)で個々の要素を指定するデータ構造。メモリ上で連続して置かれるので、要素番号から位置を計算して、どの要素も一定の時間で読み書きできる。要素番号が0から始まる言語と、1から始まる書き方がある。
連結リスト
各要素が、値と次の要素の位置を指すポインタを持ち、それをたどってつながるデータ構造。位置が分かっていれば挿入や削除はポインタの付け替えで O(1) だが、i 番目の要素を読むには先頭からたどる O(n) が必要。要素がメモリ上に散らばりやすく、配列よりキャッシュが効きにくい。
ポインタ
変数や配列の要素、関数などが置かれているメモリの番地を値として持つ変数。C言語では int *p のように宣言し、&x で x の番地を取り出し、*p で p が指す先の値を読み書きする。ポインタに整数を足すと、指す型の大きさを単位に進む。配列の名前は多くの式で先頭の要素へのポインタに変換され、a[i] は *(a + i) と同じ意味になる。どこも指さないことを表す値を NULL ポインタという。
計算量
アルゴリズムが必要とする手間(時間計算量)や記憶領域(空間計算量)が、データの数 n に対してどのように増えるかを表したもの。O(n) や O(n log n)、O(n²) のようなオーダー記法(ビッグO記法)で、係数や低い次数の項を省いて書く。
二分探索
整列済みのデータで、中央の要素と比べて探す範囲を半分ずつに絞っていく探索の方法。n 個のデータから最大でも ⌊log₂ n⌋ + 1 回の比較で見つかり、計算量は O(log n)。データが整列されていることが前提になる。
ハッシュテーブル
キーからハッシュ関数で格納する位置を計算し、平均 O(1) で探索・挿入・削除ができるデータ構造。異なるキーが同じ位置になる衝突を、チェイン法やオープンアドレス法で処理する。ハッシュ表ともいう。
空間的局所性
あるアドレスにアクセスすると、その近くのアドレスにもまもなくアクセスしやすい性質。配列を順に読む処理が例で、キャッシュラインの単位でまとめて読み込む理由になる。

関連する比較