2021 年度「計算数学a」 2021-10-08

§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)

#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) n++ は「変数 n の値を 1 だけ増せ」、 n-- は「変数 n の値を 1 だけ減らせ」の意味。

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

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

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

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

(2.1.6) くわしく説明する。 まず初期設定 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.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 double

(2.7.1) C 言語には小数点以下のある数を扱える型がいくつかある。 この授業では double 型のみを紹介する。

(2.7.2)

#include <stdio.h>

int main() {
  double x;

  scanf("%lf", &x);
  printf("%f\n", x);
}

double 型の入出力の際には上のように書く。 %lf(パーセント、エル、エフ)と %f との違いに注意せよ。 出力は小数点以下 6 桁となる。 20 桁ぐらいの大きな数や、 0.00000001 のような小さな数を入力してみよ。

  printf("%15.8f\n", x);

とすると「全体で少なくとも 15 桁の幅、小数点以下 8 桁」で表示される。

  printf("%15f\n", x);

なら「全体で少なくとも 15 桁の幅」。

  printf("%.8f\n", x);

なら「小数点以下 8 桁」。

(2.7.3) double 型には、 十進で言えば 1.234567×1089 のような形で値が格納されているので、 桁数が大きい整数も代入できる。 しかし、近似値になってしまうこともある。

(2.7.4) 自然対数の底 e の値を、e = 1 + 1/1! + 1/2! + 1/3! + ... として計算する。

#include <stdio.h>

int main() {
  int n;
  double x, sum;

  x = 1;
  sum = 1;
  for (n = 1; n <= 25; n++) {
    x = x/n;
    sum = sum + x;
    printf("%2d %.100f\n", n, sum);
  }
  printf("   2.718281828459045235360287471352... ← 数表\による値\n");
}

注:「数表\による値」と、 の後ろに \ がはいっている理由は、 (1.11.12) を見よ。

(2.7.5) 以下のプログラムは、コピペして動かし、 「こんなものか」と思えばそれでよい。

(2.7.6) 階乗を計算するプログラム。あるところから先は“無限大”になる。

#include <stdio.h>

int main() {
  int n;
  double factorial;

  factorial = 1;
  for (n = 1; n <= 200; n++) {
    factorial = factorial * n;
    printf("%2d %f\n", n, factorial);
  }
}

(2.7.7) int 型と double 型を混ぜて使う際の注意。 int 型を double 型に代入すると、(たぶん)そのまま格納される。 double 型を int 型に代入すると、(確か)小数点以下が切り捨てられる。

やや間違えやすいケースをプログラムで示す。

#include <stdio.h>

int main() {
  double x;

  x = 1/3;
  printf("%f\n", x);

  x = 1.0/3;
  printf("%f\n", x);

  x = 1;
  x = x/3;
  printf("%f\n", x);
}

これらは、C 言語のやや使いにくい点である。 (ほかにもまだまだいろいろな型があるので、 複雑な規則がある。)

(2.7.8) double 型は二進で数値を保存しており、 十進に変換して出力される。 十進の 0.1 は二進では 1/1010 なので循環小数となり、 十進の 1.333 のような近似値として扱われる。 (以下のプログラムでそのことがわかる。)

(2.7.9) (注意)二進の有限小数は十進でも有限小数である。

(2.7.10) 次のプログラムは、0.9 までが出力されるように思えるが、そうはならない。

#include <stdio.h>

int main() {
  double x;

  for (x = 0; x < 1; x = x + 0.1) {
    printf("%f\n", x);
  }
}

1 も出力される。それは、0.1 の十倍が、誤差のため、1 より少し小さい値になるからである。

(2.7.11) 以下のプログラムを動かしてみよ。 double 型の誤差がどのように出るか、わかるであろう。

もろもろの定数は、 ここのコマンドプロンプトを最大化して使うことを想定して調整してあるので、 ぜひ最大化して実行してみてほしい。

#include <stdio.h>

int main() {
  int n;
  double x;

  x = 1;
  for (n = 0; n < 230; n++) {
    printf("%f\n", x);
    x = x * 10;                         /* 次々と 10 倍する */
  }
}

(2.7.12)

#include <stdio.h>

int main() {
  int n;
  double x;

  x = 1;
  for (n = 0; n < 236; n++) {
    printf("%.234f\n", x);
    x = x / 10;                         /* 次々と 10 で割る */
  }
}

(2.7.13)

#include <stdio.h>

int main() {
  int n;
  double x;

  x = 1;
  for (n = 0; n < 761; n++) {
    printf("%236f\n", x);
    x = x * 2;                          /* 次々と 2 倍する */
  }
}

(2.7.14)

#include <stdio.h>

int main() {
  int n;
  double x;

  x = 1;
  for (n = 0; n < 780; n++) {
    printf("%.234f\n", x);
    x = x / 2;                          /* 次々と 2 で割る */
  }
}

(2.7.15)

  printf("%g\n", x);

とすると、1.2345×1067 のように表示される。 実際の出力は 1.2345e+067 となる。

(2.7.16)

#include <stdio.h>

int main() {
  int n;
  double x;

  x = 1;
  for (n = 0; n < 340; n++) {
    printf("%.207g\n", x);
    x = x / 10;                         /* 次々と 10 で割る */
  }
}

どこかから先は 0 になる。

§2.8 数学関数

(2.8.1) 妙な名前だと思うかもしれないが、 C コンパイラに備え付けの関数で数学に関連するものを「数学関数」と呼ぶ。 数学関数の一部をあげる。 K&R2 では §B4 を参照。 数学関数を使うには #include <math.h> が必要である。 つまり、プログラムの冒頭は次のようになる。

#include <stdio.h>
#include <math.h>

int main() {

(2.8.2) x, ydouble 型、 答えもすべて double 型である。 角度の単位はラジアンである。

(2.8.3) 三角関数表の一部を出力するプログラム。

#include <stdio.h>
#include <math.h>

int main() {
  double x;

  for (x = 0; x <= 90; x = x + 5) {
    printf("%9f %6f\n", x, sin(x / 180 * 3.14159265358979));
  }
}

§2.9 練習問題

(2.9.1) 未知数が二つの、連立一次方程式を解くプログラムを書け。 (ヒント:クラメールの公式。)

(2.9.2) 実数を係数とする二次方程式を解くプログラムを書け。 解が虚数の場合も扱えるようにできるとさらによい。 (よくあるミスを見抜くために、 2x2 - 3x + 1 = 0 も解かせてみよ。)


岩瀬順一