Menu
Coddy logo textTech

双方向連結リスト

最終更新

双方向連結リストは、各ノードが2つのポインタを持つ連結リストです: next(次のノードへ)と prev(前のノードへ)。この余分な後方リンクにより、両方向へたどることができ、すでに参照を持っているノードを O(1) で削除できます。先頭からたどらずに、直接その前のノードへ到達できるからです。上の再生ボタンを押すと、ノードが両方向へつながり、つなぎ直される様子を確認できます。

その代償は、ノードごとに1つ余分なポインタと、より多くの管理です。挿入と削除のたびに、隣接ノードの nextprev の両方を更新しなければなりません。これは、両端からの高速な削除が重要となる多くのデック(deque)や LRU キャッシュの背後にある構造です。

時間計算量

操作計算量備考
先頭/末尾への挿入O(1)先頭と末尾のポインタがある場合
既知のノードの削除O(1)prev に直接到達、走査なし
探索O(n)依然として線形走査
インデックスによるアクセスO(n)ランダムアクセス不可

単方向連結リストと双方向連結リストの比較

観点単方向双方向
ノードあたりのポインタ1 (next)2 (next + prev)
後方へのたどり不可
既知のノードの削除O(n) (prev を探す)O(1)
メモリのオーバーヘッド低い高い

具体例で追う

末尾に追加して [10, 20, 30] を作り、その後ノード 20 を削除する:

ステップ構造動作
開始null空のリスト: headtail はどちらも null
10 を追加10最初のノード。headtail がこれを指し、prev = next = null
20 を追加10 <-> 2010.next = 2020.prev = 10 を設定。tail = 20
30 を追加10 <-> 20 <-> 3020.next = 3030.prev = 20 を設定。tail = 30
20 を削除10 <-> 3020.prev20.next を使い、10.next = 3030.prev = 10O(1) で設定

双方向連結リストを使うべき場面

使うべき場面避けるべき場面
両方向にたどる、または継ぎ足す必要がある前方にしか進まない: 単方向リストの方が軽量
すでに参照を持つ任意のノードを削除する (O(1))主に位置でインデックスする: O(1) アクセスには配列を使う
デック、LRU キャッシュ、取り消し履歴を実装するメモリが逼迫している: 余分な prev ポインタがリンクのオーバーヘッドを倍増させる
挿入と削除が両端で頻繁に発生するデータが小さくめったに変わらない: 配列のコピーの方が安い

Doubly Linked Listのコード

Python, JavaScript, Java, C++, Cによるクリーンで実行可能なDoubly Linked Listの実装です。言語を選んでコードをコピーするか、Coddyプレイグラウンドに読み込んだ状態で開けます。

PythonでのDoubly Linked Listのコード

Python
1class Node:2    def __init__(self, value):3        self.value = value4        self.prev = None5        self.next = None6
7
8class DoublyLinkedList:9    def __init__(self):10        self.head = None11        self.tail = None12
13    def append(self, value):14        node = Node(value)15        if self.tail is None:16            self.head = self.tail = node17            return18        # Wire both directions: old tail <-> new node19        node.prev = self.tail20        self.tail.next = node21        self.tail = node22
23    def prepend(self, value):24        node = Node(value)25        if self.head is None:26            self.head = self.tail = node27            return28        node.next = self.head29        self.head.prev = node30        self.head = node31
32    def forward(self):33        values, current = [], self.head34        while current:35            values.append(current.value)36            current = current.next37        return values38
39    def backward(self):40        values, current = [], self.tail41        while current:42            values.append(current.value)43            current = current.prev44        return values45
46
47lst = DoublyLinkedList()48for value in [2, 3, 4]:49    lst.append(value)50lst.prepend(1)51
52print("Forward: ", lst.forward())53print("Backward:", lst.backward())
このコードをPythonプレイグラウンドで実行

双方向連結リストのよくある質問

単方向連結リストと双方向連結リストの違いは何ですか?
単方向連結リストはノードごとにポインタが1つ(next)なので、前方にしか進めません。双方向連結リストは prev ポインタを追加し、後方へのたどりや、参照を持つノードの O(1) での削除を可能にします。その代償として、ノードごとに余分なポインタが1つ増え、変更のたびに2つのリンクを更新する必要があります。
双方向連結リストが余分なメモリに見合うのはいつですか?
後方へのたどりや、すでに参照している任意のノードの O(1) 削除が必要なときです。典型例はデック(両端キュー)と LRU キャッシュで、エントリが両端で頻繁に移動・追い出しされます。
なぜ双方向連結リストはノードを O(1) で削除できるのですか?
ノードを取り除くには、その前のノードと次のノードをつなぐ必要があります。単方向連結リストでは前のノードを見つけるために先頭からたどる必要があります(O(n))が、双方向連結リストではノードの prev ポインタが前のノードを直接与えるため、つなぎ直しは O(1) です。
双方向連結リストと配列、どちらを使うべきですか?
インデックスによる O(1) アクセスやキャッシュに優しい反復が必要で、挿入が主に末尾で行われる場合は配列を使います。ノード参照がある状態で中間や両端に頻繁に挿入・削除する場合は双方向連結リストを使います。これは配列の O(n) のシフトに対して O(1) だからです。配列はメモリと局所性で勝り、リストは構造的な編集で勝ります。
双方向連結リストにおける番兵(sentinel)ノードとは何ですか?
番兵(またはダミー)ノードは、リストが本当に空になることがないように、先頭の前や末尾の後に置くプレースホルダです。headtail、境界での挿入/削除に関する null チェックを取り除き、すべての操作を同じつなぎ直しのコード経路に従わせます。多くの実運用の LRU キャッシュ実装では、両端を特別扱いしないために2つの番兵を使います。
双方向連結リストからの削除で最もよくあるバグは何ですか?
先頭または末尾のノードを削除するときに headtail の更新を忘れることで、解放されたノードへのダングリングポインタが残ります。隣接ノードをつなぎ直す前に、node.prevnull か(head を更新)、node.nextnull か(tail を更新)を必ず確認してください。
Coddy programming languages illustration

Coddy でアルゴリズムをマスターしよう

始める