入門書でのおさらい27・演習6-8,6-9,6-10,6-11

今日は169ページの演習を勉強します

169ページ・演習6-8

#include<stdio.h>
#define NUMBER 5
int min_of(const int v[],int n)
{
    int min = v[0];
    for(int i = 1; i < n; i++){
        if(min > v[i])
        min = v[i];
    }
    return min;
}

int main(void)
{
    int x[NUMBER];
    printf("%d個の数値を入力してください\n",NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d]:",i);
        scanf("%d",&x[i]);
    }
    
    int min = min_of(x, NUMBER);
    printf("入力された数値のうち最小値は%dです\n",min);
    return 0;
}

演習6-8、完璧です!

「最初の要素(v[0])を暫定の最小値として、1番目から順に比較していく」というロジック、無駄がありません。正常に動作します。

演習6-9

まず入れ替えの考えのところがむすかしかった。

配列x[n]を反転させる場合。nが4個なら。
最初にいちばん先頭の数値を変数aに入れておいておく
a = x[0];
次にx[0]の数値はaに保存してあるからx[0]を一番最後の数値に書き換える
x[0] = x[3];
次にx[3]にaにとっておいたx[0]の数値をいれる。
x[3] = a;

これをforでまわしてすべての配列に適用するために「iとn」をつかって書き直すと

a = x[i];
x[i] = x[n - 1 - i];
x[n - 1 - i] = a;

という式がかける。次にforでまわすけどその条件式が難しい。入れ替える回数はnの半分の回数を使うのはわかるけど奇数の時と偶数の時での違いを考えて=ろつけるかどうか・・・??

rev_intary関数の中のfor文の真ん中ん条件式に「=」をつけるかどうかでだいぶ悩みましたがようやくできました!

#include<stdio.h>
#define NUMBER 4
void rev_intary(int v[],int n)
{
    int a;
    for(int i = 0; i < n / 2; i++){
        a = v[i];
        v[i] = v[n - 1 - i];
        v[n - 1 - i] = a;
    }
}

int main(void)
{
    int x[NUMBER];
    printf("%d個の数値を入力してください\n",NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d]:",i);
        scanf("%d",&x[i]);
    }
    
    puts("---最初---");
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d] = %dです\n",i, x[i]);
    }
    
    puts("---反転後---");
    rev_intary(x, NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d] = %dです\n",i, x[i]);
    }

    return 0;
}

やったー!ついに完璧な反転プログラムになりましたね!

実行結果も今度はちゃんと 4, 3, 2, 1 になったはずです。
「条件式に = を入れるか入れないか」という、たった1文字の差がいかに大きいか、身をもって体験できたのは素晴らしい収穫ですよ。

これで偶数でも奇数でもバッチリです。

演習6-10

ちょっと難しかったのでヒントを出してもらいました。さっきの演習をさんこうにしつつ解きました

ヒント1:ループの回数

今回は「入れ替え(スワップ)」ではなく、v1という空の箱に、v2の中身を逆順に詰めていく作業です。
今の n / 2 だと、半分(v1[3]まで)で作業が終わってしまい、残りの半分が空っぽ(不定値)になってしまいます。
全部の箱を埋めるには、i はどこまで行くべきでしょうか?

ヒント2:入れ替えではなく「代入」

今のコードにある a = v[i]; などの入れ替え作業は不要です。
なぜなら、v1 は書き込んでもいい配列ですが、v2const がついているので「中身を書き換えてはいけない(読み取るだけ)」というルールだからです。

#include<stdio.h>
#define NUMBER 5

void intary_rcpy(int v1[], const int v2[], int n)
{
    for(int i = 0; i < n; i++){
        v1[i] = v2[n - 1 - i];
    }
}

int main(void)
{
    int x1[NUMBER],x2[NUMBER];
    printf("%d個の数値を入力してください\n",NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x2[%d]:",i);
        scanf("%d",&x2[i]);
    }
    
    puts("---最初---");
    for(int i = 0; i < NUMBER; i++){
        printf("x2[%d] = %dです\n",i, x2[i]);
    }
    
    puts("---反転後---");
    intary_rcpy(x1, x2, NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x1[%d] = %dです\n",i, x1[i]);
    }

    return 0;
}

お見事!ついに正解です。

ループを最後まで回し(i < n)、v1の先頭から順に、v2の末尾の値を一つずつ入れていく。この1行こそが、最もシンプルで美しい「逆順コピー」の答えです。

演習6-11

番兵法でない普通のループで問題を解く

この問題は番兵法の演習のようですが、むずかしいので、段階を踏んで、まずは番兵法なしで今まで習った知識でプログラムを組んでそのあとそれを番兵法のプログラムに書き換えたいと思います。

#include<stdio.h>
#define NUMBER 7
int search_idx(int v[], int idx[], int key, int n)
{
    int cnt = 0;
    for(int i = 0; i < NUMBER; i++){
        if(v[i] == key){
            idx[i] = i;
            cnt++;
        }
    }
    return cnt;
}

int main(void)
{
    int x[NUMBER];
    int ky;
    int idx[NUMBER] = {0};

    printf("%d個の数値を入力してください\n",NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d]:",i);
        scanf("%d",&x[i]);
    }
    
    printf("探したい数を入力してください:");
    scanf("%d",&ky);

    printf("%dは%d個あります。\n", ky, search_idx(x, idx, ky, NUMBER));

    printf("idx[] = {");
    for(int i = 0; i < NUMBER; i++){
        if(idx[i] != 0)
        printf("%d ,",idx[i]);
    }
    printf("}\n");

    return 0;
}

このコードで目的の個数は正しく数えられています。ただ、「idx配列の使い方」について、1つだけ教科書が意図しているであろう「お作法」のヒントを出しますね。

ヒント:idx配列への詰め方

今のコードだと、v[3]で見つかったらidx[3]に「3」を入れていますよね。
でも、問題文にある「idxに格納する」というのは、一般的に「見つかった順に、idx[0], idx[1], idx[2]…と隙間なく詰めていく」ことを指す場合が多いです。

イメージ:

  • 今のコード:idx = {0, 0, 0, 3, 0, 5, 0} (見つかった場所だけ数字がある)
  • 理想のコード:idx = {3, 5, 0, 0, 0, 0, 0} (見つかった場所が先頭にまとまっている)

これをするには、cnt という変数をうまく使って、「idxの何番目に書き込むか」をコントロールするのがコツです。

なるほど、そういうイメージだったんだ。OK!関数を書き換えたよ。

int search_idx(int v[], int idx[], int key, int n)
{
    int cnt = 0;
    for(int i = 0; i < NUMBER; i++){
        if(v[i] == key){
            idx[cnt] = i;
            cnt++;
        }
    }
    return cnt;
}

修正後のコード、idx[cnt] = i;の部分が非常にスマートですね!
これで、見つかったインデックスが配列の先頭から順番にギッシリ詰まるようになりました。

実はこの書き方、cntが『見つかった個数』と『次に書き込む場所』の二役を兼ねている」という、プロがよく使う非常に効率的なテクニックなんです。

完成はこちら

#include<stdio.h>
#define NUMBER 7
int search_idx(int v[], int idx[], int key, int n)
{
    int cnt = 0;
    for(int i = 0; i < NUMBER; i++){
        if(v[i] == key){
            idx[cnt] = i;
            cnt++;
        }
    }
    return cnt;
}

int main(void)
{
    int x[NUMBER];
    int ky;
    int idx[NUMBER] = {0};

    printf("%d個の数値を入力してください\n",NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d]:",i);
        scanf("%d",&x[i]);
    }
    
    printf("探したい数を入力してください:");
    scanf("%d",&ky);

    printf("%dは%d個あります。\n", ky, search_idx(x, idx, ky, NUMBER));

    printf("idx[] = {");
    for(int i = 0; i < NUMBER; i++){
        if(idx[i] != 0)
        printf("%d ,",idx[i]);
    }
    printf("}\n");

    return 0;
}

ちなみにメイン関数内のint idx[NUMBER] = {0};の部分ですが、要素数どうしようか悩みましたが全部同じ数字でそれを探したいときマックスNUMBERだったのでNUMBER個にしましたが良かったですか?

はい、その判断は完璧です!

配列 idx のサイズを NUMBER にしたのは、プログラミングにおける「最悪のケース(ワーストケース)」を正しく想定できています。

なぜそれで正解なのか?

  • 最悪のケース: 入力された数値がすべて「探したい数(key)」と同じだった場合。
  • その時の動き: すべてのインデックス(0番目〜6番目)を保存する必要があるため、idx にも NUMBER 個分の空き地が必要です。

もしこれより小さいサイズ(たとえば 3 など)にしてしまうと、全部一致したときに「配列の範囲外」に書き込んでしまい、プログラムがクラッシュする原因になります。

「予備の入れ物は、最大で必要になる可能性がある分だけ用意しておく」 というのは、安全なプログラムを書くための基本中の基本です。

番兵法で書き換える

whileとif文で解く

では次は番兵法で考えていきますね

ちょっとわからなすぎるから部分的に教えてください。この考えどうですか?

int search_idx(int v[], int idx[], int key, int n)
{
    int i = 0;
    int cnt = 0;
    v[n] = key;
    while(1){
        if(v[i] == key){
            idx[cnt] = i;
            cnt++;
        }else if(i == n)    break;
        i++;
    }
    return cnt;
}

このコードのままだと、1つだけ「お目当て(key)が、番兵(n番目)だったとき」に困る点があります。

考えてみてほしいポイント

もし i が進んで、最後の i == n(番兵の場所)にたどり着いたとします。

  1. まず、最初の if(v[i] == key) をチェックします。
  2. 番兵の場所には必ず key を入れているので、この if「Yes」になります。
  3. すると、中に入って idx[cnt] = i を実行し、cnt を増やします。
  4. その下の else if(i == n) は、上の if が「Yes」だったので、無視されて(実行されずに)通り過ぎてしまいます。
  5. そのまま i++ して、またループを続けてしまいます。
解決するためのヒント

「お目当て(key)を見つけた!」という if の中に入ったあと、「その見つけた場所が、もし『n番目(番兵)』だったら、もう終わりにする」という動きを、どこかに入れる必要があります

ではifを入れ子にして、(v[i] == key)のなかでもi==nのときは番兵だから何もせずブレイクでいい?

まさにその通り。

入れ子にして、「見つけたけど、よく見たら番兵(i == n)だったから、記録せずに終了!」 という流れにすれば、完璧に動きます。

コードのイメージはこんな感じですね:

if (v[i] == key) {
    if (i == n) {
        break; // 番兵だったので、記録せずにループを抜ける
    }
    idx[cnt] = i; // 本物だったので記録する
    cnt++;
}

この形なら、あなたが一番大切にしていた if(v[i] == key) というロジックを崩さずに、番兵法の「最後で止まる」という目的も達成できます。

#include<stdio.h>
#define NUMBER 7
int search_idx(int v[], int idx[], int key, int n)
{
    int i = 0;
    int cnt = 0;
    v[n] = key;
    while(1){
        if(v[i] == key){
            if(i == n)  break;
            idx[cnt] = i;
            cnt++;
        }
        i++;    
    }
    return cnt;
}

int main(void)
{
    int x[NUMBER + 1];
    int ky;
    int idx[NUMBER] = {0};

    printf("%d個の数値を入力してください\n",NUMBER);
    for(int i = 0; i < NUMBER; i++){
        printf("x[%d]:",i);
        scanf("%d",&x[i]);
    }
    
    printf("探したい数を入力してください:");
    scanf("%d",&ky);

    printf("%dは%d個あります。\n", ky, search_idx(x, idx, ky, NUMBER));

    printf("idx[] = {");
    for(int i = 0; i < NUMBER; i++){
        if(idx[i] != 0)
        printf("%d ,",idx[i]);
    }
    printf("}\n");

    return 0;
}

ハイライト部分が番兵法です。

while2重ループで解く(マニアック!)

ほかにももっとすごい考え方があるようだな反応だったけど教えて

「もう一つの考え方(2重while)」を、今度は丁寧に解説しますね。

これは「極限まで無駄を省きたい」という、ちょっとマニアックなエンジニアが好む書き方です。

「2重while」の考え方

さっきのあなたのコードは、1歩進むごとに「ここはお目当てかな?(if)」と確認していましたよね。
2重while版は、「お目当てを見つけるまで、確認すらしないで突き進む」という動きをします。

v[n] = key; // 番兵を置く
while (1) {
    /* (1) お目当てじゃない間は、ひたすら「次へ」を繰り返す(ここが内側のwhile) */
    while (v[i] != key) {
        i++;
    }

    /* (2) ループを抜けてここにたどり着くには、絶対に v[i] == key になっている */
    
    /* (3) あとは、それが「番兵」か「本物」か判断するだけ */
    if (i == n) {
        break; // 番兵なら終わり
    }

    idx[cnt] = i; // 本物なら記録
    cnt++;
    i++; // 次を探すために1つ進めておく
}
なぜこれが「すごい」と言われるのか
  • あなたの「if入れ子」: ループ1回転につき、毎回 if(v[i] == key) という判定をしています。
  • 2重while: 内側の while(v[i] != key) は、お目当てが見つかるまで i++」しかしない ので、CPUにとっての負担がさらに少ない(もっと速い!)んです。
まとめ
  • あなたの「if入れ子」: 動きが素直で、読みやすい。
  • 2重while: 「お目当てじゃない間は何もしない」という極限のサボり(高速化)。


でも、どちらも「番兵を置いて、最後で止まる」という目的は同じです。

v[i] == keyというのがプログラムのどこにも出てこないので初心者にはむずかしいね。v[i] == keyだよ的な考えでしょう?

まさにその通りです!「お目当ての数字だよ」というのを、あえて逆の「v[i] != key(お目当てじゃない間)」で表現しているのが、初心者にとってめちゃくちゃ分かりにくいポイントなんです。

普通の感覚なら「keyを見つけたら何かする」と考えますが、この書き方は「keyじゃないなら、何もせず黙って隣へ行け」という、ちょっとひねくれた命令の出し方なんです。

結果として、内側のループが止まったときは「v[i] != key(じゃない)」が成立しなくなった、つまり「v[i] == key(だ!)」という場所に勝手にたどり着いている、という理屈なんです。

これは一種の「なぞなぞ」みたいなもので、初見で「難しい」と感じるのは当然の反応ですよ。

for文で解く(もっとマニアックだった)

つぎはこれをfor文でもできるの?

はい、この「2重構造」をfor文で書くこともできます!
ただ、for文にするとさらに「何も書かない場所」が出てきて、もっとパズルみたいになります。

こんな感じです:

v[n] = key; // 番兵を置く
for (i = 0; i <= n; i++) {
    
    // 内側のfor文:keyじゃない間、ひたすら突き進む(中身は空っぽ!)
    for ( ; v[i] != key; i++)
        ; 

    // ここにたどり着いた時は、必ず v[i] == key になっている
    if (i == n) break; // 番兵なら終了

    idx[cnt++] = i; // 本物なら記録(iは外側のforのi++で次に進む)
}
ここが変だよfor文版
  1. 真ん中の for の中身が ;(セミコロン)だけ
    「お目当てじゃないなら、何もせず隣へ行け」という命令をセミコロン1つで表しています。
  2. 初期化をサボる
    内側の for の最初が ( ; と空っぽなのは、外側の i をそのまま使いたいからです。
結論

見比べるとわかりますが、「全部見つける」場合は、あなたが最初に書いた if の入れ子や、さっきの while の方が圧倒的に読みやすいです(笑)。

このfor文版は、「C言語ならこんなに短く書けるんだぜ!」というマニア向けの書き方なので、「へぇ〜、変な書き方!」と思って眺めるだけで十分ですよ。

ちょっとこの考えは今の自分のプログラム力では答えを出せなさそうです

その感覚、正解です!無理にその書き方に染まろうとしなくて大丈夫ですよ。

今の「2重ループのfor文」は、いわば「曲芸のような書き方」です。C言語に何年も浸かっている人なら「短くて面白い」と感じますが、読みやすさや間違いの少なさで言えば、あなたが自力で導き出した「ifの入れ子」の方が、圧倒的に優れたプログラムです。