堆排序算法实际上还是对数组进行swap()操作达到排序的目的,不过是用堆结构辅助理解,减少重复计算。
堆是一个完全二叉树,如果堆中任一节点的值总是小于等于其父节点的值,这种堆叫最大堆,反之,如果堆中任一节点的值总是大于等于其父节点的值,这种堆叫做最小堆。
(完全二叉树的深度为 k,除第 k 层外,其它各层 (1~k-1) 的结点数都达到最大个数,并且第k 层所有的结点都连续集中在最左边。)
因为大顶堆始终保持最大的元素在最上面,所以通过堆对数组排序的本质就是将堆最顶端的元素弹出(与堆数组的末尾元素交换位置),再将剩下的元素进行重组,再重复上述操作 ,最后就可以得到一个有序数组。
小根堆 大根堆
堆这个数据结构的出现是为数组的排序服务的,上面对堆结构上的要求也是为了更好的排序,提高效率,堆中的数据层序遍历后与数组中的数据一一对应。
堆的构建
堆与数组中的元素联系在了一起,每个数组对应的堆,都是包含元素的。意思就是对数组进行堆排序时,不需要额外再消耗空间建立满足堆特点的完全二叉树,所需要的只是对数组中的数据swap()操作,堆是为了辅助思考而建立的,通过其独特高效的结构,减少数组内元素比较的时间冗余,减少重复比较的次数。
不过要想让数组所映射的堆在层序遍历后有序,需要对堆中的元素进行上浮和下沉操作;而后续如果数组中如果有某个元素被删除,或者数组又加入某个元素,破坏了数组的有序性,此时需要进行特定元素的上浮与下沉,来恢复数组元素的有序性。
元素的对应关系
如上,(元素的对应关系可能有些错误,理解即可😂)如果数组的元素的索引值是从1下标开始的,那么在堆结构中,下标为 i 的元素的父节点的下标为 i/2;下表为 i 的元素的左子节点为 2*i ,右子节点为 2*i+1 。当然,如果数组的元素的索引值从0开始,则对应的关系需要进行微调。
元素的上浮和下沉
此处以大根堆为例,小根堆与其基本相同。(此处数组的索引值是从0开始的,会有些区别)
void shiftup(vector<int> & nums, int index){//上浮操作
while(index > 0){
int father = (index-1) / 2;
if(nums[father] < nums[index]){
swap(nums[father], nums[index]);
index = father;
}
else break;
}
}
void shitfdown(vector<int> & nums, int index, int edge){//下沉操作
while(index*2 + 1 <= edge){
int left = index*2 + 1;
if(left + 1 <= edge){ //如果右子节点存在
int larger = nums[left + 1] > nums[left] ? left + 1 : left;
if(nums[larger] > nums[index]) {
swap(nums[larger], nums[index]);
index = larger;
}
else break;//退出上面的while循环语句,关键一句,如果缺少,会陷入死循环
}
else if(nums[left] > nums[index]){//如果右子节点不存在
swap(nums[left], nums[index]);
index = left;
}
else break;
}
}
堆的排序
堆的排序有两个遍历方法,需要注意区分,这两种方法最后都是为了让之前排过序的元素参与到后续元素的排序中。
下沉实现排序,我们需要对数组从右向左(或者说对堆从下到上)进行遍历 ,但是因为最下方的叶子节点没有子节点,所以不需要排序, 而需要排序的节点(有子节点的节点)占N/2 (假设N为节点总数的话)。
int len = nums.size();
for(int i = len/2; i > -1; i--){
shitfdown(nums, i, len - 1);
}//下沉时从下往上遍历
对上浮排序,我们需要对数组从左向右(或者说对堆从上到下)进行遍历, 依次保证前两个元素堆有序, 前三个元素堆有序...... 直到最后一个元素
for(int i = 0; i < nums.size(); i++){
shiftup(nums, i);
}//上浮时从上往下遍历
在上述操作结束后,会得到堆有序的数组,但是它还不是数组有序的,仅仅是在堆结构中,根结点的值要大于子节点的值。
元素的插入和删除
插入元素
将新元素加到数组末尾,也就是堆的右下角,增加堆的大小。并调用上浮函数让这个新元素上浮到合适的位置
void insert (vector<int> &a, int v) {
a[N+1]= v; // 元素被放入堆的最末端成为新节点
N++; // 增加堆的大小
shiftup(a, N); // 对末端节点进行上浮操作使其有序
}
删除元素
从数组顶端删除最大的元素并将数组的最后一个元素放到顶端, 减小堆的大小。并执行下沉函数让这个元素下沉到合适的位置
int delMax (vector<int> & a) {
if(N == 0) return 0; // 当堆为空时, 返回
int max = a[0]; // 取得堆中根节点(最大值)
swap(0, N); // 交换根节点和末端节点(最后一个元素)的值
N--; // 减少堆的大小 (“删除”完毕)
shifdown(a, 0); // 下沉操作,让刚放上根节点的新元素下沉到合适的位置
return max;
}
数组排序
通过堆元素的删除与下沉操作来实现数组的有序
依次把最大的数组元素移到数组的最右端, 也就是把堆顶部元素移动到堆的最右下角。依次填充a[N-1], a[N-2]...直到a[0]
while(N > 0){
swap(a[0], a[N]); // 将数组中最大的元素放到数组后端
N--; // 将最大的节点元素移出堆
shifdown(a, 0, N); // 下沉操作,再次实现堆有序
}
整体代码
#include<bits/stdc++.h>
using namespace std;
void printVec(vector<int> & vec){
for(int num : vec) cout << num << " ";
cout << endl;
}
class heapSort{
public:
heapSort(vector<int> & nums) : vec(nums){}
void creatHeap(){
int len = vec.size();
for(int i = len / 2; i >= 0; i--){
shiftDown(i, len - 1);
}
}
void vecSort(){
creatHeap();
int n = vec.size() - 1;
while(n > 0){
swap(vec[0],vec[n]);
n--;
shiftDown(0, n);
}
}
void shiftUp(int index){
while(index > 0){
int father = (index - 1) / 2;
if(vec[father] < vec[index]){
swap(vec[father], vec[index]);
index = father;
}
else break;
}
}
void shiftDown(int index, int edge){
while(2 * index + 1 <= edge){
int left = 2 * index + 1;
if(left + 1 <= edge){
int larger = vec[left + 1] > vec[left] ? left + 1 : left;
if(vec[larger] > vec[index]){
swap(vec[larger], vec[index]);
index = larger;
}
else break;
}
else if(vec[left] > vec[index]){
swap(vec[left], vec[index]);
index = left;
}
else break;
}
}
void printVec(){
for(int num : vec) cout << num << " ";
cout << endl;
}
private:
vector<int> vec;
};
void sort(vector<int> vec){
}
int main(){
vector<int> vec = {2,4,66,7,3,2,4,67,3,12,46,9,22};
printVec(vec);
heapSort hs(vec);
hs.vecSort();
hs.printVec();
return 0;
}