查找给定字符串的最长不重复子串。
方法一
该方法很直接,依次从每一个字符开始,向后逐个遍历,直到遇到重复的字符为止。判断字符是否重复,用到了hash数组,可以在常数时间内判断当前字符在本次循环中是否访问过,由于针对的是ASCII字符集,故使用的hash数组大小为256.
int lengthOfLongestSubstring(string& s)
{
int n = s.size();
int maxLen = (0 == n)? 0:1;
char visited[256];
for(int i = 0; i < n; i++)
{
memset(visited, 0 ,256);
visited[s[i]] = 1;
int j = i + 1;
if(maxLen > n - j)
return maxLen;
for(j = i + 1; j < n; j++)
{
if(0 == visited[s[j]])
{
visited[s[j]] = 1;
}
else
{
if(j - i > maxLen)
{
maxLen = j - i;
}
break;
}
}
if(j - i > maxLen)
{
maxLen = j - i;
}
}
return maxLen;
}
分析代码,可知时间复杂度为O(
n2),空间复杂度为O(1)。
方法二
纯动态规化方法。对于给定的字符串s,用DP[i]来存储第i个字符所在的当前最长不重复子串的长度。
举个例子来说明一下,比如字符串str="abcdbd",易知从头开始的"abcd"构成一个不重复的子串,之后该考察str[4],依次与str[3]、str[2]、str[1]比较,当与str[1]比较时,第一次发现了重复。故此时DP[4] = 4 - 1 = 3; 同理,之后该考察str[5],依次与str[4]、str[3]比较,当与str[3]比较时,第一次发现了重复,故此时DP[5] = 5-3=2;
即: DP[0] = 1, DP[1] = 2, DP[2] = 3, DP[3] = 4, DP[4] = 4 - 1 = 3, DP[5] = 5 - 3 = 2.
总结该方法: 对于每个字符,都逐个向前去搜索 该字符是否出现过。如果没有出现过,那么DP[i] = DP[i-1] + 1,如果出现过,记从后向前比较中,第一次出现的位置为i,则DP[i] = j - i;
注意,这里的向前搜索只搜索到新子串的开头即结束。
理解了该原理,就会发现该方法的时间复杂度为O(n2),空间复杂度为O(n)。
int lengthOfLongestSubstring(string& s)
{
int n = s.size();
int maxLen = (0 == n)? 0:1;
unsigned short DP[31660];
DP[0] = 1;
int curStartIndex = 0;
for(int i = 1; i < n; i++)
{
int j = i-1;
for(; j >= curStartIndex; j--)
{
if(s[i] == s[j])
{
DP[i] = i - j;
curStartIndex = j;
break;
}
}
if(curStartIndex - 1 == j)
DP[i] = DP[i-1] + 1;
if(DP[i] > maxLen)
maxLen = DP[i];
}
return maxLen;
}
由空间复杂度为O(n)、时间复杂度为O( n 2)可以看出,这种方法很糟糕。
方法三
把动态规化与hash数组结合起来,应该是一种不错的组合。
如果我们要在O(n)的时间复杂度内解决问题,则必须要在常数时间内检测到重复,同时,在常数时间内确定重复发生时对应的最长不重复子串的长度,并在常数时间内确定好下一次新子串的起始位置。
因为我们每次考察的都是新的子串,所以用int curStartIndex来标记本次子串的起始位置。
我们用hash数组来存储 最新的字符的下标,用于在常数时间内检测重复及确定新子串的起始位置。
我们再用一个缓存数组DP[i]来存储第i位发生重复时,i之前的、距离i最近的最长不重复子串的长度。
这样我们就可以在O(n)时间复杂度内解决战斗。
int lengthOfLongestSubstring(string& s)
{
int n = s.size();
int maxLen = (0 == n)? 0:1;
unsigned int DP[31660];
int visited[256];
//注意,memset是按字节来赋值的
//由于-1的补码为0xFFFFFFFF,所以给每个字节赋值为0xFF
//所以再次按整型读取的时候,依然是0xFFFFFFFF
memset(visited, -1, sizeof(visited));
DP[0] = 1;
visited[s[0]] = 0;
int curStartIndex = 0;
for(int i = 1; i < n; i++)
{
//当前字符没有被访问过
//或者,被访问过,但是是在新起点之前
if(-1 == visited[s[i]] || curStartIndex > visited[s[i]])
{
//表示该字符在当前子串中没有重复出现
DP[i] = DP[i-1] + 1;
//hash数组存储字符对应的最新下标
visited[s[i]] = i;
}
else
{
//当前字符在 子串中已经出现过
//所以新不重复子串的长度
//要从前面那个重复的字符的下一位开始计算
DP[i] = i - visited[s[i]];
curStartIndex = visited[s[i]] + 1;
//更新字符下标
visited[s[i]] = i;
}
if(DP[i] > maxLen)
{
maxLen = DP[i];
}
}
return maxLen;
}
可以看到,这里动态规化的状态更新方程当前值只与上一个相邻值有关。
因为,没有必要保存 除了上一个值之外的状态值。
想到这一点,O(n)空间复杂度瞬间变O(1)。
这样,我们就实现了时间复杂度为O(n),空间复杂度为O(1)的算法。
int lengthOfLongestSubstring(string& s)
{
int n = s.size(), curStartIndex = 0;
int maxLen = (0 == n)? 0:1;
unsigned int lastMaxLen = 1;
int visited[256];
memset(visited, -1, sizeof(visited));
visited[s[0]] = 0;
for(int i = 1; i < n; i++)
{
if(-1 == visited[s[i]] || curStartIndex > visited[s[i]])
{
lastMaxLen = lastMaxLen + 1;
visited[s[i]] = i;
}
else
{
lastMaxLen = i - visited[s[i]];
curStartIndex = visited[s[i]] + 1;
visited[s[i]] = i;
}
if(lastMaxLen > maxLen)
{
maxLen = lastMaxLen;
}
}
return maxLen;
}