必学排序算法——堆排序

前言

堆排序算法是必须掌握的一种基础算法,在一些比较出名的竞赛acm、蓝桥杯,并且在一些公司面试题中都可能会出现,而且作为简单题我们必须要拿下,所以我们要引起重视,下面让我们来深入了解堆排序算法。

一、什么是堆排序

堆排序(Heap Sort)是一种基于堆数据结构的比较排序算法。堆是一个近似完全二叉树的结构,分为最大堆和最小堆。在最大堆中,父节点的值总是大于或等于其子节点的值;而在最小堆中,父节点的值总是小于或等于其子节点的值。堆排序通常使用最大堆来升序排序数组,使用最小堆则可以降序排序数组。

二、堆排序算法的主要步骤

1.构建最大堆:
从最后一个非叶子节点开始,向上至根节点,逐个节点进行堆化。
堆化过程:对于当前节点,如果其任一子节点的值大于当前节点的值,则交换它们,并继续对受影响的子树进行堆化,直到树满足最大堆的性质。

2.排序过程:
将堆顶元素(即最大值)与堆的最后一个元素交换,此时堆的大小减1(排除已排序的最大元素)。
对新的堆顶元素进行堆化,使其满足最大堆性质。
重复上述步骤,直到堆的大小为1,此时数组已完全排序。

三、算法特性

1.原地排序:堆排序不需要额外的存储空间来存储待排序的元素,它只需要一个与待排序元素数量相等的数组空间。

2.时间复杂度:堆排序的时间复杂度为O(n log n),其中n是待排序元素的数量。这个复杂度在最好、最坏和平均情况下都是一致的,因此它的性能是稳定的。

3.不稳定性:堆排序是一种不稳定的排序算法,因为相同元素的相对位置可能会因为堆的调整而发生变化。

4.适用性:堆排序在处理大规模数据集时具有显著的优势,因为它在构建堆和调整堆的过程中是逐步进行的,不需要像快速排序那样递归地划分数据。

四、算法优缺点

1.优点:
时间复杂度稳定,为O(n log n)。
原地排序,不需要额外的存储空间。
可以很容易地处理大量数据,适用于无法一次性加载整个数据集的情况。
可以作为优先级队列的基础,实现高效的插入、删除和获取操作。

2.缺点:
堆排序是一种不稳定的排序算法。
在数据规模较小时,堆排序的性能可能并不如一些简单的排序算法(如插入排序、选择排序等)好,因为堆排序需要额外的建堆开销,并且其实现相对复杂。
堆排序在构建堆和调整堆的过程中,需要频繁地访问父节点和子节点,这种访问模式可能会导致CPU缓存的命中率降低,从而影响排序的效率。但在大多数情况下,这个缺点并不明显,因为堆排序的整体时间复杂度仍然是O(n log n)。

五、应用场景

堆排序在许多实际应用场景中发挥着重要的作用,包括:
1.构建优先队列:堆排序是构建优先队列的一种常用方法,优先队列是一种特殊的队列,它可以按照元素的优先级对其进行排序,使得优先级最高的元素能够首先被取出。

2.处理大规模数据集:堆排序在处理大规模数据集时具有显著的优势,因为它只需要维护部分数据的堆结构,而不需要将所有数据都加载到内存中进行排序。

3.查找最大(或最小)的K个元素:通过维护一个大小为K的最小堆(或最大堆),可以在O(n log K)的时间复杂度内找到最大(或最小)的K个元素。

4.图算法:堆排序在图算法中也有广泛的应用,例如最短路径算法Dijkstra和Prim的实现中就使用了最小堆来维护待选节点的集合,并选择优先级最高的节点进行处理。

综上所述,堆排序是一种高效、灵活的排序算法,在众多实际应用场景中发挥着重要的作用。

六、堆排序算法动态图解

在这里插入图片描述

七、c++代码模板

#include <iostream>  
#include <vector>  
  
// 堆化函数,用于维护堆的性质  
void heapify(std::vector<int>& arr, int n, int i) {  
    int largest = i; // 初始化largest为根节点  
    int left = 2 * i + 1; // 左子节点的索引  
    int right = 2 * i + 2; // 右子节点的索引  
  
    // 如果左子节点存在且大于根节点  
    if (left < n && arr[largest] < arr[left]) {  
        largest = left;  
    }  
  
    // 如果右子节点存在且大于目前最大的节点  
    if (right < n && arr[largest] < arr[right]) {  
        largest = right;  
    }  
  
    // 如果largest不是根节点  
    if (largest != i) {  
        std::swap(arr[i], arr[largest]); // 交换  
        // 递归堆化受影响的子树  
        heapify(arr, n, largest);  
    }  
}  
  
// 堆排序函数  
void heapSort(std::vector<int>& arr) {  
    int n = arr.size();  
  
    // 构建最大堆  
    for (int i = n / 2 - 1; i >= 0; i--) {  
        heapify(arr, n, i);  
    }  
  
    // 一个个从堆顶取出元素并调整堆  
    for (int i = n - 1; i > 0; i--) {  
        std::swap(arr[0], arr[i]); // 交换堆顶和当前末尾元素  
        heapify(arr, i, 0); // 对新的堆顶进行堆化,注意堆的大小已减1  
    }  
}  
  
// 打印数组函数  
void printArray(const std::vector<int>& arr) {  
    for (int num : arr) {  
        std::cout << num << " ";  
    }  
    std::cout << std::endl;  
}  
  
// 主函数  
int main() {  
    std::vector<int> arr = {12, 11, 13, 5, 6, 7};  
    std::cout << "原始数组: ";  
    printArray(arr);  
  
    heapSort(arr);  
  
    std::cout << "排序后的数组: ";  
    printArray(arr);  
    return 0;  
}

注意事项:

1.数组索引:C++中数组索引从0开始,因此在计算子节点和父节点索引时需要特别注意。

2.堆化过程:heapify 函数是递归的,它会确保以当前节点为根的子树满足最大堆的性质。
堆排序过程:在构建完最大堆后,我们逐步将堆顶元素(最大值)与当前末尾元素交换,并减小堆的大小,然后对新的堆顶进行堆化。

3.稳定性:堆排序是不稳定的排序算法,因为相同元素的相对顺序可能会在排序过程中改变。

4.边界条件:在处理边界条件时(如子节点是否存在),需要小心确保不会访问数组之外的内存。

运行上述代码,你将看到原始数组和排序后的数组分别被打印出来。这个C++模板是一个简单而有效的堆排序实现,适用于大多数需要高效排序的场景。

八、经典例题

1.排序数组

代码题解

class Solution {
    #define eleType int
    #define idType int
    #define maxn 50000  

    idType lson(idType idx){//左子节点下标
        return idx*2+1;
    }
    
    idType rson(idType idx){//右子节点下标
        return idx*2+2;
    }
    
    idType parent(idType idx){//父节点下标
        return (idx-1)/2;
    }
    

    bool better(eleType a,eleType b){//如果是大于号就是大顶堆,否则是小顶堆
        return a>b;
    }
    
    void Heapify(vector<eleType>&heap,int size,eleType curr){//堆化函数,curr代表当前节点下标
        idType lsonId=lson(curr);
        idType rsonId=rson(curr);
        idType optId=curr;
        if(lsonId<size&&better(heap[lsonId],heap[optId])){//对左右子节点判优
            optId=lsonId;
        }
        if(rsonId<size&&better(heap[rsonId],heap[optId])){
            optId=rsonId;
        }
        if(curr!=optId){//把大的那个子节点和当前这个节点的值进行交换
            swap(heap[optId],heap[curr]);
            Heapify(heap,size,optId);
        }
    }
public:
    vector<int> sortArray(vector<int>& nums) {
        for(int i=nums.size()/2;i>=0;i--){
            Heapify(nums,nums.size(),i);//从非叶子节点进行堆化操作,因为叶子节点没有子节点,这个要好好理解
        }
        for(int i=nums.size()-1;i>=0;i--){
            swap(nums[0],nums[i]);//大顶堆的第一个元素是最大的与最后一个元素的值进行交换,也就是堆顶与叶子节点交换值
            Heapify(nums,i,0);//交换值以后从第一个值再进行下沉,使它又变成大顶堆,右能找到最大值,右进行排序
        }
        return nums;
    }
};

九、结语

学习算法是一个很艰难,漫长的过程,我们要克服一切困难,学习算法本就是由易到难,我相信通过我们坚持不懈的努力一定能理解并熟练掌握其中细节,加油,我相信你一定能行。
在这里插入图片描述

关注我让我们一起学习编程,希望大家能点赞关注支持一下,让我有持续更新的动力,谢谢大家
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值