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),为什么呢?
还可以这样想,内层循环不一定要进行的,仅仅当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)---数组字符串那些经典算法:最大子序列和,最长递增子序列,最长公共子串,最长公共子序列,字符串编辑距离,最长不重复子串,最长回文子串》