抽樣問題——《編程珠璣》讀書筆記

        問題:輸入兩個整數m和n,並且m<n。輸出一個由m個隨機數字組成的有序列表,這些隨機數的範圍是[0, n-1],並且每個整數最多出現一次。

        方法一:

        Knuth著作《Seminumerical Algorithms》中提出的方法,順序遍歷n個數,通過隨機測試條件的元素被選擇。

        以一個例子來解釋所說的隨機測試條件,比如m=2,n=5。第一個元素0被選擇的概率是2/5;第二個元素1被選擇的概率取決於第一個元素有沒有被選擇,如果0被選擇,則1被選擇的概率爲1/4,否則爲2/4,所有1被選擇的概率爲(2/5)*(1/4)+(3/5)*(2/4)=2/5;同理第三個元素2被選擇的概率取決於前兩個的選擇情況,如果都沒被選擇,則2被選擇的概率爲2/3,如果前兩個有一個被選擇,則2被選擇的概率爲1/3,如果前兩個都被選擇,則2被選擇的概率爲0,故2被選擇的概率爲(3/5)*(3/5)*(2/3)+2*(2/5)*(3/5)*(1/3)=2/5。依次類推,每個元素被選擇的概率都爲2/5。

        總的來說,從剩下的r個元素中選擇s個元素,那麼下一個元素被選中的概率爲s/r,從整個數據集合角度來講,每個元素被選擇的概率都是相同的。

        這個思想的爲代碼如下:

select = m
remaining = n
for i = [0, n)
        if (bigrand() % remaining) < select
                print i
                --select
        --remaining

       首先,該算法可以保證有m個元素被選中,不會多也不會少。證明如下,首先證明不會多於m個:因爲select等於0時不能選擇更多的整數;再證明不會少於m個:當select/remaining=1時,總會選中一個元素,因爲bigrand()%remaining<remaining總成立,所以i總會被選中。

       其次,每個元素被選擇的概率是相等的,均爲m/n,證明如上舉例證明所示。       C++實現代碼如下,同時算出從268435455約2.7億int可以表示的最大整數除以8,本來準備拿int的最大整數約21.5億測試,但方法三要先new出這麼大的空間,超出程序可以分配的最大堆棧空間)個數中選出10萬個整數,測試該方法所用的時間,便於與後面的方法進行性能比較。

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <limits>

using namespace std;

void genknuth(int m, int n)
{
	time_t t_start, t_end;
	t_start = time(NULL);
	
	for (int i = 0; i != n; ++i)
		if ((rand() % (n-i)) < m)
		{
			cout << i << " ";
			--m;
		}

	cout << endl;
	t_end = time(NULL);
	cout << "collapse time: " << difftime(t_end, t_start) << " s" << endl;
}

int main()
{
	int m = 100000;
	int n = numeric_limits<int>::max() / 8;
	srand(time(NULL));
	genknuth(m, n);
	cout << "n = " << n << endl;
	return 0;
}

       該算法的空間複雜度爲O(m),時間複雜度爲O(n)用這算法從2.7億個數中隨機找出10萬個數所用時間爲4秒

       方法二:

       方法一所需時間和搜索空間成正比,有些應用仍不能接受,因此需要繼續改進。其中一種方法是隨機插數據到一個容量爲m的集合中。爲代碼如下所示:

initialize set S to empty
size = 0
while size < m do
        t = bigrand() % n
        if t is not in S
                insert t into S
                ++size
print the elements of S in sorted order

       C++代碼實現如下所示,集合S的實現採用stl提供的set,底層用紅黑樹實現,不可重複插入相同的數據,當要插入的數據在set中已經存在時,則插入無效,數據不會被插入集合中,插入的時間複雜度爲O(logm):

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <limits>
#include <set>

using namespace std;

void gensets(int m, int n)
{
	time_t t_start, t_end;
	t_start = time(NULL);

	set<int> S;
	while (S.size() < m)
		S.insert(rand() % n);
	for (set<int>::iterator iter = S.begin(); iter != S.end(); ++iter)
		cout << *iter << " ";

	cout << endl;
	t_end = time(NULL);
	cout << "collapse time: " << difftime(t_end, t_start) << " s" << endl;
}

int main()
{
	int m = 100000;
	int n = numeric_limits<int>::max() / 8;
	srand(time(NULL));
	gensets(m, n);
	return 0;
}

       算法的時間複雜度爲O(mlogm),空間複雜度爲O(m)。同樣從2.7億數據範圍內選10萬個數,所花的時間爲2秒,可見速度會比原來的Knuth方法快。

       方法三:

        弄亂一個n個元素的數組,然後排序輸出前m個元素。後來Ashley Shepherd和Alex Woronow發現,只需弄亂數組前m個元素,關於產生隨機序列的方法可以參考我的文章《洗牌程序》維基百科

       本方法的C++代碼實現如下:

#include <iostream>
#include <ctime>
#include <cstdlib>
#include <limits>
#include <algorithm>

using namespace std;

// generate a random number between i and j,
// both i and j are include.
int randint(int i, int j)
{
	int ret = i + rand() % (j - i + 1);
	return ret;
}

void genshuf(int m, int n)
{
	time_t t_start, t_end;
	t_start = time(NULL);

	int i, j;
	int *x = new int[n];
	for (i = 0; i != n; ++i)
		x[i] = i;
	for (i = 0; i != m; ++i)
	{
		j = randint(i, n-1);
		int t = x[i]; x[i] = x[j]; x[j] = t; // swap x[i] and x[j]
	}
	sort(x, x + m);
	for (i = 0; i != m; ++i)
		cout << x[i] << " ";
	
	cout << endl;
	t_end = time(NULL);
	cout << "collapse time: " << difftime(t_end, t_start) << " s" << endl;

	delete []x;
	x = NULL;
}

int main()
{
	int m = 100000;
	int n = numeric_limits<int>::max() / 8;
	srand(time(NULL));
	genshuf(m, n);
	return 0;
}

        算法的時間複雜度是O(n+mlogm),空間複雜度是O(n)。同樣從數據範圍內選10萬個數,所花時間爲4秒,時間和方法一差不多,其中有一份部分時間花在初始化數組上,如果採用《編程珠璣》問題1.9的方法,當用到某個數時才初始化,這樣算法的時間複雜度可以減少到O(mlogm)。不過空間複雜度O(n)還是太大。

        關於具體採用方法二還是方法三,stackflow上有個大牛用數學的方法證明了一下,當m<<n時,採用方法二會比三性能好。

參考文章:

http://www.cnblogs.com/2010Freeze/archive/2012/02/27/2370284.html

http://hi.baidu.com/23star/blog/item/47f7314e5c3b0e01b2de0574.html

Taking Random Samples

A Sample of Brilliance, Programming Perls




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