题目
- 给定一个字符串,请你找出其中不含有重复字符的最长子串的长度
实例1
输入: “abcabcbb”
输出: 3
解释: 因为无重复字符的最长子串是 “abc”,所以其长度为 3。
示例 2:
输入: “bbbbb”
输出: 1
解释: 因为无重复字符的最长子串是 “b”,所以其长度为 1。
示例 3:
输入: “pwwkew”
输出: 3
解释: 因为无重复字符的最长子串是 “wke”,所以其长度为 3。
请注意,你的答案必须是 子串 的长度,“pwke” 是一个子序列,不是子串。
解析
方法一:暴力法
暴力法的时间复杂度较高,有可能会出现超时的现象,这里不推荐使用
思路
- 逐个检查所有的子字符串,看他是否不含有重复的字符
算法
- 假设我们有一个函数,该函数用来检查子子字符串的所有字符是否是唯一的,若是唯一的他会返回true,否则返回false。我们可以遍历给定字符串s的所有可能的字符串并调用检查字符子串是否唯一的函数,如果返回值为true,则得到了这个满足条件的一个字符子串,且可以得到其当前长度,与之前记录的子串长度做对比,取最大的子串长度,将值记录在指定变量中
- 为了枚举给定字符串的所有的子字符串,我们需要枚举他们开始和结束的索引,假设开始和结束的索引分别为i和j。那么我们有0 <= i < j <= n。因此,使用i从0到n-1以及j从i+1到n的这两个嵌套的循环,我们可以枚举出s的所有子字符串。
- 要检查一个字符串是否有重复字符,我们可以使用集合。我们遍历字符串中的所有字符,并且将其逐个放到set中,在放置一个字符之前,我们检查该集合是否已经包含他。如果包含,表示的是有重复的字符,返回false,如果不包含,继续循环,直至循环结束,返回true
代码实现
public class Solution {
/**
* 传过来的参数是字符串 用来获取s字符串中不重复的子串的最大的长度
* @param s
* @return
*/
public int lengthOfLongestSubstring(String s) {
// 字符串的长度
int n = s.length();
// 当前最大的子串长度是0
int ans = 0;
// 从0 开始,到最后一个结束
for (int i = 0; i < n; i++) {
// 从i+1开始,到最后一个下标+1结束(因为j是作为终止下标的,取前不取后,所以后面的判断是<j的,不存在下标越界)
for (int j = i + 1; j <= n; j++){
// 判断字符串s的[i,j)是否有重复的字符
if (allUnique(s, i, j)) {
// 选取得到最大的长度
ans = Math.max(ans, j - i);
}
}
}
return ans;
}
/**
* 检查字符子串是否有重复的字符
* @param s 字符串
* @param start 当前字符子串的起始下标
* @param end 当前字符子串的终止下标
* @return
*/
public boolean allUnique(String s, int start, int end) {
// 定义一个Set,该集合用来保存子串的字符,判断是否有重复
Set<Character> set = new HashSet<Character>();
// 根据起始下标和终止下标,进行遍历
for (int i = start; i < end; i++) {
// 取到当前下标对应的字符
Character ch = s.charAt(i);
// 如果集合中有这个字符了,表示已经重复了
if (set.contains(ch)) {
// 子串重复,直接返回false
return false;
}
// 若当前子串这个i对应的字符,不在set中,则不重复,添加到set中,继续遍历
set.add(ch);
}
// 返回true,表示字符串s的start到end下标期间没有重复的字符(下标取前不取后)
return true;
}
}
复杂度分析
- 时间复杂度: O(n^3)
- 空间复杂度: O(min(n,m))
方法二:滑动窗口
算法
- 暴力算法非常的简单,但是太慢了,我们该如何优化他?
- 在暴力法中,我们会反复检查一个子字符串中是否含有重复的字符,但这是没有必要的。如果从索引i到j-1之间的子字符S(i, j)已经被检查为没有重复的字符。我们只需要检查S(j)对应的字符是否已经存在于子字符串S(i, j)中
- 要检查一个字符是否已经存在子字符串中,我们会检查整个子字符串,这将产生一个时间复杂度为O(n^2)的算法,但是我们可以做的更好。
- 利用HashSet作为滑动窗口,我们可以用O(1)的时间来完成对字符是否在当前的子字符串中的检查
- 滑动窗口是数组/字符串问题中常用的抽象概念。窗口通常是在数组/字符串中由开始和索引定义的一系列元素的集合,即[i, j)(左闭,右开)。而滑动窗口是可以将两个边界向某一个方向“滑动”的窗口。例如,我们将[i, j)向右滑动了一个元素,则它将变为[i+1, j+1)(左闭右开)
- 回到我们的问题,我们使用HashSet将字符存储在当前窗口[i, j)(最初j==i)中。然后我们向右侧滑动索引j,如果他不在HashSet中,我们会继续滑动j。直到s[j]已经存在于HashSet中,此时,我们找到没有重复的字符的最长子字符串将会以i开头。
- 那么如何确定下一次的i的位置呢?我们只需要移动i的下标,就是类似将左窗口向 j 移动,若s[i1]等于s[j]则下一次开始的i的位置为i1 + 1
java代码实现
/**
* 用滑动窗口解决寻找字符串s中最大的不重复子串的长度
* @param s
* @return
*/
public int lengthOfLongestSubstring(String s) {
// 字符串的长度
int n = s.length();
// 该集合用来存储子串
Set<Character> set = new HashSet<Character>();
// ans是用来存储最大子串的长度的
// i是窗口的左边界
// j是窗口的右边界
int ans = 0, i = 0, j = 0;
// 当左边界和右边界都小于字符串长度的时候,进入循环
while (i < n && j < n) {
// 判断当前窗口右边界的元素是否在set集合中
if (!set.contains(s.charAt(j))){
// 若不在集合中,表示没有重复,则将其放到集合中
set.add(s.charAt(j++));
// 计算长度
ans = Math.max(ans, j - i);
}
else {
// 如果当前的右边界的元素在集合中
// 我们需要跳到,当前集合中存这个s.chartAt(j)的元素的位置,所以有了下面的方法
// 如果当前元素(s.chartAt(j))和当前set的元素不相等,表示的是不是我们要找的下标,将其从set中删除
// 删除set的原因就是如果从索引i到j-1之间的子字符S(i, j)已经被检查为没有重复的字符。我们只需要检查S(j)对应的字符是否已经存在于子字符串S(i, j)中
// Set表示的就是窗口
set.remove(s.charAt(i++));
}
}
return ans;
}
复杂度分析
- 时间复杂度:O(2n) = O(n),在最糟糕的情况下,每个字符将被i和j访问两次(s[i1] = s[j] ; i1==j -1)
- 空间复杂度:O(min(m,n)),与之前的方法相同,滑动窗口法,需要O(k)的空间,其中k表示Set的大小,而Set的大小取决于字符串n的大小以及字符集/字母m的大小
方法三:优化的滑动窗口
- 上述的方法最多需要执行2n个步骤,事实上,他可以被进一步的优化为只需要n个步骤。我们可以定义字符到索引的映射,而不是使用集合来判断一个字符是否存在。当我们找到重复的字符的时候,我们可以立即跳过该窗口。
- 也就是说,如果s[j]在[i, j)范围内有与j1重复的字符,我们不需要不断的增加i,我们可以直接跳过[i, j1]范围内的所有元素,并将i变为j1 + 1
java代码实现
/**
* 优化滑动窗口的解法
* @param s 当前传进来的字符串
* @return
*/
public int lengthOfLongestSubstring3(String s) {
// n表示字符串的长度
// ans表示的是最大的长度
int n = s.length(), ans = 0;
// 窗口这里用Map来表示
Map<Character, Integer> map = new HashMap<>();
// i是左窗口边界,j是右窗口边界
for (int j = 0, i = 0; j < n; j++) {
// 判断当前窗口是否存在当前遍历到的这个元素
if (map.containsKey(s.charAt(j))) {
// 当前元素重复,在窗口中存在
// 移动左边界,将其移动到相同字符的下一个位置和i当前位置中更靠右的位置,这样就是为了防止i向左移动
i = Math.max(map.get(s.charAt(j)), i);
}
// 如果不存在窗口中,则将其右窗口向右移动,左窗口不动
ans = Math.max(ans, j - i + 1);
// 存入map,其中key是元素的值,value是当前值的下一个坐标
map.put(s.charAt(j), j + 1);
}
return ans;
}