啊哈算法之紙牌游戲小貓釣魚


簡述

本算法摘選自啊哈磊所著的《啊哈!算法》第二章第三節的題目——紙牌游戲小貓釣魚。文中代碼使用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、《啊哈!算法》/ 啊哈磊著. 人民郵電出版社


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM