剑指Offer-48:最长的不包含重复字符的子字符串

题目描述

        请从字符串中找出一个最长的不包含重复字符的子字符串,计算该最长子字符串的长度。假设,字符串中只包含‘a-z’的字符。例如,在字符串“arabcacfr”中,最长的不含重复字符的子字符串是“acfr”,长度为4 。


        本题最直观的方法就是找出字符串的所有子字符串,然后判断每个子字符串中是否包含重复的字符。这种直观方法的唯一缺点就是效率。一个长度为n的字符串,有n^2个子字符串,我们需要O(n)的时间去判断一个子字符串是否存在重复的字符,因此该方法的总的时间效率为O(n^3)。

        用动态规划算法来提高效率的话。首先,定义函数f(i)表示以第i个字符为结尾的不包含重复字符的子字符串的最长长度。从左往右逐一扫描字符窜中的每个字符。当计算到以第i个字符结尾的不包含重复字符的子字符串的最长长度f(i)时,前面的f(i-1)已经可以得知。
        如果第i个字符没有出现过,那么f(i)=f(i-1)+1。例如,在字符串“arabcacfr”中,f(0)=1。在计算f(1)时,下标为1的字符‘r’之前没有出现过,因此f(1)等于2,即f(1)=f(0)+1。到目前为止,最长的不含重复字符的子字符串是“ar”。
        如果第i个字符之前出现过,那情况就会变得复杂很多。首先需要计算第i个字符和它上次出现在字符串中的位置距离,并记为d。接着分以下两种情况:

1. d小于或者等于f(i-1)

        此时第i个字符上次出现在f(i-1)对应的最长子字符串之中,因此f(i)=d。同时这也意味着在第i个字符出现两次所夹的子字符串中再也没有其他重复的字符了。
        在前面的例子中,我们继续计算f(2),即以下标为2的字符‘a’为结尾的不含重复字符的子字符串的最长长度。可以发现字符‘a’在前面出现过,该字符上一次出现在小标为0的位置,因此他们的距离d=2,也就是字符‘a’出现在f(1)对应的最长不含重复字符的子字符串“ar”中,此时f(2)=d,即f(2)=2,对应的最长不含重复字符的子字符串长度是“ra”。

2. d大于f(i-1)

        此时第i个字符上次出现在f(i-1)对应的最长子字符串之前,因此仍然有f(i)=f(i-1)+1。
        依然以字符串“arabcacfr”为例,分析最后一个字符‘r’结尾的最长不含重复字符的子字符串的长度,即求f(8)。
        以它前一个字符‘f’作为结尾的最长不含重复字符的子字符串是“acf”,因此f(7)=3。可以发现最后一个字符‘r’在此之前出现过,上一次出现的下标为1,两次出现的距离d=7,大于f(7)。说明上一次出现在下标为1位置的‘r’不在f(7)对应的最长不含重复的子字符串“acf”中,此时把字符‘r’拼接到“acf”的后面也不会出现重复字符。因此f(8)=f(7)+1,即f(8)=4,对应的最长不含重复字符的子字符串是“acfr”,长度为4 。
根据上述思路,可得到以下代码:

using System;
using System.Text;

namespace 最长不含重复字符的子字符串
{
    class Program
    {
        static void Main(string[] args)
        {
            string num1 = "arabcacfr";
            string num2 = "abcfdertgyhujinszmpolqsd";
            string num3 = "abcdefg";
            string num4 = "aaaaaaaa";
            Solution s = new Solution();
            Console.WriteLine(s.GetMaxSonStringLength(num1));
            Console.WriteLine(s.GetMaxSonStringLength(num2));
            Console.WriteLine(s.GetMaxSonStringLength(num3));
            Console.WriteLine(s.GetMaxSonStringLength(num4));
        }
    }

    class Solution
    {
        public int GetMaxSonStringLength(string str)
        {
            if (str == null || str.Length <= 0)
                return 0;

            //更改字符串类型,方便相应的字符操作
            StringBuilder sb = new StringBuilder(str);

            int maxLength = 0;                              //最大的子字符串长度
            int curLength = 0;                              //当前子字符串的长度

            int[] position = new int[26];                  //用于储存当前子字符串各个字母出现的下标

            for (int i = 0; i < position.Length; i++)
                position[i] = -1;

            //把每个出现的字符的下标储存在定义的数组中,并计算他们的最大长度
            for (int i = 0; i < str.Length; i++)
            {
                //得到上一个相同字符出现的位置
                int prevIndex = position[sb[i] - 'a'];
                //prevIndex为-1表示没有出现过
                //当前位置减去prevIndex大于当前最长不含重复字符的子字符串的长度,说明当前重复的字符不在最长不含重复字符的子字符串里,直接令最长长度+1
                if (prevIndex == -1 || i - prevIndex > curLength)
                    curLength++;
                else
                {
                    //如果遇到重复字符,判断当前最长不含重复字符的子字符串长度是否大于前面出现过的最大长度,如果大于,更新
                    if (curLength > maxLength)
                        maxLength = curLength;

                    //更新最大长度后,更新当前最长不含重复字符的子字符串长度
                    curLength = i - prevIndex;
                }
                //更新当前字符出现的位置
                position[sb[i] - 'a'] = i;
            }

            //当最后一个字符也判断完毕之后,判断最后的子字符串长度是否大于前面最大的子字符串长度,大于则更新
            if (curLength > maxLength)
                maxLength = curLength;

            return maxLength;
        }
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

        上述代码中创建的长度为26的数组position用于存储每个字符上次出现在字符串中位置的下标。该数组所有元素的值都初始化为-1,负数表示该元素没有在字符串中出现过或者还没有出现过。我们在扫描字符串时遇到某个字符,就把该字符在字符串中的位置存储到数组对应的元素中,可以方便计算距离时的调用。

public class P236_LongestSubstringWithoutDup {
    //动态规划
    //dp[i]表示以下标为i的字符结尾不包含重复字符的最长子字符串长度
    public static int longestSubstringWithoutDup(String str){
        if(str==null || str.length()==0)
            return 0;
        //dp数组可以省略,因为只需记录前一位置的dp值即可
        int[] dp = new int[str.length()];
        dp[0] = 1;
        int maxdp = 1;
        for(int dpIndex = 1;dpIndex<dp.length;dpIndex++){
            //i最终会停在重复字符或者-1的位置,要注意i的结束条件
            int i = dpIndex-1;
            for(;i>=dpIndex-dp[dpIndex-1];i--){
                if(str.charAt(dpIndex)==str.charAt(i))
                    break;
            }
            dp[dpIndex] =  dpIndex - i;
            if(dp[dpIndex]>maxdp)
                maxdp = dp[dpIndex];
        }
        return maxdp;
    }
    public static void main(String[] args){
        System.out.println(longestSubstringWithoutDup("arabcacfr"));
        System.out.println(longestSubstringWithoutDup("abcdefaaa"));

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值