データ構造 - ツリー
ツリー(Tree)は代表的な非線形データ構造であり、ノードとノードを接続するエッジで構成され、要素間の「一対多」の階層関係を表すために使用されます。
ツリーの基本用語
ツリーの用語図解
Aルートノード
深さ0
深さ0
B深さ1
C深さ1
D葉ノード
深さ2
深さ2
E葉ノード
F葉ノード
ルートノード親ノードなし、唯一
子ノード直接の下位ノード
葉ノード子ノードなし
深さルートからそのノードまでの辺の数
高さそのノードから最も深い葉までの辺の数
二分木各ノードは最大2つの子を持つ
ツリー構造を理解するには、まずいくつかの基本用語を押さえる必要があります:
| 用語 | 意味 |
|---|---|
| ルートノード(Root) | ツリーの開始ノードで、親ノードを持たない |
| 子ノード(Child) | あるノードに直接接続された次の層のノード |
| 親ノード(Parent) | あるノードに直接接続された前の層のノード |
| 葉ノード(Leaf) | 子ノードを持たないノード、つまりツリーの末端 |
| 深さ(Depth) | ルートノードからそのノードまでの辺の数。ルートの深さは0 |
| 高さ(Height) | そのノードから最も深い葉ノードまでの辺の数 |
二分木の走査
二分木の4つの走査方法 – 同じツリーを例に
1
2
3
4
5
前順(根→左→右)
1 2 4 5 3中順(左→根→右)
4 2 5 1 3後順(左→右→根)
4 5 2 3 1層順(レベル順)
1 2 3 4 5一般的な4つの走査方法は次のとおりです:
| 走査方法 | 訪問順序 | 代表的な用途 |
|---|---|---|
| 前順走査 | 根 → 左 → 右 | ツリーの複製、前置式の生成 |
| 中順走査 | 左 → 根 → 右 | BSTの整列済みシーケンスを出力 |
| 後順走査 | 左 → 右 → 根 | ツリーの削除、後置式の生成 |
| 層順走査 | レベルごとに左から右へ | BFS、レベルごとの出力 |
実例
#include <stdio.h>
#include <stdlib.h>
struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
};
struct TreeNode* createNode(int value) {
struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
node->data = value;
node->left = NULL;
node->right = NULL;
return node;
}
/* 前順走査:根 → 左 → 右 */
void preOrder(struct TreeNode* root) {
if (root == NULL) return;
printf("%d ", root->data); /* 最初に根を訪問 */
preOrder(root->left); /* 次に左部分木を走査 */
preOrder(root->right); /* 最後に右部分木を走査 */
}
/* 中順走査:左 → 根 → 右 */
void inOrder(struct TreeNode* root) {
if (root == NULL) return;
inOrder(root->left); /* 最初に左部分木を走査 */
printf("%d ", root->data); /* 次に根を訪問 */
inOrder(root->right); /* 最後に右部分木を走査 */
}
/* 後順走査:左 → 右 → 根 */
void postOrder(struct TreeNode* root) {
if (root == NULL) return;
postOrder(root->left); /* 最初に左部分木を走査 */
postOrder(root->right); /* 次に右部分木を走査 */
printf("%d ", root->data); /* 最後に根を訪問 */
}
int main() {
/* 次のような二分木を構築:
1
/ \
2 3
/ \
4 5 */
struct TreeNode* root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
printf("前順: "); preOrder(root); printf("\n"); /* 1 2 4 5 3 */
printf("中順: "); inOrder(root); printf("\n"); /* 4 2 5 1 3 */
printf("後順: "); postOrder(root); printf("\n"); /* 4 5 2 3 1 */
return 0;
}
#include <stdlib.h>
struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
};
struct TreeNode* createNode(int value) {
struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
node->data = value;
node->left = NULL;
node->right = NULL;
return node;
}
/* 前順走査:根 → 左 → 右 */
void preOrder(struct TreeNode* root) {
if (root == NULL) return;
printf("%d ", root->data); /* 最初に根を訪問 */
preOrder(root->left); /* 次に左部分木を走査 */
preOrder(root->right); /* 最後に右部分木を走査 */
}
/* 中順走査:左 → 根 → 右 */
void inOrder(struct TreeNode* root) {
if (root == NULL) return;
inOrder(root->left); /* 最初に左部分木を走査 */
printf("%d ", root->data); /* 次に根を訪問 */
inOrder(root->right); /* 最後に右部分木を走査 */
}
/* 後順走査:左 → 右 → 根 */
void postOrder(struct TreeNode* root) {
if (root == NULL) return;
postOrder(root->left); /* 最初に左部分木を走査 */
postOrder(root->right); /* 次に右部分木を走査 */
printf("%d ", root->data); /* 最後に根を訪問 */
}
int main() {
/* 次のような二分木を構築:
1
/ \
2 3
/ \
4 5 */
struct TreeNode* root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
printf("前順: "); preOrder(root); printf("\n"); /* 1 2 4 5 3 */
printf("中順: "); inOrder(root); printf("\n"); /* 4 2 5 1 3 */
printf("後順: "); postOrder(root); printf("\n"); /* 4 5 2 3 1 */
return 0;
}
二分探索木(BST)
二分探索木(BST)— 左は小さく右は大きい
50
30
70
20
40
60
80
BSTの性質と操作
性質:左部分木のすべての値 < 根 < 右部分木のすべての値
挿入/探索:小さい場合は左部分木へ、大きい場合は右部分木へ。平衡時は O(log n)
中順走査で昇順シーケンスを出力:20 30 40 50 60 70 80
注意:1,2,3... の順に挿入するとリンクリストに退化して O(n) になる。AVL木で解決できる。
二分探索木(BST)要件:各ノードについて、左部分木のすべてのノードの値はそのノードより小さく、右部分木のすべてのノードの値はそのノードより大きいこと。
実例
#include <stdio.h>
#include <stdlib.h>
struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
};
/* BST挿入:値の大小に応じて再帰的に適切な位置を探す
時間計算量:O(h)。hは木の高さ。平衡時は O(log n) */
struct TreeNode* insert(struct TreeNode* root, int value) {
if (root == NULL) {
struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
node->data = value;
node->left = node->right = NULL;
return node;
}
if (value < root->data) {
root->left = insert(root->left, value); /* 根より小さい場合は左部分木に挿入 */
} else if (value > root->data) {
root->right = insert(root->right, value); /* 根より大きい場合は右部分木に挿入 */
}
return root; /* 等しい場合は無視(BSTは通常、重複する値を保存しない) */
}
/* BST探索:現在のノードと比較し、左または右に探索するかを決定 */
struct TreeNode* search(struct TreeNode* root, int target) {
if (root == NULL || root->data == target) {
return root; /* 目標を見つけるか、葉ノードに到達 */
}
if (target < root->data) {
return search(root->left, target); /* 目標がより小さい場合は左部分木を探す */
}
return search(root->right, target); /* 目標がより大きい場合は右部分木を探す */
}
/* 中順走査(BSTの順序性を検証) */
void inOrder(struct TreeNode* root) {
if (root == NULL) return;
inOrder(root->left);
printf("%d ", root->data);
inOrder(root->right);
}
int main() {
struct TreeNode* root = NULL;
int values[] = {50, 30, 70, 20, 40, 60, 80};
/* 順に挿入してBSTを構築 */
for (int i = 0; i < 7; i++) {
root = insert(root, values[i]);
}
printf("BST中順走査(昇順): ");
inOrder(root); /* 出力: 20 30 40 50 60 70 80 */
printf("\n");
/* 探索テスト */
int target = 40;
struct TreeNode* found = search(root, target);
printf("%d %s\n", target, found ? "見つかりました!" : "見つかりません");
/* 出力: 40 見つかりました! */
return 0;
}
#include <stdlib.h>
struct TreeNode {
int data;
struct TreeNode* left;
struct TreeNode* right;
};
/* BST挿入:値の大小に応じて再帰的に適切な位置を探す
時間計算量:O(h)。hは木の高さ。平衡時は O(log n) */
struct TreeNode* insert(struct TreeNode* root, int value) {
if (root == NULL) {
struct TreeNode* node = (struct TreeNode*)malloc(sizeof(struct TreeNode));
node->data = value;
node->left = node->right = NULL;
return node;
}
if (value < root->data) {
root->left = insert(root->left, value); /* 根より小さい場合は左部分木に挿入 */
} else if (value > root->data) {
root->right = insert(root->right, value); /* 根より大きい場合は右部分木に挿入 */
}
return root; /* 等しい場合は無視(BSTは通常、重複する値を保存しない) */
}
/* BST探索:現在のノードと比較し、左または右に探索するかを決定 */
struct TreeNode* search(struct TreeNode* root, int target) {
if (root == NULL || root->data == target) {
return root; /* 目標を見つけるか、葉ノードに到達 */
}
if (target < root->data) {
return search(root->left, target); /* 目標がより小さい場合は左部分木を探す */
}
return search(root->right, target); /* 目標がより大きい場合は右部分木を探す */
}
/* 中順走査(BSTの順序性を検証) */
void inOrder(struct TreeNode* root) {
if (root == NULL) return;
inOrder(root->left);
printf("%d ", root->data);
inOrder(root->right);
}
int main() {
struct TreeNode* root = NULL;
int values[] = {50, 30, 70, 20, 40, 60, 80};
/* 順に挿入してBSTを構築 */
for (int i = 0; i < 7; i++) {
root = insert(root, values[i]);
}
printf("BST中順走査(昇順): ");
inOrder(root); /* 出力: 20 30 40 50 60 70 80 */
printf("\n");
/* 探索テスト */
int target = 40;
struct TreeNode* found = search(root, target);
printf("%d %s\n", target, found ? "見つかりました!" : "見つかりません");
/* 出力: 40 見つかりました! */
return 0;
}
その他の拡張BSTは理想的な場合(ツリーが平衡を保っている場合)、挿入・削除・探索はいずれも O(log n) を達成できます。しかし、データ自体が整列している場合(1, 2, 3, 4... の順で挿入するなど)、BSTはリンクリストに退化し、効率が O(n) に低下します。この問題を解決するために、コンピュータサイエンスではAVL木などの自己平衡二分探索木が開発されました。