排序算法之 堆排序

堆排序,平均时间复杂度为O(nlogn),是一种不稳定的排序算法。堆排序利用 这种数据结构来对数组进行排序,堆是一颗完全二叉树在数组中的表示,其任意一个节点的值大于等于它的父节点,但实际上它是一个数组,只是可以当成是一个完全二叉树:

假设有一个数组为 -1,1,0,它是从 0 开始编号的,而当它任意一结点的值都大于等于其父节点是,它就是一个堆了,比如 A = {-1,1,0} 或者 B = {-1,1,0}这两个序列,写成完全二叉树为(数组从左至右其实是二叉树的层序遍历):
*** -1(0) ************************************ -1(0) *****************
1(1) 0(2) 和********************************0(1) 1(2)
其中,括号内的数字是这个元素在完全二叉树中的编号,同时也是它在数组中的下标,上述两个数组列成二叉树可以发现,任意结点的值都大于它的父节点(父节点存在的话),而当一个数组构成了一个堆的话,其根结点(也就是下标为0的点)必定是最小值,此时将他与最后一个元素交换,然后将最后一个元素剔除,剩余元素再重新构成一个堆,之后把新的堆的根结点与现在最后一个元素交换,如此循环直到不能交换,此时的数组就变成了降序排列:(以 -1,1,0为例)

这是一个小顶堆(根节点的值最小),在原数组中是 {-1,1,0},然后将根节点与最后一个元素交换得到 {0,1,-1},剩余两个元素 0 和 1,再构建一个小顶堆为:
*******0(0)
1(1)
在数组中就是{0,1,-1},然后将根节点与堆中的最后一个元素交换得到{1,0,-1},排序完成

对于一个已经完成的小顶堆来说,将根结点与最后一个元素交换肯定会打破这个小顶堆,因此,需要重新修复。

对于一个已经是小顶堆的数组 A = {-5,-3,-2,1,3,4,5},他构成的堆如图

将根结点 -5 与最后一个结点 5 交换之后变为 A = {5,-3,-2,1,3,4,-5},此时 -5 将被‘剔出’数组,即所有操作都不涉及它。要对根结点修复,只需比较 5,-3, -2三个结点的大小,将最小的 -3 作为新的根节点,此时数组变为 A = {-3,5,-2,1,3,4,-5},如下图

此时可以发现,没有移动那段分支还是符合小顶堆的规则(-5 已被剔除),只有交换了的那一边才不再满足小顶堆了,因此还需要将左边分支继续修复,如下

则数组变为 A = {-3,1,-2,5,2,4,-5},满足小顶堆的条件,再将根结点 -3 跟最后一个结点交换,数组变为A = {4,1,-2,5,2,-3,-5},一直重复即可将这一数组变得有序

不过这一切的前提都是给定的数组原本就是一个小顶堆,如果不是这样的话,就需要先构建一个完整的小顶堆

假设给定的数组为 1, -1, 2, 5, -4, 0, -2, -3, 3, 4,它的完全二叉树表示为下图

这是一个完全无序的二叉树,要升序排列,首先要构建大顶堆(上面构建的小顶堆对应为降序排列),同样的,对于一个三个节点的二叉树来说,两两比较三个节点的元素值大小,将最大的交换到父节点处,从最后一个结点开始构建堆,上面说了,构建完一颗子树后交换的那一边的大顶堆也被破坏了,要修复,但没有交换的那一边不需要动,流程如下:

  • 从最后一个结点 4 开始构建,观察二叉树发现,最后一个结点所在的子树只有 2 个节点,因此就比较这两个的值,4 > -4,将 4 放到父节点的位置,则这颗子树左边 交换了,但是它没有子节点,因此不用修复,同样的,对于 5,-3,3 和 2,0,-2 这两棵子树也是构建完不用修复,构建好的数组为
    1, -1, 2, 5, 4, 0, -2, -3, 3, -4,只将 -4 与 4 交换了,其构建的二叉树如下图

    然后开始构建 -1,5,4 这颗子树,比较后将 5 作为父节点,即 -1 与 5 交换,如下图

    交换完之后,交换的那一边需要修复堆,因为交换完有可能会改变大顶堆的结构,这里明显 -1,-3,3 这三个数中 -1 不是最大的,因此修复后应该将 3 作为父节点,同时交换的那一边由于没有子节点,因此不需要再往下修复
    了,修复后得到的数组为 1,5,2,3,4,0,-2,-3,-1,-4,二叉树结构如下图

    这样一直到根结点也构建好堆,得到一个大顶堆数组为 5,4,2,3,1,0,-2,-3,-1,-4,大顶堆如下图

    这样,就由一个完全无序的数组构建好了一个大顶堆,然后按开头讲的方式,将根结点与最后一个结点交换,重新构建大顶堆,再交换,直到所有的结点都被交换完,排序就结束了

这也是一个分治递归的思想,先将一个大问题分解,考虑一个个小的问题,然后慢慢的解决直到将大问题解决完。值得注意的是,由于这是一颗完全二叉树,因此通过父节点找其子节点或是通过子节点找父节点都是很简单的。根据上述思路可以写出堆排序升序算法(构建大顶堆)

/**
 * 堆排序需要将根结点元素与最后一个元素交换,
 * 因此封装一个交换函数供多次调用
 * @param nums 待交换元素的数组
 * @param index1 要交换的第一个元素的下标
 * @param index2 要交换的第二个元素的下标
 */
void swap(int nums[], int index1, int index2) {
    int tmp = nums[index1];
    nums[index1] = nums[index2];
    nums[index2] = tmp;
}

/********************************************   大顶堆   ************************************/

/**
 * 将编号为 number 的子树构建成堆
 * @param nums 需要构建成堆的数组
 * @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
 * @param number 要构建堆的子树的父节点
 */
void heapfiyUp(int nums[], int numsSize, int number) {
    /**
     * number 为父节点编号,由于整个堆的根结点是与数组下标一一对应的,
     * 即 根结点的编号为 0,那么每个父节点对应的子节点 (如果有的话)的编号为
     * 2*number + 1,2*number + 2,一个结点如果存在子节点,那么它子节点
     * 的编号一定小于等于数组的最大编号 numsSize - 1
     */
    int child1 = 2*number + 1;
    int child2 = 2*number + 2;
    int max = number;
    /**
     * 找到三个节点的最大值,如果当前 number 所在的元素不是最大,
     * 说明此时的父节点是经过交换得到的,那么交换的那一边应该修复
     * 大顶堆,如果 number 就是最大的,说明没有交换,直接返回
     */
    if(child1<numsSize && nums[child1]>nums[max]) max = child1;
    if(child2<numsSize && nums[child2]>nums[max]) max = child2;
    if(max != number) {
        /**
         * max 保存的是三个数中最大的元素的下标,先将其与 number 交换,
         * 交换完后 max 所处的就是需要修复的那一边,因此直接把 max 修复完
         */
        swap(nums, number, max);
        heapfiyUp(nums, numsSize, max);
    }
}

/**
 * 从最后一个父结点开始构建大顶堆
 * @param nums 待排序数组
 * @param numsSize 数组长度
 */
void buildUpHeap(int nums[], int numsSize) {
    /**
     * 一个节点 number 的两个子节点为
     * c1 = 2*number + 1,
     * c2 = 2*number + 2,
     * 若已知一个子节点 c1 或 c2,反推父节点为
     * parent = (c1-1) / 2,或
     * parent = (c2-2) / 2,
     * 由于构建的是完全二叉树,因此 c2 的编号一定大于 c1,
     * 并且 c1 的编号一定是奇数,c2 为偶数,偶数减 1 和减 2
     * 然后除以 2 得到的结果是一样的,因此可以得出:
     * parent = (number-1) / 2
     * 要从最后一个父节点开始,即要找到最后一个结点的付父节点,
     * 最后一个结点的编号为 numsSize - 1,因此得到计算最后一个父节点
     * 的计算公式:
     * parent = (numsSize-2) / 2
     */
    int parent = (numsSize-2) / 2;
    /**
     * 从 parent 开始构建大顶堆
     */
    for(int i = parent; i >= 0; --i) heapfiyUp(nums, numsSize, i);
}

/**
 * 交换大顶堆的根结点和最后一个结点,修复堆之后重复
 * @param nums 待排序数组
 * @param numsSize 数组长度
 */
void heapUpSort(int nums[], int numsSize) {
    /**
     * 构建大顶堆
     */
    buildUpHeap(nums, numsSize);
    /**
     * 将根结点 (编号为 0) 与 末尾节点 i 交换
     */
    for(int i = numsSize-1; i >= 0; --i) {
        swap(nums, 0, i);
        heapfiyUp(nums, i, 0);
    }
}

将构建的堆变为小顶堆即可得到降序算法,顺便还实现了一下非递归修复大顶堆

源代码


#include <stdio.h>

/**
 * 堆排序需要将根结点元素与最后一个元素交换,
 * 因此封装一个交换函数供多次调用
 * @param nums 待交换元素的数组
 * @param index1 要交换的第一个元素的下标
 * @param index2 要交换的第二个元素的下标
 */
void swap(int nums[], int index1, int index2) {
    int tmp = nums[index1];
    nums[index1] = nums[index2];
    nums[index2] = tmp;
}

/********************************************   大顶堆   ************************************/

#if 1
/**
 * 将编号为 number 的子树构建成堆
 * @param nums 需要构建成堆的数组
 * @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
 * @param number 要构建堆的子树的父节点编号
 */
void heapfiyUp(int nums[], int numsSize, int number) {
    /**
     * number 为父节点编号,由于整个堆的根结点是与数组下标一一对应的,
     * 即 根结点的编号为 0,那么每个父节点对应的子节点 (如果有的话)的编号为
     * 2*number + 1,2*number + 2,一个结点如果存在子节点,那么它子节点
     * 的编号一定小于等于数组的最大编号 numsSize - 1
     */
    int child1 = 2*number + 1;
    int child2 = 2*number + 2;
    int max = number;
    /**
     * 找到三个节点的最大值,如果当前 number 所在的元素不是最大,
     * 说明此时的父节点是经过交换得到的,那么交换的那一边应该修复
     * 大顶堆,如果 number 就是最大的,说明没有交换,直接返回
     */
    if(child1<numsSize && nums[child1]>nums[max]) max = child1;
    if(child2<numsSize && nums[child2]>nums[max]) max = child2;
    if(max != number) {
        /**
         * min 保存的是三个数中最大的元素的下标,先将其与 number 交换,
         * 交换完后 min 所处的就是需要修复的那一边,因此直接把 min 修复完
         */
        swap(nums, number, max);
        heapfiyUp(nums, numsSize, max);
    }
}
#endif

/**此条件改为 1 且上面递归的 #if 条件改为 0 即可运行非递归方法,
 * 需要将上面递归形式的 #if 条件改为 0,不能两个都是 0 或 1,会报错
 */
#if 0
/**
 * 非递归构建大顶堆
 * @param nums 待排序数组
 * @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
 * @param index 要构建堆的子树的父节点编号
 */
void heapfiyUp(int nums[], int numsSize, int index) {
    int i = index;
    while(1) {
        /**
         * 找到两个子节点
         */
        int c1 = 2*i + 1;
        int c2 = 2*i + 2;
        /**
         * 找到三个节点中最大值
         */
        int max = i;
        if(c1<numsSize && nums[c1]>nums[max]) max = c1;
        if(c2<numsSize && nums[c2]>nums[max]) max = c2;
        /**
         * 最大值不等于 i 则说明发生了交换,
         * 继续修复;最大值等于 i ,没有发生
         * 交换,直接退出
         */
        if(max != i) {
            swap(nums, i, max);
            i = max;
        } else break;
    }
}
#endif


/**
 * 从最后一个父结点开始构建大顶堆
 * @param nums 待排序数组
 * @param numsSize 数组长度
 */
void buildUpHeap(int nums[], int numsSize) {
    /**
     * 一个节点 number 的两个子节点为
     * c1 = 2*number + 1,
     * c2 = 2*number + 2,
     * 若已知一个子节点 c1 或 c2,反推父节点为
     * parent = (c1-1) / 2,或
     * parent = (c2-2) / 2,
     * 由于构建的是完全二叉树,因此 c2 的编号一定大于 c1,
     * 并且 c1 的编号一定是奇数,c2 为偶数,偶数减 1 和减 2
     * 然后除以 2 得到的结果是一样的,因此可以得出:
     * parent = (number-1) / 2
     * 要从最后一个父节点开始,即要找到最后一个结点的付父节点,
     * 最后一个结点的编号为 numsSize - 1,因此得到计算最后一个父节点
     * 的计算公式:
     * parent = (numsSize-2) / 2
     */
    int parent = (numsSize-2) / 2;
    /**
     * 从 parent 开始构建大顶堆
     */
    for(int i = parent; i >= 0; --i) heapfiyUp(nums, numsSize, i);
}

/**
 * 交换大顶堆的根结点和最后一个结点,修复堆之后重复
 * @param nums 待排序数组
 * @param numsSize 数组长度
 */
void heapUpSort(int nums[], int numsSize) {
    /**
     * 构建大顶堆
     */
    buildUpHeap(nums, numsSize);
    /**
     * 将根结点 (编号为 0) 与 末尾节点 i 交换
     */
    for(int i = numsSize-1; i >= 0; --i) {
        swap(nums, 0, i);
        heapfiyUp(nums, i, 0);
    }
}

/********************************************   小顶堆   ************************************/

/**
 * 将编号为 number 的子树构建成堆
 * @param nums 需要构建成堆的数组
 * @param numsSize 当前数组可操作的数量,不包括被 '剔除' 的元素
 * @param number 要构建堆的子树的父节点
 */
void heapfiyDown(int nums[], int numsSize, int number) {
    /**
     * number 为父节点编号,由于整个堆的根结点是与数组下标一一对应的,
     * 即 根结点的编号为 0,那么每个父节点对应的子节点 (如果有的话)的编号为
     * 2*number + 1,2*number + 2,一个结点如果存在子节点,那么它子节点
     * 的编号一定小于等于数组的最大编号 numsSize - 1
     */
    int child1 = 2*number + 1;
    int child2 = 2*number + 2;
    int min = number;
    /**
     * 找到三个节点的最小值,如果当前 number 所在的元素不是最大,
     * 说明此时的父节点是经过交换得到的,那么交换的那一边应该修复
     * 大顶堆,如果 number 就是最大的,说明没有交换,直接返回
     */
    if(child1<numsSize && nums[child1]<nums[min]) min = child1;
    if(child2<numsSize && nums[child2]<nums[min]) min = child2;
    if(min != number) {
        /**
         * max 保存的是三个数中最大的元素的下标,先将其与 number 交换,
         * 交换完后 max 所处的就是需要修复的那一边,因此直接把 max 修复完
         */
        swap(nums, number, min);
        heapfiyDown(nums, numsSize, min);
    }
}

/**
 * 从最后一个父结点开始构建小顶堆
 * @param nums 待排序数组
 * @param numsSize 数组长度
 */
void buildDownHeap(int nums[], int numsSize) {
    /**
     * 一个节点 number 的两个子节点为
     * c1 = 2*number + 1,
     * c2 = 2*number + 2,
     * 若已知一个子节点 c1 或 c2,反推父节点为
     * parent = (c1-1) / 2,或
     * parent = (c2-2) / 2,
     * 由于构建的是完全二叉树,因此 c2 的编号一定大于 c1,
     * 并且 c1 的编号一定是奇数,c2 为偶数,偶数减 1 和减 2
     * 然后除以 2 得到的结果是一样的,因此可以得出:
     * parent = (number-1) / 2
     * 要从最后一个父节点开始,即要找到最后一个结点的付父节点,
     * 最后一个结点的编号为 numsSize - 1,因此得到计算最后一个父节点
     * 的计算公式:
     * parent = (numsSize-2) / 2
     */
    int parent = (numsSize-2) / 2;
    /**
     * 从 parent 开始构建大顶堆
     */
    for(int i = parent; i >= 0; --i) heapfiyDown(nums, numsSize, i);
}

/**
 * 交换小顶堆的根结点和最后一个结点,修复堆之后重复
 * @param nums 待排序数组
 * @param numsSize 数组长度
 */
void heapDownSort(int nums[], int numsSize) {
    /**
     * 构建大顶堆
     */
    buildDownHeap(nums, numsSize);
    /**
     * 将根结点 (编号为 0) 与 末尾节点 i 交换
     */
    for(int i = numsSize-1; i >= 0; --i) {
        swap(nums, 0, i);
        heapfiyDown(nums, i, 0);
    }
}

int main() {
    int arr1[] = {1, -1, 2, 5, -4, 0, -2, -3, 3, 4};
    int len1 = sizeof(arr1) / sizeof(arr1[0]);
    heapUpSort(arr1, len1);
    int arr2[] = {1, -1, 2, 5, -4, 0, -2, -3, 3, 4};
    int len2 = sizeof(arr2) / sizeof(arr2[0]);
    heapDownSort(arr2, len2);
    printf("UP:\t");
    for(int i = 0; i < len1; ++i) printf("%d ", arr1[i]);
    printf("\nDOWN:\t");
    for(int i = 0; i < len2; ++i) printf("%d ", arr2[i]);
    return 0;
}

结果


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值