快速选择算法,快速找到数组中第K个最大元素
感觉自己写,还是挺难了,看过题解,整理了一遍,给大家参考一些题解思路把
老菜鸡了,自己还琢磨了几个钟才理解。。
package Test.test;
/*
@CreateTime 2021/11/1 10:30
@CreateBy cfk
*/
import java.util.Arrays;
import java.util.Random;
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
String[] s = sc.nextLine().split(" ");
int[] arr = new int[s.length];
for (int i = 0; i < s.length; i++) {
arr[i] = Integer.parseInt(s[i]);
}
System.out.println(findKthLargest2(arr, 2));
}
//方法一
//最容易想到的写法, 调用系统类库函数, 直接对数组进行排序
//然后需要返回的是第k大的元素, 实际上就是第 (全部元素) - k 小的元素
public static int findKthLargest1(int[] nums, int k) {
Arrays.sort(nums);
return nums[nums.length - k];
}
//方法二
//使用快速选择法
//实际上我们可以通过任意排序算法对数组进行排序, 然后找到第 (全部元素) - k 小的元素
//实现原理 同方法一
//但是在快速排序的时候 我们可以减少排序的步骤
//因为快速排序实际上就是不断找每个元素在数组中的位置,即左边的元素全部小于它,右边的元素全部大于它
//在这个找的过程中,我们可以会直接找到(第 (全部元素) - k 小的元素){第k大的元素}在数组中的位置
//这样我们就没必要继续向下判断了
public static int findKthLargest2(int[] nums, int k) {
return quickSelect(nums, 0, nums.length-1, nums.length-k);
}
//寻找对应数
public static int quickSelect(int[] nums, int left, int right, int targetIndex) {
int index = randomPartition(nums, left, right);
if (targetIndex == index) {
return nums[index];
} else {
//如果没找到,则对基准数的左右按条件进行递归
return index > targetIndex ? quickSelect(nums, left, index-1, targetIndex) :
quickSelect(nums, index+1, right, targetIndex);
}
}
//此方法用于找到某个元素在有序数组中对应的位置
public static int partition(int[] nums, int left, int right) {
//左边初始化一个指针位置,方便后面进行换位
int temp = left-1;
//当左边运行到最右边就结束了 而不是整个数组
for (int i = left; i < right; i++) {
//如果当元素比右端基准位小时,都放在左端,每次换位的话让指针加一,如果没有发生换位指针位置不变
if (nums[i] <= nums[right]) {
swap(nums, i, ++temp); //这里要用++temp而不能用temp++,因为如果后加,在第一个元素时,temp为-1,会数组越界
}
}
//最后把基准位和temp+1位更换位置,则基准位的左边全部是小于它的元素,右边是大于它的元素
swap(nums, temp+1, right);
return temp+1;
}
//随机抽取数组内数找到它对应数组中的位置,避免如果数组稍微有序的话,进行太多无用的操作,可以提高效率
public static int randomPartition(int[] nums, int left, int right) {
Random random = new Random();
//right - left + 1 可以获取对应取间内所有元素的个数
//random.nextInt(right - left + 1) 可以返回 0 到 元素总个数(不包括)
//这里为什么要 +left???
//因为这里要随机的是右边界元素,以它为基准继续排序,不是每次数组都是从第一位开始的
// 如果不加left,做到的元素和期待的元素不对应
int randomInt = random.nextInt(right - left + 1) + left;
//交换 随机生成基准点
swap(nums, randomInt, right);
return partition(nums, left, right);
}
//交换两个元素的位置
public static void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
// 作者:LeetCode-Solution
// 链接:https://leetcode-cn.com/problems/kth-largest-element-in-an-array/solution/shu-zu-zhong-de-di-kge-zui-da-yuan-su-by-leetcode-/
// 来源:力扣(LeetCode)
// 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
补充
下面是构造大顶堆的方法实现
class Solution {
public int findKthLargest(int[] nums, int k) {
int length = nums.length;
//创建大顶堆
buildHeap(nums, length);
//循环遍历,把因为题目要求的是第k大的元素,这里循环生成大顶堆,每次把最大的元素和最后一个元素//换位置,并且移除,这样没循环一次,第一个元素就是第K个最大元素
for(int i = nums.length-1; i >= nums.length-k+1; i-- ) {
swap(nums, 0, i);
length--;
//注意这里不需要再生成完全大顶堆了,因为此时的二叉树除了第i个元素其他的都已经是大顶堆了,//所以这里直接不断按第一个元素生成大顶堆就好了
maxHeap(nums, 0, length);
}
return nums[0];
}
public void buildHeap(int[] nums, int length) {
//循环生成大顶堆树,需要找到树的中间,然后不断递减,依次生成大顶堆,即可遍历整颗树
for(int j = length/2; j >= 0; j--) {
maxHeap(nums, j, length);
}
}
public void maxHeap(int[] nums, int i, int length) {
//l代表最左边子树(④)的左子节点,r为右子结点
//用max保存i初始化最大值
int l = 2*i+1, r = 2*i+2, max = i;
//这里要注意判断 l < length 以及 r < length 同理
//因为(④)可能并没有子节点
if(l < length && nums[l] > nums[max]) {
max = l;
}
if(r < length && nums[r] > nums[max]) {
max = r;
}
if(max != i) {
//如果max不为i,说明已经发生了改变,交换此时最大值,并且递归方法
//因为当前面构成大顶堆时,元素的位置发生改变,此时结构又会发生改变
swap(nums, i, max);
maxHeap(nums, max, length);
}
}
public void swap(int[] nums, int a, int b) {
int temp = nums[a];
nums[a] = nums[b];
nums[b] = temp;
}
}
其实构造大顶堆的方法挺多的,这里在给大家分享一个不需要递归的
public void maxHeap(int[] nums, int i, int length){
//保存当前元素,最后要使用到
int x = nums[i];
//从i开始构建大顶堆,如果i下还有子树同样需要迭代构建大顶堆
for(int j = 2*i+1; j < length; j = 2*j+1) {
if(j+1 < length && nums[j] < nums[j+1]) {
j++;
}
if(nums[j] > x){
nums[i] = nums[j];
nums[j] = x;
i = j;
}
}
}