没有使用map的办法:
看过前两篇文章的佬们应该能看出来,笔者完全是一个代码小白,在最近做到一道题,名为水果成篮,取自Leecode904题:
https://leetcode.cn/problems/fruit-into-baskets/description/
有一些自己的理解想跟大家分享一下。
刚理解了滑动窗口的我兴致冲冲地去做,结果碰了一鼻子灰,比如一开始是想用一个容量为2的数组Type来表示篮子,那么就会出现一个问题,在我们要往里面新装入水果的时候,该如何知道其中哪个位置需要被替换呢?是该保留苹果还是香蕉?这个问题我们放在后面去探讨。
起初,我用left指向当前篮子里的第一个水果,right指向即将往里新装入的水果,如果right指向的水果类型和之前存入Type的两个水果类型不同,那么就让left前移,直到篮子里只剩下含right指向的水果类型的两种水果,而最开始为了保证篮子是空的,根据题目条件 fruit 中的元素都是≥0的,因此可以直接将空篮定义为{-1,-1},那么问题来了,我怎么知道篮子里何时只剩下两种水果?
很容易想到的思路是先保存 fruit[right-1] 指向的类型作为将被保存的类型,即
changeType = Type[0] == fruit[right-1] ? Type[1] : Type[0];
此时changeType就代表需要被替换的类型,随后便想到,我要找的无非是让 left 与 right 之间只有两种类型的元素,那么在 right 此时已经确定是一个新元素的情况下,我们只需要遍历从 left 到 right 之间 changeType出现的最后的位置,然后让 left 指向该位置的后一个,便可以完成 left 的前移操作,代码看起来会更清晰:
int i;
for (i=left;i<right;i++) {
if(fruits[i]==changeType)
left=i;
}
现在回到我们最开始的问题,我怎么知道哪个位置需要被替换?此时我们已经知道,菜篮子里不就俩类型的水果吗,不是第一个就是第二个呗,于是只要做一次判断:
if(Type[0] == changeType)
Type[0]=fruit[right];
else
Type[1]=fruit[right];
这样就可以得到需要替换的位置,理清楚这几个问题,代码就行云流水版写出来了:
class Solution {
public:
int totalFruit(vector<int>& fruits) {
int n = fruits.size();
int Type[2] = {-1, -1};
int ans = 0, left = 0, right = 0, Sublength = 0, changeType = -1;
while (right < n) {
if (Type[0] == -1 && Type[1] != fruits[right]) {
Type[0] = fruits[right];
Sublength = right - left+1; // 计算当前长度
ans = ans >= Sublength ? ans : Sublength; // 计算最长子数组长度
right++;
continue;
} else if (Type[1] == -1 && Type[0] != fruits[right]) {
Type[1] = fruits[right];
Sublength = right - left+1; // 计算当前长度
ans = ans >= Sublength ? ans : Sublength; // 计算最长子数组长度
right++;
continue;
}
if (fruits[right] == Type[0] ||
fruits[right] == Type[1]){ // 如果新水果和篮子类型一致就放入
Sublength = right - left+1; // 计算当前长度
ans = ans >= Sublength ? ans : Sublength; // 计算最长子数组长度
right++;
}
else {
if (Type[0] !=
fruits[right-1]) { // 先找出多余类型的水果放在哪个篮子
changeType=Type[0];
Type[0] = fruits[right]; // 进行替换
} else if (Type[1]!= fruits[right-1]) {
changeType=Type[1];
Type[1] = fruits[right];
}
int i;
for (i=left;i<right;i++) {
if(fruits[i]==changeType)
left=i;
}
left++;
}
}
return ans;
}
};
可以看到复杂度还是很低的,但是代码非常冗余,且该题花费了笔者一个小时的时间,实在是太废柴了,有没有什么更牛逼的办法能更快实现呢?
std::map
map,一种STL关联容器,在这里不过多赘述map的底层逻辑,只简单讲一下在这道题中使用,首先map是一种键值对数据结构,说白话就是里面存着的是一对一对的数据,(唉,笔者还是单身,使用的数据结构却都是成双成对的,开个玩笑),一个叫做key,代表着关键字,一个叫做value,代表着存储的值,因此这种数据结构类型被称为键值对,这种数据结构允许我们直接使用key用来搜索value,至于为什么咱们现在不用管这么多,那如何使用它呢?拿这道题举例子,我们直接定义篮子:
unordered_map<int, int> cnt
这里有人要问了,不是讲map吗?这 unordered_map是什么东西?其实它也是map的一种,只不过底层是用哈希表实现的,而map使用的是红黑树,扯远了哈,大家只要知道他们的使用方法是一样的,这里使用 unordered_map也只是为了能使代码运行快那么一点点,第一个int代表关键字是整型,第二个int代表value是整型,cnt则是我们定义的map类型的名字,在这题中,我们让key代表水果的种类,value代表水果在篮子中的数量,是不是很完美,我们现在对它进行遍历,看看会有什么不一样,首先在刚刚,我们需要对初始篮子定义为{-1,-1}的空篮,其实根本不用那么麻烦,只需要利用map的特性:
如果 fruits[right]
还没有出现在map中,cnt[fruits[right]]
会自动初始化为 0
这样一来,我们只需要,再对其进行+1的操作,就可以表示每一轮有水果进入篮子,进篮子的问题解决了,那你怎么知道现在篮子里有几种类型呢?别急,这里就要使用map自带的函数,size,,它可以直接查询现在map的大小,刚刚不是让水果进篮子了吗,现在我们查询一下cnt.size,如果size>2,就说明这个时候有新类型的水果进入了,接下来的内容我将结合代码更清晰直观地讲解,先放上代码:
for (int right = 0; right < n; ++right) {
++cnt[fruits[right]];
while (cnt.size() > 2) {
auto it = cnt.find(fruits[left]);
--it->second;
if (it->second == 0) {
cnt.erase(it);
}
++left;
}
ans = max(ans, right - left + 1);
}
没学过map的读者肯定从while开始就一头雾水,因为笔者也是这样的,因此我们还是要介绍一下这里用到的一些操作: auto可以让编译器自行推断元素类型,这样我们就不用自己绞尽脑汁地去想 it 定义为什么类型,是不是方便到爆炸了,cnt.find,顾名思义,是个查找函数,可以直接找到fruits[left]
的迭代器,啊?迭代器又是什么?这里为了方便各位理解且不影响阅读,请直接理解为map的指针,那么得到了left指向的位置,是不是就得到了刚刚我们要找的,需要被剔除的水果的类型以及个数,看看学会了STL之后,操作起来有多么方便,it->second 指的是it
指向的元素的value,不难想到 it->first 应该就是关键词key了,刚刚提到,value表示的是该元素在fruit中的数量,那此时也不需要专门定义一个指针 i 去进行 left 和 right 间的遍历了,我们直接判断 it->second 是否为0就可以,当 it->second =0,说明这个类型的水果已经被剔除干净了,我们接着就可以直接使用函数erase从cnt中删除该水果,不得不令人感叹,会STL的就是不一样,最后同样++left开始新的循环就可以了,写完这篇之后,笔者要继续学习那该死的STL了,有错误的地方希望大家能够不吝赐教批评指正。