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

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

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)数据结构的排序算法,它利用堆的性质进行排序。堆是一种特殊的二叉树,它满足以下两个性质:


最大堆性质:对于堆中任意节点 i,其父节点的值大于等于它的值,即 heap[parent(i)] >= heap[i]。

完全二叉树性质:堆是一棵完全二叉树,除了最底层外,其他层都是满的,且最底层从左到右填充。

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


堆排序的具体步骤如下:


构建最大堆:从数组的中间位置开始,从右到左对每个非叶子节点进行调整,使其满足最大堆性质。这一步将整个数组调整为一个最大堆。

堆排序:从数组的末尾开始,依次将堆顶元素与当前末尾元素交换,然后调整剩余元素为最大堆。每次交换后,将堆的大小减1,排除已排序的元素。重复这个过程,直到堆中只剩下一个元素。

排序完成:经过堆排序后,数组中的元素按照从小到大的顺序排列。

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


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


最大堆是使用树结构?


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


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


数组是一种连续存储的数据结构,相比树结构,它具有更好的缓存性能。在访问数组元素时,可以利用 CPU 缓存的特性,提高访问效率。

数组可以通过索引直接访问元素,而不需要像树结构一样需要遍历搜索。这使得对最大堆的插入、删除等操作更加高效。

数组实现的最大堆相对于树结构来说,占用的内存空间更小。树结构需要额外的指针和连接操作,而数组只需要连续的内存空间即可。

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


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


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

1687247774503.jpg

节点位置与叶子节点位置,他们之间是可以通过计算得出来的

  • 所有节点位置计算: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)


其他思路


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

相关文章
|
1月前
|
算法
Leetcode 初级算法 --- 数组篇
Leetcode 初级算法 --- 数组篇
39 0
|
17天前
|
存储 算法 Java
leetcode算法题-有效的括号(简单)
【11月更文挑战第5天】本文介绍了 LeetCode 上“有效的括号”这道题的解法。题目要求判断一个只包含括号字符的字符串是否有效。有效字符串需满足左括号必须用相同类型的右括号闭合,并且左括号必须以正确的顺序闭合。解题思路是使用栈数据结构,遍历字符串时将左括号压入栈中,遇到右括号时检查栈顶元素是否匹配。最后根据栈是否为空来判断字符串中的括号是否有效。示例代码包括 Python 和 Java 版本。
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
69 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
1月前
|
算法
每日一道算法题(Leetcode 20)
每日一道算法题(Leetcode 20)
27 2
|
1月前
|
算法 搜索推荐
数据结构与算法学习十八:堆排序
这篇文章介绍了堆排序是一种通过构建堆数据结构来实现的高效排序算法,具有平均和最坏时间复杂度为O(nlogn)的特点。
73 0
数据结构与算法学习十八:堆排序
|
1月前
|
算法 搜索推荐
算法之堆排序
本文介绍了堆排序算法的原理和实现,通过构建最大堆或最小堆,利用堆的性质进行高效的排序,并提供了具体的编程实现细节和示例。
23 0
算法之堆排序
|
1月前
|
算法 Java Go
深入了解堆排序算法
深入了解堆排序算法
26 1
|
3月前
|
算法 Java
LeetCode经典算法题:矩阵中省份数量经典题目+三角形最大周长java多种解法详解
LeetCode经典算法题:矩阵中省份数量经典题目+三角形最大周长java多种解法详解
52 6
|
3月前
|
人工智能 算法 Java
LeetCode经典算法题:井字游戏+优势洗牌+Dota2参议院java解法
LeetCode经典算法题:井字游戏+优势洗牌+Dota2参议院java解法
50 1
|
3月前
|
存储 算法 Java
LeetCode经典算法题:预测赢家+香槟塔java解法
LeetCode经典算法题:预测赢家+香槟塔java解法
61 1
下一篇
无影云桌面