【JavaScript】重複のない乱数配列を作ってみた【ES6(ES2015)】


0.前提

 「俺はJavaScriptで重複なし乱数配列(整数)が作りたいんや!」ってことでJavaScriptで書いてますが、アルゴリズム自体は多言語に応用できます。尚、アルゴリズムそのものは筆者自身がゼロから考えました。もし他の方の考えたアルゴリズムと同じだったとしてもごめんなさい。

1.はじめに

 ES6(ES2015)に対応した環境でないと動作しません。筆者は、Repl.itのJavaScript環境(Node.js v10.16.0)にて動作を確認しています。最新のメジャーブラウザでは動作しますが、IE11以前では動作しません。
 また、筆者が以前書いた記事JavaScript基礎文法を軸にコード書いているので、初心者の方は先にそちらに目を通すこと推奨。

2.アルゴリズム

 『アルゴリズムたいそう~♪』テッテテッテテッテテッテテー チャン♪
 『こっちむいて一人で・・・ってなんでやね~ん』
 はい、これ以上やると叩かれそうなのでやめておきます。
 先にアルゴリズムを説明しておくことにしましょう。コードだけ欲しい人は読み飛ばしてください。目標とするのは、重複なしの乱数配列、例えば1~5の乱数配列ならこんな感じ。

添字 0 1 2 3 4
要素 3 4 5 2 1

 こういう乱数が入った配列を作っていきます。いつ使うのかって?
 ちょっと話が外れるので最後に書くね(この記事を読むってことはそれが必要な場面があるでしょうから・・・)。
 →初めから配列をシャッフルすることを目的にしていたのですが、ここで考えたアルゴリズム自体が「配列をシャッフルする」ものだったので別で記事を書きました:配列をシャッフルするアルゴリズムを思いついたが既存だった話【JavaScript/Rubyサンプルコードあり】

 まずはじめに、候補となる配列を作るよ。

initAry:

添字 0 1 2 3 4
要素 1 2 3 4 5

 0~4(initAryの添字の最大値)の乱数を発生させて、initAryからその乱数の位置にある要素を一つ取り出し新しい配列aryに入れるよ。

randomNumber:3
取り出す要素はinitAry[3] = 4
initAry:

添字 0 1 2 3
要素 1 2 3 5

ary:

添字 0
要素 4

 これを、initAryが空になるまで繰り返すよ。

randomNumber:2
取り出す要素はinitAry[2] = 3
initAry:

添字 0 1 2
要素 1 2 5

ary:

添字 0 1
要素 4 3

 ・・・(略)・・・

randomNumber:0
取り出す要素はinitAry[0] = 2
initAry:

添字
要素

ary:

添字 0 1 2 3 4
要素 4 3 5 1 2

 おしまい。

3.実装

 さて、実際にこれを実装していこう。今回、class宣言を利用している。先に全貌を提示する。

RandomNumber.js
class RandomNumber{
  constructor(num, min = 0){
    this.num = num;
    this.min = min;
    this.init();
  }

  /*
   * getRandomIntメソッド
   * 処理:min以上max未満の乱数を返す。 
   * 引数
   *  min:最小値(整数)
   *  max:最大値(整数)
   * 返り値:生成された乱数
   */
  getRandomInt(min, max){
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
  }

  /* 
   * initメソッド
   * 処理:minを最小とするnum個の被りなしの乱数が入った配列this.aryを生成。
   * 引数
   *  num:生成個数(整数)、初期値はthis.num
   *  min:最小値(整数)、初期値はthis.min
   * 返り値:なし
   */
  init(num = this.num, min = this.min){
    this.index = 0;
    this.ary = [];
    let initAry = [];

    for(let i = 0; i < num; i++){
      initAry.push(min + i);
    }

    while(initAry.length > 0){
      this.ary.push(initAry.splice(this.getRandomInt(0,initAry.length), 1).pop());
    }
  }

  /* 
   * nextメソッド
   * 処理:実行するたびに、this.aryの要素を順に返す。
   *    配列の最後までいったときははじめの要素に戻る。
   * 引数:なし
   * 返り値:this.index位置のthis.aryの要素
   */
  next(){
    if(this.index >= this.ary.length){
      this.index = 0;
    }
    return this.ary[this.index++];
  }
}

 初期化も行いたいため、initメソッドをconstructorと別に定義している。これによって、例えばinit()とすると乱数配列を再生成できる。

constructor

処理
numとminをそれぞれ同名のクラスメンバにする。
その後、initメソッドを引数無しで実行する。
引数
num:生成する乱数の個数
min:生成する乱数の最小値、初期値は0
  constructor(num, min = 0){
    this.num = num;
    this.min = min;
    this.init();
  }

 constructor(num, min = 0)のように書いてminの初期値を0にすることで、第2引数を指定せずにクラス生成すると最小値0num個の整数乱数配列が生成されます。生成過程は後述のinitメソッドで。

getRandomIntメソッド

処理
min以上max未満の乱数を返す。
引数
min:最小値(整数)
max:最大値(整数)
返り値
生成された乱数
  getRandomInt(min, max){
    min = Math.ceil(min);
    max = Math.floor(max);
    return Math.floor(Math.random() * (max - min)) + min;
  }

コードはMath.random() - JavaScript | MDNのサンプルコードを転用したもの。
MDNのライセンスに従い掲載。

initメソッド

処理
minを最小とするnum個の重複なしの乱数が入った配列this.aryを生成。
引数
num:生成個数(整数)、初期値はthis.num
min:最小値(整数)、初期値はthis.min
返り値
なし
  init(num = this.num, min = this.min){
    this.index = 0;
    this.ary = [];
    let initAry = [];

    for(let i = 0; i < num; i++){
      initAry.push(min + i);
    }

    while(initAry.length > 0){
      this.ary.push(initAry.splice(this.getRandomInt(0,initAry.length), 1).pop());
    }
  }

引数省略時、this.minを最小とするthis.num個の整数乱数配列を生成。

this.index = 0;:nextメソッドで使うものなので後述。

this.ary = [];let initAry = [];:この2つが配列であることを宣言

for文:候補となる配列initAryを生成
 initAry.push(min + i);で最小値+iの値をinitAryに入れていくことで生成している

while文:乱数配列this.aryを生成
 initAry.splice(this.getRandomInt(0,initAry.length), 1).pop()initAryの要素からランダムで一つ取り出している。
 最後に.pop()をつけているのは、Arrayクラスのspliceメソッドは取り出す要素が一つでも要素数1の配列を返してしまうためである。
 initAry.splice(this.getRandomInt(0,initAry.length), 1)とすると、this.aryの中身が「要素数1の配列」をnum個格納した配列になってしまう。

nextメソッド

処理
実行するたびに、this.aryの要素を順に返す。配列の最後までいったときははじめの要素に戻る。
引数
なし
返り値
this.index位置のthis.aryの要素
  next(){
    if(this.index >= this.ary.length){
      this.index = 0;
    }
    return this.ary[this.index++];
  }

なにかに使えそうと思って作成したメソッド。forEachなどのループを使うことなく、生成された重複なしの整数乱数配列の中身を順に取り出すことができる。

4.使用例

const r = new RandomNumber(5,1);
console.log(r.ary);
r.init();
console.log(r.ary);
for(let i= 0; i < r.ary.length*2; i++){
  console.log(r.next());
}

実行結果

[ 4, 1, 3, 5, 2 ]
[ 5, 3, 2, 4, 1 ]
5
3
2
4
1
5
3
2
4
1

5.参考文献

(初心者向け) JavaScript のクラス (ES6 対応) - Qiita
JavaScriptのclass - Qiita
Math.random() - JavaScript | MDN
Array.prototype.splice() - JavaScript | MDN

6.さいごに

 重複なしの乱数配列を作って配列をシャッフルしようとアルゴリズム考えたら最終的に配列をシャッフルするアルゴリズムになってた。なんだこれ。
 ちなみに当初考えていたものは、乱数作って配列に入れて、その全要素と被らない乱数が生成されるまで繰り返すものでした。処理時間めっちゃ長い。
 class使う必要あるの?って思った人はわざわざclass使う必要ないです。でも、どうやらモジュール化するならclassのほうがいい?
 今の所、生のJavaScriptとjQueryをHTML、CSSと併用してしか使ってないので詳しくは知りませんが。
 ではまた。

追記

 本記事を参考にしてモジュールとコマンドラインツールを作成しnpmで公開してくださった方がいるのでシェアしておきます(Twitter上のやり取りは約2か月前なので今更感)