情報学入門(データ演習) / 第 11 章

配列(1)

たくさんの値を 1 つの名前で扱う

この章のねらい

1. スカラーから配列へ

元資料 4 枚目に、こうあります。

今まで扱ってきた値は、1 つ 1 つ別々になっていました。 例えば、5 人の体重のデータをプログラミングすると
w0 = 50: w1 = 58: w2 = 63: w3 = 69: w4 = 77
のように書く必要がありました。これらをまとめて表す方法が配列です。
w(0) = 50: w(1) = 58: w(2) = 63: w(3) = 69: w(4) = 77

見た目はほとんど同じですが、決定的な違いがあります。

w0 の「0」は名前の一部ですが、 w(0) の「0」はです。
だから w(i) と書けます ── i を変数にできるのです。 そして変数にできれば、ループで回せます。 これが配列のすべての値打ちです。

配列とは ── いくつかの変数を集めたもの(元資料 3 枚目)。

nn
[0]
50
[1]
58
[2]
63
[3]
69
[4]
77
[5]
0

2. 宣言と、添字の数え方

配列も変数と同じように宣言します(元資料 4 枚目)。

Dim w(5) As Integer

ここが最大の注意点です。 Dim w(5) で作られる箱は 5 個ではなく 6 個です ── w(0), w(1), w(2), w(3), w(4), w(5)。 括弧の中の数は「いくつ」ではなく「添字の上限」だからです。

上の図で [5] が点線になっているのはそのためです。 5 人ぶんのデータ(w(0)〜w(4))を入れたので、w(5) は使われずに残っています。 これは間違いではありませんが、「5 人だから w(5)」と覚えると必ず混乱します

3 つの書き方(どれも正しい。意図をはっきりさせるのが大事)
書き方できる箱個数使いどころ
Dim w(4) As Integerw(0)〜w(4)5 0 から数えたいとき(余りが出ない)
Dim w(5) As Integerw(0)〜w(5)6 1 から使いたいとき(w(0) を捨てる)
Dim w(1 To 5) As Integerw(1)〜w(5)5 いちばん誤解がない。これを勧めます

なぜ「1 から使う」ことが多いのか。 Excel の行と列は 1 から始まるので(第 7 章 §4)、 Cells(i, 1)a(i)i をそろえると プログラムが読みやすくなるからです。 そのために w(0) を 1 つ捨てる ── というのが実際によく使われる書き方です (元資料も Dim a(11) と宣言して a(1)a(10) を使っています)。
はっきり書きたいなら Dim w(1 To 5) です。 LBoundUBound で下限・上限を確かめられます。

Sub Macro1()
    Dim a(5) As Integer
    Dim b(1 To 5) As Integer
    Dim i As Integer

    Cells(1, 1) = "Dim a(5)"
    Cells(1, 2) = "下限": Cells(1, 3) = LBound(a)
    Cells(1, 4) = "上限": Cells(1, 5) = UBound(a)
    Cells(2, 1) = "個数": Cells(2, 2) = UBound(a) - LBound(a) + 1

    Cells(4, 1) = "Dim b(1 To 5)"
    Cells(4, 2) = "下限": Cells(4, 3) = LBound(b)
    Cells(4, 4) = "上限": Cells(4, 5) = UBound(b)
    Cells(5, 1) = "個数": Cells(5, 2) = UBound(b) - LBound(b) + 1

    ' 宣言した直後の中身は 0
    Cells(7, 1) = "a(3) の初期値": Cells(7, 2) = a(3)

    ' 範囲外を触るとエラー(' を外して試してください)
    ' Cells(8, 2) = a(9)
End Sub

3. 配列とループ ── 何件でも同じ処理

5 人の体重を配列に入れて、合計と平均を求めます。 人数が 5 人でも 500 人でも、プログラムはほとんど変わりません。

Sub Macro2()
    Dim w(1 To 5) As Integer
    Dim i As Integer
    Dim n As Integer
    Dim s As Long

    n = 5

    ' ① シートから配列へ読み込む
    For i = 1 To n
        w(i) = Cells(i + 1, 1).Value
    Next i

    ' ② 配列を使って計算する(もうシートを見なくてよい)
    s = 0
    For i = 1 To n
        s = s + w(i)
    Next i

    ' ③ 結果を書き出す
    Cells(8, 1) = "合計": Cells(8, 2) = s
    Cells(9, 1) = "平均": Cells(9, 2) = s / n

    ' 平均より重い人に印をつける
    For i = 1 To n
        If w(i) > s / n Then Cells(i + 1, 2) = "↑ 平均より上"
    Next i
End Sub

「読み込む → 計算する → 書き出す」の 3 段に分かれているところに注目してください。 配列に入れてしまえば、②の計算はシートのことを知らなくてよいのです。 第 10 章の「関数は計算するだけにする」と同じ考え方で、 データの置き場所と計算を切り離すと、あとで扱いやすくなります。
なお、②で w(i) の代わりに毎回 Cells(i+1,1).Value と 書くこともできますが、セルを読むのは配列を読むよりずっと遅いので、 件数が多いときは配列に移してから計算するのが定石です。

4. 最大値と最小値 ── 誤りを見つけて直す

以下のような 10 日間の気温データを読み込み、最高気温と最低気温を求めて表示しなさい。

24 27 26 30 35 37 32 30 34 29

元資料 12 枚目のプログラムです。そして 14 枚目に、こう書かれています。

実は、さきほど入力したプログラムは、間違っている
最高気温と、最低気温が逆に表示される
プログラムを修正して、正しい表示となるようにせよ。

これは、とてもよい課題です。 「動くけれど答えが違う」という不具合は、実際にいちばん多く、いちばん厄介です (エラーで止まってくれるほうがまだ親切なのです)。 まず本当に逆に出ることを自分の目で確かめてから、原因を探しましょう。

Function condition(ByVal m, ByVal n) As Boolean
    If m > n Then
        condition = True
    Else
        condition = False
    End If
End Function

Sub Macro610()
    Dim i As Integer
    Dim a(11) As Integer
    For i = 1 To 10
        a(i) = Cells(1, i).Value
    Next
    Dim maxi As Integer, mini As Integer
    maxi = 1
    mini = 1
    For i = 2 To 10
        If condition(a(maxi), a(i)) Then maxi = i
        If condition(a(i), a(mini)) Then mini = i
    Next
    Cells(2, 1).Value = Format(a(maxi), "最高気温##度")
    Cells(2, 2).Value = Format(a(mini), "最低気温##度")
End Sub

「最高気温24度」「最低気温37度」と出ました。 データの最大は 37、最小は 24 ですから、たしかにです。

4.1 原因を探す

condition(m, n)m > n のとき True を返す関数です。 これを踏まえて、ループの中の 2 行を読みます。

If condition(a(maxi), a(i)) Then maxi = i =「いまの最大候補より i 番目が小さければ、最大候補を i にする」
→ 小さいほうを追いかけてしまう
If condition(a(i), a(mini)) Then mini = i =「i 番目がいまの最小候補より大きければ、最小候補を i にする」
→ 大きいほうを追いかけてしまう

2 行とも、引数の順序が入れかわっています。 condition 自体は正しく「左 > 右」を判定しているので、 関数は無罪。呼び出し方が逆だったのです。
だから maxi最小値の位置を、 mini最大値の位置を指してしまい、 表示だけが「最高/最低」と書いてあるので、逆に見えるわけです。

4.2 直す

元のまま(誤り)直したもの
最大を探すcondition(a(maxi), a(i)) condition(a(i), a(maxi))
最小を探すcondition(a(i), a(mini)) condition(a(mini), a(i))
Function condition(ByVal m, ByVal n) As Boolean
    ' この関数は変えていない
    If m > n Then
        condition = True
    Else
        condition = False
    End If
End Function

Sub Macro611()
    Dim i As Integer
    Dim a(1 To 10) As Integer          ' 1 To 10 と書いて意図をはっきりさせる
    For i = 1 To 10
        a(i) = Cells(1, i).Value
    Next i

    Dim maxi As Integer, mini As Integer
    maxi = 1
    mini = 1
    For i = 2 To 10
        If condition(a(i), a(maxi)) Then maxi = i    ' i のほうが大きければ更新
        If condition(a(mini), a(i)) Then mini = i    ' 候補のほうが大きければ更新
    Next i

    Cells(2, 1).Value = Format(a(maxi), "最高気温##度")
    Cells(2, 2).Value = Format(a(mini), "最低気温##度")
    Cells(3, 1).Value = "最高は " & maxi & " 日目"
    Cells(3, 2).Value = "最低は " & mini & " 日目"
End Sub

関数を使わずに素直に書くと、間違えにくくなります。 condition のような関数をはさむと、 引数の順序を頭の中で入れかえて考えなければならないので、 かえって誤りやすいのです。 比較をその場に書いてしまうほうが読みやすいことがあります。

If a(i) > a(maxi) Then maxi = i        ' 「大きければ最大候補を更新」とそのまま読める
If a(i) < a(mini) Then mini = i        ' 「小さければ最小候補を更新」

第 10 章で「部品に分けよう」と言いましたが、 分けたほうが分かりにくくなるなら、分けないのが正解です。 判断の基準は「読んで意味が通るか」です。

Sub Macro612()
    Dim a(1 To 10) As Integer
    Dim i As Integer
    Dim maxi As Integer, mini As Integer

    For i = 1 To 10
        a(i) = Cells(1, i).Value
    Next i

    maxi = 1: mini = 1
    For i = 2 To 10
        If a(i) > a(maxi) Then maxi = i
        If a(i) < a(mini) Then mini = i
    Next i

    Cells(2, 1) = Format(a(maxi), "最高気温##度")
    Cells(2, 2) = Format(a(mini), "最低気温##度")

    ' 最高・最低の日に色をつける
    Cells(1, maxi).Interior.Color = RGB(255, 199, 206)
    Cells(1, mini).Interior.Color = RGB(183, 230, 240)
    Cells(4, 1) = "赤が最高、水色が最低の日"
End Sub

初期値を 0 にしてはいけません。 maxi = 1 のように1 番目のデータを最初の候補にするのが定石です。 もし「最大値を入れる変数を 0 で始める」と書くと、 気温が全部マイナスの日(冬)には、答えが 0 になってしまいますデータの中の 1 つを初期値にすると、この心配がありません。

5. 順位を計算する(元資料 17〜20 枚目)

以下のような成績データについて、1 次元配列を使って「国語」と「数学」の順位を計算せよ。

順位の考え方はとても単純です ── 自分より点数が高い人の数を数えて、それに 1 を足す。 1 位なら「自分より高い人は 0 人」なので 0 + 1 = 1 位です。

Sub Macro3()
    Dim n As Integer
    Dim i As Integer, j As Integer
    Dim kok(1 To 20) As Integer      ' 国語
    Dim sug(1 To 20) As Integer      ' 数学
    Dim goukei(1 To 20) As Integer
    Dim jun As Integer

    n = 8                            ' 人数

    ' 読み込む
    For i = 1 To n
        kok(i) = Cells(i + 1, 2).Value
        sug(i) = Cells(i + 1, 3).Value
        goukei(i) = kok(i) + sug(i)
    Next i

    ' 国語の順位
    For i = 1 To n
        jun = 1
        For j = 1 To n
            If kok(j) > kok(i) Then jun = jun + 1     ' 自分より上を数える
        Next j
        Cells(i + 1, 4) = jun
    Next i

    ' 数学の順位
    For i = 1 To n
        jun = 1
        For j = 1 To n
            If sug(j) > sug(i) Then jun = jun + 1
        Next j
        Cells(i + 1, 5) = jun
    Next i

    ' 合計と総合順位
    For i = 1 To n
        Cells(i + 1, 6) = goukei(i)
        jun = 1
        For j = 1 To n
            If goukei(j) > goukei(i) Then jun = jun + 1
        Next j
        Cells(i + 1, 7) = jun
        If jun = 1 Then Cells(i + 1, 7).Interior.Color = RGB(255, 235, 156)
    Next i
End Sub

同点のとき、この方法は「同じ順位」になります。 上のデータで有吉と浅岡はどちらも数学 73 点・76 点ではありませんが、 同点を作って試してみてください ── 両方とも同じ順位になり、 その次の人は順位が飛びます(1, 2, 2, 4 …)。 これはスポーツの順位づけと同じ流儀で、意図してこうなっています。 「1, 2, 2, 3」としたいなら別の数え方が必要です。

なお、この方法は 1 人の順位を求めるのに全員と比べるので、 n 人なら n² 回の比較になります。 人数が多いときは先に並べ替えてから順位を振るほうが速くなります(第 12 章)。

6. 動的配列 ── 大きさをあとで決める

元資料 8 枚目の「動的配列の宣言」です。 件数が実行するまで分からないときに使います。

Dim a() As Integer  ' 括弧を空にして宣言
ReDim a(1 To n)  ' 大きさが決まってから確保
ReDim Preserve a(1 To m) ' 中身を残して作り直す

Sub Macro4()
    Dim a() As Integer               ' 大きさを決めずに宣言
    Dim i As Integer
    Dim n As Integer
    Dim s As Long

    ' ① まず件数を数える(空になるまで下へ)
    n = 0
    Do While Cells(n + 2, 1).Value <> ""
        n = n + 1
    Loop

    ' ② 決まった大きさで確保する
    ReDim a(1 To n)

    ' ③ 読み込む
    For i = 1 To n
        a(i) = Cells(i + 1, 1).Value
        s = s + a(i)
    Next i

    Cells(9, 1) = "件数":  Cells(9, 2) = n
    Cells(10, 1) = "合計": Cells(10, 2) = s
    Cells(11, 1) = "平均": Cells(11, 2) = Format(s / n, "#.##")
    Cells(12, 1) = "配列の上限": Cells(12, 2) = UBound(a)
    Cells(13, 1) = "データを増やしても、プログラムは変えなくてよい"
End Sub

これが実務での基本形です。 「20 人ぶん」と決め打ちにすると、21 人目が来たときに直さなければなりません。 まず数えて、それから確保するようにしておけば、 データが増えてもプログラムはそのままです。 上のモデルで、A8 に点数を書き足して実行しなおしてみてください ── 件数が自動で 7 になります

7. 2 次元配列 ── 表そのものを扱う

添字を 2 つにすると、表の形のまま扱えます。 第 4 章の行列がまさにこれでした。

Dim t(1 To 4, 1 To 3) As Integer ' 4 行 3 列 = 12 個の箱

Sub Macro5()
    Dim t(1 To 4, 1 To 3) As Integer
    Dim i As Integer, j As Integer
    Dim gyoukei As Long              ' 行の合計
    Dim souke As Long                ' 全体の合計

    ' 読み込む
    For i = 1 To 4
        For j = 1 To 3
            t(i, j) = Cells(i + 1, j + 1).Value
            souke = souke + t(i, j)
        Next j
    Next i

    Cells(1, 5) = "学部計"
    Cells(1, 6) = "割合(%)"

    ' 学部ごとの合計と、全体に対する割合
    For i = 1 To 4
        gyoukei = 0
        For j = 1 To 3
            gyoukei = gyoukei + t(i, j)
        Next j
        Cells(i + 1, 5) = gyoukei
        Cells(i + 1, 6) = Format(gyoukei / souke * 100, "#.#")
    Next i

    ' 学年ごとの合計
    Cells(6, 1) = "学年計"
    For j = 1 To 3
        gyoukei = 0
        For i = 1 To 4
            gyoukei = gyoukei + t(i, j)
        Next i
        Cells(6, j + 1) = gyoukei
    Next j

    Cells(6, 5) = souke
    Cells(6, 5).Interior.Color = RGB(255, 235, 156)
    Cells(8, 1) = "縦に足すのも横に足すのも、ループの順を入れかえるだけ"
End Sub

学部ごとの合計と学年ごとの合計で、ループの内と外が入れかわっているだけです。 t(i, j) という書き方をしていれば、 どちらの向きにも同じように足せます。 これは表を Cells で直接扱っていてもできますが、 配列にしておくと何度も読み直しても速いという利点があります。

8. まとめ

  1. w0 の 0 は名前の一部、w(0) の 0 は。 だから w(i) と書けて、ループで回せる
  2. Dim w(5) は 6 個(0〜5)。 はっきり書くなら Dim w(1 To 5)
  3. 「読み込む → 計算する → 書き出す」の 3 段に分けると、計算がシートから独立する
  4. 最大・最小は1 番目を最初の候補にする。0 から始めると負のデータで破綻する
  5. 順位は自分より上を数えて +1。同点は同じ順位になり、次が飛ぶ
  6. 件数が分からないときは Dim a() → 数える → ReDim
  7. 添字を 2 つにすると表そのものを扱える。足す向きはループの順を入れかえるだけ
  8. 「動くけれど答えが違う」不具合は、まず自分の目で確かめてから原因を探す

章末問題

付録:元資料からの修正一覧

もとにしたのは「情報学入門(VBA)160-2019」全 20 枚です。

元資料この教材種別
5 人ぶんのデータに対して Dim w(5) as Integer と宣言している 正しく動くが、できる箱は 6 個(w(0)〜w(5))で 1 つ余る。 「5 人だから w(5)」と読むと混乱するので、括弧の中は「個数」ではなく「添字の上限」だと明示し、 Dim w(1 To 5) という書き方を勧めた不足
as Integer(小文字)VBA は大小を区別しないので動くが、慣例は As Integer表記
最大値・最小値の初期値の決め方について説明がない 1 番目のデータを最初の候補にするのが定石。 0 から始めると全部マイナスのデータで破綻することを追記不足
condition 関数を使う設計だが、 引数の順序を頭の中で入れかえる必要があり誤りやすい 比較をその場に書いた版(If a(i) > a(maxi) Then)も示し、 分けたほうが分かりにくくなるなら分けないという判断基準を書いた不足
順位計算で、同点のときどうなるかに触れていない 同じ順位になり次が飛ぶ(1, 2, 2, 4)。意図してそうなっていることを明記不足
動的配列の宣言が書式のみ 「まず数えて、それから ReDim」という実務の基本形として、 データを増やしても直さなくてよい例を示した不足
エラトステネスのふるいを配列で書き直す課題(15・16 枚目) 第 10 章で配列を使って実装済みなので、この章では繰り返さない重複

元資料の「間違いを直せ」という課題について。 14 枚目の「実は、さきほど入力したプログラムは、間違っている」は 意図された課題であり、誤りではありません。 むしろこの授業でいちばん優れた課題設計だと思います ── 「エラーは出ないが答えが違う」という、実務でもっとも多く、 もっとも見つけにくい種類の不具合を扱っているからです。 そこでこの教材では、まず本当に逆に出ることを実行して見せ、 原因(condition の引数の順序)を分解し、 2 通りの直し方を並べる、という構成にしました。

元資料で正しかったこと。 「配列とはいくつかの変数を集めたもの」「各箱は必ず同じ型」「添字は 0 から始まる」という説明、 w0 = 50: w1 = 58 … から w(0) = 50: w(1) = 58 … への 導入の流れ、condition 関数そのもの(m > n で True)、 気温データ(24〜37)に対する正解(最高 37 度・最低 24 度)── いずれも正確です。 12 枚目のプログラムはこのページで実行し、資料の言うとおり 「最高気温24度・最低気温37度」と逆に出ることを確認しました。