隨機算法

http://www.oschina.net/question/565065_82714

http://blog.csdn.net/morewindows/article/details/7370155

http://www.oschina.net/question/89964_48566

本文將介紹三個有趣的隨機問題,分別是隨機重新排列、從文件中隨機取一行數據、生成N個隨機數。

 

一.隨機重新排列

將一個序列打亂並對其進行隨機的重新排列,關鍵在於每種序列的被選擇概率要一樣,不然有失“公平”。現在讓我們來尋找如何保證每種序列被選擇的概率一樣大的算法。

首先假設這個數組只有二個元素,設數組a{1, 2},顯然這個數組只有二種可能的排列,要麼是{12}要麼是{21}。很容易想到一種方法——只要第二個元素有50%的概率與第一個元素交換即可。用代碼表現下:

if (rand() % 2 == 0)

       swap(a[0], a[1])

由於rand() % 2的結果要麼爲0,要麼爲1,且各佔50%的概率。因此swap(a[0],a[1])的執行概率也是50%,如果執行了,結果會是{21}。沒有執行,結果會是{12}。所以這樣兩種排列出現的可能均爲50%

 

接下來再假充這個數組有三個元素設數組a{1, 23},這個數組有六種可能的排列,要麼是{123}{132}{213}{231}{312}{321}。這麼多的排列看起來好象有點複雜,不知道從何下手。其實結合上面的分析,我們可以這樣考慮:

1.先調整前二個元素即{1, 23}先生成{123}{213}

2.然後對{123},第三個元素以1/3的概率與第一,第二,第三個元素進行交換就可以等概率的得到{321}{132}{123}

3.同理對{213},可以等概率的得到{312}{231}{213}

根據貝葉斯公式,不難計算出由這些排列出現的可能性都是1/2 * 1/3 = 1/6。完全符合每種序列的被選擇概率相同的要求。

 

這樣隨機重新排列的算法就找到了,下面給出示意代碼:

//隨機重新排列
//參照STL中的random_shuffle
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
inline void Swap(int *a, int *b)
{
	int c = *a;
	*a = *b;
	*b = c;
}
//隨機重新排列函數
void Random_shuffle(int a[], int n)
{
	srand(time(NULL));
	for (int i = 1; i < n; i++)
		Swap(&a[i], &a[rand() % (i + 1)]);
}
void PrintfIntArray(int a[], int n)
{
	for (int i = 0; i < n; i++)
		printf("%d ", a[i]);
	putchar('\n');
}
int main()
{
	printf("           隨機重新排列 \n");
	printf("--- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");

	const int MAXN = 8;
	int a[MAXN] = {1, 2, 3, 4, 5, 6, 7, 8};

	printf("原數組:\n");
	PrintfIntArray(a, MAXN);

	Random_shuffle(a, MAXN);

	printf("隨機重新排列後:\n");
	PrintfIntArray(a, MAXN);
	return 0;
}

運行結果如下:

有興趣的童鞋可以用STL中的random_shuffle()來改寫上面的程序,相信知道其實現原理後會對random_shuffle()有更深的認識。

另外,這種按順序先後來交換元素得到新排列的方法與生成全排列非常類似,可以參考《STL系列之十 全排列(百度迅雷筆試題)》然後對比下兩種方法在思路上相似之處。

 

二.從文件中隨機取一行數據

如果先統計文件有多少行,再根據rand() % 行數選擇對應行也是可以行的,但效率顯然會有點低了。有沒有一種方法可以只遍歷文件一次了?請看代碼:

//從文件中取機選取一行
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main()
{
	printf("           從文件中取機選取一行 \n");
	printf("--- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");

	int i, num, nChooseNum;
	const char strFileName[] = "in.txt";
	
	freopen(strFileName, "r", stdin);
	srand(time(NULL));
	i = 1;
	while (scanf("%d", &num) != EOF)
	{
		if (rand() % i == 0)
			nChooseNum = num;
		i++;
	}
	printf("從文件中選取出: %d\n", nChooseNum);
	return 0;
}

運行結果如下(in.txt中爲09,每個數字佔一行):

對代碼進行下講解,以三行數據爲例,首先對文本的第一行,rand() % 1,結果必然爲0。所以第一行已被選中了,然後對第二行,rand()%2,結果要麼爲0,要麼爲1。故第二行有 50的可能性被選中,然後對第三行,rand()%3,顯然被選中的概率爲1/3。故有:

選中第一行的概率爲1 * 1/2 * 2/3 = 1/3

選中第二行的概率爲1/2 * 2/3 = 1/3

選中第三行的概率爲1/3

故每一行被選中是等概率的。

 

三.生成N個隨機數

這個有很多方法,常見方法用set記錄已經生成的數據,然後判斷set的大小是否符合要求。代碼如下所示:

//生成n個指定範圍的隨機數
#include <stdio.h>
#include <time.h>
#include <set>
//生成n個[s, e)範圍的數
void GetRandNumberInRange(int *a, int n, int s, int e)
{
	int   i, j;
	std::set<int>  m;
	std::set<int>::iterator setpos;
	
	srand(time(NULL));	
	while (m.size() < n)
	{
		j = rand() % (e - s) + s;
		m.insert(j);
	}
	
	i = 0;
	for (setpos = m.begin(); setpos != m.end(); setpos++)
		a[i++] = *setpos;
}
void PrintfIntArray(int a[], int n)
{
	for (int i = 0; i < n; i++)
		printf("%d ", a[i]);
	putchar('\n');
}
int main()
{
	const int NMAX = 20;
	const int NUMSTART = 1;
	const int NUMEND = 100;

	printf("           生成%d個%d到%d之間的數 \n", NMAX, NUMSTART, NUMEND);
	printf("--- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n");	

	int    a[NMAX];
	
	GetRandNumberInRange(a, NMAX, NUMSTART, NUMEND + 1);
	PrintfIntArray(a, NMAX);
	return 0;
}

運行結果如下:

這種方法會導致調用隨機函數次數過多,從而效率低下。可以採用一種改進的方法,如要生成3110之間的數,取10-3=7,因此可以先t=rand()%7,保存t,然後再t=rand()%8,如果t這個數已經存在就取8保存,再t=rand()%9,同樣如果t這個數已經存在就取9保存。這樣只會調用3次隨機函數。當然這種方法不能完全保證每個數字被選擇概率相同,算是犧牲“公平”來保證效率吧。

生成N個隨機數的改進方法代碼如下:

//生成n個指定範圍的隨機數
#include <set>
#include <cstdio>
#include <cstdlib>
#include <ctime>
//在[s, e)區間上隨機取n個數並存放到a[]中
void GetRandomNum(int *a, int n, int s, int e)
{
	std::set<int> set_a;
	srand(time(NULL));
	for (int i = e - n; i < e; i++)
	{
		int num = (rand() % i) + s;
		if (set_a.find(num) == set_a.end())
			set_a.insert(num);
		else
			set_a.insert(i);
	}
	i = 0;
	std::set<int>::iterator pos;
	for (pos = set_a.begin(); pos != set_a.end(); pos++)
		a[i++] = *pos;
}
void PrintfIntArray(int a[], int n)
{
	for (int i = 0; i < n; i++)
		printf("%d ", a[i]);
	putchar('\n');
}
int main()
{
	const int NMAX = 20;
	const int NUMSTART = 1;
	const int NUMEND = 100;
	
	printf("           生成%d個%d到%d之間的數 改進版\n", NMAX, NUMSTART, NUMEND);
	printf("--- by MoreWindows( http://blog.csdn.net/MoreWindows )  ---\n\n"); 
	int a[NMAX];
	
	GetRandomNum(a, NMAX, NUMSTART, NUMEND + 1);
	PrintfIntArray(a, NMAX);
	return 0;
}

運行結果如下:

 

有關隨機的趣味題就介紹到這裏,有任何問題歡迎和我交流,直接留言或發送郵件均可。郵箱:[email protected]

 

下一篇《STL系列十二鏈表排序的6次改進(對《STL源碼剖析》作個補充)》將會對STL中的鏈表排序進行詳細講解,如果看過《STL源碼剖析》書中142頁的鏈表排序代碼將會更有收穫。更多內容,請繼續關注MoreWindows

 

轉載請標明出處,原文地址:http://blog.csdn.net/morewindows/article/details/7659532



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章