剑指 Offer(第2版)面试题 48:最长不含重复字符的子字符串

剑指 Offer(第2版)面试题 48:最长不含重复字符的子字符串

题目来源:

  1. AcWing 61. 最长不含重复字符的子字符串
  2. LeetCode 3. 无重复字符的最长子串

解法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)。

发现规律:如果我们依次递增地枚举子串的起始位置,那么子串的结束位置也是递增的。

这样一来,我们就可以使用「滑动窗口」来解决这个问题了:

遍历字符串的元素,加入「滑动窗口」。如果发现当前字符重复,一直把窗口左端的字符移出,直至「滑动窗口」内的字符不再重复,在这个过程中更新不含重复字符的子字符串的最大长度。

在上面的流程中,我们还需要使用一种数据结构来判断 是否有重复的字符,常用的数据结构为哈希集合,即 C++ 中的 std::unordered_set。在左指针向右移动的时候,我们从哈希集合中移除一个字符,在遍历字符串 s 的时候,我们往哈希集合中添加一个字符。

代码:

class Solution {
public:
    int longestSubstringWithoutDuplication(string s) {
        if (s.empty())
            return 0;
        int n = s.size(), maxLen = 0, left = 0;
        unordered_set<char> occurred;
        for (int i = 0; i < n; i++)
        {
            while (occurred.count(s[i])) // s[i] 在哈希表中存在
            {
                occurred.erase(s[left]);
                left++;
            }
            // 更新最大子串长度
            maxLen = max(maxLen, i - left + 1);
            occurred.insert(s[i]);
        }
        return maxLen;
    }
};

复杂度分析:

时间复杂度:O(n),其中 n 是字符串 s 的长度。

空间复杂度:O(∣Σ∣),其中 Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。AcWing 中假设字符串中只包含从 a 到 z 的字符,所以 ∣Σ∣ = 26。

解法2:动态规划

令 dp[i] 表示以下标 i 为末尾的不含重复字符的子字符串的最大长度。

哈希集合 unordered_map<char, int> charMap 存储键-值对:<字符,最大下标 + 1>。

初始化:dp[0] = 1,charMap[s[0]] = 1。

状态转移:

  1. 如果第 i 个字符没有出现过(charMap[s[i]] == 0),dp[i] = dp[i - 1] + 1。
  2. 如果第 i 个字符出现过,设上一次出现该字符的下标为 lastIndex = charMap[s[i]] - 1,该字符与上一次出现该字符的距离 distance = i - lastIndex。
    • 如果 distance <= dp[i - 1],说明第 i 个字符出现在 dp[i - 1] 对应的最长子串中,我们需要删掉前一次出现字符,再加上 s[i],最终的子串长度为 distance -1 + 1 = distance,所以 dp[i] = distance。
    • 否则,第 i 个字符不在 dp[i - 1] 对应的最长子串中,仍然有 dp[i] = dp[i - 1] + 1。

与此同时,每遍历一个字符 s[i],就要:
3. 更新哈希集合:charMap[s[i]] = i + 1。
4. 更新最长子串长度:max_len = max(max_len, dp[i])。

代码:

class Solution {
public:
    int longestSubstringWithoutDuplication(string s) {
        if (s.empty())
            return 0;
        int n = s.size();
        if (n <= 1)
            return n;
        int max_len = 0;
        unordered_map<char, int> charMap; //<字符,最大下标>
        vector<int> dp(n, 0);
        // 初始化
        dp[0] = 1;
        charMap[s[0]] = 1;
        // 状态转移
        for (int i = 1; i < n; i++)
        {
            if (charMap[s[i]] == 0)
            {
                dp[i] = dp[i - 1] + 1;
            }
            else
            {
                int lastIndex = charMap[s[i]] - 1;
                int distance = i - lastIndex;
                if (distance <= dp[i - 1])
                    dp[i] = distance;
                else
                    dp[i] = dp[i - 1] + 1;
            }
            charMap[s[i]] = i + 1;
            max_len = max(max_len, dp[i]);
        }
        return max_len;
    }
};

复杂度分析:

时间复杂度:O(n),其中 n 是字符串 s 的长度。

空间复杂度:O(n + ∣Σ∣),其中 n 是字符串 s 的长度, Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。AcWing 中假设字符串中只包含从 a 到 z 的字符,所以 ∣Σ∣ = 26。

AcWing 中假设字符串中只包含从 a 到 z 的字符,我们可以用一个长度为 26 的下标数组代替之前的哈希集合。

代码:

class Solution
{
public:
	int longestSubstringWithoutDuplication(string s)
	{
		if (s.empty())
			return 0;
		int n = s.length();
		if (n <= 1)
			return n;
		int curLength = 0, maxLength = 0;
		vector<int> lastPositon(26, -1);
		for (int i = 0; i < n; i++)
		{
			int preIndex = lastPositon[s[i] - 'a'];
			if (preIndex == -1 || i - preIndex > curLength)
				curLength++;
			else
			{
				if (curLength > maxLength)
					maxLength = curLength;
				curLength = i - preIndex;
			}
			lastPositon[s[i] - 'a'] = i;
		}
		maxLength = max(maxLength, curLength);
		return maxLength;
	}
};

更改一下状态转移:

class Solution
{
public:
	int longestSubstringWithoutDuplication(string s)
	{
		if (s.empty())
			return 0;
		int n = s.length();
		if (n <= 1)
			return n;
		int curLength = 0, maxLength = 0;
		vector<int> lastPositon(26, -1);
		for (int i = 0; i < n; i++)
		{
			int preIndex = lastPositon[s[i] - 'a'];
			if (preIndex == -1)
				curLength++;
			else
			{
                if (i - preIndex > curLength)
                    curLength++;
                else
				    curLength = i - preIndex;
			}
			if (curLength > maxLength)
				maxLength = curLength;
			lastPositon[s[i] - 'a'] = i;
		}
		maxLength = max(maxLength, curLength);
		return maxLength;
	}
};

复杂度分析:

时间复杂度:O(n),其中 n 是字符串 s 的长度。

空间复杂度:O(∣Σ∣),其中 Σ 表示字符集(即字符串中可以出现的字符),∣Σ∣ 表示字符集的大小。AcWing 中假设字符串中只包含从 a 到 z 的字符,所以 ∣Σ∣ = 26。

对比:

动态规划空间开销大,代码量也大,还要对下标做特殊处理,评价是不如滑动窗口。

  • 23
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

UestcXiye

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

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

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

打赏作者

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

抵扣说明:

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

余额充值