1.題目
給定一個字符串,請你找出其中不含有重複字符的 最長子串 的長度。
示例 1:
輸入: “abcabcbb”
輸出: 3
解釋: 因爲無重複字符的最長子串是 “abc”,所以其長度爲 3。
示例 2:
輸入: “bbbbb”
輸出: 1
解釋: 因爲無重複字符的最長子串是 “b”,所以其長度爲 1。
示例 3:
輸入: “pwwkew”
輸出: 3
解釋: 因爲無重複字符的最長子串是 “wke”,所以其長度爲 3。
請注意,你的答案必須是 子串 的長度,“pwke” 是一個子序列,不是子串。
來源:力扣(LeetCode)
鏈接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters
著作權歸領釦網絡所有。商業轉載請聯繫官方授權,非商業轉載請註明出處。
2.解法概述
該題目有三種解法:
(1)暴力法(大力出奇跡?):先找出所有的子串(時間複雜度爲O(n^2)),再判斷字串是否有重複字符(可以用HashSet,時間複雜度爲O(n))。總的時間複雜度爲O(n^3).
(2)滑動窗口:分爲左指針i和右指針j, 同樣有一個HashSet. j向右移動, 如果set中無重複字符, 則將j所指字符加入set; 如果j所指字符重複, 則將i所指字符從set中去除, 同時i向右移動, 直至j所指字符不重複. 因爲i, j 兩個指針移動, 所以時間複雜度最大爲O(2n).
(3)滑動窗口優化:在(2)的基礎上, 當j所指字符重複時, i直接移動到子串中重複的字符的位置的後一位, 然後j繼續向後, 從而時間複雜度優化爲O(n).
3.暴力法
//方法一:暴力法
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int ans = 0;
//找到所有子串
for (int i = 0; i < n; i++)
//注意,調用allUnique(s, i, j)時取不到j,這樣j-i纔是字串長,所以j <= n
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<>();
//取不到end即j的下標
for (int i = start; i < end; i++) {
//String.charAt(int index) 返回指定索引的 char值。
Character ch = s.charAt(i);
if (set.contains(ch)) return false;
set.add(ch);
}
return true;
}
4.滑動窗口
//方法二:滑動窗口 時間複雜度:O(2n)
public int SlidingWindow(String s){
int n = s.length();
int max = 0,i = 0,j = 0;
Set<Character> set = new HashSet<>();
while (i < n && j < n){
// 如果j下標處不重複,向後擴展滑動窗口
if ( !set.contains(s.charAt(j)) ){
//j下標處字符加入HashSet並且向後+1
set.add(s.charAt(j++));
//計算長度, j-i 纔是字串長度
max = Math.max(max, j - i);
}
//j下標處重複,滑動窗口i向後+1
else{
//將set集合中的i下標字符移除,並且+1,在下一次循環中檢查j下標處字符是否重複
set.remove(s.charAt(i++));
}
}
5.滑動窗口優化
//方法三 優化的滑動窗口
public int OptimizedSlidingWindow(String s){
int n = s.length();
int max = 0;
// 字符Character是鍵,下標Integer是值, 記錄的是每一個字符最後出現的位置
Map<Character, Integer> map = new HashMap<>();
//嘗試向後滑動窗口,i在左,j在右
for (int i = 0, j = 0; j < n; j++) {
//如果j在當前滑動窗口中,直接將i移到重複的字符後一個
if (map.containsKey(s.charAt(j))){
//爲何這裏取最大值?
//因爲在i之前的不重複字符仍在map中,如果這時出現它(如下面的a),i會回溯,
// 這是爲了防止i回溯
//比如當s="abba"時,
i = Math.max(map.get(s.charAt(j))+1, i);
//i = map.get(s.charAt(j)) + 1;
}
//不在,就將j加入map(也可以是更新位置)
map.put(s.charAt(j), j);
//計算長度
max = Math.max(max, j-i+1);
}
return max;
}
6.測試代碼
@Test
public void test(){
int length;
long startTime1 = System.currentTimeMillis();
length = lengthOfLongestSubstring("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\\\"#$%&'()*+,-./:;<=>?@[\\\\]^_`{|}~");
System.out.println("最長子串:暴力法:長度"+length+" 時間:"+(System.currentTimeMillis() - startTime1)+"ms");
long startTime2 = System.currentTimeMillis();
length = SlidingWindow("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\\\"#$%&'()*+,-./:;<=>?@[\\\\]^_`{|}~");
System.out.println("最長子串:滑動窗口法:長度"+length+" 時間:"+(System.currentTimeMillis() - startTime2)+"ms");
long startTime3 = System.currentTimeMillis();
length = OptimizedSlidingWindow("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\\\"#$%&'()*+,-./:;<=>?@[\\\\]^_`{|}~");
System.out.println("最長子串:優化的滑動窗口法:長度"+length+" 時間:"+(System.currentTimeMillis() - startTime3)+"ms");
}