大小堆详解&C++实现&复杂度分析

大小堆介绍部分转载自:二叉堆之图文解析

二叉堆的删除、复杂度分析和代码是自己写的~

堆和二叉堆的介绍

堆的定义

堆(heap),这里所说的堆是数据结构中的堆,而不是内存模型中的堆。堆通常是一个可以被看做一棵树,它满足下列性质:
[性质一] 堆中任意节点的值总是不大于(不小于)其子节点的值;
[性质二] 堆总是一棵完全树。
将任意节点不大于其子节点的堆叫做最小堆小根堆,而将任意节点不小于其子节点的堆叫做最大堆大根堆。常见的堆有二叉堆、左倾堆、斜堆、二项堆、斐波那契堆等等。

二叉堆的定义

二叉堆是完全二元树或者是近似完全二元树,它分为两种:最大堆最小堆
最大堆:父结点的键值总是大于或等于任何一个子节点的键值;最小堆:父结点的键值总是小于或等于任何一个子节点的键值。示意图如下:

二叉堆一般都通过"数组"来实现。数组实现的二叉堆,父节点和子节点的位置存在一定的关系。有时候,我们将"二叉堆的第一个元素"放在数组索引0的位置,有时候放在1的位置。当然,它们的本质一样(都是二叉堆),只是实现上稍微有一丁点区别。
假设"第一个元素"在数组中的索引为 0 的话,则父节点和子节点的位置关系如下:
(01) 索引为i的左孩子的索引是 (2*i+1);
(02) 索引为i的右孩子的索引是 (2*i+2);(图中性质二有错,应该改为右孩子)
(03) 索引为i的父结点的索引是 floor((i-1)/2);

假设"第一个元素"在数组中的索引为 1 的话,则父节点和子节点的位置关系如下:
(01) 索引为i的左孩子的索引是 (2*i);
(02) 索引为i的右孩子的索引是 (2*i+1);(图中性质二有错,应该改为右孩子)
(03) 索引为i的父结点的索引是 floor(i/2);

注意:本文二叉堆的实现统统都是采用"二叉堆第一个元素在数组索引为0"的方式!

二叉堆的图文解析

在前面,我们已经了解到:"最大堆"和"最小堆"是对称关系。这也意味着,了解其中之一即可。本节的图文解析是以"最大堆"来进行介绍的。

二叉堆的核心是"添加节点"和"删除节点",理解这两个算法,二叉堆也就基本掌握了。下面对它们进行介绍。

1. 添加

假设在最大堆[90,80,70,60,40,30,20,10,50]种添加85,需要执行的步骤如下:

如上图所示,当向最大堆中添加数据时:先将数据加入到最大堆的最后,然后尽可能把这个元素往上挪,直到挪不动为止!
将85添加到[90,80,70,60,40,30,20,10,50]中后,最大堆变成了[90,85,70,60,80,30,20,10,50,40]。

void insertMaxHeap(int a, vector<int>& heap) {
	heap.push_back(a);
	int insertPos=heap.size()-1, parentPos=(insertPos-1)/2;
	while(parentPos>=0 && heap[parentPos]<heap[insertPos]) {
		swap(heap, insertPos, parentPos);
		insertPos=parentPos;
		parentPos=(insertPos-1)/2;
	}
}

void insertMinHeap(int a, vector<int>& heap) {
	heap.push_back(a);
	int insertPos=heap.size()-1, parentPos=(insertPos-1)/2;
	while(parentPos>=0 && heap[parentPos]>heap[insertPos]) {
		swap(heap, parentPos, insertPos);
		insertPos=parentPos;
		parentPos=(insertPos-1)/2;
	}
}

2. 删除

在网上看了很多资料,关于二叉堆删除,大部分都只涉及到堆顶元素的删除而没有考虑到堆中任意元素的删除,具体的原因可以参考知乎:为什么二叉堆只能删除堆顶元素? - 知乎  简单来说就是因为二叉堆不支持查找元素位置,因此删除一个你完全不知道内容的元素毫无意义。如果要实现任意位置元素的删除操作,那么其他数据结构可能更合适(比如AVL树或者红黑树)。

当然这并不意味着不能删除二叉堆中的任意节点,参考algorithm - How to delete in a heap data structure? - Stack Overflow  删除堆中任意节点的一个最常见的应用是:heap representation of a priority queue of scheduled jobs, and somebody cancels one of the jobs.

本文既考虑堆顶元素的删除也考虑任意元素的删除。其实,删除堆顶元素是删除任意元素的特例,且删除堆顶元素的复杂度是删除任意元素在最坏情况下的复杂度O(logn)

2-1. 删除堆顶元素

假设从最大堆[90,85,70,60,80,30,20,10,50,40]中删除堆顶元素90,需要执行的步骤如下:

从[90,85,70,60,80,30,20,10,50,40]删除90之后,最大堆变成了[85,80,70,60,40,30,20,10,50]。
如上图所示,当从最大堆中删除堆顶元素时:先删除该数据,然后用最大堆中最后一个的元素插入这个空位;接着,把这个元素尽量往下挪,直到剩余的数据变成一个最大堆。往下挪的过程是需要找到该元素两个子节点中较大的子节点替换该元素。

2-2. 删除堆中任意元素

参考http://www.mathcs.emory.edu/~cheung/Courses/171/Syllabus/9-BinTree/heap-delete.html

删除堆中任意元素的伪码如下:

1, Delete a node from the array 
找到该元素并删除该元素对应的节点
(this creates a "hole" and the tree is no longer "complete")

2. Replace the deletion node with the "fartest right node" on the lowest level of the Binary Tree
将数组末尾元素挪至第1步删除的节点位置上
(This step makes the tree into a "complete binary tree")

3. Heapify (fix the heap):
if (value in replacement node < its parent node )
    Filter the replacement node UP the binary tree
else if (value in replacement node > its child node
    Filter the replacement node DOWN the binary tree
else
    nothing to do
调整数组元素使其重新成为一个最小堆:
若被替换位置上的元素<父节点元素(如果存在)
    将该元素与父节点元素交换,相当于把该元素往上挪
否则 若被替换位置上的元素>子节点元素(如果存在)
    将该元素与大于其的子节点元素中的较小值交换机,相当于把该元素往下挪
否则
    不需要做任何事情,该数组可以构成最小堆

假设有下面的最小堆:

当我们删除元素5时:

  • 删除元素5之后,红心的地方空出来了,此时,数组不再构成最小堆:

我们将数组末尾的元素21上挪到元素5原来的位置:

  • 此时,数组变成了完全二叉树,但仍然不满足最小堆的大小关系:
  • 我们必须调整数组元素的位置使其满足最小堆的条件。根据被替换节点也就是说21所在的节点与其父节点以及子节点的大小关系,可以将21这个元素上挪或者下移。由于21比它的父节点1小,比它的两个子节点9和11都要大,因此我们将21往下挪,将它与较小的子节点9交换:

  • 重复上述过程,继续判断21与它的子节点的大小关系。由于21比子节点17大,因此,21应该要往下挪,与它唯一的子节点交换位置:
  • 此时,21这个节点比父节点17大,同时它不存在子节点。此时的数组构成了最小堆。

下面是另一个删除堆中任意节点的例子:我们删除33这个元素对应的节点:

将33这个元素对应的节点与数组末尾元素19交换:

由于此时19比它的父节点小,且它没有子节点,因此我们交换19与它的父节点22(图中标错了)的位置:

19比它的父节点1小,同时比它的两个子节点也要小,数组构成最小堆

可以发现,删除堆中任意一个节点并将其上挪或者下移的次数最多为二叉堆的高度,当被删除的节点为根节点时,需要挪动的次数最多,为logn次,此时,删除操作的复杂度最高,为O(logn)
比如,当我们删除根节点1时:

#include<iostream>
#include<vector>
#include<cmath>
using namespace std;

void constructMaxHeap(vector<int>&);
void constructMinHeap(vector<int>&);
void insertEleMaxHeap(int, vector<int>&);
void deleteEleMaxHeap(int, vector<int>&);
void insertEleMinHeap(int, vector<int>&);
void deleteEleMinHeap(int, vector<int>&);
void swap(vector<int>&, unsigned, unsigned);
void shiftDownMaxHeap(vector<int>&, int);
void shiftUpMaxHeap(vector<int>&, int);
void shiftDownMinHeap(vector<int>&, int);
void shiftUpMinHeap(vector<int>&, int);

int main() {
	int num=0;
	vector<int> maxHeap;
	cout << "Construct the max heap using the number entered: ";
	while(cin>>num) {
		maxHeap.push_back(num);
		if(getchar()=='\n') {
			break;
		}
	}
	constructMaxHeap(maxHeap);
	for(auto i:maxHeap) {
		cout << i << " ";
	}
	cout << endl;

	cout << "please enter the number u want to insert: ";
	int insertNum=0;
	cin >> insertNum;
	insertEleMaxHeap(insertNum, maxHeap);
	for(auto i:maxHeap) {
		cout << i << " ";
	}
	cout << endl;

	cout << "please enter the number u want to delete: ";
	int deleteNum=0;
	cin >> deleteNum;
	deleteEleMaxHeap(deleteNum, maxHeap);
	for(auto i:maxHeap) {
		cout << i << " ";
	}
	cout << endl << endl;

	vector<int> minHeap;
	cout << "Construct the min heap using the number entered: ";
	while(cin>>num) {
		minHeap.push_back(num);
		if(getchar()=='\n') {
			break;
		}
	}
	constructMinHeap(minHeap);
	for(auto i:minHeap) {
		cout << i << " ";
	}
	cout << endl;

	cout << "please enter the number u want to insert: ";
	cin >> insertNum;
	insertEleMinHeap(insertNum, minHeap);
	for(auto i:minHeap) {
		cout << i << " ";
	}
	cout << endl;

	cout << "please enter the number u want to delete: ";
	cin >> deleteNum;
	deleteEleMinHeap(deleteNum, minHeap);
	for(auto i:minHeap) {
		cout << i << " ";
	}
	cout << endl;
}

void constructMaxHeap(vector<int> &v) {
	for(int i=1; i<static_cast<int>(v.size()); ++i) {
		shiftUpMaxHeap(v, i);
	}
}

void constructMinHeap(vector<int> &v) {
	for(int i=1; i<static_cast<int>(v.size()); ++i) {
		shiftUpMinHeap(v, i);
	}
}

void swap(vector<int> &v, unsigned i, unsigned j) {
	int t=v[i];
	v[i]=v[j];
	v[j]=t;
}

void insertEleMaxHeap(int ele, vector<int> &v) {
	v.push_back(ele);
	shiftUpMaxHeap(v, v.size()-1);
}

void deleteEleMaxHeap(int ele, vector<int> &v) {
	unsigned deletePos=0;
	for(; deletePos<v.size(); ++deletePos) {
		if(ele==v[deletePos]) {
			break;
		}
	}
	swap(v, deletePos, v.size()-1);
	v.erase(v.end()-1);

	// 没有父节点也没有子节点,堆中只有一个元素
	if(v.size()==1) {
		return ;
	}
	// 没有父节点但有子节点,往下调整
	else if(deletePos==0) {
		shiftDownMaxHeap(v, deletePos);
	}
	// 有父节点但没有子节点,往上调整
	else if(2*deletePos+1>=v.size()) {
		shiftUpMaxHeap(v, deletePos);
	}
	// 既有子节点又有父节点,根据大小关系判断down()或者up()
	else {
		if(v[floor((deletePos-1)/2.0)]<v[deletePos]) {
			shiftUpMaxHeap(v, deletePos);
		}
		else if(v[2*deletePos+1]>v[deletePos]||
				2*deletePos+2<v.size()&&v[2*deletePos+2]>v[deletePos]) {
			shiftDownMaxHeap(v, deletePos);
		}
	}
}

void shiftDownMaxHeap(vector<int> &v, int p) {
	unsigned leftChildPos=2*p+1, rightChildPos=2*p+2;
	unsigned maxPos=p;
	while(p<v.size()) {
		if(leftChildPos<v.size() && v[leftChildPos]>v[maxPos]) {
			maxPos=leftChildPos;
		}
		if(rightChildPos<v.size() && v[rightChildPos]>v[maxPos]) {
			maxPos=rightChildPos;
		}
		if(p!=maxPos) {
			swap(v, p, maxPos);
			p=maxPos;
			leftChildPos=2*p+1;
			rightChildPos=2*p+2;
		}
		else {
			break;
		}
	}
}

void shiftUpMaxHeap(vector<int> &v, int p) {
	int parentPos=floor((p-1)/2.0);
	while(parentPos>=0 && v[parentPos]<v[p]) {
		swap(v, parentPos, p);
		p=parentPos;
		parentPos=floor((p-1)/2.0);
	}
}

void insertEleMinHeap(int ele, vector<int> &v) {
	v.push_back(ele);
	shiftUpMinHeap(v, v.size()-1);
}

void deleteEleMinHeap(int ele, vector<int> &v) {
	unsigned deletePos=0;
	for(unsigned i=0; i<v.size(); ++i) {
		if(ele==v[i]) {
			deletePos=i;
			break;
		}
	}
	swap(v, deletePos, v.size()-1);
	v.erase(v.end()-1);

	if(v.size()==1) {
		return;
	}
	else if(deletePos==0) {
		shiftDownMinHeap(v, deletePos);
	}
	else if(2*deletePos+1>=v.size()) {
		shiftUpMinHeap(v, deletePos);
	}
	else {
		if(v[floor((deletePos-1)/2.0)]>v[deletePos]) {
			shiftUpMinHeap(v, deletePos);
		}
		else if((v[deletePos*2+1]<v[deletePos])||
				(deletePos*2+2<v.size()&&v[deletePos*2+2]<v[deletePos])) {
			shiftDownMinHeap(v, deletePos);
		}
	}
}

void shiftDownMinHeap(vector<int> &v, int p) {
	unsigned leftChild=2*p+1, rightChild=2*p+2;
	unsigned minPos=p;
	while(p<v.size()) {
		if(leftChild<v.size()&&v[leftChild]<v[minPos]) {
			minPos=leftChild;
		}
		if(rightChild<v.size()&&v[rightChild]<v[minPos]) {
			minPos=rightChild;
		}
		if(minPos!=p) {
			swap(v, minPos, p);
			p=minPos;
			leftChild=2*p+1;
			rightChild=2*p+2;
		}
		else {
			break;
		}
	}
}

void shiftUpMinHeap(vector<int> &v, int p) {
	int parentPos=floor((p-1)/2.0);
	while(parentPos>=0 && v[parentPos]>v[p]) {
		swap(v, parentPos, p);
		p=parentPos;
		parentPos=floor((p-1)/2.0);
	}
}

二叉堆的插入、删除以及构造时间复杂度分析(最大堆为例)

二叉堆的插入

最好情况下,需要插入的节点只需要一次比较,比它的父节点小即可。因此时间复杂度为O(1)

最差情况下,需要插入的节点要一直从倒数第二层比较到第一层的根节点,共比较(h-1)次,h为高度。由于高为h的完全二叉树的节点数最多为2^h-1,所以有h=log(n+1),n为节点数。故最差情况下插入的时间复杂度为O(logn)

平均情况下,需要插入的节点最终位于二叉堆的各个位置的概率均为1/(2^h-1),而第i层的节点个数为2^(i-1),如果该插入的节点最终位于第i层,则完成该插入操作需要比较(i-1)次。所以平均情况下的比较操作共:

故平均情况下,插入的时间复杂度为

求解得:

因此平均情况下插入的复杂度为O(1)

二叉堆的删除

最好情况下,二叉堆的删除只需要直接将被删除节点与数组最后的节点交换一次,并和子节点比较一次即可,复杂度为O(1)

最坏情况下,二叉堆的删除在交换后,需要将替换的节点与他的两个子节点进行比较,比较次数最多为(h-1)次,复杂度为O(logn)

二叉堆的构造

二叉堆的构造方法有两种,分别采用的是插入和删除的思想。

采用二叉堆插入思想进行二叉堆构造

可以想象刚开始是一个空堆,不停地将将元素插入这个堆。总共n个元素,每次插入时间复杂度为O(logn),因此总的复杂度为O(nlogn)。

/* Apply insertion to construct heap.
 * Complexity is O(nlogn).
 */
void constructMaxHeap(const vector<int>& nums, vector<int>& heap) {
	for(const auto i:nums) {
		heap.push_back(i);
		int insertPos=heap.size()-1, parentPos=(insertPos-1)/2;
		while(parentPos>=0 && heap[parentPos]<heap[insertPos]) {
			swap(heap, parentPos, insertPos);
			insertPos=parentPos;
			parentPos=(insertPos-1)/2;
		}
	}
}

void constructMinHeap(const vector<int>& nums, vector<int>& heap) {
	for(auto num:nums) {
		heap.push_back(num);
		int insertPos=heap.size()-1, parentPos=(insertPos-1)/2;
		while(parentPos>=0 && heap[parentPos]>heap[insertPos]) {
			swap(heap, parentPos, insertPos);
			insertPos=parentPos;
			parentPos=(insertPos-1)/2;
		}
	}
}

采用二叉堆删除的思想构造二叉堆

可以直接将输入的数组看成一个二叉堆,只不过这个二叉堆现在不是最大堆,我们需要对它进行调整。调整直接从倒数第二层靠右边第一个有孩子的节点开始(倒数第一层都是叶子节点,不需要调整)一直到根节点止。调整的过程就是不断将待调整的节点与它的孩子比较,直到该节点的值比它的孩子都大或者该节点成为叶子节点。当需要调整的节点位于第i层,则该层共有2^(i-1)个节点,每次调整需要比较(h-i)次,因此总共的比较次数为

T(h)=\sum_{i=1}^{h-1}(h-i)*2^{i-1}

以比较次数作为基本操作,T(h)即为构造二叉堆的时间复杂度。展开可得:

T(h)=2^0(h-1)+2^1(h-2)+2^2(h-3)+...+2^{h-3}(h-h+2)+2^{h-2}(h-h+1)

T(h)=2^0(h-1)+2^1(h-2)+2^2(h-3)+...+2^{h-3}*2+2^{h-2}*1

2T(h)=2^1(h-1)+2^2(h-2)+2^3(h-3)+...+2^{h-2}*2+2^{h-1}*1

\therefore 2T(h)-T(h)=2^1(h-1-h+2)+2^2(h-2-h+3)+2^3(h-3-h+4)+...+2^{h-2}+2^{h-1}-2^0(h-1)

\therefore T(h)=2^1+2^2+...+2^{h-1}-2^0(h-1)=2^h-h-1

而二叉堆中总节点数n=2^h-1,因此构造二叉堆的时间复杂度为O(n)

void constructMaxHeap(vector<int> &num) {
	int pos=(num.size()-1)/2;
	for(; pos>=0; --pos) {
		maxifyHeap(num, pos);
	}
}

void maxifyHeap(vector<int> &num, int pos) {
	int maxPos=pos, leftChildPos=2*pos+1, rightChildPos=leftChildPos+1;
	while(pos<num.size()) {
		if(leftChildPos<num.size() && num[leftChildPos]>num[maxPos]) {
			maxPos=leftChildPos;
		}
		if(rightChildPos<num.size() && num[rightChildPos]>num[maxPos]) {
			maxPos=rightChildPos;
		}
		if(maxPos!=pos) {
			swap(num, maxPos, pos);
			pos=maxPos;
			leftChildPos=2*pos+1;
			rightChildPos=leftChildPos+1;
		}
		else {
			break;
		}
	}
}

void constructMinHeap(vector<int> &num) {
	int pos=(num.size()-2)/2;
	for(; pos>=0; --pos) {
		minifyHeap(num, pos);
	}
}

void minifyHeap(vector<int> &num, int pos) {
	int minPos=pos, leftChildPos=2*pos+1, rightChildPos=2*pos+2;
	while(pos<num.size()) {
		minPos=pos;
		if(leftChildPos<num.size() && num[leftChildPos]<num[minPos]) {
			minPos=leftChildPos;
		}
		if(rightChildPos<num.size() && num[rightChildPos]<num[minPos]) {
			minPos=rightChildPos;
		}
		if(minPos!=pos) {
			swap(num, pos, minPos);
			pos=minPos;
			leftChildPos=2*pos+1;
			rightChildPos=leftChildPos+1;
		}
		else {
			break;
		}
	}
}

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值