【leetcode】刷題(python & java)解析:【無重複字元的最長字串】 重點【滑動視窗】
給定一個字串,找出不含有重複字元的最長子串的長度。
Given a string, find the length of the longest substring without repeating characters.
示例1
輸入: "abcabcbb"
輸出: 3
解釋: 無重複字元的最長子串是 "abc",其長度為 3。
示例2
輸入: "bbbbb"
輸出: 1
解釋: 無重複字元的最長子串是 "b",其長度為 1。
示例3
輸入: "pwwkew" 輸出: 3 解釋: 無重複字元的最長子串是 "wke",其長度為 3。 請注意,答案必須是一個子串,"pwke" 是一個子序列 而不是子串。
解決方案
方法一:暴力法
思路
逐個檢查所有的子字串,看它是否不含有重複的字元。
演算法
假設我們有一個函式 boolean allUnique(String substring)
,如果子字串中的字元都是唯一的,它會返回true,否則會返回false。 我們可以遍歷給定字串 s
的所有可能的子字串並呼叫函式 allUnique
。 如果事實證明返回值為true,那麼我們將會更新無重複字元子串的最大長度的答案。
現在讓我們填補缺少的部分:
-
為了列舉給定字串的所有子字串,我們需要列舉它們開始和結束的索引。假設開始和結束的索引分別為 i 和 j。那麼我們有 $0 \leq i \lt j \leq n $(這裡的結束索引 j 是按慣例排除的)。因此,使用 i 從0到 n - 1以及 j 從 i+1到 n 這兩個巢狀的迴圈,我們可以枚舉出
s
-
要檢查一個字串是否有重複字元,我們可以使用集合。我們遍歷字串中的所有字元,並將它們逐個放入
set
中。在放置一個字元之前,我們檢查該集合是否已經包含它。如果包含,我們會返回false
。迴圈結束後,我們返回true
。
Java 程式碼
import java.util.HashSet;
import java.util.Set;
public class Demotest {
public static void main(String[] args) {
String s="pwwkew";
int lengthOfLongestSubstring = new Demotest().lengthOfLongestSubstring(s);
System.out.println(lengthOfLongestSubstring);
}
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
for (int i = 0; i < n; i++)
for (int j = i + 1; j <= n; j++)
if (allUnique(s, i, j)) ans = Math.max(ans, j - i);
return ans;
}
//判斷該字元是否是無重複子串
public boolean allUnique(String s, int start, int end) {
Set<Character> set = new HashSet<>();
for (int i = start; i < end; i++) {
//將字串指定索引字元轉為Character型別
Character ch = s.charAt(i);
if (set.contains(ch)) return false;
set.add(ch);
}
return true;
}
}
執行結果為:
3
在提交測試到倒數第二項時,就出現超出時間限制了。
Python程式碼
class Solution(object):
def lengthOfLongestSubstring(self, s):
#獲取字串的長度
s_len=len(s)
result=0
for i in range(s_len):
for j in range(i+1,s_len+1):
if self.allUnique(s,i,j):result= result if result>(j-i) else (j-i)
return result
def allUnique(self,s,start,end):
#建立一個set集合
chs = set()
#將字串轉為list
s_list=list(s)
for i in range(start,end):
if s_list[i] in chs:return False
chs.add(s_list[i])
return True
s=Solution()
test_Str="pwwkew"
result=s.lengthOfLongestSubstring(test_Str)
print(result)
結果
3
可能因為電腦效能問題,這段測試在"qhgnfvcblrqctxzuxeyuipdsalsafroxzwlerphcgxhzwivtntnxlspnfjnlbdkczvgqkouqnbrkknf"自己電腦執行時間為20ms,提交時會出現超出時間限制,而通過不了。
複雜度分析
-
時間複雜度: 。
要驗證索引範圍在 [i, j)內的字元是否都是唯一的,我們需要檢查該範圍中的所有字元。 因此,它將花費$ O(j - i)$的時間。
對於給定的
i
,對於所有$ j \in [i+1, n]$所耗費的時間總和為:因此,執行所有步驟耗去的時間總和為:
-
空間複雜度:,我們需要 O(k)的空間來檢查子字串中是否有重複字元,其中 kk表示
Set
的大小。而 Set 的大小取決於字串 n 的大小以及字符集/字母 m 的大小。 -
根據實驗結果,這個方法也是不合適的。
方法二:滑動視窗
演算法
暴力法非常簡單。但它太慢了。那麼我們該如何優化它呢?
在暴力法中,我們會反覆檢查一個子字串是否含有有重複的字元,但這是沒有必要的。如果從索引 i 到 j - 1之間的子字串已經被檢查為沒有重複字元。我們只需要檢查 s[j]對應的字元是否已經存在於子字串 中。
要檢查一個字元是否已經在子字串中,我們可以檢查整個子字串,這將產生一個複雜度為 的演算法,但我們可以做得更好。
通過使用 HashSet 作為滑動視窗,我們可以用 O(1) 的時間來完成對字元是否在當前的子字串中的檢查。
滑動視窗是陣列/字串問題中常用的抽象概念。 視窗通常是在陣列/字串中由開始和結束索引定義的一系列元素的集合,即 [i, j)(左閉,右開)。而滑動視窗是可以將兩個邊界向某一方向“滑動”的視窗。例如,我們將 [i, j)向右滑動 1個元素,則它將變為 [i+1, j+1)(左閉,右開)。
回到我們的問題,我們使用 HashSet 將字元儲存在當前視窗 [i, j)(最初 j = i)中。 然後我們向右側滑動索引 j,如果它不在 HashSet 中,我們會繼續滑動 j。直到 s[j] 已經存在於 HashSet 中。此時,我們找到的沒有重複字元的最長子字串將會以索引 i 開頭。如果我們對所有的 i 這樣做,就可以得到答案。
以“pwwkew”為例有:
i=0;j=0;set=[];j-i=0
i=0;j=1;set=[p];j-i=1
i=0;j=2;set=[p, w];j-i=2
i=1;j=2;set=[w];j-i=1
i=2;j=2;set=[];j-i=0
i=2;j=3;set=[w];j-i=1
i=2;j=4;set=[w, k];j-i=2
i=2;j=5;set=[e, w, k];j-i=3
i=3;j=5;set=[e, k];j-i=2
Java程式碼
import java.util.HashSet;
import java.util.Set;
public class Demotest {
public static void main(String[] args) {
String s="pwwkew";
int lengthOfLongestSubstring = new Demotest().lengthOfLongestSubstring(s);
System.out.println(lengthOfLongestSubstring);
}
public int lengthOfLongestSubstring(String s) {
int n = s.length();
Set<Character> set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
System.out.println(set);
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))){
//需要注意,這裡是先執行set.add(s.charAt(j)),再執行j++
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
}else {
set.remove(s.charAt(i++));
}
}
return ans;
}
}
結果:
3
Python程式碼
class Solution(object):
def lengthOfLongestSubstring(self, s):
ch=set()
s_len=len(s)
s_list=list(s)
i=0
j=0
result=0
while i<s_len and j<s_len:
if s_list[j] not in ch:
ch.add(s_list[j])
j+=1
result= result if result>(j-i) else (j-i)
else:
ch.remove(s_list[i])
i+=1
return result
s=Solution()
test_Str="pwwkew"
result=s.lengthOfLongestSubstring(test_Str)
print(result)
複雜度分析
- 時間複雜度:O(2n) = O(n),在最糟糕的情況下,每個字元將被 i 和 j 訪問兩次。
- 空間複雜度:O(min(m, n)),與之前的方法相同。滑動視窗法需要 O(k)的空間,其中 k 表示
Set
的大小。而Set的大小取決於字串 n 的大小以及字符集/字母 m 的大小。
方法三:優化的滑動視窗
上述的方法最多需要執行 2n 個步驟。事實上,它可以被進一步優化為僅需要 n 個步驟。我們可以定義字元到索引的對映,而不是使用集合來判斷一個字元是否存在。 當我們找到重複的字元時,我們可以立即跳過該視窗。
也就是說,如果 s[j] 在 [i, j)範圍內有與j'重複的字元,我們不需要逐漸增加 i 。 我們可以直接跳過 [i,j′] 範圍內的所有元素,並將 i 變為j′+1。也就是[j′+1,j)。
Java程式碼
public class Demotest {
public static void main(String[] args) {
String s="pwwkew";
int lengthOfLongestSubstring = new Demotest().lengthOfLongestSubstring(s);
System.out.println(lengthOfLongestSubstring);
}
public int lengthOfLongestSubstring(String s) {
int n = s.length(), ans = 0;
Map<Character, Integer> map = new HashMap<>(); // current index of character
// try to extend the range [i, j]
for (int j = 0, i = 0; j < n; j++) {
if (map.containsKey(s.charAt(j))) {
i = Math.max(map.get(s.charAt(j))+1, i);
}
ans = Math.max(ans, j - i + 1);
//這樣設定鍵值是為了下次能做判斷,如果從0開始,下次迴圈就不可以判斷當前
map.put(s.charAt(j), j );
}
return ans;
}
}
結果:
3
Python程式碼
class Solution(object):
def lengthOfLongestSubstring(self, s):
s_len=len(s)
s_list=list(s)
s_dic={}
i=0
result=0
for j in range(s_len):
if s_list[j] in s_dic.keys():i=i if i>s_dic.get(s_list[j])+1 else s_dic.get(s_list[j])+1
#i為目標字串的首字元的索引,所以要+1
result=result if result>(j-i+1) else (j-i+1)
s_dic[s_list[j]]=j
return result
s=Solution()
test_Str="pwwkew"
result=s.lengthOfLongestSubstring(test_Str)
print(result)
結果
3
複雜度分析
- 時間複雜度:O(n),索引 jj 將會迭代 nn 次。
- 空間複雜度(HashMap):O(min(m, n)),與之前的方法相同。
三種方法執行時間對比
程式碼
from time import clock
class Solution(object):
def lengthOfLongestSubstring(self, s):
#獲取字串的長度
s_len=len(s)
result=0
for i in range(s_len):
for j in range(i+1,s_len+1):
if self.allUnique(s,i,j):result= result if result>(j-i) else (j-i)
return result
def allUnique(self,s,start,end):
#建立一個set集合
chs = set()
#將字串轉為list
s_list=list(s)
for i in range(start,end):
if s_list[i] in chs:return False
chs.add(s_list[i])
return True
def lengthOfLongestSubstring2(self, s):
ch=set()
s_len=len(s)
s_list=list(s)
i=0
j=0
result=0
while i<s_len and j<s_len:
if s_list[j] not in ch:
ch.add(s_list[j])
j+=1
result= result if result>(j-i) else (j-i)
else:
ch.remove(s_list[i])
i+=1
return result
def lengthOfLongestSubstring3(self, s):
s_len=len(s)
s_list=list(s)
s_dic={}
i=0
result=0
for j in range(s_len):
if s_list[j] in s_dic.keys():i=i if i>s_dic.get(s_list[j])+1 else s_dic.get(s_list[j])+1
#i為目標字串的首字元的索引,所以要+1
result=result if result>(j-i+1) else (j-i+1)
s_dic[s_list[j]]=j
return result
s=Solution()
test_Str="abcabcbb"
start =clock()
print("方法一執行結果:{0};執行時間:{1}".format(s.lengthOfLongestSubstring(test_Str),clock()-start))
start=clock()
print("方法二執行結果:{0};執行時間:{1}".format(s.lengthOfLongestSubstring2(test_Str),clock()-start))
start=clock()
print("方法三執行結果:{0};執行時間:{1}".format(s.lengthOfLongestSubstring3(test_Str),clock()-start))
結果:
方法一執行結果:3;執行時間:6.34e-05
方法二執行結果:3;執行時間:2.01e-05
方法三執行結果:3;執行時間:9.899999999999997e-06
[Finished in 0.2s]