通用算法 - [树结构] -堆

本文介绍了堆的基本概念,包括最大堆和最小堆的定义,并对比了堆与二叉搜索树的区别。堆的常用操作包括上浮、下沉、插入、删除堆顶元素以及建堆等,这些操作的时间复杂度大多为O(log n)。堆主要用于快速获取最大或最小元素,而非搜索。

1、堆的概念

堆分为两种:最大堆和最小堆,两者的差别在于节点的排序方式。

在最大堆中,父节点的值比每一个子节点的值都要大。在最小堆中,父节点的值比每一个子节点的值都要小。这就是所谓的“堆属性”,并且这个属性对堆中的每一个节点都成立。

例子:
在这里插入图片描述
这是一个最大堆,,因为每一个父节点的值都比其子节点要大。10 比 7 和 2 都大。7 比 5 和 1都大。

由于堆是存储在数组(长度为n,下标从0开始)中的,因此,在堆中给定下标为i的结点时:

  1. 如果 i = 0,结点 i 是根结点,无父结点;否则结点 i 的父结点为结点 (i - 1 )//2;
  2. 如果 2i + 1 > n - 1,则结点 i 无左子女;否则结点 i 的左子女为结点 2i + 1
  3. 如果 2i + 2 > n - 1,则结点 i 无右结点;否则结点 i 的右子女为结点 2i + 2

2、堆与二叉搜索树的区别

堆并不能取代二叉搜索树,它们之间有相似之处也有一些不同。我们来看一下两者的主要差别:

  • 节点的顺序。在二叉搜索树中,左子节点必须比父节点小,右子节点必须必比父节点大。但是在堆中并非如此。在最大堆中两个子节点都必须比父节点小,而在最小堆中,它们都必须比父节点大。

  • 内存占用。普通树占用的内存空间比它们存储的数据要多。你必须为节点对象以及左/右子节点指针分配额为是我内存。堆仅仅使用一个数组来存储数据,且不使用指针。

  • 平衡。二叉搜索树必须是“平衡”的情况下,其大部分操作的复杂度才能达到O(log n)。你可以按任意顺序位置插入/删除数据,或者使用 AVL 树或者红黑树,但是在堆中实际上不需要整棵树都是有序的。我们只需要满足对属性即可,所以在堆中平衡不是问题。因为堆中数据的组织方式可以保证O(log n) 的性能。

  • 搜索。在二叉树中搜索会很快,但是在堆中搜索会很慢。在堆中搜索不是第一优先级,因为使用堆的目的是将最大(或者最小)的节点放在最前面,从而快速的进行相关插入

2、堆的常用操作

有两个原始操作用于保证插入或删除节点以后堆是一个有效的最大堆或者最小堆:

  • shiftUp(): 如果一个节点比它的父节点大(最大堆)或者小(最小堆),那么需要将它与它的父节点进行交换,不断向上移动,直到它到达堆中应该存在的位置。这样是这个节点在数组的位置上升。
  • shiftDown(): 如果一个节点比它的子节点小(最大堆)或者大(最小堆),那么需要将它与它的其中一个子节点交换,不断向下移动,直到它到达堆中应该存在的位置位置。这个操作也称作“堆化(heapify)”。

由于上述两个操作的时间复杂度与堆的高度有关,所以shiftUp 或者 shiftDown 的时间复杂度是 O(log n)。

基于这两个原始操作还有一些其他的操作:

  • insert(value): 在堆的尾部添加一个新的元素,然后使用 shiftUp 来修复堆。
  • remove(): 移除并返回最大值(最大堆)或者最小值(最小堆)。为了将这个节点删除后的空位填补上,需要将最后一个元素移到根节点的位置,然后使用 shiftDown 方法来修复堆。
  • removeAtIndex(index): 和 remove() 一样,差别在于可以移除堆中任意节点,而不仅仅是根节点。当它与子节点比较位置不是无序时使用 shiftDown(),如果与父节点比较发现无序则使用 shiftUp()。
  • replace(index, value):将一个更小的值(最小堆)或者更大的值(最大堆)赋值给一个节点。由于这个操作破坏了堆属性,需要修复堆属性,以最大堆为例,如果替换的值比原来的值大,则使用shiftUp,否则使用shiftDown 。

上面所有的操作的时间复杂度都是 O(log n),因为 shiftUp 和 shiftDown 都很费时。还有少数一些操作需要更多的时间:

  • search(value):堆不是为快速搜索而建立的,但是 replace() 和 removeAtIndex() 操作需要找到节点在数组中的index,所以你需要先找到这个index。时间复杂度:O(n)。
  • buildHeap(array):可以通过反复调用 insert() 往堆中插入元素的方法生成堆。假设nnn个元素构成的堆高度为h=log2nh=log_2nh=log2n,最坏的情况,第kkk层有2k−12^{k-1}2k1 个节点,这层每插入一个节点,调整时需要比较k−1k-1k1次。那么总的需要比较的次数为∑k=1h2k−1∗(k−1)=O(nlog2n)\sum_{k=1}^{h}2^{k-1}*(k-1)=O(nlog_2n)k=1h2k1(k1)=O(nlog2n)
  • adjustHeap(array):将最后一个父节点开始,不断地通过shifDown()将一个数无序组调整为堆。假设nnn个元素构成的堆高度为h=log2nh=log_2nh=log2n,在最坏的情况下,第iii层有2i−12^{i-1}2i1个节点(满二叉树),调整时每个节点需要移动h−ih-ihi次,所以总共需要移动∑k=1h2k−1∗(h−k)=O(nlog2n)\sum_{k=1}^{h}2^{k-1}*(h-k)=O(nlog_2n)k=1h2k1(hk)=O(nlog2n),这个等式相当于计算高度为hhh的完全二叉树的所有节点的高度和。高度为hhh的完全二叉树有2h−12^{h}-12h1个节点,所有节点的高度的和为2h−1−h=n−1−log2n2^{h}-1-h=n-1-log_2n2h1h=n1log2n,所以时间按复杂度约为O(n)O(n)O(n)
  • heapSort(array):堆排序,由于堆就是一个数组,我们可以使用它独特的属性将数组从低到高排序。时间复杂度:O(n lg n)。
  • peek() :不用删除节点就返回最大值(最大堆)或者最小值(最小堆)。时间复杂度 O(1) 。

3、堆的常用操作实现

  • 上浮操作shiftUp():
//将某个节点进行上浮操作(以最大堆为例)
void shiftUp(vector<int>& heaparray,int child_ind){

	int child_value = heaparray[child_ind];
	
	//令parent_index,child_index分别指向父节点和孩子节点的索引;
	int child_index = child_ind;
	int parent_index = (child_index - 1) / 2;

	//当父节点存在时,并且父节点小于孩子节点时,将父节点和孩子节点进行交换
	while (parent_index >= 0 && heaparray[parent_index] < heaparray[child_index]){
		heaparray[child_index] = heaparray[parent_index];
		heaparray[parent_index] = child_value;

		child_index = parent_index;
		parent_index = (child_index - 1) / 2;

	}
	
}
  • 下沉操作shiftDown()
void shiftDown(vector<int>& heaparray,int parent_ind){
	int parent_value = heaparray[parent_ind];
	int length = heaparray.size();

	//令parent_index 和child_index 分别指向父节点和左孩子节点
	int parent_index = parent_ind;
	int child_index = 2 * parent_index + 1;
	while (child_index < length){
		//令child_index指向左右孩子中的最大值。
		if (child_index + 1 < length && heaparray[child_index + 1] > heaparray[child_index]){
			child_index++;
		}

		if (heaparray[parent_index] > heaparray[child_index]){
			break;
		}
		//如果父节点小于子节点,则进行交换
		heaparray[parent_index] = heaparray[child_index];
		heaparray[child_index] = parent_value;
		parent_index = child_index;
		child_index = parent_index * 2 + 1;
	}
}
  • 插入操作insert()
void insert(vector<int>& heaparray,int value){
	//在堆的尾部添加一个新的元素
	heaparray.push_back(value);
	//使用上浮操作来修复堆
	shiftUp(heaparray, heaparray.size() - 1);
}
  • 删除堆顶元素操作
void remove(vector<int>& heaparray){
	if (heaparray.empty() == false){
		//使用堆的最后一个元素覆盖堆顶元素
		heaparray[0] = heaparray.back();
		//删除最后一个元素
		heaparray.pop_back();

		//通过下沉操作修复堆
		shiftDown(heaparray, 0);
	}
}
  • 建堆操作buildHeap()
//自顶向下建堆,从空堆开始,每次都向堆中插入元素
vector<int> buildHeap(vector<int>& heaparray){
	vector<int> newheaparray;
	for (int i = 0; i < heaparray.size(); i++){
		insert(newheaparray, heaparray[i]);
	}
	return newheaparray;
}

调堆操作adjustHeap()

//自底向上调堆,将一个无序的数组调整为堆
void adjustHeap(vector<int>& heaparray){
	//从最后一个父节点开始,到根节点,多次调用下沉操作来建立最大堆
	int last_parent = (heaparray.size() - 2) / 2;
	for (int parent = last_parent; parent >= 0; parent--){
		shiftDown(heaparray, parent);
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Albert_YuHan

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值