啊哈算法之纸牌游戏小猫钓鱼

简述

本算法摘选自啊哈磊所著的《啊哈!算法》第二章第三节的题目——纸牌游戏小猫钓鱼。文中代码使用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("\nThe 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、《啊哈!算法》/ 啊哈磊著. 人民邮电出版社

点赞