3、无重复字符的最长子串
题目:给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
方法一:哈希法
思路:从下标0开始逐个取出字符串的字符,将字符作为key,下标作为value存入map中,然后下标逐渐增加,同时更新最长不重复子串的长度,即为map的size和length自身较大的。当map中包含该下标对应的key,即出现重复元素时,则将遍历下标移至map中存储的当前字符的下一个字符开始从新遍历搜索,同时要清空map,便于找下一个重复字符。
public int lengthOfLongestSubstring(String s) {
int length = 0;
Map<Character, Integer> map = new HashMap<>();
for (int i = 0;i<s.length();i++){
//map中不包含该字符,更新最大长度,并将该字符放入map中
if (!map.containsKey(s.charAt(i))){
map.put(s.charAt(i),i);
length = Math.max(length,map.size());
}
//map中包含该字符,则从和当前字符相同的map存储的字符(第一个重复的字符)的下一个开始继续寻找,
//并清空map
else {
i = map.get(s.charAt(i));
map.clear();
}
}
return length;
}
时间复杂度为:O(n^2),最坏情况下 i 需要存字符串开始带结束遍历两次。n 为字符串长度。
空间复杂度最坏为:O(n)
显然耗时较长…
方法二:官方题解之滑动窗口
思路:如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的!这里的原因在于,假设我们选择字符串中的第 k 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 r_k。那么当我们选择第 k+1个字符作为起始位置时,首先从 k+1 到 r_k的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 r_k ,直到右侧出现了重复字符为止。
这样以来,我们就可以使用「滑动窗口」来解决这个问题了:
-
我们使用两个指针表示字符串中的某个子串(的左右边界)。其中左指针代表着上文中「枚举子串的起始位置」,而右指针即为上文中的 r_k;
-
在每一步的操作中,我们会将左指针向右移动一格,表示我们开始枚举下一个字符作为起始位置,然后我们可以不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符。在移动结束后,这个子串就对应着 以左指针开始的,不包含重复字符的最长子串。我们记录下这个子串的长度;
在枚举结束后,我们找到的最长的子串的长度即为答案。
判断重复字符
在上面的流程中,我们还需要使用一种数据结构来判断 是否有重复的字符,常用的数据结构为哈希集合(即 C++ 中的 std::unordered_set,Java 中的 HashSet,Python 中的 set, JavaScript 中的 Set)。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在右指针向右移动的时候,我们往哈希集合中添加一个字符。
public int lengthOfLongestSubstring3(String s) {
// 哈希集合,记录每个字符是否出现过
Set<Character> occ = new HashSet<Character>();
int n = s.length();
// 右指针,初始值为 -1,相当于我们在字符串的左边界的左侧,还没有开始移动
int rk = -1, ans = 0;
for (int i = 0; i < n; ++i) {
if (i != 0) {
// 左指针向右移动一格,移除一个字符
occ.remove(s.charAt(i - 1));
}
while (rk + 1 < n && !occ.contains(s.charAt(rk + 1))) {
// 不断地移动右指针
occ.add(s.charAt(rk + 1));
++rk;
}
// 第 i 到 rk 个字符是一个极长的无重复字符子串
ans = Math.max(ans, rk - i + 1);
}
return ans;
}
时间复杂度:O(N), 其中 N 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。
空间复杂度:O(∣Σ∣), 其中 Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在 [0, 128]内的字符,即 ∣Σ∣=128。我们需要用到哈希集合来存储出现过的字符,而字符最多有 ∣Σ∣ 个,因此空间复杂度为 O(∣Σ∣)。
可以看到时间还是较长,还是可以优化,优化方法,是在评论区找到的代码。
方法三:滑动窗口2
public int lengthOfLongestSubstring4(String s) {
int len = 0;
// 窗口起始位置
int start = 0;
for (int cur = 1; cur < s.length(); cur++) {
// 拿到每个当前指针元素来从窗口起始位置遍历来判断是否已存在该元素
for (int i = start; i < cur; i++) {
if (s.charAt(i) == s.charAt(cur)) {
// 存在截取长度
len = Math.max(len, cur - start);
// 窗口起始点变成出现重复第一个元素的后面
start = i + 1;
// 跳出检查
break;
}
}
}
// 最后比较最大长度和遍历完窗口的长度
return Math.max(s.length() - start, len);
}
时间复杂度:O(n^2)
空间复杂度:O(1)
可以看到这里显然是用了两次循环,时间复杂度肯定是比方法二高的,但是测试的效果却比方法二好!!!
很迷惑,难道是操作set集合比较费时间???
方法三:滑动窗口3
这个方法也是滑动窗口法的一种实现。
public int lengthOfLongestSubstring2(String s) {
int i = 0;
int flag = 0;
int length = 0;
int result = 0;
while (i < s.length()) {
//返回从flag开始到第一次出现当前字符的下标
int pos = s.indexOf(s.charAt(i),flag);
//出现重复字符
if (pos < i) {
if (length > result) {
result = length;
}
//当前最长不重复子串已比剩余子串长是就可以直接返回,不用继续判断了
if (result >= s.length() - pos - 1) {
return result;
}
//计算两个重复字符之间的长度
length = i - pos - 1;
//flag 移到第一个重复字符的下一位
flag = pos + 1;
}
length++;
i++;
}
return length;
}
显然这个方法的时间复杂度是最好的,最坏时间复杂度为O(n)
空间复杂度:O(1)
参考:
Leetcode官方题解