tarjan算法_想入职字节跳动?先来试试这道算法题

10ba451ab6ade5d97ebf601f58d325f6.png

题目描述:

给定一个长度为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)

这能难得住我?

这不就是把所有的组合列出来,然后排个序,轻轻松松这不就拿到结果了?

这是字节跳动的算法题???

12d87040a5fd9bf3c0de108da8b7824d.png

思路分析:

  • 如何拿到所有的排列组合?直接双层循环,简单粗暴!
  • 如何排序?使用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};
}

如果将这样的代码交给面试官,你猜面试官会不会夸夸你?

e2386b1369ed8023f45d0c3d4159cc2b.png

简单分析一下上述解法的时间复杂度:O(N^2 * log(N^2))

高的飞起!


再来分析一下题目:

先按照(x,y)x的大小进行排序

x都是递增的趋势,我们能不能先确定x的值是哪个?根据数组的长度我们是可以确定一共有多少种组合的,以题目中的数组[3,1,2]为例,那么最终的组合数就有3^2=9种,显而易见,以每个数字为x的组合分别有三对

5c9b305320449eb2afc62a9da0a366bc.png

不管y是多少,以x排的序是一定不会再改变了,如果k=5,我们立刻就能确定符合条件的组合中的x为2

x确定了,那么y该如何确定呢?

9808f0b168ab69ad262692a08f59e17b.gif

当确定了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小的组合。

3a76e14250575476b256be6c077287a6.png

思路分析:

  • 确定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)共同研究出来的,所以算法的名称也是他们每个人首字母的组合

算法思想:

970ad8ae38079bc7ab0f3d1656764f7b.png

这是算法导论一书中对该算法的步骤描述:

简单来说

  • 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};
}

如果你有更好的解法,欢迎留言给我哦

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值