rust踩雷笔记(2)——一道hard带来的思考[哈希表、字符串、滑动窗口]

news/2024/10/23 7:33:28/

今天被一道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<char>
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改成了什么 & 代码末尾有一些注释记录unwrap、unwrap_or、unwrap_or_else的用法

注意看我把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<char>// 最新:严禁采用这种方式,根据实践这种方式速度有问题,当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,就插入并将对应的值设为0let 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,查不到就返回&0while *(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和bif cnt == t.len() && res.end - res.start > right - left {// 更新一波结果res = left..right;}}// 如果没找到结果,那么res就还是初始值0..usize::MAXif 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()入参应该是&valueunwrap_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<char>// 严禁采用这种方式// 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的入参是&charif 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::MAXif res.end - res.start > s.len() {"".to_string()} else {s[res].to_string()}}
}

http://www.ppmy.cn/news/1037841.html

相关文章

二刷LeetCode--148. 排序链表(C++版本),必会题,思维题

思路&#xff0c;本题其实考察了两个点&#xff1a;合并链表、链表切分。首先从1开始&#xff0c;将链表切成一段一段&#xff0c;因为需要使用归并&#xff0c;所以下一次的切分长度应该是当前切分长度的二倍&#xff0c;每次切分&#xff0c;我们拿出两段&#xff0c;然后将第…

Jetpack 中的 LiveData 粘性事件

什么叫粘性事件 LiveData使用篇Jetpack 中的 LiveData - 使用篇_简单不一定不好的博客-CSDN博客后再进一步了解LiveData。观察者和被观察者&#xff0c;正常情况下观察者先注册&#xff0c;被观察者再发送观察事件&#xff1b;所以粘性事件可以理解为观察者模式的升级&#xf…

LeetCode235. 二叉搜索树的最近公共祖先

235. 二叉搜索树的最近公共祖先 文章目录 [235. 二叉搜索树的最近公共祖先](https://leetcode.cn/problems/lowest-common-ancestor-of-a-binary-search-tree/)一、题目二、题解方法一&#xff1a;递归方法二&#xff1a;迭代 一、题目 给定一个二叉搜索树, 找到该树中两个指定…

ZooKeeper的应用场景(集群管理、Master选举)

5 集群管理 随着分布式系统规模的日益扩大&#xff0c;集群中的机器规模也随之变大&#xff0c;因此&#xff0c;如何更好地进行集群管理也显得越来越重要了。 所谓集群管理&#xff0c;包括集群监控与集群控制两大块&#xff0c;前者侧重对集群运行时状态的收集&#xff0c;后…

uniapp 上传比较大的视频文件就超时

uni.uploadFile&#xff0c;上传超过10兆左右的文件就报错err&#xff1a;uploadFile:fail timeout&#xff0c;超时 解决&#xff1a; 在manifest.json文件中做超时配置 uni.uploadFile({url: this.action,method: "POST",header: {Authorization: uni.getStorage…

IDEA部署配置Maven项目教程,IDEA配置Tomcat(2019.3.3)(2023.1.3)

我们往往会用到多版本的IDEA进行一个Maven项目配置部署&#xff0c;还有tomcat的配置&#xff0c;这里就有你需要的&#xff0c;有低版本的&#xff0c;也有高版本的&#xff0c;根据自己的情况来进行一个操作 一、前言 当涉及到软件开发和项目管理时&#xff0c;使用一个可靠的…

数字图像处理-AWB跳变

1、自动白平衡&#xff08;AWB&#xff09;算法是相机中常用的图像处理技术&#xff0c;它能够自动调整图像中的白平衡&#xff0c;使得图像中的颜色更加真实、自然。然而&#xff0c;在实际应用中&#xff0c;AWB算法也存在着一些问题&#xff0c;例如AWB跳变&#xff08;Whit…

LVS-DR模式

目录 1、概述 2、LVS-DR模式的工作原理&#xff1a; 3、在LVS-DR模式下&#xff0c;数据包的流向分析如下&#xff1a; 4、LVS-DR是一种用于构建高可用性负载均衡集群的技术模式。LVS-DR模式具有以下特点&#xff1a; 5、LVS-DR中的ARP问题 6、配置LVS-DR需要以下几个关键…