LeetCode No.3 Longest Substring Without Repeating Characters

这是一道关于字符串的题,首先我们还是先来理解一下题目:

给定一个字符串,求出其中不包含重复字符的最长子串的长度。


       首先我们思考一下如何确定字符串中包含重复字符呢,也就是说当我们顺序遍历这个字符串,遍历到某一个字符时发现该字符与之前遍历过的某个字符相同,那么最粗暴直接的方法就是将当前字符与之前遍历过的每个字符依次进行比较,我们知道这样做时间复杂度将达到O(n^2),这还仅仅只是确定字符串是否包含重复字符这一项操作,再加上遍历题中给定的原字符串,时间复杂度将达到O(n^3),这是我们难以接受的,那么如何改进算法呢,有了第一题的经验,我想你已经猜到了,没错,我们要使用散列来帮助我们实现快速查找,散列的查找操作仅需O(1)的时间!


本文将给出两种思路,第一种思路的时间复杂度为O(n^2),第二种可以达到O(n)!

思路一:蛮力算法

  1. 我们设置两个指针,第一个指向当前子串的起始位置,第二个指向当前子串的终止位置,当前子串初始化为第一个字符,同时创建一个散列用来存储已经遍历过的字符;
  2. 我们检查终止指针指向的字符是否出现在散列中,如果是,那么清空当前散列,将起始指针向后移动一位,终止指针指向起始指针指向的位置,同时比较当前存储的最大长度与本次遍历的子串长度,取大者为临时最大长度;如果不是,则将终止指针指向的字符插入到散列中,然后终止指针向后移动一位,子串长度加1;
  3. 重复第二步直到终止指针抵达原字符串末尾;
  4. 返回最大长度;
来看看代码:
int lengthOfLongestSubstringBrute(string s)
{
	//用来存储读取过的字符的散列
	unordered_set
    
    
     
      characters;
	//最大长度
	int maxLen = 0;
	int currLen = 0;

	auto sbegin = s.begin(), send = sbegin;

	while (send != s.end())
	{
		if (characters.find(*send) != characters.end())
		{
			characters.erase(characters.begin(), characters.end());
			currLen = 1;
			++sbegin;
			send = sbegin;
			characters.insert(*send);
		}
		else
		{
			characters.insert(*send);
			++currLen;
		}
		++send;
		maxLen = max(maxLen, currLen);
	}

	return maxLen;
}
    
    

运行结果:


这种思路非常直接,虽然使用了散列帮助快速查找,但是最终时间复杂度依然高达O(n^2),因为子串的终止指针在每次找到重复字符后都要回溯到起始指针的下一个位置。但是我们仔细思考一下,是不是每次真的有必要回溯到那么靠前的位置呢?


我们知道,当找到一个重复字符的时候,这两个重复字符之间的所有字符与当前字符必然不是重复的,如果我们每次都回溯到子串开始的下一个位置,那么就会进行很多次无谓的重复比较,举例来说:

字符串:abcabcbbbjkicad

当我们第一次发现第四个a与第一个a重复的时候,我们是知道第二个b和第三个c肯定与第四个a不同,如果这时我们将子串指针回溯到第二个b,那么接下来的两次比较都将是重复的,因为我们在当前子串遍历的过程中已经知道了b, c一定与a不同!根据这个思路,我们就可以降低时间复杂度,这和经典的KMP算法是有异曲同工之妙的!我们称这种算法具有某种记忆性。


思路二:记忆匹配

这里面有一些小细节需要注意,让我们直接看代码说明:

int lengthOfLongestSubstring(string s) 
{
    //用来存储读取过的字符的散列,同时也要记录下字符的索引,方便计算子串长度
	unordered_map
   
   
    
     characters;
	//最大长度
	unsigned int maxLen = 0;
	//当前非重复子串的长度
	unsigned int currLen = 0;

	for(int i = 0; i < s.size(); ++i)
	{
	   //如果当前访问的字符在之前访问过,也就是说存在重复的可能
	   if(characters.find(s[i]) != characters.end())
	       //更新当前子串长度,currLen + 1说明是当前字符与之前重复的字符不在一个子串中
	       currLen = min(i - characters[s[i]], currLen + 1);
	   else
	       ++currLen;
	   //更新最大长度
	   maxLen = max(maxLen, currLen);
	   //更新散列
	   characters[s[i]] = i;
	}
	    
	return maxLen;
}
   
   

这里需要注意的是,两个重复字符之间可能包含其他带有重复字符的子串,如果单纯以两个重复字符的索引来计算子串长度将出现错误,这里我们在每次发现重复字符时更新当前子串长度currLen = min(char.rank2 - char.rank1, currLen + 1),就可以保证计算出正确的当前子串长度;

运行结果:



明显快了很多!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值