Menu
Coddy logo textTech

連結リスト

最終更新

連結リストはシーケンスをノードの連鎖として格納し、各ノードは値と次のノードへのポインタを保持します。配列とは異なり、ノードはメモリ上で連続していません。リストをたどるには next ポインタを追っていきます。上の再生を押すと、ノードが先頭と末尾で連結され、連鎖をたどって値が検索され、ポインタを繋ぎ変えてノードが削除される様子を見られます。

先頭への挿入や削除は O(1) です。ヘッドを繋ぎ変えるだけだからです。途中の位置や末尾に到達するのは O(n) です。まずそこまで歩く必要があるからです。このトレードオフ(端は安いがランダムアクセスがない)こそが、連結リストを配列と区別するものです。

計算量

操作計算量備考
先頭に挿入O(1)ヘッドを繋ぎ変える
末尾に挿入O(n)まず末尾まで歩く(末尾ポインタがあれば O(1))
検索O(n)next ポインタを追う
先頭を削除O(1)ヘッドをその先へ繋ぎ変える
インデックスによるアクセスO(n)ランダムアクセスなし

連結リスト対配列

観点連結リスト配列
メモリ散在するノード + ポインタ連続したブロック
ランダムアクセスO(n)O(1)
先頭への挿入/削除O(1)O(n)(シフト)
キャッシュ親和性低い高い

実例

リスト [10, 20] を構築し、5 を先頭に追加してから 20 を削除する場合:

ステップ構造動作
開始head -> null空のリスト
先頭に 10 を挿入head -> 10 -> nullnext が旧ヘッド(null)である新ノードへヘッドを繋ぎ変える
末尾に 20 を挿入head -> 10 -> 20 -> nullノード 10 まで歩き、その next を新ノード 20 に設定する
先頭に 5 を挿入head -> 5 -> 10 -> 20 -> null新ノード 5 が旧ヘッド 10 を指し、ヘッドは今 5 を指す
20 を削除head -> 5 -> 10 -> null10 まで歩き、その next を 20 から null に設定する。ノード 20 は切り離される

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

使うべきとき避けるべきとき
インデックス参照よりも、端(または保持しているノード)での挿入・削除がはるかに多いとき位置による高速なランダムアクセス(O(1) インデックス参照)が必要なとき
サイズが大きく変動し、リサイズ/コピーのコストを避けたいときキャッシュ局所性が性能を左右する密なループを回すとき
キュー、スタック、隣接リストを構築しているときメモリが逼迫しているとき(各ノードは要素ごとに余分なポインタを消費する)
他所での挿入を生き延びる、ノードへの安定した参照が必要なとき主に読み取りでほとんど変更しないとき(配列の方が単純で速い)

Linked Listのコード

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

PythonでのLinked Listのコード

Python
1class Node:2    def __init__(self, value):3        self.value = value4        self.next = None5
6
7class LinkedList:8    def __init__(self):9        self.head = None10
11    def append(self, value):12        node = Node(value)13        if self.head is None:14            self.head = node15            return16        current = self.head17        while current.next:18            current = current.next19        current.next = node20
21    def find(self, value):22        current = self.head23        while current:24            if current.value == value:25                return True26            current = current.next27        return False28
29    def delete(self, value):30        # Re-link the previous node around the match31        current, prev = self.head, None32        while current:33            if current.value == value:34                if prev is None:35                    self.head = current.next36                else:37                    prev.next = current.next38                return True39            prev, current = current, current.next40        return False41
42    def __str__(self):43        values, current = [], self.head44        while current:45            values.append(str(current.value))46            current = current.next47        return " -> ".join(values) + " -> None"48
49
50lst = LinkedList()51for value in [3, 7, 1, 9]:52    lst.append(value)53
54print("List:    ", lst)55print("find(7): ", lst.find(7))56lst.delete(1)57print("After delete(1):", lst)
このコードをPythonプレイグラウンドで実行

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

連結リストと配列の違いは何ですか?
配列は要素を1つの連続したブロックに格納するため、O(1) のランダムアクセスができますが、要素をずらす挿入/削除は O(n) です。連結リストはノードをメモリ上の任意の場所にポインタで繋いで格納するため、既知の位置での挿入/削除は O(1) ですが、連鎖をたどる必要があるためアクセスは O(n) です。
いつ連結リストを使うべきですか?
連結リストは、端(またはすでに参照を保持しているノード)での挿入・削除が頻繁で、ランダムアクセスがほとんど不要なとき、たとえばキュー、スタック、隣接リスト、LRUキャッシュで真価を発揮します。高速なインデックスアクセスが必要なら、配列や動的配列の方が通常は優れています。
連結リストの計算量はどれくらいですか?
先頭での挿入や削除は O(1) です。検索、または末尾ポインタなしで末尾に到達するのは、next ポインタを1つずつたどるため O(n) です。O(1) のランダムアクセスはありません。連結リストへのインデックス参照は O(n) です。
単方向連結リストと双方向連結リストの違いは何ですか?
単方向連結リストはノードごとに next ポインタのみを保持するため、一方向にしかたどれず、ノードの削除には先行ノードへの参照が必要です。双方向連結リストは prev ポインタを追加し、後方へのたどりと保持中ノードの O(1) 削除を可能にしますが、ノードごとに余分なポインタが必要となり、挿入・削除のたびに管理が増えます。
先頭への挿入が O(1) なのに、なぜ末尾への挿入は O(n) なのですか?
先頭への挿入は head ポインタを繋ぎ変えるだけで、これは定数時間の作業です。末尾に到達するには、ヘッドから末尾まで next ポインタをたどる必要があり、これは O(n) です。別途 tail ポインタを保持すれば末尾への挿入も O(1) になるため、実際のリストはしばしば両端を追跡します。
連結リストはどこでも O(1) で挿入できますか?
いいえ、それはよくある誤解です。挿入自体が O(1) なのは、挿入先の直前のノードへのポインタをすでに保持している場合だけです。その位置を値やインデックスで見つけるのは、たどり着くために連鎖を歩く必要があるため、依然として O(n) かかります。
Coddy programming languages illustration

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

始める