資格道場 › レッスン一覧 › アルゴリズムとプログラミング
アルゴリズムとプログラミング・7/13・10分

連結リストの構造とポインタ操作

ゴール

連結リスト(データ部+ポインタ部)と配列の長所・短所を見分け、双方向リストや配列で作ったリストに要素を挿入したとき「どのポインタが書き換わるか」を自力で答えられるようになる。

配列とリストの違いは、科目Aで形を変えながら何度も出る定番です(観測値で本試験系5回・免除8回)。ポインタの付け替えは科目Bの擬似言語問題でもそのまま使うので、ここで手順を体に入れておくと両方で得点できます。

このレッスンで稽古する(用語チェック・過去問)

結論:リストは「データ+次の場所」を数珠つなぎにしたもの

**線形リスト(連結リスト)**は、各要素が データ部(値そのもの)と ポインタ部(次の要素が置いてある場所=アドレス)の2つを持つデータ構造です。要素はメモリ上でバラバラに置かれていてよく、ポインタをたどって順番に読みます。最後の要素のポインタには「次は無い」ことを示す値(0やNULL)を入れます。

たとえるなら宝探しのメモです。1枚目のメモに「宝その1」と「次のメモは本棚の上」と書いてあり、そこへ行くと次のメモがある、という具合です。途中のメモは、前から順にたどらないと見つかりません。

番地は飛び飛びでも、ポインタでつながる番地100(先頭)A次:300番地300B次:200番地200C次:NULL左=データ部 右=ポインタ部(次の番地)
要素はメモリ上で飛び飛びの番地に置かれていても、ポインタ部が次の番地を持っているので順にたどれる。

配列とリストの比較(ここが一番出る)

配列は、同じ型のデータを連続した領域に並べ、添字(何番目かを表す番号)で直接アクセスする構造です。リストとは長所と短所がちょうど逆になっています。

観点 配列 連結リスト(ポインタで実現)
k番目の要素を読む 添字で直接アクセス(要素数によらず一定時間) 先頭から順にたどる(要素数に比例)
途中への挿入・削除 後ろの要素をずらす必要あり(要素数に比例) ポインタの付け替えだけ(位置が分かっていれば一定時間)
2分探索 向いている(真ん中に直接飛べる) 向いていない(真ん中に飛べない)
領域 最大長分を先に確保→未使用領域が出やすい 必要な分だけ確保。ただしポインタ部の領域が余分に要る

覚え方は1つだけです。「配列は読むのが得意、リストは書き換えるのが得意」。選択肢がどちらの長所を述べているかを判定すれば、ほぼ解けます。

配列:Xを2番目に入れるABCDAXBCD後ろの要素を全部ずらす(要素数に比例)リスト:ポインタの付け替えだけABX
同じ「途中に1つ入れる」でも、配列は後ろを全部ずらし、リストはポインタを付け替えるだけで済む。

単方向リストと双方向リスト

ポインタが「次」だけのものを単方向リスト、「次」と「前」の2つを持つものを双方向リストと呼びます。双方向は逆向きにもたどれる代わりに、挿入・削除で書き換えるポインタが増えます。

種類 ポインタ 間に新要素Gを入れるとき既存要素で変わるもの
単方向リスト 次ポインタのみ 前の要素の次ポインタだけ(1か所)
双方向リスト 次ポインタ+前ポインタ 前の要素の次ポインタと、後ろの要素の前ポインタ(2か所)

どちらの場合も、新要素G自身のポインタは「新しく書き込む」もので、既存要素の値が変わるわけではありません。設問が「追加前と比べて値が変わるポインタ」を聞いているときは、既存要素だけを数えます。

flowchart LR
  A["A(前の要素)"] -->|"次: Gへ書き換え"| G["G(新要素)"]
  G -->|"次: K"| K["K(後ろの要素)"]
  K -.->|"前: Gへ書き換え"| G
  G -.->|"前: A"| A

挿入の基本手順(単方向・双方向共通の考え方)

挿入は必ず**「新要素のポインタを先に決める → 前の要素のポインタを新要素へ向ける」**の順で考えます。先に前の要素を書き換えると、元の「次」がどこだったか分からなくなるからです。

  • 新要素の「次」= 前の要素が元々指していた場所
  • 前の要素の「次」= 新要素の場所
  • (双方向なら)後ろの要素の「前」= 新要素の場所、新要素の「前」= 前の要素の場所

削除は逆で、消したい要素の前の要素の「次」を、消したい要素の「次」に付け替えるだけです。

① Gの「次」を先に決める(=K)AKG(新)Aの次=K(まだそのまま)② Aの「次」をGへ向けるAKG(新)逆の順だとKの場所が分からなくなる
挿入は「新要素Gの次を先に決める→前の要素Aの次をGへ向ける」の順。逆にするとKの場所が分からなくなる。

配列2本でリストを作る問題

ポインタが使えない場面を想定し、**値を入れる配列(box)と次の要素の添字を入れる配列(next)**の2本でリストを表すことがあります。このとき next の値は「アドレス」ではなく「添字」ですが、考え方はポインタとまったく同じです。

最大の落とし穴は、リストの順番は添字の順ではないことです。必ず先頭(next[0]など)から next をたどって、1番目・2番目…を書き出してから挿入位置を決めます。

添字boxnext01234—BDAC34012たどった順(これがリストの順番)先頭ABCD添字0添字3添字1添字4添字2next=0で終わり添字の順(B,D,A,C)とは違う
next配列を先頭(添字0)からたどると A→B→C→D の順になり、添字の並び(B,D,A,C)とは違う。

メモリ割当て失敗に備える(1〜2文で十分)

リストの要素は実行中に領域割付け関数(動的にメモリを確保する関数)で作ることが多く、メモリ不足などで失敗する場合があります。そのためコーディング規約では「割付けに失敗したときの処理を必ず書く」と定めるのが適切です。

試験での問われ方

  • 配列とリストの特徴を入れ替える:「配列は挿入・削除が効率的」「リストは任意の要素に直接アクセスできる」「リストは2分探索が得意」はすべて誤り。
  • 他のデータ構造と混ぜる:「ポインタから次の要素をハッシュ関数で求める」(ハッシュ表の話)、「先頭を根とした木」(木構造の話)は誤り。
  • ついでに他の構造の定義をずらす:「スタックは先入れ先出し」(正しくは後入れ先出し)、「2分木は全ての節が2つの子を持つ」(子は最大2つ、0や1でもよい)。
  • 双方向リストの書き換え箇所を多めに見せる:前の要素の「前ポインタ」や後ろの要素の「次ポインタ」まで変わるように見せる選択肢がある。
  • next配列の順番の取り違え:添字順にリストが並んでいると思わせる。
例題 1

双方向リストで社員Aと社員Kの間に社員Gを入れる

双方向のポインタをもつリスト構造のデータを表に示す。この表において新たな社員 G を社員 A と社員 K の間に追加する。追加後の表のポインタ a ~ f の中で追加前と比べて値が変わるポインタだけを全て列記したものはどれか。
表
アドレス社員名次ポインタ前ポインタ
100社員 A3000
200社員 T0300
300社員 K200100
追加後の表
アドレス社員名次ポインタ前ポインタ
100社員 Aab
200社員 Tcd
300社員 Kef
400社員 Gxy

ア a,b,e,f
イ a,e,f
ウ a,f
エ b,e

出典:令和5年度 基本情報技術者試験 科目A 公開問題 問2

解き方と答えを見る
  1. まず今の並びを確認する。A(100)の次=300(K)、K(300)の次=200(T)、T(200)の次=0(終わり)。よって順番は A → K → T。
  2. 前ポインタも確認する。A(100)の前=0(先頭)、K(300)の前=100(A)、T(200)の前=300(K)。
  3. 新要素G(アドレス400)をAとKの間に入れる。Gの次=300(K)、Gの前=100(A)。これは新しく書く x, y なので、設問の a〜f には含まれない。
  4. 前の要素Aの次ポインタ a:300 → 400 に変わる。
  5. 後ろの要素Kの前ポインタ f:100 → 400 に変わる。
  6. 変わらないものを確認する。Aの前ポインタ b=0 のまま、Kの次ポインタ e=200 のまま、Tの c=0・d=300 もそのまま。
  7. 変わるのは a と f だけ。

答え:ウ(a,f)

例題 2

box/next の2配列リストで3番目と4番目の間にHを入れたときの next[8]

リストを二つの 1 次元配列で実現する。配列要素 box[i] と next[i] の対がリストの一つの要素に対応し,box[i] に要素の値が入り,next[i] に次の要素の番号が入る。配列が図の状態の場合,リストの 3 番目と 4 番目との間に値が H である要素を挿入したときの next[8] の値はどれか。ここで,next[0] がリストの先頭(1 番目)の要素を指し,next[i] の値が 0 である要素はリストの最後を示し,next[i] の値が空白である要素はリストに連結されていない。
添字 0〜9 の表。box: 0=(空白),1=A,2=B,3=C,4=D,5=E,6=F,7=G,8=H,9=I。next: 0=1,1=5,2=0,3=7,4=(空白),5=3,6=(空白),7=2,8=(空白),9=(空白)。

ア 3
イ 5
ウ 7
エ 8

出典:平成30年度 春期 基本情報技術者試験 午前 問6

解き方と答えを見る
  1. 先頭から next をたどって順番を書き出す。next[0]=1 → 1番目は添字1(A)。
  2. next[1]=5 → 2番目は添字5(E)。
  3. next[5]=3 → 3番目は添字3(C)。
  4. next[3]=7 → 4番目は添字7(G)。
  5. next[7]=2 → 5番目は添字2(B)。next[2]=0 なのでここで終わり。順番は A → E → C → G → B。
  6. Hは添字8に入っている。3番目(C, 添字3)と4番目(G, 添字7)の間に入れる。
  7. 新要素の次を先に決める:next[8] = Cが元々指していた先 = next[3] の元の値 = 7。
  8. 前の要素を新要素へ向ける:next[3] = 8。
  9. よって next[8] の値は 7。

答え:ウ(7)

要点(試験直前に読む)

  • 線形リスト=データ部+ポインタ部(次の要素の格納先)。
  • 配列は「読むのが得意」(添字で直接アクセス・2分探索向き)、リストは「書き換えが得意」(ポインタ付け替えで挿入・削除)。
  • 配列で作ったリストは最大長分を先に確保するので未使用領域が出る。リストはポインタ部の領域が余分に要る。
  • 双方向リストで間に挿入すると、既存で変わるのは「前の要素の次」と「後ろの要素の前」の2つだけ。
  • next配列の問題は、先頭から必ずたどって順番を書き出す。新要素のnextは前の要素が元々指していた値。
  • 領域割付け関数を使うときは、失敗したときの処理を書く。

ひっかけ

  • 「配列は挿入・削除が効率的」「リストは任意の要素に直接アクセスできる」→長所と短所が逆。「配列は読む、リストは書き換え」で判定する。
  • 「リストは2分探索が効率的」「ハッシュ関数で次の要素を求める」→2分探索は配列向き、ハッシュは別の構造。
  • 双方向リストで、前の要素の前ポインタや後ろの要素の次ポインタまで変わると思わせる→間の側を向いたポインタだけが変わる。
  • next配列で添字の順番をそのままリストの順番と思い込む→先頭からnextをたどって並べ直す。
  • 新要素のnextに前の要素の番号を入れる→入れるのは前の要素が元々指していた「次」の番号。

用語(15)

線形リスト せんけいりすと
要素を一直線の順序でつなぎ、各要素がデータ部と次の要素を指すポインタ部を持つデータ構造。
⚠ 配列は連続した領域に並べて添字で直接アクセスする。リストはポインタをたどる。
連結リスト れんけつりすと(linked list)
ポインタで要素同士をつないだリストのことで、試験では線形リストとほぼ同じ意味で使われる。
データ部 でーたぶ
リストの各要素のうち、値そのものを入れる部分。
⚠ ポインタ部は値ではなく「次の要素の場所」を入れる部分。
ポインタ部 ぽいんたぶ
リストの各要素のうち、次(双方向なら前も)の要素の格納先を入れる部分。
⚠ ハッシュ関数で次の場所を計算するわけではない。値をそのまま保持している。
ポインタ pointer
データが置かれているメモリ上の場所(アドレス)を指し示す値。
⚠ 添字は配列の中での番号。配列2本で作るリストでは、next に入る添字がポインタの役割をする。
単方向リスト たんほうこうりすと
各要素が次の要素へのポインタだけを持ち、先頭から後ろへの一方向にしかたどれないリスト。
⚠ 双方向リストは前ポインタも持つので逆向きにもたどれる。
双方向リスト そうほうこうりすと
各要素が次の要素と前の要素の両方へのポインタを持つリスト。
⚠ 挿入時に変わる既存ポインタは、前の要素の次ポインタと後ろの要素の前ポインタの2つ。
配列 はいれつ(array)
同じ型のデータを連続した領域に並べ、添字で任意の要素に直接アクセスできるデータ構造。
⚠ 途中への挿入・削除は要素の移動が要るので苦手。ポインタの付け替えだけで済むのはリスト。
添字 そえじ(index)
配列の何番目の要素かを表す番号。
⚠ リストの並び順と添字の順は一致するとは限らない。
直接アクセス(ランダムアクセス) ちょくせつあくせす(random access)
位置を指定するだけで、前から順にたどらずに任意の要素を読めること。
⚠ 順次アクセスは先頭から順にたどる方式で、リストはこちら。
2分探索 にぶんたんさく(binary search)
整列済みのデータの真ん中と比べて探す範囲を半分ずつ絞る探索法。
⚠ 真ん中に直接飛ぶ必要があるので配列向き。リストでは効率が悪い。
スタック stack
最後に入れたデータを最初に取り出す(後入れ先出し・LIFO)データ構造。
⚠ 先入れ先出し(FIFO)はキュー。
キュー queue
最初に入れたデータを最初に取り出す(先入れ先出し・FIFO)データ構造。
⚠ スタックはLIFO。
2分木 にぶんぎ(binary tree)
各節が持つ子が最大2つの木構造。
⚠ 「全ての節が2つの子を持つ」は誤り。子が0個や1個の節があってもよい。
領域割付け関数 りょういきわりつけかんすう
プログラム実行中に必要な分のメモリ領域を確保する関数。
⚠ 必ず成功するわけではないので、失敗時の処理を書くことがコーディング規約の適切な規定。

このレッスンの過去問

このレッスンで稽古する