洗牌算法是一种将序列(如数组、列表)元素随机打乱的经典算法,核心目标是让每个元素在打乱后出现在任意位置的概率均等。在 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; // 牌的花色 黑红梅方 4321 public 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,方]
到此这篇关于c#洗牌算法的具体实现的文章就介绍到这了,更多相关c#洗牌算法内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论