かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

JavaScript 配列からランダムに要素を取り出したい

配列からランダムに n 個の要素を取り出したい要件を実装したのでメモ

要件

  • 配列からランダムに n 個の要素を取り出す
  • 取り出す要素に被りがないこと

方針

  1. 配列をシャッフルする
  2. .slide(0, n) で要素を取り出す

Math.random() * array.index のような形で配列の index をランダムに作り取り出す方法も考えられるが、既に取り出した要素の判定が面倒なのと Math.random() は比較的偏るので要素をシャッフルした配列を作成し先頭から n 個取り出す方針とした

ランダムな index を生成する方法

1つだけ取り出すのであればこちらが簡単

const randomItem = array[Math.floor(Math.random() * array.length)];

1. 配列をシャッフルする

Fisher–Yates shuffle という手法があるのでこれを使って配列をシャッフルする

Fisher–Yates shuffle
Fisher–Yates shuffle

const shuffle = <T>(arr: T[]) => {
  const clone = structuredClone(arr);
  const n = clone.length;

  return clone.reduce((_, _item, index) => {
    // 元の配列の後ろから取り出す
    const i = (n - 1) - index;
    const randIndex = Math.floor(Math.random() * (n - index));
    // ランダムな要素と現在の要素の位置を入れ替える
    [clone[i], clone[randIndex]] = [clone[randIndex], clone[i]]; // ※ この行の前にセミコロンがないとエラーになるので注意

    return [...clone];
  }, [] as T[]); 
};

順番に取り出した要素と乱数で取り出した要素を入れ替えていくので、配列そのものを変更する方法なので先に structuredClone() で配列を deep copy してシャッフルする方針とした
また reduce を使っているけど、コールバックに渡される変数を全然使ってないので reduce である意味があまりない気がしている。 for 文の方がシンプルに読みやすいかも

const shuffle = <T>(arr: T[]) => {
  const clone = structuredClone(arr);
  for(let i = clone.length - 1; i > 0; i--) {
    const randIndex = Math.floor(Math.random() * (i + 1));
    [clone[i], clone[randIndex]] = [clone[randIndex], clone[i]];
  }
  
  return [...clone];
};

for なら reduce と異なり最後に 0番目と0番目を入れ替える処理をスキップできる

2. シャッフルした配列から n 件取り出す

これは単純に array.slice(0, n) すればよい

const shuffle = <T>(arr: T[]) => {
  const clone = structuredClone(arr);
  for(let i = clone.length - 1; i > 0; i--) {
    const randIndex = Math.floor(Math.random() * (i + 1));
    [clone[i], clone[randIndex]] = [clone[randIndex], clone[i]];
  }  
  return [...clone];
};

const getRandom = <T>(arr: T[], n: number) => {
  const randomArray = shuffle(arr);
  return randomArray.slice(0, n);
}

動作確認

const idols = ['星宮いちご', '霧矢あおい', '紫吹蘭', '有栖川おとめ', '藤堂ユリカ', '一ノ瀬かえで'];
const newUnit = getRandom(idols, 3);
// => ['一ノ瀬かえで', '紫吹蘭', '星宮いちご']

実行する度にランダムな値が得られました!

おわり


[参考]