最长不含重复字符的子字符串

最长不含重复字符的子字符串

1、参考资料

https://leetcode-cn.com/problems/zui-chang-bu-han-zhong-fu-zi-fu-de-zi-zi-fu-chuan-lcof/

2、题目要求

题目描述

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

示例 1:

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

示例 2:

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

示例 3:

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

3、代码思路

动态规划

  1. 为什么要使用动态规划?我们想要求 str(0, i) 字符串中最长不含重复字符的子串,可以这样考虑:假设我们已经求出了 str(0, i-1) 中的最长子串长度,现在考虑上 str(i) 字符,看看加上 str(i) 之后,str(0, i) 字符串的最长子串长度是多少

  2. 于是我们定义 dp[i] 的含义为:以 str[i] 结尾的最长不重复子串的长度。我们遍历字符串中每一个字符:for (int i = 1; i < str.length(); i++) {,并以 str[i] 作为右边界,往 str[i] 左边寻找与 str[i] 相同并且与 str[i] 隔得最近的字符

  3. 由于 dp[i-1] 中存储的是以 str[i-1] 结尾的最长不重复子串的长度,则证明以 str[i-1] 结尾的最长不重复子串为 str(i-dp[i-1], i-1),右边界为 str[i],左边界为 i-dp[i-1],因为再往左走一步的字符就会与 str[i-1] 重复(或者数组越界),所以我们需要在 str(i-dp[i-1], i-1) 中找到与 str[i] 相同并且距离 str[i] 最近的字符

  4. 我们使用 for (j = i - 1; j >= i - dp[i - 1]; j--) { 遍历 str(i-dp[i-1], i-1) 中的所有字符,如果发现 str[j]str[i] 相同,则跳出循环,此时 j 指向 str[j];如果执行完循环,还没有找到与 str[i] 相同的字符,此时 j 指向左边界的前一个字符,该字符和 str[i-1]重复(或者 j=-1),所以 i-j 即为以 str[i] 结尾的最长不重复子串的长度

  5. 当求出 dp[] 数组之后,我们就知道了以每个字符结尾的最长子串长度,再在其中选出最大值即可,这个操作我在求 dp[] 数组的时候就一并做了,不免得又要去遍历一次 dp[] 数组


时间复杂度为 O(n2)

4、代码实现

代码

class Solution {
    public int lengthOfLongestSubstring(String str) {
        // Guard safe
        if (str == null || str.length() == 0) {
            return 0;
        }
        int[] dp = new int[str.length()];
        dp[0] = 1; // 单个字符的最长不重复子串长度为 1
        int maxLength = 1; // 默认最大值为 1
        // 遍历数组中每个字符
        for (int i = 1; i < str.length(); i++) {
            int j;
            // 以 str[i-1] 为右边界,以 str[i - dp[i - 1]] 为左边界
            // 寻找与 str[i] 相同并且距离 str[i] 最近的字符
            for (j = i - 1; j >= i - dp[i - 1]; j--) {
                if (str.charAt(i) == str.charAt(j)) {
                    break;
                }
            }
            // 以 str[i] 结尾的最长不重复子串的长度
            dp[i] = i - j;
            // 整个字符串的长不重复子串的长度
            maxLength = Math.max(maxLength, dp[i]);
        }
        return maxLength;
    }
}

5、代码优化

优化思路

使用 map 代替内层 for 循环

  1. 在上述代码的内层循环:for (j = i - 1; j >= i - dp[i - 1]; j--) { 中,不就是个判重操作,然后记录重复字符在字符串中的索引吗?我们可以使用 HashMap 来代替内层 for 循环,key 为字符,value 为该字符最近一次在字符串中出现的位置
  2. 从上面的代码可以看出,dp[i] 只与 dp[i-1] 有关系,所以我们可以将 dp[] 数组优化为一个一维的变量:lastLengthlastLength 代表 dp[i-1],即以 str[i-1] 结尾的最长不重复字串的长度,进行下一次循环(i+1)的时候,将 dp[i] 的值赋值给 lastLength

如何利用 map 进行动态规划

  1. 遍历当前字符 str[i] 时,先将看看该字符有没有在 map 中出现过:Integer index = map.get(str.charAt(i));
  2. 如果 index == null,则说明 str[i] 根本就没有出现过,以 str[i] 结尾的最长子串长度 = 以 str[i-1] 结尾的最长子串长度 + 1
  3. 如果 index != null,则说明 str[i] 在字符串中出现过,并且是最近一次出现的位置,这里又需要分为两种情况:
    1. 因为以 str[i-1] 结尾的最长子串为:str(i-lastLength, i-1),所以如果 index 没有落在这个区间内,则说明在 str(i-lastLength, i-1) 没有 str[i] 的重复字符,此时只需要将 str[i] 排在 str(i-lastLength, i-1) 的后面即可,所以最长子串长度为 lastLength+1
    2. 但是如果 index 落在 str(i-lastLength, i-1) 区间内,则说明此区间内有 str[i] 的重复字符,index 指向与 str[i] 重复的字符,所以最长子串长度为 i-index
    3. 所以我们需要取两者之间的小值

代码实现

class Solution {
    public int lengthOfLongestSubstring(String str) {
        // Guard safe
        if (str == null || str.length() == 0) {
            return 0;
        }
        // key 是字符,value 是该字符上一次在字符串中出现的位置
        Map<Character, Integer> map = new HashMap<>();
        int maxLength = 0;
        int lastLength = 0;
        // 遍历字符串中每个字符
        for (int i = 0; i < str.length(); i++) {
            // 拿到之前的索引值
            Integer index = map.get(str.charAt(i));
            // 更新当前的索引值
            map.put(str.charAt(i), i);
            if (index == null) {
                // 如果 index == null,证明该字符从未出现过,
                lastLength++;
            } else {
                // 否则 index 表示该字符最近一次在字符串中出现的位置
                // 如果 index 没有落在 str(i-lastLength, i-1) 区间内,最长子串长度为 lastLength+1
                // 如果 index 落在 str(i-lastLength, i-1) 区间内,最长子串长度为 i - index
                lastLength = Math.min(lastLength + 1, (i - index));
            }
            // 更新字符串最长不重复字串的最大值
            maxLength = Math.max(lastLength, maxLength);
        }
        return maxLength;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值