劍指 Offer 48. 最長不含重複字元的子字串(動態規劃/雙指標/hash表)
- 題目描述
請從字串中找出一個最長的不包含重複字元的子字串,計算該最長子字串的長度。 示例1: 輸入: "abcabcbb" 輸出: 3 解釋: 因為無重複字元的最長子串是 "abc",所以其長度為 3。 示例 2: 輸入: "bbbbb" 輸出: 1 解釋: 因為無重複字元的最長子串是 "b",所以其長度為 1。 示例 3: 輸入: "pwwkew" 輸出: 3 解釋: 因為無重複字元的最長子串是"wke",所以其長度為 3。 請注意,你的答案必須是 子串 的長度,"pwke"是一個子序列,不是子串。
- 解法一:hash表+動態規劃
首先可以用動態規劃求解此題。設動態規劃表dp,dp[j]是以s[j]為結尾的“最長不重複字串”的長度。dp[j-1]是第j-1個“最長不重複字串”的長度。假設s[j]上一次出現的字元位置為i,則s[i]和s[j]之間的距離d=j-1,此時,dp[j-1]存在兩種情況:
1.d>dp[j-1],想想這個距離大於dp[j-1],那麼說明s[i]不在以s[j-1]為結尾的“最長不重複字串”裡面,此時dp[j]=dp[j-1]+1
2.d<=dp[j-1],說明s[i]在在以s[j-1]為結尾的“最長不重複字串”裡面,此時dp[j] = j -i。
此外,可以用一個hash map儲存s[j]出現的位置, 如果重複出現,則更新為最近的位置,hash map的時間複雜度為O(1)。
再梳理一下動態規劃的轉移方程:
此時返回時max的最長不重複字串。
程式碼:
class Solution: '''hash表+動態規劃 ''' def lengthOfLongestSubstring(self, s: str) -> int: dp = [] res = tmp = 0 dic = {} for j in range(len(s)): i = dic.get(s[j], -1) dic[s[j]] = j # tmp = tmp + 1 if tmp < j-i else j - i if j -i <= tmp: tmp= j -i elif j -i > tmp: tmp += 1 res = max(res, tmp) return res
- 解法二:hash表+雙指標
這道題,其實最容易想到的就是時間複雜度為O(N^2)的雙指標。類似滑動視窗的思想,用指標p1和p2指向字串頭,用一個滑動視窗始終維護當前不重複的最大長度的字串s[p1:p2+1],當s[p2]不在這個字串中,則從左向右移動p2,否則,從左向右移動p1,同時每次儲存最大s[p1:p2+1]的長度,儲存每次更新的最大值。
但是呢,這樣時間複雜度高啊,p1和p2最差的情況需要O(N^2)。
class Solution: ''' 滑動視窗+雙指標 ''' def lengthOfLongestSubstring(self, s: str) -> int: p1, p2= 0,0 res = 0 while p1 < len(s) and p2 < len(s): if s[p2] not in s[p1:p2]: res = max(res, p2+1 - p1) p2 += 1 else: p1 += 1 return res
因此如果我們在p2遇到重複的s[p2]時,直接將s[p1]指向前一個s[p2]出現的位置,(為什麼要這麼做呢?因此p1原來的位置到前一個s[p2]出現的位置這段位置已經包含了重複的s[p2],因此遍歷這部分是沒有意義的,最大字串一定是在沒有重複的)。這樣的話,我們可以用一個hash表儲存s[p2]的位置,如果下一次遇到s[p2]時,則將p1的指向s[p2]出現的前一個位置。
最大字串長度則為每次的p2-p1的最大值。
此時時間複雜度O(N),空間複雜度O(N).
程式碼中i表示p1,j表示p2.
class Solution: ''' hash表+雙指標 ''' def lengthOfLongestSubstring(self, s: str) -> int: res, i , Map =0, -1, {} for j in range(len(s)): if s[j] in Map: i = max(Map[s[j]], i) #求i的新位置 Map[s[j]] = j #雜湊表記錄s[j]的索引j res = max(res, j - i) return res