题目描述
给你一个按递增顺序排序的数组 arr 和一个整数 k 。数组 arr 由 1 和若干 素数 组成,且其中所有整数互不相同。
对于每对满足 0 < i < j < arr.length 的 i 和 j ,可以得到分数 arr[i] / arr[j] 。
那么第 k 个最小的分数是多少呢? 以长度为 2 的整数数组返回你的答案, 这里 answer[0] == arr[i] 且 answer[1] == arr[j] 。
样例描述
示例 1:
输入:arr = [1,2,3,5], k = 3
输出:[2,5]
解释:已构造好的分数,排序后如下所示:
1/5, 1/3, 2/5, 1/2, 3/5, 2/3
很明显第三个最小的分数是 2/5
示例 2:
输入:arr = [1,7], k = 1
输出:[1,7]
思路
方法一:浮点数二分 + 双指针 O(nlogn)
思路:由数组递增可知道, (i, j)对应的分数必然落在 [0, 1]范围内。因此二分判断满足小于等于它的数的个数大于等于k的最小数位置,利用双指针来求小于等于mid的数的个数
方法二: 优先队列(最大堆)
思路:通过扫描所有点对,然后加入优先队列的方式。
- 建立一个大根堆(保留堆中最大元素在堆顶),如果堆中元素少于k,直接加入堆中。如果元素达到k个,比较当前待插入元素与堆顶元素的大小关系,如果当前元素大,那一定不是第k小个元素,直接丢弃,如果当前元素小,则替换掉堆顶元素。
- 最后堆顶元素留下的就是第k小个元素。
- 优先队列的比较直接键函数。
方法三:多路归并 + 优先队列(最小堆)
思路:在方法二的基础上,结合数组本身的单调递增的性质,转化为多路序列求最小值的问题。
- 将所有路序列的第一个元素(也即是最小)加入到优先队列(最小堆)中。
- 循环k次,每次弹出的都是最小的元素,所以第k次就是第k小的数。弹出后,注意判断该序列路后还有没有数(i + 1 < j)有的话就加入。
- 注意:与方法二不同的是,这里堆中存的是下标不是数,因为下标更方便判断某路序列是否还有下一个元素
代码
方法一:浮点数二分 + 双指针 O(nlogn)
class Solution {
double eps = 1e-8;
int A, B;
//双指针求mid前的树的个数
public int get(int[] arr, double mid) {
int n = arr.length;
//记录mid左边数的个数
int res = 0;
//枚举分母
for (int i = 0, j = 0; i < n; i ++ ) {
//枚举分子可能的位置,这里用j + 1试探,满足才走过去
while (j < n && (double)arr[j + 1] / arr[i] <= mid) j ++;
//上面的范围是[0, j] 总共j + 1个数
//注意判断是不越界时
if ((double)arr[j] / arr[i] <= mid) res += j + 1;
//如果已经求出答案,计算出A,B
if (Math.abs((double)arr[j] / arr[i] - mid) < eps)
{
A = arr[j];
B = arr[i];
}
}
return res;
}
public int[] kthSmallestPrimeFraction(int[] arr, int k) {
double l = 0, r = 1;
while (r - l > eps) {
double mid = (l + r) / 2;
//找到满足mid前数的个数满足 大于等于k 的最小数位置
if (get(arr, mid) >= k) {
r = mid;
}
else l = mid;
}
//此时r就是答案,在调用一次求A,B
get(arr, l);
return new int[]{A, B};
}
}
方法二: 优先队列(堆)O(n^2log(n))
class Solution {
public int[] kthSmallestPrimeFraction(int[] arr, int k) {
int n = arr.length;
//元素为有序对,就是数组 设置降序排列
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> Double.compare(1.0 * b[0] / b[1], 1.0 * a[0] / a[1]));
for (int i = 0; i < n; i ++ ) {
for(int j = i + 1; j < n; j ++ ) {
double cur = 1.0 * arr[i] / arr[j];
if (pq.size() < k) {
pq.add(new int[]{arr[i], arr[j]});
} else if (pq.size() == k){
//只有当前值比堆顶小,丢弃堆顶元素再加入当前,否则直接丢弃
if (cur < 1.0 * pq.peek()[0] / pq.peek()[1]) {
pq.poll();
pq.add(new int[]{arr[i], arr[j]});
}
}
}
}
return pq.poll();
}
}
方法三:多路归并 + 优先队列
class Solution {
public int[] kthSmallestPrimeFraction(int[] arr, int k) {
int n = arr.length;
//优先队列,最小堆 (存的是下标)
PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> {
double x = 1.0 * arr[a[0]] / arr[a[1]], y = 1.0 * arr[b[0]] / arr[b[1]];
//Double的比较函数
return Double.compare(x, y);
});
//先将所有路序列的最小元素加入堆
for (int j = 1; j < n; j ++ ) {
//多路序列arr[0]~arr[j] j属于0~n - 1
pq.add(new int[]{0, j});
}
//先弹k - 1次
while (k > 1 ) {
int num[] = pq.poll();
int i = num[0], j = num[1];
k --;
//如果该路序列后面还有数,就加入
if (i + 1 < j) {
pq.add(new int[]{i + 1, j});
}
}
//弹第k次,拿到答案
int poll[] = pq.poll();
return new int[]{arr[poll[0]], arr[poll[1]]};
}
}