今天偶然看到羣裏的朋友說道,面試被問如何將撲克牌隨機洗牌輸出。筆者以爲這道題挺有意思並且挺開放性,有多種不一樣的實現方式。而後我就隨手寫了一個算法,仔細一想這個算法的優化空間挺大,因而又寫出三種算法。html
咱們經過JDK的隨機算法獲取一個隨機下標,再經過Set集合來判斷牌是否有被抽到過,若是抽到過的話,繼續進行循環,直到抽到原牌數量爲止。面試
public class ShuffleCard1 { public static int[] getShuffleCards(int[] cards) { // 獲取隨機數種子 Random rand = new Random(System.currentTimeMillis()); // 用Set集合存儲已抽過的牌 Set<Integer> isExisted = new HashSet(); // 聲明洗牌後數組 int[] shuffleCards = new int[cards.length]; // 已抽到的牌數量 int drawCount = 0; // 當抽到的牌數量沒達到原牌數組的大小時循環 while (drawCount < cards.length) { // 獲取一個隨機下標 int index = rand.nextInt(cards.length); // 判斷該下標對應的牌是否已被抽過,沒有的話,抽出 if (!isExisted.contains(cards[index])) { shuffleCards[drawCount++] = cards[index]; isExisted.add(cards[index]); } } return shuffleCards; } }
咱們分析一下,判斷牌是否被抽到的方法能夠進一步優化,咱們可使用位數組來進行判斷效率更高,因而咱們將Set改成byte數組判斷牌是否抽到。算法
public class ShuffleCard2 { public static int[] getShuffleCards(int[] cards) { // 獲取隨機數種子 Random rand = new Random(System.currentTimeMillis()); // 利用byte數組來判斷該牌是否有被抽到過 byte[] isExisted = new byte[cards.length]; // 聲明洗牌後數組 int[] shuffleCards = new int[cards.length]; // 已抽到的牌數量 int drawCount = 0; // 當抽到的牌數量沒達到原牌數組的大小時循環 while (drawCount < cards.length) { // 獲取一個隨機下標 int index = rand.nextInt(cards.length); // 若是byte數組對應下標爲0的話,表明還未抽到 if (isExisted[index] == 0) { shuffleCards[drawCount++] = cards[index]; isExisted[index] = 1; } } return shuffleCards; } }
咱們分析一下,假設牌組內有54張牌。咱們第一次抽到一張牌後,第二次又從原來的數組隨機抽取,而此時牌已經剩53張牌,可是咱們仍是從54張牌中進行抽取,因此咱們能夠提高這部分的效率。因而咱們在每次抽取牌的時候都縮小抽牌的範圍。而且每抽到一張牌,就依次與數組尾部的元素進行交換。假設[a,b,c,d,e]五張牌,第一次抽到c,那麼c已經被抽到了,就將c移到數組末尾,變爲[a,b,d,e,c]。第二次抽取元素的時候咱們就從下標0~3的位置隨機抽取,排除掉c元素。依次類推。數組
public class ShuffleCard3 { public static int[] getShuffleCards(int[] cards) { // 獲取隨機數種子 Random rand = new Random(System.currentTimeMillis()); // 聲明洗牌後數組 int[] shuffleCards = new int[cards.length]; // 已抽到的牌數量 int drawCount = 0; // 咱們經過減小抽牌的範圍,從例如先從54開始取隨機數, // 而後是53依次類推到1。 for (int i = shuffleCards.length; i > 0; i--) { // 獲取一個隨機下標 int index = rand.nextInt(i); // 填入洗牌後數組 shuffleCards[drawCount++] = cards[index]; // 該牌若是已被抽到過,每次都放在數組尾部 cards[index] = cards[i-1]; } return shuffleCards; } }
因爲第一種和第二種算法基本上隨機抽取的次數都會大於牌組的數量,由於隨機大機率會出現重複。因此咱們能夠轉變一下思路,不經過抽取牌來達到洗牌效果,而經過隨機交換原數組內的元素來達到隨機洗牌的目的。這樣一來就能夠下降隨機的次數。dom
public class ShuffleCard4 { public static int[] getShuffleCards(int[] cards) { // 獲取隨機數種子 Random rand = new Random(System.currentTimeMillis()); // 遍歷原牌組 for (int i = 0; i < cards.length; i++) { // 獲取一個隨機下標並與之交換 int index = rand.nextInt(cards.length); int tmp = cards[i]; cards[i] = cards[index]; cards[index] = tmp; } return cards; } }
public static void main(String[] args) { int[] cards = new int[54]; for (int i = 0; i < cards.length; i++) { cards[i] = i + 1; } long t1 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { ShuffleCard1.getShuffleCards(cards); } long t2 = System.currentTimeMillis(); System.out.println("第一種方法用時:" + (t2 - t1)); long t3 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { ShuffleCard2.getShuffleCards(cards); } long t4 = System.currentTimeMillis(); System.out.println("第二種方法用時:" + (t4 - t3)); long t5 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { ShuffleCard3.getShuffleCards(cards); } long t6 = System.currentTimeMillis(); System.out.println("第三種方法用時:" + (t6 - t5)); long t7 = System.currentTimeMillis(); for (int i = 0; i < 1000000; i++) { ShuffleCard4.getShuffleCards(cards); } long t8 = System.currentTimeMillis(); System.out.println("第四種方法用時:" + (t8 - t7)); }
測試結果:測試
第一種方法用時:3300ms 第二種方法用時:2214ms 第三種方法用時:572ms 第四種方法用時:543ms