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

这篇具有很好参考价值的文章主要介绍了rust踩雷笔记(2)——一道hard带来的思考[哈希表、字符串、滑动窗口]。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

今天被一道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,就插入并将对应的值设为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的方式(保留此话修改可能)。文章来源地址https://www.toymoban.com/news/detail-654628.html

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的入参是&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()
        }
    }
}

到了这里,关于rust踩雷笔记(2)——一道hard带来的思考[哈希表、字符串、滑动窗口]的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包赞助服务器费用

相关文章

  • Rust 笔记:Rust 语言中哈希结构(哈希映射,HashMap)、集合(哈希集,HashSet)及其使用

    Rust 笔记 Rust 语言中映射(HashMap)与集合(HashSet)及其用法 作者 : 李俊才 (jcLee95):https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343 邮箱 : 291148484@163.com 本文地址 :https://blog.csdn.net/qq_28550263/article/details/130876735 【介绍】:本文介绍 Rust 中哈希结构相关概念及其使用。在 R

    2024年02月09日
    浏览(11)
  • Rust 笔记:Rust 语言中的字符串

    Rust 笔记 Rust 语言中的字符串 作者 : 李俊才 (jcLee95):https://blog.csdn.net/qq_28550263?spm=1001.2101.3001.5343 邮箱 : 291148484@163.com 本文地址 :https://blog.csdn.net/qq_28550263/article/details/130876665 【介绍】:本文介绍 Rust 语言中的字符和字符串的用法。 上一节:《 Rust 语言中使用 vector(向

    2024年02月06日
    浏览(10)
  • 【面试算法——动态规划 21】正则表达式匹配(hard)&& 交错字符串

    【面试算法——动态规划 21】正则表达式匹配(hard)&& 交错字符串

    链接: 10. 正则表达式匹配 给你一个字符串 s 和一个字符规律 p,请你来实现一个支持 ‘.’ 和 ‘*’ 的正则表达式匹配。 ‘.’ 匹配任意单个字符 ‘*’ 匹配零个或多个前面的那一个元素 所谓匹配,是要涵盖 整个 字符串 s的,而不是部分字符串。 示例 1: 输入:s = “aa”

    2024年02月08日
    浏览(14)
  • AIGC: 关于ChatGPT这个智能工具带来的几点思考

    ChatGPT的出现 2022年11月底,ChatGPT 上线,引爆 AI 圈 和 科技圈,2023年春节后, 人人都开始关注并讨论这项新技术 它是 OpenAI 研发的智能聊天工具, 基于GPT语言模型,模拟人类的对话方式 默认只能用文字进行交互,理解多种语言,有一些插件,可用语音,图表等 截止现在,Chat

    2024年02月04日
    浏览(12)
  • Qt关于QPainter绘制1px宽度图形带来的问题思考

    Qt关于QPainter绘制1px宽度图形带来的问题思考

    前段时间遇到这样一个问题,使用QPainter绘制直线的时候,设置了笔宽为1像素,但是绘制出来的线条却是2px宽度,而且设置的画笔颜色很明显是降低了透明度,不是最“纯正”的颜色。 当时就感觉非常奇怪,明明设置的画笔宽度是正常的,为啥绘制出来不是自己想要的样子。

    2023年04月20日
    浏览(12)
  • 进亦忧,退亦忧,Github Copilot 集成进入 Visual Studio 带来的思考

    进亦忧,退亦忧,Github Copilot 集成进入 Visual Studio 带来的思考

    开篇想到《岳阳楼记》的结尾: 未来30年的开发变革,与过去30年相比,是指数函数才能勉强描述的趋势。有时候回想已经过去的30年,确实有些恍惚和迷茫。AI的发展已经到了一个拐点,无论是个人还是公司,如果不去主动拥抱新的变化,必然会被时代淘汰。 随着 visual stu

    2024年02月04日
    浏览(9)
  • 实习成长之路:关于ElasticSearch深度分页带来的思考,如何解决深度分页和跳页

    实习成长之路:关于ElasticSearch深度分页带来的思考,如何解决深度分页和跳页

    我们在平常使用ElasticSearch构建查询条件的时候一般用的都是from+size的方式进行分页查询,但是如果我们的页数太深/页面大小太大(from*size)10000就会引发一个错误,我们将会得到一个错误 这是为什么呢? 因为ES的分页查询其实是这样来的 因为ElasticSeach的天生分布式的原因,我

    2023年04月27日
    浏览(12)
  • 【腾讯云 Finops Crane集训营】Finops Crane究竟能为我们带来什么价值和思考?深入探究Crane

    【腾讯云 Finops Crane集训营】Finops Crane究竟能为我们带来什么价值和思考?深入探究Crane

    目录 前言 一、Crane目的是什么? 二、Crane有哪些功能? 1.成本可视化和优化评估  2.推荐框架  3.基于预测的水平弹性器 4.负载感知的调度器 5.拓扑感知的调度器 6.基于 QOS 的混部 三.Crane的整体架构及特性 1.Crane架构 Craned Fadvisor Metric Adapter Crane Agent 2.Crane特性 一键部署 简单易

    2024年02月05日
    浏览(13)
  • rust的哈希表

    以上两种方法都必须保证访问的元素存在,否则会报错 哈希表中的元素没有顺序 两种方法,contains_key和entry contains_key方法用于检查HashMap中是否包含特定的键 它返回一个布尔值,指示键是否存在。 entry方法用于高效地处理键值对的插入和更新 它返回一个Entry枚举,可以是Oc

    2024年02月19日
    浏览(7)
  • 哈希表+字符串

    哈希表+字符串

    一)知识回顾: 1)哈希表是什么?哈希表是存储数据的容器 2)哈希表有啥用?快速的查找某一个元素 3)什么时候使用哈希表?频繁的查找某一个数的时候, 当我们快速查找某一个数的时候,不光要想到哈希表还需要想到二分查找,但是二分查找算法的局限性太强了,必须数组中有序

    2024年02月10日
    浏览(7)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包