カテゴリー
Java

Java(Step6-1-2)

配列

配列②

配列の要素の最大値を求める

配列の要素の最大値を求める手続きを考えます。配列の要素数が3であれば、3つの要素a[0],a[1],a[2]の最大値は以下のようになります。

max = a[0];
if(a[1] > max) max = a[1];
if(a[2] > max) max = a[2];

このif文をより柔軟に書き換えた形が以下になります。

max = a[0];
for(int i = 1; i < a.length; i++)
if(a[i] > max) max = a[i];

このように配列の要素1つずつ順になぞっていく手続きを走査(traverse)と呼びます。

キーボードから読み込んだ値から最高点を求めるプログラムを走査を使って作成しましょう。

//HighScore.java
//点数を読み込んで最高値を表示
import java.util.Scanner;

public class HighScore {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(System.in);
        final int num = 5;
        int[] score = new int[num];

        System.out.println(num + "人分の点数を入力してください。");
        for(int i = 0; i < num; i++){
            System.out.print((i + 1) + "番の点数:");
            score[i] = stdIn.nextInt();
        }

        int max = score[0];
        for(int i = 1; i < score.length; i++)
            if(score[i] > max) max = score[i];

        System.out.println("最高点は" + max + "点です。");
        stdIn.close();
    }
}
HighScore.java実行結果

線形探索

配列の要素の中に、ある値が含まれているか調べるプログラムを作りましょう。ある値をもつ要素の存在を調べることを探索(search)と呼び、調べる値のことをキー(key)と呼びます。

探索は、配列の要素を先頭から順に走査することによって実現できます。探すべきキー値と同じ要素に会ったら、探索が成功します。これを線形探索(liner search)または逐次探索(sequential search)のアルゴリズムと呼びます。

それを実現したプログラムが以下になります。

//LinearSearch.java
//線形探索
import java.util.Random;
import java.util.Scanner;

public class LinearSearch {
    public static void main(String[] args){
        Random rand = new Random();
        Scanner stdIn = new Scanner(System.in);

        final int n = 12;
        int[] a = new int[n];

        for(int j = 0; j < n; j++)
            a[j] = rand.nextInt(10);

        System.out.print("配列aの全要素の値\n{");
        for(int j = 0; j < n; j++)
            System.out.print(a[j] + " ");
        System.out.println("}");

        System.out.print("探す数値:");
        int key = stdIn.nextInt();

        int i;
        for(i = 0; i < n; i++)
            if(a[i] == key)
                break;
        if(i < n)
            System.out.println("それはa[" + i + "]にあります。");
        else
            System.out.println("存在しません。");
        
        stdIn.close();
    }
}
LinearSearch.java実行結果

線形探索アルゴリズムは、キーと同じ要素が複数個存在する場合にその中で、最も先頭に位置する要素を見つけます。

拡張for文

ここまでのプログラムを見て分かるように、配列を扱う際はほとんどfor文を使用します。もう1つのfor文である拡張for文(enhanced statement)を用いると、配列の走査を簡潔に実現できます。そのプログラム例が以下になります。

//ArraySumForIn.java
//配列の全要素の和を求めて表示(拡張for文)

public class ArraySumForIn {
    public static void main(String[] args){
        double[] a = {1.0, 2.0, 3.0, 4.0, 5.0};

        for(int i = 0; i < a.length; i++)
            System.out.println("a[" + i + "] =" + a[i]);
        
        double sum = 0;
        for(double i : a)
            sum += i;
        System.out.println("全要素の和は" + sum + "です。");
    }    
}
ArraySumForIn.java実行結果

このfor文は以下のように理解しましょう。

  • 配列 a の先頭から末尾までの全要素を1個ずつ走査します。ループ本体では、現在着目している要素を i と表現します。

拡張for文を利用することには以下のメリットがあります。

  • 配列の長さ(要素数)を調べる手間が省ける
  • イテレータと同じ方法で走査を行える。

配列を逆順に並び替える

配列の全要素を逆順に並べ替えるプログラムを作りましょう。まずは、アルゴリズムを考えましょう。要素数がnである配列の要素を逆転させるには以下のようにアルゴリズムを組みましょう。

for(int i = 0; i < n / 2; i++)
a[i]とa[n - i - 1]を交換する

一般に要素数がnであれば、交換回数はn / 2回になります。なぜなら、要素数が奇数のときは、中央の要素を交換する必要がないからです。

このアルゴリズムをもとに作ったプログラムが次になります。

//ReverseArray.java
//配列の要素の順序を逆順に並べて表示
import java.util.Random;
import java.util.Scanner;

public class ReverseArray {
    public static void main(String[] args){
        Random rand = new Random();
        Scanner stdIn = new Scanner(System.in);

        System.out.print("要素数:");
        int n = stdIn.nextInt();
        int[] a = new int[n];

        for(int i = 0; i < n; i++){
            a[i] = 10 + rand.nextInt(90);
            System.out.println("a[" + i + "] :" + a[i]);
        }

        for(int i = 0; i < n / 2; i++){
            int t = a[i];
            a[i] = a[n - i - 1];
            a[n - i - 1] = t;
        }
        
        System.out.println("要素の並びを逆にしました。");
        for(int i = 0; i < n; i++)
            System.out.println("a[" + i + "] :" + a[i]);

        stdIn.close();
    }      
}
ReverseArray.java実行結果

配列のコピー

ある配列の全要素の値を、別の配列にまるまるコピーするプログラムを書いてみましょう。大事なのは、コピーする際は繰り返し文によって全要素を逐一コピーする必要があることです

//CoppyArray.java
//配列の全要素をコピー
import java.util.Scanner;

public class CoppyArray {
    public static void main(String[] args){
        Scanner stdIn = new Scanner(System.in);
        
        System.out.print("要素数:");
        int n = stdIn.nextInt();
        int[] a = new int[n];
        int[] b = new int[n];

        for(int i = 0; i < n; i++){
            System.out.print("a[" + i + "] = ");
            a[i] = stdIn.nextInt();
        }

        for(int i = 0; i < n; i++)
            b[i] = a[i];

        System.out.println("aの全要素を配列bにコピーしました。");

        for(int i = 0; i < n; i++)
        System.out.println("b[" + i + "] = " + b[i]);

        stdIn.close();
    }
}
CoppyArray.java実行結果

配列のコピーを行うのが、以下の部分です。これにより、a[0]をb[0]にa[1]をb[1]にと i の数だけコピーしていくことができます。

b[i] = a[i];

文字列の配列

文字列は、String型で表せるため、その配列の型もString[]となります。まずは、ジャンケンの手である「グー」「チョキ」「パー」という文字列の配列を考えてみましょう。要素型がString型で要素数が3の配列を生成するので、以下のように書くことができます。

String[] hands = new String[3];
hands[0] = "グー";
hands[1] = "チョキ";
hands[2] = "パー";

宣言の仕方も使い方も、int型やdouble型と変わりません。以下のように宣言すれば、配列生成時に各要素を初期化できます。

String[] hands = {"グー", "チョキ", "パー"};

次に文字列の配列を利用して、月名の英単語を使用するプログラムを作りましょう。ランダムに選ばれた月の英単語を表示して、それが何月かを解答させ、当てるまで繰り返すようにします。

  1. 出題する月の値を0から11の乱数として生成し、その文字列を表示します。
  2. 解答を入力するように促して、月の値を m に読み込みます。
  3. 読み込んだ m が 英単語の月と等しくなるまで、while文で制御します。

プログラムにすると、以下のようになります。

//MonthCAI.java
//月を表す英単語の学習プログラム
import java.util.Random;
import java.util.Scanner;

public class MonthCAI {
    public static void main(String[] args){
        Random rand = new Random();
        Scanner stdIn = new Scanner(System.in);
        String[] month = {"Jenuary", "February", "March", "April", "May",
    "June", "July", "August", "September", "October", "November", "December"};

        int num = rand.nextInt(12);
        System.out.println("問題は" + month[num]);

        while(true){
            System.out.print("何月かな?:");
            int m = stdIn.nextInt();
            
            if(m == num + 1) break;
            System.out.println("違います。");
        }
        System.out.println("正解です。");
        stdIn.close();
    }    
}
MonthCAI.java実行結果

参照型とオブジェクト

以下のプログラムを実行してみましょう。これは、配列の要素の値ではなく、配列変数そのものの値を表示するプログラムです。

//PrintArray.java
//配列変換の値を表示

public class PrintArray {
    public static void main(String[] args){
        int[] a = new int[5];
        System.out.println("a = " + a);

        a = null;
        System.out.println("a = " + a);
    }
}
PrintArray.java実行結果
参照型とオブジェクト

new で生成される配列本体は、通常の変数と違って、プログラム実行時に生成され、そのための記憶領域が動的に確保されます。配列の本体は、通常の変数と異なるため、オブジェクト(object)と呼ばれます。オブジェクトを指すための変数の型が、参照型(reference type)となります。

空型と空参照・空リテラル

配列変数aにnullを代入していますが、nullは、空リテラル(null literal)と呼ばれます。代入されたaは、空参照(null reference)となります。空参照とは、何も参照していないことを表す特殊な参照で、型は空型(null type)になります。

ガーベジコレクション

配列変数に対して、nullを代入するなどして、もともとの配列本体はどこからも参照されないゴミになってしまいます。ゴミの放置は記憶領域の不足に繋がります。そのため、参照されなくなったオブジェクト用の領域は、再利用できるように自動的に解放されます。再利用できるようにすることをガーベジコレクション(garbage collection)と言います。

finalな配列

配列変数は以下のように、final変数として宣言できます。

final int[] a = new int[5];

配列変数を final にしておけば、誤ってnullを代入したり、他の配列本体への参照を代入したりするという事故を防げます。