从小白开始刷算法 Heap 堆篇 最大堆排序 leetcode.215

序言

虽然算法很难,但不应该就放弃。这是一个学习笔记,希望你们喜欢~

先自己尝试写,大概十几分钟仍然写不出来
看思路,再尝试跟着思路写
仍然写不出来,再看视频
b站up视频推荐:爱学习的饲养员

leetcode其他文章:

数组篇:
从小白开始刷算法 数组篇 leetcode.485
从小白开始刷算法 数组篇 leetcode.283
从小白开始刷算法 数组篇 leetcode.27

链表篇:
从小白开始刷算法 ListNode 链表篇 leetcode.203
从小白开始刷算法 ListNode 链表篇 leetcode.206

队列篇
从小白开始刷算法 ListNode 链表篇 leetcode.933

栈篇
从小白开始刷算法 Stack 栈篇 leetcode.20
从小白开始刷算法 Stack 栈篇 leetcode.496

哈希篇
从小白开始刷算法 Hash 哈希篇 leetcode.217
从小白开始刷算法 Hash 哈希篇 leetcode.705

树篇

从小白开始刷算法 Tree 树篇 先序遍历 leetcode.144
从小白开始刷算法 Tree 树篇 中序遍历 leetcode.94
从小白开始刷算法 Tree 树篇 后序遍历 leetcode.94
树篇

难度:中等

题目:

215.数组中的第K个最大元素

给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。

请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。

示例 1:

输入: [3,2,1,5,6,4], k = 2
输出: 5
示例 2:

输入: [3,2,3,1,2,4,5,5,6], k = 4
输出: 4

题目来源:力扣(LeetCode)

堆排序

能否写出:不能写出。

时间:1个多小时

思路:这题其实是一题排序算法,我这边先使用堆排序

第一次学习最大堆堆排序,详细介绍一下。

堆排序介绍

堆排序是一种基于二叉堆(Binary Heap)数据结构的排序算法,它利用堆的性质进行排序。堆是一种特殊的二叉树,它满足以下两个性质:

  1. 最大堆性质:对于堆中任意节点 i,其父节点的值大于等于它的值,即 heap[parent(i)] >= heap[i]
  2. 完全二叉树性质:堆是一棵完全二叉树,除了最底层外,其他层都是满的,且最底层从左到右填充。

堆排序的基本思想是先构建一个最大堆,然后将堆顶元素(最大值)与堆中的末尾元素交换,然后将剩余元素重新调整为最大堆,重复这个过程直到所有元素都有序。

堆排序的具体步骤如下:

  1. 构建最大堆:从数组的中间位置开始,从右到左对每个非叶子节点进行调整,使其满足最大堆性质。这一步将整个数组调整为一个最大堆。
  2. 堆排序:从数组的末尾开始,依次将堆顶元素与当前末尾元素交换,然后调整剩余元素为最大堆。每次交换后,将堆的大小减1,排除已排序的元素。重复这个过程,直到堆中只剩下一个元素。
  3. 排序完成:经过堆排序后,数组中的元素按照从小到大的顺序排列。

堆排序的时间复杂度为 O(nlogn),其中 n 是数组的长度。它是一种原地排序算法,不需要额外的空间,但由于涉及到交换操作,所以是一种不稳定的排序算法。

堆排序在实际应用中具有一定的局限性,因为它对数据的随机访问较多,对于较大规模的数据或者外部存储的数据,堆排序可能会导致较差的缓存性能。但在某些特定场景下,堆排序的性能相对较好,例如需要找出前 k 个最大或最小的元素时,堆排序可以通过部分排序来实现高效的解决方案。

最大堆是使用树结构?

最大堆是一种基于数组实现的完全二叉树,而不是树结构。尽管最大堆的表示形式在视觉上看起来像是一棵树,但实际上它是通过数组来实现的。

最大堆的性质是父节点的值大于等于其子节点的值。通过使用数组来表示最大堆,可以方便地满足这一性质,并且具有一些优势:

  1. 数组是一种连续存储的数据结构,相比树结构,它具有更好的缓存性能。在访问数组元素时,可以利用 CPU 缓存的特性,提高访问效率。
  2. 数组可以通过索引直接访问元素,而不需要像树结构一样需要遍历搜索。这使得对最大堆的插入、删除等操作更加高效。
  3. 数组实现的最大堆相对于树结构来说,占用的内存空间更小。树结构需要额外的指针和连接操作,而数组只需要连续的内存空间即可。

综上所述,使用数组来实现最大堆既能满足最大堆的性质,又能提供更好的性能和节省内存空间。因此,最大堆通常采用数组来表示,而不是树结构。

我们来一行行看代码,我最喜欢debug式看代码。

首先,记住这张图,关键点在于根节点(num[0]),节点位置(num[7]之前),和叶子节点位置(num[7]之后)。

在这里插入图片描述

  • 节点位置与叶子节点位置,他们之间是可以通过计算得出来的
    • 所有节点位置计算:nums.length/2-1 例:13/2-1 = 5; num[5]之前的节点位置
    • 节点位置与叶子节点位置计算:拿num[1]和num[5]举例
      • 左节点计算:int left =2 * index + 1 例:num[1]左节点 2 * 1+1= 3 num[5]子节点 2 * 5+1= 11
      • 右节点计算:int right =2 * index + 2 例:num[1]右节点 2 * 1+2= 4 num[5]子节点 2 * 5+2= 12
// 仅是我的思路代码,leetcode上大神更厉害
class Solution {
    public int findKthLargest(int[] nums, int k) {
        if (nums == null || nums.length == 0 || k <= 0 || k > nums.length) {
            return -1;  // 无效输入,返回一个无效值
        }

        // 构建最大堆
        buildMaxHeap(nums);

        // 进行堆排序
        //从nums数组的最后一个元素开始,递减地遍历到第(nums.length - k + 1)个元素。其中nums.length是数组的长度,k是要找的第K个最大元素的位置。
        
        for (int i = nums.length - 1; i >= nums.length - k + 1; i--) {
            //下沉堆顶最大元素,并断开树的链接
            swap(nums, 0, i);  // 将堆顶元素与当前末尾元素交换
            maxHeapify(nums, i, 0);  // 调整堆,保持最大堆性质
        }

        // 返回第 k 个最大元素
        return nums[0];
    }

    // 构建最大堆
    private void buildMaxHeap(int[] nums) {
        int n = nums.length;
        for (int i = n / 2 - 1; i >= 0; i--) {
            maxHeapify(nums, n, i);
        }
    }

    // 调整堆,保持最大堆性质
    private void maxHeapify(int[] nums, int heapSize, int index) {
        int largest = index;  // 假设当前节点最大
        int left = 2 * index + 1; // 获取左节点
        int right = 2 * index + 2;// 获取右节点

        // 如果左子节点存在且大于 当前节点 的值
        // 则更新 largest 为左子节点的索引
        if (left < heapSize && nums[left] > nums[largest]) {
            largest = left;
        }
        // 如果右子节点存在且大于 当前节点 和 左子节点 的值
        // 则更新 largest 为右子节点的索引
        if (right < heapSize && nums[right] > nums[largest]) {
            largest = right;
        }
        //如果 largest 不等于当前节点的索引,说明当前节点不是最大的,
        //则将当前节点与 largest 所指的节点进行交换,以保证最大值上浮到当前节点位置。
        if (largest != index) {
            swap(nums, index, largest);  // 将当前节点与最大节点交换
            //递归调用 maxHeapify,以便在交换后继续对 largest 所指的子节点进行堆化操作,以保持最大堆的性质。
            maxHeapify(nums, heapSize, largest);  // 递归调整子堆
        }
    }

    // 交换数组中两个元素的位置
    private void swap(int[] nums, int i, int j) {
        int temp = nums[i];
        nums[i] = nums[j];
        nums[j] = temp;
    }
}

第K个最大元素

时间复杂度:

  • 构建最小堆的时间复杂度为O(K)。
  • 遍历数组并插入元素的时间复杂度为O((N-K)logK),其中N为数组长度。
  • 总时间复杂度为O(K + (N-K)logK),近似于O(NlogK)

空间复杂度:

  • 最小堆的空间复杂度为O(K)

其他思路

快速排序 下次有机会写一篇文章

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小大凳子

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值