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
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
            // 最新:严禁采用这种方式,根据实践这种方式速度有问题,当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()
        }
    }
}

你可能感兴趣的:(Rust从入门到入门,rust,笔记,散列表)