浙江大学数据结构 堆 C语言

一 什么是堆?

1.问题引入

        当我们需要不断从一个有序数列中插入元素,取出最大值(或者最小值)时,通常会先将元素存放进有序数组,有序链表,查找树中,

        但这三种存储方式都有缺点

        有序数组插入值的时间复杂度为O(n),删除最值的时间复杂度为O(1)

        有序链表插入值时间复杂度为O(n),删除最值时间复杂度为O(1)

        查找树的插入值的时间复杂度为O(logn)(平衡二叉树树高)删除最值时间复杂度为O(logn),但随着不断删除元素,树也越来越倾斜,使得树高不再是logn,增大了时间复杂度

        综上我们可以用树的结构来存储数据,只不过要避免删除数据使得树越来越倾斜这个问题。

2.最大堆

        我们将最大值放在一棵完全二叉树的树根,而对于其他元素来说,都是以其为根的子树的最大值(左右节点的值都小于根节点)。

3.堆的两个特性

        1.结构性:用数组表示的完全二叉树。

        2.有序性:任一节点的关键字是其子树所有节点的最大值(或最小值)

                最大堆(MaxHeap)根节点存放最大值

                最小堆(MinHeap)根节点存放最小值

                                最大堆                                                                最小堆

                不是完全二叉树                          对于堆来说 树中的每一条路径都应该是有序的

                                                                    最大堆从大到小     最小堆从小到大

二 堆的操作

1.最大堆的创建

struct _MaxHeap
{
	int *data;      //堆使用数组实现的所以元素都存入数组中
	int size;		//堆的当前元素个数 
	int capacity;	//堆的最大容量 
};

typedef struct _MaxHeap* MaxHeap;

MaxHeap maxheap_creat(int MaxSize)
{
	MaxHeap heap = (MaxHeap)malloc(sizeof(struct _MaxHeap));
	heap -> data = (int*)malloc(sizeof(int) * (MaxSize + 1));	//零号节点为哨兵节点 不存放元素 
	heap -> data[0] = MAXDATA;
	heap -> capacity = MaxSize;
	heap -> size = 0;
	return heap; 
}

将数组元素设置为MAXSIZE+1,是因为零号元素不存放值,存放哨兵,哨兵元素会在接下来的插入操作中起到作用

2.最大堆的插入

对于树来说,我们插入一个节点,最自然的想法就是将元素插入树的最后一个位置,以保证堆仍然是一个完全二叉树

假如我们插入20在最后一个位置,可以看到每个节点都是以当前节点为根的子树的最大值,保持着最大堆的有序性

但插入58时,58>31,破坏了最大堆有序性,我们就要把58往上调,那么上调如何实现呢,见下代码。

void maxheap_insert(MaxHeap heap, int item)
{
	if(maxheap_isfull(heap))
	{
		printf("the maxheap is full\n");
		return ;
	}
	int i;
//							当前节点的父节点 
	for(i = ++heap -> size; heap -> data[i/2] < item; i /= 2)
//	注意要将哨兵节点 data[0] 设为足够大的值防止死循环 
//	或者限制 i > 1 
//	若父节点小于要插入的节点 将父节点下移 
		heap -> data[i] = heap -> data[i/2];
//	知道父节点大于要插入的节点 将要插入的值赋予当前节点	 
	heap -> data[i] = item;
}

在for循环中,将i的初始值设置为树的元素个数即数组最后一个下标加一,同时数组个数加一,判断父节点与要插入元素的大小,如果父节点(数组中父节点下标为左右子节点除以二)小于item,说明item还要向上挪动,就将父节点下移,之后 i 进入父节点在比较item与父节点之间的大小关系。

最后当父节点的值大于要插入的值的时候,就意味着当前 i 就是要插入的位置,直接赋值即可

需要注意的是,如果我们插入的值大于当前树中的任意一个值,for循环就会比较item与零号元素的大小,这时我们将零号元素设置为一个足够大的数,就可以退出循环,而将item放入根节点

或者可以设置循环条件 i>1,保证了插入元素与根节点(下标为1)比较完成之后就能退出循环

3.最大堆的删除

对于堆的删除操作,我们只删除根节点,然后将最后一个位置的元素(记为temp)移入根节点中,但根的有序性就被破坏了,这时我们要从根节点开始,选取两个子节点中值大的那一个上调,在进入被上调的那一个子树中,重复操作直到item的值大于左右子节点,或者当前位置没有左右子节点。

int maxheap_delete(MaxHeap heap)
{
	int parent, child;
	if(maxheap_isempty(heap))
	{
		printf("the heap is empty\n");
		return 0;
	}
	int maxitem = heap -> data[1];		//存放要删除的最大值 
	int temp = heap -> data[heap->size--];	//存放替换最大值的数组中的最后一个元素同时数组大小减一 
	for(parent = 1; parent * 2 <= heap -> size; parent = child)//循环步进条件 进入左右的大节点的子树 
	{//				进入循环的条件为当前节点存在子节点 
		child = 2 * parent;		//定义左儿子节点 
		if(child != heap -> size && heap -> data[child+1] > heap -> data[child])
			child++;	//判断右儿子节点是否存在并存放左右儿子中的较大者 
		if(heap -> data[child] <= temp)
			break;		//当左右儿子都小于要替换根节点的元素时 退出循环 
		heap -> data[parent] = heap -> data[child];
//		将根结点的值替换为子节点中的较大值 进入下一次循环 
	}
	heap -> data[parent] = temp;	//退出循环后将根节点值赋为数组总的最后一个元素 
	return maxitem;
}

先将堆中的第一个元素也就是要返回的最大值存起来,然后再用temp代表堆中的最后一个元素

定义parent child两个变量,分别表示当前节点 和左右节点中较大的那一个。for循环中,parent从1开始,先判断是否存在左子节点(数组中左子节点下标为父节点下标 * 2)如果有,将下标赋给child,接下来的 if 语句来判断是否存在右子节点,有的话右子节点的值是不是大于左子节点,是的话就将child变为右子节点下标,总之一句话:第一个 if 就是取出左右子节点中的较大者。

第二个 if 来判断较大子节点与temp的大小关系,如果较大子节点大于temp,将较大子节点赋给当前位置,循环步进条件parent=child,下一层循环就会进入较大子节点的树中。否则的话说明temp都大于两个子节点,退出循环即可。

循环推出条件:没有左右子节点。

4.最大堆的建立

堆的建立有两种方法

第一种 不断将元素插入堆中,共有n个元素插入一次的时间复杂度为O(logn),总的时间复杂度为O(nlogn),下面是一种时间复杂度为O(n)的方法。

第二种,回顾删除操作,删除是将堆顶的值删除将最后一个元素放到堆顶,这时虽然整棵树不是最大堆但左右子树都是最大堆,我们就可以用parent,child循环的方法重新调整成一个堆,对于一个无序二叉树,我们可以从最后一个有子树的节点开始不断向上调整(最后一个有子树的节点,左右子树只有一个节点或者没有,必是一个最大堆)。

void maxheap_perdown(MaxHeap heap, int p)
{
	int parent, child;
	int temp = heap -> data[p];
	for(parent = p; parent * 2 <= heap -> size; parent = child)
	{
		child = parent * 2;
		if(child != heap -> size && heap -> data[child+1] > heap -> data[child])
			child++;
		if(heap -> data[child] <= temp)
			break;
		heap -> data[parent] = heap -> data[child];
	}
	heap -> data[parent] = temp;
}

void maxheap_build(MaxHeap heap)
{
	int i;
	for(i = heap -> size / 2; i > 0; i--)	//找到最后一个节点的父节点 依次进入下滤函数 
		maxheap_perdown(heap, i);
}

堆的建立与堆的删除思路是一样的,堆的删除就是将最后一个元素放于堆顶之后的堆的建立。

三 堆排序

堆常见的一个应用就是对数据进行排序

堆排序的第一个很容易想到的方法就是,构建一个最大堆,每次都把最大值弹出来,在重新构造堆,直到所有元素都被弹出。这种方法有个缺点就是,必须将弹出的元素另外放入一个新开辟的空间中,再按从大到小的顺序从新排入原本的数组中,这就造成了空间上的浪费,并且来回赋值元素也会造成时间上的浪费

第二种方法 仍然构建最大堆,但每次不弹出元素,而是把最大值放到堆的最后一个位置,再将堆大小减一

本质来看两种方法很相似,只不过第二种方法是将堆空间和新开辟的临时空间合并到了一起。

#include <stdio.h>

void swap(int* a, int* b)
{
	int t = *a;
	*a = *b;
	*b = t;
}

void percdown(int a[], int i, int n)
{
	int parent, child;
	int temp = a[i];
	
	for(parent = i; parent * 2 + 1 <= n - 1; parent = child)
	{
		child = parent * 2 + 1;
		if(child != n - 1 && a[child + 1] > a[child])
			child++;
		if(a[child] < temp)
			break;
		a[parent] = a[child];
	}
	a[parent] = temp;
}

void heap_sort(int a[], int n)
{
	int i;
	for(i = n / 2; i >= 0; i--)
		percdown(a, i, n);
	for(i = n - 1; i > 0; i--)
	{
		swap(&a[0], &a[i]);
		percdown(a, 0, i);
	}
}

int main()
{
	int a[] = {81, 94, 11, 96, 12, 35, 17, 95, 28, 58, 41, 75, 15};
	heap_sort(a, sizeof(a) / sizeof(int));
	for(int i = 0; i < sizeof(a) / sizeof(int); i++)
		printf("%d ", a[i]);
	
	return 0;
}

在heap_sort函数中,第一个循环是堆的建立,第二个循环一开始就是交换堆顶的最大值与数组最后一个元素,在重新构建最大堆。

需要注意的是:在堆排序中,元素下标是从零开始的,没有堆结构中的哨兵位置,这时父节点与子节的关系就发生了改变 leftchild = parent * 2 + 1, rightchild = parent * 2 + 2,parent = leftchild / 2, parent = rightchild / 2 - 1。

同样要注意这时数组最后元素的下标不再是n了而是n - 1。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值