作者:Grey
原文地址: 求无序数组第 K 大的数
无序数组求第K
大的数,其中K
从1
开始算。
例如:[0,3,1,8,5,2]
这个数组,第2
大的数是5
OJ可参考:LeetCode 215. Kth Largest Element in an Array
设置一个小根堆,先把前K
个数放入小根堆,对于这前K
个数来说,堆顶元素一定是第K
大的数,接下来的元素继续入堆,但是每入一个就弹出一个,最后,堆顶元素就是整个数组的第K
大元素。代码如下:
class Solution {
public static int findKthLargest(int[] nums, int k) {
PriorityQueue<Integer> heap = new PriorityQueue<>();
for (int i = 0; i < k; i++) {
heap.offer(nums[i]);
}
for (int i = k; i < nums.length;i++) {
heap.offer(nums[i]);
heap.poll();
}
return heap.peek();
}
}
由于每次堆需要承担logK
的调整代价, 所以这个解法的时间复杂度为O(N*logK)
快速排序中,有一个partition
的过程, 这个过程随机选择数组中的一个数,假设叫pivot,这个过程主要作用是将数组的l...r
区间内的数:
小于pivot
的数放右边
大于pivot
的数放左边
等于pivot
的数放中间
返回两个值,一个是左边界和一个右边界,位于左边界和右边界的值均等于pivot
,小于左边界的位置的值都大于pivot
,大于右边界的位置的值均小于pivot
。简言之:如果要排序,pivot
这个值在一次partition
以后,所在的位置就是最终排序后pivot
应该在的位置。
所以,如果数组中某个数在经历上述partion
之后正好位于K-1
位置,那么这个数就是整个数组第K
大的数。
快排改进算法完整代码如下:
class Solution {
public static int findKthLargest(int[] nums, int k) {
return process(nums, 0, nums.length - 1, k - 1);
}
// arr 在L...R范围内,如果要从大到小排序,请返回index位置的值
private static int process(int[] arr, int L, int R, int index) {
if (L == R) {
return arr[R];
}
int pivot = arr[L + (int) (Math.random() * (R - L + 1))];
int[] range = partition(arr, L, R, pivot);
if (index >= range[0] && index <= range[1]) {
return pivot;
} else if (index < range[0]) {
return process(arr, L, range[0], index);
} else {
return process(arr, range[1], R, index);
}
}
public static int[] partition(int[] arr, int L, int R, int pivot) {
int less = L - 1;
int more = R + 1;
while (L < more) {
if (arr[L] > pivot) {
swap(arr, L++, ++less);
} else if (arr[L] == pivot) {
L++;
} else {
swap(arr, L, --more);
}
}
return new int[]{less + 1, more - 1};
}
public static void swap(int[] nums, int t, int m) {
int tmp = nums[m];
nums[m] = nums[t];
nums[t] = tmp;
}
}
其中process
方法表示:nums
在L...R
范围上,如果要排序(从大到小)的话,请返回index
位置的值。
int pivot = nums[L + (int) (Math.random() * (R - L + 1))];
这一行表示随机取一个值pivot
出来,用这个值做后续的partition
操作,如果index
恰好在pivot
这个值做partition
的左右边界范围内,则pivot
就是排序后第index+1
大的数(从1开始算)。
brfpt
算法和改进快排算法主流程上基本一致,只是在选择pivot
的时候有差别,快排改进是随机取一个数作为pivot
, 而bfprt
算法是根据一定的规则取pivot
,核心代码为:
public class LeetCode_0215_KthLargestElementInAnArray {
......
// nums在L...R范围上,如果要排序(从大到小)的话,请返回index位置的值
public static int bfprt(int[] nums, int L, int R, int index) {
if (L == R) {
return nums[L];
}
//int pivot = nums[L + (int) (Math.random() * (R - L + 1))];
int pivot = medianOfMedians(nums, L, R);
int[] range = partition(nums, L, R, pivot);
if (index >= range[0] && index <= range[1]) {
return pivot;
} else if (index < range[0]) {
return bfprt(nums, L, range[0] - 1, index);
} else {
return bfprt(nums, range[1] + 1, R, index);
}
}
......
}
其中
int pivot = medianOfMedians(nums, L, R);
就是bfprt
算法最关键的步骤,mediaOfMedians
这个函数表示:
将
num
分成每五个元素一组,不足一组的补齐一组,并对每组进行排序(由于固定是5个数一组进行排序,所以排序的时间复杂度O(1)
),取出每组的中位数,组成一个新的数组, 对新的数组求其中位数,这个中位数就是我们需要的值pivot
。
public static int medianOfMedians(int[] arr, int L, int R) {
int size = R - L + 1;
int offSize = size % 5 == 0 ? 0 : 1;
int[] mArr = new int[size / 5 + offSize];
for (int i = 0; i < mArr.length; i++) {
// 每一组的第一个位置
int teamFirst = L + i * 5;
int median = getMedian(arr, teamFirst, Math.min(R, teamFirst + 4));
mArr[i] = median;
}
return bfprt(mArr, 0, mArr.length - 1, (mArr.length - 1) / 2);
}
public static int getMedian(int[] arr, int L, int R) {
Arrays.sort(arr, L, R);
return arr[(R + L) / 2];
}
注:mediaOfMedians
方法中最后一句:
return bfprt(mArr, 0, mArr.length - 1, (mArr.length - 1) / 2);
就是利用bfprt
算法拿整个元素中间位置的值。
为什么是5
个一组
为什么严格收敛到O(N)
请参考:
BFPRT算法原理
BFPTR算法详解+实现+复杂度证明
算法 | 时间 | 空间 |
---|---|---|
堆 | O(N*logK) | O(N) |
快排改进 | 概率上收敛到:O(N) | O(1) |
bfprt | 严格收敛到:O(N) | O(N) |
笔记见:寻找两个正序数组中的中位数
长度为
N
的数组arr
,一定可以组成N^2
个数值对。例如arr = [3,1,2]
,数值对有(3,3) (3,1) (3,2) (1,3) (1,1) (1,2) (2,3) (2,1) (2,2)
,也就是任意两个数都有数值对,而且自己和自己也算数值对。数值对怎么排序?规定,第一维数据从小到大,第一维数据一样的,第二维数组也从小到大。所以上面的数值对排序的结果为:(1,1)(1,2)(1,3)(2,1)(2,2)(2,3)(3,1)(3,2)(3,3)
, 给定一个数组arr,和整数k,返回第k小的数值对。
代码见:Code_0015_KMinPair
算法和数据结构笔记
程序员代码面试指南(第2版)
算法和数据结构体系班-左程云
BFPRT算法原理
BFPTR算法详解+实现+复杂度证明