堆的相关算法效率计算

一、堆向上调整和向下调整的时间复杂度计算

首先我们知道堆是完全二叉树,考虑情况最坏也就是堆的节点个数最多的情况:完全二叉树的最后一层节点是满的(即满二叉树),

这里展示的图片是四层,现在假设有一颗满二叉树,它的总高度是H,也就是H层;

假设总共的节点有N个;每一层的节点个数是 2^(h-1)  (h是每一层的高度):例如第一层就是 2^0 即一个节点后面依次类推;那么N=2^0+2^1+2^2+2^3+……2^(h-2)+2^(h-1),通过等比数列计算公式可知,N=2^H -1 ;

那么满二叉树的高度和节点个数之间的关系式就是H=log2(N+1);

向上调整是调整最后一个节点,最坏的情况下是这个新插入进来的节点会调整到根节点的位置,那么就调整了 H-1 次,所以向上调整的时间复杂度是log2(N+1)-1,按照大O的渐进表示法,也就是log(N);

同样地,向下调整算法最坏的情况是把根节点调整到最后一层去,也调整了 H-1 次,那么它的时间复杂度也是 log(N);


二、不使用堆数据结构,直接利用向上调整和向下调整进行建堆

这里也反映了在堆的基本实现里面,调用向下、向上的算法是直接传数组过去而不是传堆过去的原因;

1、向上调整建堆

//不用堆数据结构直接建堆-向上调整建堆(AdjustUp直接传数组的原因)
int main()
{
	//将数组里面的数据一个一个放进堆
	int arr[5] = { 1,2,3,4,5 };
	int len = sizeof(arr) / sizeof(arr[0]);
	for (int i = 0; i< len; i++)
	{
		AdjustUp(arr, i);
	}
	
	return 0;
}
void Swap(HPDataType* x, HPDataType* y)
{
	HPDataType tmp = *x;
	*x = *y;
	*y = tmp;
}
//向上调整
void AdjustUp(HPDataType* arr,int child)
{
	int parent = (child - 1) / 2;
	while (child>0)
	{
		if (arr[child] > arr[parent])
		{
			//交换数组里面的值
			Swap(&arr[child], &arr[parent]);
			//继续比较大小要将child和parent的值向上移动
			child = parent;
			parent = (child - 1) / 2;
		}
		else
		{
			//之前的数据都是一个一个建好的大堆
			//父节点的值比子节点的大,停止
			break;
		}
	}
}

2、向下调整建堆

void test01()
{
	int arr[5] = { 1,2,3,4,5 };
	int end = sizeof(arr) / sizeof(arr[0]);
	for (int i = (end - 1 - 1) / 2; i >= 0; i--)
	{
		AdjustDown(arr, i, end - 1);
	}
}
void AdjustDown(HPDataType* arr, int parent, int size)//size指向的是最后一个有效数据
{
	int child = parent * 2 + 1;//定义为左孩子
	while (child <= size)
	{
		if (child + 1 <= size && arr[child] < arr[child + 1])//当最后一个子树只有左孩子时,child+1越界
		{
			child = child + 1;//若是右孩子较大,那么就定义成右孩子
		}//总是大的调到上面去

		if (arr[child] > arr[parent])
		{
			Swap(&arr[child], &arr[parent]);
			parent = child;
			child = parent * 2 + 1;
		}
		else
		{
			break;
		}
	}
}

向下调整建堆是从后往前第一个非叶子节点,也就是第一个子树的根节点开始调整;

原因:向下调整算法向下调整的数据只能往一边调整,若是从整棵树的根节点开始调整,那么要保证下面的节点是堆才能这样操作,否则调整了一边另外一边最后也不是堆;

所以向下调整建堆时,从最后面的最小的子树开始,先把这个子树调整成一个小堆,再依次调整前面的的子树,最后调整到整棵树的根节点,此时再向下调整,下面的节点已经构成了堆,那么最后就会构建成一个堆。

这里向下调整建堆每次传入的父节点都是每一个子树的根节点,并且每次调整完一次之后,就让父节点往前移动,因为数组是连续的,那么 i-- 找到的一定会是上一个子树的根节点,那么再继续调整。

3、向上、向下调整建堆的时间复杂度

之前说到向上调整和向下调整的时间复杂度和此处建堆是不一样的,向上、向下调整每次只是针对于一个数据;但是这里是对于整个数组的数据进行调整建堆,

###首先看到向下调整建堆的时间复杂度:

乍一看代码,一个循环加上向下调整,可能会以为是 N*log(N);

但其实不然,深入到代码执行过程去看:

这里要考虑的是最坏的情况,即除了最下面一层的每一层的每一个节点都要向下调整总层数减去节点所在的层数的次数次

利用错位相减的公式加上二叉树高度和节点个数之间的关系,得出最终的结果是:

那么时间复杂度就是O(N);

###向上调整建堆

同样是上面的图片,从最后一层开始调整,最后一层的节点个数是 2^(h-1),但是最后一层的每个节点向上调整都要调整h-1次,其他层数也是如此(此处H是定量总层数,h是变量即在哪一层)

那么向上调整建堆最坏情况下要调整的总次数就是:

2^(h-1)*(h-1)+2^(h-2)*(h-2)+……2^(3-1)*(3-1)+2^(2-1)*(2-1)+2^0*0;

根据公式计算得出它的时间复杂度是:N*log(N);

所以进行建堆的时候优先考虑向下调整建堆,这样效率更高。


三、堆排序相关问题

1、打印取出最值

假设要取出一个数组里面的前3个最大的数,那么就先对这个数组建堆,建立一个大堆;大堆根节点处的数据是最大的,那么利用HPTop取出这个数据之后打印这个数据,之后再利用HPPop删除掉这个数,剩下的数据会向下调整,最后根节点处的数据又是剩下的数据里面最大的数据,再进行重复操作,直到取出三个最大的数为止;

时间复杂度:前提条件是现有一个N个数据大堆;假设要取出k个最大的数,那么循环k次,每次都要进行HPPop(时间复杂度是log(N)),那么最后的时间复杂度就是  k * log(N);


2、堆排序实现

假设要对N个数据的数组进行升序排序,首先前提条件是利用向下调整建堆建立好了一个大堆;根节点的数据是最大的数据,先交换堆首位的数据,把最大的数据放到最后,之后再进行向下调整,此时向下调整边界是最后一个数据的下标减去1(避免对最后一个已经排好的数据造成影响),调整好了之后,那么根节点数据是次大的;再进行交换操作,之后再进行向下调整,此时向下调整的边界是倒数第二个节点的下标减去1;后面依次如此;

这样一来,每次就把最大的数据放到最后,并且最先放的最大,后面依次减小,但是每次放到后面的数据都是新的堆里面最大的数据,那么最后调整完之后,就是升序的数组了。这样就实现了堆排序;

升序建立大堆;降序建立小堆;

 代码实现:

void test02()
{
	int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
	int end = sizeof(arr) / sizeof(arr[0]);
	for (int i = (end-1-1)/2; i >= 0; i--)
	{
		AdjustDown(arr, i, end - 1);//建立好大堆
	}
	//堆排序
	for (int j = 1; j < end; j++)//总共交换 end-1 次
	{
		Swap(&arr[0], &arr[end - j]);
		AdjustDown(arr, 0, end - j - 1);
	}
}

堆排序时间复杂度:O(N*log(N));相比冒泡排序的O(N^2),堆排序效率就高了许多;


3、Top-k问题

Top-k是利用堆来解决数据量很大的时候的排序问题;

假设要从很多数据(N)中取出前k的最大或者最小的数,那么就建立一个含有k个数据的小堆或者大堆;

这里对取前k个最小的数来分析,建立一个含k个数据的大堆,并且这k个数是从N中随便取出来的;之后依次用剩下的N-k个数(x代表其中一个数)去和堆的根节点(也就是目前堆里面最大的数据)比较,若是根节点数据小,那么就给根节点数据赋值为x(把跟节点的数据覆盖掉),然后进行向下调整,把这个小的数据调整下去,把原堆中次大的数据调整上来,后面依次如此,直到把N-k个数遍历完;

每次进去覆盖都是把堆里面最大的数换成了更小的数,并且存储下来了;

可能会担心这样一种情况:调整的某一次,根节点处是N个数据里面最小的数据,此时要来比较的是倒数第二小的,假设k是10,那么这个倒数第二小的数最应该放到对立面才对,但是现在放不进去;可是这种情况会出现吗?不会,因为最小的数不可能在根节点处,这是大堆,数据越小就越会被调整到后面;讨论一般情况,当根节点的位置是要求的前k个数据里面的一个,但是这个堆里面并不是前k个最小的数,那么根节点的数据一定比堆里面的某些非前k个最小的数要小,此时会向下调整让非前k个最小的数上来到根节点处,而那N-k个数里面没有比较完的一定有前k个最小的数里面的数,当遍历到它的时候会覆盖根节点的数据,所以不会出现挡住的情况。

代码实现:

void test03()
{
	int k = 0;
	printf("输入要取出前多少个最小的数:\n");
	scanf_s("%d", &k);
	int n = 20;
	int* arr = (int*)malloc(sizeof(int) * n);
	if (arr == NULL)
	{
		exit(1);
	}
	srand((unsigned int)time(NULL));
	for (int i = 0; i < n; i++)
	{
		arr[i] = 1 + rand() % 100;//1+0~99
	}
	for (int j = (k-1-1)/2; j >=0; j--)
	{
		AdjustDown(arr, j, k - 1);//对数组里面前k个数进行堆排序,排成一个大堆
	}
	for (int m = k; m < n; m++)
	{
		if (arr[m] < arr[0])
		{
			Swap(&arr[m], &arr[0]);
			AdjustDown(arr, 0, k - 1);
		}
	}
}

  • 14
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值