今天被一道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()}}
}