数据结构之堆(大顶堆)实现

这个博客总结一下二叉树的堆概念

首先我们要了解堆是什么:
堆其实就是用一维数组实现二叉树。(当然你要是用二叉链表来实现我也没办法,只不过很占内存罢了)
堆是一种非线性结构,可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组
按照堆的特点可以把堆分为大顶堆和小顶堆
大顶堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值

完全二叉树的概念在我之前博客已经整理过了。这里不再论述

但是既然是一维数组实现,且为完全二叉树,那么它的数据存放的序号是有一定关系的:
父节点序号=子节点序号/2(这个非常重要,在下面的实现中用的很频繁)
也就是这样的图:
《数据结构之堆(大顶堆)实现》
《数据结构之堆(大顶堆)实现》
图片来源于网络

也就是这样的关系

所以我们需要的是知道大顶堆如何维护:
大顶堆:父节点永远比子节点大
小顶堆:父节点永远比子节点小
插入:向上渗透,也就是先插到尾部,然后与其父节点比较,循环往复,找到他合适的位置,维护这个父节点比子节点大/小的特性
出堆:向下渗透(也可以说是出堆后,维护性质,为各个元素调整位置),找到合适位置,然后出堆

数据逻辑是这样的:
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]

虽然可能会抽象点,但是你把图画出来,然后根据我下面的代码(写有注释,我也会讲解),来理解这个过程

来吧,我们来实现一个大顶堆
(C语言)
首先是我们写一个heap(堆)的结构体

typedef struct heap { 
	int* data;
	int heapsize;
}HEAP,*LPHEAP;//起别名方便我们下面用

然后是创建并初始化的函数:

LPHEAP createheap() { 
	LPHEAP newheap = (LPHEAP)malloc(sizeof(HEAP));//分配内存
	newheap->data = (int*)malloc(sizeof(int) * 50);//一维数组的分配内存,我分配了50个
	newheap->heapsize = 0;//大小为零,也就是空的状态
	return newheap;//把这个指针返回回去,我们就创建好了
}
int isempty(LPHEAP pheap) { //万金油函数
	return pheap->heapsize==0;
}

接下来是我们的插入,首先我们想到,大小其实也就是数据存储道德地方,我们在尾部插入的话那就是,先扩大,然后以大小作为下标然后赋值就可以(因为那个序号在完全二叉树里是按着顺序直接下来的),然后再说调整的函数。所以框架是这样的:

//向上渗透法
void insertnew(LPHEAP pheap,int data) { 
	++pheap->heapsize;//先扩大
	pheap->data[pheap->heapsize] = data;//插入
	movedata(pheap,pheap->heapsize);//对于在尾部插入的数据进行移动调整。也就是向上渗透
}

然后就是实现向上调整的函数:这个函数就是就是用新插入的节点跟它的父节点比较,如果大的话,进行交换,小的话不交换,如果交换了,交换过后,还要跟上面的父节点比较。大概就是这么个过程:

void movedata(LPHEAP heap,int curPos) { 
	while (curPos > 1) { 
		int Max = heap->data[curPos];//把插入的节点作为最大值
		int parentpos = curPos / 2;//孩子节点序号跟父节点序号的关系
		if (Max > heap->data[parentpos]) { //如果现在这个max大于父节点,意味着要交换
			//swap
			//max在这里其实担任了temp的角色
			heap->data[curPos] = heap->data[parentpos];
			heap->data[parentpos] = Max;
			curPos = parentpos;//交换过后记得更新现在节点位置
		}
		else { //也就是父节点比现在这个max大了,也就是调整好了,就不用看上面了,因为大顶堆的性质
		//所以就退出循环就行了
			break;
		}
	}
}

要理解这个过程
下面就是我们的出堆的操作,也就是出去一个最大值对吧,然后维护。
那么怎么维护,要想清的是,出堆的一定是堆顶,也就是序号为1的节点,既然要出堆,那么肯定不能破坏它的秩序,所以我们向下调整,让下一层最大的替代它的位置,每一层都是,直到它到最后一层然后出堆,这样我们就能保证父节点那个位置在调整过后永远比子节点大,这就是维护过程。
下面是代码:

int popdata(LPHEAP heap) { 
	int Max = heap->data[1];//最大值是堆顶
	int curPos = 1;
	int childpos = curPos * 2;//孩子节点与父节点的关系,因为要与让孩子比较,也是为了换位
	//找到合适的位置(其实也就是出堆的时候对堆内进行维护)
	while (childpos <= heap->heapsize) { 
		int temp = heap->data[childpos];
		//同一层节点比较,找出最大的节点交换,确保大顶堆性质不变
		if (childpos + 1 <= heap->heapsize && temp < heap->data[childpos + 1]) { 
			temp = heap->data[++childpos];//最大的子节点与父节点交换
		}
		heap->data[curPos] = temp;//交换
		curPos = childpos;//交换过后,堆顶的所在位置进行改变
		childpos *= 2;//找出下层的孩子序号
	}
	heap->data[curPos] = heap->data[heap->heapsize];
	--heap->heapsize;//出堆
	return Max;
}

好了,我们一定实现了大顶堆了
我们来检验一下:

void print(LPHEAP pheap) { 
	for (int i = 1; i <= pheap->heapsize; i++) { 
		printf("%d\t", pheap->data[i]);
	}
	printf("\n");
}
int main() { 
	LPHEAP my = createheap();
	insertnew(my, 10);
	insertnew(my, 15);
	insertnew(my, 125);
	insertnew(my, 60);
	insertnew(my, 1);
	insertnew(my, 90);
	print(my);
	int a = popdata(my);
	printf("%d\n",a);
	int max = popdata(my);
	printf("%d\n", max);
	max = popdata(my);
	printf("%d\n", max);
	print(my);
}

我的程序输出结果:
《数据结构之堆(大顶堆)实现》
可以画出来它的二叉树形式,可以看到,我们的大顶堆的性质的确像概念所说,出堆是一样的

好了,我们的大顶堆完成了,相对的,我们只需要改变它的比较规则,我们可以实现不同性质的堆

可以看到堆的性质非常适合优先队列,所以优先队列也一般是这样实现的,我的之前的博客写的优先队列是线性的结构(数组和链表),他们的插入是O(n2)。但是我们可以看到,我们的堆插入和输出是跟树的深度有关,也就是log2n+1的深度,所以这个结构要比线性的快的多,也就是O(nlog2n)的时间复杂度。这也是堆排序的实现哦。

堆排序和优先队列通过这一博客也就算是学会了吧,一举三得!!

    原文作者:goudan蒻
    原文地址: https://blog.csdn.net/lspdd/article/details/113837506
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞