🎄teratailクリスマスプレゼントキャンペーン2024🎄』開催中!

\teratail特別グッズやAmazonギフトカード最大2,000円分が当たる!/

詳細はこちら
Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

Q&A

解決済

7回答

10374閲覧

重複しない乱数の発生方法(Collections.shuffleの処理速度)

luckyclock

総合スコア74

Java

Javaは、1995年にサン・マイクロシステムズが開発したプログラミング言語です。表記法はC言語に似ていますが、既存のプログラミング言語の短所を踏まえていちから設計されており、最初からオブジェクト指向性を備えてデザインされています。セキュリティ面が強力であることや、ネットワーク環境での利用に向いていることが特徴です。Javaで作られたソフトウェアは基本的にいかなるプラットフォームでも作動します。

0グッド

2クリップ

投稿2016/03/18 11:14

編集2016/03/18 11:42

リストをシャッフルする
Collections.shuffle
というメソッドがありますが、かなり大量のリスト(1000以上)をシャッフルするとなるとやはり処理時間がかかるでしょうか?
実際はリスト全部シャッフルする必要はなく、ランダムで重複しないリストの要素30個程度が取り出せればいいのですが、処理速度を短くしようとした場合いいやり方が思いつきません。
いいやり方はないでしょうか?

現在下記のようなコードでシャッフルしていますが、これだとbufnumが大きくなるほどループ数が大きくなり、
さらにbufnumが大きいほどshuffle回数も大きくしないとランダムになりません。

java

1public class Shuffle { 2 3 public int bufnum; //用意するバッファ数(シャッフルする対象数) 4 public int[] tor; //シャッフル後の数値 5 int shuffleNum = 0; //シャッフル回数 6 7 public Shuffle() 8 { 9 } 10 11 public Shuffle(int num, int snum) 12 { 13 bufnum = num; 14 tor = new int[num]; 15 shuffleNum = snum; 16 } 17 18 public void ShuffleNumber(){ 19 int buff; 20 int ran1; 21 int ran2; 22 23 if(bufnum == 0) 24 return; 25 26 Random rnd1 = new Random(); 27 Random rnd2 = new Random(); 28 29 //数字を配置 30 for(int i = 1; i <= bufnum; i++){ 31 tor[i - 1] = i - 1; 32 } 33 34 //シャッフル 35 for(int i = 1;i <= shuffleNum;i++){ 36 37 //乱数を作成 38 ran1 = rnd1.nextInt(bufnum); 39 ran2 = rnd2.nextInt(bufnum); 40 41 //配列の入れ替え(乱数1と乱数2を入れ替え) 42 buff = tor[ran1]; 43 tor[ran1] = tor[ran2]; 44 tor[ran2] = buff; 45 } 46 } 47}

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

lib

2016/03/18 11:23

試しに短めのコードを書いてパターンで検証してみてはいかがでしょうか?もしくはすでに検証されたコードと結果をおもちでしたらその内容と開発環境も合わせてご提示いただけますか?
piyoon

2016/03/18 11:25

現在どのように実装されているかを書いて下さい。 処理速度を短くしたいとのことですが、現在がどのように組んでいるかわからないので比較したものを応えることが出来ません(現在組まれているものがベストではないとも限りませんから)。
piyoon

2016/03/18 11:26

あ、被ってしまいましたm(_@_)m
luckyclock

2016/03/18 11:42

記事修正しました
guest

回答7

0

質問文の情報から言えるのは、質問者さんが求めているのは「ランダムサンプリング」であって、「シャッフル」ではありません。「ランダムサンプリング」が高速にできればシャッフルだろうとワッフルだろうとどちらでもかまわないですよね?

Reservoir samplingを強くお勧めします。私が営業なら「お客さんにピッタリですよ」と言うでしょう。

投稿2016/03/18 12:39

sharow

総合スコア1151

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

luckyclock

2016/03/18 13:00

まさにそのとおりのようですが、リンク先のプログラムを理解できませんでした。
YsMana

2016/03/18 13:58

面白いアルゴリズムですね。 要素数が不明なときや、シャッフル対象へのランダムアクセスが高コストな時に有用そうです。 ただ、処理量が(サンプリング数ではなく)要素数に比例するので、 この条件を満たさないときはFisher–Yatesのシャッフルを途中で止めるやり方の方が良さそうですね。 フィッシャー - イェーツのシャッフル - Wikipedia https://ja.wikipedia.org/wiki/%E3%83%95%E3%82%A3%E3%83%83%E3%82%B7%E3%83%A3%E3%83%BC_-_%E3%82%A4%E3%82%A7%E3%83%BC%E3%83%84%E3%81%AE%E3%82%B7%E3%83%A3%E3%83%83%E3%83%95%E3%83%AB
ozwk

2016/03/18 14:09

フィッシャー - イェーツを途中で止めたら分布に偏り出ると思うんですが。
YsMana

2016/03/18 14:20

途中で止めてシャッフルが終わった部分だけ使います。
sharow

2016/03/18 14:53

そう言われるとReservoir samplingはオーバースペック(?)な気もしてきました。メモリに乗るならシャッフル(and途中で中断)しちゃった方が楽ですね。
ozwk

2016/03/18 23:02

> 途中で止めてシャッフルが終わった部分だけ使います。 「シャッフルが終わった部分」とは何処のことですか?
swordone

2016/03/19 00:27

「取り出し」アルゴリズムの場合、取り出した要素が配列の末尾に集まるのでそれを使うということでは? 仮にその後処理が続いたとしてもその部分は触れられないので、部分的にシャッフルが完了していることになります。
ozwk

2016/03/19 03:47

そうですね 長々と失礼しました
guest

0

ベストアンサー

Java

1//シャッフル 2for(int i = 1;i <= shuffleNum;i++){ 3 4 //乱数を作成 5 ran1 = rnd1.nextInt(bufnum); 6 ran2 = rnd2.nextInt(bufnum); 7 8 //配列の入れ替え(乱数1と乱数2を入れ替え) 9 buff = tor[ran1]; 10 tor[ran1] = tor[ran2]; 11 tor[ran2] = buff; 12}

この入れ替え方では何度繰り返してもまともにシャッフルできません。

Java

1<MyData>moto = new ArrayList<MyData>(); //抽出元のデータ 2<MyData>select = new ArrayList<MyData>(); //ランダム抽出したデータ格納先 3for(int i = 0; i < 30; i++){ 4 int ran = rnd.nextInt(moto.size()); 5 select.add(moto.get(ran)); 6}

こちらはgetした要素をちゃんと削除すれば大丈夫そうですね。

ランダムで欲しい要素数が、リストの一部と言うことなので
途中までシャッフルするコードを書いてみました。
(テストは20個中10個取得。)

Java

1import java.util.Random; 2 3class Shuffle { 4 5 private int list_len; //listの長さ 6 private int[] list; //シャッフル前 7 8 public Shuffle() 9 { 10 } 11 12 public void SetList(int num) 13 { 14 //数字を配置 15 list = new int[num]; 16 list_len = num ; 17 for(int i = 0; i < num; i++){ 18 list[i] = i; 19 } 20 } 21 22 public int[] GetShuffleList(int snum){ //snum:シャッフルして得たい要素数 23 int pick; 24 int[] s_list; 25 Random rnd = new Random(); 26 s_list = new int[snum]; 27 28 //エラーチェック省略 29 for(int i = 0;i < snum;i++){ 30 pick = rnd.nextInt(list_len); 31 s_list[i] = list[pick]; //ランダムで要素を取得、s_list[i]に格納 32 list[pick] = list[list_len-1]; //最後尾の要素を長さを減らす前に持ってくる(list[pick]はもういらないので捨てる) 33 //list[pick]を捨てたくない場合は、list[pick]とlist[list_len-1]を入れ替えば要素は消えない 34 list_len--; //取り出したので長さを減らす(減らしたと見なす) 35 } 36 return s_list; 37 } 38} 39 40class test { 41 public static void main(String args[]){ 42 Shuffle s1 = new Shuffle(); 43 44 s1.SetList(20); 45 int[] s_list = s1.GetShuffleList(10); 46 for(int i = 0;i < s_list.length;i++){ 47 System.out.println(s_list[i]); 48 } 49 } 50}

1億個から30個欲しい様な場合は連想配列(疎な配列が実現できるなら何でもいいです)を使って、
初期状態空、キーに対応する要素が存在しない場合はキーと同じ要素が入っていると見なして同じように部分シャッフルをかければできそうな気がします。

#追記
HashMapを使った広範囲からのランダム取得が可能なコードを書いてみました。
重複時のリトライがないので、1000個中1000個とかでもたぶん安定した性能になると思います。

Java

1import java.util.Random; 2import java.util.HashMap; 3 4class Shuffle { 5 6 private int list_len; 7 private HashMap<Integer,Integer> list; 8 9 public Shuffle() 10 { 11 } 12 13 public void SetList(int num) 14 { 15 list = new HashMap<Integer,Integer>(); 16 list_len = num ; 17 } 18 19 public int[] GetShuffleList(int snum){ 20 int pick; 21 int[] s_list; 22 Random rnd = new Random(); 23 s_list = new int[snum]; 24 25 for(int i = 0;i < snum;i++){ 26 pick = rnd.nextInt(list_len); 27 s_list[i] = list.containsKey(pick) ? list.get(pick) : pick; //ランダムで要素を取得 28 list_len--; //取り出したので長さを減らす 29 list.put(pick, list.containsKey(list_len) ? list.get(list_len) : list_len ); //最後尾の要素を持ってくる(list[pick]はもういらない) 30 } 31 return s_list; 32 } 33} 34 35class test02 { 36 public static void main(String args[]){ 37 Shuffle s1 = new Shuffle(); 38 39 s1.SetList(20); 40 int[] s_list = s1.GetShuffleList(10); 41 System.out.println("20 => 10"); 42 for(int i = 0;i < s_list.length;i++){ 43 System.out.println(s_list[i]); 44 } 45 46 s1.SetList(100000000); 47 s_list = s1.GetShuffleList(10); 48 System.out.println("100000000 => 10"); 49 for(int i = 0;i < s_list.length;i++){ 50 System.out.println(s_list[i]); 51 } 52 } 53}

投稿2016/03/18 13:15

編集2016/03/18 13:40
YsMana

総合スコア257

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

luckyclock

2016/03/18 14:17

非常に参考になりました。 ありがとうございます。
guest

0

速いシャッフルは、forループのカウンターをi(0はじまり)とすると、リストサイズ-i未満の整数の位置の要素と最後からi番目の要素を入れ換える、ということをしているだけです。30個しか要らないのなら、そこまででループをやめればいいでしょう。

java

1// 単純に配列をnum個分だけシャッフルするメソッドにします。適宜工夫してください。 2public int[] shuffle(int num) { 3 int[] nums = new int[1000]; 4 int[] result = new int[num]; 5 for(int i = 0; i < nums.length; i++){ 6 // 配列に1-1000の数値を入れる 7 nums[i] = i + 1; 8 } 9 10 for(int i = 0; i < num; i++){ 11 int pos = (int)(Math.random() * (nums.length - i)) 12 int temp = nums[pos]; 13 nums[pos] = nums[nums.length - i - 1]; 14 nums[nums.length - i - 1] = temp; 15 result[i] = temp; 16 } 17 return result; 18}

この方法のメリットは、1回これをやって一部ランダムなままの配列になっていても、そのままの状態で再び同じことをしても問題ないという点です。ちなみにCollections.shuffleも同じようなことをしています。

投稿2016/03/18 11:32

編集2016/03/18 15:29
swordone

総合スコア20669

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

piyoon

2016/03/18 12:17

全体のインデックス値の入れ替えを行っているため、最初の30回でループをやめると入れ替えの行われていない先頭部分はほぼ通番のままのためランダムとしては使えないと思います。
swordone

2016/03/18 15:16

先頭から順に見て、その見ている場所とそれ以降のランダムな位置と入れ替えているので、先頭にランダムな要素が集まります。
guest

0

やりたいことは、「非復元抽出」と呼ばれるもので、
このワードで調べるとわかりやすいものから
変態的なものまで色々出てきます。

5つぐらい比較しているページ(C++)

投稿2016/03/18 14:22

ozwk

総合スコア13551

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

YsMana

2016/03/18 15:48 編集

よくまとまっているページですね。 そのページでtest_knuthとされているのが、sharowさんの紹介されているReservoir samplingと同じようですね。 あと、アルゴリズムを見直していて思ったのですが、 ・Reservoir samplingは、サンプリングされた集合内の順序は元のまま(組み合わせがランダムに出てくる) ・シャッフルを途中で止める方法は、サンプリングされた集合内の順序はシャッフルされる(順列がランダムに出てくる) なので、目的(サンプリング結果のシャッフルが必要か不要か)によってはこの違いが重要になる可能性がありますね。
guest

0

要するに乱数の最大値が100億とかになっても重複のない乱数列を作りたいというわけですよね。

C++だとSTLのset系を使って作るのですが、Javaだとこんな感じでしょうか。Javaは使ったことがないので正しいコードか自信がありませんが、やり方は伝わりますよね?

java

1// 重複しない30個の乱数列を作成 2Set<int> collection = new Set<int>(); 3while(collection.size() != 30) 4{ 5 collection.add(rnd.nextInt(range)); 6} 7 8// 配列に変換 9int[] tor = new int[30]; 10collection.toArray(tor); 11 12// その配列をシャッフル 13……省略

投稿2016/03/18 13:14

catsforepaw

総合スコア5944

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

要望は次のようなものであっていますか?
1.次のような事がしたい「1~1000の中から重複が無く予測不可能な30種類の数字を抽出」
2.自作のシャッフル関数があるが、効率が悪い気がする
3.Collections.shuffleが要求を満たす手段として有望か知りたい

極端に言うと、数百億から30個とかになった場合、その数百億をシャッフルするのは無駄じゃないの?
ということですよね?多分。

配列のシャッフルから離れて
「配列の要素数までの乱数を作成して、その添え字の数値を取得&配列から削除する」
を30回繰り返したらどうでしょう?

投稿2016/03/18 12:19

編集2016/03/18 12:20
hirohiro

総合スコア2068

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

luckyclock

2016/03/18 12:38

そのとおりです。 たぶん先に回答をくれた方たちも同じようなことだと思うのですが、 中途半端に作ってみましたが、 「配列から削除する」の部分うまいやり方ないでしょうか? 2度目の抜き出しを考えると抽出元のデータから抽出したデータを削除してしまうのはうまくないです。 <MyData>moto = new ArrayList<MyData>(); //抽出元のデータ <MyData>select = new ArrayList<MyData>(); //ランダム抽出したデータ格納先 for(int i = 0; i < 30; i++){   int ran = rnd.nextInt(moto.size()); select.add(moto.get(ran)); }
hirohiro

2016/03/18 13:48 編集

元のリストが作成困難でない場合。(例示のように1~1000など) 出力がほしいときに元配列も作って、抽出が終わったら破棄してしまうのが良いと思います。元のリストが巨大であればあるほど、それをプログラムの開始から終了まで保持するのはメモリの無駄になるためです。 抽出を連続して何回も行うのであれば、配列を複製してそれを使えば良いかも知れません。メモリを倍消費するわけですが、大抵の場合処理後に両方破棄してしまえば問題にならないと思います。 逆に複製して使うとメモリ不足を心配しなければいけないほど巨大な範囲を扱うのであれば、(それは配列1つ保持するだけでも相当な負荷なので)他のアルゴリズムを検討したほうが良いです。 元配列の規模はそうでも無いけど、その配列を作成するのに非常に処理負荷が掛かる場合は、やはり配列を複製して利用してはどうでしょうか?
guest

0

0からリストのサイズ値の範囲から必要数分(30個程度)の乱数配列作成し、その値をインデックスとして利用すると良いかと。
1000以上のリストの中からで30個程度の作成であれば重複チェックを行ってもループから抜けださなくなるほど重複することはないと思います。

投稿2016/03/18 12:08

piyoon

総合スコア68

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.36%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問