スポンサーリンク
※サイト運営にサーバーは必須です※
~このサイトもエックスサーバーを使用しています~
はじめに
この記事では、Visual Studio2015でダブりのない乱数をc#で作る方法を紹介する。
ついでに、生成した乱数をテキストファイルに出力する機能もついている
考え方
乱数のシャッフルのされ方には歴史がある。
Wiki:フィッシャー – イェーツのシャッフル
は非常に参考になる。
Wikiでは、フィッシャー – イェーツ(Fisher-Yate)とダステンフェルドのアルゴリズムを区別して扱っている。フィッシャー – イェーツ(Fisher-Yate)が当初考えた方法だと計算量がO(n2)だが、ダステンフェルドのアルゴリズムでは、O(n)と少なく済む。
※多くのサイトで、ダステンフェルドのアルゴリズムのことをFisher-Yate 法と呼んでいる。理由はよくわからないが、偏りのない乱数をまじめに考えた最初期の人がフィッシャーとイェーツだからだろう。
この記事でも、これらの考えを踏まえてコードを組んだ。
まず、1~10までの重複のない乱数を作りたいと考える。
その場合、10個の数字を入れる箱(配列)を用意する。そして、1番目の箱に1を、2番目の箱には2を……10番目の箱には10を入れる。
次に、1番目の箱を1~10番目の箱のどれかと交換する(自分と交換した場合、数字は変わらない)。その次は2番目の箱を2~10番目の箱のどれかと交換する。……9番目の箱は9~10番目の箱のどれかと交換する。
※10番目の箱は、交換操作をしても、自分と交換するしかないので意味がない。そのため、この最後の交換操作は省略されているコードも世の中にはある。このサイトで紹介するコードは、交換操作の後に、交換して得た数字を記録している。その関係上、最後の(無意味な)交換操作も行っている。
この手の手法を紹介しているサイトの多くは、1番目でなく10番目からスタートしていることが多い。つまり、配列の末尾に交換操作が終わった数字を後ろに退避している。(最初にこの手法を考えた人は、先頭でなく末尾からスタートして考えたのだろうか?)。
本質的には、先頭から考えても、末尾から考えても結果は同じである。末尾から考えると、発生させる乱数の最大の数字と、残っている配列の数が一致するので、数字の管理が楽になるというメリットがある。
※このサイトでは、先頭から考えている。そのため、乱数生成の部分は、
1 |
int r = i + rnd.Next(r_num - i); // iからr_num-1の乱数を取得 |
と書いている。
末尾から考えると、
1 |
int r = rnd.Next(i); |
とすっきり書けるようになる。
先頭から考えているか末尾から考えているかは、交換操作を行っているループ文(for文やwhile文など)を制御している変数(例えばi)にインクリメント演算子(i++)がついているかデクリメント演算子(i--)が付いているかで判断できる。インクリメントの場合は先頭から考えており、デクリメントの場合は末尾から考えている。
このサイトのコードは、先頭から交換して考えている。末尾から考えると、交換操作の後に、交換して得た数字を記録する際に、少し面倒(r_arr[i]の部分をr_arr[r_num-i-1]にしないといけない)。
また、このサイトで紹介するコードを少しいじくれば、指定した数の乱数を得ることができる。例えば、1~10000までの乱数を考えるが、必要な乱数は最初の10個だけという場合に、交換操作を途中で打ち切れるようになっている (変数r_triで指定できる) 。末尾から考えると、この打ち切り操作を指定する部分で少し頭がこんがらがる。
コード
まず、Visual Studio 2015を立ち上げる。
左上にある「ファイル」→「新規作成」→「プロジェクト」から「コンソールアプリケーション」を選択。
名前は好きに指定(ここでは「r_test」としている)
ソースコードは以下のようになる。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 |
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.IO;//ファイルの出力をするために必要 namespace r_test { class Program { static void Main(string[] args) { int r_max = 100;//発生させる乱数の最大値指定 int r_min = 15;//発生させる乱数の最小値指定 int r_num = r_max - r_min + 1; int r_tri = r_num;//(trial)生成する乱数の数(r_num以下を指定) int[] r_arr = new int[r_num]; //string file_name = "test1.txt";//ファイル名を宣言 string file_name = "min" + Convert.ToString(r_min) +"_" + "max" + Convert.ToString(r_max)+"_"+ "trial"+ Convert.ToString(r_tri)+".txt";//ファイル名を宣言 FileStream fs = new FileStream(file_name, FileMode.Create);//上書き保存する StreamWriter sw = new StreamWriter(fs); //初期条件 sw.Write("//[初期条件]\t最小値:{0}\t 最大値:{1}\t 乱数を発生させる回数:{2}\r\n", r_min, r_max, r_tri); for (int k = 0; k < r_num; k++) { r_arr[k] = r_min + k; //配列がきちんと初期化できているか確認 //sw.Write(" {0} 回目:{1} \r\n", k + 1, r_arr[k]); } int seed = Environment.TickCount; Random rnd = new System.Random();// インスタンスを生成 for (int i = 0; i < r_tri; i++) { int r = i + rnd.Next(r_num - i); // iからr_num-1の乱数を取得 int temp = r_arr[i]; r_arr[i] = r_arr[r]; r_arr[r] = temp; sw.Write(" {0} 回目:{1} \r\n", i + 1, r_arr[i]); } sw.Close();//ファイルを閉じる } } } |
1 |
using System.IO;//ファイルの出力をするために必要 |
は、ファイルの出力に必要。
発生させる乱数の最大値の指定、
1 |
int r_max = 100;//発生させる乱数の最大値指定 |
発生させる乱数の最小値の指定、
1 |
int r_min = 15;//発生させる乱数の最小値指定 |
用意する箱(配列の要素)の数
1 |
int r_num = r_max - r_min + 1; |
+1される理由は説明しなくても問題ないかな?(例えば、10~20の間に含まれる数字は10個でなく11個)
生成する乱数の数。途中で交換操作を打ち切って問題ない場合に指定。
1 |
int r_tri = r_num;//(trial)生成する乱数の数(r_num以下を指定) |
配列の宣言
1 |
int[] r_arr = new int[r_num]; |
1 2 3 4 5 |
string file_name = "min" + Convert.ToString(r_min) +"_" + "max" + Convert.ToString(r_max)+"_"+ "trial"+ Convert.ToString(r_tri)+".txt";//ファイル名を宣言 FileStream fs = new FileStream(file_name, FileMode.Create);//上書き保存する StreamWriter sw = new StreamWriter(fs); |
ファイルの名前は条件に合わせて自動生成される。(例えば、min15_max100_trial86.txtと生成される)
1 2 3 |
//初期条件 sw.Write("//[初期条件]\t最小値:{0}\t 最大値:{1}\t 乱数を発生させる回数:{2}\r\n", r_min, r_max, r_tri); |
で初期条件をテキストファイルに記録。
1 2 3 4 5 6 |
for (int k = 0; k < r_num; k++) { r_arr[k] = r_min + k; //配列がきちんと初期化できているか確認 //sw.Write(" {0} 回目:{1} \r\n", k + 1, r_arr[k]); } |
で、用意した配列に数字を入れている。
乱数生成の準備
1 2 |
int seed = Environment.TickCount; Random rnd = new System.Random(); |
1 2 3 4 5 6 7 8 |
for (int i = 0; i < r_tri; i++) { int r = i + rnd.Next(r_num - i); // iからr_num-1の乱数を取得 int temp = r_arr[i]; r_arr[i] = r_arr[r]; r_arr[r] = temp; sw.Write(" {0} 回目:{1} \r\n", i + 1, r_arr[i]); } |
箱の交換操作(シャッフル)に対応。int temp = r_arr[i];で交換操作前のi番目の箱に入っている数字を覚えている。r_arr[i] = r_arr[r];でi番目とr番目の箱を交換。r_arr[r] = temp;でi番目の箱に入っていた数字をr番目の箱の箱に入れている。
最後にファイルを閉じておしまい。
1 |
sw.Close();//ファイルを閉じる |
このコードを実行するには「Ctrl+F5」を押す (ここではDebugモードで行っている) 。
コマンドプロンプト立ち上がるが、エンターを押す。
コマンドプロンプトが消えたら、「保存したファイル(ここではr_terst)」→「保存したファイル(ここではr_terst)」→「bin」→「Debug」の中にテキストファイルが生成される。
(紹介しているコードをそのまま実行するとmin15_max100_trial86.txt)
テキストファイルの中身はこんな感じになる。
~Webサイトを自分で作ってみませんか?~
Webサイトを運営するにはサーバーが必須です。
このサイトは、エックスサーバー のサーバーを使用しています。
エックスサーバーは無料で10日間お試しができます。
コメント