快排和堆排序在确定k个元素有着得天独厚的优势,原因是无论快排还是堆排序在每一轮排序中均可以确定一个元素
- 快排:每一轮排序均可以确定一个元素位置
- 堆排序:每一轮排序都可以确定一个最小值或最大值
他们的时间复杂度都是O(nlogk),但是快排的空间复杂度是O(logn),而堆排序的空间复杂度是O(k)。
所以便想着带着大家通过数组中的第K个最大元素来详细的学习一下如何将这两种思路去落地和应用
题目:《数组中的第K个最大元素》
题目描述:
给定整数数组 nums 和整数 k,请返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
你必须设计并实现时间复杂度为 O(n) 的算法解决此问题。
题目要求我们用O(n)的时间复杂度实现,但是我们可以先不用考虑时间复杂度,只单纯的透过该题目去快速上手这两种方法
首先,你要对这两个排序有一定理解,为了节约篇幅突出重点,大家先通过下面这两篇文章了解一下:
作为程序员的自我修养,请大家务必都以上两种排序的过程和代码的实现烂熟于心
1.快排思路
这道题目的核心就是找排序后的倒数第 [latex] 个位置,一般看到这种需要第N个的数据往快排思路上去思考一般方向不会出错。
现在回到题目,因为把整个数组排序是没有必要的,因为我们的目的并不是排序,而是找到length-k索引位,所以在快排的每一轮的排序中,利用它可以确定一个元素的位置就可以很快找到第k个最大元素。
核心代码如下:
private int partition(int[] nums, int begin, int end) {
int pivot = nums[begin];
while (begin < end) {
while (begin < end
&& nums[end] >= pivot) {
end--;
}
nums[begin] = nums[end];
while (begin < end
&& nums[begin] <= pivot) {
begin++;
}
nums[end] = nums[begin];
}
nums[begin] = pivot;
return begin;
}
这面这段代码是快排的核心代码,它的作用是在每一轮排序中确定一个元素的位置,然后返回这个元素的位置。
一旦partition方法返回的值和length-k相等,那么就可以直接返回这个元素了,因为这个元素就是第k个最大元素。
核心代码如下:
public int findKthLargest(int[] nums, int k) {
quickSelect(nums, 0, nums.length - 1, nums.length - k);
return nums[nums.length - k];
}
private void quickSelect(int[] nums, int left, int right, int index) {
if (left < right) {
int partitionIndex = partition(nums, left, right);
if (partitionIndex == index) {
// 找到了目标位置,无需进一步操作
return;
} else if (partitionIndex < index) {
// 目标位于右侧子数组
quickSelect(nums, partitionIndex + 1, right, index);
} else {
// 目标位于左侧子数组
quickSelect(nums, left, partitionIndex - 1, index);
}
}
}
private int partition(int[] nums, int begin, int end) {
int pivot = nums[begin];
while (begin < end) {
while (begin < end
&& nums[end] >= pivot) {
end--;
}
nums[begin] = nums[end];
while (begin < end
&& nums[begin] <= pivot) {
begin++;
}
nums[end] = nums[begin];
}
nums[begin] = pivot;
return begin;
}
2.堆排序思路
大顶堆的话是每一次调整都可以确定一个最大值,所以我们可以通过构建一个大顶堆,然后不断的调整堆,直到调整到第k次,那么这个堆顶元素就是第k个最大元素。
当然Java中我们可以使用优先队列实现堆的逻辑,但是并不推荐,原因是可以通过考察如何建堆以及如何调整堆来判断候选人的基本素养。
- 调整堆
private void buildMaxHeap(int[] nums, int length) {
int i = nums.length / 2 - 1;
while (i >= 0) {
heapify(nums, i--, length);
}
}
private void heapify(int[] nums, int i, int len) {
//找到左右子树
int left = i * 2 + 1;
int right = i * 2 + 2;
int largeIndex = i;
if (left < len
&& nums[left] > nums[largeIndex]) {
largeIndex = left;
}
if (right < len
&& nums[right] > nums[largeIndex]) {
largeIndex = right;
}
if (largeIndex != i) {
//交换
int t = nums[i];
nums[i] = nums[largeIndex];
nums[largeIndex] = t;
//交换之后子节点值改变需要再次调整
heapify(nums, largeIndex, len);
}
}
上面这个方法是调整堆的方法,它的作用是将一个堆调整为大顶堆。每一次调整均可以确定一个最大值
所以我们只需在调整k次后就会找到第k个最大元素。
核心代码如下:
private int heapSelect(int[] nums, int k) {
//构建大根堆
int len = nums.length;
buildMaxHeap(nums, len);
k--;
if (k == 0) {
return nums[0];
}
for (int i = len - 1; i >= 0; i--) {
//交换根节点和尾结点元素,并让数组大小减一
int t = nums[0];
nums[0] = nums[len - 1];
nums[len - 1] = t;
heapify(nums, 0, --len);
k--;
if (k == 0) {
return nums[0];
}
}
return nums[0];
}
完整代码如下:
public int findKthLargest1(int[] nums, int k) {
return heapSelect(nums, k);
}
private int heapSelect(int[] nums, int k) {
//构建大根堆
int len = nums.length;
buildMaxHeap(nums, len);
k--;
if (k == 0) {
return nums[0];
}
for (int i = len - 1; i >= 0; i--) {
//交换根节点和尾结点元素,并让数组大小减一
int t = nums[0];
nums[0] = nums[len - 1];
nums[len - 1] = t;
heapify(nums, 0, --len);
k--;
if (k == 0) {
return nums[0];
}
}
return nums[0];
}
private void buildMaxHeap(int[] nums, int length) {
int i = nums.length / 2 - 1;
while (i >= 0) {
heapify(nums, i--, length);
}
}
private void heapify(int[] nums, int i, int len) {
//找到左右子树
int left = i * 2 + 1;
int right = i * 2 + 2;
int largeIndex = i;
if (left < len
&& nums[left] > nums[largeIndex]) {
largeIndex = left;
}
if (right < len
&& nums[right] > nums[largeIndex]) {
largeIndex = right;
}
if (largeIndex != i) {
//交换
int t = nums[i];
nums[i] = nums[largeIndex];
nums[largeIndex] = t;
//交换之后子节点值改变需要再次调整
heapify(nums, largeIndex, len);
}
}
3.实际应用场景
大数据处理:在海量数据中查找第K大的元素
排行榜系统:获取用户排名
数据统计:获取数据分布的中位数或其他分位数