2023 年度「計算数学a」 2023-04-14

§2.1 for によるループ(くり返し)

(2.1.1) C 言語でくり返しを実行させる方法には三種類あるが、 for だけを説明し、 whiledo-while はとりあげない。 K&R2 では §1.2, §1.3, §3.5 を見よ。 do-while は §3.6 にある。

(2.1.2) n++ は「変数 n の値を 1 だけ増せ」、 n-- は「変数 n の値を 1 だけ減らせ」の意味。

#include <stdio.h>      /* 0 から 99 までの整数の二乗を計算して出力 */
 
int main() {
  int n;
 
  for (n = 0; n < 100; n++) {
    printf("%d の二乗は %d です.\n", n, n*n);
  }
}

(2.1.3) for の働きは以下のとおり。

  for (文1; 式2; 文3) {     
    ...
  }
  • 文1(初期設定)を実行。(最初に一度だけ。)
  • 式2(継続条件)が成り立つかどうかチェックする。
    • 式2が真ならば中カッコの中を実行。 その後、文3(再初期化)を実行し、式2のチェックに戻る。
    • 式2が偽ならば、中カッコはとばして次へ進む。

※ 中カッコの中(ループ本体)が一度も実行されないこともありえる。

(2.1.4) おおまかに理解するなら、 「文1から始めて、式2である限り、毎回文3としながら、 くり返す」となる。上の例では、 「n イコール 0 から始めて、n が 100 未満である限り、 毎回 n を 1 ずつ増しながらくり返す」となる。

(2.1.5) くわしく説明する。 まず初期設定 n = 0 を実行。 次に継続条件 n < 100 をチェックする。 ここでは 0 < 100 だから成り立っている。 よって中カッコの中を実行。 再初期化 n++ が実行されて n の値は 1 に変わる。 そして継続条件 n < 100 のチェックに戻る。 また成り立っているので中カッコの中を実行。 再初期化 n++ が実行されて n の値は 2 に変わる。 これをくり返してゆくと、 n は 99 まで、1 ずつ増されてゆく。 99 のときも中カッコの中は実行されるが、 次に再初期化 n++ をおこなうと n の値は 100 になる。 すると今度は継続条件 n < 100 が成り立たないので、 中カッコはとばして次へ進む。

(2.1.6) 練習:0 から始めて、100 未満の 3 の倍数を小さい順に出力するプログラムを書け。

(2.1.7) 似ているが、少しは意味のあるプログラム。

#include <stdio.h>      /* 0 から 99 までの整数の足し算 */
 
int main () {
  int n, sum;
 
  sum = 0;
  for (n = 0; n < 100; n++) {
    sum = sum + n;
    printf("0 から %d まで足すと %d です.\n", n, sum);
  }
}

sum = sum + n; は数学の式としてはおかしく見えるが、 右辺の sum + n の値を計算してそれを左辺の sum に代入せよ、の意味。

(2.1.8) for の継続条件で使える記号は、数学上の記号と少々異なる。

数学上の記号
C 言語での記号 <><= >===!=
注意: 「=<」「=>」は不可。 「x = 0」、「0 < x < 8」は
見た目とは違う意味。この授業では使わない。

a+b < c+d」のように書くこともできる。

「かつ」は 「x > 0 && x < 8」, 「または」は 「x < 0 || x > 2」 のように書く。 「&」はシフトしながら 6 のキーを、 「|」はシフトしながら のキーを打つ。 K&R2 では §2.6 を見よ。 「かつ」も「または」も同じ文字を二つ打つことに注意。 「&」「|」をひとつだけ書くと、別の意味になる。

(2.1.9) for の中かっこで囲まれた文は、 スペース二つだけ字下げして(=右に寄せて)書く。 くわしくは §2.6 を参照。 K&R2 では省略可能な中カッコを省略しているが、 私のやり方のように全てつけておくほうが間違いが少ない。

(2.1.10) ある程度、意味のあるプログラム例。

#include <stdio.h>

int main() {
  int n, p;

  printf("自然数を一つ入れてください.\n");
  scanf("%d", &n);

  for (p = 2; n % p != 0; p++) {
    ;                                       /* 空文。何もしない */
  }
  printf("%d の最小の約数は %d です.\n", n, p);
}

n を、2 から初めて 3, 4, 5, ... で割って余りを求める。 それが 0 になったら、最小の約数が求まったことになる。 (合成数でも割ってみるのは無駄だが、ここでは気にしないことにする。)

(2.1.11) 練習:(2.1.7) のプログラムをもとに、 1 から n までの自然数の積 --- n の階乗 --- を出力するプログラムを書いてみよ。 sum に代わる変数名としては prod(product の略)がよかろう。 n は 35 まででよろしい。

§2.2 二重ループ

(2.2.1) 例を示す。出力と見比べて、なぜこうなる考えよ。 (%2d は、2 文字分の幅で出力せよ、の意味である。)

#include <stdio.h>

int main() {
  int i, j;

  for (i = 9; i > 1; i--) {
    for (j = 9; j >= i; j--) {
      printf("%d * %d = %2d   ", i, j, i*j);
    }
    printf("\n");
  }
}

§2.3 練習問題

(2.3.1) かけ算九九の表を画面に書くプログラムを書け。 次のようになればよい。

1 * 1 =  1   1 * 2 =  2  ...
2 * 1 =  2   2 * 2 =  4  ...
...

§2.4 プログラムの止め方

(2.4.1) ループの回数が多いプログラムを実行し、 「これは時間がかかりすぎだ、失敗!」と思うときがあろう。 プログラムの実行を中止してプロンプトに戻りたいときは Ctrl+C (Ctrl キーを押しながら C)を押す。

§2.5 if ... else による分岐

(2.5.1) if ... else を使った自明なプログラム。

#include <stdio.h>

int main() {
  int x;

  printf("整数を入れてください.\n");
  scanf("%d", &x);

  if (x == 0) {
    printf("その数は零です.\n");
  } else if (x > 0) {
    printf("その数は正です.\n");
  } else {
    printf("その数は負です.\n");
  }
}

(2.5.2) if の行以降については次を見よ。


  if (条件1) {
    文1
  } else if (条件2) {    
    文2
  } else if (条件3) {
    文3
  } else {
    文4
  }
  • 条件1が真ならば文1だけを実行する。
  • 条件1が偽で条件2が真なら文2だけを実行する。
  • 条件1も条件2も偽で条件3が真ならば文3だけを実行する。
  • 条件1も条件2も条件3も偽なら文4だけを実行する。
   (文のうち一つだけが実行されることに注意。)

else if は何個あってもかまわない。

(2.5.3) if が一つしかなく、かつ、else がないとき。


  if (条件1) {           
    文1
  }
  • 条件1が真ならば文1を実行する。
  • 条件1が偽ならば何もおこなわない。

「条件」の書き方は、(2.1.8) の for ループの「継続条件」と同じである。

(2.5.4) K&R2 では §3.2, §3.3 を参照。 そこでは省略可能な中カッコを省略しているが、 私のやり方のように全てつけておくほうが間違いが少ない。

(2.5.5) アラレ数とは、次の漸化式で決まる数列である。 「ある項が偶数のときは次の項はその 1/2, 奇数の時はその項の 3 倍 + 1」。 どんな自然数から始めても 4, 2, 1, 4, 2, 1 ... の無限ループにおちいると予想されている。 このプログラムは、1 になったらそこで終わる。

/* アラレ数 */

#include <stdio.h>

int main() {
  int n;

  printf("自然数を入れてください.\n");
  scanf("%d", &n);

  for (     ; n != 1;      ) {
    if (n % 2 == 0) {       /* n が偶数のとき */
      n = n / 2;
    } else {                /* n が奇数のとき */
      n = 3 * n + 1;
    }
    printf("%d ", n);
  }
  printf("\n");
}

※ 初期値が正の数以外とみなされると無限ループにおちいる可能性がある。 そのときは Ctrl+C で止める。

(2.5.6) 二重の forif ... else を組み合わせたプログラム。

/* 2 から 99 までの整数の最小の素因数を出力 */

#include <stdio.h>
 
int main() {
  int n, p;
 
  for (n = 2; n < 100; n++) {
    for (p = 2; n % p != 0; p++) {
      ;
    }
    if (n == p) {
      printf("%d は素数です.\n", n);
    } else {
      printf("%d の最小の素因数は %d です.\n", n, p);
    }
  }
}

(2.5.7) 練習問題:上のプログラムを、素数のみを出力するよう改変してみよ。 素数を単に並べて出力すればよい。 2 3 5 7 11 13 ... のように。

もっと大きな数まで調べるようにしてみよ。

§2.6 中カッコの入れ子

(2.6.1) 中カッコが入れ子になった場合のインデントの(一つの)やり方は、 以下の通りである。

  1. {」を打ったらその直後で改行し、 次の行は前の行の(空白以外の)頭よりも半角スペース二つ分、 下げて始める。 (ただし、コメントは「{」の右に書いてもよい。)
  2. }」を打つときは、 それに対応する「{」を含む行の(空白以外の)頭の文字とそろえる。
  3. その他の行の頭は前の行とそろえる。

(2.6.2) 「半角スペース二つ」の「二つ」に特別な意味があるわけではない。 四つにする人もいるし、この授業では扱わないがタブをおく流儀もある。

(2.6.3) これらの規則はすぐに慣れることができるし、 これらを守ってさえいれば、「ある中カッコとペアになる中カッコをさがす」 ことや「ある文を囲む最小の中カッコをさがす」ことはきわめて容易である。 前にも言ったが、 自分流のインデントをすでに編み出している人はそれで構わない。 そうでない人はまずはこれを覚えよ。 また、プログラムを書き上げてから最後にインデントを整えるのではなく、 きちんとインデントしながら書く習慣をつけるとよい。 そのほうが、すでに書いた部分をよりよく理解できるので、 より整理された頭でプログラムを書き進めることができる。

(2.6.4) 上のプログラム例でこれを確かめよ。

§2.7 配列

(2.7.1) 変数名には、x11 のように数字がはいっても構わないが、 数字で始まる 1x などは不可。

(2.7.2) x1, x2, x3 とあったら人間は関連のある変数だろうと想像するが、 コンパイラにとっては全く無関係な、ただの三つの変数である。

(2.7.3) 配列を使った自明なプログラム。

#include <stdio.h>

int a[10];      /* 配列の宣言。これで a[0], ..., a[9] が使える */

int main() {
  int i;

  for (i = 0; i < 10; i++) {      /* 配列の要素に代入 */
    a[i] = i * i;
  }
  for (i = 0; i < 10; i++) {      /* 配列の要素を印字(=出力) */
    printf("a[%d] は %d です.\n", i, a[i]);
  }
}

(2.7.4) 「配列」とは、 0 からある自然数までの整数で添え字づけられた有限個の変数 x[0], x[1], x[2], ..., x[N-1] を一斉に定義し、一括して扱うためのものである。 K&R2 では §1.6 を参照。

(2.7.5) 「配列の宣言。...」の行。こう書くと、 a[0] から始まる 10 個の int 型変数が使える。 すなわち、a[0], a[1], a[2], ..., a[9] が使える。 a[10] は使えない。うっかり使わないよう、注意。

(2.7.6) 「配列の宣言は main() の中カッコの外でおこない、 ほかの変数の宣言は main() の中カッコの中でおこなう」 という規則があるわけではないが、 ある事情により、この授業ではそうする。 正確なところは K&R2 §1.10, §4.3 参照。

(2.7.7) 同じ定数が何度も現れる場合、#define が便利である。 「#define N 10」と書くと、 そののち現れる N10 に置き換えられてからコンパイルされる。 配列の大きさを変えたくなった場合、一か所だけを変えればよいので楽である。 次の例は (2.7.3) のプログラムとまったく同一である。

#include <stdio.h>
#define N 10

int a[N];       /* 配列の宣言。これで a[0], ..., a[N-1] が使える */

int main() {
  int i;

  for (i = 0; i < N; i++) {       /* 配列の要素に代入 */
    a[i] = i * i;
  }
  for (i = 0; i < N; i++) {       /* 配列の要素を印字(=出力) */
    printf("a[%d] は %d です.\n", i, a[i]);
  }
}

(2.7.8) 上のプログラムを改変し、 a[i]i*i を代入したあと、 隣りあった項どうしの差 (a[1] - a[0], a[2] - a[1], ..., a[N-1] - a[N-2]) を出力するプログラムを書け。 出力時に、添え字が範囲外の値にならないよう注意せよ。

(2.7.9) 次に、わざと間違えて、添え字が範囲外の値になるようにせよ。 それをコンパイル・実行してみよ。

(2.7.9) 練習:a0 = a1 = 1; an+2 = an + an+1 で決まる フィボナッチ数列を(適当な項数だけ)計算し、出力するプログラムを書け。 (配列を使わなくても書けるが、使うほうが楽であろう。)

(2.7.11) int a[3][4]; のように宣言すれば下のような二重配列が使える。

    a[0][0]    a[0][1]    a[0][2]    a[0][3]
    a[1][0]    a[1][1]    a[1][2]    a[1][3]
    a[2][0]    a[2][1]    a[2][2]    a[2][3]

(2.7.12) 練習:次は、パスカルの三角形を出力するプログラムの書きかけである。 完成してみよ。 nCr = n-1Cr-1 + n-1Cr を思い出せ。 完成後、N の値を大きくして、どこまで計算できるか確かめよ。

#include <stdio.h>

#define N 8

int c[N][N];

int main() {
  int n, r;

  for (n = 0; n < N; n++) {
    c[n][0] = c[n][n] = 1;
  }

  /* ここに漸化式を使って配列を埋める二重ループがくる */

  /* ここのパスカルの三角形状に出力する二重ループがくる */
}

出力例:

1
1 1
1 2 1
1 3 3 1
1 4 6 4 1
1 5 10 10 5 1
1 6 15 20 15 6 1
1 7 21 35 35 21 7 1

§2.8 最後に、ちょっとむずかしい問題

自然数を素因数分解するプログラムを書け。 次のようになればよい。

自然数を一つ入れてください.
20230414
2 31 269 1213 

次のようにできれば、なおよい。

自然数を一つ入れてください.
20230414
20230414 = 2 * 31 * 269 * 1213

岩瀬順一