今天被一道hard恶心坏了,算法不难,用C++几分钟的事。用rust主要还是缺乏对语言的熟练度,记录一下,主要的坑在下面这个操作:
对String取其中某个位置的char。
可能你会有疑问:这不是直接nth()取就行了么。没错,我看有些题解也是这样做的,但是那样会在某些字符长度很长的样例中OOT。
本文就从代码角度讲下这个问题。此外还有对哈希表更新操作中,不同语句的效率对比。当然我还对一些语法加了详细注释,因为我是初学者,上周六接触到rust,感觉到很有意思,但rust在一些地方的画风的确是不太一样,所以在语法上我需要多花功夫,也希望能帮到有需要的人。
https://leetcode.cn/problems/minimum-window-substring/submissions/
链接我复制下来了,题目可以自行去leetcode 76看,我也不讲详细解法,蛮简单的。主要讲讲实现的注意事项。
不得不说注释还是很有用的,一开始的方法我怎么也没找到问题所在,所以索性对所有代码写了注释,最后定位到了罪魁祸首:nth()
这部分就给出几个版本的代码,详细的内容都在注释里,我这里主要写一下注意事项:
给定String s,让你取出里面第i个char,你会怎么做?
// 取s中下标为i的字符,nth()返回的是Option
let c = s.chars().nth(i).unwrap();
相信这种写法你一定能想到,但是它会有什么问题吗?
当然!害我找了一上午bug:如果要遍历其中的char,i的范围是0~s.len() - 1
,这种情况下,s如果很长很长,可能会造成OOT。
那你可能会问了:你为啥不用for c in s.chars()
,
那是因为我要用下标啦;
那你又会问了:用for (idx, c) in s.chars().enumerate()
不也可以吗?
的确是可以,但我想了解所有的修改方式,除了上面这两种常见的,就没有别的办法了吗?初学者(出血者)的探索精神是不可阻挡的。
当然有!可以把nth的方式改为as_bytes()[](注意添加as char):
// 重要操作:当只有英文字母的时候,用下面的方式访问
let c = s.as_bytes()[right] as char;
注意看我把nth修改成什么了
use std::collections::HashMap;
impl Solution {
pub fn min_window(s: String, t: String) -> String {
// 定义滑动窗口[left,right)
let mut left = 0;
let mut right = 0;
// ht存储t所有字符的个数,hs存储s[left..right]的所有字符的个数
// 可以省略掉类型的声明,这里为了好理解所以我写上
let mut ht: HashMap<char, usize> = HashMap::new();
let mut hs: HashMap<char, usize> = HashMap::new();
// 遍历t的所有字符,将它们加入ht中。
// 遍历字符串的方式是ch in string.chars(),ch就是char型
for ch in t.chars() {
// 重要知识:如何改变hashmap中value的值
// 方式一:entry入参是key,or_insert/or_insert_with/or_default返回的是对value的可变引用
// let v = ht.entry(ch).or_insert(0);
// *v += 1;
// 方式二:直接insert覆盖掉旧值。这里的解引用*不加也不会报错,暂不详原理
ht.insert(ch, *(ht.get(&ch).unwrap_or(&0)) + 1);
}
// 给最小窗口赋予初始值。usize::MAX的方式可以赋一个很大的值
let mut res = 0..usize::MAX;
// 表示滑动窗口内,有多少字符是t中的
let mut cnt = 0;
// 遍历s中的字符,滑动窗口右端点向右移动
while right < s.len() {
// 重要操作:取s中下标为right的字符,nth()返回的是Option
// 最新:严禁采用这种方式,根据实践这种方式速度有问题,当s.len()范围可以特别大的时候会对某些样例超时
// let c = s.chars().nth(right).unwrap();
// 重要操作:当只有英文字母的时候,用下面的方式访问
let c = s.as_bytes()[right] as char;
right += 1; // 先将right移动,是因为滑动窗口是左闭右开区间[left, right)
// 将c加入hs中。v绑定的是c这个key对应的value的可变引用。如果不存在c这个key,就插入并将对应的值设为0
let v = hs.entry(c).or_insert(0);
*v += 1;
// 看一下插入的是否为有效字符
// 下面*在!=那行省略会报错;在<=那行省略不会报错
if *(hs.get(&c).unwrap_or(&0)) != 0 &&
*(hs.get(&c).unwrap_or(&0)) <= *(ht.get(&c).unwrap_or(&0)) {
cnt += 1;
}
// 最容易写错的地方,如果滑动窗口左端点是重复的,就从hs中去除,并移动左端点
// 严禁采用这种方式
// let mut left_char = s.chars().nth(left).unwrap();
let mut left_char = s.as_bytes()[left] as char;
// 这里只用unwrap会报错,编译器会认为有对None进行unwrap的风险
// 查的到就返回&value,查不到就返回&0
while *(hs.get(&left_char).unwrap_or(&0)) > *(ht.get(&left_char).unwrap_or(&0)) {
// 这可以是一个万能的更新hashmap中value的方法,用覆盖的方式更新值
// 不用担心unwrap_or(&0) - 1会不会导致扔个-1进去,能进while循环,就说明值是1起步,减一肯定不为负
hs.insert(left_char, hs.get(&left_char).unwrap_or(&0) - 1);
left += 1;
// 这里对left范围检查,前文也用unwrap_or('0')检查过
if left >= right {
break;
}
// left_char = s.chars().nth(left).unwrap();
left_char = s.as_bytes()[left] as char;
}
// res是a..b类型,可以用start和end来访问a和b
if cnt == t.len() && res.end - res.start > right - left {
// 更新一波结果
res = left..right;
}
}
// 如果没找到结果,那么res就还是初始值0..usize::MAX
if res.end - res.start > s.len() {
"".to_string()
} else {
s[res].to_string()
}
}
}
/*
unwrap()有什么风险?
如果是对None调用unwrap是错误的。
如果我确保x不会是None,并对x调用unwrap,看上去就符合逻辑。
但是编译器可能认为这有风险,从而直接报错!
所以要用unwrap_or()或者unwrap_or_else()来代替它。
unwrap_or()怎么用?
assert_eq!(Some("car").unwrap_or("bike"), "car");
assert_eq!(None.unwrap_or("bike"), "bike");
简记:如果是Some()就unwrap,如果是None就是unwrap_or()括号中的值
注意:unwrap_or()的入参类型是T,和Some(T)中的T保持一致,
所以如果是哈希表的get,查出来的是Option<&value>,那么unwrap_or()入参应该是&value
unwrap_or_else()怎么用?
let k = 10;
assert_eq!(Some(4).unwrap_or_else(|| 2 * k), 4);
assert_eq!(None.unwrap_or_else(|| 2 * k), 20);
简记:和unwrap_or类似,但如果括号内要设定一个表达式,建议用unwrap_or_else,因为它是lazily evaluated
*/
你可能注意到了,上面的代码片段中,我在注释里写了两种对hashmap的修改方式:entry和insert覆盖,它们两哪种快呢?上面代码是insert覆盖的方式,下面我用entry的方式(代码和版本一变不了多少,再看一次就当巩固了)。
实测这个稍微快一点点。也就是我们修改hashmap的时候可以主要考虑用entry的方式(保留此话修改可能)。
use std::collections::HashMap;
impl Solution {
pub fn min_window(s: String, t: String) -> String {
// 定义滑动窗口[left,right)
let mut left = 0;
let mut right = 0;
// ht存储t所有字符的个数,hs存储s[left..right]的所有字符的个数
// 可以省略掉类型的声明,这里为了好理解所以我写上
let mut ht: HashMap<char, usize> = HashMap::new();
let mut hs: HashMap<char, usize> = HashMap::new();
// 遍历t的所有字符,将它们加入ht中。
// 遍历字符串的方式是ch in string.chars(),ch就是char型
for ch in t.chars() {
// 重要知识:如何改变hashmap中value的值
// 方式一:entry入参是key,or_insert/or_insert_with/or_default返回的是对value的可变引用
*ht.entry(ch).or_insert(0) += 1;
// 方式二:直接insert覆盖掉旧值。这里的解引用*不加也不会报错,暂不详原理
// ht.insert(ch, *(ht.get(&ch).unwrap_or(&0)) + 1);
}
// 给最小窗口赋予初始值。usize::MAX的方式可以赋一个很大的值
let mut res = 0..usize::MAX;
// 表示滑动窗口内,已经有valid个字符,数量等于t中该字符数量
let mut valid = 0;
// 遍历s中的字符,滑动窗口右端点向右移动
while right < s.len() {
// 重要操作:取s中下标为right的字符,nth()返回的是Option
// 严禁采用这种方式
// let c = s.chars().nth(right).unwrap();
let c = s.as_bytes()[right] as char;
right += 1; // 先将right移动,是因为滑动窗口是左闭右开区间[left, right)
// 判断字符是否在t中,只有在ht中,才能插入到hs中
// contains_key的入参是&char
if ht.contains_key(&c) {
// 方式一:entry,在leetcode上测的时间方式一似乎比方式二快一些
*hs.entry(c).or_insert(0) += 1;
// 方式二:插入覆盖(这里不加*也可以,暂时不知道为什么)
// hs.insert(c, hs.get(&c).unwrap_or(&0) + 1);
if hs.get(&c) == ht.get(&c) {
valid += 1;
}
}
while valid == ht.len() {
if right - left < res.end - res.start {
res = left..right;
}
// 在while中不断将滑动窗口左端点移出,直到跳出这个while,那么再进入外层while
// 严禁用这种方式
// let mut left_char = s.chars().nth(left).unwrap();
let mut left_char = s.as_bytes()[left] as char;
left += 1;
if ht.contains_key(&left_char) {
// ht中有的才会加入hs,所以要先判断才将left_char从hs中移出
// 如果移出之前窗口内left_char的数量等于t中left_char数量,那么valid要减少
if hs.get(&left_char) == ht.get(&left_char) {
valid -= 1;
}
// hs.insert(left_char, hs.get(&left_char).unwrap_or(&0) - 1);
*hs.entry(left_char).or_insert(0) -= 1;
}
}
}
// 如果没找到结果,那么res就还是初始值0..usize::MAX
if res.end - res.start > s.len() {
"".to_string()
} else {
s[res].to_string()
}
}
}