1. 程式人生 > >演算法風暴之二—最小的k個數

演算法風暴之二—最小的k個數

問題描述

給定一個數組,求這個陣列最小的k個數。

方法一:排序 O(nlogn)

最直觀的方法大概就是排序了,排序大法好,很多問題排個序就可以解決,然而功能過剩的排序顯然不是此問題的最佳解法。使用快排的話,平均時間複雜度為O(nlogn),是不是有點大了呢?

快排程式碼:

#include <iostream>
#include <cstdlib>
#include <ctime>
#define RAND(l, r) l+(int)(r-l+1)*rand()/(RAND_MAX+1)
using namespace std;

void data_rand
(int *data, int n) { srand(1999); for (int i = 0; i < n; ++i) { data[i] = RAND(1, 1024); } } int Partition(int *data, int n, int start, int end) { if (start == end) return start; srand((unsigned)time(NULL)); int index = RAND(start, end); swap(data[index], data[end]); int one = start - 1;
for (index = start; index < end; ++index) { if (data[index] < data[end]) { ++one; if (one != index) { data[one] ^= data[index] ^= data[one] ^= data[index]; } } } ++one; swap(data[one], data[end]); return one; } void quick_sort(int data[], int n, int start, int end) { int
index = Partition(data, n, start, end); if (index > start) quick_sort(data, n, start, index - 1); if (index < end) quick_sort(data, n, index + 1, end); } int main() { int n = 512, data[512] = {}, k = 10; data_rand(data, n); for (int i = 0; i < n; ++i) { printf("%4d", data[i]); if ((i + 1) % 8 == 0) cout << endl; } cout << endl; quick_sort(data, n, 0, n - 1); for (int i = 0; i < k; ++i) { cout << data[i] << ' '; } cout << endl; }

方法二:找出第k大的數 O(n)

利用快排思想,我們可以找出第k大的數,同時在第kth數左邊的數都小於它,右邊的數都大於它。這樣,劃分的區間左邊就是我們要求得數了,只是此時左邊的數尚未排好序。

快速排序簡稱快排,利用分治的思想,在陣列中隨機選擇一個數,然後以這個數為基準,把大於它的數劃分到它的右側,小於它的數劃分到它的左側,並且遞迴的分別對左右兩側資料進行處理,直到所有的區間都按照這樣的規律劃分好。

那麼在這個問題中,如何利用快排的方法呢?快排是對每一個區間進行分治處理,而此問題不必,我們只要找到第k小的數。每次隨機劃分得的第m個數,如果m < k, 那麼對[m + 1, n - 1]這個區間繼續遞迴;如果m > k,那麼對[0, m - 1]這個區間進行遞迴;如果剛好有m = k,那麼函式結束,區間[0, k - 1]的數就是最小的k個數,即使他們沒有進行排序。

此演算法的平均時間複雜度為O(n), 快速排序的詳細證明可參考“演算法導論”。

但是由於這些操作會更改陣列的資料,且是對整個陣列進行操作,所以針對大規模的資料,會有所限制。這是它的缺點所在。

程式碼:

#include <iostream>
#include <ctime>
#include <cstdlib>
#define RAND(l, r) l+(int)(r-l+1)*rand()/(RAND_MAX+1)
using namespace std;

const int maxn = 512;

void rand_data(int n, int *data)
{
	srand(1999);
	for (int i = 0; i < n; ++i) {
		data[i] = RAND(1, 1024);
	}
}

int Partition(int *data, int length, int start, int end)
{
	if (start == end) return start;
	srand((unsigned)time(NULL));
	int index = RAND(start, end);
	swap(data[index], data[end]);
	int one = start - 1;
	for (index = start; index < end; ++index) {
		if (data[index] < data[end]) {
			++one;
			if (index != one) swap(data[index], data[one]);
		}
	}
	++one;
	swap(data[one], data[end]);
	return one;
}

int main()
{
	int n = maxn, data[maxn], k = 10;
	rand_data(n, data);
	cout << "The original data:" << endl;
	for (int i = 0; i < n; ++i) {
		printf("%4d", data[i]);
		if ((i + 1) % 8 == 0) cout << endl;
	}
	cout << endl;
	int index = Partition(data, n, 0, n - 1);
	int start = 0, end = n - 1;
	while (index != k - 1) {
		if (index > k - 1) {
			end = index - 1;
			index = Partition(data, n, start, end);
		} else if (index < k - 1) {
			start = index + 1;
			index = Partition(data, n, start, end);
		}
	}
	cout << "The least kth data:" << endl;
	for (int i = 0; i < k; ++i) {
		cout << data[i] << ' ';
	}
	cout << endl;
}

方法三:使用二叉樹 O(nlogk)

演算法思想

對於這個問題,我們要維護最小的k個數,那麼我們可以構建一棵二叉樹,它可以是最大堆或紅黑樹。以最大堆為例,對於前k個數,我們直接插入到最大堆中,然後對其進行有序化處理。然後遍歷第k ~ n - 1個數,對每一個數,如果它比堆最大值更大,那麼它肯定不是結果,直接跳過它;如果它比堆最大值更小,那麼把最大值剔除,同時將它插入並進行有序化。 這樣,我們始終維護了這個前k小數的序列,當遍歷完整個陣列之後,二叉樹中的資料就是最小的k個數。

時間複雜度O(nlogk), 對每個數進行有序化操作是O(logk)

從時間上來看,似乎比方法二要慢的些,但是它適合處理大規模資料的情況(記憶體無法全部存取,只能從硬碟依次讀取),它不必更改原來的資料,也不必另開那麼大的空間。

最大堆版

#include <iostream>
#include <cstdlib>
#define RAND(l, r) l+(int)(r-l+1)*rand()/(RAND_MAX+1)
using namespace std;

void rand_data(int *data, int n)
{
	srand(1999);
	for (int i = 0; i < n; ++i) {
		data[i] = RAND(1, 1024);
	}
}

void down_adjust(int *heap, int k, int index)
{
	int i = index, j = 2*index;
	while (j <= k) {
		if (j + 1 <= k && heap[j + 1] > heap[j]) {
			j = j + 1;
		}
		if (heap[i] < heap[j]) {
			swap(heap[i], heap[j]);
			i = j;
			j = 2 * j;
		} else break;
	}
}

int main()
{
	int n = 512, k = 10;
	int data[1050], heap[11] = {};
	rand_data(data, n);
	cout << "The original data:" << endl;
	for (int i = 0; i < n; ++i) {
		printf("%4d", data[i]);
		if ((i+1) % 8 == 0) cout << endl;
	}
	for (int i = 0; i < k; ++i) {
		heap[i + 1] = data[i];
	}
	for (int i = k/2; i >= 1; --i)
		down_adjust(heap, k, i);
	for (int i = k; i < n; ++i) {
		if (heap[1] <= data[i]) continue;
		heap[1] = data[i];
		down_adjust(heap, k, 1);
	}
	cout << "The least kth numbers:" << endl;
	for (int i = k; i >= 1; --i) {
		cout << heap[i] << ' ';
	}
	cout << endl;
}

multiset版(紅黑樹)

注意要使用multiset(不去重)而不是set。

#include <iostream>
#include <cstdlib>
#include <ctime>
#include <set>
#define RAND(l, r) l+(int)(r-l+1)*rand()/(RAND_MAX+1)
using namespace std;

const int maxn = 512;

void rand_data(int *data, int n)
{
	srand(1999);
	for (int i = 0; i < n; ++i) {
		data[i] = RAND(1, 1024);
	}
}

int main()
{
	int n = maxn, data[maxn], k = 10;
	rand_data(data, n);
	cout << "The original data:" << endl;
	for (int i = 0; i < n; ++i) {
		printf("%4d", data[i]);
		if ((i + 1)%8 == 0) cout << endl;
	}
	cout << endl;
	multiset<int> kth;
	for (int i = 0; i < n; ++i) {
		if (i < k) kth.insert(data[i]);
		else {
			set<int>::iterator is = kth.end();
			is--;
			if (*is > data[i]) {
				kth.erase(is);
				kth.insert(data[i]);
			}
		}
	}
	cout << "The least kth numbers:" << endl;
	for (set<int>::iterator is = kth.begin(); is != kth.end(); ++is) {
		cout << *is << ' ';
	}
	cout << endl;
}