zoukankan      html  css  js  c++  java
  • 啊哈算法之纸牌游戏小猫钓鱼

    简述

    本算法摘选自啊哈磊所著的《啊哈!算法》第二章第三节的题目——纸牌游戏小猫钓鱼。文中代码使用C语言编写,但是仔细看了一遍发现原书中有个细节是错误的,也就是说按照算法题目意思,原书中作者的代码是有出入的,具体可以往本篇博文继续看。

    博主通过阅读和理解,重新由Java代码实现了一遍,意在深刻理解队列和栈这两种数据结构的特性和操作方法,并希望能够在这种数据结构的帮助之下,解决其他的类似的能够用队列和栈来解决的问题。(哈哈,偷懒了,引用这类简述屡试不爽^_^)

    纸牌游戏

    “小猫钓鱼”的游戏规则是这样的:将一副扑克牌平均分成两份,每人拿一份。玩家U1先拿出手中的第一张扑克牌放在桌上,然后玩家U2也拿出手中的第一张扑克牌,并放在玩家U1刚打出的扑克牌的上面,就像这样两个玩家交替出牌。出牌时,如果某人打出的牌与桌上某张牌的牌面相同,即可将两张相同的牌及其中间所夹的牌全部取走,并依次放到自己手中牌的末尾。当任意一个人手中的牌全部出完时,游戏结束,对手获胜。

    现在要求你写一个算法来模拟这场游戏,并判断出谁最后获胜,获胜的同事打印出获胜者手中的牌以及桌上可能剩余的牌。

    在写程序开始前,我们暂且先做一个约定,玩家U1和U2手中牌的牌面值只有1~9

    解法思路

    我们可以先分析一下这个游戏存在哪几种操作。玩家U1有两种操作,分别是出牌和赢牌,出牌时将手中的牌打出去,赢牌的时候将桌上的牌放到手中牌的末尾,这恰好对应了队列的两个操作,出牌就是队列出队,赢牌就是队列入队。玩家U1和玩家U2的操作是一样的。而桌子很明显就可以看作是一个栈,玩家没打出一站牌就放到桌上,相当于入栈,因为顺序是往后的,当有人赢牌的时候,依次将牌从桌上拿走,这就相当于出栈。那如何判断是否赢牌呢?赢牌的判断就是:如果某人打出的牌与桌上的某张牌相同,即可将两张牌及其中间的所夹得牌全部取走。那如何直到桌上现在有哪些牌呢,很容易想到的就是每次打出牌之后遍历一遍桌上已有的牌然后比对,存在相同的牌则算赢牌。这是简单而且粗暴一点的方法,其实有更好的方法,那就是用桶来解决这个问题,牌面值只有1-9,我们可以设置一个大小为10的数组作为桶,每打出一张牌,就以此牌的牌面值作为下标找到数组对应位置,如果该位置存在值,则说明桌上有存在的牌,如果没有值,则说明桌上没有相同的牌,同时在通上做标记,即数组该下标位置设置为1。那如果赢牌了,桌上的牌拿走了桶应该怎么办呢?也很简单,出栈的时候依次按照牌面值清空桶就行了。最终怎么判断哪个玩家获得最终胜利呢,获得最终胜利的标准就是对方手上已经没牌了,如果从队列角度看,那就是头指针和尾指针相等了。

    从上面的思路分析可以看出,为了模拟这场游戏,我们需要准备两个队列、一个栈和一个桶,分别表示玩家U1U2手中的牌、桌上的牌以及桌上牌的牌面值。下面我们写具体的代码,为了方便阅读,关键代码上面我都给出非常详细的注释。

    代码实现

      1 /**
      2  * @Project: dailypro
      3  * @PackageName: com.captainad.algorithm
      4  * @Author: Captain&D
      5  * @Website: https://www.cnblogs.com/captainad/
      6  * @DateTime: 2019/6/12 21:07.
      7  * @Description:
      8  */
      9 public class KittenFishingGame {
     10 
     11     /**
     12      * 自定义队列
     13      */
     14     static class MyQueue {
     15         /**
     16          * 数据列表
     17          */
     18         int[] data = new int[64];
     19         /**
     20          * 头指针
     21          */
     22         int head;
     23         /**
     24          * 尾指针
     25          */
     26         int tail;
     27 
     28         public MyQueue() {}
     29 
     30         public MyQueue(int head, int tail) {
     31             this.head = head;
     32             this.tail = tail;
     33         }
     34     }
     35 
     36     /**
     37      * 自定义栈
     38      */
     39     static class MyStack {
     40         /**
     41          * 数据列表
     42          */
     43         int[] data = new int[64];
     44         /**
     45          * 栈顶指针
     46          */
     47         int top;
     48 
     49         public MyStack() {}
     50 
     51         public MyStack(int top) {
     52             this.top = top;
     53         }
     54     }
     55 
     56     public static void main(String[] args) {
     57         // Step 1.初始化队列和栈
     58 
     59         // 两人手中都没有牌,初始化两个空的队列
     60         MyQueue q1 = new MyQueue(0, 0);
     61         MyQueue q2 = new MyQueue(0, 0);
     62 
     63         // 初始情况下桌上也没有牌,初始化一个空的栈
     64         MyStack desktop = new MyStack(0);
     65 
     66         // 依次读入两人最初时手中的牌,假设两个人有相同张数,每张牌的大小为1~9
     67         int[] u1 = {2, 4, 1, 2, 5, 6};
     68         int[] u2 = {3, 1, 3, 5, 6, 4};
     69         int len = u1.length;
     70 
     71         // 同时插入两个用户数据,队列尾指针往后移动
     72         for(int i = 0; i < len; i++) {
     73             q1.data[i] = u1[i];
     74             q1.tail++;
     75 
     76             q2.data[i] = u2[i];
     77             q2.tail++;
     78         }
     79 
     80         // Step 2.初始化一个桶,用来记录栈中数据
     81 
     82         // 判断桌上是否存在相同的牌,可以往栈里面遍历,也可以巧妙地使用桶的方式来处理,
     83         // 用牌值作为数组下标找到桶的位置,出牌入栈时就设置为1,如果下次出牌遇到桶里这个位置存在值,
     84         // 则说明牌值重复,可以赢得之前这张牌之间的所有牌,桶用完之后,出栈时需要把桶同步清理
     85         int[] book = new int[10];
     86 
     87         // Step 3.开始游戏,双方发牌并判断是否赢牌
     88 
     89         // 准备工作完成,游戏开始,u1先出牌
     90         // 当两个人手上都有牌时,继续游戏,即当队列不为空时,继续循环
     91         while(q1.head < q1.tail && q2.head < q2.tail) {
     92             // u1出牌
     93             play(q1, desktop, book);
     94             if(q1.head >= q1.tail) {
     95                 break;
     96             }
     97 
     98             // u1出牌结束后,轮到u2开始出牌,逻辑步骤和u1是一样的
     99             play(q2, desktop, book);
    100 
    101         }
    102 
    103         // Step 4.游戏结束,看谁手上没牌,没牌则对方获胜
    104 
    105         // 谁手上先没牌,则表示对方赢牌,没牌的标准就是该队列首尾指针相等
    106         if(q1.head == q1.tail) {
    107             win("u2", q2, desktop);
    108         }else {
    109             win("u1", q1, desktop);
    110         }
    111     }
    112 
    113     /**
    114      * 赢得胜利的打印输出方法
    115      * https://www.cnblogs.com/captainad/
    116      * @param user 打牌的用户
    117      * @param q 打牌用户手中的牌,即表示手中牌的队列
    118      * @param desktop 打牌放牌的桌子,即栈
    119      */
    120     private static void win(String user, MyQueue q, MyStack desktop) {
    121         System.out.println(user + " win. the card in the " + user + "'s hand is: ");
    122         for(int k = q.head; k < q.tail; k++) {
    123             System.out.print(q.data[k] + " ");
    124         }
    125         // 桌上是否还有牌,有牌则打印出来
    126         if(desktop.top > 0) {
    127             System.out.println("
    The card in the desktop is: ");
    128             for(int k = 0; k < desktop.top; k++) {
    129                 System.out.print(desktop.data[k] + " ");
    130             }
    131         }
    132     }
    133 
    134     /**
    135      * 开始打牌的方法,谁打牌,谁就会调用这个方法
    136      * https://www.cnblogs.com/captainad/
    137      * @param q 打牌用户手中的牌,即表示手中牌的队列,谁打牌就是谁的队列
    138      * @param desktop 打牌放牌的桌子,即栈
    139      * @param book 记录桌子上已有牌的记录本,即数据桶
    140      */
    141     private static void play(MyQueue q, MyStack desktop, int[] book) {
    142         // u出一张牌,从q队列中出队一个值
    143         int t = q.data[q.head];
    144 
    145         // 判断当前打出的牌能否赢牌,即看桶中是否存在相同的值
    146         // 如果桶中不存在,则表示桌面上没有相同的牌,u1没有赢,出队的牌入栈
    147         if(book[t] == 0) {
    148             // 出队
    149             q.head++;
    150             // 入栈,指针上移
    151             desktop.data[desktop.top++] = t;
    152             // 桶记录
    153             book[t] = 1;
    154         }else {
    155             // 桶中存在相同值,u1赢牌
    156             // u1出牌,所以出队
    157             q.head++;
    158             // 将u1出的牌放到自己末尾,同时能够拿桌上剩下的牌
    159             q.data[q.tail++] = t;
    160             // 桌上出栈的临时值
    161             int n;
    162             // 逐步拿起桌上的牌进行比对,比对到和刚刚放下去的那张牌相同为止,拿牌之后放在自己牌的末尾。
    163             // 逐步将出栈的值与刚刚出队的值比对,出栈的同时下移指针,两个值不相同则继续循环
    164             do {
    165                 // 拿起的牌放到当前牌的后面,即将出栈的值放到队列末尾,同时后移尾指针
    166                 n = desktop.data[--desktop.top];
    167                 q.data[q.tail++] = n;
    168                 // 因为栈中的牌拿走了,所以将桶清理干净
    169                 book[n] = 0;
    170             } while(n != t);
    171             /* 啊哈算法书中,啊哈磊用的是while循环,这将导致桌上最后比对相同的那张牌不拿走,
    172             这是和题面有出入的地方,这里用do-while循环能够解决这个问题 */
    173         }
    174     }
    175 }

    博主在原书代码的基础之上做了重构优化,应该还是能很清晰的阅读。

    我声明了两个内部类分别是队列和栈,并在方法中使用数组book[]来充当桶的角色。

    因为玩家U1和U2在出牌和赢牌的操作上是一致的,所以我抽取出了一个公共方法。在赢牌的环节中,为了能够让玩家拿起赢得桌上的所有牌,包括比对到的最后相同的那张牌,我用了一个do-while循环来处理,因为在原书中作者使用了一个while循环没能把最后该拿起的那张牌拿走,所以从题目上看来这点估计作者没有考虑到,我们在此就不深究了。

    因为打完牌之后如果谁手上先没牌,对方就获胜了,所以在出牌的循环里面,玩家U1打完牌之后我立马判断了他手上有没有牌了,因为没有牌可能就会判断玩家U2获胜,这在原文中是没有的,但是赶紧这个细节不太重要,可有可无吧,也不过多纠结了。

    学习总结

    上面这个游戏解法,让我对队列、栈的操作以及桶的使用深有体会,熟练掌握了这些数据结构的定义以及特征,并且能够有意识的使用这些数据结构来解决一些类似的算法问题,收益颇丰。

    参考资料

    1、《啊哈!算法》/ 啊哈磊著. 人民邮电出版社

  • 相关阅读:
    Gnome-keyring总是提示输入密码问题
    ubuntu下制作u盘启动盘
    ubuntu下制作u盘启动盘
    comgt拨号
    linux命令学习笔记
    U-Boot启动过程完全分析
    从Linux程序中执行shell(程序、脚本)并获得输出结果
    Git fetch和git pull的区别
    php如何获取远程文件内容的函数
    php验证邮箱和ip地址方法总汇
  • 原文地址:https://www.cnblogs.com/captainad/p/11018987.html
Copyright © 2011-2022 走看看