上一篇: 算法风暴之一—数组中出现次数超过一半的数字
问题描述
给定一个数组,求这个数组最小的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;
}