剑指offer之二—最小的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;
}

上一篇:算法风暴之一—数组中出现次数超过一半的数字

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值