C言語の基本|2次元配列と二重ループ

二重ループの向きを変えれば、表データの見え方も変わる。行と列を自在に集計しよう。

2次元配列は、行と列を持つ表形式のデータを管理するための仕組みです。複数の機械について数日分の生産数を記録したり、学生ごとの科目点数を保存したりするときに便利です。

ただし、2次元配列のすべての要素を処理するには、行と列の両方を順番に動かす必要があります。そこで使うのが、for文を2つ組み合わせた二重ループです。

外側のfor文で行を進め、内側のfor文で列を進めれば、表を横方向に処理できます。反対に、外側で列、内側で行を進めると、縦方向の集計ができます。

このループの向きの違いが分かると、次のような処理を自然に書けるようになります。

  • 行ごとの合計と平均を求める
  • 列ごとの合計と平均を求める
  • 2次元配列のすべての要素を表示する
  • 行と列の意味に合わせて集計方向を切り替える

ここでは、3台の機械について4日分の生産数を管理し、機械ごと、日ごとの合計と平均を求めるプログラムを通して、2次元配列と二重ループの関係を詳しく見ていきます。

2次元配列を表として考える

2次元配列は、行と列を持つ表として考えると分かりやすくなります。

今回扱う生産数は、次のようなデータです。

機械\日1日目2日目3日目4日目
機械124302733
機械220282531
機械326322935

この表は、次の2次元配列で表せます。

int production[3][4] = {
    {24, 30, 27, 33},
    {20, 28, 25, 31},
    {26, 32, 29, 35}
};

配列の宣言にある3が行数、4が列数です。

3行 × 4列 = 12要素

今回の配列では、行と列に次の意味を持たせています。

方向データ上の意味
機械
production[行][列]指定した機械の指定日の生産数

行と列の添字を確認しよう

2次元配列では、2つの添字を使って要素を指定します。

production[行の添字][列の添字]

C言語の添字は0から始まります。

要素意味
production[0][0]機械1の1日目24
production[0][2]機械1の3日目27
production[1][1]機械2の2日目28
production[2][3]機械3の4日目35

人から見た機械1は行の添字0、1日目は列の添字0です。

行数が3なので、使用できる行の添字は0から2までです。列数が4なので、使用できる列の添字は0から3までです。

図1:行と列で見る2次元配列

この図から分かること

最初の添字は行、2番目の添字は列を指定します。

今回の配列では、行が機械、列が日を表します。たとえばproduction[1][2]は行1、列2なので、人から見れば機械2の3日目の生産数です。

二重ループが必要になる理由

1次元配列は1つの添字で要素を指定するため、1つのfor文で全要素を処理できます。

for (int i = 0; i < 要素数; i++) {
    array[i]を処理する;
}

2次元配列には行と列があるため、両方の添字を動かす必要があります。

for (int row = 0; row < 行数; row++) {
    for (int col = 0; col < 列数; col++) {
        array[row][col]を処理する;
    }
}

それぞれのfor文には、次の役割があります。

ループ役割
外側のfor文処理する行を選ぶ
内側のfor文選んだ行の列を順番に処理する

外側のrowが0の間に、内側のcolは0から最後の列まで変化します。

row = 0
    col = 0
    col = 1
    col = 2
    col = 3

row = 1
    col = 0
    col = 1
    col = 2
    col = 3

row = 2
    col = 0
    col = 1
    col = 2
    col = 3

この動きによって、3行4列の12要素をすべて処理できます。

行方向に集計する考え方

機械ごとの合計を求める場合は、1台の機械を固定して、その機械の4日分を横方向に足します。

機械1なら、次の4要素を加算します。

production[0][0]
production[0][1]
production[0][2]
production[0][3]

計算結果は次のとおりです。

24 + 30 + 27 + 33 = 114

二重ループでは、外側で機械を固定し、内側で日を動かします。

for (int machine = 0; machine < MACHINE_COUNT; machine++) {
    for (int day = 0; day < DAY_COUNT; day++) {
        machine_totals[machine] += production[machine][day];
    }
}

外側のループ変数machineは機械、内側のループ変数dayは日を表します。

列方向に集計する考え方

日ごとの合計を求める場合は、1日を固定して、その日に記録された3台分の値を縦方向に足します。

1日目なら、次の3要素を加算します。

production[0][0]
production[1][0]
production[2][0]

計算結果は次のとおりです。

24 + 20 + 26 = 70

今度は外側で日を固定し、内側で機械を動かします。

for (int day = 0; day < DAY_COUNT; day++) {
    for (int machine = 0;
         machine < MACHINE_COUNT;
         machine++) {
        day_totals[day] += production[machine][day];
    }
}

同じ2次元配列を使用していても、外側と内側のループを入れ替えることで、集計する方向が変わります。

外側のループ内側のループ集計する方向
機械行方向・機械ごと
機械列方向・日ごと

機械ごと・日ごとの生産数を集計する

3台の機械について生産数の合計と平均を求めるプログラム

ファイル名:10_9_1.c

#include <stdio.h>

#define MACHINE_COUNT 3
#define DAY_COUNT 4

int main(void)
{
    /* 3台の機械について4日分の生産数を管理する */
    int production[MACHINE_COUNT][DAY_COUNT] = {
        {24, 30, 27, 33},
        {20, 28, 25, 31},
        {26, 32, 29, 35}
    };

    /* 機械ごとの集計結果を保存する */
    int machine_totals[MACHINE_COUNT] = {0};
    double machine_averages[MACHINE_COUNT] = {0.0};

    /* 機械ごとの合計と平均を求める */
    for (int machine = 0;
         machine < MACHINE_COUNT;
         machine++) {

        for (int day = 0; day < DAY_COUNT; day++) {
            machine_totals[machine] +=
                production[machine][day];
        }

        machine_averages[machine] =
            (double)machine_totals[machine] / DAY_COUNT;
    }

    /* 機械ごとの結果を表示する */
    printf("機械ごとの合計生産数と平均生産数\n");

    for (int machine = 0;
         machine < MACHINE_COUNT;
         machine++) {

        printf("機械%d:合計=%d、平均=%.2f\n",
               machine + 1,
               machine_totals[machine],
               machine_averages[machine]);
    }

    /* 日ごとの集計結果を保存する */
    int day_totals[DAY_COUNT] = {0};
    double day_averages[DAY_COUNT] = {0.0};

    /* 日ごとの合計と平均を求める */
    for (int day = 0; day < DAY_COUNT; day++) {
        for (int machine = 0;
             machine < MACHINE_COUNT;
             machine++) {

            day_totals[day] +=
                production[machine][day];
        }

        day_averages[day] =
            (double)day_totals[day] / MACHINE_COUNT;
    }

    /* 日ごとの結果を表示する */
    printf("\n日ごとの合計生産数と平均生産数\n");

    for (int day = 0; day < DAY_COUNT; day++) {
        printf("%d日目:合計=%d、平均=%.2f\n",
               day + 1,
               day_totals[day],
               day_averages[day]);
    }

    return 0;
}
実行結果の例
機械ごとの合計生産数と平均生産数
機械1:合計=114、平均=28.50
機械2:合計=104、平均=26.00
機械3:合計=122、平均=30.50

日ごとの合計生産数と平均生産数
1日目:合計=70、平均=23.33
2日目:合計=90、平均=30.00
3日目:合計=81、平均=27.00
4日目:合計=99、平均=33.00

プログラムの全体像を確認しよう

このプログラムでは、同じproduction配列を2つの方向から集計しています。

集計内容データを見る方向
機械ごとの集計行を横方向に見る
日ごとの集計列を縦方向に見る

2次元配列の中身は変わりません。変わるのは、外側のfor文で何を固定するかです。

機械ごとの合計を求める処理

機械ごとの集計では、外側のfor文で機械を1台ずつ選びます。

for (int machine = 0;
     machine < MACHINE_COUNT;
     machine++) {

内側のfor文では、選んだ機械について4日分の生産数を順番に加算します。

for (int day = 0; day < DAY_COUNT; day++) {
    machine_totals[machine] +=
        production[machine][day];
}

machineが0の場合、次の処理が行われます。

machine_totals[0]へproduction[0][0]を加える
machine_totals[0]へproduction[0][1]を加える
machine_totals[0]へproduction[0][2]を加える
machine_totals[0]へproduction[0][3]を加える

具体的な計算は次のとおりです。

0 + 24 + 30 + 27 + 33 = 114

結果はmachine_totals[0]へ保存されます。

図2:行方向に機械ごとの値を集計する

この図から分かること

機械ごとの集計では、外側のfor文によって1台の機械を固定します。内側のfor文が、その機械の1日目から4日目までを横方向に処理します。

内側のループが終了した時点で、1台分の合計が完成します。その合計を日数で割れば、機械ごとの平均を求められます。

機械ごとの平均を求める処理

合計を求めたあと、4日分の平均を計算します。

machine_averages[machine] =
    (double)machine_totals[machine] / DAY_COUNT;

機械1の合計は114なので、計算は次のようになります。

114 ÷ 4 = 28.5

平均値には小数部分が含まれる可能性があるため、結果をdouble型の配列へ保存しています。

double machine_averages[MACHINE_COUNT] = {0.0};

日ごとの合計を求める処理

日ごとの集計では、外側のfor文で日を1日ずつ選びます。

for (int day = 0; day < DAY_COUNT; day++) {

内側のfor文では、選んだ日について3台分の生産数を加算します。

for (int machine = 0;
     machine < MACHINE_COUNT;
     machine++) {

    day_totals[day] +=
        production[machine][day];
}

dayが0の場合は、1日目の列を処理します。

production[0][0] = 24
production[1][0] = 20
production[2][0] = 26

合計は次のようになります。

24 + 20 + 26 = 70

結果はday_totals[0]へ保存されます。

日ごとの平均を求める処理

日ごとの合計を求めたあと、機械の台数で割ります。

day_averages[day] =
    (double)day_totals[day] / MACHINE_COUNT;

1日目の合計は70、機械は3台なので、次の計算になります。

70 ÷ 3 = 23.333...

printfで%.2fを指定しているため、表示は小数第2位までの23.33になります。

図3:列方向に日ごとの値を集計する

この図から分かること

日ごとの集計では、外側のfor文によって1日を固定します。内側のfor文は、その日の機械1から機械3までを縦方向に処理します。

同じ2次元配列でも、外側で行を固定するか、列を固定するかによって集計対象が変わります。

ループの向きと集計対象の関係

行方向と列方向の違いを整理すると、次のようになります。

求めたい結果外側のループ内側のループ加算先
機械ごとの合計machinedaymachine_totals[machine]
日ごとの合計daymachineday_totals[day]

機械ごとの集計では、machineを固定してdayを動かします。

production[machine][0]
production[machine][1]
production[machine][2]
production[machine][3]

日ごとの集計では、dayを固定してmachineを動かします。

production[0][day]
production[1][day]
production[2][day]

どちらの添字を固定し、どちらを動かしているかを見ると、二重ループの目的が分かりやすくなります。

{0}で集計用配列を初期化する

合計を保存する配列は、すべての要素を0で初期化しています。

int machine_totals[MACHINE_COUNT] = {0};
int day_totals[DAY_COUNT] = {0};

初期化子が要素数より少ない場合、残りの要素も0で初期化されます。

そのため、次の配列は全要素が0になります。

int values[5] = {0};
要素初期値
values[0]0
values[1]0
values[2]0
values[3]0
values[4]0

合計は0から加算を始める必要があります。不定値が入った状態で加算すると、正しい結果を得られません。

平均値を保存する配列も0.0で初期化しています。

double machine_averages[MACHINE_COUNT] = {0.0};
double day_averages[DAY_COUNT] = {0.0};

この場合も、すべての要素が0.0で初期化されます。

平均計算でdoubleへ型変換する理由

次の式では、合計値をdouble型へ変換しています。

(double)machine_totals[machine] / DAY_COUNT

整数同士の割り算では、小数部分が切り捨てられます。

計算結果
70 / 323
(double)70 / 323.333333...

合計値の前に(double)を付けると、計算が浮動小数点数として行われます。これによって、小数部分を含む平均値を求められます。

結果を小数第2位まで表示するときは、printfで%.2fを使用します。

printf("%.2f\n", average);

マクロで行数と列数を管理する

サンプルでは、機械数と日数をマクロで定義しています。

#define MACHINE_COUNT 3
#define DAY_COUNT 4

2次元配列の宣言とfor文の条件で、同じマクロを使用しています。

int production[MACHINE_COUNT][DAY_COUNT];
for (int machine = 0;
     machine < MACHINE_COUNT;
     machine++) {
for (int day = 0; day < DAY_COUNT; day++) {

マクロを使うと、行数や列数の意味が伝わりやすくなります。

記述読み取れる意味
production[3][4]3行4列
production[MACHINE_COUNT][DAY_COUNT]機械数×日数

数値を直接何度も書かずに済むため、変更時の修正漏れも防ぎやすくなります。

二重ループを読む3つのポイント

外側のループが何を固定しているか

次の外側ループでは、機械を1台ずつ固定しています。

for (int machine = 0;
     machine < MACHINE_COUNT;
     machine++) {

この内側でdayを動かせば、1台分の全日データを処理できます。

一方、次の外側ループでは日を固定しています。

for (int day = 0; day < DAY_COUNT; day++) {

この内側でmachineを動かせば、1日分の全機械データを処理できます。

内側のループが何を動かしているか

内側のループ変数がdayなら、横方向に日を進めています。

内側の変数がmachineなら、縦方向に機械を進めています。

どの配列要素へ加算しているか

次の式は、機械ごとの合計を更新しています。

machine_totals[machine] +=
    production[machine][day];

次の式は、日ごとの合計を更新しています。

day_totals[day] +=
    production[machine][day];

右辺のproductionは同じですが、左辺の保存先が異なります。どこへ結果を残しているかを見ると、何を集計しているのか判断できます。

2次元配列と二重ループで注意したいこと

注意点確認すること
行と列の意味配列を宣言するときに決めておく
添字の順番最初が行、次が列
外側のループ何を1つずつ固定するのか
内側のループ固定した対象の中で何を動かすのか
ループ条件行には行数、列には列数を使う
合計用配列{0}で全要素を0にする
平均の計算doubleへ型変換してから割る
表示する番号人向けの表示には添字へ1を加える
マクロ名行数と列数の意味が分かる名前にする

2次元配列の二重ループは、外側で何を固定し、内側で何を動かすかを考えると読みやすくなります。

行方向へ集計したいなら外側で行を固定し、列方向へ集計したいなら外側で列を固定します。この切り替えができるようになると、同じ表データから異なる視点の合計や平均を求められるようになります。