数据结构-堆和堆排序 C++实现

为什么使用堆

堆的主要用途是在处理优先队列上,相比于普通队列的先进先出,后进后出,优先队列出队顺序和入队顺序无关,只和优先级有关。比如头等舱登机就比普通乘客先登机,而和先来后到没有关系。但是有人会说我把数组先进行排序再处理呗,但是现实生活中往往是需要进行动态处理的,如果每次有新元素都要进行重新排序,那处理起来是十分复杂的。此时使用堆的数据结构处理速度会有一定的提升。
在这里插入图片描述
通过对比可以看出,虽然使用堆在入队和出队都不是最优,但是在同时出入队就会有较好的性价比。

如何实现堆

从上可以看出堆的时间复杂度为 O ( l o g n ) O(logn) O(logn),由此可以推测堆应该是一种树形结构。堆最为经典的一种实现方式就是二叉堆,二叉堆就是指每个父亲节点只有两个子节点。同时二叉堆还是一棵完全二叉树,完全二叉树就是指如果这颗树总共有 n n n层,那么第 n − 1 n-1 n1层子节点必须是满的,按照二叉树的规则应有 2 n − 2 2^{n-2} 2n2个节点,而对于最后一层所有的子节点必须从左向右依次排列。满足上述条件的就可以称为一个堆。如果再定义某节点不能大于父节点,那么就成为一个最大堆,反之则是最小堆。
在这里插入图片描述
如果想要构建一个堆,我们可以像树一样在节点内定义左右节点的指针。但这里我们先采用数组实现。首先我们将堆从上至下、从左至右按顺序排列,那么每个节点的数据可以存储在数组中。这里是将节点从1开始排列,如果从0开始排列也是可以的。
在这里插入图片描述
观察父节点和左右子节点的关系可以发现关系如下:节点 i i i的父节点 p a r e n t ( i ) = i / 2 parent(i)=i/2 parent(i)=i/2,父节点 i i i的左右子节点可以表示为 l e f t c h i l d ( i ) = 2 ∗ i , r i g h t c h i l d ( i ) = 2 ∗ i + 1 left child(i) = 2*i,right child(i) = 2*i+1 leftchild(i)=2i,rightchild(i)=2i+1
在这里插入图片描述
我们可以用一个类来实现堆。

#include <iostream>
#include <algorithm>
#include <string>
#include <ctime>
#include <cmath>
#include <cassert>
using namespace std;
template <typename T>
class maxHeap{
private:
	T* date;
	int count;
public:
	maxHeap(int capacity){
		data = new T[capacity+1];  //数组从索引1开始
		count = 0;
	}
	~maxHeap() {delete[] date;}
	int size() {return count;}
	bool isEmpty() {return count==0;}
}

添加元素

如果需要向堆中添加一个元素,先将新元素插入数组末尾,但是此时并不满足最大堆定义,所以要将新元素调整到合适的位置。
在这里插入图片描述
不满足最大堆其实就是父节点的值小于子节点,所以只要将子节点不断与父节点比较,调整至合适位置即可。
在这里插入图片描述
在这里插入图片描述
在刚刚的类里添加一个public函数insert(),交换的函数设定为private函数

private:
	void shiftUp(int k){    //对第k个元素进行shiftup
		while(k > 1 && data[k] > data[k/2]){
			swap(data[k], data[k/2]);
			k = k/2;
		}
	}  
public:
	void insert(T item){
		data[count+1] = item; //此时并没有判断数组是否越界,可以
		           //添加一个capacity成员变量,每次添加时进行判断
		count++;
		shiftUp(count);
	}

取出元素

取出元素时,只能取出优先级最高的,也就是值最大的。然后将最后一个元素放到最大元素处,然后不断与两个子节点比较,选择较大的子节点交换,直至满足最大堆。

private:
	void shiftDown(int k){
		while(2*k <= count+1){  //保证有左子节点
			int j = 2*k;       //j指向左子节点
			if(j+1 <= count && data[j] < data[j+1]) //右节点存在且大于左节点
				j = j+1;       //使j指向左右子节点中的较大值
			if(data[k] > data[j])  
				break;
			swap(data[k],data[j]);
			k = j; 
		}
	}
public:
	T extractMax(){
		assert(count>0);
		T ret = data[1];
		swap(data[1],data[count]);
		count--;
		shiftDown(1);
		return ret;
	}

使用堆进行排序

既然可以使用extractMax函数提取最大元素,所以可以使用堆进行排序。

template <typename T>
void heapSort(T arr[], int n){
	maxHeap<T> maxheap = maxHeap<T>(n);
	for(int i=0; i<n; i++)
		maxheap.insert(arr[i]);
	for(int i=n-1; i>=0; i--)
		arr[i] = maxheap.extractMax();
}

因为堆也是一种树形结构,所以在排序时每次插入取出元素的复杂度为 O ( l o g n ) O(logn) O(logn),整个排序的复杂度 O ( n l o g n ) O(nlogn) O(nlogn)。但是整体排序速度还是还是略慢于归并排序和快速排序。我们可以对堆的构造方法进行一定的改善,提高运行速度。我们在插入时可以不使用insert的方法进行插入,而采用一种Heapify的方法。将原数组复制到堆中,可以发现没有叶子节点的元素本身就是最大堆的形式,所以只需要从最后一个有叶子节点的元素开始进行最大堆整理即可。最后一个有叶子节点的元素索引 k = c o u n t / 2 k=count/2 k=count/2
在这里插入图片描述
那么对于有叶子节点的元素,它们不满足最大堆的性质,那么可以通过之前的shiftDown操作将元素调整至合理位置。
在这里插入图片描述
这里使用一个新的构造函数将数组构造成堆。

public:
	maxHeap(T arr[], int n){
		data = new T[n+1];
		for(int i=0; i<n; i++)
			data[i+1] = arr[i];
		count = n;
		for(int k = n/2; k>0; k--)
			shiftDown(k);
	}

template <typename T>
void heapSort2(T arr[], int n){
	maxHeap<T> maxheap = maxHeap<T>(arr , n);
	for(int i=n-1; i>=0; i--)
		arr[i] = maxheap.extractMax();
}

改进后的堆排序比之前的要快一点,但相比较于其它算法还是不够快,这也是为什么系统级别的排序没有采用堆的原因,堆只要适用于动态数据维护。

优化的堆排序

上述方法都是对数组复制后构造堆,但是这样会开辟新的空间,提高空间复杂度,有没有方法可以原地排序而无需开辟新空间呢?答案是有的,但是就不能将堆按照1,2,3…排列,而需要从0开始。从0开始的话,父节点和子节点之间的关系可以表示为:
在这里插入图片描述
可以利用上述关系对原数组进行堆得构造。构造成堆后,原数组是一个最大堆,那么第一个元素就是最大值,因为需要将数组从小到大排序,那么交换第一个元素和最后一个元素就将最大值移至正确位置。
在这里插入图片描述
此时前面的数组已经不满足最大堆,但是如果将w进行shiftDown操作后,前面的数字又可以变成最大堆。
在这里插入图片描述
此时继续将橙色部分构造的最大堆中最大元素与最后一个元素交换,那么倒数第二大的元素也放入它应该在的位置。
在这里插入图片描述
在这里插入图片描述

template <typename T>
void __shiftDown(T arr[], int n, int k){
	while(2*k + 1 < n){
		int j = 2*k + 1;
		if(j+1 <= n && arr[j]<arr[j+1])
			j = j+1;
		if(arr[k] > arr[j])   
			break;
		swap(arr[k], arr[j]);
		k = j;
	}
}

template <typename T>
void heapSort3(T arr[], int n){
	for(int i=(n-2)/2; i>=0; i--)    //最后一个有子节点的数据索引为(n-1-1)/2
		__shiftDown(arr, n ,i);
	for(int i=n-1; i>=0; i--){
		swap(arr[i],arr[0]);
		__shiftDown(arr, i, 0);
	}
}

上述算法节省了元素复制的过程,所以无论从空间复杂度还是时间复杂度都是比之前的要快的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值