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]之后)。
节点位置与叶子节点位置,他们之间是可以通过计算得出来的
- 所有节点位置计算:
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)
。
其他思路
快速排序 下次有机会写一篇文章