前程提要
相信写过这条算法题的人都知道可以用 滑动窗口 这个思路来解题,我也根据这个思路写一下我的解题过程以及我踩过的坑。
一、未优化的滑动窗口
func lengthOfLongestSubstring(_ s: String) -> Int {
if s.count < 2 { return s.count }
var length = 1
var leftIndex = 0
var rightIndex = -1
var counterStr = String()
while leftIndex < s.count {
//右指针+1长度小于s 且 窗口不包含s[右指针+1]的字符
if ( rightNextIndex < s.count && !counterStr.contains( s[s.index(s.startIndex, offsetBy:rightNextIndex)]) )
{
//s[右指针+1]的字符
let rightNextValue = s[s.index(s.startIndex, offsetBy: rightNextIndex)]
counterStr = counterStr + String(rightNextValue)
rightIndex = rightNextIndex
}
else {
counterStr = String(counterStr.dropFirst()) //窗口扔掉第一个元素
leftIndex = leftIndex + 1
}
length = max(length, rightIndex - leftIndex + 1)
}
return length
}
这个代码提交后直接超时了,因为当右指针的下一个元素是重复元素时,左指针只+1,窗口扔掉第一个元素,这种方法在最糟糕的情况,左右指针都要遍历完数组,最多需要执行 2n 个步骤,时间复杂度为O(2n),因此在leeCode上提交会超时。
二、优化后的滑动窗口
func lengthOfLongestSubstring(_ s: String) -> Int {
if s.count < 2 { return s.count }
var length = 1
var leftIndex = 0
var dict = [String :Int]()
for (rightIndex, rightValue) in s.enumerated() {
let rightNextValue:String = String(s[s.index(s.startIndex, offsetBy: rightIndex)])
if dict.keys.contains(rightNextValue) && dict[rightNextValue]! >= leftIndex {
leftIndex = dict[rightNextValue]! + 1
}
length = max(length, rightIndex - leftIndex + 1)
dict[String(rightValue)] = rightIndex
}
return length
}
这是优化后的代码,用字典记录窗口内容,key为s右指针的值,value为s右指针的index,当遇到重复字符时,左指针直接跳到右指针的后一位,这样只需要遍历一次s,时间复杂度为O(n)。
这次代码提交后通过了,但是这份代码在leetCode上执行是4296 ms, 在所有 Swift 提交中击败了5.11%的用户
为什么时间效率会这么低呢?经过调查资料,我发现虽然用dict[key]获取字典的值时间复杂度为O(1),但是由于每次遍历都需要在字典dict中的所有键来查找是否含有这个字符,这个操作的时间复杂度是 O(n),其中 n 是字典中键的数量。所以这一份代码的时间复杂度实际并不是O(n),而是O(keyNum*n)
三、用空间换时间
由于题目提示字符串的内容是由英文字母、数字、符号和空格组成,这些都是在ASCII码128位内的字符,那么是不是可以把字典改成初始空间定义为128位的数组(数组内所有初始值默认都是-1),数组的下标index是s字符串里某个字符的ASCII码,该下标对应的值array[index]存的是该字符对应在字符串s里的下标,然后判断是否存在重复则直接用array[字符的ASCII码]来获取该字符对应在字符串s里的下标charIndex,如果charIndex不是-1,则表示有这个值,碰到重复字符。又用charIndex和左指针对比大小,如果比左指针大,则说明重复的字符在左指针的右边,这时才移动左指针的位置到重复字符的index+1,优化后的代码如下:
func lengthOfLongestSubstring(_ s: String) -> Int {
if s.count < 2 { return s.count }
var length = 1
var leftIndex = 0
var charIndexArray = Array(repeating: -1, count: 128)
for (rightIndex,rightValue) in s.enumerated() {
let ascii = Int(rightValue.asciiValue!)
if charIndexArray[ascii] >= leftIndex { //charIndex[ascii]拿到的是值是s里面的index
leftIndex = charIndexArray[ascii] + 1
}
length = max(length, rightIndex - leftIndex + 1)
charIndexArray[ascii] = rightIndex
}
return length
}
提交后终于得到不错的效果
总结
算法做多了,其实都有点规律,分而治之,自上而下,空间换时间,还是要多练多写,信心是练习积累起来的。