题目描述:
给定一个长度为N的整数数组arr,一定可以组成N^2个数值对;
例如[3,1,2],你需要将它们之间两两组合出来的数中第k小的组合找出来;
排序规则为:先按照(x,y)x的大小进行排序,如果x相同,再按照y进行排序
上面示例中数组的组合为:
(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)
如果要找出第3小的组合,那就是 (1,3)
这能难得住我?
这不就是把所有的组合列出来,然后排个序,轻轻松松这不就拿到结果了?
这是字节跳动的算法题???
思路分析:
- 如何拿到所有的排列组合?直接双层循环,简单粗暴!
- 如何排序?使用java提供的自定义排序策略。
这两个问题都解决了,那么这个题也就做出来了!
代码:
public static class Pair{
public int x;
public int y;
Pair(int x,int y){
this.x = x;
this.y = y;
}
}
//自定义比较策略
public static class PairComparator implements Comparator<Pair> {
@Override
public int compare(Pair o,Pair p) {
return o.x != p.x ? o.x - p.x : o.y - p.y;
}
}
public static int[] kthMinPair(int[] arr,int k){
int N = arr.length;
if (k > N * N) {
return null;
}
Pair[] res = new Pair[N * N];
int index = 0;
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
res[index++] = new Pair(arr[i],arr[j]);
}
}
Arrays.sort(res,new PairComparator());
return new int[]{res[k - 1].x,res[k - 1].y};
}
如果将这样的代码交给面试官,你猜面试官会不会夸夸你?
简单分析一下上述解法的时间复杂度:O(N^2 * log(N^2))
高的飞起!
再来分析一下题目:
“先按照(x,y)x的大小进行排序”
x都是递增的趋势,我们能不能先确定x的值是哪个?根据数组的长度我们是可以确定一共有多少种组合的,以题目中的数组[3,1,2]为例,那么最终的组合数就有3^2=9种,显而易见,以每个数字为x的组合分别有三对
不管y是多少,以x排的序是一定不会再改变了,如果k=5,我们立刻就能确定符合条件的组合中的x为2
x确定了,那么y该如何确定呢?
当确定了x=2,排在2前面的数字就可以不用考虑了,那么总的组合数9减去x等于1的组合数3,就只剩下6种组合了,这时,我们只需要找出第2小的组合(前面三种都被排除掉了)。
在拍序之后,可以观察到每种x的组合中,x和y的值都是顺序递增的
(1,1),(1,2),(1,3)
(2,1),(2,2),(2,3)
(3,1),(3,2),(3,3)
有顺序都是三个三个一排列,也就是按照数组的大小N进行分段排列的。
现在只需要在x=2的组合中找出y第2小的组合。
思路分析:
- 确定x的值x=arr[(k - 1) / N];
- 找出数组中小于x的值的个数lessXCount(缩小查询范围)和等于x的个数XCount(第k小的数字就在等于x组合中)
- 确定y的值y=arr[(k - (lessXCount * N) - 1) / XCount](数组下标从0开始)
lessXCount * N :计算x前面有多少个组合不用再考虑,因为都是小于x的,排序一定是在x组合的前面 k - (lessXCount * N) - 1 :目标组合存在于这些组合中 XCount :等于x的数有多少,(k - (lessXCount * N) - 1) / XCount表示y在数组中的下标
代码:
public static int[] kthMinPair(int[] arr,int k){
int N = arr.length;
if (k > N * N) return null;
//O(N*logN)
Arrays.sort(arr);
//先确定左边的数字x
int x = arr[(k - 1) / N];
//找出小于x的数字和等于x的数字有多少
int lessXCount = 0,XCount = 0;
for (int i = 0; i < N && arr[i] <= x; i++) {
if (arr[i] < x) {
lessXCount++;
} else {
XCount++;
}
}
int y = arr[(k - (lessXCount * N) - 1) / XCount];
return new int[]{x,y};
}
Arrays.sort()方法的时间复杂度为O(N*logN),for循环处的时间复杂度为O(N),那么整个程序的时间复杂度为O(N * logN)
比起上一种解法的复杂度提高了不止一个等级吧。
你是不是以为到这就结束了?
年轻人,还早着呢
解出该题最关键的点在于需要数组是有序的,程序的大头都花费在了排序上,要得到第k小的值,我们得对整个数组进行排序,这样势必会造成资源浪费(因为没有必要对第k个数后面的数进行排序啊)
有没有一种方法能够在一堆乱掉的数字中,拿出第k小的数字呢?
这就是TOP-K问题,有没有可以快速解决该问题的算法呢?
是的,那就是大名鼎鼎的BFPTR算法(也叫中位数的中位数算法),该算法可以从一堆无序数字中拿出第k小的数字,而且,而且最神奇的是!!!该算法在最坏情况下的时间复杂度竟然只有O(N)
没错,就是O(N)!
该算法是有五位大佬(Blum、Floyd、Pratt、Rivest、Tarjan)共同研究出来的,所以算法的名称也是他们每个人首字母的组合
算法思想:
这是算法导论一书中对该算法的步骤描述:
简单来说:
- 1.将这一堆数五个五个分成一组,最后剩下的小于五个的自成一组;
- 2.对每个分好的小组进行插入排序,拿到每个组中的中位数;
- 3.对前两个步骤找出来的中位数递归继续1,2(如果最后的中位数个数是偶数的话,一般取左边的这个);
- 4.得到的中位数的中位数就是主元x,将小于主元的放在主元的左侧,大于主元的放在右侧;
- 5.如果i等于k,直接返回x(x就是第k小的数);否则的话,如果i < k,去i的左边递归找,否则去i的右边递归。
寻找中位数:
public int findMid(int[] arr,int l,int r){
if (l == r) return arr[l];
int i;
int n = 0;
for(i = l; i < r - 5; i += 5){
insertSort(arr, i, i + 4);
n = i - l;
swap(arr,l + n / 5, i + 2);
}
//处理剩余元素
int num = r - i + 1;
if(num > 0){
insertSort(arr, i, i + num - 1);
n = i - l;
swap(arr,l + n / 5, i + num / 2);
}
n /= 5;
if(n == l) return arr[l];
return findMid(arr, l, l + n);
}
寻找中位数下标
public int findMidIndex(int[] arr,int l,int r,int num){
for (int i = l; i <= r; i++) {
if (arr[i] == num) return i;
}
return -1;
}
对数组进行分区
public int Partition(int[] arr, int l, int r, int p){
swap(arr,p, l);
int i = l;
int j = r;
int pivot = arr[l];
while(i < j){
while(arr[j] >= pivot && i < j)
j--;
arr[i] = arr[j];
while(arr[i] <= pivot && i < j)
i++;
arr[j] = arr[i];
}
arr[i] = pivot;
return i;
}
//插入排序
public void insertSort(int[] arr,int l,int r){
for (int i = l + 1; i <= r; i++) {
if (arr[i - 1] > arr[i]) {
int t = arr[i];
int j = i;
while (j > l && arr[j - 1] > t) {
arr[j] = arr[j - 1];
j--;
}
arr[j] = t;
}
}
}
//数组元素交换
public void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
BFPTR主方法调用
public int bfptr(int[] arr,int l,int r,int k){
int num = findMid(arr, l, r); //寻找中位数的中位数
int p = findMidIndex(arr, l, r, num); //找到中位数的中位数对应的index
int i = Partition(arr, l, r, p);
int m = i - l + 1;
if(m == k) return arr[i];
if(m > k) return bfptr(arr, l, i - 1, k);
return bfptr(arr, i + 1, r, k - m);
}
替换上一种算法:
public int[] kthMinPair(int[] arr,int k){
int N = arr.length;
if (k > N * N) return null;
//先确定左边的数字x
int x = bfptr(arr,0,N - 1,k / N + 1);
//找出小于x的数字和等于x的数字有多少
int lessXCount = 0,XCount = 0;
for (int i = 0; i < N && arr[i] <= x; i++) {
if (arr[i] < x) {
lessXCount++;
} else {
XCount++;
}
}
int y = bfptr(arr,0,N - 1,k - (lessXCount * N));
return new int[]{x,y};
}
如果你有更好的解法,欢迎留言给我哦