[LeetCode]#3 Longest Substring Without Repeating Characters

一、题目

Given a string, find the length of the longest substring without repeating characters. For example, the longest substring without repeating letters for "abcabcbb" is "abc", which the length is 3. For "bbbbb" the longest substring is "b", with the length of 1.

 

二、解析

求最大不重复子序列的长度,只需认清一个事实:若新来字符与已有自序列中的字符重复,则自序列该重复字符之前(包括自身)都不可用,只保留之后的。

例如abcbe,这里b是重复的,所以第一个b之前(包括自身)所组成的字串都是重复的,如abcb, bcb。所以要删除ab,保留cbe,继续向下走。

 

三、实现思路

1、用串+hash实现,通过字符串操作实现解析中步骤。将目前得到的最长字串存到哈希表中。144ms

2、用串实现,通过字符串操作实现解析中步骤,用maxlen记录目前最长字串长度。104ms

3、用hash实现,通过哈希操作实现解析中步骤,同时保留串做辅助。500ms

 

四、难点

  串的难点,会了之后好像没什么难点。主要还是思路上吧,最开始思路有点跑偏,将重复字符分成两种情况:1.与当前串第一个字符相同,就用类似队列的方式把第一个挤出去。2.与当前串其中一个字符相同,就截掉当前串之前的,从重复处开始:比如dvdf,截掉后为df。但是这样明显不对,最长应该是vdf才对。

  与同学讨论后发现,我这样分没错,的确只有这两种情况,但是错在对第2中问题的处理方法不够全面,截断的不对。所以还是按照解析中的来,可以包括所有情况。

 

五、Python代码(三个版本)

1、list+hash

class Solution:
    # @param {string} s
    # @return {integer}
    
    _dict = {}
    def createHashTable(self, longestSub):
        global _dict
        value = longestSub
        key = len(value)
        isExist = _dict.get(key, 'no')
        if isExist == 'no':
            _dict[key] = value
        else:
            pass
        
    def lengthOfLongestSubstring(self, s):
        global _dict
        longestSub = ""
        length = len(s)
        _dict = {}
        for cur in range(length):
       #检查当前要元素s[cur]是否已经在当前最长自序列longestSub里
       #如果不在,则说当前元素是新的,直接加进来。之所以建立哈希表是为了存储当前最长值,为{len:longestSub}
       if s[cur] not in longestSub: longestSub += s[cur] self.createHashTable(longestSub)
       #如果在,就按照解析中做法,删除重复元素之前的元素,加上当前元素后组成longestSub。
#这里不用哈希操作是因为这里是减字符,肯定少,没必要存。
else: pos = longestSub.index(s[cur]) longestSub = longestSub[pos+1:] + s[cur]
#字典中key为子串的长度,得到长度,得到最大,返回即可。 checkMaxList
= _dict.keys() if len(checkMaxList) == 0: return 0 else: longestInt = max(checkMaxList) return longestInt

   这种方法运行时间为144ms,在Python排名中排中间靠后。后来想了一下,完全没必要用哈希去存当前最大,本题只要返回最大长度即可,用一个变量就可以搞定。至于为什么最开始会用哈希,是因为看了一眼题目的tag,里面有hash......第二种方法是纯list实现

2、list

 1 class Solution:
 2     # @param {string} s
 3     # @return {integer}
 4     def lengthOfLongestSubstring(self, s):
 5         longestSub = ""
 6         length = len(s)
 7         maxlen = 0
 8         _dict = {}
 9         for cur in range(length):
10             if s[cur] not in longestSub:
11                 longestSub += s[cur]
12                 len_sub = len(longestSub)
13                 if len_sub > maxlen:
14                     maxlen = len_sub
15                 else:
16                     pass
17             else:
18                 pos = longestSub.index(s[cur]) 
19                 longestSub = longestSub[pos + 1 :] + s[cur]
20                 
21         return maxlen

这样就清爽多了,执行时间104ms,在Python中排在前10%,还能怎么快呢?

当前时间复杂度应该是O(n^2),第一反应是优化成O(nlogn),但这样就得有二分查找的过程。但本题中子串并不是按顺序排列的,怎么优化呢?感觉这条路走不通。

分析了下程序,感觉开销应该在查找longestSub中是否存在重复字符上,即if s[cur] in longestSub,是遍历的所以比较慢。于是我将这一步换成了在哈希中查找,那查找的复杂度不就是O(1)了吗?

3、Hash

 1 class Solution:
 2     # @param {string} s
 3     # @return {integer}
 4     def lengthOfLongestSubstring(self, s):
 5         longestSub = ""
 6         length = len(s)
 7         maxlen = 0
 8         _dict = {}
 9         for cur in range(length):
10             #check wheather the current character already exist in the dict
11             pos = _dict.get(s[cur], 'no')
12             #用字典方法查找,复杂度为O(1)13             if pos == "no":
14                 longestSub += s[cur]
15                 _dict[s[cur]] = longestSub.index(s[cur])
16                 len_dict = len(_dict)
17                 if len_dict > maxlen:
18                     maxlen = len_dict
19                 else:
20                     pass
21             #in, cut off the previous substring, and change the value by -
22             else:
#tempString为重复字符之前的子串,需要被删除的。用tempString做索引,删除dict中相应字符
23 tempString = longestSub[: pos + 1] 24 longestSub = longestSub[pos + 1 :] + s[cur] 25 #del previous character in dict. 26 for i in tempString: 27 _dict.pop(i) 28 for i in longestSub[:-1]: 29 _dict[i] = _dict[i] - pos - 1
#新来的重复字符,由于已经删掉之前重复,现在这个就变得不重复了,放在最后,用str中的下标作为value 30 _dict[s[cur]] = longestSub.index(s[cur]) 31 32 return maxlen

这个方法执行时间为508ms.....在排名上已经靠后的找不着了。为什么会这么慢呢,已经解决查找的问题了呀。分析决定慢在这里:

1 for i in tempString:
2     _dict.pop(i)
3 for i in longestSub[:-1]:
4     _dict[i] = _dict[i] - pos - 1

  即要遍历hash表,去删除重复子串,并且需要挨个修改不重复子串的下标。好吧,顾的上这一头,就顾不上那一头。。为什么需要下标呢?

  因为哈希表的建立是用longestSub<string>来做索引,比如"abc",在哈希中表示为{'a':0, 'b':1, 'c':2}。这样当有重复时,比如abcb,先得到重复字符b在哈希中的value值为1,这一步完成了longestSub中的查找,复杂度为O(1),解决了方法二中的问题。得到后pos为1后,要删除的是"ab",保留的是"cd"。然后就在哈希中pop(a,b),修改哈希中c的value为2-1-1=0,并加上b,为1,即哈希变成{'c':0,'b':1}。这样一来,哈希和longestSub就时刻对应,相互依存。

  如何解决哈希变更value的问题,使得真正发挥哈希的效果呢?看看discuss找找启发再看吧。

 

==================================================================

补充,O(n)算法。

从理论上来说,用哈希一定会比字符串要快的多,可问题出在哪里呢?其实上面已经分析出来了:在我只想着用哈希去做查找,然后照搬字符串的方法去挨个的做删除和修改。这样一来,没遍历出一个字符,就会遍历一遍哈希表,这样复杂度依然是O(n^2)。

既然症结出在对哈希表的遍历,那么改一下,不逐个改变哈希中value值不就行了吗?那要改变什么呢——改变从哪一位开始算。就像排队取票,前面取完一个走一个,后面往上补,每个人的标号都要减一,这是我方法三的做法;但是也可以人群不懂,售票点挨个往前挪,这样的话,不是就可以保证不改变哈希表中的value值,也可以实现同样的效果嘛~

代码如下:

 1 class Solution:
 2     # @param {string} s
 3     # @return {integer}
 4     def lengthOfLongestSubstring(self, s):
 5         _dict = {}
 6         maxlen = start = 0
 7     
 8         for i in range(len(s)):
 9             if s[i] in _dict and start <= _dict[s[i]]:
10                 start = _dict[s[i]] + 1
11                 
12             else:
13                 maxlen = max(maxlen, i - start + 1)
14                 
15             _dict[s[i]] = i
16             
17         return maxlen

非常简洁对不对?是不是看不太懂,我来具体分析一下代码为什么这么写。

想要实现O(n)的时间复杂度,方法只有一个,即只遍历一遍必须遍历的字符串,在遍历过程中不能有任何的遍历操作。这样的话,就只能在遍历的过程中保存最大值,即maxlen。

每遍历出一个字符,就将该字符标一个号放入哈希表中,这里由_dict[s[i]] = i实现。无论遍历出的字符有没有发生重复,都要加到当前子串中,所以这句放在if..else外边。start用来表示当前不重复子串的开始位置(取票窗口的位置),即如果当前字符与子串中已经重复(前面人取完票),就将窗口位置向后移,移到没取票的人那里(不重复的字符)。例如s="abcb",假设已经遍历了3个字符’c‘,当前子串为"abc" ,start = 0,现在要遍历第四个字符,即'b',发现'b'与之前的重复,那么就将start=_dict['b'] + 1 = 2,也就说从第三位的'c'开始往后,是新的子串。

maxlen是用来保存最长子串的长度的,是maxlen和i-start+1中比较大的那个。还是"abcb"的例子,当子串为"abc"时,maxlen=3;当发生重复,子串变成“cb”时,i-start+1=2,没有3大,所以maxlen依然是3。其实i - start + 1可以这样理解:i是当前s中遍历的位置,是当前子串的终止位置,start是表示s中迄今为止不重复子串的开始位置,那么长度自然就是i-start+1了。

另外可能这句判断有些让人摸不着头脑,就是”and start <= _dict[s[i]]“,最开始我也没想到,但是有一个例子过不去,就是"tmmzuxt"。在这里,在第一次重复中,我们只修改了'm’的值,start=_dict['m'] + 1 = 2就往后开始新子串了。到'x'的时候,start = 2, i = 5, maxlen = 4。这时到't'了,和当前最大子串”mzux“没有重复的,那么maxlen应该为5才对,但是程序会怎么走呢?由于最开始哈希表中有't',而且{‘t’:0},所以到最后一个t时,程序就判定当前t是重复的,start = _dict['t'] + 1 = 0 + 1 = 1,start又回去了,这当然不对的。所以要加上一个限制条件,不让start往已经被抛弃的子串中走,就是当重复且start<=_dict[s[i]]的时候,才改变start的值。

回到终极问题上来,为什么哈希比字符串的方法要快?是因为这是两种保存当前不重复子串的方法。如果有哈希,那么查找的时间复杂度就是O(1),如果是字符串,那就是O(n)。总结一下,与字符串s相关的是:start, i。与当前子串相关的是_dict。用于统计结果的是maxlen。

 

六、总结

1、知道了LeetCode中定义子函数、使用全局变量的方法。

2、可能细心的同学在看我代码时会发现一个问题,那就是“舍近求远,生搬硬套,想法僵硬”。为什么这么说呢,因为在方法一中,我用哈希去存储{最大值:子串},而不是直接用一个参数去存(方法二)。方法三中,想用哈希,却还要多余的用一个longestSub去做索引,但其实不多于,因为我是按照取票人往前补的想法来的。直到方法4,看了别人的代码,才最终采取取票点往后挪的方法。

怎么说呢,还是不够敏捷,变通太少。一看题目的tag是哈希,就非想往哈希上靠,所以才有了方法一中的用哈希去存储最大值,想想也是蛮奇怪的,根本就没理解哈希是用来取代list的。吃一堑长一智,写了这么长的解题报告,不在于要讲清楚题怎么做,代码怎么写,而是我自己在理清思路,知道哪里有收获,哪里还欠缺。

 

posted @ 2015-07-08 10:37  面包包包包包包  阅读(151)  评论(0编辑  收藏  举报