数据结构之堆(我猜,关于堆的这些维护细节,你肯定不清楚,不信你来看!)

  在前面分析了二叉搜索树红黑树等众多树结构,今天博主给大家换个口味,深入分析一下的实现原理与维护规则。(其实与二叉树有点相似)

一、的概述

1、什么是

  ,在日常生活中是一个量词,比如:一堆木头,下面这张图就是一堆木头,大家注意它的摆放规则,成金字塔型(上窄下宽)。
在这里插入图片描述
  数据结构中的,与上面的结构类似,不过借助了二叉树的结构。肯定会有小伙伴来一句,woc,这不就是二叉树么。。。其实你也可以暂时这么理解,看完博客你就会发现还是有些区别。
在这里插入图片描述

2、的划分

  根据中元素摆放顺序的不同,可分为两种堆结构,小顶堆大顶堆

小顶堆:对于堆中的任意节点A,如果它存在子节点B、C,则 节点A的值 ≤ min{子节点B的值, 子节点C的值}

大顶堆:对于堆中的任意节点A,如果它存在子节点B、C,则 节点A的值 ≥ max{子节点B的值, 子节点C的值}
在这里插入图片描述
\color{red}注意:堆只对父节点的值与子节点的值有大小限定,但是对于节点左、右子节点的值相对关系没有限定(二叉搜索树的特征才是 左子节点 < < 右子节点)。

3、的作用

  讲了半天,也画了好几张图,辣么有什么作用呢?

  根据前面小顶堆大顶堆的定义,我们知道小顶堆堆顶存放的是堆中最小的元素,大顶堆堆顶存放的堆中最大的元素。而这正是的作用,可能会有小伙伴一脸鄙夷,就这找最值的功能都要特意设计一个数据结构来实现?

  那来一道面试题,给你10亿个数,如何在最短的时间里找出最大的10个? (我觉得如果没做出这道题,辣么只能说了解堆的定义,但是不理解、不会运用,边看博客,边思考吧,文末附答案)

二、的底层实现

  的实现一般使用数组,而不是二叉树什么的。从形态来看,堆不就是棵二叉树么,为啥不用二叉树而用数组呢?主要原因是为了随机访问通过下标访问元素)。本篇博客将只讨论使用数组实现的,如果你偏要用二叉树,可以自己实现一个。

  以数组实现不仅带来了随机访问通过下标访问元素),其实还有两个很重要的规律

  1. 下标为index的左、右孩子的下标分别是index * 2 + 1(index + 1) * 2
  2. 堆中有子节点的节点最大下标为 size / 2 - 1(注意:size为堆的大小,不是数组的大小)

在这里插入图片描述

三、的维护

  首先说明一下,一般对外展示的只有堆顶,也就是说插入移除,在外界看来都是在堆顶操作。(下面的维护都是基于小顶堆大顶堆的维护操作是类似的。)

1、扩容

  如果数组没有剩余空位置,此时需要对数组进行扩容。由于基于数组实现的只有数组这个结构,只是通过数组下标逻辑上存在,所以只要将数组中的元素按原来的顺序复制到一个更长的数组即可。
在这里插入图片描述

2、插入元素(上浮)

  对于插入操作,我们首先将元素放到下一个空闲的位置,然后对插入的元素进行上浮操作。
在这里插入图片描述
插入元素上浮伪代码如下:

// data是堆数组,size是堆的大小(并不是data数组的长度),element是待插入的节点
void insert(int data[], int &size, int element) {
	// 假设data有空位置插入,将待插入元素直接放到堆尾的下一个空位置
	int insertIndex = size++;
	data[insertIndex] = element;
	// 上浮插入节点
	while (insertIndex > 0) {
		// 求出父节点的下标(根据父节点下标能求出左右子节点下标,那么根据子节点下标同样也能求出父节点下标)
		int parentIndex = (insertIndex - 1) / 2;
		// 如果插入的元素大于父节点
		if (data[insertIndex] > data[parentIndex]) {
			// 上浮完毕
			break;
		}
		// 否则插入的元素小于父节点元素的值,交换
		swap(data[insertIndex], data[parentIndex]);
		// 更新插入元素的下标,继续上浮
		insertIndex = parentIndex;
	}
}

3、删除(堆顶)元素(下沉)

  插入元素是将尾端元素上浮,而删除堆顶元素,是将堆尾元素放入堆顶,然后将该元素下沉
在这里插入图片描述

删除元素下沉伪代码如下:

// data是堆数组,size是堆的大小(并不是data数组的长度)
void deleteTop(int data[], int &size) {
	if (size == 0) {
		// 堆为空,无需进行删除操作
		return;
	}
	// 将堆尾替换堆顶
	data[0] = data[--size];
	// 记录需要下沉节点的下标,寻找最大的有子节点的下标(前说过这是一条规律)
	int index = 0, lastHaveChildIndex = size / 2 - 1;
	// 只要当data[index]存在子节点才有下沉的必要
	while (index >= lastHaveChildIndex) {
		// 右子节点必定有左子节点(左子节点的公式: 父节点下标 * 2 + 1)
		// minChildIndex用于记录左、右子节点更小的下标
		int minChildIndex = index * 2 + 1, rightChildIndex;
		if ((rightChildIndex = index * 2 + 2) < size && data[minChildIndex] > data[rightChildIndex]) {
			// 如果index存在右子节点,并且右子节点的值比左子节点的值小,更新minChildIndex
			minChildIndex = rightChildIndex;
		}
		// 如果下沉节点data[index]比左右子节点最小值都大,停止下沉
		if (data[index] >= data[minChildIndex]){
			break;
		}
		// 否则与左、右子节点较小则交换,并且更新index,继续下沉
		swap(data[index], data[minChildIndex]);
		index = minChildIndex;
	}
}

四、总结

  堆中插入元素,直接放入数组的下一个空位置,然后上浮插入元素,即可完成堆的调整;删除堆顶元素,将堆尾的元素替换堆顶,再对堆顶的元素进行下沉,即可完成堆的调整。(再次强调一下,基于数组实现的,只有数组这个结构,只是根据数组下标的依赖关系在逻辑上存在。如果你将堆的实现修改为二叉树,那么才是真正存在。)

  现在提下前面提出的面试题的答案,给你10亿个数,如何在最短的时间里找出最大的10个?
  我们只要构建一个大小为10的小顶堆,前期取10个数直接插入堆中,然后对剩下的10亿-10个数,每此取出一个都与堆顶(堆中最小值)比较,如果比堆顶大,则替换堆顶,接着调整堆(下沉堆顶元素即可),最终堆中的元素就是10亿个数中最大的10个数。(如果答案都没看懂,再看一遍博客吧,有点走马观花哦)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章