数据结构与算法八: 8)排序算法--堆排序

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战node

关注我,如下内容持续更新面试

数据结构与算法(一):时间复杂度和空间复杂度算法

数据结构与算法(二):桟api

数据结构与算法(三):队列数组

数据结构与算法(四):单链表markdown

数据结构与算法(五):双向链表数据结构

数据结构与算法(六):哈希表函数

数据结构与算法(七):树oop

数据结构与算法(八):排序算法post

数据结构与算法(九):经典算法面试题

前言

在介绍堆排序以前,先简单回顾下彻底二叉树和堆

  • 彻底二叉树:若是二叉树中除去最后一层节点为满二叉树,且最后一层的结点依次从左到右分布,则此二叉树被称为彻底二叉树。

  • 是一棵顺序存储彻底二叉树,堆的存储通常用数组来实现

    • 大顶堆: 每一个结点的值都大于或等于其左右孩子结点的值

    • 小顶堆: 每一个结点的值都小于或等于其左右孩子结点的值

关于彻底二叉树和堆,具体的能够看数据结构与算法(七):树, 明天会更新

堆排序思路

堆排序是一种选择排序,能够用一维数组顺序存储堆. 那么如何利用堆进行排序呢?拿升序排序来说,先把待排序序列按照从上至下从左至右的顺序构建成一个大顶堆,那么大顶堆的根节点就是最大的元素,把根节点取出来,再把剩下的元素再构形成一个新堆(如何保证取走最大值更高效的构建新堆呢,就是把根节点arr[0]和最后一个叶子节点arr[n-1]进行调换,而后取走最后一个叶子节点arr[n-1],此时堆不知足大顶堆的条件,只能说它是一个彻底二叉树,因此要递归调整彻底二叉树使它成为一个大顶堆),一样知足根节点是最大的元素,此时再取出根节点,继续构建新堆,循环执行.

例如把数组{100,33,3,7,11,6,8,5}进行升序排序的过程以下:

第一步:构建彻底二叉树

首先根据数组创建一个彻底二叉树,以下图

(代码中这一步无需操做,由于彻底二叉树能够用一维数组来存储,初始数组默认就是一个彻底二叉树)

彻底二叉树.png

第二步:把彻底二叉树构建成大顶堆

从最后一个父节点开始,也就是从下标为3的7开始调整,由于7做为父节点,大于它的孩子节点5,因此不用调整;再看下标为2的3,由于3小于孩子节点,把最大的孩子节点8与3进行交换;而后再看下标为 1 的 33,它大于两个孩子节点,不用交换;再看下标为0的100,它大于孩子节点,不用交换;此时大顶堆已构建完成. 如图所示 截屏2021-08-11 19.35.14.png

这里须要注意:若是要构造完整的堆,要从下面往上构造,并且每次交换后,都要递归维护以"交换的那个孩子节点"为父节点的下面的堆,由于若是交换的节点很是小,那么可能小于它下面的堆,因此要递归向下维护堆,递归维护堆的函数在代码中是heapify方法(heapify方法后面会贴代码),heapify方法里边若是递归过程当中父节点比某个孩子节点小,那么就交换位置,继续从被交换的那个孩子节点的位置往下递归维护堆;若是递归过程当中父节点比两个孩子节点都大,不用交换,固然这个父节点确定会大于孩子节点下边的堆,因此不须要继续递归下面的堆

第三步:取出最大值,并调整成为新堆 这时大顶堆的堆顶元素是最大值,把它与最后一个元素进行交换,而后从堆中拿走最后一个元素,也就是最大值,以下图

截屏2021-08-11 19.41.55.png

这时已经不知足堆的特色,只能说它是一个彻底二叉树,因此此时要进行调整,注意调整堆和建立堆顺序不同,调整堆应该从根节点开始调整,父节点和孩子节点交换后,下次递归调整的子堆就是以交换后的那个孩子为父节点的子堆,例如这一步,从下标为0的5开始调整,5和33交换后,下次递归就要调整下标为1的5为父节点的子堆,由于 5 比它的右孩子 11 大,因此交换位置(注意33 和 5 交换后,下标为2的8做为父节点的子堆不须要递归调整,由于交换后,以8为父节点的子堆没有动过,依然知足堆的特色)

此时已经调整好成为新堆,再把对顶元素和最后一个元素交换位置,即 33 和 3 交换,而后把取走33,如图

截屏2021-08-11 19.57.34.png

以此类推,中间的调整过程省略,最后的一次调整完如图所示

截屏2021-08-11 20.00.55.png

每一次交换完堆顶元素和最后一个元素后,也就是数组的arr[0]和 arr[n-i],例如第一次交换就是 arr[0]和 arr[n-1],第二次交换就是 arr[0]和 arr[n-2],以此类推,最后一次调整完成后,数组的元素就是升序排列.

注意

取出最大值后,调整堆时,若是要构造完整的堆,也要调用heapify方法递归维护下面的堆,可是这里说明一点,对于这里的堆排序来说能够不用递归维护,由于只须要保证堆顶元素是最大的就能够,没必要维护成为完整的堆.

堆排序完整代码

//8 堆排序

-(void)heapSort:(NSMutableArray*)arr{

    //1. 构建大顶堆
    [self buildHeap:arr];

    int n = (int)arr.count;

    //2. 依次取出最大值,而后继续调整堆
    for (int i = n - 1; i>=0; i--) {
        [self swapArray:arr index1:0 index2:i];//取出最大值放到数组的最后,也就是把堆顶的元素与最后一个交换
        [self heapify:arr parent:0 n:i];//下次堆的个数逐渐减1,由于最大值取出后,就要从堆中拿走
    }

}

/**
1,针对结点 i,将其两个子节点找出来,此三个结点构成一个最小单位的彻底二叉树(越界的忽略)

2,找到这个最小单位的彻底二叉树 的最大值,并将其交换至父节点的位置

3,递归调用,维护交换后 子节点与其子结点被破坏的堆关系,递归出口为叶节点
*/
-(void)heapify:(NSMutableArray*)arr parent:(int)i n:(int)n{

    int c1 = i*2+1;//左孩子节点下标

    int c2 = i*2+2;//右孩子节点下标

    int max = i;

    //找出三个节点中的最大值的下标,记录为max
    if (c1 < n && [arr[c1] intValue] > [arr[max] intValue]) {
        max = c1;
    }

    if (c2 < n && [arr[c2] intValue] > [arr[max] intValue]) {
        max = c2;
    }


    //max != i,也就是最大值不是原来的父节点,须要交换位置,可是交换后,被交换位置的新元素可能比下面的堆还小,因此须要递归向下维护堆
    if (max != i) {
        [self swapArray:arr index1:max index2:i];
        [self heapify:arr parent:max n:n];
    }

}

-(void)buildHeap:(NSMutableArray*)arr{

    int n = (int)arr.count;
    
    //彻底二叉树的下标与数组的下标一一对应,因此彻底二叉树的最后一个节点的下标为last_node
    int last_node = n - 1;
    
    //最下面一个堆的父节点 根据求父节点的公式来的:p = (i-1)/2
    int parent = (last_node-1)/2;
    for (int i = parent; i>=0; i--) {
        [self heapify:arr parent:i n:n];
    }
}
复制代码

堆排序的性能

堆排序的最好、最好和平均时间复杂度均为O(nlogn),空间复杂度是 O(1), 它是不稳定的排序

其余排序算法

排序算法:1)直接插入排序

排序算法:2)希尔排序

排序算法:3)冒泡排序

排序算法:4)快速排序

排序算法:5)选择排序

排序算法:6)归并排序

排序算法:7)基数排序

排序算法:8)堆排序

相关文章
相关标签/搜索