第 9 章 木構造¶
はじめに¶
木構造は階層的なデータを表現するデータ構造です。C では再帰関数と動的メモリを使って実装します。この章では二分探索木と最小ヒープを実装します。
1. 二分探索木(BST: Binary Search Tree)¶
性質: - 左部分木のすべてのキー < ルートのキー - 右部分木のすべてのキー > ルートのキー - 左右の部分木もまた二分探索木
typedef struct _BSTNode {
int key;
struct _BSTNode *left;
struct _BSTNode *right;
} BSTNode;
typedef struct {
BSTNode *root;
int no;
} BST;
Red — 失敗するテストを書く¶
void test_bst_insert_search(void) {
BST *t = bst_new();
bst_insert(t, 5);
bst_insert(t, 3);
bst_insert(t, 7);
ASSERT_TRUE(bst_search(t, 5));
ASSERT_FALSE(bst_search(t, 99));
ASSERT_EQ_INT(3, bst_len(t));
bst_free(t);
}
Green — search / insert¶
int bst_search(const BST *t, int key) {
BSTNode *ptr = t->root;
while (ptr != NULL) {
if (key == ptr->key) return 1;
ptr = (key < ptr->key) ? ptr->left : ptr->right;
}
return 0;
}
void bst_insert(BST *t, int key) {
if (t->root == NULL) {
t->root = malloc(sizeof(BSTNode));
t->root->key = key;
t->root->left = t->root->right = NULL;
t->no++;
return;
}
BSTNode *ptr = t->root;
while (1) {
if (key == ptr->key) return; /* 重複は無視 */
if (key < ptr->key) {
if (ptr->left == NULL) {
ptr->left = malloc(sizeof(BSTNode));
ptr->left->key = key;
ptr->left->left = ptr->left->right = NULL;
t->no++;
return;
}
ptr = ptr->left;
} else {
if (ptr->right == NULL) {
ptr->right = malloc(sizeof(BSTNode));
ptr->right->key = key;
ptr->right->left = ptr->right->right = NULL;
t->no++;
return;
}
ptr = ptr->right;
}
}
}
走査(Traversal)¶
3 種類の深さ優先探索を実装します。
static int _inorder(BSTNode *node, int *buf, int bufsz, int idx) {
if (node == NULL || idx >= bufsz) return idx;
idx = _inorder(node->left, buf, bufsz, idx);
if (idx < bufsz) buf[idx++] = node->key;
idx = _inorder(node->right, buf, bufsz, idx);
return idx;
}
| 走査順序 | 順番 | 結果(例: 5,3,7,1,4) |
|---|---|---|
| 中順(inorder) | 左→根→右 | 1, 3, 4, 5, 7(昇順) |
| 前順(preorder) | 根→左→右 | 5, 3, 1, 4, 7 |
| 後順(postorder) | 左→右→根 | 1, 4, 3, 7, 5 |
削除(delete)¶
削除は 3 パターンに分岐します。
void bst_delete(BST *t, int key) {
/* ... 削除対象を探す ... */
if (ptr->left == NULL && ptr->right == NULL) {
/* 葉ノード: そのまま削除 */
replacement = NULL;
} else if (ptr->right == NULL) {
/* 右子なし: 左子で置き換え */
replacement = ptr->left;
} else if (ptr->left == NULL) {
/* 左子なし: 右子で置き換え */
replacement = ptr->right;
} else {
/* 子が 2 つ: 右部分木の最小ノード(中順後継)で置き換え */
BSTNode *succ_parent = ptr;
BSTNode *succ = ptr->right;
while (succ->left != NULL) { succ_parent = succ; succ = succ->left; }
ptr->key = succ->key;
/* 後継ノードを削除 */
if (succ_parent == ptr) succ_parent->right = succ->right;
else succ_parent->left = succ->right;
free(succ);
return;
}
/* ... 置き換え処理 ... */
}
2. 最小ヒープ(MinHeap)¶
完全二分木を配列で表現します。親インデックス i に対して、左子は 2i+1、右子は 2i+2 です。
typedef struct {
int *data;
int size;
int capacity;
} MinHeap;
ヒープ条件:すべてのノードで 親 ≤ 子
Red — 失敗するテストを書く¶
void test_min_heap_push_pop(void) {
MinHeap *h = heap_new(10);
heap_push(h, 5);
heap_push(h, 3);
heap_push(h, 7);
heap_push(h, 1);
ASSERT_EQ_INT(1, heap_pop(h));
ASSERT_EQ_INT(3, heap_pop(h));
heap_free(h);
}
Green — push(上方向に浮かせる)¶
void heap_push(MinHeap *h, int val) {
int i = h->size++;
h->data[i] = val;
while (i > 0) {
int parent = (i - 1) / 2;
if (h->data[parent] <= h->data[i]) break;
int tmp = h->data[parent];
h->data[parent] = h->data[i];
h->data[i] = tmp;
i = parent;
}
}
Green — pop(下方向に沈める)¶
int heap_pop(MinHeap *h) {
int top = h->data[0];
h->data[0] = h->data[--h->size];
int i = 0;
while (1) {
int left = 2*i+1, right = 2*i+2, smallest = i;
if (left < h->size && h->data[left] < h->data[smallest]) smallest = left;
if (right < h->size && h->data[right] < h->data[smallest]) smallest = right;
if (smallest == i) break;
int tmp = h->data[i];
h->data[i] = h->data[smallest];
h->data[smallest] = tmp;
i = smallest;
}
return top;
}
テスト実行結果¶
$ make test-chapter09/tree
=== Testing chapter09/tree ===
30 tests run: 30 passed, 0 failed
全章テスト¶
$ make test
=== Testing chapter01/basic_algorithms ===
17 tests run: 17 passed, 0 failed
=== Testing chapter02/arrays ===
13 tests run: 13 passed, 0 failed
=== Testing chapter03/search ===
22 tests run: 22 passed, 0 failed
=== Testing chapter04/stack_queue ===
25 tests run: 25 passed, 0 failed
=== Testing chapter05/recursion ===
14 tests run: 14 passed, 0 failed
=== Testing chapter06/sort ===
51 tests run: 51 passed, 0 failed
=== Testing chapter07/strings ===
11 tests run: 11 passed, 0 failed
=== Testing chapter08/linked_list ===
21 tests run: 21 passed, 0 failed
=== Testing chapter09/tree ===
30 tests run: 30 passed, 0 failed
All tests passed.
合計 204 テスト全通過
まとめ¶
| データ構造 | 操作 | 計算量 | C の特徴 |
|---|---|---|---|
| BST 探索 | bst_search |
O(h) | 非再帰ループ |
| BST 挿入 | bst_insert |
O(h) | 非再帰ループ |
| BST 走査 | bst_inorder |
O(n) | 再帰 + 出力バッファ |
| BST 削除 | bst_delete |
O(h) | 3 パターン分岐 |
| MinHeap push | heap_push |
O(log n) | 配列で完全二分木 |
| MinHeap pop | heap_pop |
O(log n) | 最小子と交換 |
(h: 木の高さ、平均 O(log n)、最悪 O(n))
参考文献¶
- 『新・明解 C で学ぶアルゴリズムとデータ構造』 — 柴田望洋
- 『テスト駆動開発』 — Kent Beck