双方向連結リスト
双方向リストのノードは next と prev を持ち、リストは head と tail を保存します。逆向きにたどれ、対象ノードをすでに持っていれば、前を探さず O(1) で外せます。
代価はメモリと参照の管理です。途中への挿入は新ノードの二参照と隣接ノードの各一参照、計四回を書きます。片側を忘れると、順方向と逆方向で異なる列を見てしまいます。
双方向リストの仕組み
ノードは両端に連結器のある車両のようなものです。対象を見つけた後は、前後の車両をつなげば取り外せます。
pred と succ の間への挿入は newNode.prev = pred、newNode.next = succ、pred.next = newNode、succ.prev = newNode と書きます。画面は新ノードが両側を指す段階と、両側が新ノードを指す段階に分けます。
内部ノードの削除では node.prev と node.next が隣接点を与えます。node.prev.next = node.next と node.next.prev = node.prev で O(1) の取り外しです。ただし値による発見は O(n)。単方向リストで対象だけを持つ場合と違い、前のノードの情報が保存されています。
search は head から next、search_back は tail から prev をたどります。比較は同じで方向が逆です。
容量スライダーの 4–10、既定 7 は画面とコードの max に対応します。双方向リスト自体の固定容量ではありません。
操作をステップで追う
空から始めます。
- 先頭に 5:
5.prev、5.nextは null。headとtailが 5 を指す。 - 先頭に 8:
8.next = 5、8.prev = null、次に5.prev = 8、headを 8 へ。8 ⇄ 5。 - 末尾に 3:
tailで 5 に直接到達。3.prev = 5、3.next = null、次に5.next = 3、tailを 3 へ。8 ⇄ 5 ⇄ 3。 - 添字 1 に 7:
currは添字 0 の前のノード 8 へ。7.prev = 8、7.next = 5、次に8.next = 7、5.prev = 7。8 ⇄ 7 ⇄ 5 ⇄ 3。 - 前向きに 7 を検索:添字 0 で
8 ≠ 7、1 で7 = 7。1 を返す。 - 後ろ向きに 5 を検索:末尾の添字 3 で
3 ≠ 5、prevをたどり添字 2 で5 = 5。2 を返す。 - 7 を削除:
currは前のノードではなく対象 7 に到達。そのprevは 8、nextは 5。8.next = 5、5.prev = 8と書いて外す。8 ⇄ 5 ⇄ 3。
画面の上側が next、下側が prev です。新ノードはポインタの書き込みが完了するまで列の上で待ちます。削除時はカーソルが対象を指し、両方の迂回矢印がそこを飛び越えます。
時間・空間計算量
| 操作 | 費用 | 理由 |
|---|---|---|
| 添字アクセス | O(n) | 一端から参照をたどる |
| 先頭挿入 | O(1) | 定数回の書き込み |
| 末尾挿入 | O(1) | tail を保存 |
| 添字 k へ挿入 | O(k) | この実装は前向きに前のノードを探す |
| 保持済みノードの削除 | O(1) | prev と next が両側を示す |
| 値による削除 | O(n) | 発見 O(n)、取り外し O(1) |
| 順・逆方向の検索 | O(n) | ノードごとの比較 |
| 空間 | O(n) | 値一つと参照二つ |
head だけの単方向リストに比べ、逆向き参照と tail を追加しています。逆向き参照は逆走と既知ノードの削除を支えます。末尾挿入 O(1) は tail の効果で、単方向リストにも尾への参照だけを追加できます。
境界ケース
- 空への挿入:唯一のノードが
headとtail。両隣は空。 - 先頭の削除:
node.prevは空なのでhead = node.next。新先頭があればhead.prev = null。 - 末尾の削除:
tail = node.prev。新末尾があればtail.next = null。 - 唯一のノードの削除:両隣が空で、
headとtailをともに空にする。 - 値がない:どちら向きでも端まで進み、削除は
False、検索は-1。例外はない。 - 末尾を超える挿入添字:表示コードには完全な範囲検査がなく、空参照に達する。可視化は停止して説明する。
k = 0はinsert_head、k = sizeはinsert_tail。
よくある間違い
- 一方向だけ更新する。
pred.next = newNodeだけでは、逆走時に新ノードを飛ばします。succ.prev = newNodeも必要です。 - 端の参照を忘れる。 頭尾を削除したら
head、tailを更新し、一ノードなら両方を扱います。 - 保持済みノードの前を再探索する。
prevに保存してあるので、再走査は構造の利点を捨てます。 - 常に双方向を選ぶ。 一端での出し入れと前向き走査だけなら追加参照が不要な場合があります。操作に応じて単方向リストや配列を選びます。
続けて学ぶ
同じ手法につながるレッスンや、次の概念への入口です。
誤りや気になる点を見つけましたか? フィードバックを送る。