配列 array 続編(教科書7.1〜7.6節)

2次元配列(教科書p.199〜202)

同種のデータの単なる集まりや数列のようなデータは1次元配列で表現できる。一方、表計算ソフトのように2次元、すなわち縦と横の2つの軸のあるデータを表現するには2次元配列を使うと良い。

2次元配列の宣言は次の形式である:

配列のデータ型 配列名[要素数][要素数];

1次元の配列の宣言に、更に[要素数]が1つ分増えている。具体的には、例えば次のようになる:

int array[3][5];

2次元配列の場合、2つの[]はそれぞれ行番号と列番号と考えればよい。模式的に表すと下図のようになる。

実際には、メモリ上にこの図のように2次元的に変数が並んで記憶されているのではなく、順番に一列に連続して記憶されている。

使い方は1次元配列と同様に、2つの添字を指定することで要素を指定する。1次元配列と同様にそれぞれの添字は0から始まることに注意。有効な範囲外の要素番号を指定した場合の結果は不定で、最悪プログラムが異常終了するのも1次元配列と同じ。

練習:int array[3][5]; で宣言される3x5の2次元配列に対して、正しくない(文法的に間違っている、あるいは、文法的には間違いではないがプログラムが異常を起こす可能性のある)書き方を次の中から選べ。

array[0][4] = 3;
array[0][5] = 4;
array[1][3] = array[2][0];
array[3][1] = array[1][3] + 1;
array = 0;
array[2] = 123;
array[0] = array[1];

具体的な使い方の例を挙げてみよう。例えば、あるクラスで英語、国語、数学のテストの点数を集計しようとする。表計算ソフトなら次のような表を作るだろう。

クラスの人数の上限を50人とし、1次元配列で点数を扱うと、例えば次のようになる。名前は文字列として扱う必要があるのでここでは省略する。受験番号は、上の表では1からになっているが、ここでは便宜上0から始まる番号とする。

int eng[50]; // 英語Englishの点数 int lang[50]; // 国語languageの点数 int math[50]; // 数学mathematicsの点数

練習 配列eng, lang, mathがそれぞれ上の表のどの部分の値を記憶しようとしているか考えてみよう。

このようにしても不都合は無いが拡張性に乏しい。例えば、全員分の個人合計や個人平均を計算して表示するプログラムを考えてみると、次のようになる。

int sum;  // 個人合計
int ave;  // 個人平均
int i;  // カウンタ変数

for (i = 0; i < NUM_CLASS; i++) {  // NUM_CLASSはクラスの人数とする
    sum = eng[i] + lang[i] + math[i];
    ave = sum / 3;
    printf("%d番: 合計 %d、平均 %d\n", i, sum, ave);
}

科目数が少なくて固定的であればこれでも問題ないが、例えば10科目になったり、特定の科目の平均だけ計算したい、などとなるともう少し工夫したくなる。そこで2次元配列を使うことを考える。例えば次のような2次元配列で点数を扱うことにする。

int score[50][3];  // 50人分、3科目分の点数

このようにすると、個人合計や個人平均を計算するプログラムは次のように書ける。

int sum;  // 個人合計
int ave;  // 個人平均
int i, j;  // カウンタ変数

for (i = 0; i < NUM_CLASS; i++) {  // NUM_CLASSはクラスの人数とする
    sum = 0;  // 合計をリセットしておく
    for (j = 0; j < NUM_SUBJECT; j++) {  // NUM_SUBJECTは科目数、この場合3
        sum += score[i][j];  // i番目の学生のj番目の科目の点数をsumに加算する
    }
    ave = sum / NUM_SUBJECT;
    printf("%d番: 合計 %d、平均 %d\n", i, sum, ave);
}

このプログラムは先ほどのプログラムよりも長く、複雑になっている。しかし、科目数が何科目になっても変更する必要が無いという利点がある。逆に、2次元目のどの配列要素がどの科目に対応するかは分かりにくくなる問題がある。とりあえずのプログラムで分かりやすく書くなら先に説明した1次元配列のプログラムでも構わないが、将来性を考えたり、そもそも2次元的なデータであることを考慮する場合は2次元配列を使ったプログラムの方がよい。

※0番が英語で、などの番号と科目の対応関係を間違えると計算結果が正しくないといった不具合を起こすことになる。科目名を定数化したり、enumというデータ構造を使用することでこの間違いを防ぎやすくできる。更に良い方法としては、複数の変数をまとめたデータ構造を定義する構造体がある。興味があれば調べてみよう。

以上のようにどちらが優れているかは一概には言えないが、確実に言えることは、この程度ならば両方とも理解できて書けなければならない。

多次元配列

3次元以上の配列も同様に宣言して使うことができる。3次元配列の宣言は例えば次のようになる:

int array[3][5][2];

4次元以上になると意味が直感的に理解しにくく、必要となる場合も限られてくるが、原理的には何次元でも使用できる。ただし、要素数が爆発的に増加するので必要になるメモリ量には注意する必要がある。

確認課題

確認課題1. ゲーム盤

9x9のマス目のゲーム盤を2次元配列で表現することにする。それぞれのマス目にコマが無い状態を0、コマがある状態を1で表すとする。下図のように、右下がりの対角線上にコマが並んでいる状態を2次元配列に作成し表示するプログラムboard.cを作成せよ。配列の初期化および表示には繰り返し処理を使うこと。また、単に表示するだけでなく、必ず2次元配列に値を格納してから、その配列の内容を表示すること。
注意:HandyGraphicを使う必要はない。実行例のように数字で表示すればよい。

【実行例】
% gcc -o board board.c
% ./board
1 0 0 0 0 0 0 0 0 
0 1 0 0 0 0 0 0 0 
0 0 1 0 0 0 0 0 0 
0 0 0 1 0 0 0 0 0 
0 0 0 0 1 0 0 0 0 
0 0 0 0 0 1 0 0 0 
0 0 0 0 0 0 1 0 0 
0 0 0 0 0 0 0 1 0 
0 0 0 0 0 0 0 0 1 
%

2次元の配列の要素全体を処理するには、まず0行目について0列目、1列目、と順に処理する。最後の列に達したら次の行に進み、0列目から順に処理する。これを最後の行まで繰り返す、という手順になる。この手順を実現するには2重ループを使えばよい。

2次元配列の値を表示するコードは、例えば次のとおり(プログラムの一部分なのでこれだけでは正しく表示されない)。

int board[9][9];  // 9x9のゲーム盤
int i, j;  // カウンタ変数

for (i = 0; i < 9; i++) {  // 縦方向の繰り返し
    for (j = 0; j < 9; j++) {  // 横方向の繰り返し
        print("%d ", board[i][j]);  // 座標(i,j)の値を表示する。改行はしない。
    }
    printf("\n");  // 1行分表示したので改行する
}

盤の状態を表示する前に、盤にコマを並べなければならない(=配列の各要素にコマの値を設定しなければならない)。このためには上記と同様の2重ループを使い、コマがあるマス目は行番号と列番号が同じマス目であるので、2重ループの2つのカウンタ変数(それぞれ行番号と列番号に相当)が一致すればその要素の値を1、一致しなければ0とすればよい。

配列の初期化処理をフローチャートで描くと次のようになる(表示処理は含まれていない)。

盤を表す配列の初期化処理と、盤の状態を表示する処理はまとめるのではなく、それぞれ別の繰り返しとするのがよい。

確認課題2. 続・ゲーム盤

9x9のマス目のゲーム盤を2次元配列で表現することにする。それぞれのマス目にコマが無い状態を0、コマがある状態を1で表すとする。下図のように、両対角線上にコマが並んでいる状態を2次元配列に作成し表示するプログラムboard2.cを作成せよ。配列の初期化および表示には繰り返し処理を使うこと。また、単に表示するだけでなく、必ず2次元配列に値を格納してから、その配列の内容を表示すること。
注意:HandyGraphicを使う必要はない。実行例のように数字で表示すればよい。

確認課題1.で作成したboard.cに少し追加するだけでできるはずなので、board.cをboard2.cにコピーして追加部分を書き足せばよい。

【実行例】
% gcc -o board2 board2.c
% ./board2
1 0 0 0 0 0 0 0 1
0 1 0 0 0 0 0 1 0 
0 0 1 0 0 0 1 0 0 
0 0 0 1 0 1 0 0 0 
0 0 0 0 1 0 0 0 0 
0 0 0 1 0 1 0 0 0 
0 0 1 0 0 0 1 0 0 
0 1 0 0 0 0 0 1 0 
1 0 0 0 0 0 0 0 1 
%

ヒント:右下がりの対角線のマス目は確認課題1.のとおり、i==jである。右上がりの対角線のマス目の条件はi+j==8である。

確認課題3. 続々・ゲーム盤 [advanced]

9x9のマス目のゲーム盤を2次元配列で表現することにする。それぞれのマス目にコマが無い状態を0、黒コマがある状態を1、白コマがある状態を2で表すとする。下図のようにコマが配置されている状態を2次元配列に作成し表示するプログラムboard3.cを作成せよ。配列の初期化および表示には繰り返し処理を使うこと(初期値を直接代入するようなプログラムは不可)。また、単に表示するだけでなく、必ず2次元配列に値を格納してから、その配列の内容を表示すること。
注意:HandyGraphicを使う必要はない。実行例のように数字で表示すればよい。

確認課題2.で作成したboard2.cに少し追加するだけでできるはずなので、board2.cをboard3.cにコピーして追加部分を書き足せばよい。

【実行例】
% gcc -o board3 board3.c
% ./board3
1 0 2 0 2 0 2 0 1
0 1 0 2 0 2 0 1 0 
2 0 1 0 2 0 1 0 2 
0 2 0 1 0 1 0 2 0 
2 0 2 0 1 0 2 0 2 
0 2 0 1 0 1 0 2 0 
2 0 1 0 2 0 1 0 2 
0 1 0 2 0 2 0 1 0 
1 0 2 0 2 0 2 0 1 
%

ヒント:黒コマの配置は確認課題2.と同じ。白コマが配置されるマス目の条件は、i+jが偶数、ただし黒コマが配置されるマス目でないこと、となる。黒コマの配置の条件が成り立たない場合だけ、白コマの配置の条件を判定すればよい。あるいは、i+jが偶数となるマス目全部に先に白コマを配置して、黒コマを後から上書きしてもよい。

本日の提出課題

提出課題 ゲーム盤・改

9x9のマス目のゲーム盤を2次元配列で表現することにする。それぞれのマス目にコマが無い状態を0、コマがある状態を1で表すとする。まず、20個のコマをランダムに配置する。このプログラムは下記のリストの通りである。このプログラムで配置した後、縦または横に4つコマが並んでいるかどうかを判定するプログラムboardKai.cを作成せよ。余裕があれば斜めも判定してみよ。
注意:HandyGraphicを使う必要はない。乱数の使い方の詳しい説明は高度なテクニック集を見よ(特に見なくても下のプログラムをそのまま使えばよい)。

提出期限:次回授業開始時まで。プログラムファイル名が確認課題1〜3と類似しているので間違えないように。

/*****
      boardKai.c
      9x9のゲーム盤にランダムに20個のコマを配置し、
      縦か横に4つ並んでいるか判定する
*****/

#include <stdio.h>
#include <stdlib.h>  // rand関数を使うために必要
#include <time.h>  // time関数を使うために必要

int main() {
    int board[9][9] = {};  // 盤、0は駒無し、1は駒あり、初期状態はすべて0
    /*
	   配列に初期値を代入する際に{}の中に何も書かなければ
	   すべての要素に0を代入することができる
	*/

    int x, y;  // コマを配置する座標
    int koma;  // 配置するコマの個数を数えるカウンタ変数
    int i, j;  // カウンタ変数

    srand(time(NULL));  // 乱数を現在時刻で初期化する

    // 20個のコマをランダムな座標に配置する
    koma = 0;
    do {
        x = rand() % 9;  // xの値を0〜8の乱数値とする
        y = rand() % 9;  // yの値を0〜8の乱数値とする
        if (board[x][y] == 0) {  // (x,y)の位置にコマがなければ
            board[x][y] = 1;  // コマ有りにする
            koma++;  // 置いたコマの個数を1増やす
        }
    } while (koma < 20);  // 20個コマを置くまで繰り返す
    
    // 盤の表示
    for (i = 0; i < 9; i++) {
        for (j = 0; j < 9; j++) {
            printf("%d ", board[i][j]);
        }
        printf("\n");  // 1行分表示したら改行する
    }

    // 判定する
    /* この部分を書いて完成させよ */

    return 0;
}
【実行例】(乱数を使っているので実行するごとに結果は変わる)
% gcc -o boardKai boardKai.c
% ./boardKai
0 0 0 0 1 1 0 0 0 
0 1 0 0 1 0 0 0 0 
1 0 0 1 0 0 0 1 0 
1 0 0 0 0 0 0 0 1 
0 0 1 0 0 0 0 0 1 
0 0 1 0 0 1 0 1 1 
0 1 0 0 0 0 0 1 0 
0 0 0 0 0 0 1 0 0 
0 0 1 0 0 1 0 0 0 
% ./boardKai
0 1 0 0 0 0 1 1 0 
1 1 0 0 0 0 0 0 0 
0 1 0 0 1 0 1 0 1 
1 1 0 0 1 1 0 0 0 
1 0 0 0 0 1 0 0 0 
0 1 0 0 0 0 0 0 0 
1 1 0 0 0 0 0 0 0 
0 0 0 0 1 1 0 0 0 
0 0 0 0 0 0 0 0 0 
BINGO!
% ./boardKai
0 0 0 0 0 0 1 0 0 
0 1 0 0 0 1 0 0 0 
0 0 0 0 0 1 0 1 0 
0 0 1 0 0 1 1 1 1 
0 0 1 0 0 1 0 1 0 
1 0 0 0 0 0 0 0 1 
0 0 0 0 0 0 0 0 0 
0 0 0 0 0 0 0 1 1 
0 1 1 0 1 0 0 0 0 
BINGO!
BINGO!
%

上記の例では4つ並んだときにBINGO!と表示しているが、これは自由に変えてもよい。
また、4つの並びが複数あったり、5つ以上並んだときは複数回メッセージを表示してもよいし、
1回だけの表示としてもよい。

ヒント:あるマス目を起点として、縦あるいは横に連続する4つの要素の値がすべて1であることを判定する。これを、2重ループを使って起点を変えながら必要なだけ繰り返す。縦と横をチェックする2重ループは分けた方がよい。配列の範囲外にならないよう注意。