632. 最小区间 - 力扣(LeetCode)
也是滑动窗口的思路,但是要困难很多。
这个题有个特点就是数组都是升序的,先这么想,从k个数组里面取出每个数组的最小值(就是第一个元素),现在这些最小值就对应一个区间:[k个值里面最小的那个, k个值里面最大的那个],我们可以记录下该区间的长度或者索引。然后把k个值中最小的那个踢出去,从这个值所属的数组里面再取下一个值,那么又得到一个新的区间,和上一个区间比较,取较小者,依次比较下去即可。
那么问题来了,k = 3我们就设置三个参数i, j, k去扫描这三个数组,但是每次取最小值最大值怎么取?每次都k个值遍历查找吗?显然不行,不过这题leetcode 第 239 题:滑动窗口最大值(C++)里面我们通过维护单调队列的方法来实现常数时间获取窗口最大值的操作,可以用在这儿吗?维护两个单调队列分别用来存取最大值最小值?似乎也是可行,不过感觉好麻烦。
能不能简单点?
如果我们一次性将k个数组全部合并为一个(同时打上标签记录这个数来自哪个数组,使用pair<元素值,标签>),然后进行排序,那我们要做的就是在合并后的数组里面找到一个最小的窗口,该窗口包含所有的标签,是不是一下就很明朗了?
如果sort函数对vector
那就直接套滑动窗口的思路就可以了:
class Solution {
public:
vector smallestRange(vector>& nums) {
vector> all_element;
for(int i = 0; i < nums.size(); ++i){
for(const auto &c : nums[i]) all_element.push_back({c, i});
}
sort(all_element.begin(), all_element.end()); //默认会按照pair.first进行排序
// nums.size()个数组代表 nums.size()个标签
unordered_map need;
for(int i = 0; i < nums.size(); ++i) need[i] = 1; //初始化need,每个标签都只需要一个
int left = 0, right = 0;
int count = 0;
int res_l = INT_MIN, res_r = INT_MIN; //记录最小区间左右边界
while(right < all_element.size()){
auto c = all_element[right].second;//取出标签
++right;
if(need.count(c)){
--need[c];
if(need[c] == 0) ++count;
}
while(count == need.size()){
if(res_l == INT_MIN || res_r - res_l > all_element[right-1].first - all_element[left].first){
res_l = all_element[left].first;
res_r = all_element[right-1].first;
}
auto d = all_element[left].second;
++left;
if(need.count(d)){
if(need[d] == 0)
--count;
++need[d];
}
}
}
return vector{res_l, res_r};
}
};
上述代码为了可读性写的比较长,其实很多地方可以做一下简写的,因为每个标签的需求量都是1,也是是只要窗口里有这个标签就行。
如果要进行优化的话其实也很简单,将上面的哈希表修改为数组就能提升运行效率,使用数组之后基本就可以运行用时和内存消耗都在双95%以上了。
class Solution {
public:
vector smallestRange(vector>& nums) {
vector> all_element;
for(int i = 0; i < nums.size(); ++i){
for(const auto &c : nums[i]) all_element.push_back({c, i});
}
sort(all_element.begin(), all_element.end()); //默认会按照pair.first进行排序
vector need(nums.size(), 1);//初始化need,每个标签都只需要一个,下标即对应标签
int left = 0, right = 0;
int count = 0;
int res_l = INT_MIN, res_r = INT_MIN; //记录最小区间左右边界
while(right < all_element.size()){
auto c = all_element[right].second;//取出标签
++right;
if(need[c] > INT_MIN){ //因为下面的递减会导致某些下标对应的值小于0
--need[c];
if(need[c] == 0) ++count;
}
while(count == need.size()){
if(res_l == INT_MIN || res_r - res_l > all_element[right-1].first - all_element[left].first){
res_l = all_element[left].first;
res_r = all_element[right-1].first;
}
auto d = all_element[left].second;
++left;
if(need[d] > INT_MIN){
if(need[d] == 0)
--count;
++need[d];
}
}
}
return vector{res_l, res_r};
}
};
再修改一下,因为数组的每个下标即对应了相应元素的标签,没有空缺的部分(空缺的意思是就像之前处理字符串那样,把字符转化为ASCII码,将码值作为数组下标,但是码值并非连续(字符不连续时)的,所以数组里有一些下标对应的值并不是我们关心的,我们仅仅关心字符码值对应的下标),所以上面代码里和INT_MIN的部分并没有必要,因为每个标签都在数组里能找到对应的下标的。所以可以将比较操作去掉,其他地方也优化一下:
class Solution {
public:
vector smallestRange(vector>& nums) {
vector> all_element;
for(int i = 0; i < nums.size(); ++i){
for(const auto &c : nums[i]) all_element.push_back({c, i});
}
sort(all_element.begin(), all_element.end()); //默认会按照pair.first进行排序
vector need(nums.size(), 1);//初始化为1,因为每个标签(一个数组就对应一个标签)都只要有一个就行
int left = 0, right = 0; //窗口左右边界
int count = 0; //计数器,当count = nums.size() 的时候,说明窗口里每个标签的值都有了
int res_l = INT_MIN+1, res_r = 0; //记录最小区间左右边界
while(right < all_element.size()){
auto c = all_element[right++].second;//取出标签,并右移窗口
--need[c];
if(need[c] == 0) ++count;//窗口里每多一个标签,计数器就+1
while(count == need.size()){
if(res_r - res_l > all_element[right-1].first - all_element[left].first){
res_l = all_element[left].first;
res_r = all_element[right-1].first;
}
auto d = all_element[left++].second; //取出标签,并左移窗口
if(need[d] == 0) --count;
++need[d];
}
}
return vector{res_l, res_r};
}
};
现在的代码运行效率已经双高了,如果还要优化,那其实还可以从排序函数入手,因为k个升序数组本来就是有序的(或者手写一个快排应该也能有所提升)。那将本来就是有序的多个数组合并为一个数组,自然让人想到归并排序的merge函数思路,依次两两合并。不过效率未必会增加,因为得看k值的设定,代码就懒得写了。