二维数组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,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。