最近在看《数据结构与算法分析》, 里面有提到选择问题, 也就是从一组数(N个数)中找出其中第K大的数字.

我的思考过程 :

我一开始想到的第一种是排序, 快排这N个数, 然后取出第N个即可, 这样的话, 时间复杂度为O(NlogN), 但是这样的话似乎有点太浪费了, 就是说其实只需要排列前面的K个数字就行了, 然而这里却排出了所以的数字. 所以我自然而然的想到了第二种更简单的方式(没办法, 脑洞太小), 就是遍历K遍数组, 这样的话自然是找出了第K大的数, 时间复杂度为O(N * K). 上面连个都比较傻啊, 但是其实这两个算法谁快谁慢还不一定, 要看K和N的大小. 以我的智商想到这里就该结束了, 于是我开始在网上寻找答案, 还真有几种解法 :

类快速排序解法 :

这种解法是利用快速排序过程中每一次排序得出的目标数的位置左边的数都大于目标数, 右边的数都小目标数这一性质. 首先随便在这组数中选出一个目标数, 那么如果这个数前面有K - 1个数, 那么很好, 选出的这个数就是第K大的数. 如果这个数前面的数多于K - 1, 那么只需要找出前面这些数中的第K大的数. 如果这个数前面的数小于K - 1, 那么只需要在这个数后面的数中找出第K - 前面的数的数量 - 1大的数即可. 那么这样算出来的平均时间复杂度是O(NlogK)(反正算出来就是这个值, 我也不知道怎么算出来的). 代码如下 :

int SelectQuestion(int* array, size_t n, size_t k) {
	int* left = array;
	int* right = left + n - 1;
	int* target = left;           

	while (left < right) {
		while (left < right && *target >= *right  ) {
			--right;
		}
		while (left < right && *left >= *target  ) {
			++left;
		}
		swap(left, right);
	}
	swap(target, left);
	int temp = left - array;
	if (temp + 1 == k) {
		return *left;
	}
	if (temp + 1 > k) {
		return SelectQuestion(array, temp, k);
	}
	if (temp + 1 < k ) {
		return SelectQuestion(left + 1, n - temp - 1, k - temp - 1);
	}
}

小堆根解法

据说很多时候考察的都是这种解法, 当N很大, 大到甚至需要几个存储设备才能容纳的时候, 显然此时不可能将所有的元素都放在内存中, 此时尽可能少地遍历数组, 不需要将数组同时加载到内存中成了最关键的问题. 所以上面这几种效率都不尽人意. 此时可以先读入k个数, 然后构建小根堆O(KlogK), 然后将剩下的数依次读入, 如果该数字小于堆根(比目前的最大的前k个数中最小的还小, 显然不在前k个数之列), 直接放弃. 否则调整根堆O(logK). 所以总的时间复杂度是O(KlogK + (N - K)logK)O(NlogK).代码如下 :

#include "QuickSort.h"

static void buildHeap(int* list, size_t size);
static void adjust(int* list, int i, int size);

int SelectQuestion(int* array, size_t n, size_t k) {
	int* heap = new int[k + 1];
	for (int i = 0; i < k; ++i) {
		heap[i + 1] = array[i];
	}

	buildHeap(heap, k);
	for (int i = k; i < n; ++i) {
		if (array[i] > heap[1]) {
			heap[1] = array[i];
			adjust(heap, 1, k);
		}
	}
	int temp = heap[1];
	delete[] heap;
	return temp;
}

void buildHeap(int* list, size_t size) {
	for (int i = size / 2; i > 0; --i) {
		adjust(list, i, size);
	}
}
void adjust(int* list, int i, int size) {
	if (i > size / 2)	return;

	int smaller = 2 * i;
	if (2 * i + 1 <= size) {
		smaller = list[2 * i] > list[2 * i + 1] ? (2 * i + 1) : (2 * i);
	}
	if (list[i] > list[smaller]) {
		swap(&list[i], &list[smaller]);
		adjust(list, smaller, size);
	}
}

线性解法

理论上如果N比较小的话, 是存在线性解法的, 可以构建一个大小为N的数组, 然后遍历这一组数, 用这个是数组记录每个数出现的次数, 然后只需要从大到小, 找到第k个出现的数即可, 这个实现起来也很简单, 是典型的用空间换取时间, 这里就不多说了...

posted on 2016-08-29 14:02  内脏坏了  阅读(178)  评论(0编辑  收藏  举报