C#洗牌算法的具體實(shí)現(xiàn)
洗牌算法是一種將序列(如數(shù)組、列表)元素隨機(jī)打亂的經(jīng)典算法,核心目標(biāo)是讓每個(gè)元素在打亂后出現(xiàn)在任意位置的概率均等。在 C# 中,常用的洗牌算法有Fisher-Yates 洗牌算法(也稱(chēng) Knuth 洗牌算法),它高效且公平,時(shí)間復(fù)雜度為 O (n),空間復(fù)雜度為 O (1)。
一、Fisher-Yates 洗牌算法原理
- 核心思想:從序列的最后一個(gè)元素開(kāi)始,依次與前面的隨機(jī)位置元素交換,直到處理完第一個(gè)元素。
- 公平性保證:每個(gè)元素被放置在任意位置的概率均為 1/n(n 為序列長(zhǎng)度),避免了 “部分隨機(jī)” 導(dǎo)致的分布不均問(wèn)題。
二、C# 實(shí)現(xiàn)示例
以下是使用 Fisher-Yates 算法對(duì)整數(shù)數(shù)組、字符串列表進(jìn)行洗牌的實(shí)現(xiàn):
代碼分塊分析
這段代碼是一個(gè)簡(jiǎn)化的斗地主游戲?qū)崿F(xiàn),主要包含撲克牌的生成、洗牌、發(fā)牌和排序功能。下面我將對(duì)代碼進(jìn)行分塊分析。
1. 主程序結(jié)構(gòu)與初始化
static void Main(string[] args)
{
int[] ints1 = new int[54];
ints1 = RandomUNorepeatArray(ints1);
// 后續(xù)代碼...
}這部分代碼首先創(chuàng)建了一個(gè)包含 54 個(gè)元素的整數(shù)數(shù)組ints1,并調(diào)用RandomUNorepeatArray方法生成 0-53 的隨機(jī)不重復(fù)數(shù)組,用于作為撲克牌的隨機(jī)索引。
2. 撲克牌對(duì)象模型
class Puke
{
public string number;
public char color;
public override string ToString()
{
return $"[{number},{color}]";
}
}Puke類(lèi)表示一張撲克牌,包含兩個(gè)屬性:
number:牌面數(shù)字(字符串類(lèi)型,"1"-"13" 或 "joker")color:花色(字符類(lèi)型,' 黑 '、' 紅 '、' 梅 '、' 方 ')- 重寫(xiě)的
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(用數(shù)字 1-13 表示)
- 最后兩張是大小王("joker")
4. 洗牌與發(fā)牌
Puke[] puke2 = new Puke[54];
for (int i = 0; i < 54; i++)
{
puke2[i] = puke[ints1[i]];
}
?
// 發(fā)牌給三個(gè)玩家和底牌
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]]; // 這里索引計(jì)算有問(wèn)題!
}
for(int i = 0; i < 3; i++)
{
puke6[i] = puke2[ints1[i+51]];
}這部分代碼實(shí)現(xiàn)了洗牌和發(fā)牌:
- 使用隨機(jī)索引數(shù)組
ints1重新排列撲克牌數(shù)組 - 將牌分發(fā)給三個(gè)玩家(各 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;
});
?
// 對(duì)puke4和puke5有相同的排序代碼...這部分代碼對(duì)每個(gè)玩家的手牌進(jìn)行排序:
- 將牌面值轉(zhuǎn)換為整數(shù)進(jìn)行比較(Joker 設(shè)為 100)
- 牌面值相同則比較花色
6. 輔助方法:生成隨機(jī)不重復(fù)數(shù)組
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();
}這個(gè)方法使用 Fisher-Yates 洗牌算法生成 0-53 的隨機(jī)排列數(shù)組。
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;
//});
?
}
?
//生成一定范圍內(nèi)隨機(jī)不成重復(fù)數(shù)字的數(shù)組
static int[] RandomUNorepeatArray(int[] ints )
{
int min = 0;
int max = 53; // 生成1~20之間的不重復(fù)數(shù)字
int count = 54; // 需要的數(shù)量
?
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個(gè)
//for (int i = 0; i < count; i++)
//{
// Console.Write(pool[i] + " ");
//}
//Console.WriteLine();
?
?
return pool.ToArray();
}
}
?
?
class Puke
{
// 牌的數(shù)字 A-K 用1-13表示
public string number;
// 牌的花色 黑紅梅方 4321
public char color;
public override string ToString()
{
return $"[{number},{color}]";
}
?
}
?
}
?三、代碼說(shuō)明
- 泛型方法:
Shuffle<T>支持任意類(lèi)型的數(shù)組和列表,通用性強(qiáng)。 - 隨機(jī)索引生成:
random.Next(i + 1)確保生成的索引j在[0, i]范圍內(nèi),避免越界。 - 元素交換:使用 C# 7.0 引入的元組交換語(yǔ)法
(a, b) = (b, a),簡(jiǎn)潔高效(也可使用臨時(shí)變量交換)。 Random實(shí)例:在方法內(nèi)創(chuàng)建單個(gè)Random實(shí)例,避免短時(shí)間內(nèi)多次創(chuàng)建導(dǎo)致的隨機(jī)序列重復(fù)問(wèn)題。
四、算法優(yōu)勢(shì)
- 公平性:每個(gè)元素在每個(gè)位置的概率嚴(yán)格相等,無(wú)偏差。
- 高效性:僅需一次遍歷和 n-1 次交換,時(shí)間復(fù)雜度 O (n),空間復(fù)雜度 O (1)(原地洗牌,無(wú)需額外空間)。
- 適用性:適用于任何可索引的序列(數(shù)組、列表等),廣泛應(yīng)用于卡牌游戲、隨機(jī)排序、數(shù)據(jù)打亂等場(chǎng)景。
五、注意事項(xiàng)
Random的線(xiàn)程安全:若在多線(xiàn)程環(huán)境中使用,需確保Random實(shí)例的線(xiàn)程安全(可使用Random.Shared或加鎖)。- 重復(fù)執(zhí)行的隨機(jī)性:若需每次運(yùn)行生成不同的打亂結(jié)果,不要手動(dòng)指定
Random的種子(默認(rèn)使用系統(tǒng)時(shí)間作為種子)。
示例輸出
[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,方]


到此這篇關(guān)于C#洗牌算法的具體實(shí)現(xiàn)的文章就介紹到這了,更多相關(guān)C#洗牌算法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
c#高效率導(dǎo)出多維表頭excel的實(shí)例代碼
這篇文章介紹了c#高效率導(dǎo)出多維表頭excel的實(shí)例代碼,有需要的朋友可以參考一下2013-11-11
C#多線(xiàn)程編程中的鎖系統(tǒng)(四):自旋鎖
這篇文章主要介紹了C#多線(xiàn)程編程中的鎖系統(tǒng)(四):自旋鎖,本文講解了基礎(chǔ)知識(shí)、自旋鎖示例、SpinLock等內(nèi)容,需要的朋友可以參考下2015-04-04
WPF實(shí)現(xiàn)類(lèi)似360安全衛(wèi)士界面的程序源碼分享
最近在網(wǎng)上看到了新版的360安全衛(wèi)士,感覺(jué)界面還不錯(cuò),于是用WPF制作了一個(gè),時(shí)間有限,一些具體的控件沒(méi)有制作,用圖片代替了。感興趣的朋友一起跟著小編學(xué)習(xí)WPF實(shí)現(xiàn)類(lèi)似360安全衛(wèi)士界面的程序源碼分享2015-09-09
C#實(shí)現(xiàn)二叉排序樹(shù)代碼實(shí)例
今天小編就為大家分享一篇關(guān)于C#實(shí)現(xiàn)二叉排序樹(shù)代碼實(shí)例,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10
使用代理模式來(lái)進(jìn)行C#設(shè)計(jì)模式開(kāi)發(fā)的基礎(chǔ)教程
這篇文章主要介紹了使用代理模式來(lái)進(jìn)行C#設(shè)計(jì)模式開(kāi)發(fā)的基礎(chǔ)教程,代理模式主張?jiān)诳蛻?hù)端和目標(biāo)對(duì)象中間建立中介來(lái)降低程序設(shè)計(jì)的耦合度,需要的朋友可以參考下2016-02-02

