今回は簡単な例題として「キュー (queue)」という基本的なデータ構造を作ってみましょう。
たとえばチケットを買う場合、カウンタの前に並んで順番を待たなくてはいけません。キューはカウンタの前に並ぶ行列と考えてください。列の先頭にいる人から順番にチケットを買うことができますが、あとから来た人は列の後ろに並ばなくてはいけません。列の先頭まで進むと、チケットを購入することができます。これを表したのが下図です。
out in ────────────── <= A B C D E . . . Z <= ────────────── 図 : キューの動作
このように、キューはデータを取り出すときは列の先頭から行い、データを追加するときは列の後ろへ行います。このため、キューは「待ち行列」とか「先入れ先出し (FIFO : first-in, first-out)」と呼ばれます。
キューは配列を使って簡単に実現できます。先頭位置を示す front と末尾を示す rear を用意し、front と rear の間にあるデータをキューに格納されているデータとするのがポイントです。次の図を見てください。
0 1 2 3 4 5 6 7 8 9 rear = 0 ↓ QUEUE [ ] : QUEUE は空 front= 0 ↑ rear = 3 ↓ QUEUE [10 20 30 ] : データの追加 front= 0 ↑ rear = 3 ↓ QUEUE [10 20 30 ] : 10を取り出す front= 1 ↑ rear = 3 ↓ QUEUE [10 20 30 ] : 20,30を取り出す front= 3 ↑ 図 : キューの動作
まずキューは空の状態で、rear, front ともに 0 です。データの追加は、rear が示す位置にデータを書き込み、rear の値を +1 します。データ 10, 20, 30 を追加すると、図のようにデータが追加され rear は 3 になります。このとき front は 0 のままなので、先頭のデータは 10 ということになります。
次に、データを取り出す場合、front の示すデータを取り出しから front の値を +1 します。この場合、front が 0 なので 10 を取り出して front の値は 1 となり、次のデータ 20 が先頭になります。データを順番に 20, 30 と取り出していくと、3 つしかデータを書き込んでいないので当然キューは空になります。このとき front は 3 になり rear と同じ値になります。このように、front と rear の値が 0 の場合だけが空の状態ではなく、front と rear の値が等しくなると、キューは空になることに注意してください。
rear, fornt ともに値は増加していく方向なので、いつかは配列の範囲をオーバーします。このため、配列を先頭と末尾がつがっているリング状と考えて、rear, front が配列の範囲を超えたら 0 に戻すことにします。これを「循環配列」とか「リングバッファ」と呼びます。一般に、配列を使ってキューを実装する場合は、リングバッファとするのが普通です。
それでは実際にプログラムを作ってみましょう。最初に、基本的な操作関数を説明します。今回はプログラムを簡単にするため、キューに格納するデータは浮動小数点数 (double) に限定します。
関数名 | 機能 |
---|---|
Queue *make_queue(int n); | 指定した大きさのキューを作る。 |
void queue_delete(Queue *que); | キューを廃棄する。 |
double dequeue(Queue *que, bool *err); | キューからデータを取り出して返す。 |
bool enqueue(Queue *que, double x); | キューにデータを追加する。 |
double top(Queue *que, bool *err); | キューの先頭データを返す。データはキューから削除されない。 |
bool is_empty(Queue *que); | キューが空の場合は true を、そうでなければ false を返す。 |
bool is_full(Queue *que); | キューが満杯の場合は true を、そうでなければ false を返す。 |
int queue_length(Queue *que); | キューに格納されたデータ数を返す。 |
void clear(Queue *que); | キューを空にする。 |
キュー (Queue) は構造体を使って定義します。
リスト : Queue の定義 typedef struct { int front, rear, count, size; double *buff; } Queue;
メンバ変数 count はキューに格納されたデータ数をカウントします。この変数を用意することで、キューの状態を簡単にチェックすることができます。buff がキューの本体(配列)です。大きさはメンバ変数 size に格納します。
リスト : キューの生成 Queue *make_queue(int n) { Queue *que = malloc(sizeof(Queue)); if (que != NULL) { que->front = 0; que->rear = 0; que->count = 0; que->size = n; que->buff = malloc(sizeof(double) * n); if (que->buff == NULL) { free(que); return NULL; } } return que; }
関数 make_queue はキューを生成します。引数 n がキューの大きさです。関数 malloc で Queue のメモリを確保します。メンバ変数 front, rear, count は 0 で初期化します。あとは malloc でバッファの本体を取得して、que->buff にセットするだけです。
次はデータを追加する関数 enqueue を作ります。
リスト : キューにデータを追加する // キューは満杯か bool is_full(Queue *que) { return que->count == que->size; } // データの挿入 bool enqueue(Queue *que, double x) { if (is_full(que)) return false; que->buff[que->rear++] = x; que->count++; if (que->rear == que->size) que->rear = 0; return true; }
最初に関数 is_full を呼び出してキューが満杯かチェックします。is_full は que->count と que->size が等しいかチェックするだけです。キューが満杯であれば、enqueue は false を返します。データは que->rear の位置に格納し、q->count と q->rear の値を更新します。そして、q->rear の値が配列の範囲を超えたならば 0 に戻します。
次は、キューからデータを取り出す関数 dequeue を作ります。
リスト : キューからデータを取り出す // キューは空か bool is_empty(Queue *que) { return que->count == 0; } // データを取り出す double dequeue(Queue *que, bool *err) { if (is_empty(que)) { *err = false; return 0; } double x = que->buff[que->front++]; que->count--; *err = true; if (que->front == que->size) que->front = 0; return x; }
まず、関数 is_empty を呼び出してキューにデータがあるかチェックします。is_empty は q->count が 0 かチェックするだけです。キューが空の場合、dequeue は *err に false をセットして 0 を返します。データがある場合は q->buff[q->front] の位置にあるデータを取り出します。あとは、q->count と q->front の値を更新し、q->front の値が配列の範囲を超えたら 0 に戻します。
あとの操作関数は簡単なので説明は割愛します。詳細はプログラムリスト1をお読みくださいませ。
これでプログラムは完成です。それでは、簡単な実行例を示しましょう。
リスト : 簡単なテスト int main(void) { Queue *que = make_queue(8); bool err; for (int i = 0; i < 4; i++) enqueue(que, i); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); printf("%d\n", is_full(que)); while (!is_empty(que)) printf("%.3f\n", dequeue(que, &err)); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); printf("%d\n", is_full(que)); for (int i = 0; i < 8; i++) enqueue(que, i); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); printf("%d\n", is_full(que)); while (!is_empty(que)) printf("%.3f\n", dequeue(que, &err)); queue_delete(que); return 0; }
$ clang queue1.c $ ./a.out 4 0 0 0.000 1.000 2.000 3.000 0 1 0 8 0 1 0.000 1.000 2.000 3.000 4.000 5.000 6.000 7.000
make_queue でキューを作成して変数 que にセットします。for ループでキューにデータを 4 個セットします。queue_length の返り値は 4 で、is_empty と is_full の返り値はどちらも 0 (false) です。次に、while ループでキューにデータがある間、dequeue でデータを取り出します。先に入れたデータから順番に取り出されていることがわかります。
最後にデータを 8 個追加します。これでキューは満杯になるので、これ以上データを追加することはできません。is_full の返り値は 1 (true) になります。次に、dequeue でデータを取り出します。これでキューは空の状態になります。
キューは連結リストを使っても簡単に実装することができますが、大きな欠点がひとつあります。連結リストをキューとして使う場合、データを追加するときに最後尾までセルをたどっていく操作が必要になるため、要素数が多くなるとデータの追加に時間がかかってしまうのです。
そこで、先頭のセルを参照する変数のほかに、最後尾のセルを参照する変数を用意します。こうすると、先頭からセルをたどらなくても、最後尾にデータを追加することができます。下図を見てください。
rear ─→ NULL front ─→ NULL (1) キューが空の状態 rear ─────────────────────┐ ↓ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ ┌─┬─┐ front ─→│・│・┼→│・│・┼→│・│・┼→│・│・┼→ NULL └┼┴─┘ └┼┴─┘ └┼┴─┘ └┼┴─┘ ↓ ↓ ↓ ↓ data1 data2 data3 data4 (2) キューにデータがある場合 図 : キューの構造
この変数を front と rear としましょう。キューにデータがない場合は、(1) のように front と rear は NULL になっています。データがある場合は、(2) のように front は先頭のセルを参照し、rear は最後尾のセルを参照しています。これで、データの追加を効率的に行うことができます。
今回はセル (Cell) を直接操作してキューを作ってみましょう。定義するメソッドを下表に示します。
関数名 | 機能 |
---|---|
Queue *make_queue(void); | 空のキューを作る。 |
void delete_queue(Queue *que); | キューを廃棄する。 |
double dequeue(Queue *que, bool *err); | キューからデータを取り出して返す。 |
bool enqueue(Queue *que, double x); | キューにデータを追加する。 |
double top(Queue *que, bool *err); | キューの先頭データを返す。データはキューから削除されない。 |
bool is_empty(Queue *que); | キューが空の場合は true を、そうでなければ false を返す。 |
int queue_length(Queue *que); | キューに格納されたデータ数を返す。 |
void clear(Queue *que); | キューを空にする。 |
プログラムは次のようになります。
リスト : キューの定義 // セル typedef struct cell { double item; struct cell *next; } Cell; // セルの生成 Cell *make_cell(double x, Cell *next) { Cell *cp = malloc(sizeof(Cell)); if (cp != NULL) { cp->item = x; cp->next = next; } return cp; } // キューの定義 typedef struct { Cell *front; Cell *rear; } Queue; // キューの生成 Queue *make_queue(void) { Queue *que = malloc(sizeof(Queue)); if (que != NULL) que->front = que->rear = NULL; return que; }
構造体 Cell がセルを表します。メンバ変数 item のデータ型は double になります。関数 make_cell は新しいセルを生成して返します。キューは構造体 Queue で表します。関数 make_queue は malloc で Queue の本体を取得します。そして、メンバ変数 front と rear を NULL で初期化して、取得したメモリ que を返します。
リスト : キューの廃棄 void delete_cell(Cell *cp) { while (cp != NULL) { Cell *temp = cp->next; free(cp); cp = temp; } } void delete_queue(Queue *que) { delete_cell(que->front); free(que); }
キューの廃棄も簡単です。関数 delete_cell でセルを廃棄したあと、free でキューの本体 que を解放します。
次はデータを挿入する関数 enqueue を作ります。
リスト : データの挿入 bool enqueue(Queue *que, double x) { Cell *cp = make_cell(x, NULL); if (cp != NULL) { if (is_empty(que)) que->front = que->rear = cp; else { que->rear->next = cp; que->rear = cp; } return true; } return false; }
最初に make_cell で引数 x を格納するセルを生成します。生成できない場合は false を返します。次に、キューが空かチェックして、そうであれば、front と rear に新しいセル cp をセットします。キューが空でない場合、最後尾にデータを追加します。que->rear->next にセル cp セットして、最後尾のセルの後ろにつなげます。それから、que->rear を cp に書き換えて true を返します。
次はデータを取り出す関数 dequeue を作ります。
リスト : データの取り出し // キューは空か bool is_empty(Queue *que) { return que->front == NULL; } // キューからデータを取り出す double dequeue(Queue *que, bool *err) { if (is_empty(que)) { *err = false; return 0; } Cell *cp = que->front; double x = cp->item; *err = true; que->front = cp->next; free(cp); if (que->front == NULL) que->rear = NULL; return x; }
最初に関数 is_empty でキューが空かチェックします。is_empty は que->front が NULL であれば true を返します。空の場合は *err に false をセットして 0 を返します。次に、先頭のセル que->front を cp に、cp->item を x に、*err の値を true にセットします。あとは、que->front の値を cp->next に書き換えてから、削除するセル cp を free で解放します。que->front が NULL ならば、que->rear も NULL に書き換えます。最後に return で x を返します。
あとのプログラムは簡単なので説明は割愛します。詳細はプログラムリスト2をお読みください。
それでは簡単な実行例を示します。
リスト : 簡単なテスト int main(void) { Queue *que = make_queue(); bool err; for (int i = 0; i < 8; i++) enqueue(que, i); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); while (!is_empty(que)) printf("%.3f\n", dequeue(que, &err)); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); delete_queue(que); return 0; }
$ clang queue2.c $ ./a.out 8 0 0.000 1.000 2.000 3.000 4.000 5.000 6.000 7.000 0 1
キューに 0 から 7 まで enqueue で格納して dequeue でデータを取り出すと 0, 1, 2, 3, 4, 5, 6, 7 になります。データを入れた順番にデータが取り出されていることがわかります。
リスト : リングバッファによるキューの実装 (queue1.c) #include <stdio.h> #include <stdlib.h> #include <stdbool.h> // キューの定義 typedef struct { int front, rear, count, size; double *buff; } Queue; // キューの生成 Queue *make_queue(int n) { Queue *que = malloc(sizeof(Queue)); if (que != NULL) { que->front = 0; que->rear = 0; que->count = 0; que->size = n; que->buff = malloc(sizeof(double) * n); if (que->buff == NULL) { free(que); return NULL; } } return que; } // キューは満杯か bool is_full(Queue *que) { return que->count == que->size; } // キューは空か bool is_empty(Queue *que) { return que->count == 0; } // データの挿入 bool enqueue(Queue *que, double x) { if (is_full(que)) return false; que->buff[que->rear++] = x; que->count++; if (que->rear == que->size) que->rear = 0; return true; } // データを取り出す double dequeue(Queue *que, bool *err) { if (is_empty(que)) { *err = false; return 0; } double x = que->buff[que->front++]; que->count--; *err = true; if (que->front == que->size) que->front = 0; return x; } // 先頭データを参照する double top(Queue *que, bool *err) { if (is_empty(que)) { *err = false; return 0; } return que->buff[que->front]; } // キューをクリアする void clear(Queue *que) { que->front = que->rear = que->count = 0; } // キューの個数を求める int queue_length(Queue *que) { return que->count; } // キューの削除 void queue_delete(Queue *que) { free(que->buff); free(que); } // 簡単なテスト int main(void) { Queue *que = make_queue(8); bool err; for (int i = 0; i < 4; i++) enqueue(que, i); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); printf("%d\n", is_full(que)); while (!is_empty(que)) printf("%.3f\n", dequeue(que, &err)); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); printf("%d\n", is_full(que)); for (int i = 0; i < 8; i++) enqueue(que, i); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); printf("%d\n", is_full(que)); while (!is_empty(que)) printf("%.3f\n", dequeue(que, &err)); queue_delete(que); return 0; }
リスト : 連結リストによるキューの実装 (queue2.c) #include <stdio.h> #include <stdlib.h> #include <stdbool.h> // セル typedef struct cell { double item; struct cell *next; } Cell; // セルの生成 Cell *make_cell(double x, Cell *next) { Cell *cp = malloc(sizeof(Cell)); if (cp != NULL) { cp->item = x; cp->next = next; } return cp; } // キューの定義 typedef struct { Cell *front; Cell *rear; } Queue; // キューの生成 Queue *make_queue(void) { Queue *que = malloc(sizeof(Queue)); if (que != NULL) que->front = que->rear = NULL; return que; } // キューの廃棄 void delete_cell(Cell *cp) { while (cp != NULL) { Cell *temp = cp->next; free(cp); cp = temp; } } void delete_queue(Queue *que) { delete_cell(que->front); free(que); } // キューは空か bool is_empty(Queue *que) { return que->front == NULL; } // データの挿入 bool enqueue(Queue *que, double x) { Cell *cp = make_cell(x, NULL); if (cp != NULL) { if (is_empty(que)) que->front = que->rear = cp; else { que->rear->next = cp; que->rear = cp; } return true; } return false; } // キューからデータを取り出す double dequeue(Queue *que, bool *err) { if (is_empty(que)) { *err = false; return 0; } Cell *cp = que->front; double x = cp->item; *err = true; que->front = cp->next; free(cp); if (que->front == NULL) que->rear = NULL; return x; } double top(Queue *que, bool *err) { if (is_empty(que)) { *err = false; return 0; } return que->front->item; } void clear(Queue *que) { delete_cell(que->front); que->front = que->rear = NULL; } int queue_length(Queue *que) { int c = 0; for (Cell *cp = que->front; cp != NULL; cp = cp->next) c++; return c; } // 簡単なテスト int main(void) { Queue *que = make_queue(); bool err; for (int i = 0; i < 8; i++) enqueue(que, i); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); while (!is_empty(que)) printf("%.3f\n", dequeue(que, &err)); printf("%d\n", queue_length(que)); printf("%d\n", is_empty(que)); delete_queue(que); return 0; }