抽獎 Fisher-Yates Shuffle(費雪-耶茲洗牌演算法)

引言

工作需要洗牌演算法。問了問 AI 回答紀錄。

詢問有關軟體演算法。抽獎的演算法 Fisher-Yates Shuffle(費雪-耶茲洗牌演算法)。 用 C# 實作。可在 NET 8 平台執行。

Fisher-Yates Shuffle 是一個經典的洗牌演算法,特別適合用於抽獎系統,因為它能產生完全隨機且公平的排列。以下是 C# .NET 8 的實作:

using System;
using System.Collections.Generic;
using System.Security.Cryptography;

namespace LotterySystem
{
  public class FisherYatesLottery : IDisposable
  {
    private readonly Random _random;
    private readonly RandomNumberGenerator _cryptoRandom;
    private bool _disposed = false;

    public FisherYatesLottery()
    {
      // 使用系統時間作為種子
      _random = new Random();
      // 使用密碼學安全的隨機數產生器(更適合重要抽獎)
      _cryptoRandom = RandomNumberGenerator.Create();
    }

    /// <summary>
    /// Fisher-Yates Shuffle 標準實作
    /// </summary>
    /// <typeparam name="T">陣列元素類型</typeparam>
    /// <param name="array">要洗牌的陣列</param>
    public void Shuffle<T>(T[] array)
    {
      if (array == null || array.Length <= 1)
        return;

      // 從最後一個元素開始,向前遍歷
      for (int i = array.Length - 1; i > 0; i--)
      {
        // 在 0 到 i(包含)之間選擇隨機索引
        int j = _random.Next(i + 1);

        // 交換元素
        (array[i], array[j]) = (array[j], array[i]);
      }
    }

    /// <summary>
    /// 使用密碼學安全隨機數的 Fisher-Yates Shuffle
    /// 適用於需要高安全性的抽獎
    /// </summary>
    /// <typeparam name="T">陣列元素類型</typeparam>
    /// <param name="array">要洗牌的陣列</param>
    public void SecureShuffle<T>(T[] array)
    {
      ObjectDisposedException.ThrowIf(_disposed, this);

      if (array == null || array.Length <= 1)
        return;

      for (int i = array.Length - 1; i > 0; i--)
      {
        int j = GetSecureRandomInt(i + 1);
        (array[i], array[j]) = (array[j], array[i]);
      }
    }

    /// <summary>
    /// 產生密碼學安全的隨機整數
    /// </summary>
    /// <param name="maxValue">最大值(不包含)</param>
    /// <returns>0 到 maxValue-1 之間的隨機整數</returns>
    private int GetSecureRandomInt(int maxValue)
    {
      ObjectDisposedException.ThrowIf(_disposed, this);

      if (maxValue <= 1) return 0;

      byte[] bytes = new byte[4];
      int result;

      do
      {
        _cryptoRandom.GetBytes(bytes);
        result = Math.Abs(BitConverter.ToInt32(bytes, 0));
      } while (result >= int.MaxValue - (int.MaxValue % maxValue));

      return result % maxValue;
    }

    /// <summary>
    /// 進行抽獎,返回指定數量的中獎者
    /// </summary>
    /// <typeparam name="T">參與者類型</typeparam>
    /// <param name="participants">參與者列表</param>
    /// <param name="winnerCount">中獎人數</param>
    /// <param name="useSecureRandom">是否使用密碼學安全隨機數</param>
    /// <returns>中獎者陣列</returns>
    public T[] DrawLottery<T>(IEnumerable<T> participants, int winnerCount, bool useSecureRandom = false)
    {
      var participantArray = participants.ToArray();

      if (participantArray.Length == 0)
        return Array.Empty<T>();

      if (winnerCount >= participantArray.Length)
        return participantArray;

      // 洗牌
      if (useSecureRandom)
        SecureShuffle(participantArray);
      else
        Shuffle(participantArray);

      // 取前 winnerCount 個作為中獎者
      var winners = new T[winnerCount];
      Array.Copy(participantArray, winners, winnerCount);

      return winners;
    }

    public void Dispose()
    {
      Dispose(true);
      GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
      if (!_disposed && disposing)
      {
        _cryptoRandom?.Dispose();
        _disposed = true;
      }
    }

    /// <summary>
    /// 檢查物件是否已釋放
    /// </summary>
    protected void ThrowIfDisposed()
    {
      ObjectDisposedException.ThrowIf(_disposed, this);
    }
  }

  // 示範用的參與者類別
  public record Participant(int Id, string Name, string Email);

  // 主程式示範
  public class Program
  {
    public static void Main(string[] args)
    {
      var lottery = new FisherYatesLottery();

      try
      {
        // 示範 1: 基本洗牌
        Console.WriteLine("=== 基本 Fisher-Yates Shuffle 示範 ===");
        int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        Console.WriteLine($"原始順序: [{string.Join(", ", numbers)}]");

        lottery.Shuffle(numbers);
        Console.WriteLine($"洗牌後:   [{string.Join(", ", numbers)}]");

        // 示範 2: 參與者抽獎
        Console.WriteLine("\n=== 抽獎示範 ===");
        var participants = new List<Participant>
                {
                    new(1, "張三", "zhang@example.com"),
                    new(2, "李四", "li@example.com"),
                    new(3, "王五", "wang@example.com"),
                    new(4, "趙六", "zhao@example.com"),
                    new(5, "孫七", "sun@example.com"),
                    new(6, "周八", "zhou@example.com"),
                    new(7, "吳九", "wu@example.com"),
                    new(8, "鄭十", "zheng@example.com")
                };

        Console.WriteLine("參與者名單:");
        foreach (var p in participants)
        {
          Console.WriteLine($"  {p.Id}. {p.Name} ({p.Email})");
        }

        // 抽出 3 名中獎者
        var winners = lottery.DrawLottery(participants, 3, useSecureRandom: false);

        Console.WriteLine("\n🎉 中獎者公布 🎉");
        for (int i = 0; i < winners.Length; i++)
        {
          Console.WriteLine($"第 {i + 1} 名: {winners[i].Name} ({winners[i].Email})");
        }

        // 示範 3: 安全抽獎(適用於重要場合)
        Console.WriteLine("\n=== 高安全性抽獎示範 ===");
        var secureWinners = lottery.DrawLottery(participants, 2, useSecureRandom: true);

        Console.WriteLine("使用密碼學安全隨機數抽獎結果:");
        for (int i = 0; i < secureWinners.Length; i++)
        {
          Console.WriteLine($"第 {i + 1} 名: {secureWinners[i].Name}");
        }

        // 示範 4: 多次洗牌驗證隨機性
        Console.WriteLine("\n=== 隨機性驗證 ===");
        int[] testArray = { 1, 2, 3, 4, 5 };
        Console.WriteLine("連續 5 次洗牌結果:");

        for (int round = 1; round <= 5; round++)
        {
          int[] temp = (int[])testArray.Clone();
          lottery.Shuffle(temp);
          Console.WriteLine($"第 {round} 次: [{string.Join(", ", temp)}]");
        }
      }
      finally
      {
        // 確保資源被正確釋放
        lottery.Dispose();
      }

      Console.WriteLine("\n按任意鍵結束...");
      Console.ReadKey();
    }
  }
}          

這個實作包含了以下重要特點:

演算法核心概念

Fisher-Yates Shuffle 的工作原理:

  1. 從陣列的最後一個元素開始

  2. 隨機選擇一個位置(0 到當前位置)

  3. 交換當前元素與隨機位置的元素

  4. 重複直到處理完所有元素

主要功能

  1. 基本洗牌 (Shuffle): 使用 System.Random 進行標準洗牌

  2. 安全洗牌 (SecureShuffle): 使用密碼學安全的隨機數產生器

  3. 抽獎功能 (DrawLottery): 整合洗牌與抽獎邏輯

  4. 泛型支援: 可以處理任何類型的物件

適用場景

  • 一般抽獎活動: 使用基本洗牌即可

  • 重要商業抽獎: 建議使用安全洗牌

  • 遊戲開發: 卡牌洗牌、道具隨機等

  • 統計採樣: 隨機抽樣研究

演算法優勢

  • 時間複雜度: O(n),非常高效

  • 空間複雜度: O(1),原地操作

  • 數學保證: 每種排列的機率完全相等

  • 無偏差: 不像其他洗牌方法可能有統計偏差

這個實作可以直接在 .NET 8 環境中運行,並提供了完整的示範案例。你可以根據需要調整參與者結構或抽獎邏輯。

編譯執行結果

=== 基本 Fisher-Yates Shuffle 示範 ===
原始順序: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
洗牌後:   [1, 4, 10, 6, 8, 7, 3, 2, 9, 5]

=== 抽獎示範 ===
參與者名單:
  1. 張三 (zhang@example.com)
  2. 李四 (li@example.com)
  3. 王五 (wang@example.com)
  4. 趙六 (zhao@example.com)
  5. 孫七 (sun@example.com)
  6. 周八 (zhou@example.com)
  7. 吳九 (wu@example.com)
  8. 鄭十 (zheng@example.com)

?? 中獎者公布 ??
第 1 名: 周八 (zhou@example.com)
第 2 名: 張三 (zhang@example.com)
第 3 名: 鄭十 (zheng@example.com)

=== 高安全性抽獎示範 ===
使用密碼學安全隨機數抽獎結果:
第 1 名: 王五
第 2 名: 周八

=== 隨機性驗證 ===
連續 5 次洗牌結果:
第 1 次: [5, 4, 2, 1, 3]
第 2 次: [3, 4, 1, 5, 2]
第 3 次: [1, 5, 2, 4, 3]
第 4 次: [1, 2, 3, 5, 4]
第 5 次: [4, 5, 1, 2, 3]

按任意鍵結束...

比較真實的應用:先洗牌再抽獎

測試程序

/// <summary>
/// 測試洗牌數次後再抽獎。
/// </summary>
public static void TestShuffleAndDraw()
{
  // 主程式示範
  using var lottery = new FisherYatesLottery();

  Console.WriteLine("\n=== 測試洗牌 26 筆 ===");

  var strArray = Enumerable.Range(1, 26).Select(c => $"{c:00}").ToArray();

  Console.WriteLine($"原始順序: [{string.Join(",", strArray)}]");
  Console.WriteLine("連續 5 次洗牌:");
  for (int round = 1; round <= 5; round++)
  {
    lottery.SecureShuffle(strArray);
    Console.WriteLine($"第 {round} 次: [{string.Join(",", strArray)}]");
  }

  Console.WriteLine("再抽取前 3 名中獎者:");
  var winnerArray = lottery.DrawLottery(strArray, 3, useSecureRandom: true);
  Console.WriteLine($"中獎者: [{string.Join(",", winnerArray)}]");
}

執行結果

=== 測試洗牌 26 筆 ===
原始順序: [01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26]
連續 5 次洗牌:
第 1 次: [11,26,06,22,13,12,07,17,04,10,19,25,21,03,23,16,20,02,14,01,18,05,24,15,08,09]
第 2 次: [11,26,07,17,03,04,06,05,08,22,16,20,18,25,15,09,02,12,23,10,14,19,01,24,13,21]
第 3 次: [11,06,23,04,05,24,07,25,21,18,20,13,12,19,02,14,15,16,26,09,01,08,10,03,17,22]
第 4 次: [04,11,26,14,06,01,16,12,03,07,18,05,02,13,20,25,22,21,15,09,24,10,17,08,23,19]
第 5 次: [24,25,17,26,19,21,11,07,12,20,08,01,06,15,16,23,22,02,03,04,09,14,10,05,13,18]
再抽取前 3 名中獎者:
中獎者: [04,14,12]

按任意鍵結束...

完整程式碼

(EOF)

Last updated