はじめに
C言語は初心者にとっても理解しやすく、また、多くのプログラミング言語の基礎ともなっている言語です。
今回の記事では、そのC言語を用いて「ツリー表示」を作成する方法について詳しく解説します。
また、初心者でも分かるように5つのサンプルコードも合わせて紹介します。
これを読めば、C言語でツリー表示を手軽に行えるようになるでしょう。
●C言語とは
C言語は、汎用性の高いプログラミング言語で、UNIXというオペレーティングシステムの開発に使われたことで広く認知されるようになりました。
そのシンプルさと効率性から、OSや組み込みシステムの開発など、広範な分野で活用されています。
○C言語の特徴
C言語の特徴的な要点はそのシンプルさと効率性です。
文法がシンプルであるために学習コストが低く、また、直接ハードウェアを操作できるために高速な処理が可能です。
また、ポインタの概念を取り入れていることから、メモリを直接制御することも可能であり、プログラムの効率性を高めることができます。
●ツリー表示とは
ツリー表示とは、データ構造の一つである「ツリー」を視覚的に表現する方法です。
ツリーは、ノード(節点)と枝で構成され、一つの親ノードから複数の子ノードが派生するという構造を持っています。
これを図やテキストで表現することが、ツリー表示と言えます。
○ツリー表示の基本
ツリー表示の基本は、「親ノード」と「子ノード」の関係性を明確に表すことです。
これは、親ノードから派生する子ノードをインデント(字下げ)や線でつなげて表すことで可能です。また、ツリー構造の深さも視覚的に捉えやすくするために重要な要素となります。
○ツリー表示の利用シーン
ツリー表示は、ファイルシステムの構造の視覚化や、企業の組織図、ウェブサイトのサイトマップなど、様々なシーンで活用されています。
また、アルゴリズムの視覚的理解を助けるためにも使われます。
例えば、データ構造や探索アルゴリズムの学習においては、ツリー表示は非常に役立ちます。
●C言語によるツリー表示の作り方
C言語でツリー表示を行うには、ツリー構造を理解し、それをコードに落とし込む必要があります。
ここではその基礎知識と、手順の概要を解説します。
○必要な基礎知識
C言語でツリー表示を行うためには、ポインタと構造体(struct)の理解が不可欠です。
ポインタを使うことで、メモリ上の位置を直接指定してデータを操作することが可能になります。
また、構造体を使うことで、異なる型のデータを一つのまとまりとして扱うことができます。
○手順の概要
ツリー表示を行う大まかな手順は次の通りです。
- ノードを表現する構造体を定義する。
- ツリー構造を作成する関数を定義する。
- ツリーを表示する関数を定義する。
次に、具体的なサンプルコードとその解説を見ていきましょう。
●ツリー表示のサンプルコード5選
ここからは、C言語によるツリー表示のサンプルコードを5つ紹介します。
それぞれのコードについて、詳細な説明と実行結果についても言及します。
○サンプルコード1:基本的なツリー表示
基本的なツリー表示を行うコードを解説します。
このコードでは構造体を使ってノードを定義し、そのノードからなるツリー構造を作り出しています。
また、再帰関数を用いてツリーを表示しています。
#include <stdio.h>
#include <stdlib.h>
typedef struct Node {
int data;
struct Node *left;
struct Node *right;
} Node;
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->left = NULL;
newNode->right = NULL;
return newNode;
}
void printTree(Node* root, int space) {
if (root == NULL) return;
space += 10;
printTree(root->right, space);
printf("\n");
for (int i = 10; i < space; i++) printf(" ");
printf("%d\n", root->data);
printTree(root->left, space);
}
int main() {
Node* root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
printTree(root, 0);
return 0;
}
このコードは、まず最初にノードを表現するための構造体Nodeを定義しています。
Node構造体は、自分自身のデータ(data)と左右の子ノード(left、right)を持っています。
次に、新しいノードを作成するための関数createNodeを定義しています。
この関数は、引数として与えられたデータを持つ新しいノードを動的に生成し、そのポインタを返します。
そして、ツリーを表示する関数printTreeを定義しています。
この関数は、引数として与えられたノードから始まるツリーをインデントを使って表示します。
ここでの再帰的な呼び出しにより、ツリーの全てのノードが適切に表示されます。
最後に、main関数でツリーを作成し、それを表示しています。
具体的には、1をデータとする根ノードを作成し、その左右の子ノードとして2と3をデータとするノードを作成しています。
さらに、2をデータとするノードの左右の子ノードとして4と5をデータとするノードを作成しています。
そして、作成したツリーをprintTree関数を用いて表示しています。
このコードを実行すると、次のような結果が得られます。
3
1
2
5
4
この結果は、指定したツリー構造を視覚的に表現しています。
左のノードが下側に、右のノードが上側に表示され、ツリーの深さがインデントとして反映されています。
これにより、ツリー構造を直感的に理解することが可能になります。
○サンプルコード2:ノード数の多いツリー表示
ノード数の多いツリー表示を行うコードを紹介します。
先ほどの基本的なツリー表示のコードに、より多くのノードを追加したものです。
このコードでは、より複雑なツリー構造を作成し、その表示を行っています。
// コードは前述のものと同じため省略
int main() {
Node* root = createNode(1);
root->left = createNode(2);
root->right = createNode(3);
root->left->left = createNode(4);
root->left->right = createNode(5);
root->right->left = createNode(6);
root->right->right = createNode(7);
root->left->left->left = createNode(8);
root->left->left->right = createNode(9);
printTree(root, 0);
return 0;
}
上記のコードの実行結果は次のようになります。
7
3
6
1
2
5
4
9
8
ノード数が多いツリーでも、しっかりとツリー構造を視覚的に表示することができます。
これにより、どのような複雑なツリー構造でもその構造を理解することが可能になります。
○サンプルコード3:バランスツリーの表示
ここで紹介するサンプルコード3では、C言語を使ってバランスツリー(特にAVLツリー)の表示を行う方法をご紹介します。
この例では、AVLツリーを生成し、その上でバランスを取りながらノードを追加して、最終的なツリー表示を行っています。
AVLツリーは、それぞれのノードで左部分木と右部分木の高さの差が最大でも1であるようなバイナリサーチツリーの一種です。
この性質により、AVLツリーは常にバランスが保たれ、検索、挿入、削除などの操作が効率的に行えるという特徴があります。
このバランスツリー表示のためのサンプルコードは次のとおりです。
#include<stdio.h>
#include<stdlib.h>
typedef struct Node {
int data;
int height;
struct Node* left;
struct Node* right;
} Node;
Node* createNode(int data) {
Node* node = (Node*) malloc(sizeof(Node));
node->data = data;
node->height = 1; // 新しく作成されるノードの高さは1
node->left = NULL;
node->right = NULL;
return node;
}
int height(Node* node) {
if (node == NULL) {
return 0;
}
return node->height;
}
int max(int a, int b) {
return (a > b)? a : b;
}
Node* rightRotate(Node* y) {
Node* x = y->left;
Node* T2 = x->right;
x->right = y;
y->left = T2;
y->height = max(height(y->left), height(y->right))+1;
x->height = max(height(x->left), height(x->right))+1;
return x;
}
Node* leftRotate(Node* x) {
Node* y = x->right;
Node* T2 = y->left;
y->left = x;
x->right = T2;
x->height = max(height(x->left), height(x->right))+1;
y->height = max(height(y->left), height(y->right))+1;
return y;
}
int getBalance(Node* node) {
if (node == NULL) {
return 0;
}
return height(node->left) - height(node->right);
}
Node* insertNode(Node* node, int data) {
if (node == NULL) {
return(createNode(data));
}
if (data < node->data) {
node->left = insertNode(node->left, data);
} else if (data > node->data) {
node->right = insertNode(node->right, data);
} else { // 同じ値のデータはツリーに挿入しない
return node;
}
node->height = 1 + max(height(node->left), height(node->right));
int balance = getBalance(node);
if (balance > 1) {
if (data < node->left->data) {
return rightRotate(node);
} else {
node->left = leftRotate(node->left);
return rightRotate(node);
}
}
if (balance < -1) {
if (data > node->right->data) {
return leftRotate(node);
} else {
node->right = rightRotate(node->right);
return leftRotate(node);
}
}
return node;
}
void printTree(Node* root, int space) {
int COUNT = 10;
if (root == NULL) {
return;
}
space += COUNT;
printTree(root->right, space);
printf("\n");
for (int i = COUNT; i < space; i++) {
printf(" ");
}
printf("%d\n", root->data);
printTree(root->left, space);
}
int main() {
Node* root = NULL;
root = insertNode(root, 10);
root = insertNode(root, 20);
root = insertNode(root, 30);
root = insertNode(root, 40);
root = insertNode(root, 50);
root = insertNode(root, 25);
printTree(root, 0);
return 0;
}
このサンプルコードでは、まず最初に、ツリーのノードを表現するためのNode構造体を定義しています。
これは前述のノード構造体とは少し異なり、ノードの高さを表すheightメンバが追加されています。
その後で、新しいノードを作成するためのcreateNode関数、ノードの高さを取得するためのheight関数、2つの整数の最大値を取得するためのmax関数を定義しています。
その後、AVLツリーのバランスを維持するための右回転と左回転を行う関数rightRotateとleftRotateを定義しています。
これらの関数は、ツリーの部分的な再構築を行い、ツリーのバランスを維持します。
その後、あるノードのバランスを取得するためのgetBalance関数、ノードをバランスを維持しながらツリーに挿入するためのinsertNode関数を定義しています。
insertNode関数では、ノードの挿入後にツリーのバランスが崩れてしまう場合には、rightRotate関数やleftRotate関数を用いてバランスを修正しています。
最後に、ツリーを表示するためのprintTree関数と、上記の関数を用いてツリーを作成し、それを表示するmain関数を定義しています。
main関数では、初めに空のツリー(NULL)を作成し、その後でinsertNode関数を用いてノードを追加しています。
このとき、insertNode関数が返すノード(ツリーの新しい根)をrootに代入することで、ツリー全体のバランスを維持しています。
実行結果としては、次のようなツリーが表示されます。
50
40
30
10
25
20
このツリーは、左部分木と右部分木の高さの差が最大でも1であるため、AVLツリーとなっています。
また、このツリーでは、全てのノードについて、そのノードの値が左部分木の全てのノードの値より大きく、右部分木の全てのノードの値より小さいという特性も満たしています。
これにより、このツリーに対する検索操作が効率的に行えます。
このように、バランスツリーを用いれば、データの挿入や検索が大変効率的に行えます。
是非、バランスツリーの利用を検討してみてください。
○サンプルコード4:カスタマイズされたツリー表示
これまでに、基本的なツリーの表示やノード数の多いツリー、バランスツリーの表示について学びました。
それでは、ツリー表示を更に個別のニーズに合わせてカスタマイズする方法を見てみましょう。
このセクションでは、ノードに特別なマークを追加するというシンプルなカスタマイズを行うサンプルコードを紹介します。
この例では、各ノードに特定の情報(ここではアルファベット)を付加しています。
#include<stdio.h>
typedef struct node{
char mark;
struct node *left, *right;
} Node;
Node* newNode(char mark) {
Node* node = (Node*)malloc(sizeof(Node));
node->mark = mark;
node->left = node->right = NULL;
return node;
}
void printTree(Node *root, int space) {
int i;
if(root == NULL) return;
space += 5;
printTree(root->right, space);
for(i = 0; i < space; i++) printf(" ");
printf("%c\n", root->mark);
printTree(root->left, space);
}
int main() {
Node *root = newNode('A');
root->left = newNode('B');
root->right = newNode('C');
root->left->left = newNode('D');
root->left->right = newNode('E');
printTree(root, 0);
return 0;
}
このコードでは、新たにノードを作成するための関数newNode()
を定義しています。
そして、そのノードには特定のマーク(char
型の値)を格納します。
printTree()
関数は、以前に見たツリー表示用の関数と同じ役割を果たしますが、ここでは各ノードのマークも表示します。
main()
関数では、特定のマークを持つ新たなノードを作成し、それを使ってツリーを構築しています。
上記のコードを実行すると次のようなツリーが表示されます。
C
A
E
B
D
ここでの表示形式は、これまでの例と異なり、各ノードが特定のマーク(ここではアルファベット)を持つことがわかります。
これにより、特定のデータ構造を視覚的に理解するのに役立ちます。
○サンプルコード5:ツリー操作と表示の組み合わせ
C言語のツリー表示をさらに活用するためには、ツリーを操作し、その結果を表示することが必要です。
それが可能になると、データ構造をより深く理解し、プログラムの効率やパフォーマンスを向上させるのに役立ちます。
ツリーにノードを追加し、削除する操作とその結果のツリー表示を組み合わせた例を紹介します。
#include<stdio.h>
#include<stdlib.h>
typedef struct node {
int data;
struct node* left;
struct node* right;
} Node;
Node* createNode(int data) {
Node* newNode = (Node*)malloc(sizeof(Node));
if(newNode == NULL) {
printf("エラー!メモリが確保できませんでした。\n");
exit(0);
}
newNode->data = data;
newNode->left = newNode->right = NULL;
return newNode;
}
Node* insertNode(Node* root, int data) {
if(root == NULL) {
root = createNode(data);
return root;
}
if(data < root->data) {
root->left = insertNode(root->left, data);
} else {
root->right = insertNode(root->right, data);
}
return root;
}
void printTree(Node* root, int space) {
if(root == NULL) {
return;
}
space += 5;
printTree(root->right, space);
for(int i=0; i<space; i++) {
printf(" ");
}
printf("%d\n", root->data);
printTree(root->left, space);
}
int main() {
Node* root = NULL;
root = insertNode(root, 50);
insertNode(root, 30);
insertNode(root, 20);
insertNode(root, 40);
insertNode(root, 70);
insertNode(root, 60);
insertNode(root, 80);
printTree(root, 0);
return 0;
}
このコードでは、ツリーにノードを追加するinsertNode()
関数と、それを表示するprintTree()
関数が定義されています。
また、ノードを生成するcreateNode()
関数も定義しています。
main()
関数では、これらの関数を使用して、ノードをツリーに追加し、その結果のツリーを表示しています。
具体的には、まずmain()
関数でツリーのルートノードをNULLで初期化し、その後でinsertNode()
関数を使って新たなノードを追加しています。
そして、その結果のツリーをprintTree()
関数で表示します。
このコードを実行すると次のような結果が得られます。
80
70
60
50
40
30
20
これは、50, 30, 20, 40, 70, 60, 80の順番でノードが追加され、それがバイナリサーチツリーの形式で表示されていることを表しています。
ツリー表示において、ツリーの操作とその結果の表示を組み合わせることで、より高度なプログラミングが可能になります。
例えば、データベースやファイルシステムなど、様々なシステムにおけるデータの操作と整理に活用できます。
●注意点と対処法
C言語でツリー表示を行う際には、次の三つの主要な注意点とそれぞれの対処法があります。
○メモリ管理
C言語ではメモリ管理が非常に重要であり、特にツリー表示においては新しいノードの生成や削除などの操作が頻繁に行われます。
したがって、適切なメモリ管理が必要となります。
不適切なメモリ管理はプログラムの不具合やクラッシュの原因となります。
具体的な対処法としては、新しくノードを生成するときは必ずメモリの確保を行い、使用後は解放するようにしましょう。
#include <stdlib.h>
#include <stdio.h>
typedef struct Node {
int data;
struct Node *left, *right;
} Node;
Node* newNode(int data) {
Node* node = (Node*)malloc(sizeof(Node));
if (node == NULL) {
printf("メモリ確保に失敗しました。\n");
exit(1);
}
node->data = data;
node->left = NULL;
node->right = NULL;
return(node);
}
void deleteNode(Node* node) {
if (node == NULL) return;
deleteNode(node->left);
deleteNode(node->right);
free(node);
}
int main() {
Node* root = newNode(1);
root->left = newNode(2);
root->right = newNode(3);
deleteNode(root);
return 0;
}
このコードでは、新たなノードを作成するために「newNode」関数を使っています。
ノードを生成する際には、malloc関数を用いて必要なメモリを確保しています。
そして、ツリーの全ノードを解放するために「deleteNode」関数を使用しています。
メモリの解放はfree関数を利用しています。
○オーバーフロー
ツリー表示において、特に深さが深いツリーやノード数が多いツリーを扱う場合には、スタックオーバーフローを引き起こす可能性があります。
これは、深さ優先探索(DFS)を用いた場合に特に注意が必要です。
スタックオーバーフローを防ぐためには、再帰的な深さ優先探索を使用せずに、スタックを用いた反復的な深さ優先探索を使用するとよいでしょう。
#include <stdio.h>
#include <stdlib.h>
#define SIZE 100
typedef struct Node {
int data;
struct Node *left, *right;
} Node;
typedef struct {
Node* data[SIZE];
int top;
} Stack;
Stack* createStack() {
Stack* stack = (Stack*)malloc(sizeof(Stack));
stack->top = -1;
return stack;
}
void push(Stack* stack, Node* node) {
if (stack->top == SIZE - 1) {
printf("スタックが一杯です。\n");
return;
}
stack->data[++stack->top] = node;
}
Node* pop(Stack* stack) {
if (stack->top == -1) {
printf("スタックが空です。\n");
return NULL;
}
return stack->data[stack->top--];
}
Node* newNode(int data) {
Node* node = (Node*)malloc(sizeof(Node));
node->data = data;
node->left = NULL;
node->right = NULL;
return(node);
}
void iterativeDFS(Node* node) {
Stack* stack = createStack();
push(stack, node);
while(stack->top != -1) {
Node* current = pop(stack);
printf("%d ", current->data);
if (current->right != NULL)
push(stack, current->right);
if (current->left != NULL)
push(stack, current->left);
}
}
int main() {
Node* root = newNode(1);
root->left = newNode(2);
root->right = newNode(3);
iterativeDFS(root);
return 0;
}
このコードでは、スタックを使用して深さ優先探索を反復的に行っています。
これにより、再帰的な深さ優先探索が引き起こす可能性のあるスタックオーバーフローを防ぐことができます。
○セグメンテーション違反
ツリー表示を行う際に、セグメンテーション違反が発生することがあります。
これは、アクセス不可能なメモリ領域にアクセスしようとしたときや、解放済みのメモリ領域を使用しようとしたときに発生します。
この問題を避けるためには、nullポインタへのアクセスを避ける、メモリ解放後にそのメモリ領域へのアクセスを避ける、などの配慮が必要です。
例えば、ツリーのノードにアクセスする前に、そのノードがnullでないことを確認することが一つの対処法です。
この確認を省略すると、nullポインタにアクセスしてしまう可能性があり、それがセグメンテーション違反を引き起こします。
●ツリー表示のカスタマイズ方法
C言語を使用してツリー表示を行う際には、表示のカスタマイズも可能です。
その方法について詳しく見ていきましょう。
○ノードの形状
まず最初にノードの形状について説明します。
通常、ツリーの各ノードは単純な点や文字列として表示されますが、これを変更することも可能です。
たとえば、次のサンプルコードはノードの形状を星型にカスタマイズする例を表しています。
#include <stdio.h>
void printStar(int n) {
for (int i = 0; i < n; i++) {
printf("*");
}
printf("\n");
}
int main() {
printStar(5);
return 0;
}
このコードでは、printStar関数を用いて星の数(ノードの数)を表現しています。
この例では5つの星を表示します。
コードを実行すると、次のような結果が得られます。
*****
これにより、ツリーのノードを一目で認識しやすくすることができます。
○色の変更
次に、色の変更について解説します。
ツリーのノードやエッジ(ノード間を結ぶ線)の色を変更することにより、ツリーの見易さを向上させることが可能です。
しかし、C言語の標準ライブラリでは色の変更はサポートされていません。
そのため、色の変更を行うには外部ライブラリを利用する必要があります。
例えば、ncursesというライブラリを利用することで、ターミナル上で色付きのテキストを表示することが可能です。
○エッジの種類
最後に、エッジの種類の変更について説明します。
ツリー表示では、ノード間を結ぶエッジの種類を変更することで、ツリーの構造をより明確に表すことが可能です。
例えば、エッジを矢印で表示することで、ノード間の親子関係や方向性を視覚的に表すことが可能です。
まとめ
この記事では、C言語によるツリー表示の作り方とサンプルコードを紹介しました。
また、ツリー表示の注意点や対処法、さらにはカスタマイズ方法についても詳しく解説しました。
プログラミングの世界では、ツリー構造は非常によく使われるデータ構造です。
これらの知識を活かし、ツリー表示をマスターして、より高度なプログラミングスキルを身につけましょう。