大家好,我是前端西瓜哥。

最近在试图做一个在线斗地主的游戏,为此需要实现一个洗牌算法,最后是给它实现了。一起来看看我是怎么将它实现的吧。

思路其实也不复杂,就两步:

  1. 拿到完整的一副牌(这里我们需要设计一下牌的数据存储方式)

  2. 洗牌

getShuffledCards

我们先从顶层的算法出发,将上面的两个流程抽为两个子函数。

function getShuffledCards() {const cards = getCards();shuffle(cards);return cards;
}

getCards 获取扑克牌数组

下面我们先看看 getCards 子算法。该算法的作用是返回一个完整扑克牌数组。

我们用字符串来表示一张牌。

对于牌的大小:

// 牌的大小
const nums = [3, 4, 5, 6, 7, 8, 9, 10, 'J', 'Q', 'K', 'A', 2];

至于扑克牌花色,我们用 0 到 3 表示。也可以用它们英文的首字母来表示:S、H、C、D,都可以。

// 黑桃、红心、梅花、方块
const types = [0, 1, 2, 3];

然后对它们做组合,就能表示一张特定的卡牌:

3_0 // 黑桃 3
J_3 // 方块 J

这里还缺两张比较特殊的大小王。因为他们花色的概念,所以要做特殊处理,随意找两个字符来表示。

大小王的英文为 Joker,可以考虑 J(Joker)或 K(王),但它们都被占用了。最后我就随意找两个连续字母 M 和 N 来表示了。你看这个两个字母是不是很像小丑的帽子,其实还挺像的。

const getCards = (() => {let cacheCards;return () => {if (cacheCards) return [...cacheCards];const nums = [3, 4, 5, 6, 7, 8, 9, 'A', 2, 'J', 'Q', 'K']; // 牌的大小const types = [0, 1, 2, 3]; // 黑桃、红心...cacheCards = nums.reduce((cards, curr) => {for (const type of types) {cards.push(curr + '_' + type); // 也可以使用其他分隔符}return cards;}, []);cacheCards.push('M', 'N'); // 大小王return [...cacheCards];}
})();

这里返回的是完整的一副扑克牌数组。

我用了闭包,主要是为了做缓存,因为我们每次调用这个函数的返回值其实都是一样的,缓存一下能够用空间换时间,降低时间复杂度。

这里需要注意的是,我们需要返回缓存数组的拷贝,而不是直接返回缓存数组。如果你直接返回缓存数组,返回的其实是对缓存数组的引用,因为它们指向同一个内存对象。

使用缓存需要拷贝数组,拷贝的时间复杂度是 O(n)。不做缓存不需要拷贝,时间复杂度也是 O(n),我好像优化了个寂寞。

如果你想返回两副牌,你可以在 return 前将数组自拷贝一下再放到数组尾部。

cacheCards.push(...cacheCards);

shuffle 洗牌算法

shuffle 方法是一个通用的洗牌算法,它会将传入的数组随机打乱。实现如下;

function shuffle(arr) {for (let i = arr.length - 1; i >= 0; i--) {const randIdx = getRand(0, i);[arr[randIdx], arr[i]] = [arr[i], arr[randIdx]];}
}// 获取 [min, max] 区间中的一个随机整数
function getRand(min, max) {return Math.floor(Math.random() * (max - min + 1) + min);
}

核心逻辑为:从后往前遍历,i 递减。从 0 ~ i 的索引范围内随机找一个元素,和 arr[i] 交换。

在 i 的动态变化过程中,i 右侧为打乱的元素区间,当 i 递减到 0,整个数组就洗完了。

这种实现是一种原地算法,空间复杂度为 O(1),时间复杂度为 O(n)。

两个子函数实现完了,我们来看看执行 getShuffledCards 函数的输出结果:

['K_0', 'Q_3', 'A_2', '4_1', '4_3', '8_3','N',   '8_1', 'Q_0', '3_0', '7_2', '7_0','8_0', '9_0', '3_3', 'A_0', '9_2', '2_2','8_2', '6_3', '4_2', 'J_2', '4_0', '5_2','9_3', '2_0', 'K_1', '7_1', '9_1', '6_0','Q_2', '2_1', 'J_1', '7_3', 'K_3', 'A_1','J_3', '2_3', '6_1', 'K_2', 'J_0', '6_2','3_2', 'M',   '5_3', '3_1', '5_0', 'A_3','5_1', 'Q_1'
]

西瓜哥我很满意。

将乱序的牌再排序起来

这里我们再扩展一下,实现一下将乱序的牌排好序的算法。

假设我们在玩斗地主,我们把牌洗好了,先留下给地主的 3 张牌,然后每人发 17 张牌,但都是乱序的。

玩家问:“你 TMD 能不能给我把牌排好序?日内瓦!退钱!”

function doudizhu() {const cards = getShuffledCards();const dizhuCards = cards.splice(0, 3); // 地主的额外三张牌const playerCards = [];// 剩下的牌均分playerCards[0] = cards.splice(0, cards.length / 3);playerCards[1] = cards.splice(0, cards.length / 2);playerCards[2] = cards;// 排序playerCards.forEach(cards => sortCards(cards));
}

玩家貌似很愤怒(无感情),我们赶紧来实现上面将卡牌数组排序的 sortCards 方法。

首先明确平时我们平时打牌时的排序规则。

  1. 大的牌在左边

  2. 同样大的两张牌,花色为黑桃的在最左边,方块在最右边。

实现思路就是用 JS 自带的 Math.sort() 方法进行排序,难点是怎么对比两个字符串。

我们无法用字典序,因为 A 比 K 大,大小王 M 和 N 又比较特殊。我使用的方案就是计算出它们的等价的数值,通过它们来比较。实现如下:

function sortCards(cards) {function getEqualVal(s) {if (s === 'M') return 9999999;if (s === 'N') return 999999;let [num, type] = s.split('_');if (num === 'J') num = '11'if (num === 'Q') num = '12'if (num === 'K') num = '13'if (num === 'A') num = '14'if (num == '2') num = '15'return parseInt(num) * 10 - parseInt(type);}// 用等价数的方式对比cards.sort((a, b) => {return getEqualVal(b) - getEqualVal(a);})
}

我们把牌大小作为更高的位(parseInt(num) * 10)。

对于花色,则要采取负收益的做法,因为我是用 1 来表示黑桃,3 来表示方块,排序要求从大到小,且黑桃要最左,所以需要对它取反,来保证黑桃的值要比方块的要大。

有些非数字字符,我们需要依照它们的大小,给它们提供对应的数字。然后是大小王,需要最特殊处理,直接返回非常大的比其他牌要大的等价数。

完整实现


const getCards = (() => {let cacheCards;return () => {if (cacheCards) return [...cacheCards];const nums = [3, 4, 5, 6, 7, 8, 9, 'A', 2, 'J', 'Q', 'K']; // 牌的大小const types = [0, 1, 2, 3]; // 黑桃、红心...cacheCards = nums.reduce((cards, curr) => {for (const type of types) {cards.push(curr + '_' + type); // 也可以使用其他分隔符}return cards;}, []);cacheCards.push('M', 'N'); // 大小王return [...cacheCards];}
})();function shuffle(arr) {for (let i = arr.length - 1; i >= 0; i--) {const randIdx = getRand(0, i);[arr[randIdx], arr[i]] = [arr[i], arr[randIdx]];}
}// 获取 [min, max] 区间中的一个随机整数
function getRand(min, max) {return Math.floor(Math.random() * (max - min + 1) + min);
}function getShuffledCards() {const cards = getCards();shuffle(cards);return cards;
}function sortCards(cards) {function getEqualVal(s) {if (s === 'M') return 9999999;if (s === 'N') return 999999;let [num, type] = s.split('_');if (num === 'J') num = '11'if (num === 'Q') num = '12'if (num === 'K') num = '13'if (num === 'A') num = '14'if (num == '2') num = '15'return parseInt(num) * 10 - parseInt(type);}// 用等价无的方式对比cards.sort((a, b) => {return getEqualVal(b) - getEqualVal(a);})
}function doudizhu() {const cards = getShuffledCards();const dizhuCards = cards.splice(0, 3); // 地主的额外三张牌const playerCards = [];// 剩下的牌均分playerCards[0] = cards.splice(0, cards.length / 3);playerCards[1] = cards.splice(0, cards.length / 2);playerCards[2] = cards;// 排序playerCards.forEach(cards => sortCards(cards));console.log(dizhuCards,playerCards,);
}doudizhu();

结尾

真正地给扑克牌洗牌,然后将它们发给玩家,再帮他们拍好牌,你们学会了吗?

我们看到,实现扑克牌洗牌的算法其实并没有想象中的那么简单,当然也不难。因为我们可以使用工程化的思维,将一个大问题不断地拆分,拆分成合适大小的子问题。一个个将子问题解决,大问题自然也就被解决了。

我是前端西瓜哥,最近在研究斗地主游戏,欢迎关注我,一起学前端。

真•扑克牌洗牌算法实现相关推荐

  1. 扑克牌洗牌算法-random_shuffle

    扑克牌洗牌有多种算法: 第1个:每次从原数组A取出范围[1,i]的数放入B数组. 缺点是每次都要将数组i后面的元素进行移动. 是一个o(n2)算法 void xipai(int n){ // o(n2 ...

  2. 洗牌算法java 剑指_扑克牌的完美洗牌算法

    思路: 递归思想.我们有n张牌,不妨先假设有一个洗牌函数shuffle(....),能完美的洗出n-1张牌 .拿第n张牌来打乱前面n-1的洗牌顺序,从而得到n张牌的最终结果. 代码如下: #inclu ...

  3. 使用随机数实现扑克牌洗牌的算法(弱智版)

    最近刷leetCood有点着魔了,突然想写一个扑克牌洗牌的实现方式. 大脑中第一印象就是用随机数来实现: 1)随机数范围为1-54 2)建立一个集合来存放随机生成的数 3)新随机出来的数如果该集合有, ...

  4. 卡牌大师:玩转“洗牌算法”,幸运女神在微笑 (*^_^*)

    关注并将「趣谈前端」设为星标 每日定时推送技术干货/优秀开源/技术思维 我们目前的世界仍是基于 P ≠ NP,所以有理由相信:只要我们把牌洗的足够乱,幸运女神或许就会降临.(生活就像英雄联盟,运气游戏 ...

  5. 我以前用过的一个洗牌算法

    前两天和几个做在线棋牌游戏的朋友聚会,聊到了洗牌算法,正好以前写过一些扑克牌的游戏,中间做了一个洗牌算法,就写了个例子给他们做试验. 基本思路很简单,就是交换法,54张牌排好,随机选择两张牌交换,一般 ...

  6. c++随机打乱数组_【洗牌算法】你确定这样的抽奖算法是随机的?

    洗牌算法在实际应用中使用的比较广泛,比如抽奖.三国杀游戏等等.由于要完全理解洗牌算法存在一定的难度,所以洗牌算法也经常被拿来做算法笔试题.例如以下两个常见的笔试题:在n个不同的数中随机取出不重复的m个 ...

  7. java 洗牌_java数组之完美洗牌算法

    题目详情 有个长度为2n的数组{a1,a2,a3,...,an,b1,b2,b3,...,bn},希望排序后{a1,b1,a2,b2,....,an,bn},请考虑有无时间复杂度o(n),空间复杂度0 ...

  8. 一步一步写算法(之洗牌算法)

    [ 声明:版权全部,欢迎转载,请勿用于商业用途.  联系信箱:feixiaoxing @163.com] 扑克牌洗牌是我们生活中比較喜欢玩的一个游戏.那么我们有没有什么办法自己设计一个扑克牌洗牌的方法 ...

  9. 扑克游戏的洗牌算法及简单测试

    2019独角兽企业重金招聘Python工程师标准>>> 我在学习<写给大家看的C语言书>这本书时,对书后面附录的一个扑克游戏程序非常感兴趣.源代码在帖子最后. PS:这本 ...

  10. 洗牌程序c语言原理,C语言经典算法 - 洗牌算法

    下面是编程之家 jb51.cc 通过网络收集整理的代码片段. 编程之家小编现在分享给大家,也给大家做个参考. 说明: 洗扑克牌的原理其实与乱数排列是相同的,都是将一组数字(例如1-N)打乱重新排列,只 ...

最新文章

  1. 在Flash中利用PCRE正则式漏洞CVE-2015-0318的方法
  2. 闲诗一首:《扬州即行》
  3. Swift团队把Swift之父气跑了,网友:Python之父的仁慈独裁者模式才是王道
  4. HDOJ 2041 超级楼梯
  5. java对.txt文件进行读取方法实战---室友是网络安全方向,帮他提取文件里的特定ip
  6. web高德maker动画_Web Maker —我如何构建一个快速的离线前端游乐场
  7. C语言 野指针 - C语言零基础入门教程
  8. 应用层——使用 Socket 通信实现 FTP 客户端程序
  9. java listview用法_Java ListView.setMultiChoiceModeListener方法代码示例
  10. 使用 jQuery Mobile 与 HTML5 开发 Web App (三) —— jQuery Mobile 按钮
  11. [Python] 更改矩阵形状:reshape(m,n)、view(m,n)和view_as(tensor)
  12. Java自然语言处理NLP工具包
  13. linux下wps的字体缺失解决方法
  14. Selenium爬虫 -- 操控滚动条方法
  15. jdk Integer 具体实现
  16. eclipse mysql tomcat,java+eclipse+tomcat+mysql+jdbc
  17. python制作烟花特效_过大年,用Python去绽放最绚丽的烟花
  18. iOS 开发中出现假死解决思路
  19. 对接支付宝、微信、第三方支付,超详细讲解+demo演示
  20. 微信小程序第七章 图片替换

热门文章

  1. kali kda安装 linux_KDA-无损音乐下载
  2. NVIDIA NGC镜像使用笔记
  3. 基于labview的打地鼠游戏制作
  4. 一个咖啡杯装下全世界的数据,DNA存储芯片神奇在哪?
  5. GeoTools入门(八)-- (SLD)样式处理
  6. 鹤林全集·闲情志友 | 第一篇——阿鑫
  7. 图灵机和通用计算机,数学的不完美之美——阿兰?图灵与图灵机
  8. myBattery电池应用正式登陆WP8
  9. MySQL多表左右连接查询
  10. 要读的书---培根说:历史使人明智,诗词使人巧慧,算学使人精密,哲理使人深刻,伦理学人庄重,逻辑修辞使人善辩。...