《剑指offer》面试题48:最长不含重复字符的子字符串

题目:输入一个字符串(只包含a~z的字符),求其最长不含重复字符的子字符串的长度。例如对于arabcacfr,最长不含重复字符的子字符串为acfr,长度为4。


思路:

使用动态规划,记录当前字符之前的最长非重复子字符串长度f(i-1),其中i为当前字符的位置。每次遍历当前字符时,分两种情况:

1)若当前字符第一次出现,则最长非重复子字符串长度f(i) = f(i-1)+1。
2)若当前字符不是第一次出现,则首先计算当前字符与它上次出现位置之间的距离d。若d大于f(i-1),即说明前一个非重复子字符串中没有包含当前字符,则可以添加当前字符到前一个非重复子字符串中,所以,f(i) = f(i-1)+1。若d小于或等于f(i-1),即说明前一个非重复子字符串中已经包含当前字符,则不可以添加当前字符,所以,f(i) = d。

基于以上思路,java参考代码如下:

public class LongestSubstringWithoutDup {
    //动态规划
    //dp[i]表示以下标为i的字符结尾不包含重复字符的最长子字符串长度
    public static int longestSubstringWithoutDup(String str){
        if(str==null || str.length()==0)        	
            return 0;
        
        //position数组用于记录26个字符(a~z)上一次出现在字符串中位置的下标。该数组所有元素初始化为-1,负数表示该元素对应的字符在字符串中还没有出现。
        int[] position = new int[26];
        for(int i=0;i<str.length();i++) {
        	position[i]=-1;
        }
        
        int curLength=0;
        int maxLength=0;  //注意点1:需要用到变量是才声明,有助于阅读
        for(int i = 1;i<str.length();i++){
            //i最终会停在重复字符或者-1的位置,要注意i的结束条件
            int preIndex = position[str.charAt(i)-'a'];
            int distance=i-preIndex;当前字符与它上次出现位置之间的距离
            if(preIndex<0||distance>curLength)//i处字符之前未出现过,或者d>f(i-1)
            	++curLength;//f(i)=f(i-1)+1
            else {
            	if(curLength>maxLength)
            		maxLength=curLength;
            	
            	curLength=distance;//d<=f(i-1),f(i)=d
            }
            position[str.charAt(i)-'a']=i;//注意点2:一定要每次刷新当前字符的位置
        }
    	if(curLength>maxLength)
    		maxLength=curLength; 
        
        return maxLength;
    }
    public static void main(String[] args){
        System.out.println(longestSubstringWithoutDup("arabcacfr"));//4
        System.out.println(longestSubstringWithoutDup("abcdefaaa"));//6

    }
}

注意点见代码注解部分。

思路二:

我们之前手动推导的方法实际上是维护了一个滑动窗口,窗口内的都是没有重复的字符,我们需要尽可能的扩大窗口的大小。由于窗口在不停向右滑动,所以我们只关心每个字符最后出现的位置,并建立映射。窗口的右边界就是当前遍历到的字符的位置,为了求出窗口的大小,我们需要一个变量left来指向滑动窗口的左边界,这样,如果当前遍历到的字符从未出现过,那么直接扩大右边界,如果之前出现过,那么就分两种情况,在或不在滑动窗口内,如果不在滑动窗口内,那么就没事,当前字符可以加进来,如果在的话,就需要先在滑动窗口内去掉这个已经出现过的字符了,去掉的方法并不需要将左边界left一位一位向右遍历查找,由于我们的HashMap已经保存了该重复字符最后出现的位置,所以直接移动left指针就可以了。我们维护一个结果res,每次用出现过的窗口大小来更新结果res,就可以得到最终结果啦。

这道题还有一个简洁的写法(将26个字母扩充至256个),
下面这种写法是上面解法的精简模式,这里我们可以建立一个256位大小的整型数组来代替HashMap,这样做的原因是ASCII表共能表示256个字符,但是由于键盘只能表示128个字符,所以用128也行,然后我们全部初始化为-1,这样的好处是我们就不用像之前的HashMap一样要查找当前字符是否存在映射对了,对于每一个遍历到的字符,我们直接用其在数组中的值来更新left,因为默认是-1,而left初始化也是-1,所以并不会产生错误,这样就省了if判断的步骤,其余思路都一样:

    //第二种解法:
    public static int longestSubstringWithoutDup2(String s) {
        int[] m = new int[256];
        Arrays.fill(m, -1);//调用库函数初始化数组。
        int res = 0, left = -1;
        for (int i = 0; i < s.length(); ++i) {
            left = Math.max(left, m[s.charAt(i)]);
            m[s.charAt(i)] = i;
            res = Math.max(res, i - left);//i-left包含了两种情况
        }
        return res;
    }

仅仅包含a~z的代码:

    public static int longestSubstringWithoutDup5(String str){
        int left=-1,res=0;
        int[] m=new int[26];
        for(int i=0;i<str.length();i++){
            left=Math.max(left,m[str.charAt(i)-'a']);
            m[str.charAt(i)-'a']=i;
            res=Math.max(res,i-left);
        }
        return res;
    }
测试用例:

a.功能测试(包含多个字符的字符串;只有一个字符的字符串;所有字符都唯一的字符串;所有字符都相同的字符串)。
b.特殊输入测试(空字符串)。

参考:

https://blog.csdn.net/m0_37862405/article/details/80369128
http://www.cnblogs.com/grandyang/p/4480780.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值