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

一、题目

题目链接:力扣

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

示例 1:

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

示例 2:

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

示例 3:

输入: "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
     请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

s.length <= 40000


二、题解

1、思路

🐎 动态规划

最长字串,直接想到动态规划。

"pwwkew"

动态规划四大步骤:

穷举分析

p→最长不重复子串长度为1;p;

pw→最长不重复子串长度为2;pw;

pww→最长不重复子串长度为2;pw;

pwwk→最长不重复子串长度为2;pw、wk;

pwwke→最长不重复子串长度为3;wke;

pwwkew→最长不重复子串长度为3;wke、kew。

分析找规律,拆分子问题

考虑问题:如何将当前字符串最长不重复子字符串长度,与上一个(甚至更往前一个)字符串最长子字符串长度联系起来。

最后面记录了我按照上面这个考虑问题的思路得到的一种“错误的”想法。

假设 dp[i] 为以 str[i] 结尾的(关键)子字符串的不重复字串长度(是最长)。设i从0开始。

通过分析我们发现,如下规律:

最简单的边界情况

dp[0]=1,以str[0]结尾的不重复子字符串长度。

状态转移方程

设字符 s[j] 左边距离最近的相同字符为 s[i] ,即 s[i]=s[j] 。

  1. 当 i < 0 ,即 s[j] 左边无相同字符,则 dp[j]=dp[j−1]+1 ;
  2. 当 dp[j−1]<j−i ,说明字符 s[i] 在子字符串 dp[j−1] 区间之外 ,则 dp[j]=dp[j−1]+1 ;
  3. 当 dp[j−1]≥j−i ,说明字符 s[i] 在子字符串 dp[j−1] 区间之中 ,则 dp[j] 的左边界由 s[i] 决定,即 dp[j] = j - i;

max(dp) ,即 “最长不重复子字符串” 的长度。

下面举个例子,比如“abcdbaa”,索引从0开始。 我们容易得到,当 j = 4时,以s[4]结尾字符串sub[4] = “cdb”的 长度dp[4] =3。 接下来我们看 j +1的情况。根据定义,sub[4]字符串中的字符肯定不重复,所以当 j = 5时,这时距离字符s[5]的左侧的重复字符a的索引 i = 0, 也就是说s[ 0 ]在子字符串sub[ 4 ]之外了,以s[5]结尾的字符串自然在sub[4]的基础上加上字符s[5]就构成了新的最长的不重复的子串sub[5],长度dp[5] = dp[4] + 1; 接下来我们继续看 j =6的情况,这时s[6]的左侧重复字符a的索引 i = 5,该重复字符在sub[ 5 ]中。新的最长不重复的字串sub[6]左边界以 i 结尾,长度dp[6] = j - i = 1。

🐎 滑动窗口

以示例一中的字符串 abcabcbb 为例:

以 (a)bcabcbb 开始的最长字符串为 (abc)abcbb;
以 a(b)cabcbb 开始的最长字符串为 a(bca)bcbb;
以 ab(c)abcbb 开始的最长字符串为 ab(cab)cbb;
以 abc(a)bcbb 开始的最长字符串为 abc(abc)bb;
以 abca(b)cbb 开始的最长字符串为 abca(bc)bb;
以 abcab(c)bb 开始的最长字符串为 abcab(cb)b;
以 abcabc(b)b 开始的最长字符串为 abcabc(b)b;
以 abcabcb(b) 开始的最长字符串为 abcabcb(b)。

假设我们选择字符串中的第 k 个字符作为起始位置,并且得到了不包含重复字符的最长子串的结束位置为 rk 。那么当我们选择第 k+1 个字符作为起始位置时,首先从 k+1 到 rk 的字符显然是不重复的,并且由于少了原本的第 k 个字符,我们可以尝试继续增大 rk,直到右侧出现了重复字符为止。

一句话讲就是右指针遇到重复字符时,左指针跳到子串里第一个重复字符下一个位置,并且把哈希表中子串中重复字符及其之前的字符去掉,重新右指针再往右扩展。

本质:寻找所有以当前字符结尾的子字符串长度,取最大值!

看下面代码理解:

  • 在没有遇到相同的时候,left不动,right一直+1,right - left +1 即以当前字符结尾的不重复子串长度;
  • 当遇到相同的时候,更新哈希表,并将left移动到前面重复字符的下一个位置,即计算以当前字符结尾的不重复子串长度。

发现没有,和上面动态规划一毛一样?

  • 上方第一条就是动态规划第二条;
  • 上方第二条就是动态规划第三条。
  • 但是动态规划侧重点不一样,动态规划侧重与上一个字符结尾的不重复字符串长度的关系,双指针侧重当前不重复子串长度的直接查找。

2、代码实现

🐎 动态规划

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int size = s.size();
        if(size < 2)return size;
        // if(s.size() == 0)return 0;// 空串
        // if(s.size() == 1)return 1;// 只有一个字符

        unordered_map<char, int> mp;
        mp[s[0]] = 0;// 一定要将s[0]加入哈希表
        int res_max = 0;// 最大子串,一定会被修改
        int last_dp = 1;// dp[j-1],初始化为dp[0]=1
        int i = 0;// s[i] == s[j]

        for(int j = 1; j < size; j++)
        {
            // 寻找s[i]的位置,s[i]==s[j]
            if(mp.find(s[j]) == mp.end())i = -1;
            else i = mp[s[j]];

            // 记录字符最后一次出现的位置,因为上方找的是最后一个
            mp[s[j]] = j;
            // 根据i所在的位置和dp[j-1]判断dp[j]
            last_dp = last_dp < j-i ? last_dp+1 : j-i;
            //cout<< s[j]<< " "<< last_dp<<endl;
            res_max = max(res_max, last_dp);
        }
        return res_max;
    }
};

🐎 滑动窗口

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        unordered_set<char> window;
        int res_max = 0;// 一定要初始化为0,不然s为空的时候无法进入循环,直接返回该值
        int left = 0, right = 0;
        for(int right = 0; right < s.length(); ++right)
        {
            // 如果没有重复的,右指针右移并记录到哈希表
            if(window.find(s[right]) == window.end())
            {
                window.insert(s[right]);
            }
            else// 遇到重复的左指针移动到重复元素后一个位置,并删除哈希表中重复字符及其前面所有内容
            {
                while(s[left] != s[right])
                {
                    window.erase(s[left]);
                    ++left;
                }
                left += 1;
            }
            res_max = max(right - left + 1, res_max);
        }
        return res_max;
    }
};

3、复杂度分析

🐎 动态规划

时间复杂度:O(n);

空间复杂度:O(1)。哈希表最多存放26个内容,因为只有26个字母。如果其他字符也可以,如%#@!,最多只有128个。

🐎 滑动窗口

时间复杂度:O(n);

空间复杂度:O(1)。哈希表最多存放26个内容,因为只有26个字母。如果其他字符也可以,如%#@!,最多只有128个。

4、运行结果

🐎 动态规划

🐎 滑动窗口

附录:一个“误导”我的想法

最初的一个错误想法,也不能说错误,只能说难以实现,不如“官方”的简单,直接忽略不看哈。

穷举分析

p→最长不重复子串长度为1;p;

pw→最长不重复子串长度为2;pw;

pww→最长不重复子串长度为2;pw;

pwwk→最长不重复子串长度为2;pw、wk;

pwwke→最长不重复子串长度为3;wke;

pwwkew→最长不重复子串长度为3;wke、kew。

分析找规律,拆分子问题

pw是在p的基础上,max( w能够和p后方构成的最长子串长度2,p的最长不重复子串长度1 )=2。

pww,max( w能够和p后方构成的最长子串长度1,pw的最长不重复子串长度2 )=2。

pwwk,max( w能够和p后方构成的最长子串长度2,pww的最长不重复子串长度2 )=2。

pwwke,max( w能够和p后方构成的最长子串长度3,pwwk的最长不重复子串长度2 )=3。

pwwkew,max( w能够和p后方构成的最长子串长度3,pwwke的最长不重复子串长度3 )=3。

最简单的边界情况

假设dp[i]为以str[i]结尾的字符串(该字符串可能包含重复内容)最长不重复字串长度。设i从0开始。

dp[0] = 1 一个字符最长不重复子串长度为1。

状态转移方程

dp[i] = max( dp[i-1], str[i]和前面结合的最大不重复字串长度 )

"pwwkew"

k前方无重复k,但是有重复w,而且难以判断w位于前方可能重复的k的相对位置,因此,本方法实现困难。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Kashine

你的鼓励将是我创作的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值