一、基础知识
堆结构是一个用数组实现的完全二叉树结构,分为大根堆和小根堆两种,其性质分别为:
(1)大根堆:将数组还原成完全二叉树结构后,每个节点的值都不大于其父节点的值 ;
(2)小根堆:将数组还原成完全二叉树结构后,每个节点的值都不小于其父节点的值。
本文假设堆中实际记录元素的数组从下标为0的位置开始存储堆中的数据。完全二叉树中的父子关系可以转换为数组的下标变换关系,比如对于节点i(数组中对应nums[i]位置的元素),其左孩子节点应为数组中的nums[2*i + 1],其右孩子节点应为数组中的nums[2*(i + 1)],父节点应为数组中的nums[(i - 1) / 2]。
二、堆结构实现
1.接口定义
堆结构需要实现两个接口:
(1)push:将一个新元素插入堆结构中,并维护堆结构(保证修改后仍满足堆的性质)
(2)pop:(大根堆)弹出并返回堆中最大的元素,并维护堆结构。(小根堆)弹出并返回堆中最小的元素,并维护堆结构。
分析这两个函数实现的具体细节
- push
先将插入的元素放在内部数组的末端,实现数据的插入。为了维护大根堆结构,首先将新插入元素与其父节点比较大小,若比父节点大则交换这两个节点,循环与父节点比较的过程,两个停止条件:
(1)新插入的节点移动到根节点则停止;
(2)新插入的节点移动到某个位置,其值不再大于此时的父节点值则停止。
push函数中维护堆结构的部分调用heapInsert函数实现。最后,更新堆中元素个数。
- pop
当前数组中首元素即最大元素,记录该值,选取当前数组末尾元素替换首元素,并收缩堆元素的范围。由于补位的末尾元素可能破坏堆的结构,需要对该元素进行判断,判断的方法如下:
(1)若补位元素的值大于其左子节点和右子节点中的较大值,满足堆结构,判断结束;
(2)若补位元素无左子节点和右子节点,满足堆结构,判断结束;
(3)除上述两种情况,该元素至少存在左子节点,将补位元素与当前的左子节点和右子节点(若存在)中的较大值进行比较,记录较大值所在的下标,交换较大值节点与该补位节点;
循环判断上述过程至满足结束判断条件。
2.代码实现
以下以大根堆为例,介绍大根堆的实现方法:
#include <vector>
#include <iostream>
using namespace std;
template <typename T>
class heap{
public:
heap():m_size(0){}
~heap() = default;
//在大根堆中插入新元素
void push(T elem){
nums.push_back(elem); //元素下标为m_size,其父节点下标为(m_size - 1) / 2
heapInsert(m_size); //维护堆结构
++m_size;
}
//返回并删除大根堆中的最大值
T pop(){
T res = nums[0];
nums[0] = nums[m_size - 1]; --m_size;
heapify(0); //维护堆结构
return res;
}
//打印验证函数
void print(){
for(int i = 0; i < m_size; ++i){
cout << nums[i] << " ";
}
cout << endl;
}
private:
vector<T> nums;
int m_size;
//heapInsert: 插入新元素后维护堆结构的函数, 其功能是判断指定下标的元素是否需要在树结构中向上移动,如需要将其移动到相应位置
void heapInsert(int index){
int parent = (index - 1) / 2;
while(parent != index && nums[index] > nums[parent]){
swap(nums[index], nums[parent]);
index = parent;
parent = (index - 1) / 2;
}
}
//heapify: 删除元素后维护堆结构的函数,其功能是判断指定下标的元素是否需要向在树结构中向下移动,如需要将其移动到相应位置
void heapify(int index){
//其左节点的下标为index * 2 +1 其右节点的下标为2*(index + 1)
int left = index * 2 + 1;
while(left < m_size){
int largerIndex = -1;
if(left + 1 < m_size){
largerIndex = nums[index] >= max(nums[left], nums[left + 1]) ?
index : (nums[left] > nums[left + 1] ? left : left + 1);
}else{ //没有右子节点
largerIndex = nums[index] >= nums[left] ? index: left;
}
if(largerIndex == index) break;
swap(nums[largerIndex], nums[index]);
index = largerIndex;
left = index * 2 + 1;
}
}
};
插入操作的时间复杂度为O(logN),返回并删除最大元素的时间复杂度为O(logN)。
三、堆排序
- 方法一:利用priority_queue结构实现堆排序
可以借助C++中的priority_queue容器实现堆排序,默认为大根堆,具体代码如下:
#include <queue>
using namespace std;
void heapSort(vector<int>& nums){
priority_queue<int, vector<int>> q;
for(int i = 0; i < nums.size(); ++i){
q.push(nums[i]);
}
for(int i = nums.size() - 1; i >= 0; --i){
nums[i] = q.top(); q.pop();
}
}
时间复杂度为O(NlogN),该方法中使用额外容器实现堆结构,空间复杂度为O(N)。
- 方法二:借助堆结构性质实现空间复杂度为O(1)的堆排序
void heapify(vector<int>&nums, int index, int n){
//其左节点的下标为index * 2 +1 其右节点的下标为2*(index + 1)
int left = index * 2 + 1;
while(left < n){
int largerIndex = -1;
if(left + 1 < n){
largerIndex = nums[index] >= max(nums[left], nums[left + 1]) ? index : (nums[left] > nums[left + 1] ? left : left + 1);
}else{ //没有右子节点
largerIndex = nums[index] >= nums[left] ? index: left;
}
if(largerIndex == index) break;
swap(nums[largerIndex], nums[index]);
index = largerIndex;
left = index * 2 + 1;
}
}
void heapSort(vector<int>& nums){
int n = nums.size();
for(int i = n - 1; i >= 0; --i){
heapify(nums, i, n);
}
for(int i = 1; i < n; ++i){
swap(nums[0], nums[n - i]);
heapify(nums, 0, n - i);
}
}