215.给定整数数组 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
- 直接排序取倒数第 k 个元素就不多说什么了,面试官直接沉默
-
public int findKthLargest(int[] nums, int k) { Arrays.sort(nums); return nums[nums.length-k]; }
- 这里用最小堆也可以,当堆长度大于 k 时就移除堆顶元素,最后的堆顶元素就是解
-
public int findKthLargest(int[] nums, int k) { PriorityQueue<Integer> queue = new PriorityQueue<>(); for(int n:nums){ queue.add(n); if(queue.size() > k)queue.poll(); } return queue.peek(); }
- 快速排序(超时):借鉴快排的思路,降序排序,如果快排时的基准数 mid 为第 k 大的数就直接返回
-
public int findKthLargest(int[] nums, int k) { int len = nums.length; int left=0,right = len-1; while(left<=right){ int mid=left; // 将数组分为两部分 [left,mid] 都大于 nums[mid] // [mid,right] 都小于 nums[mid] for(int j=mid+1;j<=right;j++){ if(nums[j]<nums[left])swap(nums,j,++mid); } swap(nums,left,mid); // 记得降序数组下标为 0 的数为第 1 大的数,所以要 +1 if(mid+1==k)return nums[mid]; // 降序数组为[第1大,第2大,第3大...] // 比如是要第 3 大的,但是现在拿到的 mid 对应第 2 大的,那我们要的值就在右半部分 // 所以更新 left else if(mid+1<k)left=mid+1; // 同理 else right=mid-1; } return -1; } public void swap(int[] nums,int i,int j){ if(i!=j){ nums[i]^=nums[j]; nums[j]^=nums[i]; nums[i]^=nums[j]; } }
- 快排优化:上述情况可能会超时,是因为当数组中包含大量重复元素,每次都划分出长度为 1 和 n-1 的两个部分,那时间复杂度就退化至 O(N2),所以我们可以采用“三路划分”的方式,每次分出大于,等于以及小于基准数的三个数组,如果 k 在等于基准数的部分中就直接返回,为了进一步提升算法的稳健性,我们采用随机选择的方式来选定基准数。
- 主要说一下当 k 在小于基准数部分的划分思路,画个图就很好理解判断 k 是否在小于的部分
- 由于 k 是表示第 k 大的数,所以我们降序展示三部分,这样 k 在坐标轴中从前往后展示时为第 1,2,…,k
- n:
nums.size()
- 由图可知,当 k 处于 small 部分时,
n-small.size()<k
- 然后更新 k,k 在 small 部分中是第几大的,把 n-small.size 看做第 1 大的,此时的 k 其实就是原本的 k 相较于 n-small.size 长了多少,即
k-(n-small.size())=k-n+small.size()
,同理,你也可以写成k-big.size-equal.size
-
private int quickSelect(List<Integer> nums, int k) { // 随机选择基准数 Random rand = new Random(); int pivot = nums.get(rand.nextInt(nums.size())); // 存放大于等于小于基准数的三部分 List<Integer> big = new ArrayList<>(); List<Integer> equal = new ArrayList<>(); List<Integer> small = new ArrayList<>(); for(int n:nums){ if(n>pivot)big.add(n); else if(n<pivot)small.add(n); else equal.add(n); } // 这个很好理解,big.size()>=k 说明第 1,2...,k 大的数被包含其中 if(big.size()>=k)return quickSelect(big,k); // 当 k 在小于基准数部分的划分 else if(nums.size()-small.size()<k) return quickSelect(small,k - nums.size() + small.size()); else return pivot; } public int findKthLargest(int[] nums,int k){ List<Integer> list = new ArrayList<>(); for(int n:nums){ list.add(n); } return quickSelect(list,k); }
- 手写大根堆:直接找了官方题解,数组转成大根堆后删除 k-1 次堆顶元素,此时堆顶元素就是答案
-
public int findKthLargest(int[] nums, int k) { int heapSize = nums.length; buildMaxHeap(nums, heapSize); for (int i = nums.length - 1; i >= nums.length - k + 1; --i) { swap(nums, 0, i); --heapSize; maxHeapify(nums, 0, heapSize); } return nums[0]; } public void buildMaxHeap(int[] a, int heapSize) { for (int i = heapSize / 2; i >= 0; --i) { maxHeapify(a, i, heapSize); } } public void maxHeapify(int[] a, int i, int heapSize) { int l = i * 2 + 1, r = i * 2 + 2, largest = i; if (l < heapSize && a[l] > a[largest]) { largest = l; } if (r < heapSize && a[r] > a[largest]) { largest = r; } if (largest != i) { swap(a, i, largest); maxHeapify(a, largest, heapSize); } } public void swap(int[] a, int i, int j) { int temp = a[i]; a[i] = a[j]; a[j] = temp; }