配列(1)

目次

配列とは

コンピュータによる情報処理においては,四則演算や比較などの「データ処理法」と,条件分岐・繰り返しなどの制御構文にくわえて, コンピュータ上で「データをどのように表現するか」の設計が重要となる.
これらは,それぞれアルゴリズムデータ構造と呼ばれ,プログラムを書くためには双方が必要である.

工学で用いるデータ処理プログラムでは,数MB〜数GB以上の大量のデータを扱う場合がある.
例えば,高速度カメラによる動画像処理では,1000x1000pixel, 1000fps(毎秒1000枚の画像取得)のデータ取得があり,これは1秒あたり1GBのデータを扱うこととなる.
このような大量のデータをメモリ上に保存し,計算処理に用いるためには,通常の単純変数では不可能である.

一例として,キーボードから入力した 10 個の実数の平均と標準偏差を求めるプログラムを考えてみよう.

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

int main(void)
{
    // 10 個の float 型変数
    float a0, a1, a2, a3, a4, a5, a6, a7, a8, a9;

    // KBD 入力させる
    printf("a0 = ");    scanf("%f", &a0);
    printf("a1 = ");    scanf("%f", &a1);
    printf("a2 = ");    scanf("%f", &a2);
    printf("a3 = ");    scanf("%f", &a3);
    printf("a4 = ");    scanf("%f", &a4);
    printf("a5 = ");    scanf("%f", &a5);
    printf("a6 = ");    scanf("%f", &a6);
    printf("a7 = ");    scanf("%f", &a7);
    printf("a8 = ");    scanf("%f", &a8);
    printf("a9 = ");    scanf("%f", &a9);


    // 平均(average, mean)の計算
    float ave = 0.0;
    ave = ( a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 ) / 10.0;

    // 分散(variance)の計算
    float var;
    var = (a0-ave)*(a0-ave) + (a1-ave)*(a1-ave) + (a2-ave)*(a2-ave)      // 長いので途中で改行
        + (a3-ave)*(a3-ave) + (a4-ave)*(a4-ave) + (a5-ave)*(a5-ave)
        + (a6-ave)*(a6-ave) + (a7-ave)*(a7-ave) + (a8-ave)*(a8-ave) + (a9-ave)*(a9-ave);
    var /= 10;

    // 標準偏差(standard deviation)の計算
    float stdev = sqrt(var);

    // 結果の表示
    printf("average = %f, standard deviation = %f \n", ave, stdev);

    return 0;
}

このプログラムで正しい計算を行うことができる.
しかしよく見ると,このプログラムでは

など,似たような同種の処理が繰り返されている.
このような場合は,for, while などの「繰り返し文」を使いたいところだが, 変数名が a0, a1, ... , a9 と,それぞれ異なるため,繰り返し処理でうまく扱うことはできない.

さらに問題は,処理するデータ件数が10件ではなく5件,20件,100件と変化した時に,ソースコードを大幅に変更しなければならない.
これは煩雑になるだけでなく,記述ミスによるバグの温床になる.

そこで,複数の変数をまとめて 単一の識別子(=名前)で扱う方法が用意されている.
これを,配列, array(アレイ)と呼ぶ.

配列の使い方

配列の「定義」の仕方

配列変数を使用したい場合は,プログラム中において以下の様に記述する.

型名 配列名[個数];

例1:整数型で,要素数10個の配列
int a[10];

例2:実数型,要素数100個の配列
float data[100];

配列定義のルール

配列変数の使い方

配列変数をプログラム中で参照したり,値を代入するには,以下のように記述する.
基本的には普通の変数と同じである.

練習問題

要するに,配列名 + 添字で一つの変数だとみなせば良い.

具体例1

10人分のテストの点数を格納する整数型の配列.

おすすめ:配列の要素数(ここでは10という定数)は,プログラム中の様々な場所で何度も使用するので,一般に定数変数(const 指定した変数)として定義しておく

const int N = 10;    // N は配列の要素数. const は「定数」という意味
int score[N];        // 配列の定義

この場合,1個あたり 4 byteの int 型変数が 10個分,メモリ上に確保される.
(具体的には,40 byte の領域がメモリ上に確保される.)

この配列を使って,何か処理をしてみよう.

// 値を順次代入する
score[0] = 100;   // 配列の  先頭に 100 を代入
score[1] = 50;    // 配列の 2番目に  50 を代入
.
.
.
score[9] = 90;    // 配列の  終端に  90 を代入


for(int i=0; i<N; i++) {     // i<10; とは書かない方が良い

    printf(" %d 番目のデータは,%d です.\n", i, score[i]);    // 全員の点数を画面に表示

}

Cの文法規則では,配列の添字は必ず0から始まるので,配列の最後の要素は score[9] である.(score[10]ではない)
したがって,要素数が10個の配列変数に,先頭から順にアクセスするための繰り返し文は,for(i=0; i<10; i++)となる. 繰り返すが,この場合,i は 10 以上の値にならないように気を付ける.
一般に,要素数 N 個の配列要素の最後の添字は N-1 である.添字の値はこの値を超えてはならない.

以下のソースコードは,初めに示した平均と標準偏差を計算するプログラムを,配列を用いて書き直したバージョンである.
どの箇所が異なるか,比較してみよう.

#include <stdio.h>
#include <math.h>    // sqrt()用

int main(void)
{
    const int N = 10;    // const とは,値を変更できない変数であるという指示
                         // データの個数

    float a[N];          // 配列の定義.float型変数,N個を記憶する場所が確保される

    int i;               // 繰り返し処理用のカウンタ変数

    for(i=0; i<N; i++) {    // N 回繰り返す
        printf("a[%d] = ", i);
        scanf("%f", &a[i]);    // KBD入力.変数名の前に &
    }

    // 平均の計算
    float ave = 0.0;
    for(i=0; i<N; i++) {
        ave += a[i];
    }
    ave /= N;

    // 分散の計算
    float var = 0.0;
    for(i=0; i<N; i++) {
        var += (a[i] - ave) * (a[i] - ave);
    }
    var /= N;

    // 標準偏差の計算
    float stdev = sqrt(var);    // sqrt=square root. 平方根の計算用ライブラリ関数

    // 結果の表示
    printf("average = %f, standard deviation = %f \n", ave, stdev);

    return 0;
}

練習問題

配列要素数のconst指定について

基本的には,配列の要素数は,変数として用意しておく. 以下の様に,変数 N は定数でなければならないが,最近の C99 規格に準拠したコンパイラでは,この制限が緩和されている.
コンパイラによっては,コンパイル時にオプションが必要な場合がある.

#include <stdio.h>

int main(void)
{
    // 古いコンパイラでは, const int N = 100; としないと,エラーとなる.
    int N = 100;
    
    int aaa[N];

    aaa[0]=0;
    aaa[1]=10;

    return 0;
}

具体例2

配列では数多くのデータを扱うことが多いので,その値を代入・設定する方法に工夫が必要である.
実習課題で,以下のように複数のデータを入力させる問題がある場合は,これらの方法を用いる

実行例:

Input 5 integers.
1 2 3 4 6      <-この行をキーボード等から入力
1 2 3 4 6      <-この行が出力される

#include <stdio.h>

int main(void)
{
    int i;
    const int N = 5;
    int a[N];

    printf("Input %d integers.\n", N);

    scanf("%d %d %d %d %d", &a[0], &a[1], &a[2], &a[3], &a[4]);

    //	配列を表示して確認
    for(i=0; i<N; i++) {
        printf("%d ", a[i]);
    }
    return 0;
}

通常は様々な配列サイズに対応できるよう,繰り返し文を使用する.

#include <stdio.h>

int main(void)
{
    int i;
    const int N = 5;
    int a[N];

    for(i=0; i<N; i++) {
        scanf("%d", &a[i]);
    }

    //	配列を表示して確認
    for(i=0; i<N; i++) {
        printf("%d ", a[i]);
    }
    return 0;
}

実行例1:1行にスペース区切りで入力

1 2 3 4 6      <-この行をキーボード等から入力

1 2 3 4 6      <-この行が出力される


実行例2:1行に1個,数値を入力

1      <-この行から
2
3
4
6      <-この行までをキーボード入力,毎回Enterキーを押す

1 2 3 4 6      <-この行が出力される
リダイレクトによる入力

毎回数値を手入力するのは面倒なので,数値データなどを別ファイルなどに記録しておき,そこから「リダイレクト」と呼ばれる方法でデータをプログラム内の配列に流し込む方法もある.
以下の例では,あらかじめ in.txt というテキストファイルを作成しておき,ファイル内に書かれたデータを一度に配列に入力させることができる.

テキストファイル「in.txt」の中身をこのように設定し,.exeファイルと同じフォルダに置く.
以下の内容をin.txtという名前で保存

1 2 3 4 6
実行方法(リダイレクト入力)

.\a.exe < in.txt

1 2 3 4 6      <-実行結果

配列使用のルールのまとめ

配列を使用するときの注意

1.配列の添字の範囲は,チェックはされない.

C言語では,処理効率を優先するため,配列の添字範囲が正しいかどうかのチェックは,プログラム作成者に委ねられている
したがって,配列の添字が有効範囲を超えてもコンパイラはエラーを出さない
さらに悪いことに,実行時にエラーとなることもあればエラーとならないこともある. また,一見うまく動作しているように見えても,偶発的にソフトウエアが落ちることがある. (buffer overflow という)

以下のプログラムを実行して,動作を確認せよ.

配列要素を超えるとどうなるか,実行してみよう.for文中の 定数 1000 を 10000 や 100000 などに変更してみよう.
#include <stdio.h>

int main(void)
{
    const int N = 10;
    int x[N];      // 要素数 N 個の配列

    int i;         // カウンタ変数
    for(i=0; i<1000; i++) {     // 添え字の範囲は 0 から N-1(ここでは9) まで許されるが...

        x[i] = i*i;
        printf("x[%d] = %d \n", i, x[i]);

    }

    return 0;
}

練習問題

  1. 上記のプログラムを実行すると何が起こるだろうか.
  2. 上記のプログラムのどこがいけなかったのだろうか.
  3. どこをどのように直せばよいだろうか?
  4. 同じような誤りを犯さない様にするにはどうすればよいだろうか?

【ヒント】繰り返し文などの終了判定に,直接「定数」を置くのが間違いのもと!

2.配列から配列へのコピーは,繰り返し文で1つずつ

C言語の配列の名称は,配列の先頭アドレス(=メモリ中の場所のこと)を示すラベルにすぎない.
したがって,C言語では,配列全体を一度に代入する 「=」 演算子が無いため,面倒でも1つ1つ代入する必要がある.
(ここでは説明しないが,メモリの内容をコピーするライブラリ関数は存在する.)

配列全体の正しいコピー方法
const int N = 10;
int a[N], b[N];    // 配列が二つあるとする.

// a の中身を順に b にコピー
int i;
for(i=0; i<N; i++) {
    b[i] = a[i];    // 各要素を 1 個づつ代入 
}
以下は,全て誤った配列のコピー方法.
正しくないばかりか,プログラムが不正終了する場合がある.
b = a;          // ダメな例.

b[9] = a[9];    // 最後の要素一つがコピーされるだけ. 

b[10] = a[10];  // そもそも [10] というのがアクセス違反

配列の初期化

配列は,ただ定義しただけでは中身は「不定」となる仕様となっている. 即ち,どのような値が入っているか規格上は決められていない.
したがって,必要に応じて,あらかじめ 0 などの初期値を代入する必要がある.
以下は,実数型配列の中身を全てゼロに設定する例である.

// clear array : 配列のゼロクリア

const int N = ???;    //  N は要素数.いくつでも良い.
float array[N];       // 配列本体

int i;
for(i=0; i<N; i++) {
    array[i] = 0.0;     // ひとつずつ0を代入
}

配列中にゼロ以外の値を設定するためには,基本的には1つ1つの要素に値を代入する.

int array[5];   // 要素数5の配列変数の定義

array[0] = 1;    // 1個ずつ代入
array[1] = 2;
array[2] = 3;
array[3] = 4;
array[4] = 5;

しかし,配列の「定義」と同時に値を設定する場合には,以下のような初期化リストを使った「配列の初期化」を行うことができる.

// これはOK.代入ではなく初期化という.

int array[5] = { 1, 2, 3, 4, 5 };

さらに,初期化リストより配列の要素数が確定しているので[ ] の中の要素数を省略できる.

// この場合は,配列サイズ省略OK

int array[] = { 1, 2, 3, 4, 5 };

この場合,配列のサイズはコンパイラが自動的に決定してくれる.
しかし,配列の要素数はそのあとの処理で必要になるので,一般的には省略しないほうが良い

ここまでの「=」記号は,「配列の初期化」であり,(記号は同じであるが)代入ではない.
従って,以下のコード中の = はすべて代入とみなされるので,いずれもコンパイルエラーとなる.

int array1[5];                // 配列の定義

array1 = { 1, 2, 3, 4, 5 };    // これはエラー! 

array1[] = { 1, 2, 3, 4, 5 };  // これもエラー 

array1[0] = { 1, 2, 3, 4, 5 };  // もちろんこれもエラー

2次元配列

Cでは,1次元の配列だけではなく,2次元の配列を作ることもできる.
例えば,3x4の2次元配列を作るには,

int a[3][4];      // 2次元配列の定義

a[0][0] = 10;   // 代入例
a[1][2] = 5;

とする. この時の配列の形は,概念的には以下のようになる. 代数でいうところの行列によく似ている. 最初の[]が行を,後の[]が列を表している.

実際のコンピュータのメモリは1次元なので,あくまで概念的に2次元と考えられる,と理解すると良い.
(内部で2次元→1次元の添字の変換計算が自動的に行われる.)

2次元配列の初期化

2次元配列の場合も以下のとおり「定義と初期化」が可能である.

const int M = 4;
const int N = 3;

int array2D [M][N] = {{ 1,  2,  3},   // <- これで1行分
                      { 4,  5,  6},
                      { 7,  8,  9},
                      {10, 11, 12} };

ただし,要素数の省略は,最初の[]のみ許される.

const int M = 4;
const int N = 3;

int array2D [ ][N] = {{ 1,  2,  3},
                      { 4,  5,  6},
                      { 7,  8,  9},
                      {10, 11, 12} };

2次元配列にデータを読み込む方法

1次元配列と同様,2次元配列にデータを格納するには以下のように2重ループを用いる. 添字の順序に注意して記述する.

#include <stdio.h>

int main(void)
{
    const int M = 4;
    const int N = 3;
    
    int a[M][N];

    int i,j;
    for(i=0; i<M; i++) {
        for(j=0; j<N; j++) {
            scanf("%d", &a[i][j]);    // a[i][j] で,1つの変数と見れば良い.
        }
    }

    //	配列を表示して確認
    for(i=0; i<M; i++) {
        for(j=0; j<N; j++) {
            printf("%d ", a[i][j]);
        }
        printf("\n");           // 1行分表示したら改行
    }
    return 0;
}

実行例1

1 2 3        <- この行から
4 5 6
7 8 9
10 20 30     <- この行までをキーボード入力,1行入力するごとにEnterキー

1 2 3        <- 出力
4 5 6 
7 8 9 
10 20 30

実行例2:行-列の順に,1行にまとめて入力
1 2 3  4 5 6  7 8 9  10 20 30        <- 1行で入力

1 2 3        <- 出力
4 5 6 
7 8 9 
10 20 30

多次元配列

多次元配列の定義

一次元,二次元の配列だけではなく,三次元以上の配列を作ることもできる.

int a3[10][3][4];

これは,3×4 の 二次元配列が 10 個,計120個の整数が利用できる.

さらに配列の次元を増やして,

int a3[10][3][4][5][10][3];

のような記述も可能であるが,工学上の問題の性質を考えれば,せいぜい2∼3次元程度が実用的である.
いずれの多次元配列も,コンピュータのメモリ上では1次元配列として格納されている.

まとめ

Q and A

Q and A
配列の添え字は1からでなくゼロから始まるのはなぜですか?
配列の添え字は,配列の「先頭アドレスからのオフセット」を表し,先頭はオフセット==0であるからです.ポインタの回も参照してください.
配列に一括で値を(初期化ではなく)代入する方法はありますか?
基本的にはできません.繰り返し文を使って,1個1個,代入します.
ただし,最近のC++規格では可能となっています.

練習問題

以下の問それぞれに対応するプログラムを作成しなさい.

  1. キーボードから整数を10個入力して配列に順次格納し,正順および逆順で画面に表示せよ.

    #include <stdio.h>
    
    int main(void)
    {
        const int N = 10;     // 配列の要素数
        int array[N];       // 整数 N 個分の配列
        
        // データ入力
        for(...) {
    //      printf(...);    // 必要に応じて
            scanf(...);
        }
    
        // 配列データを正順に出力
    
        // 配列データを逆順に出力
    
        return 0;
    }
    
    
    実行例:
    1 0 6 2 5 3 7 9 8 4     <- キーボードから入力 またはファイルからリダイレクト
    
    1 0 6 2 5 3 7 9 8 4     <- 正順で出力
    4 8 9 7 3 5 2 6 0 1     <- 逆順で出力
    
  2. キーボードから実数を10個入力し配列に格納し,それらの平均値を指定の桁数(例えば小数点以下3桁)で画面に表示せよ.

    ヒント:合計値を記憶しておく変数を別途用意し,0で初期化しておき,そこに配列の各要素の値を順次,足してゆく.

    #include <stdio.h>
    
    int main(void)
    {
        const int N = 10;     // 配列の要素数
        double data[N];       // 実数 N 個分の配列
        
        // データ入力
        for(...) {
    //      printf(...);    // 必要に応じて
            scanf("%lf", &...);    // double には %lf
        }
    
        // 合計値の計算
        double sum = 0.0;
        for(...)
            ...
    
        // 結果の出力
        printf(" ??? ", .... );
        return 0;
    }
    
    
    実行例:(結果の数値は正しいとは限らない)
    
    1 2 5 3 10 9 0.9 1.1 2.2 0.1     <- キーボードから入力 またはファイルからリダイレクト
    5.320                            <- 小数点以下3桁で出力
    

  3. 0から100までの整数値をランダムに10個生成して配列に格納,そのなかから最大値を探し,値を画面に表示せよ.

    注1:ランダムな整数値(乱数と呼ぶ)の生成には,ライブラリ関数であるrand()を用いる. 参考までに,以下ソースコードを用いてよい.
    注2:渡された配列内の値の最大値を探索する場合,暫定1位を記憶しておく変数maxを用意して,配列の値を順次比較する.

    #include <stdio.h>
    #include <time.h>     // time()用
    #include <stdlib.h>   // rand() 用
    
    int main(void)
    {
        const int N = 10;
        int a[N];   // 整数 N 個分の配列
    
        ////////////////////////////////////////////
        //  0~100 の乱数を生成して配列に代入
    
        srand(time(NULL));  // 乱数テーブルを初期化 
    
        for(int i=0; i<N; i++) {
            a[i] = rand() % 101;      // rand()は呼ばれるたびに整数の乱数を返す.
    //      printf("%d ", a[i]);    // debug用
        }
        printf("\n");
    
    
        //////////////////////////////////////////////////
        // 配列の中から最大値を見つける
    
        int max;       // ここに最大値を格納
    
        // 最大値を探す処理
        ????
    
    
        // 結果の出力
        printf("max = ...\n", max);
        return 0;
    }
    
    実行例:(乱数の値は毎回異なる.)
    
    55 82 57 62 4 81 19 49 0 26     <- 確認の為の表示
    max = 82
    
    
  4. 配列の数値データを降順(大きい順)に並べ替えて,前後の値を画面に表示せよ.
    このような処理を「ソーティング」という. 基本的に二重のループ処理となる.

    #include <stdio.h>
    
    int main(void)
    {
        // 配列サイズや値を変更しても正しく動作すること!
        const int N = 5;
        int a[N] = {3, 2, 5, 4, 1};
    
        
        //////////////////////////////
        // 最初の配列データの出力
        printf("Before :");
        for(...) {
            printf(" ...", ???);
        }
        printf("\n");
    
        
        //////////////////////////////
        // 並べ替え.
        for(...) {
            for(...) {
                ...
            }
        }
    
    
        //////////////////////////////
        // 結果の出力
        printf("After  :");
        for(...) {
            printf(" ...", ???);
        }
        printf("\n");
    
        return 0;
    }
    
    実行例
    Before : 3 2 5 4 1
    After  : 5 4 3 2 1
    
    並べ替え(ソーティング)について

    配列データの並べ替えには,さまざまなアルゴリズムが提案されている. 一番単純な降順の並べ替えは,以下のような手順で実現可能である.
    ここでは配列名を a,要素数をN とする.

    1. 配列の k 番目の値に注目する.(最初は k=0 とする.)
    2. k 番目の値 a[k]と,それ以降の配列の値 a[l], l=k+1, k+2, ... , N-1 とを順次比較.
      a[l]a[k]より大きければ,a[k]の値とa[l]の値を入れ替える.swapという. l=N-1の時点でa[k]の値が最大値となっている.
    3. これを,k=0 ∼ N-2まで繰り返す.

    この手順を繰り返し,全てのkに対する処理が完了した時点で,配列の先頭から大きい値が順に並んでいる.
    また,上記の「大きければ」の比較を「小さければ」にすれば,昇順(小さい順)の並べ替えができる.

    このほかにもさまざまなソーティングアルゴリズムがあるので,興味があれば各自で調べてみよう.
    キーワード:バブルソート,クイックソート,ヒープソートなど.