Swift写算法|无重复字符的最长子串

632 阅读2分钟

前程提要

相信写过这条算法题的人都知道可以用 滑动窗口 这个思路来解题,我也根据这个思路写一下我的解题过程以及我踩过的坑。

一、未优化的滑动窗口

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%的用户

image.png

为什么时间效率会这么低呢?经过调查资料,我发现虽然用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
    } 

提交后终于得到不错的效果

image.png

总结

算法做多了,其实都有点规律,分而治之,自上而下,空间换时间,还是要多练多写,信心是练习积累起来的。