由于研究Libra等数字货币编程技术的需要,学习了一段时间的Rust编程,一不小心刷题上瘾。

刷完欧拉计划中的63道基础题,能学会Rust编程吗?

“欧拉计划”的网址:

https://projecteuler.net

英文如果不过关,可以到中文翻译的网站:

http://pe-cn.github.io/

这个网站提供了几百道由易到难的数学问题,你可以用任何办法去解决它,当然主要还得靠编程,编程语言不限,论坛里已经有Java、C#、Python、Lisp、Haskell等各种解法,当然如果你直接用google搜索答案就没任何乐趣了。

这次解答的是第54题:

https://projecteuler.net/problem=54

题目描述:

扑克手牌

在扑克游戏中,玩家的手牌由五张牌组成,其等级由低到高分别为:

  • 单牌:牌面最大的一张牌。
  • 对子:两张牌面一样的牌。
  • 两对:两个不同的对子。
  • 三条:三张牌面一样的牌。
  • 顺子:五张牌的牌面是连续的。
  • 同花:五张牌是同一花色。
  • 葫芦:三条带一个对子。
  • 四条:四张牌面一样的牌。
  • 同花顺:五张牌的牌面是连续的且为同一花色。
  • 同花大顺:同一花色的10、J、Q、K、A。

牌面由小到大的顺序是:2、3、4、5、6、7、8、9、10、J、Q、K、A。

如果两名玩家的手牌处于同一等级,那么牌面较大的一方获胜;例如,一对8胜过一对5(参见例1);如果牌面相同,例如双方各有一对Q,那么就比较玩家剩余的牌中最大的牌(参见例4);如果最大的牌相同,则比较次大的牌,依此类推。

S代表黑桃(Spade),H表示红桃(Heart),D表示方块(Diamond),C表示梅花(Club),T表示10(Ten),考虑以下五局游戏中双方的手牌:

手牌 玩家1 玩家2 胜者
1 5H 5C 6S 7S KD 2C 3S 8S 8D TD 玩家2
一对5 一对8
2 5D 8C 9S JS AC 2C 5C 7D 8S QH 玩家1
单牌A 单牌Q
3 2D 9C AS AH AC 3D 6D 7D TD QD 玩家2
三条A 同花方片
4 4D 6S 9H QH QC 3D 6D 7H QD QS 玩家1
一对Q 一对Q
最大单牌9 最大单牌7
5 2H 2D 4C 4D 4S 3C 3D 3S 9S 9D
葫芦 葫芦
(三条4) (三条3)

poker.txt文本文件中,包含有两名玩家一千局的手牌。每一行包含有10张牌(均用一个空格隔开):前5张牌属于玩家1,后5张牌属于玩家2。你可以假定所有的手牌都是有效的(没有无效的字符或是重复的牌),每个玩家的手牌不一定按顺序排列,且每一局都有确定的赢家。

其中有多少局玩家1获胜?

解题过程:

遇到一个复杂的问题,可以尝试将问题分解,变为一个个简单的情况,然后慢慢逼近最终的问题。

第一步: 先读文件,将玩家1和玩家2的牌分开。

第22题里已经学会了读文件,并且将字符串分隔成向量,再利用切片功能将前5个赋给玩家1,后5个赋给玩家2。

  1. let data = std::fs::read_to_string("poker.txt").expect("打开文件出错");
  2. let data2 = data.replace("\r\n", "\n");
  3. let lines = data2.trim().split('\n');
  4. for line in lines {
  5. let hand1 = &line[..14];
  6. let hand2 = &line[15..];
  7. println!("{:?} {:?}", hand1, hand2);
  8. }

第二步: 多文件管理

这个项目涉及到手牌、牌张、花色等概念,适合用面向对象的编程思路。Rust项目对多源文件的功能支持也相当不错,main.rs放主程序,poker.rs放扑克相关的模块。

一手牌Hand由多张牌Card组成,一个Card由牌点(用8位整数表示)和花色Suit构成,花色只有4种,适合用枚举表示。Rust里的枚举看上去与C/C#/Java等语言的枚举很像,但实际上它的功能远远不是一个简单的枚举。

  1. // 文件poker.rs
  2. pub enum Suit {
  3. Spade, // 黑桃
  4. Heart, // 红桃
  5. Diamond, // 方块
  6. Club, // 梅花
  7. }
  8. pub struct Card {
  9. value: u8, // 用2到14表示2, 3, ..., 10, J, Q, K, A
  10. suit: Suit,
  11. }
  12. pub struct Hand {
  13. cards: Vec<Card>,
  14. }

main.rs需要加一行语句,告诉主程序要使用poker.rs中定义的模块。

  1. mod poker;

这个时候,程序可以编译,会给出几个警告,提示Hand,Card和Suit这些类型从来没用过。

第三步: 构建一张牌Card

我们的任务要通过一个字符串构建出一个Card对象。比如,"8C"构建出梅花8,"TS"构建也黑桃10,"KC"为梅花K,"9H"为红桃9,"4S"为黑桃4。

这个时候要先学会Rust中的Trait概念,Trait这个东西很像Java/C#里的接口,但又不是。Rust内置不支持构造函数,下面这段代码相当于给Card定义了一个静态方法new(),相当于其它语言里的构造函数。

  1. impl Card {
  2. pub fn new(str_card: &str) -> Card {
  3. let first_char = str_card.chars().next().unwrap();
  4. let card_value = "..23456789TJQKA"
  5. .chars()
  6. .position(|c| c == first_char)
  7. .unwrap() as u8;
  8. let second_char = str_card.chars().nth(1).unwrap();
  9. let card_suit = if second_char == 'S' {
  10. Suit::Spade
  11. } else if second_char == 'H' {
  12. Suit::Heart
  13. } else if second_char == 'D' {
  14. Suit::Diamond
  15. } else {
  16. Suit::Club
  17. };
  18. Card {
  19. value: card_value,
  20. suit: card_suit,
  21. }
  22. }
  23. }

主程序里可以构造一个card(这里用梅花8),尝试打印出来。

  1. let card = poker::Card::new("8C");
  2. println!("{:?}", card);

编译时Rust会给出相当清楚的错误信息,还给出了修改建议

  1. error[E0277]: `poker::Card` doesn't implement `std::fmt::Debug`
  2. --> src\main.rs:14:22
  3. |
  4. 14 | println!("{:?}", card);
  5. | ^^^^ `poker::Card` cannot be formatted using `{:?}`
  6. |
  7. = help: the trait `std::fmt::Debug` is not implemented for `poker::Card`
  8. = note: add `#[derive(Debug)]` or manually implement `std::fmt::Debug`
  9. = note: required by `std::fmt::Debug::fmt`

我们声明了一个新类型Card,但系统并不知道如何把它转换成字符串显示出来。按照提示,我们在Card和Suit前面各加上一行语句,让系统帮我们自动实现一个输出格式。

  1. #[derive(Debug)]
  2. pub enum Suit {
  3. Spade, // 黑桃
  4. Heart, // 红桃
  5. Diamond, // 方块
  6. Club, // 梅花
  7. }
  8. #[derive(Debug)]
  9. pub struct Card {
  10. value: u8, // 用2到14表示2, 3, ..., 10, J, Q, K, A
  11. suit: Suit,
  12. }

此时程序可以顺利编译,运行可以得到如下结果:

  1. Card { value: 8, suit: Club }

输出得虽然有点复杂,但容易理解。如果我们就想输出"8C"这样的字符串,则需要实现Display这个trait里的fmt()函数。注意write!语句后面千万别习惯性地加个分号,否则出现的编译错误让人好困惑!

  1. use std::fmt::{Display, Error, Formatter};
  2. impl Display for Suit {
  3. // 只用一个字母表示: S,H,D,C
  4. fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
  5. let name = format!("{:?}", self);
  6. write!(f, "{}", &name[..1])
  7. }
  8. }
  9. impl Display for Card {
  10. fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
  11. let first_char = "..23456789TJQKA".chars().nth(self.value as usize).unwrap();
  12. write!(f, "{}{}", first_char, self.suit)
  13. }
  14. }

现在构建5张牌,输出出来。

  1. let card1 = poker::Card::new("8C");
  2. let card2 = poker::Card::new("TS");
  3. let card3 = poker::Card::new("KC");
  4. let card4 = poker::Card::new("9H");
  5. let card5 = poker::Card::new("4S");
  6. println!("{} {} {} {} {}", card1, card2, card3, card4, card5);

第四步: 构建一手牌Hand

对于Hand,也要实现fmt()函数,还要实现一个构造函数new()。

  1. use itertools::Itertools;
  2. impl Display for Hand {
  3. fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
  4. let str_cards = self.cards.iter().map(|x| x.to_string()).join(" ");
  5. write!(f, "{}", str_cards)
  6. }
  7. }
  8. impl Hand {
  9. pub fn new(str_cards: &str) -> Hand {
  10. let mut v = vec![];
  11. for s in str_cards.split(' ') {
  12. v.push(Card::new(s));
  13. }
  14. Hand { cards: v }
  15. }
  16. }

主程序这样写:

  1. let hand = poker::Hand::new("8C TS KC 9H 4S");
  2. println!("{}", hand);

第五步: 比较两个对象的大小

现在我们想比较两手牌的大小,主程序写成这样。

  1. let hand1 = poker::Hand::new("8C TS KC 9H 4S");
  2. let hand2 = poker::Hand::new("7D 2S 5D 3S AC");
  3. if hand1 > hand2 {
  4. println!("player1 wins" );
  5. }

想让两个对象能够相互比较大小,需要实现四个trait(Ord、PartialOrd、Eq和PartialEq)中的几个函数。

  1. use std::cmp::{Ord, Ordering};
  2. impl Ord for Hand {
  3. fn cmp(&self, other: &Self) -> Ordering {
  4. self.to_string().cmp(&other.to_string())
  5. }
  6. }
  7. impl PartialOrd for Hand {
  8. fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
  9. Some(self.cmp(other))
  10. }
  11. }
  12. impl Eq for Hand {}
  13. impl PartialEq for Hand {
  14. fn eq(&self, other: &Self) -> bool {
  15. self.to_string().eq(&other.to_string())
  16. }
  17. }

现在,这里的比较逻辑还没有实现,暂时用字符串的比较代替。有几点要留意:

1)Ord里的cmp()函数,PartialOrd里的partial_cmp()函数,一个是表示全序,一个表示偏序。

2)cmp()和partial_cmp()两个函数的返回值有点区别,后面的多Option<>

3)Eq里的内容是空的,但必须要写

4)PartialEq里的函数名是eq()

5)实现了这些trait后,程序会自动理解“<”、“>”、“==”这些比较运算符。

第六步: 比较两手牌的大小

这时需要细心了,判断同花、顺子、四条、三条、对子等情况,为了后面的比较,我声明了一个枚举enum,用来区分各种牌型,从这里可以领略Rust里枚举的强大。

  1. pub enum HandType {
  2. HighCard(u8, u8, u8, u8, u8), // 单牌
  3. //对子
  4. OnePair {
  5. value_pair: u8,
  6. max_remain: u8, // 除了对子之外,剩下最大的牌点
  7. },
  8. //两对
  9. TwoPairs {
  10. high_pair: u8, // 最大的一对
  11. low_pair: u8, // 最小的一对
  12. max_remain: u8, // 除了两对之外,剩下最大的牌点
  13. },
  14. KindThree(u8), // 三条
  15. Straight(u8), // 顺子
  16. Flush(u8), // 同花
  17. //葫芦,即三条带一个对子
  18. FullHouse {
  19. value_kind_three: u8,
  20. value_pair: u8,
  21. },
  22. KindFour(u8), // 四条
  23. StraightFlush(u8), // 同花顺
  24. RoyalFlush, // 同花大顺
  25. }

再声明两个函数ranking1()和ranking2(),两次比较后能够区分大小。

  1. impl HandType {
  2. pub fn ranking1(&self) -> u8 {
  3. match self {
  4. HighCard(_, _, _, _, _) => 0,
  5. OnePair { .. } => 1,
  6. TwoPairs { .. } => 2,
  7. KindThree(_) => 3,
  8. Straight(_) => 4,
  9. Flush(_) => 5,
  10. FullHouse { .. } => 6,
  11. KindFour(_) => 7,
  12. StraightFlush(_) => 8,
  13. RoyalFlush => 9,
  14. }
  15. }
  16. pub fn ranking2(&self) -> u64 {
  17. // ...
  18. }

这里有一个".."的语法点,忽略结构体里的内容。

  1. FullHouse { .. } => 6,

相当于:

  1. FullHouse {
  2. value_kind_three: _,
  3. value_pair: _,
  4. } => 6,

Ord里的cmp()和PartialEq里的eq()的逻辑也要相应修改一下。

  1. impl Ord for HandType {
  2. fn cmp(&self, other: &Self) -> Ordering {
  3. self.ranking1()
  4. .cmp(&other.ranking1())
  5. .then(self.ranking2().cmp(&other.ranking2()))
  6. }
  7. }
  8. impl PartialEq for HandType {
  9. fn eq(&self, other: &Self) -> bool {
  10. self.ranking1() == other.ranking1() && self.ranking2() == other.ranking2()
  11. }
  12. }

剩下就是依次判断各种情况,需要足够的耐心和测试。

  1. use itertools::Itertools;
  2. impl Hand {
  3. //是否五张牌同一个花色。
  4. fn is_flush(&self) -> bool {
  5. self.cards.iter().map(|card| card.suit).all_equal()
  6. }
  7. //判断五张牌是否连号,先将牌面数值从小到大排序,两两之差为1就是顺子。
  8. fn is_straight(&self) -> bool {
  9. let mut v: Vec<u8> = self.cards.iter().map(|x| x.value).collect();
  10. v.sort();
  11. (0..4).all(|i| v[i + 1] - v[i] == 1) //两两之差都为1
  12. }

第七步: 将相关类整理到一个文件夹下

源文件较多时,可以放在一个文件夹下,模块里还可以有子模块。比如这样组织文件:

  1. src/
  2. +---main.rs
  3. +---poker/
  4. +---card.rs
  5. +---hand.rs
  6. +---hand_type.rs
  7. +---mod.rs
  8. +---suit.rs

poker文件夹可以自动识别为一个mod,需要mod.rs文件的配合,这里声明用到的子模块,编译器可以自动找到相应的源文件。

  1. pub mod card;
  2. pub mod hand;
  3. pub mod hand_type;
  4. pub mod suit;

hand.rs文件里使用其它模块的内容时,需要用use语句。

  1. use super::card::*;
  2. use super::hand_type::*;

--- END ---

我把解题的过程记录了下来,写成了一本《用欧拉计划学 Rust 编程》PDF电子书,请随意下载。

链接:https://pan.baidu.com/s/1NRfTwAcUFH-QS8jMwo6pqw

提取码:qfha

该PDF文件将来会不定期更新,可以在公众号后台回复“rust”,得到最新的下载链接。

历史文章:

学会10多种语言是种什么样的体验?

刷完欧拉计划中的63道基础题,能学会Rust编程吗?

通过欧拉计划学Rust编程(第54题)的更多相关文章

  1. 通过欧拉计划学Rust编程(第500题)

    由于研究Libra等数字货币编程技术的需要,学习了一段时间的Rust编程,一不小心刷题上瘾. "欧拉计划"的网址: https://projecteuler.net 英文如果不过关 ...

  2. 用欧拉计划学Rust编程(第26题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  3. 通过欧拉计划学习Rust编程(第22~25题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  4. 用欧拉计划学Rust语言(第17~21题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  5. 用欧拉计划学习Rust编程(第13~16题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  6. 用欧拉计划学Rust语言(第7~12题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,所以先补一下Rust的基础知识.学习了一段时间,发现Rust的学习曲线非常陡峭,不过仍有快速入门的办法. 学习任何一项技能最怕没有 ...

  7. 通过欧拉计划学Rust(第1~6题)

    最近想学习Libra数字货币的MOVE语言,发现它是用Rust编写的,看来想准确理解MOVE的机制,还需要对Rust有深刻的理解,所以开始了Rust的快速入门学习. 看了一下网上有关Rust的介绍,都 ...

  8. 刷完欧拉计划中难度系数为5%的所有63道题,我学会了Rust中的哪些知识点?

    我为什么学Rust? 2019年6月18日,Facebook发布了数字货币Libra的技术白皮书,我也第一时间体验了一下它的智能合约编程语言MOVE,发现这个MOVE是用Rust编写的,看来想准确理解 ...

  9. 【欧拉计划4】Largest palindrome product

    欢迎访问我的新博客:http://www.milkcu.com/blog/ 原文地址:http://www.milkcu.com/blog/archives/1371281760.html 原创:[欧 ...

随机推荐

  1. Callable,阻塞队列,线程池问题

    一.说说Java创建多线程的方法 1. 通过继承Thread类实现run方法   2. 通过实现Runnable接口 3. 通过实现Callable接口 4. 通过线程池获取 二. 可以写一个Call ...

  2. [vsCode实践] 实践记录

    [vsCode实践] 实践记录 版权2019.5.1更新 Q1:代码中涉及到操作本地文件时,相对路径总是不对 操作本地文件时,路径方式有两种 相对路径 例如:代码文件所在路径/Users/tp0829 ...

  3. 「BZOJ1385」「Baltic2000」Division expression 解题报告

    Division expression Description 除法表达式有如下的形式: \(X_1/X_2/X_3.../X_k\) 其中Xi是正整数且\(X_i \le 1000000000(1 ...

  4. 「Luogu P2278」[HNOI2003]操作系统 解题报告

    题面 一道模拟题,模拟CPU的处理过程?!省选模拟题 思路: 模拟退火大法+优先队列乱搞 要注意的点 1.空闲时,CPU要处理进程 2.当队列中没有进程时,要先进行判断,然后访问 3.当优先级高的进程 ...

  5. 快速开发架构Spring Boot 从入门到精通 附源码

    导读 篇幅较长,干货十足,阅读需花费点时间.珍惜原创,转载请注明出处,谢谢! Spring Boot基础 Spring Boot简介 Spring Boot是由Pivotal团队提供的全新框架,其设计 ...

  6. GDAL集成GEOS

    因为要用到缓冲区分析,在使用Buffer的时候提示:ERROR 6: GEOS support not enabled,查了一下资料需要集成GEOS库.因为GDLA默认编译是没有集成GEOS库的. 现 ...

  7. Java List集合的介绍与常用方法

    List接口的介绍 List接口简介: java.util.List接口继承自Collection接口,是单列集合的一个重要分支,习惯性地会将实现了List接口的对象称为List集合. 在List集合 ...

  8. SpringCloud-Hystrix原理

    Hystrix官网的原理介绍以及使用介绍非常详细,非常建议看一遍,地址见参考文档部分. 一 Hystrix原理 1 Hystrix能做什么 通过hystrix可以解决雪崩效应问题,它提供了资源隔离.降 ...

  9. springboot2 整合redis

    1.添加依赖 <dependency> <groupId>org.springframework.boot</groupId> <artifactId> ...

  10. Spring Boot2 系列教程 (九) | SpringBoot 整合 Mybatis

    前言 如题,今天介绍 SpringBoot 与 Mybatis 的整合以及 Mybatis 的使用,本文通过注解的形式实现. 什么是 Mybatis MyBatis 是支持定制化 SQL.存储过程以及 ...