できない.dev

配列をランダムに並べ替えるには

配列やリストの要素をランダムな順に並べ替える方法を各言語で示す。
元の配列を書き換えるか新しい配列を返すかの違いと、JavaScript で出回る簡易的な書き方が偏る理由を扱う。

公開:

選んだ言語は、ほかのサンプルコードを開いたときも最初に表示する。

Python

import random
 
nums = [1, 2, 3, 4, 5]
random.shuffle(nums)  # 元のリストを直接並べ替える(戻り値は None)
print(nums)           # 例: [3, 1, 5, 2, 4]
 
original = ["a", "b", "c", "d"]
shuffled = random.sample(original, k=len(original))  # 新しいリストを返す
print(shuffled)       # 例: ['c', 'a', 'd', 'b']
print(original)       # ['a', 'b', 'c', 'd'](元のリストは変わらない)

random.shuffle は渡したリストを直接並べ替えて None を返すので、print(random.shuffle(x)) のように戻り値を使うと None が出る。
元のリストを残したいときは random.sample(リスト, k=len(リスト)) で新しいリストを作る。
タプルや range は書き換えられず TypeError になるので、先に list() で変換する。

JavaScript

// Fisher-Yates 法。元の配列は変えず、新しい配列を返す
function shuffle(items) {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)); // 0 以上 i 以下
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}
 
const nums = [1, 2, 3, 4, 5];
console.log(shuffle(nums)); // 例: [ 3, 1, 5, 2, 4 ]
console.log(nums);          // [ 1, 2, 3, 4, 5 ](元の配列は変わらない)

標準には配列をシャッフルする関数が無いため、Fisher-Yates 法を自分で書く。
末尾から先頭へ向かい、各位置を「その位置以前のランダムな位置」と入れ替えると、すべての並びが等しい確率で出る。
元の配列を変えないように、先にスプレッド構文でコピーしている。

TypeScript

// Fisher-Yates 法。どんな型の配列にも使え、元の配列は変えない
function shuffle<T>(items: readonly T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1)); // 0 以上 i 以下
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}
 
const names: string[] = ["a", "b", "c", "d"];
console.log(shuffle(names)); // 例: [ 'c', 'a', 'd', 'b' ]
console.log(names);          // [ 'a', 'b', 'c', 'd' ](元の配列は変わらない)

ジェネリクスにしておくと、数値・文字列・オブジェクトのどの配列にも使え、戻り値も同じ要素の型になる。
引数を readonly T[] にして、元の配列を書き換えないことを型でも示している。

Go

package main
 
import (
	"fmt"
	"math/rand/v2"
)
 
func main() {
	nums := []int{1, 2, 3, 4, 5}
	rand.Shuffle(len(nums), func(i, j int) {
		nums[i], nums[j] = nums[j], nums[i]
	})
	fmt.Println(nums) // 例: [3 1 5 2 4](元のスライスを直接並べ替える)
 
	fmt.Println(rand.Perm(5)) // 例: [2 0 4 1 3](0〜4 の並べ替えを新しいスライスで返す)
}

math/rand/v2 の rand.Shuffle は、要素数と「2 つの添字を入れ替える関数」を渡す形で、スライスを直接並べ替える。
新しいスライスで 0 から n-1 の並べ替えがほしいときは rand.Perm(n) を使い、その結果を添字として使う。

Rust

// Cargo.toml: rand = "0.10"
use rand::seq::SliceRandom;
 
fn main() {
    let mut nums = vec![1, 2, 3, 4, 5];
    nums.shuffle(&mut rand::rng());
    println!("{:?}", nums); // 例: [3, 1, 5, 2, 4](元のベクタを直接並べ替える)
}

rand クレートの SliceRandom トレイトを use すると、スライスやベクタで shuffle メソッドが使える。
引数には rand::rng() が返す乱数生成器を &mut で渡す。
元のベクタを直接並べ替える。

Java

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
 
public class Main {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>(List.of(1, 2, 3, 4, 5));
        Collections.shuffle(nums);
        System.out.println(nums); // 例: [3, 1, 5, 2, 4](元のリストを直接並べ替える)
 
        // List.of が返すリストは変更できないので、そのまま渡すと UnsupportedOperationException になる
        try {
            Collections.shuffle(List.of(1, 2, 3));
        } catch (UnsupportedOperationException e) {
            System.out.println("UnsupportedOperationException");
        }
    }
}

Collections.shuffle はリストを直接並べ替える。
List.of が返すリストは変更できないため、そのまま渡すと UnsupportedOperationException になる。
new ArrayList<>(List.of(...)) のようにコピーしてから渡す。

C#

using System;
using System.Linq;
 
var nums = new[] { 1, 2, 3, 4, 5 };
Random.Shared.Shuffle(nums); // .NET 8 以降。元の配列を直接並べ替える
Console.WriteLine(string.Join(", ", nums)); // 例: 3, 1, 5, 2, 4
 
// 元を変えずに新しい配列がほしいときは、ランダムな値をキーに並べ替える
var copy = nums.OrderBy(_ => Random.Shared.Next()).ToArray();
Console.WriteLine(string.Join(", ", copy)); // 例: 2, 5, 1, 3, 4

.NET 8 で追加された Random.Shuffle は、配列を直接並べ替える。
元を変えない新しい配列がほしいときは、OrderBy にランダムな値をキーとして渡す方法がある。

sort にランダムな比較関数を渡すと偏る

JavaScript では array.sort(() => Math.random() - 0.5) という短い書き方が出回っているが、結果が偏る。
[1, 2, 3] を 600,000 回並べ替えて、出た並びを数えた(Node.js 24.20.0)。

  • Fisher-Yates 法は、6 通りがすべて 99,604〜100,463 回だった。
  • sort にランダムな比較関数を渡した場合は、元の並びの [1, 2, 3] が 225,000 回(37.5%)、[3, 2, 1] が 188,101 回(31.3%)で、残りの 4 通りは 37,354〜74,493 回だった。

偏りがなければ 6 通りとも約 16.7% である。
5 要素の [0, 1, 2, 3, 4] で 0 の行き先を 200,000 回数えても、先頭が 32.2%、末尾が 25.1% で、真ん中の 3 か所は 12.0〜16.6% だった(偏りがなければ各 20%)。
sort は比較結果が一貫していることを前提にしており、比較のたびに答えを変えると分布が実装に左右される。
シャッフルには専用の関数か Fisher-Yates 法を使う。

標準の関数は偏らなかった

同じ方法で、Python の random.shuffle、Go の rand.Shuffle、Rust の shuffle、Java の Collections.shuffle、C# の Random.Shared.Shuffle を 600,000 回ずつ試したところ、6 通りの並びがそれぞれ 99,416〜100,646 回に収まった。
C# の OrderBy にランダムな値を渡す方法も 99,674〜100,320 回で、偏りは見られなかった。

ただし、これらの乱数は暗号用途向けではない。
賭け金が絡むカードゲームの配札のように、並びを推測されると困る場面では、Python なら random.SystemRandom().shuffle のように OS の乱数を使う版を選ぶ。

元の配列を書き換えるか

並べ替える関数には、渡した配列を直接書き換えるものと、新しい配列を返すものがある。
Python の random.shuffle・Go の rand.Shuffle・Rust の shuffle・Java の Collections.shuffle・C# の Random.Shuffle は直接書き換える。
元の並びを後で使うなら、先にコピーを取るか、Python の random.sample のように新しい配列を返す書き方を選ぶ。