ワークシート上で頻出する2種類のデータ操作(整列、検索)を行うプログラムが作成できるようになることを目標とします。それぞれのデータ操作について、処理手順(アルゴリズム)の定石を参考にしながら、自ら手順を構築する方法を学習します。その上で、Excel VBAに用意された機能(メソッド)を用いて、より簡便にデータ操作を行うプログラムを作成する方法を学習します。 .
1.データの操作とアルゴリズム
2. データの整列(ソート)
3. データの検索
ここではプログラミングの技法を学ぶ前に、この回のテーマである「データの操作」と「アルゴリズム」という用語の概略を説明します。
「データの操作」とは、ユーザーがコンピュータを用いてデータに何らかの働きかけを行い望む結果を得ようとする行為を指します。たとえばランダムに入力された買い物の記録を日付順に並べ替えたり、入力済みの家計簿のデータの中から特定の日付の項目だけを取り出したり、といった操作がそれに当たります。そもそもコンピュータはデータを処理するための「計算機」であり、こういったデータの操作はコンピュータ利用において最も広汎に見られる用途であるといえます。
今回はデータの操作の中でも最も基本的なデータの整列(ある定められた秩序にしたがってデータ群を並べること)とデータの検索(データ群の中から特定のデータを探し出すこと)を扱います。これらの操作は、コンピュータを用いた問題解決の中に普遍的に見られます。また、Excelというアプリケーション利用においても最も頻繁に行われる操作です。
問題を解くための手順のことを一般にアルゴリズム(algorithm、算法)と呼びます。プログラムとはアルゴリズムをプログラミング言語によって表現したものだといえます。より効率のいいアルゴリズムを作成することは、プログラマーにとって重要なことです。なぜならば、アルゴリズムの組み方次第で、同じ問題解決にかかる時間が大幅に変わるからです(特にデータの件数が多いときはその差が顕著です)。特にデータの整列やデータの検索など、コンピュータ利用において頻繁に行う操作には、必ずアルゴリズムの「定石」があります。アルゴリズムの工夫は、まずはこの定石を学ぶところから始まります。今回はその定石のいくつかを学習します。
前半ではデータの整列の問題を扱います。データの整列の最も基本的なアルゴリズムを3つ紹介します。また、Excel VBAに用意されたSortメソッド(データの整列を行う機能)を利用したプログラムの書き方も学習します。
データの整列(ソートあるいはソーティングと呼ぶこともある)とは、ある秩序にしたがってデータ群を(再)配置する操作をさします。たとえば、以下の得点表を得点の高い順に並べ替える場合、表に並ぶ「得点(データ群)」を「得点の高い順(秩序)」に従って整列する、すなわちデータの整列の問題であると考えます。このときデータを小さいものから順に並べることを「昇順」、大きいものから順に並べることを「降順」といいます。
はじめに、最も単純な整列のアルゴリズムを学習します。たとえば、次のようにA2からA7まで並んだ6個の数値を昇順(小さい順)に整列するプログラムを考えましょう(practice06.xls, Sheet1)。
データ列の先頭はA2です。したがって、A2に入るべき数値はデータ列の中で最も小さい数値です。A3にはその次に小さい数値が入ります。そのあとA4, A5…と続き、A7が最も大きい数値となります。ちなみに、A7の行数である「7行目」は、データの個数(6個)に1を加えた値です。
そこで、まずA2に入る数値を確定し、その次にA3に入る数値を確定し、その次にA4…という順序で処理を行うプログラムを考えましょう。ここで仮に、確定の対象となるセルの行をpivot、データの個数をnという変数(整数型)で表すとします。すると、以下のような処理手順を考えることができます。
以下は、この考え方に基づいて作成したプログラム(一部省略)です。
Sub 最も単純なソート() '最も単純なアルゴリズムによる昇順ソート Dim pivot As Integer '最新のソート対象行 Dim i As Integer '繰り返しのカウンタ Dim n As Integer 'データの個数 Dim tmp As Integer '入れ替え時に一時的にデータを格納する変数 'データの個数を設定する n = 6 '昇順にデータを整列する For pivot = 2 To n 'ソート対象行を最上位行から最終行の手前まで進める '未走査のデータ列(ソート対象行より1行下から最終行まで)を走査して、 '最小値をソート対象行のセルに書き込む For i = pivot + 1 To n + 1 '現在のソート対象行の値よりも小さな値があれば値同士を入れ替える If Cells(pivot, 1).Value > Cells(i, 1).Value Then ・・・Cells(pivot, 1)の値とCells(i, 1)の値の入れ替え・・・ End If Next i Next pivot End Sub
「Cells(pivot, 1)の値とCells(i, 1)の値の入れ替え」の部分を補って上記のプログラムを完成させましょう。ただし変数tmpを用いることを条件とします。(practice06.xls, Sheet1; D列のデータはコピー用です)
次に、もう少しだけ効率の良い整列のアルゴリズムを学習します。2.2のアルゴリズムでは、pivotで表されるソート対象行の値よりも小さな値が発見されるたびにデータの入れ替えを行っていました。しかし、pivot以下の行のデータと繰り返し比較を行う際に「その時点での最小値が格納されるセルの行番号」を記憶する変数を用意し、より小さな値が発見されたらその都度その行番号を上書きしておけば、比較を終えた際にはpivot以下で最小値のある行番号を取得することができます。すべての比較を終えた後で、その行番号の値とpivotの値を1度だけ入れ替えれば、毎回小さな値を発見するたびに入れ替えを行わずに済みます。このアルゴリズムは「選択ソート」と呼ばれます。
以下は選択ソートによるソースコードです(一部抜粋)。その時点での最小値が格納されるセルの行番号」を記憶する変数は、minRowという変数名で用意されています。
Sub 選択ソート() '選択ソートによる昇順ソート Dim minRow As Integer 'その時点での最小値が格納されるセルの行番号 Dim pivot As Integer '最新のソート対象行 Dim i As Integer '繰り返しのカウンタ Dim n As Integer 'データの個数 Dim tmp As Integer '入れ替え時に一時的にデータを格納する変数 'データの個数を設定する n = 6 '昇順にデータを整列する For pivot = 2 To n 'ソート対象行を最上位行から最終行の手前まで進める '最小値格納行を初期化する minRow = pivot '未走査のデータ列(ソート対象行より1行下から最終行まで)を走査して、 '最小値が格納されている行番号を取得する For i = pivot + 1 To n + 1 '現在の最小値格納行の値よりも小さな値があれば、 'その値の格納される行番号を最小値格納行に書き込む If Cells(minRow, 1).Value > Cells(i, 1).Value Then minRow = i End If Next i '取得された最小値格納行にある値を現在のソート対象行の値と入れ替える ・・・ソースコードは省略・・・ Next pivot End Sub
省略部を補って上記のソースコードを完成させましょう。(practice06.xls, Sheet1)
選択ソートのアルゴリズムを使って、Sheet2のA, B列の名簿を得点の高い順(降順)に整列するプログラムを作成しましょう。(practice06.xls, Sheet2 ; E, F列のデータはコピー用です)
選択ソートよりも場合によっては効率的な整列アルゴリズムに挿入ソートがあります。挿入ソートは以下の図のように、対象とする値(青で示されたもの)が入るべき位置(ワークシート上では行)を、それより前の整列済みのデータ列の中から探し出して挿入する方法です。挿入位置を探す作業は、対象となる値(青で示されたもの、以下「対象値」と呼ぶ)をループの進行によって1つずつ(ワークシート上のデータ列ならば1行ずつ)末尾側に下げながら行います(つまり、ループ1巡目はデータ列の第2行目の値が対象値、以下、2巡目は第3行目の値、3巡目は第4行目の値…と進む)。
整列済みデータ列の中から対象値が入るべき位置(行)を探すやり方は次の通りです。
判明した「対象値が入る位置(すなわち行)」に対して、現在のその位置及びそれよりも後ろの位置に格納された値を1つずつ末尾側にコピーして下げてゆく(図の左側にある下に向かう矢印)ことで、対象値が挿入される位置を確保する点がポイントです。
この方法は、特にデータがほとんど正しい順に並んでいる場合、処理の回数が少なくなる傾向があります(データ挿入位置を確保するために整列済みデータ列を後ろへ下げる作業が減るため)。ここから、場合によって選択ソートよりもより高速に整列を行うことが可能であると言えます。
以下はSheet1の6つのデータの整列を想定した挿入ソートのソースコードです。
Sub 挿入ソート() '挿入ソートによる昇順ソート Dim n As Integer 'データの個数 Dim key As Integer '対象値 Dim keyR As Integer '対象値の行番号 Dim cmpR As Integer '比較値の行番号 'データの個数を設定する n = 6 '昇順にデータを整列する For keyR = 3 To n + 1 '対象値の行をデータ列の2行目から最終行まで進める '対象値を取得する key = Cells(keyR, 1).Value '対象値を挿入すべき行が見つかるまで、 '整列済みデータ列を1行ずつ後退させる cmpR = keyR - 1 '整列済みデータ列の最終行(対象値の直前行)を設定する While cmpR > 1 And Cells(cmpR, 1).Value > key '整列済みデータ列から、対象値よりも '小さい比較値が見つかるまでデータを後退させ続ける Cells(cmpR + 1, 1).Value = Cells(cmpR, 1).Value 'データを1行後退させる cmpR = cmpR - 1 '比較する行を1行前にずらす Wend '見いだされた行に対象値を挿入する Cells(cmpR + 1, 1).Value = key Next keyR End Sub
上記のプログラム中の「While〜Wend」で囲まれた部分は、「Whileステートメント」による繰り返しの対象となる部分です。Whileステートメントでは、あらかじめ設定した終了条件(上記の場合は「cmpR > 1 And Cells(cmpR, 1).Value > key」)が不成立になるまで繰り返しを行います。Andでつながれた終了条件のうち、「cmpR > 1」は検索対象となる整列済みデータ列が残っていることを確かめるものです(cmpRが2のときに整列済みデータ列の先頭行を指す。cmpが1になったらデータ列からはみ出てしまう)。後半の「Cells(cmpR, 1)value > key」は、cmpR行に格納された比較値がkeyの値(すなわち対象値)よりも大きいことを確かめるものです。この双方の条件が成立し続ける間は「keyに格納された対象値よりも小さい比較値が見つかっていない」ことを表します。したがって、「cmpR = cmpR - 1」(すなわち比較行の1行先頭側への移動)を実行して再び同様の処理を繰り返します(「cmpR = cmpR - 1」が欠けてしまうと、このWhileループは終了しません)。
挿入ソートのアルゴリズムを使って、Sheet2のA, B列の名簿を得点の高い順(降順)に整列するプログラムを作成しましょう。(practice06.xls, Sheet2)
Excel VBAにおいては、プログラムの命令による操作対象を「オブジェクト」と呼びます。たとえばワークシート上の単独のセルまたはセル範囲はVBAにおいてはRangeオブジェクトと呼ばれます。オブジェクトには、そのオブジェクトの保持する値に対して操作を行うための機能があらかじめ命令のような形で用意されています。その「機能」のことを「メソッド」と呼びます。
ワークシート上のセル範囲を表すRangeオブジェクトには、セル範囲において保持する値(Valueプロパティでアクセスされる)を整列するSortメソッドが用意されています。Sortメソッドを用いると、簡便に整列を行うプログラムを書くことができます。たとえば、Sheet2の名簿を得点の高い順に整列する場合、以下のようなソースコードを書きます。
Sub 得点表の降順ソート3() 'Sortメソッドによる降順ソート Dim n As Integer 'データの個数 'データの個数を設定する n = 26 Range(Cells(1, 1), Cells(n + 1, 2)).Sort Key1:=Cells(1, 2), Order1:=xlDescending End Sub
Sortメソッドは、整列対象となるデータが格納されたセル範囲を表すRangeオブジェクトを通して利用します。「Range(Cells(1, 1), Cells(n+1, 2))」はセル範囲の端から端をCellsオブジェクトによって表すことで、表の書かれたセル範囲を特定します。このようにセル範囲が特定されたRangeオブジェクトに対して、ピリオド「.」を介して直接Sortメソッドを記述します。「Key1:=Cells(1, 2), Order1:=xlDescending」は、Sortメソッドに与えられる2つの引数です。Key1は整列キー、すなわち整列の際に優先される項目(列)の先頭行を表します。Order1は昇順か降順かを設定します。昇順ならばxlAscendingを、降順ならばxlDescendingを記述します。
引き続き、データの検索の問題を扱います。基本的な2つのアルゴリズムを学習します。さらに、Findメソッドを用いた検索プログラムの書き方も学習します。
はじめに、最も単純な検索アルゴリズムである線形探索を学習します。線形探索とは、簡単に言えば、与えられた検索キー(探したい値)と一致する値を、データ列の先頭から末尾まで順次検索するというやり方です。データ列を「線」のように先頭から末尾に向けてくまなく検索するためにこの名がついています。
たとえば、ワークシート上のA2から一列に並んだn個のデータに対して、変数keyに格納された値(検索キー)と同じ値を持つセルを探して、発見されたらそのセルを選択状態(マウスでワンクリックしたのと同じ状態)にするプログラムは次のように書きます(一部抜粋)。
'データ列を先頭から順に走査して、検索キーに一致する値の '格納されたセルを選択してプログラムを終了する For i = 2 To n + 1 If Cells(i, 1).Value = key Then Cells(i, 1).Select Exit Sub End If Next i
変数iで2行目から1行ずつずらしながら、データ列の末尾の行(n+1行目)までForループで検索を進めます。その間に、「If Cells(i, 1).Value = key Then」でkeyと等しい値が見つかったら、そのセル(Cells(i, 1))を選択状態にします(上記ではSelectメソッドを用いています)。また、値が見つかった段階でプログラムを終了するために、すべての処理を停止する「Exit Sub」という命令を記述しておきます。
keyの値を設定する方法として、今回はダイアログボックスを用いてユーザーから直接値を受け取る方法を用います。そのために、ここではInputBoxという関数を用います。
'ユーザーから検索キーを取得する keyStr = Application.InputBox("検索キーを入力してください") 'InputBoxを表示して検索キーの値を取得する If keyStr = "False" Then 'キャンセルの場合はプログラムを終了する MsgBox ("キャンセルされました") Exit Sub End If key = Val(keyStr) '検索キーを整数型へ変換する
InputBox関数を用いると、ユーザーに対して以下のような入力欄があるダイアログボックスを示して値を受け取ることができます。受け取った値は文字列型のデータで返り値として渡されます。ですから、上記のプログラムの変数keyStrは文字列型で宣言されていることが前提です。それをValという関数で整数型(Integer)に変換し、新たにInteger型の変数keyで受け取ります。
また、ユーザーに示されるダイアログボックスにはキャンセルボタンがあります。仮にユーザーがキャンセルボタンを押した場合は、返り値にFalseという文字列が与えられます。そこで、IFステートメントでFalseか否かを検出し、Falseだった場合にはMsgBox関数(これは入力欄のないダイアログボックスで、メッセージの表示だけが行われる)でキャンセルされたことを確認し、プログラムを終了します。
仮に検索の結果として値が見つからなかった場合、その旨をユーザーに知らせるのが親切です。そこで、MsgBoxと文字列の連結(&)を用いてプログラムの末尾に以下のような一行を加えておくと、ユーザーに対して値が見つからなかったときだけ「〜はありませんでした」というメッセージを表示することができます。
'値が見つからない場合はMsgBoxでコメントを出力する MsgBox key & "はありませんでした"
上記の説明をよく読んだ上で、線形探索によってSheet3にあるデータ列から値を検索するプログラムを作成しなさい。(practice06.xls, Sheet3)
次に、ある前提が満たされている場合にのみ適用でき、その際には線形探索よりも効率の良い検索を行うことができる二分探索というアルゴリズムを紹介します。ある前提とは、検索対象のデータが昇順または降順に整列されていることです。
二分探索のアルゴリズムでは、データが整列されていることを前提に、検索範囲のデータ列の中央値をチェックします。仮にデータが昇順で整列されている場合、中央値が検索したい値(検索キー)よりも小さいならば、それより前のすべてのデータ列は自動的に検索キーよりも小さい値であることが確定します。そこで、改めて検索範囲を中央値よりも後ろの列のみに絞り込みます(つまり、ここで検索範囲を二分して後半の半分だけに絞り込むことになります)。絞り込んだ新しい検索範囲に対しても同様のことを繰り返します。そうすると、もしもデータ列に検索キーと同じ値が存在するならば、いずれかの時点でそれが発見されます。
検索範囲を2分割することを繰り返すというやり方は、直感的には理解しやすいかもしれません。しかしプログラムにする際には、検索範囲の全体像と中央地点を知るための工夫を施すなど、少々やっかいな部分があります。ここでは上限、下限、中央を、それぞれセルの行番号で把握するとして、以下のようなプログラムを書きます(検索対象のデータはSheet4に掲載)。
Sub 二分探索() 'プロシージャ名に注意。「2分探索」は数値から始まるのでNG Dim high As Integer '検索範囲の上限(行番号) Dim mid As Integer '検索範囲の中央(行番号) Dim low As Integer '検索範囲の下限(行番号) Dim keyStr As String '検索キー(InputBoxから取得した文字列) Dim key As Integer '検索キー(整数化したもの) Dim n As Integer 'データの個数 Dim i As Integer 'データの個数を設定する n = 8 'ユーザーから検索キーを取得する ・・・ソースコードは省略・・・ 'データの検索範囲を設定する high = n + 1 low = 2 '検索範囲を二分しながら検索キーと一致する値を探し、 '発見したらそれが格納されたセルを選択してプログラムを終了する While low <= high '検索範囲の中央を算出する mid = (high + low) / 2 '値を発見したらセルを選択してプログラムを終了、 'そうでなければ検索範囲の下限または上限を修正する If Cells(mid, 1).Value = key Then Cells(mid, 1).Select Exit Sub ElseIf Cells(mid, 1).Value < key Then low = mid + 1 Else high = mid - 1 End If Wend '値が見つからない場合はMsgBoxでコメントを出力する ・・・ソースコードは省略・・・ End Sub
上記のプログラムでは、検索範囲の上限、下限、中央を変数high、low、midで保持します。その上で、検索範囲をlowは2行目(これはSheet4の条件から)、highをデータの個数n+1(検索範囲は2行目から始まるため)と設定します。さらにWhileステートメントでlowがhighを超えないこと(超えた場合、検索範囲の消滅を意味します)を条件にループを構成します。
二分探索の肝心な点はこのWhileループの中にあります。ループの内部は「検索範囲の中央(を示す行)の算出→中央のセルをチェックして<1.データが発見された場合><2. keyが中央値よりも大きい場合><3. keyが中央値よりも小さい場合>のいずれかを実行」となっています。<1.データが発見された場合>の場合にはそのまま発見したセルを選択して、プログラムを終了します。<2. ><3. >の場合には、それぞれ検索範囲を二分するために、midの値を手がかりにlowまたはhighの値を設定し直します。
上記を参考に、Sheet4のデータについて検索を行うプログラムを完成させましょう。(practice06.xls, Sheet4)
Sheet5に掲載された名簿について以下のプログラムを作成しましょう。(practice06.xls, Sheet5)
最後に、参考までにRangeオブジェクトに用意されたFindメソッドを用いた検索プログラムを示しておきます。このプログラムはA〜D列にデータが並んだ場合(すなわちSheet5)に適用することを念頭に置いて作られています。
Sub Findメソッドによる検索() 'Findメソッドを用いて数値の検索を行う Dim key As String '検索キー Dim rng As Range '検索対象のセル範囲 'ユーザーから検索キーを取得する key = Application.InputBox("検索キーを入力してください") 'InputBoxを表示して検索キーの値を取得する If key = "False" Then 'キャンセルの場合はプログラムを終了する MsgBox ("キャンセルされました") Exit Sub End If '検索キーに一致する値を検索する Set rng = Range("A:D").Find(What:=key, LookAt:=xlWhole) '検索範囲と検索条件を与えて検索を実行する If rng Is Nothing Then '値が見つからない場合はMsgBoxでコメントを出力する MsgBox key & "はありませんでした" Exit Sub End If '検索キーに一致する値の格納されたセルを選択する rng.Select End Sub