【C言語入門の次】 第04.5回 連結リストのバリエーション


双方向リストとは
typedef struct student {
unsigned int id;
char name[20];
struct student* pnext;
} student;
typedef struct student {
unsigned int id;
char name[20];
struct student* pprev;
struct student* pnext;
} student;

双方向リストのメリット
双方向リストの実装
とりあえず 双方向リスト を実装してみましょう
まず、ヘッダーファイル"student.h"dです
#pragma once
#ifndef _INC_STUDENTS
#define _INC_STUDENTS
#include <stdio.h>
#define NAME_SIZE (20)
typedef struct student {
unsigned int id;
char name[NAME_SIZE];
struct student* pprev;
struct student* pnext;
} student;
typedef struct {
student* phead;
student* ptail;
student* pcurrent;
size_t curr_idx;
size_t num;
} student_list;
void init_student_list(student_list* plist);
void final_student_list(student_list* plist);
int move_forward(student_list* plist);
int move_backward(student_list* plist);
int move_to(student_list* plist, size_t idx);
int delete_node(student_list* plist);
int insert_node(student_list* plist, unsigned int id, const char name[]);
#endif // _INC_STUDENTS
"student"構造体には、前のノードを参照する"pprev"を追加しています
"student_list"構造体には、 双方向リスト の先頭のノードへのポインタ"phead"のほかに最後のノードへのポインタ"ptail"を追加しています
また、挿入や削除を行うためのインデックスを示すため、"pcurrent"および"curr_idx"を設けています
初期化や終了の関数のほかに、インデックスを動かすための関数move_forward、move_backward、move_toを設けました
また、ノードの挿入、削除のためにdelete_nodeやinsert_node関数を設けています。
ちなみに、現在のインデックスのノードのデータを取得するには、"student_list"構造体の"pcurrent"を参照すればOKなので、特に関数は設けていません
次は実装です
#include "students.h"
#include <stdlib.h>
#include <string.h>
void init_student_list(student_list* plist) {
plist->phead = NULL;
plist->ptail = NULL;
plist->pcurrent = NULL;
plist->curr_idx = 0;
plist->num = 0;
return;
}
void final_student_list(student_list* plist) {
int rc = -1;
move_to(plist, 0);
do {
rc = delete_node(plist);
} while (rc == 0);
return;
}
int move_forward(student_list* plist) {
int rc = -1;
if (plist->pcurrent != NULL) {
plist->pcurrent = plist->pcurrent->pnext;
rc = ++(plist->curr_idx);
}
return rc;
}
int move_backward(student_list* plist) {
int rc = -1;
if (plist->pcurrent == NULL) {
if (plist->phead != NULL) {
plist->pcurrent = plist->ptail;
rc = --(plist->curr_idx);
}
} else if (plist->pcurrent->pprev != NULL) {
plist->pcurrent = plist->pcurrent->pprev;
rc = --(plist->curr_idx);
}
return rc;
}
int move_to(student_list* plist, size_t idx) {
int rc = -1;
if (idx <= plist->num) {
while (idx < plist->curr_idx) {
move_backward(plist);
}
while (idx > plist->curr_idx) {
move_forward(plist);
}
rc = plist->curr_idx;
}
return rc;
}
int delete_node(student_list* plist) {
int rc = -1;
if (plist->pcurrent != NULL) {
student* p2delete = plist->pcurrent;
if (plist->pcurrent->pprev == NULL) {
plist->phead = plist->pcurrent->pnext;
if (plist->phead != NULL) {
plist->phead->pprev = NULL;
}
plist->pcurrent = plist->phead;
} else if (plist->pcurrent->pnext == NULL) {
student* pprev = plist->pcurrent->pprev;
pprev->pnext = NULL;
plist->ptail = pprev;
plist->pcurrent = NULL;
} else {
student* pprev = plist->pcurrent->pprev;
student* pnext = plist->pcurrent->pnext;
pprev->pnext = pnext;
pnext->pprev = pprev;
plist->pcurrent = pnext;
}
free(p2delete);
plist->num--;
rc = plist->curr_idx;
}
return rc;
}
student* make_student_node(int id, const char name[]) {
student* pstudent = (student*)malloc(sizeof(student));
if (pstudent != NULL) {
pstudent->id = id;
snprintf(pstudent->name, NAME_SIZE, "%s", name);
pstudent->pprev = NULL;
pstudent->pnext = NULL;
}
return pstudent;
}
int insert_node(student_list* plist, unsigned int id, const char name[]) {
int rc = -1;
student* pnew_node = make_student_node(id, name);
if (pnew_node != NULL) {
if (plist->phead == NULL) {
plist->phead = pnew_node;
plist->ptail = pnew_node;
plist->pcurrent = pnew_node;
} else if (plist->pcurrent == NULL) {
pnew_node->pprev = plist->ptail;
plist->ptail->pnext = pnew_node;
plist->ptail = pnew_node;
plist->pcurrent = pnew_node;
} else {
student* pprev = plist->pcurrent->pprev;
pnew_node->pprev = pprev;
pnew_node->pnext = plist->pcurrent;
if (pprev == NULL) {
plist->phead = pnew_node;
} else {
pprev->pnext = pnew_node;
}
plist->pcurrent->pprev = pnew_node;
plist->pcurrent = pnew_node;
}
plist->num++;
rc = plist->curr_idx;
}
return rc;
}
まず初期化関数init_student_listですが、今回はダミーノードを使用しませんので、各メンバをNULLや0で初期化しています
次に終了関数final_student_listですが、インデックスを先頭に移動した後、全てのノードが削除されるまで、delete_node関数により、ノードの削除を行っています
非常に効率は悪いのですが、判り易いのでこの方法を取りました
リストを辿って、直接、ノードを解放すれば、効率は良くなります
たとえばこんな感じです
void final_student_list(student_list* plist) {
int rc = -1;
student* p2delete = plist->phead;
while (p2delete != NULL) {
student* pnext = p2delete->pnext;
free(p2delete);
p2delete = pnext;
}
init_student_list(plist);
return;
}
move_forward関数は、現在のインデックスのノードを次のインデックスのノードに移動します
現在のインデックスのノード"pcurrent"がNULLの場合には、ノードが無いか、最後のノードの次を指しているので、移動はできません
move_backward関数は、現在のインデックスのノードを前のインデックスのノードに移動します
双方向リスト の威力が発揮される場面です
現在のインデックスのノード"pcurrent"がNULLの場合には、ノードが無いか、最後のノードの次を指しています
ノードが無い場合には移動はできませんが、最後のノードの次を指している場合には、最後のノードを指すようにします
また、"pcurrent"が最初のノードを指している場合にも移動はできませんのでスキップします
その他の場合には、"student"構造体の"pprev"を辿って、前のノードに"pcurrent"を移します
move_to関数については、指定のインデックスに"pcurrent"を移動します
指定のインデックスによって、move_backward関数やmove_forward関数で移動を行います
なお、指定のインデックスがリストの最初や最後に近い場合に、リストの最初や最後からノードを辿るようにすれば、効率が良くなります
次にdelete_node関数ですが、"pcurrent"がNULL、つまりノードが無いか、最後のノードの次を指している場合には、ノードの削除はできません
その他の場合、削除するノードがリストの最初か、最後か、途中かでリンクのつなぎ直しの方法が変わるので、場合分けしています
最後にinsert_node関数ですが、まず、make_student_node関数で挿入するノードを作成します
そして、挿入するノードが最初のノードか、リストの最後への挿入か、途中への挿入かでリンクのつなぎ直しの方法が変わるので、場合分けしています
それではメイン関数で 双方向リスト を使ってみましょう
#include <stdio.h>
#include <string.h>
#include "students.h"
int main(int argc, char* argv[]) {
student_list list;
init_student_list(&list);
insert_node(&list, 1, "佐藤紬");
insert_node(&list, 2, "鈴木陽翔");
insert_node(&list, 3, "高橋翠");
insert_node(&list, 4, "田中朝陽");
insert_node(&list, 5, "伊藤凛");
move_to(&list, 2);
delete_node(&list);
insert_node(&list, 6, "渡辺暖");
move_to(&list, 0);
student* pstudent = list.pcurrent;
while (pstudent != NULL) {
printf("学生番号: %d, 名前: %s\n", pstudent->id, pstudent->name);
move_forward(&list);
pstudent = list.pcurrent;
}
final_student_list(&list);
return 0;
}

環状リスト

環状リストのメリット
環状リストの実装
とりあえず 環状リスト を実装してみましょう
まず、ヘッダーファイル"student.h"です
#pragma once
#ifndef _INC_STUDENTS
#define _INC_STUDENTS
#include <stdio.h>
#define INIT_NODE_NUM (5)
#define NAME_SIZE (20)
typedef struct student {
unsigned int id;
char name[NAME_SIZE];
struct student* pnext;
} student;
typedef struct {
student* ppop;
student* ppush;
student* pprev;
size_t data_num;
size_t node_num;
} student_list;
int init_student_list(student_list* plist);
void final_student_list(student_list* plist);
int push(student_list* plist, unsigned int id, const char name[]);
int pop(student_list* plist, student* pstudent);
#endif // _INC_STUDENTS
"student"構造体は 連結リスト と同じです
"student_list"構造体は、データを書き込むノードへのポインタ"ppush"と、データを読み出すためのノードへのポインタ"ppop"を設けています
"pprev"は、データが満杯の時にノードを追加する際に必要な、"ppush"の前のノードを指すようにします
また、挿入や削除を行う際のノードの使用状態を確認するため、"data_num"および"node_num"を設けています
関数としては、初期化や終了の関数のほかに、データの入出力のためのpopおよびpush関数を宣言しています
次は実装です
#include "students.h"
#include <stdlib.h>
#include <string.h>
student* make_student_node() {
student* pstudent = (student*)malloc(sizeof(student));
if (pstudent != NULL) {
pstudent->id = 0;
pstudent->name[0] = 0;
pstudent->pnext = NULL;
}
return pstudent;
}
int insert_node(student_list* plist) {
int rc = -1;
student* pnew_student = make_student_node();
if (pnew_student != NULL) {
plist->pprev->pnext = pnew_student;
pnew_student->pnext = plist->ppush;
plist->ppush = pnew_student;
plist->node_num++;
rc = 0;
}
return rc;
}
int init_student_list(student_list* plist) {
int rc = -1;
student* pnew_student = make_student_node();
if (pnew_student != NULL) {
pnew_student->pnext = pnew_student;
plist->ppush = pnew_student;
plist->pprev = pnew_student;
plist->node_num = 1;
for (size_t i = 0; i < (INIT_NODE_NUM - 1); i++) {
rc = insert_node(plist);
}
plist->ppop = plist->ppush;
plist->data_num = 0;
}
return rc;
}
void final_student_list(student_list* plist) {
for (size_t i = 0; i < plist->node_num; i++) {
student* p2delete = plist->ppop;
plist->ppop = plist->ppop->pnext;
free(p2delete);
}
plist->ppop = NULL;
plist->ppush = NULL;
plist->data_num = 0;
plist->node_num = 0;
return;
}
int push(student_list* plist, unsigned int id, const char name[]) {
int rc = 0;
if (plist->data_num == plist->node_num) {
rc = insert_node(plist);
}
if (rc == 0) {
plist->ppush->id = id;
snprintf(plist->ppush->name, NAME_SIZE, "%s", name);
plist->pprev = plist->ppush;
plist->ppush = plist->ppush->pnext;
plist->data_num++;
}
return rc;
}
int pop(student_list* plist, student* pstudent) {
int rc = -1;
if ((pstudent != NULL) && (plist->data_num > 0)) {
pstudent->id = plist->ppop->id;
snprintf(pstudent->name, NAME_SIZE, "%s", plist->ppop->name);
pstudent->pnext = NULL;
plist->ppop = plist->ppop->pnext;
plist->data_num--;
rc = 0;
}
return rc;
}
まず初期化関数init_student_listですが、とりあえず5つの空のノードを作成、連結しています
最初のノードは特殊なので別途作成し、その後はinsert_nodeで残りの4つのノードを作例、連結しています
insert_node内では、 make_student_node で新たなノードを作成し、"student_list"の"pprev"が指すノードと"ppush"が指すノードの間に挿入します
なお、初期化時には"student_list"の"ppop"と"ppush"は同じノードを指していますが、"data_num"は0となっています
次に終了関数final_student_listですが、"student_list"の"ppop"が指すノードを辿って、"node_num"だけfree関数でノードを解放しています
データの挿入については、push関数を用意しています
基本的には"student_list"の"ppush"が指すノードに指定されたデータをコピーし、"ppush"を次のノードに移動します
ただ、"student_list"の"data_num"と"node_num"が等しい場合は空のノードが無いため、insert_nodeで新たなノードを追加します
データの取得はpop関数を使います。
引数として渡された"student"構造体に、"student_list"の"ppop"が指すノードのデータをコピーします
それではメイン関数で 環状リスト を使ってみましょう
#include <stdio.h>
#include <string.h>
#include "students.h"
int main(int argc, char* argv[]) {
student_list list;
init_student_list(&list);
push(&list, 1, "佐藤紬");
push(&list, 2, "鈴木陽翔");
push(&list, 3, "高橋翠");
push(&list, 4, "田中朝陽");
push(&list, 5, "伊藤凛");
push(&list, 6, "渡辺暖");
student person;
int rc = pop(&list, &person);
while (rc == 0) {
printf("学生番号: %d, 名前: %s\n", person.id, person.name);
rc = pop(&list, &person);
}
final_student_list(&list);
return 0;
}

二分木

二分木のメリット
二分木の実装
とりあえず簡単な二分木を実装してみましょう
まず、ヘッダーファイル"students.h"です
#pragma once
#ifndef _INC_STUDENTS
#define _INC_STUDENTS
#include <stdio.h>
#define NAME_SIZE (20)
typedef struct student {
unsigned int id;
char name[NAME_SIZE];
struct student* pleft;
struct student* pright;
} student;
typedef struct {
student* proot;
} student_tree;
int init_student_tree(student_tree* ptree);
void final_student_tree(student_tree* ptree);
int insert(student_tree* ptree, unsigned int id, const char name[]);
const student* at(const student_tree* plist, unsigned int id);
#endif // _INC_STUDENTS
今回は簡易な実装のため、ノードの削除は省いています
"student"構造体は、子ノードを2つ持つので、"pleft"と"pright"ポインタを設けています
"student_tree"構造体は、 ルート へのポインタ"proot"のみとなっています
"student_tree"構造体として設けなくても実装は可能なのですが、意外と便利なのでこのようにしています
後は初期化関数と終了関数、ノードの追加関数とデータ取得関数の宣言を追加しています
次は実装です
#include "students.h"
#include <stdio.h>
#include <stdlib.h>
int init_student_tree(student_tree* ptree) {
int rc = -1;
if (ptree != NULL) {
ptree->proot = NULL;
}
return rc;
}
void free_subtree(student* pnode) {
if (pnode != NULL) {
free_subtree(pnode->pright);
free_subtree(pnode->pleft);
free(pnode);
}
return;
}
void final_student_tree(student_tree* ptree) {
if (ptree != NULL) {
free_subtree(ptree->proot);
ptree->proot = NULL;
}
return;
}
student* make_student_node(unsigned int id, const char name[]) {
student* pnode = (student*)malloc(sizeof(student));
if (pnode != NULL) {
pnode->id = id;
snprintf(pnode->name, NAME_SIZE, "%s", name);
pnode->pleft = NULL;
pnode->pright = NULL;
}
return pnode;
}
int insert(student_tree* ptree, unsigned int id, const char name[]) {
int rc = -1;
if (ptree != NULL) {
student* pnew_node = make_student_node(id, name);
if (pnew_node != NULL) {
if (ptree->proot == NULL) {
ptree->proot = pnew_node;
rc = 0;
} else {
student* pnode = ptree->proot;
student* pparent = NULL;
while ((pnode != NULL) && (pnew_node != NULL)) {
pparent = pnode;
if (id < pnode->id) {
pnode = pnode->pleft;
} else if (id > pnode->id) {
pnode = pnode->pright;
} else { // IDの重複
free(pnew_node);
pnew_node = NULL;
}
}
if (pnew_node != NULL) {
if (id < pparent->id) {
pparent->pleft = pnew_node;
} else {
pparent->pright = pnew_node;
}
rc = 0;
}
}
}
}
return rc;
}
const student* at(const student_tree* ptree, unsigned int id) {
const student* pnode = NULL;
if (ptree != NULL) {
pnode = ptree->proot;
while ((pnode != NULL) && (pnode->id != id)) {
if (id < pnode->id) {
pnode = pnode->pleft;
} else if (id > pnode->id) {
pnode = pnode->pright;
}
}
}
return pnode;
}
まず、初期化関数init_student_treeですが、単に"proot"をNULLにセットしているだけです
次に終了関数final_student_treeですが、ノードを順番に解放するためにfree_subtreeを呼び出しています
free_subtreeは再帰関数で、"pleft"および"pright"の指す子ノード以下をfree_subtreeで解放しています
なお、終了条件は渡された"pnode"がNULLの場合です
次に挿入関数insertです
まず、make_student_nodeで新たなノードを取得します
次に、各ノードの"id"と比較して、小さければ左の、大きければ右のノードに移って比較を継続します
なお、今回は"id"の重複を許可していませんので、重複した場合は挿入失敗とします
一番最後のノードでは、"id"の値により"pleft"もしくは"pright"に新規のノードを配置します
今回は 二分木 の紹介のようなものなので、ここで終わりです
ただ、もう少し実用的な 二分木 では、左右のノードの数が概ね同じになるよう、追加で木構造の組み換えを行います
最後はデータ取得関数atです
引数の"id"と同じ値を持ったノードを探して、そのノードを返します
今回はノードを削除する関数を作成していませんので、ノードは終了関数が呼ばれるまで残っています
なので、無効な領域を指す心配が少ないことから、at関数ではノードへのポインタをそのまま返しています
それではメイン関数で 二分木 を使ってみましょう
#include <stdio.h>
#include <string.h>
#include "students.h"
int main(int argc, char* argv[]) {
student_tree tree;
init_student_tree(&tree);
insert(&tree, 3, "高橋翠");
insert(&tree, 2, "鈴木陽翔");
insert(&tree, 1, "佐藤紬");
insert(&tree, 5, "伊藤凛");
insert(&tree, 4, "田中朝陽");
insert(&tree, 6, "渡辺暖");
for (unsigned int i = 1; i <= 6; i++) {
const student* pperson = at(&tree, i);
printf("学生番号: %d, 名前: %s\n", pperson->id, pperson->name);
}
final_student_tree(&tree);
return 0;
}
初期化関数を呼び出した後、insert関数でデータを追加しています
ただし、順番は"id"順ではありません
今回のようなinsert関数では、"id"順にデータを挿入すると、 連結リスト と同じように直線的なリストとなってしまいます
そこで左右のノード数が概ね均等になるように、"id"の順番を入れ替えて挿入しています

後は"id"を順番に指定してデータを取得し、コンソールに表示しています

Discussion