二维数组list,每行长度不同,寻找最窄区间[a,b],保证list每行至少有一个数在[a,b]上

二维数组list,每行长度不同,寻找最窄区间[a,b],保证list每行至少有一个数在[a,b]上

提示:本题是系统有序表的经典应用,贪心算法,舍弃思想,确实很难想到解决方案
经常在互联网大厂中考,第一道大题,往往就是有序表,或者堆来解决,设计排序的事情。

本题涉及有序表的基础知识:
【4】有序表TreeMap/TreeSet底层实现:AVL树、傻逼树SBT、红黑树RBT、跳表SkipMap失衡类型
【5】傻逼树SBT:Size Balanced Tree的实现原理,增删改查,调平衡
【6】跳表SkipList:可二分查找的有序链表,实现有序表,思想先进,操作复杂度O(logn)

有序表可以通过SBT,跳表,红黑树啥的实现,
最先进的实现方式就是跳表实现有序表,简单,快速

用有序表可以优化很多查找排序的事情。


题目

给你一个二维动态数组list,每一行的长度不一,但是每一行都是有序的1维动态数组
让你找一个区间,[a,b],使得,每一行数组都至少有一个元素在[a,b]上
而且取最窄的区间,如果有两个相同的很窄的区间,则取a比较小的那个区间


一、审题

示例:
list=
1 3 9
4 10 11
4 7 12

区间a–b可选为:
1–4
2–4
3–4
3–5
3–7
等等等等……
反正至少保证list的每一行,有一个数在a–b区间上
咱们取最窄的区间是谁呢?
自然是3–4最窄,且包含所有行的至少一个数


暴力解不可:过于复杂o(n^2)

咋做呢?
先找list的min和max值
默认start=min,end=max,这一定满足,但是过大
外循环:从min–max每一个i做一次开头
内循环:j=i+1–max做此刻i的结尾
i–j就是可能的区间,查list每一行,如果保证每行至少有一个数在i–j范围内,达标
后续每一个区间,j-j<此前的j-i就可以更新i为start,将j更新为end,得到更窄的结果
不过这么做,过于复杂!
必然是**o(n^2)**复杂度!

咱们从min–max大量去搜索了可能list压根不存在的关键点
比如上面的例子
示例:
list=
1 3 9
4 10 11
4 7 12

如果你想要判断3–5是否为结果,完全没必要,因为5压根不在list中
3–5的答案,一定比3–4长,没必要去找了
其实我们只需要关注已经存在list中的各个点


最优解:有序表舍弃非list中的关键点,o(nlog(n))速度

方法挺难想的:贪心算法,舍弃没出现在list中的那些关键点

这个用系统的有序表TreeMap,
(1)最开始,直接以list每行i中的j=0列那个元素,放入有序表map,然后取map的firstK为start,取lastKey为end,得到一个区间a–b
为什么?因为每次加入每行的最小值,必然map包含了list至少每行一个元素,
而且进map还是有序的,a–b必然是map的首位元素,这样才能保证a–b区间是包含至少每行的每一个元素
(2)然后让弹出map的firstKey,用list中firstKey那行的下一个最小元素进map,即保证每行必须有一个
再检查此时首尾差值,如果更窄,更新给start和end,否则不管,遇到俩窄度一样的,不更新。
(3)途中,只要发现map不足2个元素了,或者list某一行没有元素可以补充加入map了,停止循环,返回start–end作为结果
因为某一行没了可加的元素,就无法保证每一行都必须有一个数在a–b中了。

这么做,相当于是让每一个list的最小值,做一次开头,然后收取最窄的区间,既保证窄,又保证至少包含每行一个元素,还能避免没出现在list中的那些关键点。——这是本题最难想到的地方。就是利用有序表完成的这种操作!

举例:list=
1 3 9
4 10 11
4 7 12
(1)组开始让每行最小值进map,1 4 4 进入map
(2)取map的firstK=1为start,取lastKey=4为end
在这里插入图片描述
(3)然后将map的firstK=1弹出,将firstK所在行的最小值3放入map,得到绿色的有序表,
判断此刻的lastKey=4,firstKey=3,差值为1,小于刚刚的4-1
故,需要更新a–b区间:取map的firstK=1为start,取lastKey=4为end
(4)然后将map的firstK=3弹出,将firstK所在行的最小值9放入map,得到粉色的有序表,
判断此刻的lastKey=9,firstKey=4,差值为5,大于等于刚刚的4-3=1
故,不需要更新a–b区间
(5)然后将map的firstK=4弹出,将firstK所在行的最小值10放入map,得到橘色的有序表,
判断此刻的lastKey=10,firstKey=4,差值为6,大于等于刚刚的4-3=1
故,不需要更新a–b区间
……
不断这么找下去,每一个list中的最小值,做一次开头,收集更新一次答案,最后最窄区间必在其中
一旦0行那个9进去了,又出来了,就没元素可以加了,停止寻找。

但凡这种遇到舍弃的思想,都挺难,但是有序表是一个好东西!
经常在互联网大厂中考,第一道大题,往往就是有序表,或者堆来解决,设计排序的事情。

手撕代码:

    public static int[] minLenBetweenab(List<List<Integer>> list){
        if (list == null || list.size() == 0) return new int[] {-1, -1};

        TreeMap<Integer, Integer> map = new TreeMap<>();//有序表默认由小到大
        //key记录数组中的值,value记录它来自哪个数组

        int N = list.size();
        for (int i = 0; i < N; i++) {
            map.put(list.get(i).get(0), i);//key是arr i行j=0那个数,value是i行
            list.get(i).remove(0);//然后i行j=0列那个数废掉
        }

        int start = map.firstKey();//firstKey,做start,lastKey做end
        int end = map.lastKey();
        //然后准备更新结果
        //每一个arrij都做一次排头,看看结尾是
        while (map.size() != 1){//至少map中有2个
            //抓取firstKey的行i编号,然后弹出firstKey,
            int i = map.get(map.firstKey());
            map.remove(map.firstKey());
            //最短的那行数据用完,就结束
            if (list.get(i).size() == 0) break;
            //还有数的话,然后新加,弹出节点的那行数据的下一个值,并记录它的行号i
            map.put(list.get(i).get(0), i);
            list.get(i).remove(0);//然后i行j=0列那个数废掉
            // 检查此刻firstKey做start,lastKey做end,可行吗?新来的比之前间隔更短,可以更新,间隔相同的,不管
            if (map.lastKey() - map.firstKey() < end - start){
                start = map.firstKey();
                end = map.lastKey();
            }
        }
        //map数不够2个了,或者list全部加过了,那就算了
        return new int[] {start, end};

    }

测试一波;

    public static void test(){
        List<List<Integer>> list = new ArrayList<>();
        List<Integer> arr0 = new ArrayList<>();
        arr0.add(1);
        arr0.add(3);
        arr0.add(9);
        List<Integer> arr1 = new ArrayList<>();
        arr1.add(4);
        arr1.add(10);
        arr1.add(11);
        List<Integer> arr2 = new ArrayList<>();
        arr2.add(4);
        arr2.add(7);
        arr2.add(12);
        list.add(arr0);
        list.add(arr1);
        list.add(arr2);

        int[] ans = minLenBetweenab(list);
        System.out.println("最窄区间为:"+ ans[0] +"--"+ ans[1]);
    }

    public static void main(String[] args) {
        test();
    }
最窄区间为:3--4

很OK
咱们来看看算法复杂度

由于每个list的最小值可能都要做一个排头,故外循环是o(n)
内部每次map排序都需要log(n),准确地说是log(3)=1
但是lastKey方法是o(log(n))速度查找的,跳表的话,它需要去最右下角那获取lastKey
故算法复杂度其实是
o(nlog(n))

够牛吧,这种有序表来舍弃的思想,贪心思想,还是够强的。


总结

提示:重要经验:

1)经常在互联网大厂中考,第一道大题,往往就是有序表,或者堆来解决,设计排序的事情。
2)本题的最窄区间a–b,是通过有序表掌握首尾位置来快速拿到区间值的,有序表的强大之处就在它的各种索引方法速度快o(log(n))
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值