洗牌算法是一種將序列(如數組、列表)元素隨機打亂的經典算法,核心目標是讓每個元素在打亂后出現在任意位置的概率均等。在 C# 中,常用的洗牌算法有Fisher-Yates 洗牌算法(也稱 Knuth 洗牌算法),它高效且公平,時間復雜度為 O (n),空間復雜度為 O (1)。
一、Fisher-Yates 洗牌算法原理
核心思想:從序列的最后一個元素開始,依次與前面的隨機位置元素交換,直到處理完第一個元素。
公平性保證:每個元素被放置在任意位置的概率均為 1/n(n 為序列長度),避免了 “部分隨機” 導致的分布不均問題。
二、C# 實現示例
以下是使用 Fisher-Yates 算法對整數數組、字符串列表進行洗牌的實現:
代碼分塊分析
這段代碼是一個簡化的斗地主游戲實現,主要包含撲克牌的生成、洗牌、發牌和排序功能。下面我將對代碼進行分塊分析。
1. 主程序結構與初始化
static void Main(string[] args)
{int[] ints1 = new int[54];ints1 = RandomUNorepeatArray(ints1);// 后續代碼...
}
這部分代碼首先創建了一個包含 54 個元素的整數數組ints1
,并調用RandomUNorepeatArray
方法生成 0-53 的隨機不重復數組,用于作為撲克牌的隨機索引。
2. 撲克牌對象模型
class Puke
{public string number;public char color;public override string ToString(){return $"[{number},{color}]";}
}
Puke
類表示一張撲克牌,包含兩個屬性:
number
:牌面數字(字符串類型,"1"-"13" 或 "joker")color
:花色(字符類型,' 黑 '、' 紅 '、' 梅 '、' 方 ')重寫的
ToString
方法用于格式化輸出牌的信息
3. 撲克牌生成與初始化
Puke[] puke = new Puke[54];
int num = 1;
char[] str = new char[4] {'黑', '紅', '梅', '方' };
int num2 = 3;
for (int i = 0; i < 52; i++)
{puke[i] = new Puke();if (num > 13){num = 1;num2--;}puke[i].number = num.ToString();num++;puke[i].color = str[num2];
}
puke[52] = new Puke { number = "joker", color = '黑' };
puke[53] = new Puke { number = "joker", color = '紅' };
這段代碼生成了 54 張撲克牌:
前 52 張是四種花色的 A-K(用數字 1-13 表示)
最后兩張是大小王("joker")
4. 洗牌與發牌
Puke[] puke2 = new Puke[54];
for (int i = 0; i < 54; i++)
{puke2[i] = puke[ints1[i]];
}
?
// 發牌給三個玩家和底牌
Puke[] puke3 = new Puke[17];
Puke[] puke4 = new Puke[17];
Puke[] puke5 = new Puke[17];
Puke[] puke6 = new Puke[3];
for (int i = 0; i < 17; i++)
{puke3[i] = puke2[ints1[i]];puke4[i] = puke2[ints1[i + 17]];puke5[i] = puke2[ints1[i + 24]]; // 這里索引計算有問題!
}
for(int i = 0; i < 3; i++)
{puke6[i] = puke2[ints1[i+51]];
}
這部分代碼實現了洗牌和發牌:
使用隨機索引數組
ints1
重新排列撲克牌數組將牌分發給三個玩家(各 17 張)和底牌(3 張)
5. 排序算法
Array.Sort(puke3, (a, b) =>
{int numA = a.number == "joker" ? 100 : int.Parse(a.number);int numB = b.number == "joker" ? 100 : int.Parse(b.number);int result = numA.CompareTo(numB);if (result == 0)return a.color.CompareTo(b.color);return result;
});
?
// 對puke4和puke5有相同的排序代碼...
這部分代碼對每個玩家的手牌進行排序:
將牌面值轉換為整數進行比較(Joker 設為 100)
牌面值相同則比較花色
6. 輔助方法:生成隨機不重復數組
static int[] RandomUNorepeatArray(int[] ints )
{int min = 0;int max = 53;int count = 54;
?List<int> pool = new List<int>();for (int i = min; i <= max; i++)pool.Add(i);
?Random rand = new Random();// 洗牌算法for (int i = pool.Count - 1; i > 0; i--){int j = rand.Next(0, i + 1);int temp = pool[i];pool[i] = pool[j];pool[j] = temp;}
?return pool.ToArray();
}
這個方法使用 Fisher-Yates 洗牌算法生成 0-53 的隨機排列數組。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
?
namespace 斗地主
{internal class Program{static void Main(string[] args){int[] ints1 = new int[54];ints1 = RandomUNorepeatArray(ints1);
?// //Console.WriteLine(string.Join(" ", ints1));
?// string[] strings = new string[]//{// ? ? "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K",// ? ? "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K",// ? ? "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K",// ? ? "A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K",// ? ? "j1", "j2"//};
?// string[] strings2 = new string[60];
?// Random random = new Random();
?// for (int i = 0; i < ints1.Length; i++)// {// ? // int ints3 = random.Next(ints1.Length);// ? ? strings2[i] = strings[ints1[i]];// }// int num = 0;
?// foreach (string s in strings2)// {// ? ? Console.Write($"{s,-4}");// ? ? num++;// ? ? if (num % 17 == 0) Console.WriteLine();// }
?Puke[] puke = new Puke[54];int num = 1;char[] str = new char[4] {'黑', '紅', '梅', '方' };int num2 = 3;for (int i = 0; i < 52; i++){puke[i] = new Puke();if (num > 13){num = 1;num2--;}puke[i].number = num.ToString();num++;puke[i].color = str[num2];}puke[52] = new Puke { number = "joker", color = '黑' };puke[53] = new Puke { number = "joker", color = '紅' };
?Puke[] puke2 = new Puke[54];for (int i = 0; i < 54; i++){puke2[i] = puke[ints1[i]];}
?int count = 0;foreach (var item in puke2){Console.Write($"{item,-4}");count++;if (count % 17 == 0) Console.WriteLine();
?//count++;//if (count%13 == 0) Console.WriteLine();}
?Puke[] puke3 = new Puke[17];Puke[] puke4 = new Puke[17];Puke[] puke5 = new Puke[17];Puke[] puke6 = new Puke[3];for (int i = 0; i < 17; i++){puke3[i] = puke2[ints1[i]];puke4[i] = puke2[ints1[i + 17]];puke5[i] = puke2[ints1[i + 24]];}for(int i = 0; i < 3; i++){puke6[i] = puke2[ints1[i+51]];}
?Array.Sort(puke3, (a, b) =>{int numA = a.number == "joker" ? 100 : int.Parse(a.number);int numB = b.number == "joker" ? 100 : int.Parse(b.number);int result = numA.CompareTo(numB);if (result == 0)return a.color.CompareTo(b.color);return result;});
?Array.Sort(puke4, (a, b) =>{int numA = a.number == "joker" ? 100 : int.Parse(a.number);int numB = b.number == "joker" ? 100 : int.Parse(b.number);int result = numA.CompareTo(numB);if (result == 0)return a.color.CompareTo(b.color);return result;});
?Array.Sort(puke5, (a, b) =>{int numA = a.number == "joker" ? 100 : int.Parse(a.number);int numB = b.number == "joker" ? 100 : int.Parse(b.number);int result = numA.CompareTo(numB);if (result == 0)return a.color.CompareTo(b.color);return result;});
?Console.WriteLine();Console.Write("=============================");Console.WriteLine();
?int c = 0;foreach (var item in puke3){Console.Write($"{item,-6}");//count++;//if (count%13 == 0) Console.WriteLine();}Console.WriteLine();foreach (var item in puke4){Console.Write($"{item,-6}");//count++;//if (count%13 == 0) Console.WriteLine();}Console.WriteLine();foreach (var item in puke5){Console.Write($"{item,-6}");//count++;//if (count%13 == 0) Console.WriteLine();}Console.WriteLine();foreach (var item in puke6){Console.Write($"{item,-6}");//count++;//if (count%13 == 0) Console.WriteLine();}
?//Array.Sort(puke2, (a, b) =>//{// ? int result = a.number.CompareTo(b.number);// ? if (result == 0)// ? {// ? ? ? return b.color.CompareTo(a.color);// ? }// ? return result;//});
?}
?//生成一定范圍內隨機不成重復數字的數組static int[] RandomUNorepeatArray(int[] ints ){int min = 0;int max = 53; // 生成1~20之間的不重復數字int count = 54; // 需要的數量
?List<int> pool = new List<int>();for (int i = min; i <= max; i++)pool.Add(i);
?//Fisher-Yates 洗牌算法Random rand = new Random();// 洗牌for (int i = pool.Count - 1; i > 0; i--){int j = rand.Next(0, i + 1);int temp = pool[i];pool[i] = pool[j];pool[j] = temp;}
?// 取前count個//for (int i = 0; i < count; i++)//{// ? Console.Write(pool[i] + " ");//}//Console.WriteLine();
?
?return pool.ToArray();}}
?
?class Puke{// 牌的數字 A-K ? 用1-13表示public string number;// 牌的花色 黑紅梅方 ? 4321public char color;public override string ToString(){return $"[{number},{color}]";}
?}
?
}
?
三、代碼說明
泛型方法:
Shuffle<T>
支持任意類型的數組和列表,通用性強。隨機索引生成:
random.Next(i + 1)
確保生成的索引j
在[0, i]
范圍內,避免越界。元素交換:使用 C# 7.0 引入的元組交換語法
(a, b) = (b, a)
,簡潔高效(也可使用臨時變量交換)。Random
實例:在方法內創建單個Random
實例,避免短時間內多次創建導致的隨機序列重復問題。
四、算法優勢
公平性:每個元素在每個位置的概率嚴格相等,無偏差。
高效性:僅需一次遍歷和 n-1 次交換,時間復雜度 O (n),空間復雜度 O (1)(原地洗牌,無需額外空間)。
適用性:適用于任何可索引的序列(數組、列表等),廣泛應用于卡牌游戲、隨機排序、數據打亂等場景。
五、注意事項
Random
的線程安全:若在多線程環境中使用,需確保Random
實例的線程安全(可使用Random.Shared
或加鎖)。重復執行的隨機性:若需每次運行生成不同的打亂結果,不要手動指定
Random
的種子(默認使用系統時間作為種子)。
示例輸出
[8,梅][4,方][1,梅][3,梅][12,方][4,紅][9,黑][2,方][13,梅][9,方][7,黑][joker,紅][11,方][joker,黑][13,黑][9,紅][6,紅] [10,紅][12,黑][9,梅][11,黑][3,紅][10,方][11,梅][12,梅][10,黑][6,梅][7,梅][2,紅][5,梅][12,紅][4,黑][3,黑][1,紅] [10,梅][8,紅][6,方][5,紅][11,紅][1,黑][3,方][6,黑][5,黑][1,方][2,梅][13,紅][8,方][4,梅][8,黑][5,方][2,黑] [7,方][7,紅][13,方] ============================= [3,梅] [4,方] [4,梅] [4,黑] [5,梅] [7,方] [7,紅] [7,黑] [9,紅] [10,梅][10,黑][11,黑][13,方][13,梅][13,紅][joker,紅][joker,黑] [2,紅] [2,黑] [3,紅] [5,方] [5,紅] [5,黑] [6,梅] [6,黑] [7,梅] [8,紅] [8,黑] [9,方] [9,梅] [10,紅][11,梅][12,梅][12,黑] [1,梅] [1,紅] [1,黑] [4,紅] [5,紅] [5,黑] [6,方] [6,梅] [6,黑] [7,梅] [8,黑] [9,梅] [10,方][10,紅][12,梅][12,紅][12,黑] [9,黑] [3,黑] [11,方]