講義第11回:C言語 の基礎 --- 配列

今日は、たくさんのデータをまとめて扱うための道具である配列と、それに対 するいくつかの処理について学ぶ。

配列

/* calculate_sum
 *
 * (C) J. Makino Version 1.0 Jan 26, 1997
 */
#include <stdio.h>

int  n ;
int  data[100];
int  i,sum;

void main()
{
    printf("How many numbers you want to input?");
    scanf("%d",&n);
    for (i=0;i<n;i++) scanf("%d",&data[i]);
    sum = 0;
    for (i=0;i<n;i++){
        sum += data[i];
        printf("data[%2d]=%5d, sum = %d\n", i,data[i],  sum);
    }
}
このプログラムでは、配列というものを使う。

int  data[100] ;
と変数宣言のところに書くと、100個の整数型変数ができ、それぞれが data[0] から data[99] までといった風に、番号で指定して操作 できる。この番号のことを、「専門用語」で「添字」(index)という。

配列の便利なところは、まず、変数宣言で int data0,data1,data2, .... data99 と書くよりずっと短くて済む(100万個変数が欲しかったら 大きな違いである)ということ、それから実行部も簡潔な繰り返しで書けると いうことである。特に大事なのは、番号(添字)に変数が使えるということで ある。このことのために、上のように、ループの中で配列の要素を順番に見る とか値をいれるとかいったことができる。

実行例

How many numbers you want to input?5
1
2
3
4
5
data[ 0]=    1, sum = 1
data[ 1]=    2, sum = 3
data[ 2]=    3, sum = 6
data[ 3]=    4, sum = 10
data[ 4]=    5, sum = 15

ソート

上の例では、配列を使うとどんないいことがあるかというのがそんなによくは わからないと思う。これまでの講義内容がわかっていれば、配列を使わなくて もおおむね同じ実行結果を得ることはできるはずである。もう少し、配列を使 わないとできないようなことをしてみよう。

/* sort_data_from_file
 *
 * (C) J. Makino Version 1.0 Jan 26, 1997
 */
#include <stdio.h>

int n;
double data[100];
int   i,j;
double  work;

void main()
{
    for (i=0;scanf("%lf",&data[i])==1;i++);
    n =i;
    printf("Input data\n");
    for (i=0;i<n;i++) printf(" %5.2f",data[i]); printf("\n");
    for (i=0;i<n-1;i++){
        for (j=i+1;j<n;j++){
	    if (data[i] > data[j]){
                work = data[i];
                data[i] = data[j];
                data[j]= work;
	    }
	}
    }
    printf("Sorted data\n");
    for (i=0;i<n;i++) printf(" %5.2f",data[i]); printf("\n");
}
このプログラムは、入力した数を小さい順に並べ変えて出力する。並べ変える 原理は、以下のようなものである。
  1. まず、一番小さい数を先頭に持ってくる。そのために、配列の2番 目の要素から最後の要素までを順に見て、それが先頭の値よりも小さければ2 つを入れ換える。

  2. これで先頭にもっとも小さい数が来た。次に、2番目の位置に残りの 数の中で最小のものを持ってくる。これには、配列の3番 目の要素から最後の要素までを順に見て、それが2番目の値よりも小さければ2 つを入れ換える。

  3. 以下、同様に、 N-1 番目まで順にやっていく。
このプログラムを実行するには、まずデータファイルを別にエディタで作って おく。これは一行に一つ数字を書いておけばいい。このデータファイルの名前 を sample.dat、コンパイルしてできた実行ファイルの名前を simple_sort とすれば、

simple_sort < sample.dat
とシェルウインドウで入力すれば結果が得られるはずである。

注意 コンパイルして作るプログラムの名前には sort は使わな いこと。これは、この名前の別のプログラムがあらかじめ準備されていて、そ ちらが実行されてしまうからである。

なお、

    for (i=0;scanf("%lf",&data[i])==1;i++);
という繰り返しは「実行部」が";"だけで何もない。これは、 scanf という関 数が「データがうまく読めればその読んだデータの数を返す」関数であるとい うことを利用した、 C 言語では良く使う書き方である。既にやったように、 for は
for ([0]初期化文; [1]条件; [3]文){[2]実行文 ; ...}
という形をしていて、0-1-2-3-1-2-3... の順に繰り返し実行される。ここで、「条件」のところに関数が入った式があれば、その関数が実行されて、返ってきた値が使われる。

練習

  1. 最初の、合計を求めるプログラムを配列を使わないで書き直して みよ。また、合計だけでなく平均と標準偏差をもとめるように拡 張せよ。配列を使わないで標準偏差を求めることはできるだろう か。

  2. ソートのプログラムを手続きを使う形に書き直してみよ。メ インプログラムは手続きを呼 ぶだけの形になるようにすること。

  3. ソートのプログラムについて、データの数を非常に大きくした時、実行時間がどうなるかを測定せよ。配列宣言の数字を大き くするのを忘れないこと。また理論上どうなるはずか考えてみよ。

  4. (応用)適当な参考書または教科書を見て、クイックソート、マージソート、またはヒープソートのプログラムを作り、上でやったソートプログラムとの実行時間の違いについて考察せよ。
なお、あるプログラムの実行時間を計るには time コマンドが使える。

time simple_sort < sample.dat
という風に入力すると、実行結果のあとにもう一行
0.040u 0.080s 0:00.56 21.4% 0+135k 2+0io 21pf+0w
というような出力が出てくる。この最初の数字が秒単位の実行時間である。な お、実際にかかった時間はもっと長いことがあり得る。これは、ファイルの読 み書きの時間は正確には入らないこと、また何人かで1台の計算機を使ってい るのでその分余計に時間がかかることによる。

再帰

再帰とは、手続きが「自分自身を呼び出す」ことである。これは、プログラミ ングの理論的な扱いという観点からも、また、実用上からも非常に重要な考え 方である。簡単な例から考えてみよう。

もっとも簡単な例

/* factrial
 *
 * (C) J. Makino Version 1.0 Jan 26, 1997
 */
#include <stdio.h>
double factrial(double n)
{
    if (n <=1.0){
	return 1.0;
    }else{
	return  n*factrial(n-1);
    }
}

void main()
{
    double n;
    printf("Enter n : ");
    scanf("%lf",&n);
    printf("N = %5.0f  N! = %20.13g\n", n,factrial(n) );
}

上の例 は、階乗を計算するプログラムである。階乗は、下のように定義される:

CやPascalのような「近代的」プログラミング言語では、この定義に従って素直にプログ ラムを書くことができる(これに対して、古いBASICなどではできない)。上の例では、まさに、 n が 1 なら 1 を返 し、それ以外では n*factrial(n-1)を返すというのが、 factrial(n)の中身になっている。 例えば factrial(5) を計算させるとすると、実際の計算は、以下のよ うな手順で進む。

factrial(5)
→5*factrial(4)
→5*(4*factrial(3))
→5*(4*(3*factrial(2)))
→5*(4*(3*(2*factrial(1))))
→5*(4*(3*(2*1)))
→5*(4*(3*2))
→5*(4*6)
→5*24
→120

練習

  1. 階乗を計算する関数を、再帰を使わない形に書き直してみよ。
  2. ある程度大きな数の階乗は正しく計算できない。計算できるのはい くつまでか。またそれより大きな数を入れた時にでる答えはどういう意味があ るか。
  3. フィボナッチ数列$F(n) = F(n-1) + F(n-2), F(0)=0, F(1)=1を再 帰、再帰でないものの両方を使って書け。
  4. 上のフィボナッチ数列について、n を大きくしていった時に計算の手間が どのように増えるかを、再帰の場合とそうでない場合のそれぞれについ て考察せよ。

お絵書き

再帰を使って複雑な図形を書いてみよう。
/* tree
 *
 * (C) J. Makino Version 1.0 Jan 26, 1997
 */
#include <stdio.h>
#include <math.h>
#include </home/makino/text/xgraph.h>
#define PI  3.14159265358979
int gdriver, gmode;
double    x,y,vx,vy;

void tree(int n, int x, int y, double angle, double length)
{
    int x1, y1;
    if (n > 0){
	setcolor( n % 7 + 1);
	x1 = x + length*cos(angle);
	y1 = y - length*sin(angle);
	line(x,y,x1,y1);
	tree(n-1,x1,y1,angle + 0.25, length*0.7);
	tree(n-1,x1,y1,angle - 0.25, length*0.7);
    }

}

void main()
{
    char dumstr[100];
    initgraph(gdriver, gmode, "");
    cleardevice();
    tree(10, getmaxx()/2, getmaxy(), PI/2, getmaxy()/ 4);
  gets(dumstr); 
  gets(dumstr); 
  closegraph();
}
これは、樹形図を書くプログラムである。原理は、指定した長さと角度で一本 線を引き、その先から角度をつけて2本線を引き、それぞれの端からまた角度 をつけて、、、というのを繰り返すだけである。

再帰を使うことで、角度、長さを変えて自分自身を呼び出すという形で簡潔に アルゴリズムが表現できている。

なお、このプログラムを打ち込んで、そのままコンパイルしようとしてもうま くできない。これは、三角関数(sin, cos) がどこにあるか指定されてい ないからである。これは以下のように, -lXtc -lX11 にさらに追加して指定する。

gcc tree.c -o tree -lXtc -lX11 -lm

練習

  1. この木では、枝が2本ずつ出ているが、もっとたくさん出すにはどうすればい いだろうか?
  2. この木は枝分かれが完全に規則的で不自然である。自然にするにはどうすれ ばいいだろうか?
    (ヒント:drand48() という関数を呼ぶと、0 から x まで の範囲の乱数 --- デタラメな数 --- を返す。これを使って枝分かれの長さ、 角度を変えて見よう)
  3. 以下に示すようなフラクタル図形も、うえのプログラムと同じよう な考え方で作ることができる。どのようにすればいいかを考えて、 プログラムを作ってみよ。

レポート

前回と今回の「練習」の中から、2つ以上を選んで解き、 をまとめて提出すること。ただし、前回の2番は、それだけでは1問と認めな い。なお、提出は、メイルで
makino-1@chianti.c.u-tokyo.ac.jp
あてに、 Subject を kadai-3 として提出すること。グラフィックスの出力結 果は、以下のようにして提出すること。
  1. 出力結果が画面に出たところで、 ~makino/bin/gfile というコマンド を実行する。これは、実行したウインドウとはべつのところで行 なうこと。
  2. Enter file name: と聞いてくるので、名前をいれる。これは別にどん な名前でもいいが、例えば課題3の1 枚目ならkadai3-1 というように つけるのがいい。
  3. しばらくすると、上の場合なら kadai3-1.uu というファイルができる ので、この中身をメイルにして送る。ただし、上に書いたように、こ れとプログラム、考察はまとめて1つのメイルで送ること。絵が何枚か ある場合も同じで、全体を1つのメイルでおくること。このためには mule の Insert-file (Ctrl-x i) などの機能を使う。
なお、今回のレポートは適当な時期に WWW 上で公開するので、公開さ れても構わないような内容にして下さい。締切は試験日の前日ということにします。

次回予告

次回は、全体のまとめとする。内容は未定。