配列要素をランダムに入れ替える

プログラミング

配列要素をランダムに入れ替える

 特に専用の関数があるわけではありませんので、Rnd関数を使って自作します。
 Rnd関数については、別ページで説明していますので参考にしてみてください。
 
 アルゴリズム(プログラムの仕組み)については、ランダムで発生した数を基にして、その配列番号の値と入れ替えるという方法が一般的です。

 まずは、Rnd関数のおさらいです。

Rnd関数

 Rnd関数は常套句として覚えると良いでしょう。
 まず、Randomizeで乱数の初期化を行います。
 次にRnd関数を実行するたびに0~1の間の数が出力されるので、それを利用して最小値、最大値の範囲内で値を取得します。
 
 Randomize
 乱数 =  Int((最大値 – 最小値 + 1) * Rnd + 最小値)

 出力される乱数は、常に異なる数が返ってくるわけでは無いという点に注意してください。
 重複させたくない場合はそれなりに工夫が必要になります。

ランダム入れ替えプログラム

 プロシージャは、引数に取った配列要素を入れ替えます。元の配列が変化しますので、注意してください。

プログラムコード

Sub myShuffleList(ByRef myList)
    Dim r As Long, i As Long
    Dim temp
    
    If UBound(myList) < 2 Then Exit Sub
    
    Randomize
    For i = 0 To UBound(myList)
        r = Int((UBound(myList) + 1) * Rnd)
        temp = myList(i)
        myList(i) = myList(r)
        myList(r) = temp
    Next
End Sub

Sub TestCode()
    Dim arr
    arr = Array(1, 2, 3, 4, 5, 6)
    
    Call myShuffleList(arr)
    
    '結果表示
    Dim i As Long
    For i = 0 To UBound(arr)
        Debug.Print arr(i)
    Next i
End Sub

プログラム解説

 今回はプログラムを部品化しています。
 myShuffleList関数に配列を渡すと、その配列要素を入れ替えます。
 ByRef はその引数自体がプロシージャ内で変更された場合、元の値も変化する事を示しています。
 この事を「参照渡し」と言います。

 ByRef は省略可能です。
 (「参照渡し」の反対が値渡し」です。値渡しは ByVal を指定します。元の変数値は変化しません。)

 変数の宣言の後に、配列の要素数が1以下の場合は処理を終えるようにしています。
 (配列の要素数が1つの場合は意味がありませんので,,,)
  If UBound(myList) < 2 Then Exit Sub

 次に、Randomize で乱数の初期化を行っています。乱数を使うための準備ですね。
 今回はForループの外で1回だけ行っています。
 Randomize は時間情報から乱数を作りますので、短時間で何度も初期化を行っても無意味です。
 
 Forループは、0 から配列の最終要素まで繰り返します。
 配列の最終要素数は UBound(arr) を使います。

 次に乱数の発生部分です。
 r = Int((UBound(myList) + 1) * Rnd)
 上記の例の「最小値」は 0 ですので、式が簡単になっています。
 最大値は配列の最大要素数を指定しています。
 最後に Int で整数部分を取り出せば r に乱数値が格納されます。
 
 後は temp 変数を介して、2つの配列の中身を入れ替えていきます。
 
 TestCodeで結果を確認できます。

プログラムの検証

 ランダムな入れ替えとはいえ、変化しない要素になることもあります。
 (それがランダムというものです!)
 完全に他の要素と入れ替えたいのであれば、If文で同じ要素番号かを判断して、やり直すようにする必要があるでしょう。

不具合箇所

 引数がバリアント型になっていますので、オブジェクト型の配列も取ることができます。
 ただ、Set を使って代入していませんので、エラーとなることが予想されます。
 オブジェクト型の配列の時は以下のように変更する必要があります。
  Set temp = myList(i)
  Set myList(i) = myList(r)
  Set myList(r) = temp

 さらに、バリアント変数自体に値が無い場合もエラーとなります。
 引数が配列でない場合などは、NullNothingなど個別の対応になるので厄介です。
 できれば、変数型にあったプログラムを書く方が無難でしょう。

最後に

 今回記事を書くにあたり、色々なサンプルを見てみましたが、Forループを逆順にして乱数を発生させる例がいくつかありました。
 以下のような感じです。

 ’/// 良くない例 ///
    For i = UBound(myList) To 0 Step -1
        r = Int((i + 1) * Rnd)
        Debug.Print r
        temp = myList(i)
        myList(i) = myList(r)
        myList(r) = temp
    Next

 これに関しては r を観察すればわかると思いますが、後半に行くに従い乱数の幅が制限されてしまうため、良い方法とは言えません。
 扱うものが乱数ですので、結果は一意とならないために気づかないのかもしれませんが、ランダムと言っても同じ値が繰り返される事は良くあります。
 以下のプログラムは1~6までの値を出力します。Rnd関数の挙動を確認してみてください。

Sub RndTest()
    Dim n
    Dim i As Long, s As String
    
    Randomize
    For i = 0 To 5
        n = Int(6 * Rnd + 1)
        s = s & n & ","
    Next i
    
    s = Left(s, Len(s) - 1)
    Debug.Print s
End Sub
タイトルとURLをコピーしました