您的位置:首页 > 其它

LeetCode(3) Longest Substring Without Repeating Characters

2013-11-23 04:25 337 查看
LeetCode的第3题,给定一个字符串,找到其中的一个最长的字串,使得这个子串不包含重复的字符。

Given a string S, find the longest palindromic substring in S. You may assume that the maximum length of S is 1000, and there exists one unique longest palindromic substring.

1 时间复杂度O(N²)的算法

//
//  Solution.h
//  LeetCodeOJ_003_LongestUniqueSubstr
//
//  Created by feliciafay on 11/21/13.
//  Copyright (c) 2013 feliciafay. All rights reserved.
//

#ifndef LeetCodeOJ_003_LongestUniqueSubstr_Solution_h
#define LeetCodeOJ_003_LongestUniqueSubstr_Solution_h
#include <iostream>
#include <string>
#include <map>
using std::string;

class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        // IMPORTANT: Please reset any member data you declared, as
        // the same Solution instance will be reused for each test case.
        
        unsigned long limit =s.length();
        if(limit==0)
            return 0;
        int start = 0;
        int p = 0;
        int q = 0; //q对程序其实没作用,主要用来debug的时候观察。q与p的区别是,p表示当前进行探测的字符,q表示p的前面一位字符。
        unsigned long max_length = 1;
        unsigned long cur_length = 1;
        string sub1;
        std::map<char,int> char_int_map;
        std::map<char,int>::iterator it = char_int_map.begin();
        while(p<limit) {
            it = char_int_map.find(s[p]);
            if(it == char_int_map.end()) {
                q = p;
//                std::cout<<"1-1 q: "<<q<<std::endl;
                char_int_map.insert(std::pair<char, int>(s[p],p));
                p++;
            }else{
                cur_length = q-start+1;
                if(cur_length >= max_length) {
                    max_length = cur_length;
                    sub1 = s.substr(start,max_length);
//                    std::cout<<"2-1 substring1: "<<sub1<<std::endl;
                }
                start = it->second+1;
                p = start;
                q = start;

                char_int_map.clear();
                char_int_map[(char)s[start]] = start; //更新map
//                std::cout<<"2-2 q: "<<q<<std::endl;
                p++;
            }
        }
        string tail_segment = s.substr(start,limit-start+1);
        unsigned long tail_length = tail_segment.length();
        if(tail_length >= max_length)
        {
            sub1 = tail_segment;
            max_length = tail_length;
        }
            
//        std::cout<<"largest sub string is "<<sub1<<std::endl;
//        std::cout<<"largest sub string length is "<<max_length<<std::endl;
        return (int)max_length;
    }
};

#endif


在MAC的Xcode下的运行结果



小结:

(1) 注意边界条件的检查,当输入串为空时候应该找到的字串也为空,此时的返回的字串长度应该为0。第一次提交的时候,犯了这个错误。

(2) 采取逐步右移的方式逐一检查,使用hash表来记录每个字符出现的位置,时间复杂度为O(N²),空间复杂度为O(N)。

(3) 程序中的q表示p的前一位,基本上不起作用,写着是为了debug方便。



2 时间复杂度为O(N)的算法

使用i和j两个指针进行搜索,i代表候选的最长子串的开头,j代表候选的最长子串的结尾。

先假设i不动,那么在理想的情况下,我们希望可以一直右移j,直到j到达原字符串的结尾,此时j-i就构成了一个候选的最长子串。每次都维护一个max_length,就可以选出最长子串了。

实际情况是,不一定可以一直右移j,如果字符j已经重复出现过(假设在位置k),就需要停止右移了。记录当前的候选子串并和max_length做比较。接下来为下一次搜寻做准备。

在下一次搜寻中,i应该更新到k+1。这句话的意思是,用这个例子来理解,abcdef是个不重复的子串,abcdefc中(为了方便记录为abc1defc2),c1和c2重复了。那么下一次搜寻,应该跨过出现重复的地方进行,否则找出来的候选串依然有重复字符,且长度还不如上次的搜索。所以下一次搜索,直接从c1的下一个字符d开始进行,也就是说,下一次搜寻中,i应该更新到k+1。

LeetCode给出的参考答案非常漂亮

int lengthOfLongestSubstring(string s) {
  int n = s.length();
  int i = 0, j = 0;
  int maxLen = 0;
  bool exist[256] = { false };
  while (j < n) {
    if (exist[s[j]]) {
      maxLen = max(maxLen, j-i);
      while (s[i] != s[j]) {
        exist[s[i]] = false;
        i++;
      }
      i++;
      j++;
    } else {
      exist[s[j]] = true;
      j++;
    }
  }
  maxLen = max(maxLen, n-i);
  return maxLen;
}

这个解答的时间复杂度是O(N)。虽然有两个while的嵌套,但是时间复杂度依然是O(N),为什么呢?

因为i和j都只把这个string从开始到结束遍历了一遍。

可以这样想,外层while在改变j的值,j最多从0改变到n(n为字符串的长度),内层while在改变i的值,同样的,i最多从0改变到n(n为字符串的长度)。所以加起来,时间复杂度为O(2*N),也就是O(N)。

还可以这样想,内层循环不一定要进行的,仅仅当j遇到了重复字符后需要更新i的值时,才会进行内存循环,而且i加起来走过的步数最多为n(n为字符串的长度)。

这段代码还有很有意思的一点,就是别忘了在循环体之外,还要写上,maxLen = max(maxLen, n-i)。这是为什么呢? 因为可能最后一次检查的时候,j知道走到字符串末尾都没有遇到重复字符。而while循环体中找到的最长不重复子串只是在j遇到重复字符时才进行的。

另外,下面这个算法我自己测试没有通过

这个解法是动态规划+hash的结合体,主要参考了《找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串》这篇文章。

update: 2014-12-23 写出来居然Time Limit Exceeded了,O(N²), 看来Leetcode要求变严格了,正解还得用上面的O(N)的办法,用空间换时间。

//TLE 
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int i = 0; //每次搜索候选子串的起始位置
        int j = 0; //每次搜索候选子串的结束位置
        std::unordered_map<char, int> map;
        int length = 0;
        int max_length = 0;
        while (i < s.length() && j < s.length()) {
            map.clear();
            j = i;
            while (j < s.length() && map.find(s[j]) == map.end()) {
                map[s[j]] = j;
                ++j;
            }
            length = j - i;
            //std::cout<<"sub str = "<<s.substr(i, length)<<std::endl;
            max_length = length > max_length ? length : max_length;
            i = map[s[j]] +1; // 新的起始位置是从重复的两个字符中的第一个字符的后面开始的。
//            i = j  一开始错写为了这句,这样写的话,新的起始位置将是从重复的两个字符中的第二个字符的后面开始,所以会错过对重复字符中间的部分的检查。
        }
        return max_length;
    }
};


update: 2014 - 12- 23

又看了一遍leetcode的官网上的解法,就是上面的O(N)的时间复杂度的算法,因为最后计算maxLength 的时候还要考虑剩余的尾巴的部分,很容易忘记,所以我把代码改了一下,这样看上去会if和else这两部分的操作更加整齐一些。

//72ms  i和j都只遍历s一次,所以时间复杂度为O(N)
class Solution {
public:
    int lengthOfLongestSubstring(string s) {
        int n = s.length();
        int i = 0, j = 0; //i是候选字符串的起点, j是候选字符串的终点。
        int max_length = 0;
        int cur_length = 0;
        bool exist[256] = { false };
        while (j < n) {
            if (!exist[s[j]]) {
                exist[s[j]] = true;  //遍历过,记录为true
                j++;
            } else {
                while(s[i]!= s[j]) {
                    exist[s[i]] = false; 
                    //新候选字串从第一个重复字符(当s[i] == s[j]时候的i)的后一位开始算,之前的i不算,等效于没有被扫描到,所以设为false.
                    i++;
                }
                i++;
                j++;
            }
            cur_length = j - i;
            max_length = max_length > cur_length ? max_length : cur_length;
        }
        return max_length;
    }
};


参考资料

1 Longest Substring Without Repeating Characters

2
《找工作知识储备(2)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串》
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: