このページでは「キュー」を “ポインタを用いて” 実装する方法について解説していきたいと思います。
キューがどのようなものであり、キューの配列での実装方法は下記ページで解説しています。もし、これらをご存知ない方は、まず下記ページを読んでいただけると、以降の解説が理解しやすくなると思います!
【C言語/データ構造】スタックとキューの配列での実装方法ポインタで実装するので、ポインタの理解も必要です。また、構造体も利用するため構造体の理解も必要です。そのほかにも malloc
関数や NULL
の知識も必要です。
本ページでもこれらに関して極力解説するようにはしていますが、キューをポインタで実装するにあたって必要になる知識の解説ページへのリンクを下に貼っておくので、必要に応じてご参照していただければと思います。
【C言語】ポインタを初心者向けに分かりやすく解説 【C言語】構造体について初心者向けに分かりやすく解説 【C言語】malloc関数(メモリの動的確保)について分かりやすく解説 【C言語】「NULL」の意味とNULLを用いた「安全なポインタの使い方」Contents
キューのポインタでの実装方法
では、キューを “ポインタを用いて” 実装する方法について解説していきたいと思います。
前述の通り、キューを配列で実装する方法は下記ページで解説しています。
【C言語/データ構造】スタックとキューの配列での実装方法ポイントだけ抜粋すると、キューを実装するためのポイントは下記の4つになります。
- データの先頭を管理する
- データの最後尾を管理する
- データの追加時にはデータの最後尾の1つ後ろにデータを格納する
- データの取り出し時にはデータの先頭からデータを取得する
要は、これらのポイントを、配列を使わずに “ポインタで” 実現していきます。
データの格納先のメモリを用意する
まず、配列を使わないので、配列に代わるデータの格納先が必要になります。
このデータの格納先は malloc
関数で確保したメモリとしたいと思います。
malloc
関数は下記ページで解説していますが、要はプログラム内で利用するメモリを追加で取得する関数です。
引数に取得したいメモリのサイズをバイト単位で指定して実行すると、そのサイズのメモリが確保され、そのメモリの先頭アドレスを戻り値として取得することができます。
データの追加時には、その都度、データを格納する用のメモリを malloc
で取得し、取得したメモリにそのデータを格納していくようにします。
スポンサーリンク
各メモリをポインタで繋ぐ
で、キューを実装しようと思うと、どのデータがどの順番で追加されたのかが分からないといけません。
配列の場合は、この順番は配列の添字から確認できましたね!
ただし、単に malloc
したメモリはそれぞれで独立しており、このままだと、どのデータがどの順番で追加されたのかが分からないのでキューを実現することができません…。
なので、malloc
したメモリには単にデータを格納するだけでなく、”次に追加されたデータが格納されたメモリ” を指すためのアドレスも格納するようにします。
これを実現するために、例えば下記のような NODE_T
構造体を定義します。
typedef struct NODE {
/* 追加されたデータそのものを格納する変数 */
int data;
/* 次のノードを指すポインタ */
struct NODE *next;
} NODE_T;
data
が “追加されたデータそのもの” を格納するためのメンバで、next
が “次に追加されたデータが格納されたメモリ のアドレス” を格納するためのメンバ(つまりポインタ)になります。
そして、malloc
でメモリを取得するときには、上記の NODE_T
構造体のサイズ分のメモリを取得するようにし、さらにこのメモリのアドレスを NODE_T*
でキャストします。
node = (NODE_T*)malloc(sizeof(NODE_T));
これにより取得したメモリを NODE_T
構造体として扱うことができます。例えば data
メンバに 100
を格納する場合は、下記のようにアロー演算子を用いて data
メンバにアクセスします。
node->data = 100;
アロー演算子について詳しく知りたい方は、下記ページで解説していますのでこちらをご参照いただければと思います。
C言語のアロー演算子(->)を分かりやすく、そして深く解説今後は、malloc
によって取得したメモリ(NODE_T
構造体)をノードと呼びます。つまり、ノードは「データそのもの」と次のノードを指す「ポインタ」から構成されるデータです。
この next
で次に追加されたノードを指すようにすることで、どのデータがどの順番で追加されたかを把握することができるようになります。
上の図では、next
が次のノードを指している様子を矢印で表現しています。ポインタとは下記ページで解説している通り、矢印です。
例えばあるノードから次のノードを辿る処理は、下記のように next
を利用することで実現することができます。
/* nodeはノード構造体を指すポインタ */
nextNode = node->next;
キューの先頭と最後尾をポインタで指す
ここまでの内容により、キューに追加したデータの格納先が用意でき、さらに各データの追加された順番も把握できるようになりました。
次はいよいよキューへの「データの追加」と「データの取り出し」を実装していきます。
で、下記ページで解説しているように、キューへの「データの追加」と「データの取り出し」を行うためには、「データの最後尾」と「データの先頭」がどこであるかを管理しておく必要があります。
【C言語/データ構造】スタックとキューの配列での実装方法ポインタでキューを実装する際も同じで、これらの2つの位置を管理していく必要があります。
そのために、2つのポインタを導入し、1つ目のポインタでは「先頭のノード」、2つ目のポインタでは「最後尾のノード」を指させるようにします。
ここではこの1つ目のポインタの変数名を head
、2つ目のポインタの変数を tail
としたいと思います。
つまり、データの追加時には、tail
が指しているノードの1つ後ろにノードを追加することになります。そして、tail
には新しく追加したノードを指させるようにします。
また、データの取り出し時には、head
が指しているノードの「データそのもの」を取得して返却します。そして、head
が指しているノードの1つ後ろのノードを head
に指させるようにします。
データの追加やデータの取り出し時に、データの先頭とデータの最後尾を head
と tail
でしっかり管理しておくところが、ポインタでキューを実現するときのポイントになります。
また、malloc
したメモリは free
で解放する必要がありますので、データの取り出し時に不要になったノードをしっかり free
するところもポイントです。
それでは、このデータの追加とデータの取り出しの処理の詳細を、ここから解説していきたいと思います。
データの追加
データの追加は下記の2つの場合で処理が異なるので、この2つの場合それぞれの処理について解説していきたいと思います。
- キューが空でない場合
- キューが空の場合
キューが空でない場合のデータの追加
まずデータの追加を行う際には、データの新たな格納先が必要になります。
この格納先は前述の通り、malloc
により用意します。
ですので、下記のように malloc
を実行してノードを追加し、追加したノードの data
メンバにデータそのものを格納します(malloc
の戻り値 node
には、追加したノードの先頭アドレスが格納されます)。
node = (NODE_T*)malloc(sizeof(NODE_T));
/* inputは追加するデータ */
node->data = input;
ただし、このままの状態だとノードは独立した状態で、どのタイミングで追加されたのか分かりません。
そのため、今までの最後尾のノード(つまり tail
が指しているノード)の next
で、追加したノード(node
)を指させるようにします。
これにより、新しく追加したノード(node
)が 、tail
が指しているノードの “次に追加されたノード” であることが分かるようになります。
また、このノードの追加により最後尾ノードの位置も変わりますので、これに合わせて tail
の指す先も変更します。
具体的には、tail
には追加したノードを指させるようにします。追加したノードのアドレスは malloc
関数の戻り値 node
となりますので、tail = node
を行えば良いことになります。
まとめると、キューが空でない場合のデータの追加時には下記の4つの処理を行うことになります。
malloc
でノードを追加- 追加したノードにデータを格納
tail
が指すノードのnext
に追加したノードを指させるtail
に追加したノードを指させる
キューが空の場合のデータの追加
続いてキューが空の状態でのデータの追加について説明します。キューが空とは、要は head
も tail
も NULL
を指している場合になります。
で、この場合も malloc
してノードを追加し、その data
にデータそのものを格納することになるのですが、キューが空の状態なので、tail
が指すノードは存在しません。
ですので、「キューが空でない場合のデータの追加」とは異なった処理を行う必要があります。
ただし、処理自体は簡単で、head
と tail
両方に、malloc
で追加したノードを指させてやれば良いだけです。
つまり、「キューが空の場合」にデータを追加した場合、その追加したデータを格納するノードが「先頭ノード」であり、さらに「最後尾ノード」になります。
まとめると、キューが空の場合のデータの追加時には下記の4つの処理を行う必要があります。
malloc
でノードを追加- 追加したノードにデータを格納
head
に追加したノードを指させるtail
に追加したノードを指させる
スポンサーリンク
データの取り出し
データの取り出しは下記の2つの場合で処理が異なるので、この2つの場合それぞれの処理について解説していきたいと思います。
- 取り出してもキューが空にならない場合
- 取り出すとキューが空になる場合
取り出してもキューが空にならない場合のデータの取り出し
まず、データの取得先は先頭のノードに格納されているデータ、つまり head
が指しているノードに格納されているデータ(data
メンバ)です。
さらに、データの取り出しによってノードの先頭の位置が変わります。具体的には、ノードの先頭は head
が指しているノードの “次のノード”(next
が指しているノード)になりますので、head
に next
を指させます。
以上によりデータの取り出し自体は完了するのですが、データの取り出し先のノードはキューから不要なデータになりましたので、free
関数で解放してやる必要があります。
malloc
で取得したメモリは、不要になった際に free
しないとメモリリークになります
メモリリークが大量に発生すると、簡単に言うと、メモリが不足してプログラムが正常に動作できなくなります
free
関数の引数に指定するのは不要になったメモリのアドレスになります。この場合は、「データの取り出し先のノード」のアドレスです。つまり元々の head
ですね。
ただし、free
を行う前に head
に next
を指させてしまうと(head
に next
の値を代入してしまうと)、free
を行うアドレスが分からなくなってしまいます。
そのため、head
に next
を指させる前に、まず head
が指しているノード(データの取り出しを行う前の先頭ノード)のアドレスを他のポインタに退避しておくようにします。
そして、head
が next
を指した後に、その退避しておいたアドレスのノードを free
するようにします。
これで、不要になったノードを free
関数で解放することができます。
まとめると、取り出してもキューが空にならない場合のデータの取り出し時には、下記の4つの処理を行うことになります。
head
の指すノードからデータ取得head
のアドレスを他のポインタに退避head
に、head
が指すノードの次のノードを指させる- 他のポインタに退避しておいた元々の
head
のアドレスのノードをfree
する
取り出すとキューが空になる場合のデータの取り出し
最後に、データの取り出しを行うとキューが空になる場合のデータの取り出しを行う場合の処理について解説します。
「データの取り出しを行うとキューが空になる場合」とは、要は head
と tail
の指す先が同じノードである場合です(head
== tail
)。
この場合もデータの取得先は head
が指すノードの data
メンバになります。
また、データを取り出して不要になったノードを free
するところも同じです。free
するノードは head
が指すノードになります。
ただし、データを取り出すことでキューが空になるので、head
や tail
が指すノードは存在しません。ですので、単に head
と tail
には NULL
を指させます。
「取り出してもキューが空にならない場合のデータの取り出し」とは異なり、head
が指す先は NULL
固定ですので、他のポインタにアドレスを退避しておくような処理は不要になります。
まとめると、「取り出しを行うとキューが空になる場合」のデータの取り出し時には、下記の4つの処理を行うことになります。
head
の指すノードからデータ取得head
の指すノードをfree
head
にNULL
を指させるtail
にNULL
を指させる
キューをポインタで実装したソースコード
最後に、ここまでの解説を踏まえた「キューをポインタで実装したソースコード」を紹介しておきます。
#include <stdio.h>
#include <stdlib.h>
/* ノード構造体 */
typedef struct NODE {
/* 追加されたデータそのものを格納する変数 */
int data;
/* 次のノードを指すポインタ */
struct NODE *next;
} NODE_T;
/* キューの情報を構造体 */
typedef struct QUEUE {
/* キューの先頭を指すポインタ */
NODE_T *head;
/* キューの最後尾を指すポインタ */
NODE_T *tail;
} QUEUE_T;
void initQueue(QUEUE_T*);
void finishQueue(QUEUE_T*);
void printQueue(QUEUE_T*);
void enqueue(QUEUE_T*, int);
int deqeueue(QUEUE_T*);
/* キューを初期化する関数 */
void initQueue(QUEUE_T *queue){
queue->head = NULL;
queue->tail = NULL;
}
/* キューの終了処理をする関数 */
void finishQueue(QUEUE_T *queue){
NODE_T *p, *next;
/* キュー内の全ノードをfree */
p = queue->head;
while(p != NULL){
next = p->next;
free(p);
p = next;
}
}
/* エンキューする関数 */
void enqueue(QUEUE_T *queue, int input){
NODE_T *node;
/* mallocでノードを追加 */
node = (NODE_T*)malloc(sizeof(NODE_T));
/* メモリが足りなければ何もせず関数終了 */
if(node == NULL){
printf("メモリが取得ないためエンキューできません\n");
return ;
}
/* 追加したノードデータを格納 */
node->data = input;
/* 次のノードはないのでNULLを設定 */
node->next = NULL;
if(queue->head == NULL && queue->tail == NULL){
/* キューが空の場合 */
/* headに追加したノードを指させる */
queue->head = node;
/* tailに追加したノードを指させる */
queue->tail = node;
return ;
}
/* 空でない場合はキューの最後尾にnodeを追加 */
/* tailが指すノードのnextに追加したノードを指させる */
queue->tail->next = node;
/* tailに追加したノードを指させる */
queue->tail = node;
}
/* デキューする関数 */
int dequeue(QUEUE_T *queue){
int ret = 0;
NODE_T *oldHead;
/* キューが空なら何もせずに関数終了 */
if(queue->head == NULL){
printf("キューが空です\n");
return -1;
}
/* headの指すノードからデータを取得 */
ret = queue->head->data;
if (queue->head == queue->tail) {
/* 取り出すとキューが空になる場合 */
/* headの指すノードをfree */
free(queue->head);
/* headとtailにNULLを指させる */
queue->head = NULL;
queue->tail = NULL;
return ret;
}
/* 取り出してもキューが空にならない場合 */
/* headのアドレスをoldHeadに退避 */
oldHead = queue->head;
/* headに、headが指すノードの次のノードを指させる */
queue->head = queue->head->next;
/* デキューしたノード(元々のhead)を解放 */
free(oldHead);
return ret;
}
/* キューの中身を表示 */
void printQueue(QUEUE_T *queue){
NODE_T *p;
p = queue->head;
while(p != NULL){
printf("%d,", p->data);
p = p->next;
}
printf("\n");
}
int main(void){
int m;
int input;
int output;
QUEUE_T queue;
/* キューを初期化 */
initQueue(&queue);
while(1){
/* ユーザからメニューを選んでもらう */
printf("メニュー\n 1:エンキュー\n 2:デキュー\n 3:キュー表示\n それ以外:終了\n");
scanf("%d", &m);
/* 選ばれたメニューに応じて処理を分岐 */
if(m == 1){
printf("エンキューする数は?(0以上の整数のみ可)");
scanf("%d", &input);
if(input < 0){
printf("負の数は受け付けていません\n");
continue;
}
printf("%dをエンキューします\n", input);
enqueue(&queue, input);
}else if(m == 2){
printf("デキューします\n");
output = dequeue(&queue);
if(output != -1){
printf("%dをデキューしました\n", output);
}
} else if(m == 3){
printf("キューの中身を表示します\n");
printQueue(&queue);
} else {
/* 1, 2, 3以外の場合はwhile文を抜け出すためにbreakを実行 */
printf("終了します\n");
break;
}
}
/* 最後にキューのノードを全て解放 */
finishQueue(&queue);
return 0;
}
データの先頭を指す head
とデータの最後尾を指す tail
は下記の QUEUE_T
構造体にまとめています。
/* キューの情報を構造体 */
typedef struct QUEUE {
/* キューの先頭を指すポインタ */
NODE_T *head;
/* キューの最後尾を指すポインタ */
NODE_T *tail;
} QUEUE_T;
キューを扱う関数では、上記の QUEUE_T
構造体(のアドレス)を受け取り、この構造体の head
と tail
を用いて処理を行うようにしています。
キューの実装でのポイントとなる「データの追加」と「データの取り出し」は、それぞれ enqueue
関数と dequeue
関数で行なっています。
これらはキューのポインタでの実装方法で解説した内容をほぼそのままプログラミングしているだけなので、解説内容と見比べながらソースコードを読んでいただければ何をやっているかは理解していただけると思います。
また、メモリリークが発生しないように、プログラム終了時には finishQueue
関数で、キュー内の全てのノードのメモリを解放するようにしています。
まとめ
このページでは、キューをポインタで実装する方法の解説と、実際にキューをポインタで実装したソースコードの紹介を行いました。
キューをポインタで実装する時のポイントは下記の3つだと思います
- データの格納先のメモリを
malloc
で取得する - データの最後尾とデータの先頭をしっかり管理する
malloc
したメモリを不要になった時にしっかりfree
する
これらを実装しようと思うと、ポインタの指す先を意識しながらプログラミングする必要がありますので、実際に実装してみるとかなりポインタへの理解を深めることができると思います!
是非解説を理解した上で、ご自身でもキューをポインタで実装してみてください!