问题: 求一个字符串中的最长无重复子串,或者说求一个数组中最长的无重复子数组(都是一种意思)
输入:abcdedafg
输出:5
解释:abcde这是一个最长的无重复子串,同理edafg也是。所以最长的无重复子串是5
也就是以下图中题目示意:
方法一:借助unordered_set和left,right指针
算法思想:left指针开始的时候指向set中的第一个元素。right向后遍历,遍历到的数字如果set中没有则加进myset中,同时全局变量统计出现无重复元素的最大值;如果right遍历到v[i]时,set中存在此数,则set中要从left指针的位置开始释放,直到释放掉次数的时候,再将v[i]加入进set中,继续往后遍历。 这样,全局变量就可以在过程中统计出现的无重复次数,所以O(N)的复杂度就可以完成。
算法实现:
int main()
{
int n = 0;
int left = 0;
int right = 0;
int result = 0;
cin >> n;
vector<int> v(n);
unordered_set<int> myset;
for (int i = 0; i < n; i++)
{
cin >> v[i];
}
while (left < n && right < n)
{
if (!myset.count(v[right])) // 如果myset中没有v[i],将它加入进来.
{
myset.insert(v[right++]); // 加入进来之后,right++,走向下一个位置
result = max(result, right - left);
}
else
{ // 如果myset中存在right值,则将myset中从第一个开始取出,直到把原来值与v[right]相等的取出,在把v[right]放进去
myset.erase(v[left++]);
}
}
cout << result << endl;
system("pause");
}
总结:字符串中最长无重复子串也可以这种方法实现,在这里类似,就不在实现了。
方法二:借助unordered_map巧妙完成统计
算法思想:与方法一不同的是,每一次的start都是直接取到哪个重复字符的位置。比如 abcdab, map中存放的是{{a,1},{b,2},{c,3},{d,4}},当a再次出现的时候,start可以快速定位到map中它的位置,是1。那现在的start就是1,算全局最大值的时候,就不算之前的a,算这个再次出现的a。所以全局最大值要 end-start+1。但是要注意将map中的值要换成第二次到来的下标,即{a,4}。因为后面字符串中还有a字符时,start还要更新位置。(不好理解的话,调试一波,方便理解)
算法实现:
int main(){
string A;
while (cin >> A){
int size = A.length();
int res = 0;
int start = 0;
unordered_map<int, int> map;
for (int end = 0; end < size; end++){
if (map.count(A[end])){
start = max(start, map[A[end]]);
}
map[A[end]] = end + 1;
res = max(res, end - start + 1);
}
cout << res << endl;
}
return 0;
}
总结:总结两种方法虽然都是O(N),但是方法二明显要快于方法一,因为寻找start位置的时候直接就通过map找到。而方法一还要通过erase,增大开销。
方法三:借助滑动窗口
算法思想:类似之前的思路,使用 window 作为计数器记录窗口中的字符出现次数,然后先向右移动 right,当 window 中出现重复字符时,开始移动 left 缩小窗口,如此往复。显然还是O(N)
算法实现:
int LengthOfLongestSubstring(string s)
{
int left = 0;
int right = 0;
unordered_map<char, int> window; // 窗口
int res = 0; //记录长度
while (right < s.size())
{
char c1 = s[right];
window[c1]++;
right++;
//如果window中出现重复字符
//开始移动left缩小窗口
while (window[c1] > 1)
{
char c2 = s[left];
window[c2]--;
left++;
}
res = max(res, right - left);
}
return res;
}