プログラミングC
第2回 演習問題

A問題(50点)

演習問題1

通常、平均値を求める場合は、全ての要素(数値)を足してから、その個数で除算する。しかし、前回までの平均値と新たな入力値から逐次その時点での平均値を求めることもできる。このような平均値の算出方法を逐次平均と呼び、プログラムの実行中にデータが次々と入力されるようなときに、入力毎逐次平均値を計算、表示させることもできる。

逐次平均は以下のような式で計算できる。
平均=(前回までの平均*前回までのデータの個数+今回のデータ)/今回までのデータの個数
  1. 外部変数を用いて上記の方法で逐次平均を計算する関数void SeqAvgExt (double)を作成し、これを用いて実行例の通りに動作するプログラムを作成せよ。
    なお、関数およびプログラム全体の仕様は以下の通りとする。
    • 前回までの平均値(初期値は適宜設定すること)をdouble型の外部変数avgで保持する
    • データの個数(初期値は適宜設定すること)をint型の外部変数cntで保持する
    • 関数SeqAvgExtの引数は新規入力された値とする
    • main関数でキーボードからデータの入力を受け付け、関数SeqAvgExtに引数として渡す
    • 関数SeqAvgExtは逐次平均値の計算を行い、データの個数と平均値の更新も行う
    • main関数で外部変数avgとcntを参照してデータ個数、平均値の表示を行う
    • ctrl+d でデータ入力を終了し、最終的な平均値の表示を行う
    (提出ファイル名: prog01a.c)

  2. 上で作ったプログラムを改良し、外部変数の代わりに静的変数を用いて上記の方法で逐次平均を計算する関数double SeqAvgStatic(double)を作成し、これを用いて実行例の通りに動作するプログラムを作成せよ。
    なお、関数およびプログラム全体の仕様は以下の通りとする。
    • 関数SeqAvgStaticの引数は新規入力された値とする
    • 関数SeqAvgStaticの戻り値は逐次平均の計算法で求めた新しい平均値とする
    • 前回までの平均値(初期値は適宜設定すること)を関数SeqAvgStaticの内部で静的変数として保持する
    • データの個数(初期値は適宜設定すること)を関数SeqAvgStaticの内部で静的変数として保持する
    • main関数でもデータの個数を自動変数として保持する
    • main関数でキーボードからデータの入力を受け付け、関数SeqAvgStaticに引数として渡す
    • 関数SeqAvgStaticは逐次平均値の計算を行い、戻り値として新しい平均値を返す
    • main関数でデータ個数、平均値の表示を行う
    • ctrl+d でデータ入力を終了し、最終的な平均値の表示を行う
    • 外部変数は使用しない(使用している場合は減点する)
    (提出ファイル名: prog01b.c)
実行例
% ./a.out
データを入力して下さい: 2.0
データの個数 = 1,ここまでの平均 = 2.000000
データを入力して下さい: 4.0
データの個数 = 2,ここまでの平均 = 3.000000
データを入力して下さい: 6.0
データの個数 = 3,ここまでの平均 = 4.000000
データを入力して下さい: 8.0
データの個数 = 4,ここまでの平均 = 5.000000
データを入力して下さい: ^D
最終的な平均値は5.000000です。
% ./a.out
データを入力して下さい: 2.2
データの個数 = 1,ここまでの平均 = 2.200000
データを入力して下さい: 3.6
データの個数 = 2,ここまでの平均 = 2.900000
データを入力して下さい: 0.6
データの個数 = 3,ここまでの平均 = 2.133333
データを入力して下さい: 7.9
データの個数 = 4,ここまでの平均 = 3.575000
データを入力して下さい: 7.7
データの個数 = 5,ここまでの平均 = 4.400000
データを入力して下さい: 4.9
データの個数 = 6,ここまでの平均 = 4.483333
データを入力して下さい: ^D
最終的な平均値は4.483333です。
%
2つのプログラムを比較すると、外部変数を使用する方が静的変数を使う場合に比べて必要な変数の種類も少なく、全体としてはシンプルな形になるはずです。つまり、この演習問題程度の規模であれば外部変数の方が便利と言えるかもしれません。しかし、プログラムの規模が大きくなり、複雑になるとプログラム中の どこからでもアクセスできるという外部変数の特性がかえって問題となり、安易に使用すると予想外の 動作を引き起こす原因になります。外部変数と静的変数、それぞれの特性と使いかたをよく把握した上で上手に使い分けるようにしてください。基本的には今後のプログラミングCの 演習や試験では、問題文中で外部変数を使うよう指示があった場合のみ 使用するようにして下さい。

B問題(50点)

演習問題2

与えられた 2 次元配列の要素の中から、入力された数値と一致するものを リニアサーチにより探し出すプログラムを作成する。一致の可能性は複数回ある可能性も考慮し、サーチ終了後に一致の回数も表示されるようにすること。
(提出ファイル名: prog02.c)
#include <stdio.h>
#define ROW 3
#define COL 4

int main()
{
  int array[ROW][COL] = {
    { 2,  3, 12,  3},
    { 4, 10,  5,  6},
    { 8,  9,  0,  7},
  };
  int n;
  /* 必要に応じて変数宣言を追加 */

  printf("数値を入力してください: ");
  scanf("%d", &n);
  /* 

    ここを作成

  */
}
実行例
% ./a.out
数値を入力してください: 2
array[0][0] が 2 です
2次元配列 array の要素に 2 は 1 個ありました
% ./a.out
数値を入力してください: 3
array[0][1] が 3 です
array[0][3] が 3 です
2次元配列 array の要素に 3 は 2 個ありました
% ./a.out
数値を入力してください: 27
2次元配列 array の要素に 27 はありません
%

演習問題3

机の上に本を何冊か積み上げ、そこから本を取る場合、 一番最後に積んだ本を最初に取り出すことになり、 一番最初に積んだ本は最後に取り出されることになる。 このように 最後に入れたデータを最初に取り出す形式(後入れ、先出し、Last In, First Out: LIFO)のデータ構造は スタック(stack)と呼ばれ、コンピュータの基本的なデータ構造の1つである。 以下にスタックに関する基本的な用語を記す。 プログラミング入門ハンドアウトの第13回(p68-)も参照のこと。
以下の仕様を満たす関数 stackIO を用いてスタックを実現するプログラムを作成する。 関数の中身を埋めて、プログラムを完成させなさい。
「関数 stackIO の仕様」
(提出ファイル名: prog03.c)
スタックの実装にあたってはプログラミング入門ハンドアウトの第13回(p68-)を参考にすること。 ただし、ここに示した例は外部変数を用い、プッシュ(格納)とポップ(取り出し)はそれぞれ別の関数で行なっている。 今回の問題では静的変数を用いて関数内に全ての情報を格納し、プッシュ(格納)とポップ(取り出し)、そして表示も 同じ関数を用いる点が異なるので注意すること。
#include <stdio.h>
#define N 5
#define EMPTY -1
#define FULL  -2

int stackIO(int);

int main()
{
  int n, r;

  printf("スタックを実現するプログラム\n");
  printf("  正の整数値:入力値をスタックに格納する(Push)\n");
  printf("  負の整数値:スタックからデータを取り出す(Pop)\n");
  printf("  0:終了\n");
  while (1) {
    printf("整数値を入力 (正:格納(Push),負:取出(Pop),0:終了): ");
    scanf("%d", &n);
    if (n == 0) break; /* 終了 */
    r = stackIO(n); /* 格納または取出 */
    if (r > 0) printf("取出データ = %d\n", r); /* 取得データの表示 */
    else if (r == EMPTY) printf("エラー(スタックが空です)\n"); 
    else if (r == FULL ) printf("エラー(スタックが満杯です)\n"); 
    stackIO(0); /* 表示 戻り値は使用しない */
  }
  return 0;
}

/* 
[引数]
正の整数: 格納,負の整数: 取出,0: 表示
[戻り値]
格納の場合) スタックが満杯: マクロ定数 FULL,それ以外: 0
取出の場合) スタックが空: マクロ定数 EMPTY,それ以外: 取り出した値
表示の場合) 0
*/
int stackIO(int x)
{
  static int stack[N]; /* データを格納する配列 */
  static int size = 0; /* データ数 */
  /* 

    ここを作成

  */
}
実行例
% ./a.out
スタックを実現するプログラム
  正の整数値:入力値をスタックに格納する(Push)
  負の整数値:スタックからデータを取り出す(Pop)
  0:終了
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 31
size = 1 [31 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 41
size = 2 [31 41 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 59
size = 3 [31 41 59 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 59
size = 2 [31 41 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 26
size = 3 [31 41 26 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 53
size = 4 [31 41 26 53 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 58
size = 5 [31 41 26 53 58 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 58
size = 4 [31 41 26 53 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 53
size = 3 [31 41 26 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 26
size = 2 [31 41 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 97
size = 3 [31 41 97 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 97
size = 2 [31 41 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 41
size = 1 [31 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 31
size = 0 []
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 0
% ./a.out
スタックを実現するプログラム
  正の整数値:入力値をスタックに格納する(Push)
  負の整数値:スタックからデータを取り出す(Pop)
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 1
size = 1 [1 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 2
size = 2 [1 2 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 3
size = 3 [1 2 3 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 4
size = 4 [1 2 3 4 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 5
size = 5 [1 2 3 4 5 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 6
エラー(スタックが満杯です)
size = 5 [1 2 3 4 5 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 5
size = 4 [1 2 3 4 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 4
size = 3 [1 2 3 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 3
size = 2 [1 2 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 2
size = 1 [1 ]
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
取出データ = 1
size = 0 []
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): -1
エラー(スタックが空です)
size = 0 []
数値を入力 (正:格納(Push),負:取出(Pop),0:終了): 0
%

Extra問題

演習問題4

バス停で列を作ってバスを待つ場合、一番最初に並んだ人が最初にバスに乗り、 一番最後に並んだ人は最後にバスに乗ることになる。 このように最初に入れたデータを最初に取り出す(先入れ、先出し、First In, First Out: FIFO)形式のデータ構造は キュー(queue)と呼ばれ、スタック(stack)と共にコンピュータの基本的なデータ構造の1つである。 以下にキューに関する基本的な用語を記す。 プログラミング入門ハンドアウトの第13回(p70-)も参照のこと。
以下の仕様を満たす関数 queueIO を用いてキューを実現するプログラムを作成する。 関数の中身を埋めて、プログラムを完成させなさい。
「関数 queueIO の仕様」
(提出ファイル名: prog04.c)
キューの実装にあたってはプログラミング入門ハンドアウトの第13回(p70-)を参考にすること。ただし、そこで示されている例は外部変数を使い、エンキュー(格納)とデキュー(取り出し)は それぞれ enqueue, dequeue という 2 つの関数で行っている。 今回の問題では静的変数を用いて関数内に全ての情報を格納し、エンキュー(格納)とデキュー(取り出し)そして表示も同じ関数を用いている。 また、仕様の説明で触れた通り、先頭の位置、末尾の位置に加えてデータ数も変数で管理することによって、先頭位置と末尾位置が一致する場合でもキューが空なのか満杯なのかを区別できるようになり、配列の全ての要素にデータを格納できる。
#include <stdio.h>
#define N 5
#define EMPTY -1
#define FULL  -2

int queueIO(int);

int main()
{
  int n, r;

  printf("キューを実現するプログラム\n");
  printf("  正の整数値:入力値をキューに格納する(Enqueue)\n");
  printf("  負の整数値:キューからデータを取り出す(Dequeue)\n");
  printf("  0:終了\n");
  while (1) {
    printf("整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): ");
    scanf("%d", &n);
    if (n == 0) break; /* 終了 */
    r = queueIO(n); /* 格納または取出 */
    if (r > 0) printf("取出データ: %d\n", r); /* 取得データの表示 */
    else if (r == EMPTY) printf("エラー(キューが空です)\n"); 
    else if (r == FULL ) printf("エラー(キューが満杯です)\n"); 
    queueIO(0); /* 表示 戻り値(0)は使用しない */
  }
  return 0;
}

/* 
[引数]
正の整数: 格納,負の整数: 取出,0: 表示
[戻り値]
格納の場合) キューが満杯: マクロ定数 FULL,それ以外: 0
取出の場合) キューが空: マクロ定数 EMPTY,それ以外: 取り出した値
表示の場合) 0
*/
int queueIO(int x)
{
  static int queue[N]; /* データを格納する配列 */
  static int head = 0, tail = 0, size = 0; /* 先頭,末尾,データ数 */
  /* 

    ここを作成

  */
}
実行例
% ./a.out
キューを実現するプログラム
  正の整数値:入力値をキューに格納する(Enqueue)
  負の整数値:キューからデータを取り出す(Dequeue)
  0:終了
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 31
head = 0, tail = 1, size = 1 [31 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 41
head = 0, tail = 2, size = 2 [31 41 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 59
head = 0, tail = 3, size = 3 [31 41 59 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 31
head = 1, tail = 3, size = 2 [41 59 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 26
head = 1, tail = 4, size = 3 [41 59 26 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 53
head = 1, tail = 0, size = 4 [41 59 26 53 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 58
head = 1, tail = 1, size = 5 [41 59 26 53 58 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 41
head = 2, tail = 1, size = 4 [59 26 53 58 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 59
head = 3, tail = 1, size = 3 [26 53 58 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 26
head = 4, tail = 1, size = 2 [53 58 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 97
head = 4, tail = 2, size = 3 [53 58 97 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 53
head = 0, tail = 2, size = 2 [58 97 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 58
head = 1, tail = 2, size = 1 [97 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 97
head = 2, tail = 2, size = 0 []
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 0
% ./a.out
キューを実現するプログラム
  正の整数値:入力値をキューに格納する(Enqueue)
  負の整数値:キューからデータを取り出す(Dequeue)
  0:終了
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 1
head = 0, tail = 1, size = 1 [1 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 2
head = 0, tail = 2, size = 2 [1 2 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 3
head = 0, tail = 3, size = 3 [1 2 3 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 4
head = 0, tail = 4, size = 4 [1 2 3 4 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 5
head = 0, tail = 0, size = 5 [1 2 3 4 5 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 6
エラー(キューが満杯です)
head = 0, tail = 0, size = 5 [1 2 3 4 5 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 1
head = 1, tail = 0, size = 4 [2 3 4 5 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 2
head = 2, tail = 0, size = 3 [3 4 5 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 3
head = 3, tail = 0, size = 2 [4 5 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 4
head = 4, tail = 0, size = 1 [5 ]
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
取出データ = 5
head = 0, tail = 0, size = 0 []
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): -1
エラー(キューが空です)
head = 0, tail = 0, size = 0 []
整数値を入力 (正:格納 (Enqueue),負:取出 (Dequeue),0:終了): 0
%

演習問題5

外部変数を用いて、配列を操作するプログラムを書く。
(提出ファイル名: prog05.c)

会津大での評点(100点満点)と評価(A,B,C,D,F)並びに Grade Point(GP, 0-4点)は以下のように対応している。

 しかし、今回の試験の点数は配点の都合により110点満点になったとする。 最終的な評点は100点満点にする必要があるので、そのための補正を行う。 つまり、素点に対して100点/110点を掛ける操作を行う。この計算では小数点以下の値が生じるので、小数点第1位で四捨五入することとし、この補正処理のためにcalib関数を作成、使用する。
 また、実行例のように、統計データとして、補正前後の平均点や評価分布も求め、 表示するものとする。

プログラムには以下の外部変数を用いる。
#define MAX_ST_NUM 50                   /* 学生数の最大値 */
#define MAX_SCORE 110                   /* 110点満点 */
int student_id[MAX_ST_NUM];             /* 学籍番号  */
int abs_score[MAX_ST_NUM];              /* 点数 (補正前) */
char abs_grade[MAX_ST_NUM];             /* 補正前の評価 (A - F) */
int rel_score[MAX_ST_NUM];              /* 点数 (補正後) */
char rel_grade[MAX_ST_NUM];             /* 補正後の評価 (A - F) */
int grade_dist[2][5];                   /* 評価の分布 [0]:補正前, [1] 補正後 */
                                        /* 例 grade_dist[0][0] 補正前の Fの人数 */
char cgrade[5]={'F','D','C','B','A'};   /* 評価(A - F) */
int num_student;                        /* 実際に成績を読み込んだ学生の数 */
double abs_ave=0.0;                     /* 平均点(補正前) */
double rel_ave=0.0;                     /* 平均点(補正後) */
main 関数および その他の関数のおおまかな構成と動作を以下に示す。
また、第1回演習問題3の関数 scoreToGrade を、 (必要なら gradeToPoint も、一部修正した上で)使用しても構わない。
int calib(int); /* 満点の補正を行う。内部では実数で計算し、小数点第1位で四捨五入した整数を返す */

int main()
{
  /* 標準入力から学籍番号と点数を読みこむコードをここに書く。 */

  adjust_score();  /* 点数を補正し、補正後の点数の配列に書きこむ。*/
  		          /* 補正前・補正後の各学生の評価(A~F)を決定し、それぞれ
                     平均点と評価分布を求める*/
  print_grade();   /* 補正前と後の点数と評価を表示。実行例参照 */
  print_stat();    /* 統計と評価分布を表示。フォーマットは実行例参照 */
  return 0;
}

実行例

サンプル入力ファイル(score_data.txt)はこちら
% ./a.out < score_data.txt
 ID   点数 評価 点数 評価
      (補正前)  (補正後)
-------------------------
 10001    67 B    61 C
 10002   110 A   100 A
...
(略)
 10016    64 C    58 C
-------------------------
統計
学生数 16人 補正前平均点 57.9点 補正後平均点 52.8点
評価分布
補正前 A 3 B 3 C 3 D 4 F 3 
補正後 A 2 B 3 C 4 D 2 F 5 
%

演習問題6

問題5について、入力データをリダイレクションを使わず、直接ファイル score_data.txtを開いて読み込み処理するプログラムに変更せよ。 さらに、評価分布表示の後に、キーボード入力で指定した補正後の評価 の該当者一覧を表示するように変更せよ。 その際、補正後の評価に対して、引数で指定した評価 (A, B, ..., F)に該当するデータを表示する関数 void grade_search(char) を追加し、使用すること。 評価該当者一覧表示のためのキーボード入力は、繰り返し入力できるようにしておき、 EOF(Ctrl+D, ^D)が入力された場合、プログラムを終了するものとする(この時、評価指定の入力の後に打鍵するリターンは読み飛ばすようにしておく必要がある。この処理を入れないと、繰り返し入力時に支障が出る)。
ヒント:次回のハンドアウトを参照してください。
(提出ファイル名: prog06.c)

実行例 (評価分布以降、それ以前は前問と同じ)

% ./a.out
(略)
評価分布
補正前 A 3 B 3 C 3 D 4 F 3 
補正後 A 2 B 3 C 4 D 2 F 5 
表示したい評価を大文字で入れてください(A-D,F): A
Aの一覧
 10002  100 A
 10014   84 A
表示したい評価を大文字で入れてください(A-D,F): C
Cの一覧
 10001   61 C
 10005   57 C
 10011   53 C
 10016   58 C
表示したい評価を大文字で入れてください(A-D,F): ^D
%