] > Stack, queue

第 5 回 キュー、スタック

本日の内容


このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。

5-1. キュー

FIFO(First In First Out)とは、データの処理の順番として、先に 入ったものは先に出されるという意味です。 これは順番待ちなどで起きることです。 この仕組みを実現するには、来たデータを順番に並べて、来た順にアクセスす るようにします。 このようなデータ記憶の方式をキュー(queue)と言います。 また、待ち行列とも言います。 キューはデータを送る側と受ける側が同期していないような状況で良く使われ ます。 例えば、プリンタ出力やメールの送受などの通信のやりとりの他、プログラム の内部処理でもイベントやファイルの入出力でも使用されます。

キューに対する操作は、次の 3 つからなります。

  1. キューが空かどうか調べる(empty)
  2. キューに要素を入れる(enqueue)
  3. キューから要素を取り出す(enqueue)

配列を使った処理

C 言語で、まず配列を使用して実現する方法を考えます。 配列を使うと言うことは、最初から有限の領域になりますので、キューが溢れ る可能性があります。 とりあえずそれは考慮しないで実装することにします。 まず、大きな配列が用意されているとします。 そして、キューに要素を入れるには、要素を入れるべき位置(ポインタ)に要素 を入れ、位置(ポインタ)を一つずらします。 これをプログラムで素朴に書くと次のようになります。


#define MAX 50
int q[MAX];
int *e=q;
void enqueue(int x){
  *e++=x;
  return;
}

これでまずいのは書き込む要素が多くなると配列で確保した領域をはみ出して しまうことです。そのためなんらかの措置をする必要があります。 しかし、その前に dequeue の仕組みを考えましょう。 一つの考え方は常に配列の先頭から取り出す方式です。 しかし、これを行うと、配列の先頭から要素を取り出す度に、取り出したあと を埋めるために queue の内容を前に詰めなければなりません。 この手間はばかにならないので、詰めずに済ませる方法が必要です。 そこで、 enqueue と同様に要素を取り出すためのポインタを考えます。 要素を取り出したら次の要素を指すようにします。すると、前に詰める必要が なくなるため速く要素を取り出せます。 問題は、配列の容量をはみ出してしまうことです。

キューの動作

ここで、 dequeue の動作を考えると、一回要素を取り出してしまった部分は 使用しません。そこで、これを再利用することを考えます。 そのために、配列で用意した領域を使い切ったら、配列の先頭に戻るようにし ます。こうすることにより、多くのデータを高速に処理することができます。 以下に配列を用いたキューのプログラムを示します。


#define MAX 50
int q[MAX];
int *e=q;
int *d=q;
int enqueue(int x){
  int *next;
  next=e+1;
  if(next>=q+MAX){next=q;}
  if(next!=d){
    *e=x;
    e=next;
    return 1;
  }else{
    return 0;
  }
}
int empty(){
  return e==d;
}
int dequeue(){
  int value;
  if(!empty()){
    value=*d++;
    if(d>=q+MAX){
      d=q;
    }
    return value;
  }
  /* 要素がないのに要素を取り出そうとした時 */
  return 0; /* C++ ならエラーを発生できるのだが…… */
}

演習5-1

この queue をテストするため、次のプログラムを用意しました。 これを実際に実行して正常に動くか確かめなさい。


#include <stdio.h>
int enqueue(int x);
int empty();
int dequeue();
main(){
  enqueue(5);
  enqueue(2);
  enqueue(8);
  while(!empty()){
    printf("%d\n",dequeue());
  }
}

線形リストを使った処理

配列を使用したキューは、あらかじめ配列の容量を決めておく必要があるため、 見積りより多くの要素が来た場合破綻します。 コンピュータのメモリが許す限りデータを受け入れられるようにしたい場合、あ らかじめ全てのメモリをキューに割り当てるよりは、メモリを動的に確保すべ きです。 上のプログラムで示したように、キューを実現するには、注目している場所の データの出し入れと、次の領域の計算できれば良いわけで、これは別に配列で ある必要はありません。 線形リストという構造を使っても可能です。 線形リストとは要素が一直線に並んでいるものです。 構造として、「値」と「次の要素の位置」の二つを持ちます。

線形リスト

このような構造を使えばメモリの許す限りキューを作ることができます。 これを構造体で作るには、値を入れる要素と、次の要素を指すためのポインタ を入れる必要があります。このポインタの型はそのポインタを含む構造体自身 を指すポインタの型になります。 一方、キューに対する各処理は次のように書けます。

empty
dequeue のポインタが NULL かどうか
enqueue
新しい領域を確保し、値を代入、ポインタは Null にする。 今、自分の指しているものがある時は、現在の領域のポインタに新しい領域を 代入する。ない場合は初めの一個目の要素なので、 dequeue ポインタに新し い領域の番地を入れる。 そして、どちらとも、enqueue ポインタを新しい領域を指すようにする。
dequeue
ポインタが NULL だったらエラー。 そうでなければ、要素をとりだし、次の領域のポインタを代入し、今の領域を 捨てる。

これを実現すると下記のようになります。llist は自分自身の型を指すポイン タを含んでいることに注意して下さい。


#include <stdlib.h>
struct llist {
 int value;
 struct llist *pointer;
};
struct llist *e=NULL;
struct llist *d=NULL;
int empty(){
  return d==NULL;
}
int enqueue(int x){
  struct llist* next;
  next = (struct llist *) malloc( sizeof (struct llist));
  if(next!=NULL){ /* メモリが確保できないと NULL が返される */
    next->value=x;
    next->pointer=NULL;
    if(e==NULL){
      d=next;
    }else{
      e->pointer=next;
    }
    e=next;
    return 1;
  }else{
    return 0;
  }
}  
int dequeue(){
  int x;
  struct llist *next;
  if(!empty()){
    x=d->value;
    next=d->pointer;
    free(d);
    d=next;
    return x;
  }else{
  /* 要素がないのに要素を取り出そうとした時 */
  return 0; /* C++ ならエラーを発生できるのだが…… */
  }
}  

なお、ここで、typedef struct llist { ... } LLIST; とする と、それ以降 struct llist と書かなければならない部分を LLIST と短く書 くことが出来ます。 書き直すと次のようになります。


#include <stdlib.h>
typedef struct llist {
 int value;
 struct llist *pointer;
} LLIST;
LLIST *e=NULL;
LLIST *d=NULL;
int empty(){
  return d==NULL;
}
int enqueue(int x){
  LLIST* next;
  next = (LLIST *) malloc( sizeof (LLIST));
  if(next!=NULL){ /* メモリが確保できないと NULL が返される */
    next->value=x;
    next->pointer=NULL;
    if(e==NULL){
      d=next;
    }else{
      e->pointer=next;
    }
    e=next;
    return 1;
  }else{
    return 0;
  }
}  
int dequeue(){
  int x;
  LLIST *next;
  if(!empty()){
    x=d->value;
    next=d->pointer;
    free(d);
    d=next;
    return x;
  }else{
  /* 要素がないのに要素を取り出そうとした時 */
  return 0; /* C++ ならエラーを発生できるのだが…… */
  }
}  

演習5-2

演習5-1 で使ったテストを利用し、上のプログラムのテストをし、正常に動作 するか確かめなさい。


5-2. スタック

スタックのプログラム

FILO(First In Last Out)とは、データ処理の順番として、一番最後 に来たものから順に遡って処理をするという意味です。 そして、これを実現するデータ構造をスタックと言います。 スタックにデータを入れることを pushと言い、データを取り出す ことを popと言います。 また、データを入れる位置と取り出す位置は常に同じ位置になりますが、そこ を指すポインタをスタックポインタと言います。 スタックも直線的にデータを並べれば実現できるので、キューと同様に配列や 線形リストを使うと実現できます。 スタックを実現するプログラムを以下に示します。


#define MAX 50
int q[MAX];
int stackpointer=0;
int push(int x){
  if(stackpointer+1<MAX){
    q[++stackpointer]=x;
    return 1;
  }else{
    return 0;
  }
}
int empty(){
  return stackpointer==0;
}
int pop(){
  if(!empty()){
    return q[stackpointer--];
  }
  /* 要素がないのに要素を取り出そうとした時 */
  return 0; /* C++ ならエラーを発生できるのだが…… */
}


#include <stdio.h>
typedef struct st {
  int num;
  struct st *pointer;
} stack;
stack *stackpointer=NULL;
int empty(){
  return stackpointer==NULL;
}
int push(int x){
  stack *next=(stack *) malloc(sizeof(stack));
  if(next!=NULL){ /* メモリが確保できないと NULL が返される */
    next->num = x;
    next->pointer=stackpointer;
    stackpointer=next;
    return 1;
  }else{
    return 0;
  }
}
int pop(){
  int x=stackpointer->num;
  stack *p=stackpointer;
  stackpointer=p->pointer;
  free(p);
  return x;
}

演習5-3

スタックを実現するこれらのコードをテストしなさい。


スタックは、式やプログラムの解釈と密接な関係がありとても重要です。 サブルーチンを呼び出す時などに利用されます。 一方、式の処理にも利用されています。

カッコの処理

まずカッコの処理について考えてみます。 カッコの処理の基本として正しく閉じているか閉じてないかを判断することを 考えます。 カッコが一種類だけなら、スタックを使わずとも、整数変数を一つ用意して、 開きカッコで数を足し、閉じカッコで数を減らしていき、一回もマイナスにな らずに最後 0 で終るかどうか判断することで処理できます。 しかし、 HTML や XML のタグのように対応する開始タグと終了タグの種類が 多い場合どうすれば良いでしょうか? この場合、閉じカッコは一番近い開きカッコに対応するということを利用し、 開きカッコをスタックに順に push していき、閉じカッコが出現したらスタッ クから開きカッコを pop して対応しているかを調べることで処理できます。 開きタグと閉じタグの文法は次のようになっています。

開きタグ
<要素名 オプション1="値1" オプション2="値2" ... >
閉じタグ
</要素名>

この文法を踏まえ、開きタグと閉じタグの対応を確認するプログラムを示しま す。スタックには要素名を指す文字のポインタを入れます。 なお、このプログラムでは要素名を解釈中かそうでないかという状態を保持す るフラグという手法を使っています。 あと、要素名を取り出すたびに文字列の領域を確保し、要素名をスタックから 取り出した後領域を解放してます。


#include <stdio.h>
#include <string.h>
int push(char *);
char * pop();
int empty();

void error(int i){
  while(!empty()){
    free(pop());
  }
  printf("error %d\n",i);
  exit();
}
main(){
  int flag=0;
  int l;
  char buffer[50];
  char c;
  char *p,*q;
  while((c=getchar())!=EOF){
    if(flag){
      if((c==' ')||(c=='>')){/* 要素名終了 */
	*p='\0';
	flag=0;
	if(buffer[0]=='/'){ /* 終了タグ処理 */
	  if(empty()){error(1);}/* 開始タグ無し */
	  else{
	    q=pop();
	      printf("%s popped\n",q);
	    if(strcmp(q,buffer+1)!=0){
	      free(q);/* 不一致 */
	      error(2);
	    }else{
	      free(q);/* Ok */
	    }
	  }
	}else{
	  /* 開始タグ処理(プッシュ) */
	  l=strlen(buffer);
	  q=(char *)malloc((l+1)*sizeof(char));
	  strcpy(q,buffer);
	  push(q);
	  printf("%s pushed\n",q);
	}
      }else{
	*p++=c;
      }
    }else{
      if(c=='<'){
	p=buffer;
	flag=1;
      }
    }
  }
  if(empty()){
    printf("Ok.\n");
  }else{
    error(3);
  }
}

なお、HTML、 XML とも DOCTYPE 宣言 <!DOCTYPE ... > という終了タ グのないタグがあります。 また、 HTML では開始タグや終了タグを省略できます。一方 XML では基本的 に終了タグは省略できませんが、 <hr /> のように最後に / を付けて 開始タグと終了タグを兼用出来ます。 上のプログラムを実用化するにはこれらを考慮する必要があります。

演習5-4

stack に入れられる要素を文字へのポインタに改造し、上のプログラムを動か しなさい。 そして、テストデータを作り、正常に動作することを確かめなさい。 但し、上のプログラムでは終了状態として Ok, error 1, error 2, error 3 があるので、これら全てが正常に発生するようにテストデータを作りなさい。


数式処理

次に、数式を考えます。 数式は数と演算子と呼ばれるものからできています。 +,-,*,/ など我々が使う演算子はその演算子をはさむ両方の値に対して計算を 行い、答を出します。 その際、どのような順番で計算しても良いわけではなく、各演算子には優先順 位があります。 また、カッコを利用すると演算の順序を変えることができます。 例えば、 2*3+4*5 を考えた時、これは ((2*3)+(4*5))と同じ意味になります。 このカッコをつけた式に対して、各カッコの中の値がわかれば計算を進めるこ とができます。 ここで、カッコつきの演算を抽象的に考えてみます。 演算子というのは両隣の値から一つの値を計算するものなので、これは二つの 引数を持つ関数と考えることができます。 つまり、 (2*3) は 2 と 3 が引数になるわけです。 いまのところカッコの中は数が 2 つと演算子がひとつという関係です。 そこで、演算子が真中にあるという書き方の他に、先頭に置くという方式と、 最後に置くと言う方式も考えられます。

中置記法
((2*3)+(4*5))
前置記法
(+ (* 2 3)(* 4 5))
後置記法(逆ポーランド記法)
((2 3 *)(4 5 *) +)

このうち注目したいのは逆ポーランド記法です。 演算子が閉じカッコの前にあるので、 演算子が現れたら、直前の二つの値に対して計算をすることで、カッコの中の値が求まります。 演算子があると必ずカッコが閉じるので、一番内側のカッコから自然に計算が 出来ます。実際、逆ポーランド記法ではカッコを無くしても計算の順序に曖昧 さは生じません。つまり 2 3 * 4 5 * + と書いても正しく計算が可能です。 このような記法に対して、左から式を見ていき、演算子が現れたら一番近い二 つの値に対して計算を行えばいいので、スタックを利用すれば計算が出来ます。

逆ポーランド記法を(足し算だけ)計算するプログラムを以下に示します。


#include <stdio.h>
typedef struct st {
  int num;
  struct st *pointer;
} stack;
stack *stackpointer=NULL;
int empty(){
  return stackpointer==NULL;
}
int push(int x){
  stack *next=(stack *) malloc(sizeof(stack));
  if(next!=NULL){ /* メモリが確保できないと NULL が返される */
    next->num = x;
    next->pointer=stackpointer;
    stackpointer=next;
    return 1;
  }else{
    return 0;
  }
}
int pop(){
  int x=stackpointer->num;
  stack *p=stackpointer;
  stackpointer=p->pointer;
  free(p);
  return x;
}
main(){
  char *p;
  char first;
  int i;
  int x,y;
  char *formula[] ={"1","2","+","3","+",NULL};
  for(i=0; formula[i]!=NULL; i++){
    first=*(formula[i]);
    switch(first){
    case '0':
    case '1':
    case '2':
    case '3':
    case '4':
    case '5':
    case '6':
    case '7':
    case '8':
    case '9':
      push(atoi(formula[i]));
      break;
    case '+':
      y=pop();
      x=pop();
      push(x+y);
      break;
    }
  }
  printf("%d\n",pop());
}

演習5-5

上のプログラムを改造してかけ算も計算できるようにしなさい。 そして、 2 3 * 4 5 * + が正しく計算できるか確かめなさい。

5-3. C++ でのキューとスタック

C++ では STL でキューとスタックが用意されています。 以下のプログラムはその使用例です。 キューでは enqueue, dequeue の代わりに push(), pop() が使われ、 先頭の要素は pop() で得ずに front() をつかいます。


#include <iostream>
#include <queue>
#include <deque> // または list
using namespace std;
main(){
  queue<int, deque<int> > q;
  q.push(5);
  q.push(2);
  q.push(8);
  while(!q.empty()){
    cout << q.front() << endl;
    q.pop();
  }
}

一方スタックでも pop() で値を取り出さずに top() で取り出してから pop() で値を取り除きます。


#include <iostream>
#include <stack>
#include <deque> // または list か vector
using namespace std;
main(){
  stack<int, deque<int> > q;
  q.push(5);
  q.push(2);
  q.push(8);
  while(!q.empty()){
    cout << q.top() << endl;
    q.pop();
  }
}

5-4. Java でのキューとスタック

Java では java.util.LinkedList という線形リストのクラスに先頭と最後の 要素の出し入れをするメソッドが実装されています。 また C++ と同様に要素を取り出すメソッドと、要素を消すメソッドは別になっ ています。 なお、LinkedList では、格納できる値は Object になります。 従って、値を取り出す時、必ずキャストしないと取り出した値を元のように扱 うことができなくなります。 一方、値が int などの基本型の場合、そのままでは Object として使えませ ん。この場合、ラッパークラスというクラスを使って値を変換す る必要があります。 int であれば java.lang.Integer というクラスを使ってオブジェクトに変換 します。 従って、値を取り出す時は、 java.lang.Integer でキャストした後、インタ フェースである intValue() メソッドで元の値に変換します。

以下は Java でのプログラム例です。


class TestQueue {
    public static void main(String arg[]){
	java.util.LinkedList l = new java.util.LinkedList();
	l.addLast(new java.lang.Integer(5));
	l.addLast(new java.lang.Integer(2));
	l.addLast(new java.lang.Integer(8));
	while(!l.isEmpty()){
	    System.out.println(((java.lang.Integer)l.removeFirst()).intValue());
	}
    }
}


class TestStack {
    public static void main(String arg[]){
	java.util.LinkedList l = new java.util.LinkedList();
	l.addFirst(new java.lang.Integer(5));
	l.addFirst(new java.lang.Integer(2));
	l.addFirst(new java.lang.Integer(8));
	while(!l.isEmpty()){
	    System.out.println(((java.lang.Integer)l.removeFirst()).intValue());
	}
    }
}

坂本直志 <sakamoto@c.dendai.ac.jp>
東京電機大学工学部情報通信工学科