3. 无重复字符的最长子串

题目

原题链接:https://leetcode-cn.com/problems/longest-substring-without-repeating-characters/

给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。

输入: "abcabcbb"
输出: 3 
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

输入: "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。

请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

解题思路

以字符串\(\texttt{abcabcbb}\)为例,其中括号中表示选中的字符以及最长的字符串:

  • \(\texttt{(a)bcabcbb}\)开始的最长字符串为\(\texttt{(abc)abcbb}\)
  • \(\texttt{a(b)cabcbb}\)开始的最长字符串为\(\texttt{a(bca)bcbb}\)
  • \(\texttt{ab(c)abcbb}\)开始的最长字符串为\(\texttt{ab(cab)cbb}\)
  • \(\texttt{abc(a)bcbb}\)开始的最长字符串为\(\texttt{abc(abc)bb}\)
  • \(\texttt{abca(b)cbb}\)开始的最长字符串为\(\texttt{abca(bc)bb}\)
  • \(\texttt{abcab(c)bb}\)开始的最长字符串为\(\texttt{abcab(cb)b}\)
  • \(\texttt{abcabc(b)b}\)开始的最长字符串为\(\texttt{abcabc(b)b}\)
  • \(\texttt{abcabcb(b)}\)开始的最长字符串为\(\texttt{abcabcb(b)}\)

如果依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的

假设选择字符串中的第\(k\)个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为\(r_k\)。那么当选择第\(k+1\)个字符作为起始位置时(移除第\(k\)个字符hashSet.remove(s.charAt(i - 1));),首先从\(k+1\)\(r_k\)的字符显然是不重复的,并且由于少了原本的第\(k\)个字符,可以尝试继续增大\(r_k\),直到右侧出现了重复字符为止(不必从第\(k+2\)个字符开始遍历)

因此,可以使用滑动窗口来解决这个问题:

  • 使用两个指针表示字符串中的某个子串(的左右边界)。其中左指针代表着上文中枚举子串的起始位置,而右指针即为上文中的\(r_k\)

  • 在每一步的操作中

    • 左指针向右移动一格,表示开始枚举下一个字符作为起始位置
    • 不断地向右移动右指针,但需要保证这两个指针对应的子串中没有重复的字符
    • 在移动结束后,这个子串就对应着以左指针开始的,不包含重复字符的最长子串。记录下这个子串的长度;
  • 在枚举结束后,找到的最长的子串的长度即为答案。

判断重复字符

还需要使用一种数据结构来判断是否有重复的字符,常用的数据结构为哈希集合(HashSet)。在左指针向右移动的时候,从哈希集合中移除一个字符,在右指针向右移动的时候,往哈希集合中添加一个字符

代码实现思路:

  1. 遍历字符;
  2. 以当前字符为起点,向其右侧遍历字符,如果在HashSet中不存在遍历到的字符,则存入HashSet中(避免重复),直到遇到HashSet中存在的字符,记录截至位置rk
  3. 向右侧移动起点,重复步骤二。

代码实现

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashSet;
import java.util.Set;

/**
 * 3.无重复字符的最长子串
 * @date 2021-3-7
 * @author chenzufeng
 */
public class No3_LengthOfLongestSubstring {

    public static void main(String[] args) throws IOException {
        BufferedReader  bufferedReader = new BufferedReader(new InputStreamReader(System.in));
        System.out.println(getLengthOfLongestSubstring(bufferedReader.readLine()));
    }

    public static int getLengthOfLongestSubstring(String string) {
        // 使用HashSet来判断是否有重复的字符
        Set<Character> hashSet = new HashSet<>();
        int lengthOfString = string.length();
        // 当前最长字串的截止位置
        int endIndexOfLongestSubstring = 0;
        int result = 0;

        // 以字符串每一个字符为起点,向右侧遍历,找到不重复的长度
        for (int i = 0; i < lengthOfString; i++) {
            // 当选择下一个字符作为起始位置时,需要去除前一个字符
            if (i != 0) {
                hashSet.remove(string.charAt(i - 1));
            }

            while (endIndexOfLongestSubstring < lengthOfString &&
                       ! hashSet.contains(string.charAt(endIndexOfLongestSubstring))) {
                // 注意这里是endIndexOfLongestSubstring
                hashSet.add(string.charAt(endIndexOfLongestSubstring));
                endIndexOfLongestSubstring++;
            }

            // 跳出while循环后,此时endIndexOfLongestSubstring对应的是重复的字符
            result = Math.max(result, endIndexOfLongestSubstring - i);
        }

        return result;
    }
}

复杂度分析

时间复杂度:\(O(N)\),其中 \(N\) 是字符串的长度。左指针和右指针分别会遍历整个字符串一次。

空间复杂度:\(O(|\Sigma|)\),其中\(\Sigma\)表示字符集(即字符串中可以出现的字符),\(|\Sigma|\)表示字符集的大小。在本题中没有明确说明字符集,因此可以默认为所有 ASCII 码在\([0, 128)\)内的字符,即\(|\Sigma| = 128\)。我们需要用到哈希集合来存储出现过的字符,而字符最多有\(|\Sigma|\)个,因此空间复杂度为 \(O(|\Sigma|)\)

posted @ 2021-03-08 10:46  chenzufeng  阅读(247)  评论(0)    收藏  举报