1. 程式人生 > >【leetcode】刷題(python & java)解析:【無重複字元的最長字串】 重點【滑動視窗】

【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,那麼我們將會更新無重複字元子串的最大長度的答案。

現在讓我們填補缺少的部分:

  1. 為了列舉給定字串的所有子字串,我們需要列舉它們開始和結束的索引。假設開始和結束的索引分別為 i 和 j。那麼我們有 $0 \leq i \lt j \leq n $(這裡的結束索引 j 是按慣例排除的)。因此,使用 i 從0到 n - 1以及 j 從 i+1到 n 這兩個巢狀的迴圈,我們可以枚舉出 s

    的所有子字串。

  2. 要檢查一個字串是否有重複字元,我們可以使用集合。我們遍歷字串中的所有字元,並將它們逐個放入 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,提交時會出現超出時間限制,而通過不了。

複雜度分析

  • 時間複雜度:O(n3)O(n^3)

    要驗證索引範圍在 [i, j)內的字元是否都是唯一的,我們需要檢查該範圍中的所有字元。 因此,它將花費$ O(j - i)$的時間。

    對於給定的 i,對於所有$ j \in [i+1, n]$所耗費的時間總和為:

    i+1nO(ji)\sum_{i+1}^{n}O(j - i)

    因此,執行所有步驟耗去的時間總和為:

    O(i=0n1(j=i+1n(ji)))=O(i=0n1(1+ni)(ni)2)=O(n3)O\left(\sum_{i = 0}^{n - 1}\left(\sum_{j = i + 1}^{n}(j - i)\right)\right) = O\left(\sum_{i = 0}^{n - 1}\frac{(1 + n - i)(n - i)}{2}\right) = O(n^3)

  • 空間複雜度:O(min(n,m))O(min(n, m)),我們需要 O(k)的空間來檢查子字串中是否有重複字元,其中 kk表示 Set 的大小。而 Set 的大小取決於字串 n 的大小以及字符集/字母 m 的大小。

  • 根據實驗結果,這個方法也是不合適的。

方法二:滑動視窗

演算法

暴力法非常簡單。但它太慢了。那麼我們該如何優化它呢?

在暴力法中,我們會反覆檢查一個子字串是否含有有重複的字元,但這是沒有必要的。如果從索引 i 到 j - 1之間的子字串sijs_{ij}已經被檢查為沒有重複字元。我們只需要檢查 s[j]對應的字元是否已經存在於子字串 sijs_{ij}中。

要檢查一個字元是否已經在子字串中,我們可以檢查整個子字串,這將產生一個複雜度為 O(n2)O(n^2)的演算法,但我們可以做得更好。

通過使用 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]