堆排序--TOP-K问题解决及复杂度分析

数组变堆:

上面的情况是在输入时直接对数组进行堆处理,下面是对一个给定的数组进行堆排序。

输入一个简单的无序数组:

int a[] = {9,0,3,5,6,7,8,2}

这个数组用二叉树表示为

堆排序--TOP-K问题解决及复杂度分析_第1张图片

改变此二叉树为堆有两种思路;第一种是向下调整,第二种是向上调整。

向下调整的思想为:

  1. 先对最后一个非叶子结点进行向下调整

  2. 对非叶子结点的上一个结点进行向下调整,直到调整到根节点结束

堆排序--TOP-K问题解决及复杂度分析_第2张图片

最终就会调整成一个最小堆。

向上调整思想:

  1. 找到根结点对根节点下一个结点进行向上调整
  2. 直到找到最后一个节点结束

堆排序--TOP-K问题解决及复杂度分析_第3张图片

代码实现:

//向上对数组建堆处理
void HeapInit1(int* a, int n) {
	for (int i = 0; i < n; i++) {
		AdjustUp(a, i);
	}
}
//向下对数组建堆处理
void HeapInit2(int* a, int n) {
	for (int i = (n - 1 - 1) / 2; i >= 0; --i)
	{
		AdjustDown(a, n, i);
	}
}

向上调整算法复杂度分析:

我们用一颗如图所示的满二叉树进行算法复杂度分析

堆排序--TOP-K问题解决及复杂度分析_第4张图片

第一层不用调整从第二层开始,第二层有21个结点,每个结点最坏的情况是调整1次**,所以**第二层次数为21*1第三层有2^2个结点,每个结点最坏的情况是调整2次,以此类推。

假设向上调整次数设为T(h),T(h) = 2^1*1 + 2^2*2 + 2^3*3 + …… + 2^(h-1)*(h-1)

T(h) = 2^1*1 + 2^2*2 + 2^3*3 + …… + 2^(h-1)*(h-2)+ 2^h*(h-1)

2T(h) = 2^2*1 + 2^3*2 + …… + 2^(h-1)*(h-1)

2T(h) - T(h)

得到 T(h) = -2^0+(-2^1*1) + (-2^2)+(-2^3) + …… + (-2^(h-1)) + 2^h*(h-1)+ 2^0

T(h) = -(2^h-1) + 2^h*(h-1)+1 = 2^h*(h-2) + 2

结点个数N = 2^h-1, 层数h = log(N+1)

所以得到 T(n) = (N+1)(log(N+1)-2)+2

算法复杂度 O(n) = nlogn

向下调整复杂度分析

堆排序--TOP-K问题解决及复杂度分析_第5张图片

第一层有20个结点**而最坏情况需要**向下移动h-1层**,第二层有**21个结点最坏情况下需要向下移动h-1层…倒数第二层有2^h-1个结点需要向下移动1层

所以根据这种情况可以设移动的总步数为 T(n) = 2^0*(n-1) + 2^1*(n-2) + …… + 2^(h-2)*1

2T(n) = 2^1*(n-1) + 2^2*(n-1) + …… + 2^(h-2)*2 + 2^(h-1)*1

继续采用错位相减得到 T(n) = 2^h -1 -h = n - log(n+1) ≈ n

所以时间复杂度为 O(n) = n

排序:

我们采用向下调整这一思路,先对数组进行建堆,然后将堆的根节点与最后一个结点进行交换,最后对新的堆进行向下调整。

代码如下:

void HeapSort(int* a, int n) {
	for (int i = (n - 1 - 1) / 2; i >= 0; i--) {
		AdjustDown(a, n, i);
	}//向下调整建立大堆

	size_t end = n - 1;
	while (end > 0) {
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		--end;
	}
}

**算法复杂度分析:**建堆的算法复杂度为O(N),每次交换然后再进行向下调整的算法复杂度为O(NlogN),所以堆排序的算法复杂度为O(NlogN),由于直接在数组上进行操作空间复杂度为O(1)。

TOP-K问题

TOP-K问题就是在N个数中找到前K个数,一般来说TOP-K问题数据量较大穷举会浪费资源,我们可以采用堆排序的思想解决此问题。

算法思路:

  1. 用前K个数进行建堆。
  2. 然后将堆的根和剩下N-K个数比较,如果比根大的话就加入堆。
  3. 堆进行向下调整,保持堆属性。
void TOP_K(int* a, int n, int k) {
	int* tmp = (int*)malloc(sizeof(int) * k);
	assert(tmp);

	for (int i = 0; i < k; ++i)
	{
		tmp[i] = a[i];
	}

	// 建小堆
	for (int j = (k - 1 - 1) / 2; j >= 0; --j)
	{
		AdjustDown_S(tmp, k, j);
	}

	for (int i = k; i < n; i++) {
		if (tmp[0] < a[i]) {
			tmp[0] = a[i];
			AdjustDown_S(tmp, k, 0);
		}
	}

	for (int i = 0; i < k; i++) {
		printf("%d ", tmp[i]);
	}
	printf("\n");
	free(tmp);
}

每次建立的堆的最小进行比对,最后堆中剩下的就是前K个数据。

算法复杂度分析:

先对K个数建堆算法复杂度为O(K),然后N-K个数和根数据比较,然后恢复堆属性,最坏情况下算法复杂度为O(log(K)*(N-K)),所以最后整个算法的算法复杂度为O(K+log(K)(N-K)),空间复杂度为O(1),和建立大堆pop出K个数据相比较由于K一般比较小所以算法复杂度较低。

你可能感兴趣的:(简单数据结构,算法,c语言,数据结构,c++)