堆排序 heapsort

1. 介绍

1)什么是堆?

堆是一棵顺序存储完全二叉树

每个结点的关键字都 小于或等于 其所有子结点的关键字,这样的堆称为小根堆

每个结点的关键字都 大于或等于 其所有子结点的关键字,这样的堆称为大根堆

2)什么是堆排序?

堆排序Heapsort是指利用堆这种数据结构来进行排序的选择排序算法。堆积是一个近似完全二叉树的结构,并同时满足子结点的值总是小于(或者大于)它的父节点。

  • 小根堆 在排序算法中的结果是 **降序 **排列;

  • 大根堆 在排序算法中的结果是 **升序 **排序;

3)注意:

  • 堆排序 只适用于 顺序结构

  • 堆排序的平均时间复杂度是 O(nlongn),最好和最坏的时间复杂度也是 O(nlongn)。空间复杂度是 O(n)

  • 堆排序是先建堆,依此输出堆顶元素(把堆顶元素换到数组后面)后调整堆。

2. 算法步骤

以 大根堆 为例,

  • 创建一个大根堆
  • 交换堆顶和最后一个元素,剩下的元素重新调整为 大根堆

3. 完整代码

堆排序的全部代码如下(代码使用 Java 编写):

public void heapSort(int[] nums) {
    // 1. 构建大根堆
    build(nums);
    // 2. 输出堆顶元素后调整堆为大根堆
    for (int i = nums.length - 1; i >= 0; i--) {
        adjust(nums, 0, i);
    }
}

public void build(int[] nums) {
    // 从最后一个非叶子节点开始,反复调整堆
    for (int i = (nums.length - 2) >> 1; i >= 0; i--) {
        adjust(nums, i, nums.length - 1);
    }
}

// 以 root 为根节点调整堆为大根堆
public void adjust(int[] nums, int root, int endIndex) {
    int lastNonLeaf = (endIndex - 1) >> 1;
    int maxValueIndex = root, leftChildIndex = 0, rightChildIndex = 0;
    int index = root;
    // 沿最大的子节点的那棵树找
    while (index <= lastNonLeaf) {
        // 找最大值
        leftChildIndex = (index << 1) + 1;      // index << 1 不加括号会优先计算 +. 因为 << 的优先级小于 +
        rightChildIndex = (index << 1) + 2;
            maxValueIndex = index;
        if (leftChildIndex <= endIndex && nums[maxValueIndex] < nums[leftChildIndex])
            maxValueIndex = leftChildIndex;
        if (rightChildIndex <= endIndex && nums[maxValueIndex] < nums[rightChildIndex])
            maxValueIndex = rightChildIndex;
        if (maxValueIndex == index) break;
        // 交换
        int tmp = nums[index];
        nums[index] = nums[maxValueIndex];
        nums[maxValueIndex] = tmp;
        // 下次遍历的节点
        index = maxValueIndex;
    }
}

4. 代码说明

以 无序序列 a = [1,3,4,5,2] 为例,经过堆排序得到的排好序的数组是 a = [1,2,3,4,5] (也可以降序排列)。从此可以看出,把堆排序看成一个处理过程,那堆排序的输入是一个无序数组(链表不可以),输出是一个有序数组。这里以大根堆为例

堆排序

一个无序序列 a = [1,3,4,5,2] ,看出完全二叉树的物理存储结构。先建成一个初始的大根堆,建堆完成之后,数组是一个大根堆 a = [5,3,4,1,2] 。建堆完成之后,就需要将大根堆依次调整为有序序列 a = [1,2,3,4,5]

4.1 建堆

建堆的过程,就是从最后一个父节点开始,把每一棵树调整为堆的过程。

public void build(int[] nums) {
    // 从最后一个非叶子节点开始,反复调整堆
    for (int i = (nums.length - 2) >> 1; i >= 0; i--) {
        adjust(nums, i, nums.length - 1);
    }
}

首先来看一下,如果将一个无序序列建成大根堆。 a = [1,3,4,5,2] 画成完全二叉树之后,最后一个父节点是 [3] 。从二叉树的最后一个父节点 [3] 开始,依次向前遍历每一个父节点,直到根节点 [1] 为止。

从最后一个父节点开始要做什么事情呢??

以当前父节点为根节点的二叉树,也就是 [3,5,2] ,将其调整为大根堆。

最后一个父节点的下标怎么计算??

节点下标是节点在数组中的位置,所以是从 0array.length - 1

如果一个节点下标是 i ,那么它的左子叶节点下标是 2*i+1 ,右子叶节点下标是 2*i+2

所以,如果数组中最后一个父节点下标为 i ,最后一个元素下标是 endIndex ,那么,最后一个元素一定是最后一个父节点的子节点。所以,就有 2*i + 1 <= endIndex2*i + 2 <= endIndex ,所以可以得到最后一个父节点的下标是 (endIndex - 1) / 2,使用位移运算计算是 (endIndex - 1) >> 1

建堆为什么是从最后一个非叶子节点 (array.length - 2) >> 1 开始向前遍历,而不是从 array.length - 1 开始?

因为要保证调整堆时的参数是有意义的,没有越界产生。调整堆的代码中并没有判断下标是否合法。

建堆的顺序是按层次遍历的顺序遍历的,数组按从 (array.length - 2) >> 10 访问,是按层次遍历的方式向前遍历的。

4.2 调整堆

调整堆的过程,就是把堆顶元素放在合适的位置。

// 以 root 为根节点调整堆为大根堆
public void adjust(int[] nums, int root, int endIndex) {
    int lastNonLeaf = (endIndex - 1) >> 1;
    int maxValueIndex = root, leftChildIndex = 0, rightChildIndex = 0;
    int index = root;
    // 沿最大的子节点的那棵树找
    while (index <= lastNonLeaf) {
        // 找最大值
        leftChildIndex = (index << 1) + 1;      // index << 1 不加括号会优先计算 +. 因为 << 的优先级小于 +
        rightChildIndex = (index << 1) + 2;
        maxValueIndex = nums[index] < nums[leftChildIndex] ? index : leftChildIndex;
        if (rightChildIndex <= endIndex && nums[rightChildIndex] < nums[leftChildIndex])
            maxValueIndex = rightChildIndex;
        if (maxValueIndex == index) break;
        // 交换
        int tmp = nums[index];
        nums[index] = nums[maxValueIndex];
        nums[maxValueIndex] = tmp;
        // 下次遍历的节点
        index = maxValueIndex;
    }
}

将哪个区间内的元素调整为堆??

[root, endIndex] ,包含左右两个下标的元素。因为调整堆adjust()的代码中并没有判断数组下标的合法性,所以传入的参数要是合法的。

怎么调整??

首先,调整的前提是,这个树满足堆的性质,除了堆顶元素。

1)找出左右子节点中的最大值,然后和堆顶元素比较。

  • 如果堆顶元素较大,则不用交换,跳出循环。
  • 如果孩子节点较大,则将堆顶元素和较大的孩子节点交换。

2)继续调整下一个节点

调整的顺序是按层次遍历的顺序遍历的,数组按从 0 到最后一个非叶子节点 lastNonLeaf 访问,是按层次遍历的方式遍历的。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值