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