演算法風暴之二—最小的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;
}