数据结构之堆与堆排序(时间复杂度极其优胜的排序方法)

1.堆的概念及结构

1.1 堆的概念

如果有一个关键码的集合K = {k0,k1, k2,…,kn-1},把它的所有元素按完全二叉树的顺序存储方式存储 在一个一维数组中,并满足:Ki = K2i+1 且 Ki >= K2i+2) i = 0,1,2…,则称为 小堆(或大堆)。将根节点最大的堆叫做最大堆或大根堆,根节点最小的堆叫做最小堆或小根堆。

1.2 堆的性质

        (1)堆中某个节点的值总是不大于或不小于其父节点的值;

        (2)堆总是一棵完全二叉树。

大根堆实例

                     89

             56           78

        32      28    65     49

.........

小根堆实例

                    12

             29           31

        34      48   35      69

.........

1.3 堆的底层

逻辑结构

物理结构

2.向上调整算法

        push_back数据时,由于堆的底层是一个数组,只需要堆的底层数组++size即可实现,但是只插入数据就好了嘛?显然不是,我们还要保证尾插入这个数据后,小堆(大堆)的性质仍不变,即小堆(大堆)插入数据后,仍是一个小堆(大堆)。于是需要比较插入的这个数据与他的parent,通过向上调整算法,为他找到合适的位置。

简单举一个例子,图示如下:

        这个算法思路我们明白了,那具体算法代码如何实现呢?

        我们下文均以小堆来举例,那么大堆也是同理。

void AdjustUp(HPDataType* a,int child)
{
    int parent=(child-1)/2;
    //while(parent>=0)--不能用这个来判断,因为当parent为0的时候,0/2=0,陷入死循环
    while(child>0)
    {
        if(a[child]<a[parent])
        {
            Swap(&a[child],&a[parent]);
            child=parent;
            parent=(child-1)/2;
        }
        else
        {
            break;
        }
    }
}

        有了向上调整算法,我们就可以轻松实现在堆尾插一个数据。

代码实现如下:

void HeapPush(HP* php, HPDataType x)
{
	assert(php);
	if (php->size == php->capacity)
	{
		int newCapacity = php->capacity == 0 ? 4 : php->capacity * 2;
		HPDataType* tmp = (HPDataType*)realloc(php->a, newCapacity * sizeof(HPDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			exit(-1);
		}

		php->a = tmp;
		php->capacity = newCapacity;
	}

	php->a[php->size] = x;
	php->size++;

	AdjustUp(php->a, php->size - 1);
}

向上调整算法的时间复杂度:

        O(log2N)

3.向下调整算法

        当我们要pop掉堆顶的数据时,由于小堆的堆顶为最小的数,但是孩子位置的两个数没有固定的大小关系,也就是说堆顶元素的左孩子和右孩子不一定谁大谁小,那我们删除堆顶元素之后,应该怎样调整数据位置来维持堆形态呢?

        首先我们来了解一下向下调整算法。

        (同样我们会以小堆来举例)

        假如堆顶元素是一个较大的元素,而堆顶元素的左子树和右子树均为一个小堆,那么我们可以通过向下调整堆顶元素来完成小堆形态的维持。

图示如下:

        了解原理之后,我们来学习一下如何通过代码来具体实现呢?

代码实现如下:

void AdjustDown(HPDataType* a,int n,int parent)
{
	int minChild = parent * 2 + 1;
	//假设左孩子是较小的孩子
	while (minChild < n)
	{
		//找出小的那个孩子
		if (minChild + 1 < minChild)
		{
			minChild++;
			//如果右孩子小于左孩子,则右孩子是较小的孩子
		}

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

        注意:使用向下调整的前提是parent元素的左子树和右子树堆形态相同(即都是小堆或者都是大堆)。

接下来,我们来实现删除堆顶元素。

我们采用一种独特的方法:

(1)我们将堆顶元素和堆中最后一个元素交换(这样做我们就可以使用向下调整,因为堆顶元素的左子树和右子树仍均为小堆)

(2)堆数组--size(目的删掉最后一个元素(此时是原来的堆顶元素))

(3)对堆顶元素使用向下调整算法,直至堆形态建立

具体代码实现如下:

void HeapPop(HP* php)
{
	assert(php);
	assert(!HeapEmpty(php));
	Swap(&php->a[0], &php->a[php->size - 1]);
	php->size--;

	AdjustDown(php->a, php->size, 0);
}

向下调整时间复杂度:

        O(log2N)

4.使用向下调整法建堆

思想:(以用数组{15,1,19,25,8,34,65,4,27,7}建小堆为例)

        因为使用向下调整法是有前提的(左子树,右子树堆形态一致),所以我们考虑从数组后端元素开始。假设所有数组元素按所给顺序按层序排成完全二叉树,找到最后一个非叶结点的结点(即最后一个叶结点的parent,本结论读者可以自己尝试画图推出),开始向下调整(因为满足向下调整条件),直到调整到根,则堆建好。

代码如下:

for (int i = (n - 1 - 1) / 2; i >= 0; --i)
{
	//n-1为数组最后一个数的下标,(下标-1)/2得到最后一个非叶结点的下标
	AdjustDown(a, n, i);
}

5.使用向上调整法建堆

思想:(以用数组{15,1,19,25,8,34,65,4,27,7}建小堆为例)

        插入第一个元素15,把15当做堆顶元素,插入第二个元素1,因为1<15,插入1后小堆形态无法维持,对1使用向上调整算法,依次类推,直至数组中所有元素均进堆并且维持小堆形态,则小堆建立好。

代码实现:

for (int i = 1; i < n; ++i)
{
	AdjustUp(a, i);
}

6.两种算法建堆时间复杂度对比

6.1使用向下调整法建堆时间复杂度:

        假设树的高度为h

        调整次数=每一层结点个数*这一层结点最坏向下调整次数

        T(N)=2^0*(h-1)+2^1*(h-2)+....2^(h-3)*2+2^(h-2)*1

        使用错位相减法求得

        T(N)=N-log2(N+1)

6.2使用向上调整法建堆时间复杂度:

        假设树的高度为h

        调整次数=每一层结点个数*这一层结点最坏向下调整次数

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

        精确算结果的话,读者可以自行使用错位相减法

        这里有一个结论:高度为h,节点数量为N的完全二叉树,那么2^(h-1)=N,h=log2(N+1)

        我们这里算一下大概,即只考虑最后一层的调整次数:

        2^(h-1)*(h-1)*2/2=2^h*(h-1)/2=(N+1)*(log2(N+1)-1)/2

        则时间复杂度大致为O(N*log2N)

由上显然知,向下调整法更优,因此我们选用向下调整法建堆

7.堆排序

大思路:选择排序,依次选数,从后往前排

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

	//选数
	int i = 1;
	while (i < n)
	{
		Swap(&a[0], &a[n - 1]);
		AdjustDown(a, n - i, 0);
		++i;
	}
}

8.TopK问题 -- 选取一堆数据中前K大或者前K小的数

        对于选前K大的数这种问题,我们有两种方法,我们先谈思路

        第一步:堆排序  O(N*logN)

        第二步:堆选数

        (1)建大堆?  建N个数大堆,选K次即可(Pop K次)     这种的时间复杂度为O(N+log N*K)

        (2)建小堆?  假设N很大,K很小。比如:N=100亿 K=100,那么(1)方法的时间复杂度过高,不实用。

                我们可以考虑用前K个数,建K个的小堆,然后依次遍历后续N-K个数,比堆顶的数据大,就替换堆顶数据,向下调整进堆。

                则最后堆里面的数据就是最大的前K个。

void CreateDataFile(const char* filename, int N)
{
	FILE* fin = fopen(filename, "w");
	if (fin == NULL)
	{
		perror("fopen fail");
		return;
	}
	srand(time(0));

	for (int i = 0; i < N; ++i)
	{
		fprintf(fin, "%d\n", rand()%1000000);
	}

	fclose(fin);
}

void PrintTopK(const char* filename, int k)
{
	assert(filename);

	FILE* fout = fopen(filename, "r");
	if (fout == NULL)
	{
		perror("fopen fail");
		return;
	}

	int* minHeap = (int*)malloc(sizeof(int)*k);
	if (minHeap == NULL)
	{
		perror("malloc fail");
		return;
	}
	// 如何读取前K个数据
	for (int i = 0; i < k; ++i)
	{
		fscanf(fout, "%d", &minHeap[i]);
	}

	// 建k个数小堆
	for (int j = (k - 2) / 2; j >= 0; --j)
	{
		AdjustDown(minHeap, k, j);
	}

	// 继续读取后N-K
	int val = 0;
	while (fscanf(fout, "%d", &val) != EOF)
	{
		if (val > minHeap[0])
		{
			minHeap[0] = val;
			AdjustDown(minHeap, k, 0);
		}
	}

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

	free(minHeap);
	fclose(fout);
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值