1. 程式人生 > >【演算法詳解】洗牌演算法

【演算法詳解】洗牌演算法

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),且各元素隨機概率相等。