正在找工作的猿六凯在投出无数份简历后,准备看会儿电影放松下。突然接到面试官的电话,急忙跑到厕所接听。
黑脸面试官
小猿同学,看你简历说算法掌握的比较扎实,给你出道算法题。
黑脸面试官
给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
黑脸面试官
比如:输入: "abcabcbb",输出: 3。因为无重复字符的最长子串是 "abc",所以其长度为 3。
猿六凯
找出字符串中的所有子串,然后对每一个进行判读是否包含重复字符,把不包含重复字符的最大子串长度输出来就好。
黑脸面试官
写下代码吧。
class Solution {public: int lengthOfLongestSubstring(string s) { int res = 0;//保存结果 for(int i = 0; i < s.size(); i++){ for(int j = i; j >= 0; j--){//双重遍历,获得以s[i]为结尾的每一个子串 set<int> myset; for(int k = j; k <= i; k++){ myset.insert(s[k]);//把字符去重 } if (myset.size() == i - j + 1)//如果不同字符个数与子串长度相同,则子串不包含重复字符。 res = max(res, i - j + 1); else break;//如果出现了重复字符,j就不用再扩展了。 } } return res; }};
黑脸面试官
时间复杂度和空间复杂度是多少?
猿六凯
用两个循环找出所有子串:第一个循环共n次;第二个循环开始时是0次,最后是n次,总共0 + 1 + 2 + ...+ n = n*(0+n)/2。再用一个set判断是否有重复元素,这个过程的时间复杂度是O(n)。所以总时间复杂度是是O(n^3)。
猿六凯
开辟了一个与字符串长度线性相关的set,空间复杂度是O(n)。
黑脸面试官
有没有更好的方法?
猿六凯
我想想,可用双指针。开始时,用两个指针i,j指字符串第一个元素。向右一步步向右扩展j指针,过程中,如果i,j之间的子串出现了重复元素,就向左移动i指针,直到i,j之间没有重复元素。这样,指针i,j之间的字符串含义就是:以j指向元素为结尾的最长无重复元素子串。
例如处理abcab过程如下:
猿六凯
用res保存最长的不包含重复字符的子串长度,开始时res = 0。如下图:
猿六凯
用两个指针i,j分别指向字符串的第一个字符,这个时候i,j之间的字符串只有一个字符a,肯定不包含重复元素。故以j指向的元素a为结尾的最长不包含重复元素子串为:a,长度为j - i + 1 = 1。1 > res,就更新res = j - i +1。如下图:
猿六凯
j指针向后移动一格,i,j之间的字符串中字符串为ab,不包含重复字符。故以j指向的元素b为结尾的最长不包含重复字符的子串为:ab,长度为:j - i + 1 = 2 > res,更新res:res = j - i + 1 = 2。如下图:
猿六凯
j指针继续向后移动一格,i,j之间的字符串中字符串为abc,不包含重复字符。故以j指向的元素c为结尾的最长不包含重复字符的子串为:abc,长度为:j - i + 1 = 3 > res,更新res:res = j - i + 1 = 3。如下图:
猿六凯
j指针继续向后移动一格,i,j之间的字符串中字符串为abca,包含重复字符。如下图:
猿六凯
这时候需要将i指针向左移动,使得i,j之间的字符串中没有重复字符。
猿六凯
i向左移动一格后,i,j之间的字符串为bca。没有重复字符。故以j指向的元素a为结尾的最长不包含重复字符的子串为:bca,长度为:j - i + 1 = 3 = res。res 保持不变。如下图
猿六凯
j继续往后移动一格,此时i,j之间的字符串为bcab,包含重复字符。如下图:
猿六凯
需要将i指针向左移,直到i,j之间的字符串中没有重复字符。
猿六凯
i移动一格后,i,j之间的字符串为cab,没有重复字符。故以j指向的元素b为结尾的最长不包含重复字符的子串为:cab,长度为:j - i + 1 = 3 = res。res 保持不变。如下图。
猿六凯
j指向了字符串结尾,结束。
猿六凯
整个过程中,通过j对字符串的遍历,求出了以各个元素为结尾的最长不包含重复字符的子串长度。并且把最大的长度保存在了res中。res就是答案。
黑脸面试官
过程中需要判断j指向的元素是重复,如果重复,i向左移。这个过程具体怎么实现?
猿六凯
可以记录下一下i,j之间的字符串中各个字符出现的次数。
猿六凯
开始时,i,j之间没有字符。所以各个字符串出现次数为0。如下图:
猿六凯
i,j指向第一个元素a时,a字符出现了一次,a出现的次数+1,更新为a:1。如下图:
猿六凯
j往后移动一格后,j指向b,i,j指向之间的字符串中b出现的次数增加1,更新为b:1。如下图:
猿六凯
j再次往后移动一格后,j指向c,i,j指向之间的字符串中,c出现的次数增加1,更新为c:1。如下图:
猿六凯
j再次往后移动一格后,j指向a,i,j指向之间的字符串中,a出现的次数增加1,更新为a:2。如下图:
猿六凯
这个时候,i,j之间的字符串中,a出现了2次。所以需要向左移动i,减少一次a出现的次数。
猿六凯
i向左移动一格,i由指向a变成指向b,i,j之间字符串中a出现的次数减少1,更新为a:1。这个时候i,j之间各个字符出现的次数都为1。所以i,j之间的字符串就是以j指向的的元素b为结尾的不包含重复字符的子串。如下图:
猿六凯
概括一下就是:维护一个数组,来记录i,j之间各个字符出现的次数。j向右移动之前,各个字符出现的次数都小于2。j向右移动后,j指向的字符出现次数+1,如果现的次数变为2,就向左移动i。i每移动一格,i指向的元素出现次数-1。直到i走过与j更新后指向的那个元素重复的字符后,该元素出现次数有2变为1。i,j之间的字符串就不包含重复元素了。如下图:
猿六凯
i从指向a往右移动一格后,i,j之间的字符串中,a出现的次数减少1,变为1。这个时候,i,j之间就没有重复字符了。
黑脸面试官
具体的,用什么记录字符出现的次数?
猿六凯
计算机中,字符有127个,它们的ascii码值时从1-127。我们开辟一个大小为128的整数数组,把各个元素初始化为0。遇到一个字符,就把它ascii码对应的值+1。就能在O(1)的时间内进行判断了。
黑脸面试官
说的不错,写下代码吧
class Solution {public: int lengthOfLongestSubstring(string s) { int heap[128] = {0};//记录数组,初始化为0 int res = 0; for(int i = 0, j = 0; j < s.size(); ++j){//依次添加新的字符 heap[s[j]]++;//添加字符出现的次数+1 while(heap[s[j]] > 1){//如果添加字符出现次数大于1 heap[s[i]]--;//i指向的的符出现次数-1 i++;//i向后移动 }//直到新添加的字符出现次数变为1 res = max(res, j - i + 1);//取新字符串长度与上一个字符串长度最大值 } return res; }};
黑脸面试官
分析下时间复杂度和空间复杂度。
猿六凯
在时间上,指针i遍历了一遍数组,指针j在i的遍历过程中,也只遍历了一遍数组,所以时间复杂度是O(n)。空间上开辟了一个固定大小的数组,所以空间复杂度是O(1)。
黑脸面试官
恩,今天先到这,等下一轮面试吧。
猿六凯
恩恩,谢谢黑脸面试官。
黑脸面试官
你说谁脸黑,你没下一次面试了。
对应leetcode题目:3. 无重复字符的最长子串,难度:中等。
此思路解题详情:
欢迎订阅,每周更新面试高频算法题。从思路,代码,时间复杂度等多方面进行分析。
关注后点击联系我,加我好友备注加群,拉你进刷题打卡群,每月抽奖送奶茶。