3. 没有重复字母的最长子串 [leetcode 3: Longest Substring Without Repeating Characters]

本文深入解析LeetCode第3题“无重复字符最长子串”的多种解法,包括三重循环、二重循环加hash判重、滑动窗口等,并对比了不同算法的时间复杂度。

3. 没有重复字母的最长子串 [leetcode 3: Longest Substring Without Repeating Characters]

原题链接

https://leetcode.com/problems/longest-substring-without-repeating-characters

老王的解法链接

https://github.com/simplemain/leetcode/blob/master/3/analysis.md

难度

★★☆☆☆

标签

字符串 / 判重

题目描述

输入一个字符串, 求他的一个子串的长度: 在这个子串里, 没有重复的字母.

说明: 这里是要求子串(要求连续), 而不是子序列(不要求连续)

输入样例

第一组: abcabcbb
第二组: bbbbb
第三组: pwwkew

输出样例

第一组: 3 (因为最长无重复的子串是: abc)
第二组: 1 (最长就只有一个: b)
第三组: 3 (最长的是: wke)

解法分析

  • 解法1 : 三重循环

这道题刚一看到的时候, 就觉得有点懵圈. 感觉完全无头绪. 但是镇静30秒以后, 其实还是有思路, 最简单的无非就是我用两个指针分别指向字符串的两个位置, 看看这两个位置之间的子串有没有重复的字符.

  1. 如果没有, 就记录一下他的长度;

  2. 如果有, 就不符合我们要求, 跳出循环.

比如:

abcdefgcd
  ^    ^
  p    q

我们用指针p和q分别指向字符串的两个位置, 然后再判断p和q之间是否有重复字符. 他们之间明显没有重复的, 所以他们的长度就可以记录下来, 看看是否是最大长度.

如果q指针再往后走一个, 他们之间就有重复的字母c了, 所以就不符合条件了.

最后, 将长度最大的那一个输出就是了.

for (int p = 0; p < len; p++)
{
	for (int q = p; q < len; q++)
	{
		final boolean isOK = judge(cs, p, q);
		if (!isOK) break;
		final int curLen = q - p + 1;
		if (curLen > maxLen)
		{
			maxLen = curLen;
		}
	}
}

代码看起来非常简洁, 不过呢, 就是执行效率不高. 大家看到基本上就是三重循环, 所以时间复杂度O(n^3).

完整代码请点击这里: 完整代码


  • 解法2 : 二重循环 + hash判重

我们既然有了第一种解法大的思路, 那么我们有没有优化的可能呢?

其实是可以的. 就是在解法1的第三重循环那里, 我们的目的是要判断是否有重复. 那既然是要判重复, 就可以不用循环, 可以直接用hash的方法. 这不就可以直接把性能提升了吗?

时间复杂度就降到: O(n^2) ~ O(n^2 * lgn) [为什么Hash是这个复杂度, 请参见老王在第一题中的分析哈]

完整代码请点击这里: 完整代码


  • 解法3: 二重循环 + 数组判重

因为是英文的网站, 所以要处理的字符串是ascii字符. 这里字符取值的范围就会落在[0, 255]之间. 那么, 我们就可以用一个char[256]数组, 取代HashSet来判断重复. 这样, 一定就可以保证时间复杂度控制在O(n^2).

这里其实是一个偷巧的做法. 不过这种做法应用场景也比较多, 只要范围是固定的范围的最大和最小值差值不大, 我们就可以用这种方法.

即使这题含有中文字符, 用unicode编码, 我们也可以用一个char[65536]的数组来处理, 因为他满足我们上面说的两个条件.

完整代码请点击这里: 完整代码


  • 解法4: 滑动窗口

上述三种方法, 其实都是一种思路的衍生品. 那我们有没有更好的方法呢?

其实是有的(这句是废话 o).

我们最先用一个1 * 1的框, 将第一个字符框住.

[a]bcdefgcd

大家可以看到, 字符a已经被中括号代表的框给框起来了.

好, 接下来我们把框试着往右边扩大一个, 看看有没有重复.

如果没有, 我们就把右边的字符框进来.

[ab]cdefgcd

以此类推, 我们可以一直把框扩大到字符g.

[abcdefg]cd

不过, 当我们继续扩张的时候, 发现字符c重复了, 他之前就已经出现在框里了. 这个时候我们怎么办呢?

既然他已经出现在框里了, 就说明从框起始的地方开始, 已经不能继续往下延伸了, 对吧.

那我们就需要重新调整框起始的位置, 一直往右边挪. 挪到什么时候才停止呢? 直到新加入的那个元素c之前没在这个框里出现过. 也就是第一个c后面的位置.

abc[defgc]d

这样, 我们就能很快的找到没有重复字母出现的子串, 对不对?

不过新的子串有可能不是符合条件最长的, 没关系, 我们重复上面的动作, 让他的右边继续扩张, 直到抵达字符串的最后一个字符.

这样, 我们就通过一遍扫描, 完成了算法的要求.

完整代码请点击这里: 完整代码


  • 解法5: 滑动窗口 + 位置记录

这个算法就是解法4的优化算法. 我们在找窗口里出现过的字符的时候, 需要将左括号[一个个的挪动, 看他和新的元素是不是相等, 这样效率不是很高.

我们可以将窗口里每个元素的位置记录一下, 到时挪动的时候, 只需要从记录表里面查一下, 就知道应该将左括号[挪动到这个元素的下一个位置. 对吧~

完整代码请点击这里: 完整代码


好了, 这一题就分析到这里. 如果觉得老王的讲解有意思或有帮助, 可以给老王点个赞或者打个赏啥的, 老王就很开心啦~

咱下一题继续~~

面试题,是纸上写的,发现了些错误,回来改进了下。写纸上和写计算机里并编译成功完全是两个效果。 开始没太多字符操作,很繁琐、难点也多,后逐渐改进。 典型问题1: sizeof()局限于栈数组 char a[] = "asd213123123"; 形式,并且这种能用&#39;\0&#39;判断是否结束(这种判断方式能很方便加在while条件中用于判断越界——b != &#39;\0&#39;)。 如果是字符常量: char *b = "dasadafasdf"; 这种情况,sizeof()就废掉了! 总之: 对号入座,前者sizeof、后者strlen~!过sizeof(a)和strlen(b)还有另外一个区别,strlen计算&#39;\0&#39;,而sizeof要计算(前提是sizeof()针对char指针) 典型问题2: 用什么来暂存并输出结果?还是只是记录下来相关位置——这是我底下未完成版本1想到的思路——用一个count[sizeof(A)]数组记录下A每个位置作为起点所能和B达到的合,后判断查找数组中大值,此时目标子字符的起点下标(i)和 i 对应的长度(counter[i])都有了。 这是针对知道字符大小并且占用额外空间的做法,需要非常繁琐的操作,要加很多标记,越界判断也会有些麻烦(结合优势么,用字符常量而是栈空间中的字符数组,有&#39;\0&#39;——就好判断了!) (关于空间的占用,如果要用一个和字符a一样长的数组counter来计录a中各起点对应与b合子字符,这个数组也要和a一样长,空间上也合适,除非情形很特殊,a短b长,如直接malloc()一个堆空间来储存当前长“子字符”,并实时更新) 先放一个改完编译测试成功的。 release1 //题目:要求比较A字符(例如“abcdef"),B字符(例如(bdcda)。找出合度大的子字符,输出(根据OJ经验,输>出结果对即可) #include #include #include main(){ char *A = "abcderfghi"; char *B = "aderkkkkkabcd"; int i,j,c = 0,count = 0; unsigned int maxSeg = 0; int max = strlen(A) > strlen(B) ? strlen(A) : strlen(B); char* final = (char*)malloc(sizeof(char) * (max + 1)); final[max] = &#39;\0&#39;; for(i = 0;A[i] != &#39;\0&#39;;i++){ for(j = 0;B[j] != &#39;\0&#39;;j++){ while(A[i + c] == B[j] && A[i+c] != &#39;\0&#39; && B[j] != &#39;\0&#39;){ count++; c++; j++; }                         if(count > maxSeg){                                 strncpy(final,(A + i),count);                                 maxSeg = count;                         } count = 0; c = 0; } } printf("%s\n",final); free(final); } 这是能将就用的第一个版本~!关于结束符&#39;\0&#39;能否影响free()的使用,觉得是完全用操心的,因为malloc的大小是系统来保存的,删除时候系统来接手就完了,而&#39;\0&#39;结束符只是针对一些常规字符操作,比如printf()用%s控制输出时~! 新难点:找到的子字符同时一样长怎么办?那我这只能叫做”第一个长的合字符“用两块空间来存储?三段等长怎么办? 如: "abclbcdlcdel" "kabckbcdkcde" abc长3,bcd长3,cde长3。。。 未完成版本1:这段是错误示范,初期定位模糊思路乱,有些函数和功能把握,又在纸上写。 思路乱的一个后果就是前期想用i和j简单判断越界问题,后期又弄了i+c之类的下标, 修改思路: 把字符换成“字符常量”——&#39;\0&#39;的,这样在小while中用 != &#39;\0&#39;就能判断出界问题。 把字符变成字符常量以后的另一个问题是sizeof能用了,引入string.h,用strlen()替代即可。 //题目:要求比较A字符(例如“abcdef"),B字符(例如(bdcda)。找出合度大的子字符,输出(根据OJ经验,输出结果对即可) //遗忘,未使用string.h相关函数。 #include main(){ char A[] = "asdasd"; char B[] = "asdasd"; /*本版本处理方式为通用的针对字符大小未知情况的遍历——比如“字符常量”——此时可用strlen()代替sizeof(),并引入即可。 *但是因在纸上做题,在条件上做了简化————使用了sizeof()可确定大小的字符数组而非“字符常量”。具体用sizeof()还是strlen()。这些小问题请读者自行区分。 *如果可用sizeof()确定大小,就可以用malloc()创建一个临时字符来存储并输出大字符子段,代码会简化很多~! *过如果用malloc()保存大子段,随着大子段变化,需要停的free()再新malloc(),要注意 */ int i,j,flag,c = 0,temp = 0,max = 0,count[sizeof(A)]; for(i = 0;i < sizeof(A);i++){//以i为A中“子字符”首位,遍历B,看B中与A[i]起的子字符大匹配数量是多少,记为count[i],每个count[i]对应A中一个字符 for(j = 0;j count[i])//找出B中匹配度高子段,用记录下标,只需记录匹配的字符数量,A[i]是固定的起点,加上偏移量,就是这段 count[i] = temp; } //清零,准备面对新的起点j~!以j为起点再找匹配的一段字符 //j用恢复~!恢复原样的话,算上j++是移动了一位,会死循环~!但是,因为这一段本来就是连续的,abcd都连续了,bcd和cd用看了。 temp = 0; c = 0; } } //比较count数组,看哪个i对应子段越大 //temp = 0;//节省空间的考虑(虽然只有4B),怕适应就改叫max,去声明一个max变量。 for(i = 0;i max){//找出大的一个计数器~~~~并记录i!!! max = count[i];//这句可以精简掉,可能?可以,作为“下标”可以被精简,因为有了flag~!但作为max能少,做比较用——叫max比较好理解。 flag = i;//用flag记录相应大子段的起始偏移量 } //输出该子段 for(i = flag;i < flag + count[flag];i++){//temp来源于前一个for循环,意为大偏移量。 printf("%c",A[i]); } printf("\n"); } ———————————————— 版权声明:本文为优快云博主「秦伟H」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:https://blog.csdn.net/huqinwei987/article/details/25316699
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值