陣列/列表隨機重排(Shuffle)是常見需求:撲克牌發牌、題目隨機、測試資料打散。本文實作經典 Fisher-Yates (Knuth) Shuffle,並比較三種隨機數來源對結果隨機性的影響,附帶 C++ STL 標準實作參考。
從陣列尾端向前遍歷,每步在「當前索引 ≤ i 的範圍」隨機選一個位置交換:
for (int i = list.Count - 1; i > 0; i--)
{
int j = random.Next(0, i + 1); // [0, i]
(list[i], list[j]) = (list[j], list[i]); // 交換
}
時間複雜度 O(n)、空間 O(1)、無偏差(每個排列機率均等 1/n!)。
public static List<int> Shuffle(int size)
{
var list = Enumerable.Range(0, size).ToList();
var rand = new Random(); // 預種子 Environment.TickCount
for (int i = list.Count - 1; i > 0; i--)
{
int j = rand.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
return list;
}
缺點:短時間內連續實例化 `new Random()` 會取得相同種子,導致相同序列。解決:共用單一 `Random` 實例(需執行緒安全)或改用下列策略。
public static List<int> ShuffleEx(int size)
{
var list = Enumerable.Range(0, size).ToList();
// 使用加密強度雜湊碼作種子
var rand = new Random(new RNGCryptoServiceProvider().GetHashCode());
for (int i = list.Count - 1; i > 0; i--)
{
int j = rand.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
return list;
}
種子熵較高,但 `Random` 本身仍為偽隨機演算法,長序列有週期性。
public static List<int> ShufflePro(int size)
{
var list = Enumerable.Range(0, size).ToList();
var rng = new RNGCryptoServiceProvider();
var bytes = new byte[4];
for (int i = list.Count - 1; i > 0; i--)
{
rng.GetNonZeroBytes(bytes); // 取得 4 位元組非零隨機數
// 組合成 32 位元整數(大端序)
int randomSeed = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
int j = Math.Abs(randomSeed % (i + 1)); // 映射到 [0, i]
(list[i], list[j]) = (list[j], list[i]);
}
return list;
}
優點:真隨機來源,適合密碼學、博弈、安全敏感場景。缺點:效能較 `Random` 低約 10-50 倍。
| 策略 | 隨機品質 | 效能 (10萬筆) | 適用場景 |
|---|---|---|---|
Random (共用實例) | 偽隨機,統計均勻 | ~5 ms | 一般遊戲、測試資料、UI 打散 |
Random + Crypto 種子 | 偽隨機,種子不可預測 | ~6 ms | 需不可重現序列的一般應用 |
RNGCryptoServiceProvider | 密碼學強度 | ~120 ms | 線上賭場、抽獎、金融、安全 Token |
// Random.Shared (執行緒安全單例) - .NET 6+
public static void ShuffleModern<T>(IList<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Shared.Next(i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
// RandomNumberGenerator (取代 RNGCryptoServiceProvider) - .NET Core 3.0+
public static void ShuffleSecure<T>(IList<T> list)
{
using var rng = RandomNumberGenerator.Create();
var bytes = new byte[4];
for (int i = list.Count - 1; i > 0; i--)
{
rng.GetNonZeroBytes(bytes);
int r = (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
int j = Math.Abs(r % (i + 1));
(list[i], list[j]) = (list[j], list[i]);
}
}
template <class _RanIt, class _Diff> inline
void _Random_shuffle(_RanIt _First, _RanIt _Last, _Diff *)
{
const int _RANDOM_BITS = 15; // rand() 至少 15 bits
const int _RANDOM_MAX = (1U << _RANDOM_BITS) - 1;
_RanIt _Next = _First;
for (unsigned long _Index = 2; _Next != _Last; ++_Next, ++_Index)
{
unsigned long _Rm = _RANDOM_MAX;
unsigned long _Rn = ::rand() & _RANDOM_MAX;
for (; _Rm < _Index && _Rm != ~0UL; _Rm = _Rm << _RANDOM_BITS | _RANDOM_MAX)
_Rn = _Rn << _RANDOM_BITS | (::rand() & _RANDOM_MAX); // 擴大隨機範圍
std::iter_swap(_Next, _First + _Diff(_Rn % _Index));
}
}
STL 為了應對 `rand()` 位數不足,採用多次疊加擴大隨機範圍,再取模。