このドキュメントは http://edu.net.c.dendai.ac.jp/ 上で公開されています。
前回は線形リストにより、先入れ先出し方式を学びました。 これは大量で容量があらかじめわからないデータを保存するのに便利な方法で すが、入れたデータがそのまま出てきてしまうということでデータ処理には結び付く手 法ではありません。 これに対して先入れ後出し方式(First In Last Out, FILO)または後入れ先出 し方式(LIFO)は最初に入れたものが最後に出るような記憶方式です。 このような記憶方式をスタックと呼びます。 スタックは最近入れたデータをすぐ取り出せるため、位置が近いデータ同士の 関係が深いような場合にその関係を解析できます。 このような状況はよくある状況ですので、スタックは良く使われます。 ここでは例として、括弧の処理を考えましょう。 括弧の処理とは複数の括弧が全て開く→閉じるという関係になっているかどう か調べることです。
単純に丸括弧のみの場合は、左括弧で数を増やし、右括弧で数を減らし、一回 も負の数にならずに最後が 0 になれば括弧が完全に閉じていると言えます。
#include <stdio.h>
int main(void){
FILE *f;
int c;
int kazu=0;
char file[]="samp1.c";
if((f=fopen(file,"r"))!=NULL){
while((c=getc(f))!=EOF){
if(c=='('){
kazu++;
}else if(c==')'){
kazu--;
if(kazu<0){
fprintf(stderr,"閉じ括弧が多過ぎます。\n");
fclose(f);
return 1;
}
}
}
fclose(f);
if(kazu==0){
fprintf(stderr,"括弧は正常です。\n");
return 0;
}else{
fprintf(stderr,"閉じ括弧が足りません。\n");
return 2;
}
}else{
fprintf(stderr,"ファイル %s を開けませんでした\n",file);
return 9;
}
}
しかし、 C 言語のプログラムなどの括弧が丸括弧だけでない場合のチェックする場合はどうで しょうか? C 言語では開き括弧、閉じ括弧の関係にあるものには次のものがあります。
()
{}
[]
''
""
/* */
< >
さて、では単純な例を考え、プログラムを作る方針を立てましょう。
ここでは簡単のために、(1)一文字で、(2)開き括弧と閉じ括弧が別の文字で、
(3)プリプロセッサ用ではない、(){}[]
のチェックプログラムを
作ってみましょう。
この場合も、上のプログラム同様、閉じ括弧が来る時にチェックを行います。
({[( )
({[( )]
この分析から予想されることは次のことです。
この処理のためにスタックを使用します。 まず、開き括弧はスタックに順番に格納します。これをスタッ クにプッシュすると言います。 そして、閉じ括弧が来たら、スタックから一つデータを取り出します。 取り出すデータは一番最近にプッシュした開き括弧です。 そして、この時、そのデータはスタックからデータが取り除かれます。 このような操作をスタックからデータがポップされると言います。 そして、ポップされた開き括弧と閉じ括弧が対応していればその括弧の対応は 取れていると言えます。 どこかで対応が取れなかったり、スタックが空になってしまったのにポップし ようとしたら括弧の対応がとれてなかったことになります。 入力が終った時、スタックが空であれば括弧の対応が取れていたことになりま す。
ここではスタックをグローバル変数の配列を使って実現します。 配列のサイズはあらかじめ決めておきます。したがってこのような方式ではそ のサイズ以上にプッシュができない制約があります。 さて、スタックを制御するのにスタックポインタを用意します。 はじめはなにも指さない状態ですが、値がプッシュされると配列の 0 番目か ら順番にデータを入れていきます。つまり、スタックポインタを一つ増やして は値を入れると言う動作をします。 一方、ポップはスタックポインタの指しているデータを返し、スタックポイン タは一つ前のデータを指すようにします。 このようにすると、スタックポインタが負の値かどうかを調べることにより、 スタックが空かどうかが分かります。 これらを実装したのが以下のプログラムです。
#include <stdio.h>
#define N 100
#define DEBUG
char stack[N];
int stackpointer=-1;
void printstack(void){
int i;
for(i=0;i<=stackpointer;i++){
printf("%c",stack[i]);
}
printf("\n");
}
void push(char c){
stack[++stackpointer]=c;
#ifdef DEBUG
printstack();
#endif
}
char pop(void){
char ret;
ret=stack[stackpointer--];
#ifdef DEBUG
printstack();
#endif
return ret;
}
int empty(void){
return stackpointer<0;
}
int main(void){
FILE *f;
int c;
int kazu=0;
char file[]="samp1.c";
if((f=fopen(file,"r"))!=NULL){
while((c=getc(f))!=EOF){
if((c=='(')||(c=='{')||(c=='[')){
push(c);
}else if(c==')'){
if(empty()){
fprintf(stderr,"閉じ括弧が多過ぎます。\n");
fclose(f);
return 1;
}else{
if(pop()!='('){
fprintf(stderr,"閉じ括弧が合ってません。\n");
fclose(f);
return 4;
}
}
}else if(c=='}'){
if(empty()){
fprintf(stderr,"閉じ括弧が多過ぎます。\n");
fclose(f);
return 1;
}else{
if(pop()!='{'){
fprintf(stderr,"閉じ括弧が合ってません。\n");
fclose(f);
return 4;
}
}
}else if(c==']'){
if(empty()){
fprintf(stderr,"閉じ括弧が多過ぎます。\n");
fclose(f);
return 1;
}else{
if(pop()!='['){
fprintf(stderr,"閉じ括弧が合ってません。\n");
fclose(f);
return 4;
}
}
}
}
fclose(f);
if(empty()){
fprintf(stderr,"括弧は正常です。\n");
return 0;
}else{
fprintf(stderr,"閉じ括弧が足りません。\n");
return 2;
}
}else{
fprintf(stderr,"ファイル %s を開けませんでした\n",file);
return 9;
}
}
スタックの容量を事実上無制限にするには線形リストを使います。 始めは空のスタックを用意します。スタックは pop で値を取り出すと、次の pop ではその前に push した要素を取り出せるようにしなければならりません。 そのため線形リストを用いた push, pop は次のように実現します。
typedef struct lst {
struct lst *next;
char *item;
} LIST;
LIST *stackpointer=NULL;
LIST* push(char *i){
LIST *newnode=(LIST *)malloc(sizeof(LIST));
if(newnode!=NULL){
newnode->item=i;
newnode->next=stackpointer;
stackpointer=newnode;
}
return newnode;
}
char* pop(void){
LIST *d=stackpointer;
char *i=stackpointer->item;
stackpointer=stackpointer->next;
free(d);
return i;
}
int empty(void){
return stackpointer==NULL;
}
push("a");
push("b");
次のプログラムでスタックをテストしなさい。
#include <stdio.h>
#include <stdlib.h>
typedef struct lst {
struct lst *next;
char *item;
} LIST;
/* 上のスタックのコードをここにコピーする */
int main(void){
push("abc");
push("def");
printf("%s\n",pop());
push("ghi");
printf("%s\n",pop());
printf("%s\n",pop());
return 0;
}
スタックを利用してカッコの対応がとれているかどうかをチェックするプログ ラムを書きなさい。但し、チェックするカッコは (){} [] の三種類とします。
我々が一般に数式と呼んでいるのは、数と数の間に演算子が入る形のものです。 ところで、この位置関係は実際には本質的ではありません。 例えば、英語では「Add 1 and 2」と先頭に演算(Add)を指定しますし、 日本語では「 1 と 2 を加える」と演算(加える)は最後に指定します。 そしてどちらも 1 + 2 という同じ式を表しています。 つまり、演算子と二つの数の並べ方には次の三通りがあります。
さて、 2 * 3 + 4 * 5 という式を考えてみましょう。 カッコをつけて表すと次のようになります。
中置記法では足し算とかけ算の演算子の優先順位という問題がありますが、前 置記法、後置記法ともそのような問題は発生しません。 特に後置記法では、カッコは必要ありません。 例えば、 (1+2)*3 という式は後置記法では 1 2 + 3 * となり、 1+(2*3) は 1 2 3 * + とカッコなしで区別できます。 さらに、後置記法では演算子が来た時に直前の二つの値に対してその演算を行 えば計算ができることになります。 この「直前の二つ」という情報を扱うのにスタックを使うことで計算プログラ ムが作れます。 後置記法での計算の概要は次の通りです。
逆ポーランド記法の数式を計算するプログラムを書きなさい。
例12-2 のプログラムに対し、(),{},[] でそれぞれ似たような処理がありま す。 この括弧をパラメータ化して(文字配列に入れて)、似たようなプログラムを一 つにまとめるように、プログラムを改良しなさい。