最长不含重复字符的子字符串(滑动窗口/双指针,动态规划)

剑指 Offer 48. 最长不含重复字符的子字符串

题目:请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。

思路
  长度为 N N N的字符串共有 ( 1 + N ) N 2 \frac{(1 + N)N}{2} 2(1+N)N个子字符串(复杂度为 O ( N 2 ) O(N^2) O(N2) ),判断长度为 N N N的字符串是否有重复字符的复杂度为 O ( N ) O(N) O(N) ,因此本题使用暴力法解决的复杂度为 O ( N 3 ) O(N^3) O(N3)

★1.滑动窗口/双指针解法

  题目中要求答案必须是子串的长度,意味着子串内的字符在原字符串中一定是连续的。可以将答案看作原字符串的一个滑动窗口,并维护窗口内不能有重复字符,同时更新窗口的最大值。
在这里插入图片描述
算法流程

  • 初始化头尾指针 h e a d head head t a i l tail tail
  • t a i l tail tail指针右移,判断 t a i l tail tail指向的元素是否在 [ h e a d : t a i l ] [head:tail] [head:tail]的窗口内;
  • 如果窗口中没有该元素,则将该元素加入窗口,同时更新窗口长度最大值, t a i l tail tail指针继续右移;如果窗口中存在该元素,则将 h e a d head head指针右移,直到窗口中不包含该元素。
  • 返回窗口长度的最大值。

代码实现

    //滑动窗口/双指针法,时间复杂度O(n^2),空间复杂度O(1)
    int lengthOfLongestSubstring(string s) {
    
    
        if(s.size() < 2) return s.size();
        int maxSlidingWinLen = 0;
        int head = 0, tail = 0;
        for(; tail < s.size(); ++tail) {
    
    
            //获取左边与s[tail]相同且相邻的下标
            int sameCharLoc = inSlidingWin(s, tail - head + 1, tail);
            if(sameCharLoc == -1) 
                //若是不存在,则结果取当前滑动窗口和历史滑动窗口长度最大值中的较大值
                maxSlidingWinLen = max(maxSlidingWinLen, tail - head + 1);
            else {
    
    
                //若是存在,则结果取除去相同字母区间后窗口长度和历史滑动窗口长度最大值中的较大值
                maxSlidingWinLen = max(maxSlidingWinLen, tail - sameCharLoc);
                //更新head的值
                head = sameCharLoc + 1;
            }
        }
        return maxSlidingWinLen;
    }

    //根据滑动窗口大小和当前tail的位置,找出tail前相邻的s[tail]的字母位置
    //若是存在,则返回字母的位置下标;若是不存在,返回-1;
    int inSlidingWin(const string& s, int slidingWinLen, int tail) {
    
    
        int head = tail - slidingWinLen + 1;
        while(head < tail && s[head] != s[tail]) head++;
        return head == tail ? -1 : head;
    }

如上解法在每次循环中,需调用inSlidingWin函数确定head的可能的新位置,若是使用哈希表记录每个字符的下一个索引,直接查找哈希表,则程序实现可以简单得多,实现如下:

    //(滑动窗口)双指针+哈希表,即优化过的滑动窗口,时间复杂度O(n),空间复杂度O(1)
    int lengthOfLongestSubstring(string s) {
    
    
        if(s.size() == 0) return 0;
        unordered_map<char, int> map;
        int ptr1 = -1, ptr2 = 0;
        int ret = 0;
        for(;ptr2 < s.size(); ++ptr2) {
    
    
            if(map.find(s[ptr2]) != map.end()) 
                //若是不取最大值,回造成ptr1向左移动的情况,如"abba"
                ptr1 = max(map.find(s[ptr2])->second, ptr1); 
            map[s[ptr2]] = ptr2; //更新哈希表中的记录
            ret = max(ret, ptr2 - ptr1); //更新结果
        }
        return ret;
    }

2.动态规划解法

  • 状态定义: 设动态规划列表 d p dp dp d p [ j ] dp[j] dp[j]代表以字符 s [ j ] s[j] s[j]为结尾的 “最长不重复子字符串” 的长度;
  • 转移方程: 固定右边界 j j j ,设字符 s [ j ] s[j] s[j]左边距离最近的相同字符为 s [ i ] s[i] s[i],即 s [ i ] = s [ j ] s[i] = s[j] s[i]=s[j];
    ①当 i < 0 i < 0 i<0,即 s [ j ] s[j] s[j]左边无相同字符,则 d p [ j ] = d p [ j − 1 ] + 1 dp[j] = dp[j-1] + 1 dp[j]=dp[j1]+1
    ②当 d p [ j − 1 ] < j − i dp[j - 1] < j - i dp[j1]<ji,说明字符 s [ i ] s[i] s[i]在子字符串 d p [ j − 1 ] dp[j-1] dp[j1]区间之外 ,则 d p [ j ] = d p [ j − 1 ] + 1 dp[j] = dp[j - 1] + 1 dp[j]=dp[j1]+1
    ③当 d p [ j − 1 ] ≥ j − i dp[j - 1] \geq j - i dp[j1]ji ,说明字符 s [ i ] s[i] s[i]在子字符串 d p [ j − 1 ] dp[j-1] dp[j1]区间之中 ,则 d p [ j ] dp[j] dp[j]的左边界由 s [ i ] s[i] s[i]决定,即 d p [ j ] = j − i dp[j] = j - i dp[j]=ji
    注意:当 i < 0 i<0 i<0时,由于 d p [ j − 1 ] ≤ j dp[j - 1] \leq j dp[j1]j恒成立,因而 d p [ j − 1 ] < j − i dp[j - 1] < j - i dp[j1]<ji恒成立,因此分支①和②可被合并。
  • 返回值 max ⁡ ( d p ) \max(dp) max(dp),即全局的 “最长不重复子字符串” 的长度。
  • 空间复杂度优化
    ①由于返回值是取 d p dp dp列表最大值,因此可借助变量 t m p tmp tmp存储 d p [ j ] dp[j] dp[j],变量 r e s res res每轮更新最大值即可。
    ②此优化可节省dp列表使用的 O ( N ) O(N) O(N)大小的额外空间。

d p [ j ] = { d p [ j − 1 ] + 1   , d p [ j − 1 ] < j − i j − i   , d p [ j − 1 ] ≥ j − i dp[j] = \begin{cases} dp[j - 1] + 1 & \ , dp[j-1] < j - i \\ j - i & \ , dp[j-1] \geq j - i \end{cases} dp[j]={ dp[j1]+1ji ,dp[j1]<ji ,dp[j1]ji

  • 代码实现
    //方法一
    //动态规划+哈希表,,时间复杂度O(n),空间复杂度O(1)
    int lengthOfLongestSubstring(string s) {
    
    
        if(s.length() == 0) return 0;
        unordered_map<char, int> map;
        //ret记录最长的不含重复字符的子字符串长度
        //tmp记录f[i-1]的值
        int ret = 0, tmp = 0;
        for(int j = 0; j < s.size(); ++j) {
    
    
            if(map.find(s[j]) != map.end()) {
    
    
                int dist = j - map.find(s[j])->second;
                if(dist > tmp) tmp = tmp + 1;
                else tmp = dist;
            } else {
    
    
                tmp = tmp + 1;
            }
            map[s[j]] = j;
            ret = max(ret, tmp);
        }
        return ret;
    }
	//方法二
    //动态规划+线性遍历,时间复杂度O(n^2),空间复杂度O(1)
    int lengthOfLongestSubstring(string s) {
    
    
        if(s.size() == 0) return 0;
        int ret = 0, tmp = 0;
        for(int i = 1; i < s.size(); ++i) {
    
    
            int dist = sameAppearDist(s, i);
            if(dist == 0 || dist > tmp) tmp = tmp + 1;
            else tmp = dist;
            ret = ret > tmp ? ret : tmp;
        }
        return ret;
    }

    int sameAppearDist(const string& s, int index) {
    
    
        //查找从index位置向左最近的相同字母的位置,若是不存在,则返回0,存在则返回距离dist
        if(index < 1) return 0;
        int dist = 0;
        for(int i = index-1; i >= 0; --i) {
    
    
            dist++;
            if(s[i] == s[index]) return dist;
        }
        return dist == index ? 0 : dist;
    }

参考资料:

  1. 图解 滑动窗口(双指针)及优化方法
  2. 面试题48. 最长不含重复字符的子字符串(动态规划 / 双指针 + 哈希表,清晰图解)
  3. 《剑指Offer》第2版

猜你喜欢

转载自blog.csdn.net/yueguangmuyu/article/details/112850609