题目
Given a string, find the length of the longest substring without repeating characters.
举例
Given "abcabcbb", the answer is "abc", which the length is 3.
Given "bbbbb", the answer is "b", with the length of 1.
Given "pwwkew", the answer is "wke", with the length of 3.
Note that the answer must be a substring, "pwke" is a subsequence and not a substring.
思路一:暴力求解
这个思路是最容易想到,最容易实现。效率也是最低的。但是也不失为一种解决方案。只是没有那么优秀。
就是将字符串拆解为所有可能的情况,然后对每一种情况判断是否有重复字符。
java代码
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;
}
复杂度分析
时间复杂度:O(n^3)
空间复杂度:O(min(n,m)),取决于我们所使用的Set的大小
思路二:滑动窗口
滑动窗口是数组/字符串问题中常用的抽象概念。 窗口通常是在数组/字符串中由开始和结束索引定义的一系列元素的集合,即 [i, j)[i,j)(左闭,右开)。而滑动窗口是可以将两个边界向某一方向“滑动”的窗口。例如,我们将 [i, j)[i,j) 向右滑动 11 个元素,则它将变为 [i+1, j+1)[i+1,j+1)(左闭,右开)。
思路一,暴力求解时间复杂度实在太高,而且从实现思路我们很容易发现,有很多不必要的判断。也就是说,有很多字符串根本没有判断的必要。比如:对于abcbd这样的字符串,可以拆解为总共15个字串。按照暴力求解的方法,我们需要判断这15个字串是否有重复字符。但是实际上,我们并不需要如此,因为我们可以从已经判断过的字串中,获取到一些必要的信息,来决定是否需要判断后续字串的重复性。
比如:对于abcbd这个字符串,当abc字串不重复,判断到第二个b的时候,发现和abc字串有重复字符,那么我们可以以c这个字符作为新的子串起始点,这样就避免了很多不必要的判断。
java代码
public int lengthOfLongestSubstring2(String s) {
int n = s.length();
Set set = new HashSet<>();
int ans = 0, i = 0, j = 0;
while (i < n && j < n) {
// try to extend the range [i, j]
if (!set.contains(s.charAt(j))){
set.add(s.charAt(j++));
ans = Math.max(ans, j - i);
}
else {
set.remove(s.charAt(i++));
}
}
return ans;
}
复杂度分析:
时间复杂度:O(n),最糟糕的情况就是字符串全是同一个字符的情况
空间复杂度:O(min(n,m))
个人的思考
个人思考,这个方法和暴力法相比,通过滑动窗口的特性,避免了很多不必要的判断。大大提升了效率。
思路三:优化窗口方法
上述方法还有优化空间,在滑动窗口的滑动过程中,左边i的值是一个一个递增的,实际上,对于abcdbc这样字符串来说,当右边的j值指向第二个b的时候,左边的i值可以直接从a跳跃到c。可以进一步减少判断步骤。
java代码
public int lengthOfLongestSubstring3(String s) {
int n = s.length(), ans = 0;
Map map = new HashMap<>();
for (int j = 0, i = 0; j < n; j++) {
if (map.containsKey(s.charAt(j))) {
i = Math.max(map.get(s.charAt(j)), i);
}
ans = Math.max(ans, j - i + 1);
map.put(s.charAt(j), j + 1);
}
return ans;
}
复杂度分析
时间复杂度:O(n)
空间复杂度:O(min(m,n))
思路四:针对特定字符集(假设字符集为ASCII 128)的优化
我们可以用数组替换思路三中的Map,因为使用map,使用set的核心,就是利用hash算法的特性,达到快速找到字符对应的index的目的。如果知道了字符集,完全可以利用数组作为直接访问表来完成这个过程。这个时候,hash算法的hash相当于是一个一次函数 y=x。
常用的表如下所示:
int [26] 用于字母 ‘a’ - ‘z’或 ‘A’ - ‘Z’
int [128] 用于ASCII码
int [256] 用于扩展ASCII码
java代码
public int lengthOfLongestSubstring4(String s) {
int n = s.length(), ans = 0;
int[] index = new int[128];
for (int j = 0, i = 0; j < n; j++) {
i = Math.max(index[s.charAt(j)], i);
ans = Math.max(ans, j - i + 1);
index[s.charAt(j)] = j + 1;
}
return ans;
}
复杂度分析
时间复杂度:O(n)
空间复杂度:O(m),m是字符集大小
总结
通过对于已经处理过的数据记录必要信息的方式,可以避免很多不必要的操作。比如,记录好重复字符的位置,重复的字符相关信息,可以避免重复判断很多字符串。达到优化目的。