入門書でのおさらい20・演習5-6,5-7,5-8

今日は129ページ演習5-6から始めたいと思います。

129ページ・演習5-6

うーんList5-12を参考に作ってみたけど{1, 2, 3, 4}の表示の部分がちょっと納得いかない・・。もっとスマートに書けそうなんだよな

#include<stdio.h>
#define DATA 20
int main(void){
    int n;
    int a[DATA];
    printf("データ数を入力してください:");
    scanf("%d",&n);

    printf("データを入力してください\n");
    for(int i = 0; i < n; i++){
        printf("データ%d:", i+1);
        scanf("%d",&a[i]);
    }
     printf("{");
    for(int i = 0; i < n; i++){
        if(i == n-1)    printf("%d",a[i]);
        else printf("%d, ",a[i]);
    }
    printf("}\n");

    return 0;
    
}

括弧の中に表示するやり方もっとスマートなやり方あるかな?

    printf("{");
    for(int i = 0; i < n; i++){
        printf("%d",a[i]);
        if(i < n-1)    printf(", ");
    }
    printf("}\n");

そっか!なるほど!if文で最後以外のときに「, 」(コンマ半角スペース)を入れる。という書き方にすればいいのか。これならすっきりだね!

#include<stdio.h>
#define DATA 20
int main(void){
    int n;
    int a[DATA];
    printf("データ数を入力してください:");
    scanf("%d",&n);

    printf("データを入力してください\n");
    for(int i = 0; i < n; i++){
        printf("データ%d:", i+1);
        scanf("%d",&a[i]);
    }
    printf("{");
    for(int i = 0; i < n; i++){
        printf("%d",a[i]);
        if(i < n-1)    printf(", ");
    }
    printf("}\n");

    return 0;
    
}

君の答えを参考に書き直ししました!

修正お疲れ様です!スッキリしましたね。

「最後以外は後ろに何かを付ける」という書き方は、C言語だけでなく他の言語でもよく使われる鉄板のパターンです。これなら、要素が何個になっても綺麗に {23, 74, 9, 835} と並びますね。

ちなみに、先ほど少し触れた「データ数に 0 を入れた場合」ですが、今のコードだと実行結果はどうなるか想像つきますか?

forだけ実行されない結果になったよ

正解です!i < 0 が最初から偽になるので、ループの中身は一度も実行されず、結果として {} だけが表示されたはずです。

実務レベルだと「0件のときは別のメッセージを出す」といった処理を加えることもありますが、まずはこの挙動を理解できていることが一番大事です。

0以下の数字が入れられた時の対策これでどう?

#include<stdio.h>
#define DATA 20
int main(void){
    int n;
    int a[DATA];
    printf("データ数を入力してください:");
    do{
        scanf("%d",&n);
        if(n <= 0){
            printf("0以外の正の整数を入力してください\n");
        }
    }while(n <= 0);

    printf("データを入力してください\n");
    for(int i = 0; i < n; i++){
        printf("データ%d:", i+1);
        scanf("%d",&a[i]);
    }
    printf("{");
    for(int i = 0; i < n; i++){
        printf("%d",a[i]);
        if(i < n-1)    printf(", ");
    }
    printf("}\n");

    return 0;
    
}

いいですね!do-while 文を使って、不正な値(0以下)が入力されたときのエラーチェック(バリデーション)が追加されました。これでプログラムの堅牢性が一気に上がりましたね。

ちなみに、今のコードだと n100 のような大きな値を入れると、用意した配列 DATA (20) を超えてしまいます。

do-while の条件を少し工夫して、配列のサイズ(20)を超えないように制限をかけるとしたら、どう書き換えますか?

書き換えますのでまってて。これlist5-12のように書かないといけなかったんだね

#include<stdio.h>
#define DATA 20
int main(void){
    int n;
    int a[DATA];
    printf("データ数を入力してください:");
    do{
        scanf("%d",&n);
        if(n < 1 || n > DATA){
            printf("\a1~%dでにゅうりょくせよ:",DATA);
        }
    }while(n < 1 || n > DATA);

    printf("データを入力してください\n");
    for(int i = 0; i < n; i++){
        printf("データ%d:", i+1);
        scanf("%d",&a[i]);
    }
    printf("{");
    for(int i = 0; i < n; i++){
        printf("%d",a[i]);
        if(i < n-1)    printf(", ");
    }
    printf("}\n");

    return 0;
    
}

whileの中も5-12に合わせて0以下を1未満に書き換えました

完璧ですね!n > DATA の条件を加えたことで、配列の範囲外にアクセスしてプログラムが壊れる心配がなくなりました。

特に、printf の中でマクロの DATA をそのまま使っているのが素晴らしいです。これなら、もし後から「やっぱりデータ数は50個まで扱いたい」となっても、一番上の #define を変えるだけでメッセージまで自動的に切り替わりますね。

これで演習5-6は完全にマスターしたと言えそうです。

演習5-7

これは簡単そうです。かえるだけなので

#include<stdio.h>
#define NUMBER 120
int main(void){
    int num;
    int tensu[NUMBER];
    int bunpu[11] = {0};
    printf("人数を入力せよ:");

    do{
        scanf("%d",&num);
        if(num < 1 || num > NUMBER){
            printf("1~%dで入力せよ:",NUMBER);
        }
    }while(num < 1 || num > NUMBER);
    printf("%d人の点数を入力せよ\n",num);

    for(int i = 0; i < num; i++){
        printf("%2d番:", i + 1);
        do{
            scanf("%d", &tensu[i]);
            if(tensu[i] < 0 || tensu[i] > 100){
                printf("0~100で入力せよ:");
            }
        }while(tensu[i] < 0 || tensu[i] > 100);
        bunpu[tensu[i] / 10]++;
    }

    puts("\n---分布グラフ---");

    for(int i = 0; i <= 9; i++){
        printf("%3d~%3d:", i * 10, i * 10 +9);
        for(int j = 0; j < bunpu[i]; j++){
            putchar('*');
        }
        putchar('\n');
    }
    printf("     100:");

    for(int j = 0; j < bunpu[10]; j++){
        putchar('*');
    }
    putchar('\n');

    return 0;
}

お見事です!正解ですね。

for ループの制御を i = 0; i <= 9; i++ に変更し、さらに単独で表示していた「100」の処理をループのに持ってくることで、指示通りの昇順(低い順)に完璧に書き換えられています。

特に、100 の行だけは他の行(0〜9など)と書式が異なるため、このようにループの外に出して最後に配置するのが非常にスマートな解決策ですね。

分布グラフの表示順を入れ替える感覚、掴めましたか?

演習5-8

やば、これ難しいね

この問題は「C言語入門の最初の壁」と言われることもあるほど、頭の使い方が面白い良問です。

横向きの時は「1行ずつ完成させていく」という考え方でしたが、縦向きにするには「一番上の段から順番に、各点数帯にアスタリスクがあるかないかを判定して描画していく」という視点が必要になりますね。

まずは、どのようなループ構造にすれば縦に積み上がって見えるか、じっくり設計図を練ってみてください。

やばいこれはわからん過ぎる。一つ目のヒントください

ついに来ましたね、この難問!これは本当に頭の体操になります。

「ノーヒント」の約束でしたが、リクエストをいただいたので最初の一つ目のヒントを出します。

ヒント1:視点を「横」から「縦」に切り替える

横向きのグラフは「1行にアスタリスクを何個出すか」を考えればよかったですが、縦向きは「1段(1行)に全点数帯の状況を一度に表示する」必要があります。

一番上の段から表示していくとして、こう考えてみてください。

  1. まず、全点数帯の中で「一番アスタリスクが多い数(最大値)」が何段目(高さ)になるかを探します。
  2. その「最大値」の段から、1段ずつ下に降りていきます。
  3. 各段において、左(0〜9点)から右(100点)へと順番に、「この段の高さに、アスタリスクを表示すべきか? それとも空白を表示すべきか?」を判定していきます。

「今、自分が描こうとしている『段(高さ)』よりも、その点数帯の人数(bunpu[i])が多ければ、アスタリスクを書く」というイメージです。

まずは、「どうやって一番高い段(最大値)を見つけるか」と、「高さ(段数)を制御する外側のループ」をどう作るか、考えてみてください。

まずは簡単な罫線と数字のとこだけやってしまおう

    for(int i = 0; i <= 10; i++){
        printf("---");
    }    
    putchar('\n');
    printf(" 0 ");
    for(int i = 1; i <= 10; i++){
        printf("%d ", i * 10);
    }

数字一つに「—」(半角ハイフン3つ)を使ってるので、0は一桁なので0の場合だけ—の真ん中に表示させてのそのほかの数字10~100(2から3桁)は—の左側に合わせるようにしました。

bunpuの最大値をまず求めたらいい?

その通りです!

一番高いところ(最大値)が分かれば、そこを「グラフの最上段」として、1段ずつ下に降りながら描画していけるようになります。

例えば、もし分布の最大値が 5人 だったら、グラフは 5段目 からスタートすることになりますよね。

まずは、bunpu 配列の中から最大値を見つけ出す処理から書いてみてください。

そこができたら、次はその最大値を基準に「どうやって上から下にループを回すか」というステップに進めます。

int max = bunpu[0];
    for(int i = 1; i <= 10; i++){
        if(max < bunpu[i]){
            max = bunpu[i];
        }
    }

まずmaxという変数を作り、そこにbunpu[0]をいれておく。bunpu[0]とあとのbunpu[1]~bunpu[10]をループで比較させて大きければmaxの値を書き換えて最大値を出すようにしました。

完璧です!これでグラフの「一番高い段」が分かりましたね。

次は、その max を使って「上から下へ、1段ずつ描画していく」メインのループに挑戦です。

ヒント2:二重ループの組み立て方

  • 外側のループmax 段目から 1 段目まで、1つずつ降りていきます(高さの制御)。
  • 内側のループ:左の列(bunpu[0])から右の列(bunpu[10])まで、1つずつチェックしていきます(横方向の制御)。

内側のループの中で、「今の段(高さ)にアスタリスクが必要かどうか」if 文で判定するのが最大の難所です。

ヒントはここまでにしておきますね。この max を使ったループ、まずは自力で組み立ててみてください!

なるほど表のようになってるから二重ループを使うんだね

すっげー途中だけどこの考えからでいいか見て

for(int i = 0; i < max; i++){
        for(int j = 0; j <= 10; j++){
            if(bunpu[j] == max - i){
                putchar('*');
            }
        }
    }

いい視点ですね!「上から下に降りていく」ために max - i を使って判定しようとしているのが分かります。

その方向で進める際に、あと一歩だけ考えてみてほしいポイントが2つあります。

  1. アスタリスクがない場所はどうするか?
    今のままだと、ifに当てはまった時だけアスタリスクを書きますが、当てはまらなかった場所(空白になるべき場所)は何もしないことになります。そうすると、グラフの形が崩れて左に寄ってしまいませんか?
  2. 「==」で判定して大丈夫か?
    例えば、ある点数帯に3人(bunpu[j] == 3)いるとき、3段目にはアスタリスクを書きますが、その下の2段目や1段目には書かなくていいのでしょうか?

この2点をふまえて、if文の条件と、elseのときの動きをイメージしてみてください。

かなり核心に近づいていますよ!修正したコード、または続きが書けたら教えてください。

うーんちょっと2の条件がわかりにくいから1から解決していきます

for(int i = 0; i < max; i++){
        for(int j = 0; j <= 10; j++){
            if(bunpu[j] == max - i){
                printf(" * ");
            }else{
                printf("   ");
            }
        }
        putchar('\n');
    }

アスタリスクは—の真ん中に配置したかったのでputcharだと一文字しか表示できないのでprintfにかえました。アスタリスクを打つ時は「半角スペース/*/半角スペース」何もない時は「半角スペー3つ」を表示。1はこれで解決。あとは2だね

その通り!1番の「空白」問題はバッチリ解決しましたね。

次は2番の「アスタリスクが積み上がる(下の段にも表示される)」ための条件です。

ヒントが必要になるまで、ノーヒント・ノー答えで待っています。
==」をどう変えれば、高い段から低い段までアスタリスクが続くようになるか、じっくり考えてみてください。

条件がわからないから2つ目の条件に対するヒントください

いいところに気づきましたね。

ヒント:アスタリスクは「地面(1段目)」まで繋がっている必要があります。

今の条件 bunpu[j] == max - i だと、「その高さが、ちょうど人数と一致した瞬間」だけしかアスタリスクが書かれません。

例えば、bunpu[j]3人 の場合を考えてみましょう。

  • max - i5段目 のとき:アスタリスクは必要?
  • max - i3段目 のとき:アスタリスクは必要?
  • max - i1段目 のとき:アスタリスクは必要?

「今の段の高さ」と「人数」を比べたとき、人数の方が多ければ(あるいは同じなら)アスタリスクを書く、という関係性が見えてきませんか?

「ちょうど同じ(==)」ではなく、どんな比較演算子を使えば「その高さ以下すべて」にアスタリスクを敷き詰められるか、試してみてください!

「今の段の高さ」と「人数」を比べたとき、人数の方が多ければ(あるいは同じなら)アスタリスクを書くという条件にするのか!

    for(int i = 0; i < max; i++){
        for(int j = 0; j <= 10; j++){
            if(bunpu[j] >= max - i){
                printf(" * ");
            }else{
                printf("   ");
            }
        }
        putchar('\n');
    3

完成したコードはこれです

#include<stdio.h>
#define NUMBER 120
int main(void){
    int num;
    int tensu[NUMBER];
    int bunpu[11] = {0};
    printf("人数を入力せよ:");

    do{
        scanf("%d",&num);
        if(num < 1 || num > NUMBER){
            printf("1~%dで入力せよ:",NUMBER);
        }
    }while(num < 1 || num > NUMBER);
    printf("%d人の点数を入力せよ\n",num);

    for(int i = 0; i < num; i++){
        printf("%2d番:", i + 1);
        do{
            scanf("%d", &tensu[i]);
            if(tensu[i] < 0 || tensu[i] > 100){
                printf("0~100で入力せよ:");
            }
        }while(tensu[i] < 0 || tensu[i] > 100);
        bunpu[tensu[i] / 10]++;
    }

    printf("\n        ---分布グラフ---\n");

    int max = bunpu[0];
    for(int i = 1; i <= 10; i++){
        if(max < bunpu[i]){
            max = bunpu[i];
        }
    }
    for(int i = 0; i < max; i++){
        for(int j = 0; j <= 10; j++){
            if(bunpu[j] >= max - i){
                printf(" * ");
            }else{
                printf("   ");
            }
        }
        putchar('\n');
    }
    
    
    for(int i = 0; i <= 10; i++){
        printf("---");
    }    
    putchar('\n');
    printf(" 0 ");
    for(int i = 1; i <= 10; i++){
        printf("%d ", i * 10);
    }

    return 0;
}

ついに完成しましたね!おめでとうございます!

if (bunpu[j] >= max - i) という条件、バッチリです。これで「その段の高さよりも人数が多い(または同じ)ならアスタリスクを置く」というロジックが完成し、見事に縦に積み上がったグラフが表示されるようになりました。

さらに、下部の境界線(---)や目盛りの表示も追加して、グラフとしての完成度が一段と増していますね。

実際に動かしてみて、分布が縦に伸びる様子を確認できた時の達成感は大きいのではないでしょうか。

この「2次元の空間をループで制御する」感覚は、この先の多次元配列や複雑なアルゴリズムでも必ず役に立ちます。