【演算法詳解】洗牌演算法
1. 問題描述
洗牌演算法是常見的隨機問題;它可以抽象成:得到一個M以內的所有自然數的隨機順序陣列。
常見問題描述:
1.將自然數1 ~ 100隨機插入到一個大小為100的陣列,無重複元素
2. 1 ~ 52張撲克牌重新洗牌
什麼是好的洗牌演算法:
洗牌之後,如果能夠保證每一個數出現在所有位置上的概率是相等的,那麼這種演算法是符合要求的;這在個前提下,儘量降低時間和空間複雜度。
2. 演算法實現
第一個演算法:
隨機抽出一張牌,檢查這種牌是否被抽取過,如果已經被抽取過,則重新抽取,知道找到沒有被抽取的牌;重複該過程,知道所有的牌都被抽取到。
這種演算法是比較符合大腦的直觀思維,這種演算法有兩種形式:
1. 每次隨機抽取後,將抽取的牌拿出來,則此時剩餘的牌為(N-1),這種演算法避免了重複抽取,但是每次抽取一張牌後,都有一個刪除操作,需要在原始陣列中刪除隨機選中的牌(可使用Hashtable實現)
2. 每次隨機抽取後,將抽取的符合要求的牌做好標記,但並不刪除;與1相比,省去了刪除的操作,但增加了而外的儲存標誌為的空間,同時導致可每次可能會抽取之前抽過的牌
這種方法的時間/空間複雜度都不好。
第二個演算法:
每次隨機抽出兩張牌交換,交換一定次數後結束:
void shuffle(int* array, int len)
{
const int suff_time = len;
for (int idx = 0; i < suff_time; i++)
{
int i = rand() % len;
int j = rand() % len;
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
這是一個常見的洗牌演算法; 但是如何確定一個合適的交換次數?
假設交換了m此,則某張牌始終沒有被交換的概率為 (n-2)/n * (n-2)/n, ... ...* (n-2)/n = ((n-2)/n)^m;我們希望其概率小於摸個值,求出m的解.假設概率小於1/1000,對於n=52,m大概為176,實際上遠遠大於陣列的長度.
第三個演算法:
該演算法每次隨機選取一個數,然後將該數與陣列中最後(或最前)的元素相交換(如果隨機選中的是最後/最前的元素,則相當於沒有發生交換);然後縮小選取陣列的範圍,去掉最後的元素,即之前隨機抽取出的數。重複上面的過程,直到剩餘陣列的大小為1,即只有一個元素時結束:
void shuffle(int* array, int len) { int i = len; int j = 0; int temp= = 0; if (i == 0) { return; } while (--i) { j = rand() % (i+1); temp = array[i]; array[i] = array[j]; array[j] = temp; } }
該演算法的數學證明請參照具體的論文或者博文;
該演算法複雜度為O(n),且各元素隨機概率相等。