在0~N(不包括N)範圍內隨機生成一個長度爲M(M

PS: 代碼涉及的隨機函數和一些容器雖然是C++的, 但算法是通用的, 這些容器java等其它語言裏也都能找到類似的存在.

1. 最樸素暴力的做法.

void cal1()
{
	int i = 0, j = 0, num = 0;
	int result[M];

	result[0] = rand() % N;	//第一個肯定不重複, 直接加進去

	for (i = 1; i < M; i++)	//獲得剩下的(M-1)個隨機數
	{
		num = rand() % N;	//生成0 ~ N之間的隨機數字

		for (j = 0; j < i; j++) 
		{
			if (num == result[j]) //如果和result數組中某個元素重複了
			{
				i--;	//重新開始此次循環
				break;
			}
		}

		if (j == i) // 說明新產生的數和數組裏原有的元素都不同, 則add進去
		{
			result[i] = num;
		}
	}
}


2. 在方法1的基礎上我們可以進行優化每輪遍歷n太耗時那麼優化成logn如何於是採用一下set作爲輔助爲了利用它logn的時間複雜度代價是多了一個M大小的set的空間.

void cal2()
{
	set<int> s;
	int num = 0, index = 0;
	int result[M];

	while (index < M)
	{
		num = rand() % N;
		if (s.find(num) == s.end())	//如果沒找到
		{
			s.insert(num);
			result[index++] = num;
		}
	}
}


3. 如果你被方法12無窮的重複比較弄煩了可能想到每次新產生的隨機數都要和已有的數去進行比較是否已存在越往後這種方法的效率越低不停在做無用功那麼反其道而行如何幾斤幾兩咱們都亮在牌面上, OK, 把所有數先都列出來從裏面往外篩選那麼每次選出來的肯定是不重複的只需要選M次就可以了.

void cal3()
{
	int result[M] = {0};
	deque<int> deq;	//隊列
	int i = 0, index;

  	for (i = 0; i < N; i++) //初始化, 把所有N個數都放到容器裏, 從這裏面往外挑, 每次必不重複
	{
		deq.push_back(i);
	}

	for (i = 0; i < M; i++)	//挑選出M個數
	{
		index = rand() % deq.size(); //注意deq.size()是不斷變小的, 但是每次都符合隨機特性
		result[i] = deq.at(index);	//把deq數組index位置的元素賦給result[i]
		deq.erase(deq.begin() + index);	//從deq隊列中把該元素刪除
	}
}


4. 方法3是思路比較理想化和直接實際操作中你會發現比方法1都要慢很多很多原因就出在容器的erase函數內部實現的本質是內存片的拷貝這個操作相當相當的耗時.這個和語言無關換成java等其它語言類似的這種函數都是同樣的原理其實思考到方法3, 真理已經呼之欲出了我們沿着這個思路繼續優化算法從中剔除元素的想法是好的但是方法不佳其實我們需要的本來就是基本的數組就可以了速度還快deque, vector這些容器無非是爲了使用他們的erase函數把某個數剔除出去不參與下次的隨機過程隨着一個個數被選出容器的大小也在不停變小其實使用數組利用下標的偏移我們直接就可以做到了和用數組實現一個隊列或者棧不是一樣的嗎無非就是數組下標的移動於是沿着方法3的思想我們每次隨機出來一個下標index(0 <= index < size(size初值爲N)), 每次把arr[index]這個位置的元素甩到數組最後面就可以了就相當於剔除操作了!

void cal4()
{
	int result[M] = {0};
	int data[N] = {0};
	int i = 0, index = 0;

	for (i = 0; i < N; i++)	//初始化
	{
		data[i] = i;
	}

	for (i = 0; i < M; i++)
	{
		index = rand() % (N - i);
		result[i] = data[index];	//把data數組index位置的元素賦給result[i]
		data[index] = data[N - i - 1];	//從data數組末尾(這個位置在不停前移)拿一個數替換到該位置, 相當於這個元素被剔除了
	}
}


算法都寫出來了貼一下實際測試的時間數據增加一下直觀感受.

(注下面數據在vs2008平臺下測試用的GetTickCount()函數, 這個函數精度在10~16ms範圍內由於機器配置不同下面數據僅供參考只爲表達一種直觀上的時間差異)

N = 1,000, M = 1,000 : (可以看出, N值較小時方法1甚至優於方法2, 因爲這時logn相對於n的速度優勢並不明顯卻多了set的處理開銷方法3最慢)


N = 10,000, M = 10,000 : (方法2logn優勢已經顯現方法3的龜速放大的很明顯)


N = 30,000, M = 30,000 : (方法2較之1的優勢更加明顯方法3依然最耗時)


N = 50,000, M = 50,000 : (方法12已經算不出結果了方法1下班沒關機執行了一宿第二天還是沒出結果方法2等了15分鐘也沒結果雖然會比1會快點原理是一樣的就是越到後面隨機出不重複數的機率越小了做太多無用功方法3雖然慢還是出的來結果的方法4依然堅挺, 0ms!)


N = 100,000, M = 100,000: (十萬依舊是0)


N = 1,000,000, M = 1,000,000: (1百萬GetTickCount()函數有10~16ms的誤差從下面1千萬和1億的時間來看這個算法是絕對線性的因此此時實際時間應該在23ms左右當然這時開始已經換成了堆數組)


N = 10,000,000, M = 10,000,000: (1千萬)


N = 100,000,000, M = 100,000,000: (1將近100M的內存啦十億的時候編譯器不支持了)



總結無論從代碼的實現來看還是實際的效率來看方法4都是當之無愧的佼佼者當然文中所測都是在MN相等的情況這種測試用例數量級越大對於方法12來說越是艱難算法是死的實際情況是多變的實際應用中我們靈活選擇甚至混合使用這些方法比如N非常大, M相對N來說卻又比較小(比如N=1, M=1), 這時候方法1也是完全可以接受的畢竟方法1比較省空間而方法4是用空間換了時間可以說MN越趨近方法4的多餘空間開銷是越值得的否則也可能會有得不償失的情況呀

鑑於此又一種思路浮現出來可以再繼續演化一下用於M比較大但是仍遠小於N的情況

5. 我們把方法4data數組去掉但是想象中是有這麼個數組的初始時這個數組裏的值都等於其下標循環的過程中如果某個座標index被隨機到了那麼把這個記錄加到map, keyindex, 從數組尾部拿來一個元素作爲value. 

void cal5()
{
	map<int, int> m;
	int result[M] = {0};
	int index = 0;

	for (int i = 0; i < M; i++)
	{
		index = rand() % (N - i);

		if (m.find(index) == m.end())	//如果沒找到, 說明該座標第一次被隨機到, 該位置的值沒有被改過, 值和座標相等
		{
			result[i] = index;
		}
		else	//如果該位置的值被改過, 那麼值在m[index]是這個位置的值
		{
			result[i] = m[index];
		}

		//更新這個index對應的值, 和方法同樣道理, 用數組後面的數替代該位置的值
		//這個地方比方法複雜點, 需要多個判斷, 因爲N-i-1位置的值的情況不同
		if (m.find(N - i - 1) == m.end())	//如果map裏沒有N-i-1這個key
		{
			m[index] = N - i - 1;
		} 
		else
		{
			m[index] = m[N - i - 1];
		}
	}
}


測試結果如下:

M = N = 100,000的時候仍然1秒多就可以出結果當然這種算法是應用於M遠小於N的情況, M,N都是十萬的時候有這個結果還是不錯的:


這是N = 10,000,000, M = 10,000時的結果可見純數組操作還是更快些雖然有很多輪無用功.


N = 10,000,000, M = 100,000方法1就已經無響應了而方法5的優點則顯而易見了, 這個速度還是可以接受的


可見, N越大, MN越接近方法1越無力反之方法1甚至是個不錯的方法速度快省空間所以還是那句話, 算法是死的, 而實際情況是複雜的, 但是我們可以把死的算法進行靈活運用.沒有最好只有最適合的, 一切取捨依實際情況吧! : )


全文完

版權所有, 轉載請標明出處.   http://blog.csdn.net/aa2650/article/details/12507817

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