LeetCode 无重复字符的最长子串

题目

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

示例

input: "abcabcbb"
output: 3

input: "bbbbb"
output: 1

input: "pwwkew"
output: 3

解法

暴力解法( Python )

class Solution:
    def lengthOfLongestSubstring(self, s:str) -> int:
        maxlen = 0;
        slist = list(s)
        nums = len(slist)
        for i in range(0, nums):
            for j in range(nums - i, -1, -1) :
                tmp = slist[i : (i + j)]
                k = len(tmp)
                if(k > len(set(tmp))):
                    continue;
                else:
                    if(k > maxlen):
                        maxlen = k
        return maxlen
基本思想

  之所以暴力,是因为这种算法遍历了所有的子序列,判断是否有重复元素。

复杂度分析

  时间复杂度为O(N^2), 这种方法用两层循环遍历了所有的子字符串,并且使用set判断每个字符串的元素是不是都是唯一的。空间复杂度为O(N),因为我们需要用一个set来判断是不是有重复字符。

​  为什么这么慢呢?因为每个子字符串里未出现重复元素之前的元素都被重复判断了, 以'abca'为例,子序列'abc'已经被判断过没有重复元素了,但是在判断'abca'时仍会将前3个元素查找一遍,耗费大量时间。实际上只需要查找新的元素a是否已经出现过了即可,由此引出了滑动窗口技术。

滑动窗口( Python )

class Solution:
    def lengthOfLongestSubstring(self, s:str) -> int:
        maxlen, i, j = 0, 0 ,0
        c_set = set()
        while(j < len(s)):
            if(s[j] not in c_set):
                c_set.add(s[j])
                j = j + 1
                maxlen = max(maxlen, j - i)
            else:
                c_set.remove(s[i])
                i = i + 1
        return maxlen
基本思想

  关键在于怎么判断滑动窗口新增字符是否已经出现过了,这里可以使用set。如果新增字符不在前一子序列的set里,那么将滑动窗口右侧边界j加一,否则,移除位于最左边界的字符,左边界i加一。
  这里有个小问题,左边界i会大于右边界j吗?每次集合增大都会使得j右移,每次集合减少都会使得i右移,但是集合增大之后才会导致集合减少,所以不会超过。

复杂度分析

  滑动窗口的目的就在于消除内层循环,在最差情况下(比如'aaaa'),ij会经过每一个字符,此时时间复杂度最差为O(2N) = O(N)。空间的花销在于set的使用,set最大长度等于字符集的大小,即O(N)

参考资料
  • Window Sliding Technique

滑动窗口 ( Java )

public class Solution {
    public int lengthOfLongestSubstring(String s) {
        int n = s.length();
        Set<Character> 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;
    }

  因为没有使用过 Java 的 HashSet,出于学习的目的,所以这里给出官方使用HashSet解题的代码,原理同上。

HashSet 博客参考:

  1. Java集合 — HashSet底层实现和原理(源码解析)
  2. java提高篇(二四)-----HashSet

HashTable( Python )

class Solution:
    def lengthOfLongestSubstring(self, s:str) -> int:
        maxlen = 0
        d = {}
        count = 0
        for i in range(0, len(s)):
            if(s[i] in d):
                count = max(count, d[s[i]])
            d[s[i]] = i + 1;
            maxlen = max(maxlen, i - count + 1)
        return maxlen
基本思想

​  其主要思想依然还是滑动窗口, 但经过了优化。之前的滑动窗口每次遇到重复字符串左边界只会往右移动一格,而优化后会直接跳过整个重复区域。在这里i作为右边界,而count作为左边界,始终指向两个重复字符中左字符的位置(比如cgbafbjklb,当i到第二个b时,count会直接到索引为 2 的位置, 当i到第三个b时,count会直接到索引为 5 的位置)。这里关键在于count, 只要遇到重复字符,count就会更新。除此之外,HashTablekey对应的value不是它们本身的索引,而是索引加一,并且求maxlen时窗口大小也要加一,本质上是因为i是从 0 开始的,当遇到类似'b' 的单字符串时,i - count = 0, 与实际不符。

复杂度分析

  时间复杂度为O(N), 空间复杂度为也为O(min(m,n)), m为字符集大小,n为字符串长度。

你可能感兴趣的:(LeetCode)