C语言实现堆

1.堆的物理结构和逻辑结构

堆的逻辑结构是一个完全二叉树。完全二叉树的定义是树的结点是连续的。如图:
在这里插入图片描述
下面这一颗二叉树就不是完全二叉树。因为它的结点不是连续的如图:
在这里插入图片描述
堆的物理结构是一个数组。也可以看成顺序表。元素是每一个结点的值。
在这里插入图片描述
这里有一个前人总结的规律,我们可以通过父亲的位置来推算孩子的位置。

leftchild = parent*2+1;
rightchild = parent*2+2;

用上面的图可以验证这个规律。
当然用父亲也可以找到孩子

parent = (child-1)/2;

这里有一个问题:为什么是-1,-2行不行?我们举个例子就可以说明这个问题。

父亲是第二个元素。(下标为1)
孩子是第4,第5个元素(下标为3,4)

用下标运算
(3-1)/2==1
(4-1)/2==1

由于下标是整型,1.5的小数部分被舍弃了。还是1.

2.大根堆和小根堆

堆分为大根堆和小根堆两种。不同的堆有不同的用法。在排序中会讲到。
大根堆的定义是:父亲结点大于等于孩子结点(在数值上)。
如图:
在这里插入图片描述

小根堆的定义是:父亲结点小于等于孩子结点(在数值上)
如图:
在这里插入图片描述

3.堆的实现(向下调整算法)

堆的定义:

typedef int HeapDataType;
typedef struct Heap
{
	HeapDataType* a;
	int size;
	int capacity;
}Heap;
  1. 这里的数组用指针来代替。这样可以写出动态的堆。(静态的没有实用价值)

  2. size是用来记录现在堆的大小的。

  3. capacity是来记录堆的容量。方便扩容。当size等于capacity时就扩容

int a[] = { 80,5,3,8,7,6 };

现在我们把这个数组的元素放入堆中。但是很明显就这样放进去堆里面,它并不满足堆的定义(既不是大堆也不是小堆)。
在这里插入图片描述
那我们怎么把这个顺序调整一下,使它符合堆的要求呢?
我们要使用向下调整算法。

这里又有一个问题了。我们要建大堆还是建小堆呢?
我们这里先建大堆。(小堆也是同理的)

向下调整,名副其实就是从第一个往下一直调整。调整的规则是这样的。

  1. 选出左右孩子较大的那一个。
  2. 大的孩子和父亲比较
  3. 若大的孩子比父亲大,则与父亲交换。并且把原来孩子的位置赋给父亲。父亲去找下一个孩子。迭代起来。结束条件是孩子超出了树的结点的大小
  4. 若大的孩子比父亲小,就结束迭代。

写代码的时候怎么找比较大的孩子呢?这里提供一种比较好的思路。

(孩子是下标)
假设比较大的孩子是左孩子,比较小的孩子是右孩子。
如果说左孩子比右孩子小,就把孩子加加。让孩子指向右孩子
否则就不加加,使孩子指向左孩子。

这里还有一个问题:为什么要传n(树的结点个数),原来堆里面不是已经有一个size来记录树结点个数了吗?

原因在于堆排序的设计上。这个后面再说。

void adjustDown(Heap* ph,int n, int parent)//不一定是对整一个size进行向下调整,在堆排中就最后一个排好了就不用调整了。重点!!!!!!!!
{
	int child = parent * 2 + 1;
	while (child<n)
	{
		//找大的孩子
		if (child+1<n && ph->a[child] < ph->a[child + 1])//要加一个条件防止child越界。
		{
			child++;
		}
		//如果孩子大于父亲就交换
		if (ph->a[child] > ph->a[parent])
		{
			swap(&ph->a[child], &ph->a[parent]);
			//往下迭代
			parent = child;
			child = parent * 2 + 1;
		}
		//否则结束迭代
		else
		{
			break;
		}
	}
}

注意了:当这个节点的左子树和右子树都是大堆的时候,向下调整一次就可以使整个堆变成大堆。但很多时候并没有这种情况。当这个节点的左子树和右子树都不是大堆的时候,我们就要进行多次向下调整。在HeapInit里面再讲。

4.实现堆的各种接口

下面这些接口里面有一些要注意的地方。

  1. 插入数据后和删除数据后要重新建堆
  2. 建堆时向下调整算法的使用方法
void HeapInit(Heap* ph, HeapDataType* a, int n);//建堆
void HeapDestroy(Heap* ph);//毁坏堆
void HeapPush(Heap* ph, HeapDataType x);//往堆里面插入数据。
void HeapPop(Heap* ph);//把堆顶的数据删掉
HeapDataType HeapTop(Heap* ph);//返回堆顶的数据
int HeapSize(Heap* ph);//堆的大小
int HeapEmpty(Heap* ph);//堆是否为空
void HeapPrint(Heap* ph);//打印堆,这个可有可无
void adjustUp(Heap* ph, int child);//向上调整算法
void adjustDown(Heap* ph,int n, int parent);//向下调整算法
void HeapSort(Heap* a);//堆排序,复杂度O(n*logn)

4.1建堆HeapInit

建堆是用向下调整算法。这里先建个大堆吧。
我们知道,向下调整算法只能调整堆顶的那个节点。
当这个节点的左子树和右子树都不是大堆的时候,我们应该怎么办呢?

第一次调整
在这里插入图片描述
第一次调整后的样子,要进行第二次调整了。parent-1即可以找到兄弟节点。
在这里插入图片描述
第二次调整好之后是这个样子的。parent-1找到它的父亲节点。
在这里插入图片描述
这样我们就建好一个大堆了。
问题又来了:我们怎么找到最后一个叶子的父亲节点呢?

parent = (n-1-1)/2;//n是整个树的节点大小,也就是size

代码如下:

void HeapInit(Heap* ph, HeapDataType* a, int n)
{
	//堆的初始化
	ph->a = (HeapDataType*)malloc(sizeof(HeapDataType) * n);
	ph->size = 0;
	ph->capacity = n;
	
	for (int i = 0; i < n; i++)
	{
		ph->a[i] = a[i];
		ph->size++;
	}
	
	//建堆
	for (int i = (ph->size - 1 - 1) / 2; i >= 0; i--)
	{
		adjustDown(ph,ph->size, i);
	}
}

注意:如果想建一个小堆,就在向下调整算法中把比较部分稍微修改一下即可。

4.2销毁堆HeapDestroy

销毁堆很简单,直接上代码

void HeapDestroy(Heap* ph)
{
	free(ph->a);
	ph->a = NULL;
	ph->size = 0;
	ph->capacity = 0;
}

4.3插入HeapPush(向上调整算法)

由于堆的物理结构是一个数组。Push在数组最后面即可。
问题又来了:Push在最后面,那原先的堆是否被破坏了?仍否符合大堆的标准呢?

因此我们在push之后,要对这个节点进行向上调整。
这是代码:如果数组满了就增容。

void AdjustUp(HPDataType* a, int child)
{
	int parent;
	assert(a);
	parent = (child-1)/2;
	//while (parent >= 0)
	while (child > 0)
	{
        //如果孩子大于父亲,进行交换
		if (a[child] > a[parent])
		{
			Swap(&a[parent], &a[child]);
			child = parent;
			parent = (child-1)/2;
		}
		else
		{
			break;
		}
	}
}

那向上调整代码怎么写呢?其实道理和向下调整差不多。调整规则如下(简直和向下调整一模一样):

  1. 选出左右孩子较大的那一个。
  2. 大的孩子和父亲比较
  3. 若大的孩子比父亲大,则与父亲交换。并且把原来孩子的位置赋给父亲。父亲去找下一个孩子。迭代起来。结束条件是孩子超出了树的根节点
  4. 若大的孩子比父亲小,就结束迭代。
void adjustUp(Heap* ph,int child)
{
	while (child>0)
	{
		//通过孩子找父亲节点
		int parent = (child - 1) >> 1;
		//找大的那一个孩子节点
		if (ph->a[child] < ph->a[child - 1])
		{
			child--;
		}
		//如果孩子节点比父亲节点大就交换
		if (ph->a[child] > ph->a[parent])
		{
			swap(&ph->a[child], &ph->a[parent]);
			child = parent;
			parent = (parent - 1) >> 2;
		}
		//否则就break
		else
		{
			break;
		}
	}
}

4.4删除HeapPop

删除是删除堆顶的元素。

问题来了:是直接删除吗?
答案是否定的。我们可以想象一下,如果直接删除堆顶元素,由堆顶的左孩子担任根节点。它们之间的大小关系就全乱了。那我们就要重新建堆了。这很明显效率很低。(建堆时间复杂度是O(N)).那这和顺序表的头删效率一样了。实现堆的删除也没啥价值了。

因此我们可以把堆顶的数据和堆尾的数据交换一下。然后删除堆尾的数据。再做一次向下调整。由于此时根节点的左子树和右子树都是大堆。因此做一次向下调整就可以了。(向下调整的时间复杂度是O(logn))

void HeapPop(Heap* ph)
{
	swap(&ph->a[0], &ph->a[ph->size - 1]);
	ph->size--;
	adjustDown(ph, ph->size,0);
}

注意了:删除元素其实就是把堆的size减一下就好,没必要变成0.万一原来元素就是0呢?

4.5堆的大小HeapSize

int HeapSize(Heap* ph)
{
	return ph->size;
}

4.6判断堆是否为空HeapEmpty

如果为空返回1,非空返回0

int HeapEmpty(Heap* ph)
{
	return ph->size == 0;
}

5.堆排序

堆排序可以排成升序和降序。对应建的堆也是不一样的。

先说结论:
升序--------建大堆
降序--------建小堆

文字说明堆排序原理:
堆排序的原理其实是选择排序。由于大堆的堆顶是整个堆里面最大的元素,它可以直接与数组最后一个位置的元素交换。然后不动。这一次的运算就可以让最大的数归位。然后对堆顶进行一次向下调整。又找到了剩下的数最大的那一个,然后和数组倒数第二个位置进行交换。
…一直迭代知道排序完成

图解:
在这里插入图片描述
第一次交换完之后向下调整
在这里插入图片描述
第二次交换
在这里插入图片描述
…一直迭代,排序完成
在这里插入图片描述
代码:

void HeapSort(Heap* ph)
{
	int end = ph->size - 1;
	while (end > 0)
	{
		swap(&ph->a[0], &ph->a[end]);
		adjustDown(ph,end, 0);
		end--;
	}
}

注意:每次交换完成后,最后一个数字是不用动的。因此你也不需要对它进行向下调整。这也是向下调整算法中为什么要多传一个调整个数n的原因。

最后来说一下为什么升序要排大堆,降序要排小堆的问题。
假如升序排小堆,那么堆顶的数是最小的数。它的位置就固定不动了。也就是说,根节点的位置变成了第二个元素。这样的话,剩余节点的相对大小就乱了,这也不再是堆了。要重新建堆才行。而建堆的时间复杂度是O(N),那这样的话排序完就要O(N²)的复杂度了。显然效率低。

而排大堆的时间复杂度是O(N*logN) ,遍历一遍是O(N),向下调整是O(logN).

因此升序排大堆的原因是效率高。降序排小堆也是同理。

  • 6
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值