【数据结构】二叉树--堆排序

上次通过数组实现堆排序的时间复杂度为O(NlogN),空间复杂度为O(N)。

优化后时间复杂度O(NlogN),空间复杂度O(1).

考虑优化方案:

1.向下调整建堆。

2.向上调整建堆。

建堆只是为了找到堆中最大或者最小的元素。

1.向上调整建堆

使用向上调整,插入数据的思想建堆。

优化方案本质上就是不开辟新的空间在原数组上进行处理,使其空间复杂度变为O(1)。

void HeapSort2(int* a,int n)
{
	for (int i = 1; i < n; i++)
	{
		AdjustUp(a, i);
	}
	for(int i= 0;i<n;i++)
		printf("%d ", a[i]);
	printf("\n");
}

现在对其时间复杂度进行计算,为方便计算,采用满二叉树来计算。

以下考虑的是最坏的境况:

第二层: 2^1 个结点 ,向上调整1层;

第三层: 2^2 个结点 ,向上移动2层;

……

第h层:2^(h-1)个结点,向上移动h-1层。

则需要移动的结点的次数:

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

根据数列错位相减法求和公式可知

T(n) = (N+1) * ( log(N+1) - 2 )+2 当n->无穷大  T(n) = NlogN

向上建堆的时间复杂度为O(N*logN).

2.向下调整建堆

前提要求:对必须是大堆或者小堆。

所以要先对数组进行调整。

从倒数第一个非叶子结点开始(最后一个结点的父亲结点)。

因为叶子结点可以单独看作大堆或者小堆。

画图:

调整次数:4次

代码:

void HeapSort3(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(a, n, i);
	}

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

空间复杂度:O(1),时间复杂度O(N)

现证明时间复杂度:

第一层:2^0个结点,需要向下移动h-1层。

第二层:2^1个结点,需要向下移动h-2层。

第三层:2^2个结点,需要向下调整h-3层。

……

第h-1层,2^(h-2)个结点,需要向下移动1层。

所以移动的总次数:

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

根据错位相减法可得出:

T(n) = 2^h - 1 - h

又 n = 2^h - 1

T(n) = n - log(n+1) 

当n无穷大时,近似认为T(n) = n

证明完毕。

既然已经有向上建堆和向下建堆两种方法,那么哪种情况更是配建小队,哪种更适合见大堆??

升序 -- 建大堆

理由:若是升序建小堆最小的数已经在第一个位置了,剩下的关系全都乱了,需要重新建堆,建堆要O(N),再选出次小的,不断建堆选数,如此以时间复杂度O(N)的情况还不如直接遍历轻松。

总之,升序建小堆,效率低,还复杂。

降序--建小堆

那如何建大堆完成排序呢?

建大堆本质上就是尽可能维持堆的稳定。

找到最大的数后,将根位置的数与最后一个数相交换,在n-1组数据中进行向下调整,找到次小的数,将根与第n-1位置的数交换,以此实现升序。

相关代码:

void HeapSort4(int* a, int n)
{
	for (int i = (n - 1 - 1) / 2; i >= 0; i--)
	{
		//建大堆
		AdjustDown(a, n, i);
	}
	//找到最后一个数的下标
	int end = n - 1;
	while (end > 0)
	{
		//交换根和最后一个位置的值
		Swap(&a[0], &a[end]);
		AdjustDown(a, end, 0);
		end--;
		
	}
	for (int i = 0; i < n; i++)
		printf("%d ", a[i]);
	printf("\n");
}

Top - k 问题

求数据结合中前k个最大元素或者最小的元素,一般情况下数据量都比较大。

Top-k中最简单的方法就是排序,如果数据量非常大,排序就会出现问题(数据不能一次性加载到内存中),但是堆排序可以解决。

1.用数组中前看个元素来建堆。

求最大建小堆,求最小则建大堆。

与升序建大堆,降序建小堆原理相似。

2.用剩余的n-k个元素与对顶的元素作比较,来替换对顶的元素。

记得向下调整。

void TopK(int* a,int n,int k)
{
	//建小堆
	int* topk = (int*)malloc(sizeof(int) * k);
	assert(topk);
	for (int i = 0; i < k; i++)
	{
		topk[i] = a[i];
	}
	//向下调整建堆
	for (int i = (k - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(topk, k, i);
	}
	for (int j = k; j < n; j++)
	{
		if (topk[0] < a[j])
		{
			topk[0] = a[j];
			AdjustDown(topk, k, 0);
		}
	}
	for (int i = 0; i < k; i++)
	{
		printf("%d ", topk[i]);
	}
	printf("\n");
	free(topk);
}
int main()
{    
	int n = 10000;
	int* a = (int*)malloc(sizeof(int) * n);
	assert(a);
	srand(time(0));
	for (int i = 0; i < n; i++)
	{
		a[i] = rand() % 10000;
	}
	a[0] = 100001;
	a[101] = 100002;
	a[159] = 123456;
	a[7459] = 100003;
	a[7412] = 100009;
	a[7826] = 111111;
	a[7712] = 222222;
	a[9635] = 999999;
	TopK(a,n,8);
	return 0;
}

时间复杂度 O( K+ K*log(N-K) )

空间复杂度:O(K)

当K<<N时,效率也蛮高的。

有不对的地方还请大佬指出。 

猜你喜欢

转载自blog.csdn.net/weixin_61932507/article/details/124082628