当前位置: 首页 > news >正文

高效洗牌:Fisher-Yates算法详解

        在编程中,有时候我们需要对数据进行随机打乱操作,比如卡牌游戏中的洗牌、抽奖程序中的随机排序等。

        而实现真正公平且高效的随机打乱并非易事,那么,我们可以尝试使用 Fisher-Yates 算法解决这一问题。


什么是 Fisher-Yates 算法?

        Fisher-Yates 算法(也称为 Knuth 洗牌算法)是一种用于随机打乱数组元素顺序的高效算法,它由英国统计学家 Ronald Fisher 和 Frank Yates 于 1938 年提出,后来经计算机科学家 Donald Knuth 改进并推广。该算法的核心思想是通过迭代方式,从数组末尾开始,将每个元素与它前面(包括自身)的随机一个元素进行交换,从而实现完全随机的排列。


为什么选择 Fisher-Yates 算法?

Fisher-Yates 算法相比其他洗牌方法有几个显著优势:

  • 公平性:每个元素出现在每个位置的概率相等,确保了真正的随机性。
  • 高效性:时间复杂度为 O (n),只需遍历一次数组。
  • 空间效率:原地洗牌,不需要额外的存储空间,空间复杂度为 O (1)。

        相比之下,一些直观但低效的方法(如生成随机序列后排序)不仅时间复杂度高(O (n log n)),还可能导致概率分布不均。


Fisher-Yates 洗牌算法原理

        Fisher-Yates 算法的核心思想是通过逐步交换数组元素实现随机排列。算法从数组末尾开始,每次选择一个随机位置(范围从当前索引到数组起始位置),并将该位置的元素与当前索引位置的元素交换。这一过程确保每个元素在最终排列中出现的概率均等。


算法步骤

  1. 初始化数组
    给定一个长度为 n 的数组 arr,从最后一个元素(索引 n-1)开始逆向遍历。

  2. 随机选择交换位置
    对于当前索引 i(从 n-1 递减到 1),生成一个随机整数 j,满足 0 ≤ j ≤ i

  3. 交换元素
    arr[i]arr[j] 交换,确保每个元素在未处理部分被选中的概率一致。

        这个过程可以想象成:我们从牌堆底部开始,每次随机从剩下的牌中抽一张放到当前位置,然后继续处理前一张牌。

  1. 假设有一个长度为 n 的数组。
  2. 从数组的最后一个元素(索引 i = n-1)开始处理。
  3. 生成一个范围在 [0, i] 之间的随机整数 j。
  4. 交换数组中索引 i 和 j 处的元素。
  5. 将 i 的值减 1,重复步骤 3-4,直到 i 等于 0。

数学证明

        每一步交换时,元素 arr[i] 被固定到最终位置的概率为 1/n,且后续操作不会影响其位置。通过数学归纳法可证明所有排列的概率均为 1/n!,满足均匀随机性。


应用场景

  • 扑克牌洗牌、随机播放音乐列表等需要公平随机排列的场景。
  • 机器学习中的数据集随机化处理。

实际应用案例:卡牌游戏洗牌

        Fisher-Yates 算法最经典的应用就是卡牌游戏中的洗牌操作。以下是基于该算法完成斗地主洗牌发牌系统,展示如何使用该算法实现一副扑克牌的洗牌:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>// 常量定义
#define TOTAL_CARDS 54       // 总牌数
#define PLAYER_CARDS 17      // 每个玩家的牌数
#define BOTTOM_CARDS 3       // 底牌数量
#define SUIT_COUNT 4         // 花色数量
#define RANK_COUNT 13        // 每种花色的牌数
#define JOKER_COUNT 2        // 王牌数量// 牌的花色和点数定义
const char *suits[] = {"红桃", "黑桃", "方块", "梅花"};
const char *ranks[] = {"3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A", "2"};
const char *jokers[] = {"小王", "大王"};// 牌的结构体
typedef struct {int suit;  // 0-3:普通花色, 4:小王, 5:大王int rank;  // 0-12:普通牌点数, -1:王牌
} Card;// 函数声明
void initDeck(Card *deck);
void shuffleDeck(Card *deck);
void dealCards(const Card *deck, Card *player1, Card *player2, Card *player3, Card *bottomCards);
void printCard(Card card);
void sortCards(Card *cards, int count);
int compareCards(const void *a, const void *b);int main() {Card deck[TOTAL_CARDS];Card player1[PLAYER_CARDS], player2[PLAYER_CARDS], player3[PLAYER_CARDS];Card bottomCards[BOTTOM_CARDS];// 初始化并洗牌initDeck(deck);shuffleDeck(deck);// 发牌dealCards(deck, player1, player2, player3, bottomCards);// 排序玩家的牌sortCards(player1, PLAYER_CARDS);sortCards(player2, PLAYER_CARDS);sortCards(player3, PLAYER_CARDS);// 打印结果printf("玩家 1 的牌:\n");for (int i = 0; i < PLAYER_CARDS; i++) {printCard(player1[i]);}printf("\n\n");printf("玩家 2 的牌:\n");for (int i = 0; i < PLAYER_CARDS; i++) {printCard(player2[i]);}printf("\n\n");printf("玩家 3 的牌:\n");for (int i = 0; i < PLAYER_CARDS; i++) {printCard(player3[i]);}printf("\n\n");printf("底牌:\n");for (int i = 0; i < BOTTOM_CARDS; i++) {printCard(bottomCards[i]);}printf("\n");return 0;
}// 初始化牌组
void initDeck(Card *deck) {int index = 0;// 初始化普通牌for (int suit = 0; suit < SUIT_COUNT; suit++) {for (int rank = 0; rank < RANK_COUNT; rank++) {deck[index].suit = suit;deck[index].rank = rank;index++;}}// 初始化王牌deck[index].suit = 4;  // 小王deck[index].rank = -1;index++;deck[index].suit = 5;  // 大王deck[index].rank = -1;
}// 洗牌(Fisher-Yates 算法)
void shuffleDeck(Card *deck) {srand((unsigned)time(NULL));for (int i = TOTAL_CARDS - 1; i > 0; i--) {int j = rand() % (i + 1);  // 生成0到i的随机数// 交换牌Card temp = deck[i];deck[i] = deck[j];deck[j] = temp;}
}// 发牌
void dealCards(const Card *deck, Card *player1, Card *player2, Card *player3, Card *bottomCards) {int index = 0;// 给三个玩家发牌for (int i = 0; i < PLAYER_CARDS; i++) {player1[i] = deck[index++];player2[i] = deck[index++];player3[i] = deck[index++];}// 发底牌for (int i = 0; i < BOTTOM_CARDS; i++) {bottomCards[i] = deck[index++];}
}// 打印单张牌
void printCard(Card card) {if (card.suit >= 4) {// 王牌printf("%s\t", jokers[card.suit - 4]);} else {// 普通牌printf("%s%s\t", suits[card.suit], ranks[card.rank]);}
}// 排序牌组
void sortCards(Card *cards, int count) {qsort(cards, count, sizeof(Card), compareCards);
}// 牌的比较函数(用于排序)
int compareCards(const void *a, const void *b) {const Card *cardA = (const Card *)a;const Card *cardB = (const Card *)b;// 先比较点数(大小王特殊处理)int valueA, valueB;if (cardA->suit >= 4) {valueA = 15 + (cardA->suit - 4);  // 小王15,大王16} else {valueA = cardA->rank + 3;  // 3是3,...,2是15}if (cardB->suit >= 4) {valueB = 15 + (cardB->suit - 4);} else {valueB = cardB->rank + 3;}// 如果点数不同,直接比较点数if (valueA != valueB) {return valueA - valueB;}// 点数相同则比较花色return cardA->suit - cardB->suit;
}

常见误区与注意事项

  1. 随机数生成器的初始化:在实际应用中,应确保随机数生成器只初始化一次,而不是在每次洗牌时都初始化。

  2. 伪随机性的局限:计算机生成的是伪随机数,对于加密级别的随机需求,需要使用专门的加密随机数生成器。

  3. 数组越界问题:确保随机索引 j 的范围是 [0, i],而不是 [0, n-1],否则会导致概率分布不均。

  4. 原地修改 vs 副本:根据需求决定是原地修改数组还是返回新数组,避免意外修改原数据。


总结

        Fisher-Yates 算法是实现公平高效洗牌的最佳选择,它通过简单而巧妙的思路,确保了每个元素都有均等的机会出现在任何位置。无论是开发游戏、实现随机抽样,还是需要打乱数据顺序,Fisher-Yates 算法都是值得掌握的基础算法。

        掌握这一算法不仅能帮助你写出更高效的代码,也能让你理解随机性在计算机科学中的重要性和实现方式。下次需要打乱数组时,不妨试试 Fisher-Yates 算法!

http://www.dtcms.com/a/314192.html

相关文章:

  • 软考 系统架构设计师系列知识点之杂项集萃(118)
  • 直播 app 系统架构分析
  • 如何在 Ubuntu 24.04 LTS 上安装 Docker
  • 计算机网络:
  • 团购商城 app 系统架构分析
  • (五)系统可靠性设计
  • android TextView lineHeight 是什么 ?
  • 国产化低代码平台如何筑牢企业数字化安全底座
  • 学习日志27 python
  • 远程机器操作--学习系列004
  • Vue Router快速入门
  • 数据从mysql迁移到postgresql
  • Petalinux快捷下载
  • 项目一:Python实现PDF增删改查编辑保存功能的全栈解决方案
  • WPF 按钮背景色渐变
  • LLM开发——基于Graph RAG知识图谱检索增强生成
  • steam Rust游戏 启动错误,删除sys驱动,亲测有效。
  • MySQL 约束知识体系:八大约束类型详细讲解
  • Spring Cloud Gateway 实现登录校验:构建统一认证入口
  • 网站从HTTP升级到HTTPS网址方法
  • AWS Lambda Function 全解:无服务器计算
  • 力扣top100--哈希
  • AWS VPC Transit Gateway 可观测最佳实践
  • 【MySQL】配置复制拓扑
  • Qt 商业应用开发流程与规范
  • 【Pytorch✨】LSTM03 三大门
  • 飞算科技:用自主创新技术,为行业数字化转型按下 “加速键”
  • Selenium教程(Python 网页自动化测试脚本)
  • 补:《每日AI-人工智能-编程日报》--2025年7月31日
  • 每日一leetcode:移动零