一篇文章10分钟带你了解堆和堆排序

定义

堆必须是完全二叉树,符合完全二叉树的性质才可以构成堆,如果不符合则进行堆调整(上虑和下虑)

完全二叉树

  • 只有叶子不满
  • 从左到右
  • 不可有空缺(间隔)

堆序性,即堆的类型

这也是堆进行调整的依据
大根堆:如果父节点小于子节点,则交换(递归)

  • 大根堆:每个父节点的元素大于其子元素,同时完全二叉树不是二叉搜索树,没有左根右的严格顺序
    在这里插入图片描述
    root(大)
  /      \
left    right
  • 小根堆:每个父节点都小于(或者说不大于?)其子节点
    root(小)
  /      \
left    right

存储

  • 层序遍历(bfs --> 宽度优先的方式,从上至下)
  • 这种映射关系使得堆可以用一维数组进行描述和存储
    在这里插入图片描述

因为是完全二叉树,每个树的下标和数是一一对应的?

数组下标

left = 2i+1
right = 2i+2

堆的基本操作

下虑

向下调整堆,因为其只有根破坏了堆序性

以大根堆为例
1. 当根节点不满足大根堆性质,即比子节点小,需调整堆
2. 将堆的根节点与其最大的子节点进行交换
3. 最大子节点上移成根,其满足大于旧根节点以及两一个子节点
4. 旧根继续判断是否满足堆性质,如不满足则继续进行
5. 当完全二叉树性质满足或旧根调整到叶子节点即结束
6. 时间复杂度O(logN)

上虑

  • 向上调整堆,因只有叶节点破坏了堆序性
  • 主要用来插入元素,新增一个元素,放到队尾的叶子接节点
  • 直接和父节点进行交换即可
  • 时间复杂度O(logN)

建堆

不同的建堆方式对应不同的堆操作
而且其插入的效率不同

自顶向下的建堆

  • 对应上滤,调整的是最后一个元素
1. 将新元素放到堆的最后一位,即放在尾部
2. 根堆堆性质进行堆调整,即上滤操作
3. 时间复杂度O(NlogN)
4. 自顶向下体现在哪? -> 是从上到下开始构建的,先保证插入元素之前的元素都是有序的,即上部分有序

自下而上的建堆

  • 对应的是下滤
1. 找到最后一个父节点
2. 进行堆序行调整
3. 调整后堆完全有序
4. 依次向上调整,每次只处理两个元素
5. 时间复杂度为O(N)

在这里插入图片描述

堆的使用

优先队列 --> 默认小根堆,实现大根堆要使用比较器

  • 弹出元素:弹出根,将最后一个元素放在根,最后一个元素肯定小于根的左右节点,必然发生调整

堆排序

使用优先队列完成排序

  • 将元素放入优先队列,o(logN)
  • 依次弹出元素,即完成排序,O(N)
  • 所以整体的时间复杂度为0(NlogN)
  • 并且有单独的空间申请
小根堆排序
大根堆排序
1. 使用大根堆进行存储元素
2. 弹出最大元素,放入队尾,堆的size-1
3. 队尾元素放在根,此时需要下滤调整堆
4. 调整后,将堆的根节点放在size-3的位置,递归完成
5. 此时数组在不申请新的空间的情况下完成了排序

在这里插入图片描述

大根堆及其堆排序实现

大根堆(Max Heap)是一种特殊的完全二叉树,其中每个父节点的值都大于或等于其子节点的值。堆排序利用大根堆的性质对数组进行排序。具体步骤如下:

  1. 构建大根堆:将数组调整成大根堆。
  2. 堆排序:将堆顶元素(最大值)与堆的最后一个元素交换,然后重新调整剩余部分为大根堆,重复此过程直到整个数组有序。

以下是大根堆及其堆排序的 Java 实现:

public class HeapSort {
    public static void heapSort(int[] nums) {
        int n = nums.length;

        // 构建大根堆
        for (int i = n / 2 - 1; i >= 0; i--) {
            heapify(nums, n, i);
        }

        // 进行堆排序
        for (int i = n - 1; i > 0; i--) {
            // 将堆顶元素(最大值)与堆的最后一个元素交换
            int temp = nums[0];
            nums[0] = nums[i];
            nums[i] = temp;

            // 重新调整剩余部分为大根堆
            heapify(nums, i, 0);
        }
    }

    // 调整大根堆
    private static void heapify(int[] nums, int n, int i) {
        int largest = i; // 初始化最大值为当前节点
        int left = 2 * i + 1; // 左子节点
        int right = 2 * i + 2; // 右子节点

        // 如果左子节点大于当前节点
        if (left < n && nums[left] > nums[largest]) {
            largest = left;
        }

        // 如果右子节点大于当前最大值
        if (right < n && nums[right] > nums[largest]) {
            largest = right;
        }

        // 如果最大值不是当前节点,交换并继续调整
        if (largest != i) {
            int swap = nums[i];
            nums[i] = nums[largest];
            nums[largest] = swap;

            // 递归调整受影响的子树
            heapify(nums, n, largest);
        }
    }

    public static void main(String[] args) {
        int[] nums = {12, 11, 13, 5, 6, 7};
        heapSort(nums);

        // 打印排序后的数组
        for (int num : nums) {
            System.out.print(num + " ");
        }
    }
}

代码解释

  1. 构建大根堆

    for (int i = n / 2 - 1; i >= 0; i--) {
        heapify(nums, n, i);
    }
    

    从最后一个非叶子节点开始,逐个调整节点,使其满足大根堆的性质。最后一个非叶子节点的索引为 n / 2 - 1

  2. 堆排序

    for (int i = n - 1; i > 0; i--) {
        int temp = nums[0];
        nums[0] = nums[i];
        nums[i] = temp;
    
        heapify(nums, i, 0);
    }
    

    将堆顶元素(最大值)与堆的最后一个元素交换,然后重新调整剩余部分为大根堆,重复此过程直到整个数组有序。

  3. 调整大根堆

    private static void heapify(int[] nums, int n, int i) {
        int largest = i;
        int left = 2 * i + 1;
        int right = 2 * i + 2;
    
        if (left < n && nums[left] > nums[largest]) {
            largest = left;
        }
    
        if (right < n && nums[right] > nums[largest]) {
            largest = right;
        }
    
        if (largest != i) {
            int swap = nums[i];
            nums[i] = nums[largest];
            nums[largest] = swap;
    
            heapify(nums, n, largest);
        }
    }
    
    • largest 初始化为当前节点 i
    • leftright 分别表示左子节点和右子节点的索引。
    • 比较当前节点与其子节点,找到最大值的索引。
    • 如果最大值不是当前节点,交换节点值,并递归调整受影响的子树。

通过上述步骤,可以实现大根堆的构建和堆排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值