C#洗牌算法

发布于:2025-07-15 ⋅ 阅读:(21) ⋅ 点赞:(0)

洗牌算法是一种将序列(如数组、列表)元素随机打乱的经典算法,核心目标是让每个元素在打乱后出现在任意位置的概率均等。在 C# 中,常用的洗牌算法有Fisher-Yates 洗牌算法(也称 Knuth 洗牌算法),它高效且公平,时间复杂度为 O (n),空间复杂度为 O (1)。

一、Fisher-Yates 洗牌算法原理

  1. 核心思想:从序列的最后一个元素开始,依次与前面的随机位置元素交换,直到处理完第一个元素。

  2. 公平性保证:每个元素被放置在任意位置的概率均为 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}]";
        }
​
    }
​
}
​

三、代码说明

  1. 泛型方法Shuffle<T> 支持任意类型的数组和列表,通用性强。

  2. 随机索引生成random.Next(i + 1) 确保生成的索引 j[0, i] 范围内,避免越界。

  3. 元素交换:使用 C# 7.0 引入的元组交换语法 (a, b) = (b, a),简洁高效(也可使用临时变量交换)。

  4. 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,方]


网站公告

今日签到

点亮在社区的每一天
去签到