Day 2: 配列と連結リスト
今日学ぶこと
- 配列の仕組みと操作の計算量
- 連結リストの仕組みと実装
- 単方向・双方向連結リストの違い
- 配列と連結リストの使い分け
配列(Array)
配列は、メモリ上に連続した領域にデータを格納するデータ構造です。インデックスを使って任意の位置に瞬時にアクセスできます。
flowchart LR
subgraph Array["配列(メモリ上の連続領域)"]
A0["[0]\n42"]
A1["[1]\n17"]
A2["[2]\n93"]
A3["[3]\n8"]
A4["[4]\n56"]
end
style Array fill:#3b82f6,color:#fff
配列の基本操作
// Create an array
const arr = [42, 17, 93, 8, 56];
// Access by index: O(1)
console.log(arr[2]); // 93
// Search for a value: O(n)
console.log(arr.indexOf(93)); // 2
// Add to end: O(1) amortized
arr.push(71);
// Add to beginning: O(n) - all elements must shift
arr.unshift(0);
// Remove from end: O(1)
arr.pop();
// Remove from beginning: O(n)
arr.shift();
// Insert at middle: O(n)
arr.splice(2, 0, 99);
配列の計算量
| 操作 | 計算量 | 理由 |
|---|---|---|
| インデックスアクセス | O(1) | メモリアドレスの直接計算 |
| 末尾に追加 | O(1)* | 末尾のアドレスは既知 |
| 先頭に追加 | O(n) | 全要素をシフト |
| 中間に挿入 | O(n) | 挿入位置以降をシフト |
| 値の検索 | O(n) | 先頭から順に比較 |
| 末尾の削除 | O(1) | 末尾のアドレスは既知 |
| 先頭の削除 | O(n) | 全要素をシフト |
*償却計算量(amortized):配列の拡張が必要な場合はO(n)
連結リスト(Linked List)
連結リストは、各要素(ノード)がデータと次のノードへの**ポインタ(参照)**を持つデータ構造です。メモリ上に連続している必要はありません。
flowchart LR
H["Head"] --> N1["42\nnext →"]
N1 --> N2["17\nnext →"]
N2 --> N3["93\nnext →"]
N3 --> N4["8\nnext →"]
N4 --> NULL["null"]
style H fill:#f59e0b,color:#fff
style N1 fill:#3b82f6,color:#fff
style N2 fill:#3b82f6,color:#fff
style N3 fill:#3b82f6,color:#fff
style N4 fill:#3b82f6,color:#fff
単方向連結リストの実装
class Node {
constructor(value) {
this.value = value;
this.next = null;
}
}
class LinkedList {
constructor() {
this.head = null;
this.size = 0;
}
// Add to beginning: O(1)
prepend(value) {
const node = new Node(value);
node.next = this.head;
this.head = node;
this.size++;
}
// Add to end: O(n)
append(value) {
const node = new Node(value);
if (!this.head) {
this.head = node;
} else {
let current = this.head;
while (current.next) {
current = current.next;
}
current.next = node;
}
this.size++;
}
// Remove from beginning: O(1)
removeFirst() {
if (!this.head) return null;
const value = this.head.value;
this.head = this.head.next;
this.size--;
return value;
}
// Search: O(n)
find(value) {
let current = this.head;
while (current) {
if (current.value === value) return current;
current = current.next;
}
return null;
}
// Insert at position: O(n) to find position, O(1) to insert
insertAt(index, value) {
if (index === 0) return this.prepend(value);
const node = new Node(value);
let current = this.head;
for (let i = 0; i < index - 1 && current; i++) {
current = current.next;
}
if (!current) return;
node.next = current.next;
current.next = node;
this.size++;
}
// Convert to array for display
toArray() {
const result = [];
let current = this.head;
while (current) {
result.push(current.value);
current = current.next;
}
return result;
}
}
// Usage
const list = new LinkedList();
list.prepend(3);
list.prepend(2);
list.prepend(1);
list.append(4);
console.log(list.toArray()); // [1, 2, 3, 4]
双方向連結リスト
双方向連結リストでは、各ノードが前のノードへの参照も持ちます。これにより、末尾からの操作も O(1) で行えます。
flowchart LR
H["Head"] --> N1
N1["← 42 →"] <--> N2["← 17 →"]
N2 <--> N3["← 93 →"]
N3 --> T["Tail"]
style H fill:#f59e0b,color:#fff
style T fill:#f59e0b,color:#fff
style N1 fill:#8b5cf6,color:#fff
style N2 fill:#8b5cf6,color:#fff
style N3 fill:#8b5cf6,color:#fff
class DoublyNode {
constructor(value) {
this.value = value;
this.next = null;
this.prev = null;
}
}
class DoublyLinkedList {
constructor() {
this.head = null;
this.tail = null;
this.size = 0;
}
// Add to end: O(1)
append(value) {
const node = new DoublyNode(value);
if (!this.tail) {
this.head = node;
this.tail = node;
} else {
node.prev = this.tail;
this.tail.next = node;
this.tail = node;
}
this.size++;
}
// Add to beginning: O(1)
prepend(value) {
const node = new DoublyNode(value);
if (!this.head) {
this.head = node;
this.tail = node;
} else {
node.next = this.head;
this.head.prev = node;
this.head = node;
}
this.size++;
}
// Remove from end: O(1)
removeLast() {
if (!this.tail) return null;
const value = this.tail.value;
if (this.head === this.tail) {
this.head = null;
this.tail = null;
} else {
this.tail = this.tail.prev;
this.tail.next = null;
}
this.size--;
return value;
}
// Remove specific node: O(1) if node reference is known
removeNode(node) {
if (node.prev) node.prev.next = node.next;
else this.head = node.next;
if (node.next) node.next.prev = node.prev;
else this.tail = node.prev;
this.size--;
return node.value;
}
}
配列 vs 連結リスト
flowchart TB
subgraph Compare["どちらを選ぶ?"]
Q1{"ランダムアクセス\nが必要?"}
Q2{"先頭への挿入/削除\nが頻繁?"}
Q3{"メモリ効率\n重視?"}
end
Q1 -->|Yes| Array["配列"]
Q1 -->|No| Q2
Q2 -->|Yes| LL["連結リスト"]
Q2 -->|No| Q3
Q3 -->|Yes| Array
Q3 -->|No| LL
style Compare fill:#3b82f6,color:#fff
style Array fill:#22c55e,color:#fff
style LL fill:#8b5cf6,color:#fff
| 操作 | 配列 | 連結リスト |
|---|---|---|
| インデックスアクセス | O(1) | O(n) |
| 先頭への挿入 | O(n) | O(1) |
| 末尾への挿入 | O(1)※ | O(n) / O(1)※※ |
| 中間への挿入 | O(n) | O(n)※※※ |
| 値の検索 | O(n) | O(n) |
| メモリ使用 | 効率的 | ポインタ分のオーバーヘッド |
| キャッシュ効率 | 高い | 低い |
※ 償却計算量 ※※ tail参照がある場合 ※※※ 位置が既知なら O(1)
実践例:LRUキャッシュ
配列と連結リストを組み合わせた実践的なデータ構造の例として、LRU(Least Recently Used)キャッシュがあります。
class LRUCache {
constructor(capacity) {
this.capacity = capacity;
this.map = new Map(); // O(1) lookup
this.list = new DoublyLinkedList(); // O(1) reorder
}
get(key) {
if (!this.map.has(key)) return -1;
const node = this.map.get(key);
// Move to front (most recently used)
this.list.removeNode(node);
this.list.prepend(node.value);
this.map.set(key, this.list.head);
return node.value.val;
}
put(key, value) {
if (this.map.has(key)) {
this.list.removeNode(this.map.get(key));
}
this.list.prepend({ key, val: value });
this.map.set(key, this.list.head);
if (this.map.size > this.capacity) {
const removed = this.list.removeLast();
this.map.delete(removed.key);
}
}
}
まとめ
| 概念 | 説明 |
|---|---|
| 配列 | 連続したメモリに格納、インデックスでO(1)アクセス |
| 連結リスト | ノードがポインタで繋がる、先頭操作がO(1) |
| 双方向連結リスト | 前後の参照を持ち、両端からO(1)操作可能 |
| キャッシュ効率 | 配列はCPUキャッシュに有利 |
重要ポイント
- 配列はランダムアクセスが高速、連結リストは挿入・削除が高速
- JavaScriptの
Arrayは動的配列で、内部的にメモリを自動拡張する - 双方向連結リストは両端からの操作が効率的
- 実際のアプリケーションでは両方を組み合わせて使うことが多い
練習問題
問題1: 基本
連結リストを逆順にする関数を実装してください。
function reverseList(head) {
// Your implementation here
}
問題2: 応用
連結リストの中央のノードを返す関数を実装してください(ヒント:2つのポインタを使う)。
チャレンジ問題
2つのソート済み連結リストを1つのソート済み連結リストにマージする関数を実装してください。
参考リンク
次回予告: Day 3では「スタックとキュー」について学びます。LIFO(後入れ先出し)とFIFO(先入れ先出し)の概念を理解しましょう。