你們好,我是 lucifer,衆所周知,我是一個小前端 (不是) 。其實,我是 lucifer 的 1379 號迷妹觀察員,我是一粒納米前端。(不要回答,不要回答,不要回答!!!)前端
這是第一次投稿,因此能夠廢話幾句,說一下我爲何作題和寫題解。剛開始作算法題的時候,只是純粹以爲好玩,因此不只沒有刷題計劃,寫題解也只是隨便記下幾筆,幾個月後本身也看不懂的那種。一次偶然機會發現了 lucifer 的明星題解倉庫,是找到了 onepiece 的感受。受他的啓發,我也開始寫些儘可能能讓人看懂的題解,雖然還趕不上 lucifer,但跟本身比總算是有了些進步。git
身爲迷妹觀察員,lucifer 的 91 天學算法固然是不能錯過的活動,如今活動的第二期正在 🔥 熱進行中,有興趣的同窗瞭解一下呀。言歸正傳,跟着 91 課程我再也不是漫無目的,而是計劃清晰,按照課程安排的專題來作題,這樣不只更有利於瞭解某一類題涉及的相關知識,還能熟悉這類題的套路,再次碰見類似題型也能更快有思路。github
廢話就這麼多,如下是正文部分。等等,還有最後一句,上面的"不要回答"是個三體梗,不知道有沒有人 GET 到我。算法
今天給你們帶來一道力扣簡單題,官方題解只給出了一種最優解。本文比較貪心,打算帶你們用四種姿式來解決這道題。編程
<!-- more -->數組
題目地址:https://leetcode-cn.com/probl...app
給定一個字符串 S 和一個字符 C。返回一個表明字符串 S 中每一個字符到字符串 S 中的字符 C 的最短距離的數組。 示例 1: 輸入: S = "loveleetcode", C = 'e' 輸出: [3, 2, 1, 0, 1, 0, 0, 1, 2, 2, 1, 0] 說明: 字符串 S 的長度範圍爲 [1, 10000]。 C 是一個單字符,且保證是字符串 S 裏的字符。 S 和 C 中的全部字母均爲小寫字母。
這是最符合直覺的思路,對每一個字符分別進行以下處理:優化
C
。
咱們須要對每個元素都進行一次擴展操做,所以時間複雜度就是 $N$ * 向兩邊擴展的總時間複雜度。spa
而最壞的狀況是目標字符 C 在字符串 S 的左右兩個端點位置,這個時候時間複雜度是 $O(N)$,所以總的時間複雜度就是 $O(N^2)$指針
JavaScript Code
/** * @param {string} S * @param {character} C * @return {number[]} */ var shortestToChar = function (S, C) { // 結果數組 res var res = Array(S.length).fill(0); for (let i = 0; i < S.length; i++) { // 若是當前是目標字符,就什麼都不用作 if (S[i] === C) continue; // 定義兩個指針 l, r 分別向左、右兩個方向尋找目標字符 C,取最短距離 let l = i, r = i, shortest = Infinity; while (l >= 0) { if (S[l] === C) { shortest = Math.min(shortest, i - l); break; } l--; } while (r < S.length) { if (S[r] === C) { shortest = Math.min(shortest, r - i); break; } r++; } res[i] = shortest; } return res; };
空間換時間是編程中很常見的一種 trade-off (反過來,時間換空間也是)。
由於目標字符 C
在 S
中的位置是不變的,因此咱們能夠提早將 C
的全部下標記錄在一個數組 cIndices
中。
而後遍歷字符串 S
中的每一個字符,到 cIndices
中找到距離當前位置最近的下標,計算距離。
和上面方法相似,只是向兩邊擴展的動做變成了線性掃描 cIndices
,所以時間複雜度就是 $N$ * 線性掃描 cIndices
的時間複雜度。
C
在字符串中出現的次數。因爲 $K <= N$。所以時間上必定是優於上面的解法的。C
出現的次數,這是記錄字符 C
出現下標的輔助數組消耗的空間。實際上,因爲 cIndices
是一個單調遞增的序列,所以咱們可使用二分來肯定最近的 index,時間能夠優化到 $N*logK$,這個就留給各位來解決吧。若是對二分不熟悉的,能夠看看我往期的《二分專題》
JavaScript Code
/** * @param {string} S * @param {character} C * @return {number[]} */ var shortestToChar = function (S, C) { // 記錄 C 字符在 S 字符串中出現的全部下標 var cIndices = []; for (let i = 0; i < S.length; i++) { if (S[i] === C) cIndices.push(i); } // 結果數組 res var res = Array(S.length).fill(Infinity); for (let i = 0; i < S.length; i++) { // 目標字符,距離是 0 if (S[i] === C) { res[i] = 0; continue; } // 非目標字符,到下標數組中找最近的下標 for (const cIndex of cIndices) { const dist = Math.abs(cIndex - i); // 小小剪枝一下 // 注:由於 cIndices 中的下標是遞增的,後面的 dist 也會愈來愈大,能夠排除 if (dist >= res[i]) break; res[i] = dist; } } return res; };
其實對於每一個字符來講,它只關心離它最近的那個 C
字符,其餘的它都無論。因此這裏還能夠用貪心的思路:
從左往右
遍歷字符串 S
,用一個數組 left 記錄每一個字符 左側
出現的最後一個 C
字符的下標;從右往左
遍歷字符串 S
,用一個數組 right 記錄每一個字符 右側
出現的最後一個 C
字符的下標;優化 1
再多想一步,其實第二個數組並不須要。由於對於左右兩側的 C
字符,咱們也只關心其中距離更近的那一個,因此第二次遍歷的時候能夠看狀況覆蓋掉第一個數組的值:
C
字符i - left
> right - i
(i 爲當前字符下標,left 爲字符左側最近的 C
下標,right 爲字符右側最近的 C
下標)若是出現以上兩種狀況,就能夠進行覆蓋,最後再遍歷一次數組計算距離。
優化 2
若是咱們是直接記錄 C
與當前字符的距離,而不是記錄 C
的下標,還能夠省掉最後一次遍歷計算距離的過程。
上面我說了要開闢一個數組。而實際上題目也要返回一個數組,這個數組的長度也剛好是 $N$,這個空間是不可避免的。所以咱們直接利用這個數組,而不須要額外開闢空間,所以這裏空間複雜度是 $O(1)$,而不是 $O(N)$,具體能夠看下方代碼區。
JavaScript Code
/** * @param {string} S * @param {character} C * @return {number[]} */ var shortestToChar = function (S, C) { var res = Array(S.length); // 第一次遍歷:從左往右 // 找到出如今左側的 C 字符的最後下標 for (let i = 0; i < S.length; i++) { if (S[i] === C) res[i] = i; // 若是左側沒有出現 C 字符的話,用 Infinity 進行標記 else res[i] = res[i - 1] === void 0 ? Infinity : res[i - 1]; } // 第二次遍歷:從右往左 // 找出如今右側的 C 字符的最後下標 // 若是左側沒有出現過 C 字符,或者右側出現的 C 字符距離更近,就更新 res[i] for (let i = S.length - 1; i >= 0; i--) { if (res[i] === Infinity || res[i + 1] - i < i - res[i]) res[i] = res[i + 1]; } // 計算距離 for (let i = 0; i < res.length; i++) { res[i] = Math.abs(res[i] - i); } return res; };
直接計算距離:
JavaScript Code
/** * @param {string} S * @param {character} C * @return {number[]} */ var shortestToChar = function (S, C) { var res = Array(S.length); for (let i = 0; i < S.length; i++) { if (S[i] === C) res[i] = 0; // 記錄距離:res[i - 1] + 1 else res[i] = res[i - 1] === void 0 ? Infinity : res[i - 1] + 1; } for (let i = S.length - 1; i >= 0; i--) { // 更新距離:res[i + 1] + 1 if (res[i] === Infinity || res[i + 1] + 1 < res[i]) res[i] = res[i + 1] + 1; } return res; };
Python Code:
class Solution: def shortestToChar(self, S: str, C: str) -> List[int]: pre = -len(S) ans = [] for i in range(len(S)): if S[i] == C: pre = i ans.append(i - pre) pre = len(S) * 2 for i in range(len(S) - 1, -1, -1): if S[i] == C: pre = i ans[i] = min(ans[i], pre - i) return ans
把 C
當作分界線,將 S
劃分紅一個個窗口。而後對每一個窗口進行遍歷,分別計算每一個字符到窗口邊界的距離最小值,並在遍歷的過程當中更新窗口信息便可。
因爲更新窗口裏的「搜索」下一個窗口的操做總共只須要 $N$ 次,所以時間複雜度仍然是 $N$,而不是 $N^2$。
JavaScript Code
/** * @param {string} S * @param {character} C * @return {number[]} */ var shortestToChar = function (S, C) { // 窗口左邊界,若是沒有就初始化爲 Infinity let l = S[0] === C ? 0 : Infinity, // 窗口右邊界 r = S.indexOf(C, 1); const res = Array(S.length); for (let i = 0; i < S.length; i++) { // 計算字符到當前窗口左右邊界的最小距離 res[i] = Math.min(Math.abs(i - l), Math.abs(r - i)); // 遍歷完了當前窗口的字符後,將整個窗口右移 if (i === r) { l = r; r = S.indexOf(C, l + 1); } } return res; };
本文給你們介紹了這道題的四種解法,從直覺思路入手,到使用空間換時間的策略,再到貪心算法思想,最後是一個簡單直白,同時複雜度也是最優的思路。
對於剛開始作題的人來講,"作出來"是首要任務,但若是你有餘力的話,也能夠試試這樣"一題多解",多鍛鍊一下本身。
但不管怎樣,只要你對算法感興趣,必定要考慮關注 lucifer 這個算法燈塔哦。不要嫌我囉嗦,真話不囉嗦。
更多題解能夠訪問:https://github.com/suukii/91-days-algorithm
你們對此有何見解,歡迎給我留言,我有時間都會一一查看回答。更多算法套路能夠訪問個人 LeetCode 題解倉庫:https://github.com/azl3979858... 。 目前已經 37K star 啦。你們也能夠關注個人公衆號《力扣加加》帶你啃下算法這塊硬骨頭。
我整理的 1000 多頁的電子書已限時免費下載,你們能夠去個人公衆號《力扣加加》後臺回覆電子書獲取。