第1章:数组与字符串——两数之和、三数之和、最长无重复子串、字符串反转与回文判断

数组和字符串,几乎是每一场iOS面试的“开胃菜”。

说实话,这两类题目看似基础,但恰恰是区分“背答案”和“真理解”的分水岭。我面试过不少候选人,两数之和能秒写哈希表解法,但一问为什么不用暴力法,就卡住了。嗯,这章我们就来把这些高频题彻底讲透。

1.1 两数之和:从暴力到哈希的进化

题目很简单:给定一个整数数组和一个目标值,找出数组中和为目标值的两个数的下标。

暴力法——双层循环,O(n²)时间复杂度。我刚开始刷题时也这么写,但有一次在项目里处理一个百万级的数据集,这种写法直接让App卡死了。从那以后,我对O(n²)的解法就特别敏感。

核心思路:用哈希表(Dictionary)存储“当前值”和“目标值的差值”。遍历一次数组,每次检查差值是否已在哈希表中。

// Swift 实现 - 两数之和
func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
    var dict = [Int: Int]() // 值 -> 下标
    for (i, num) in nums.enumerated() {
        let complement = target - num
        if let index = dict[complement] {
            return [index, i]
        }
        dict[num] = i
    }
    return []
}

我的小技巧:面试时先提暴力法,再说“但我们可以优化到O(n)”。这能展示你的思考过程,而不是直接甩出最优解。

1.2 三数之和:双指针的经典应用

三数之和比两数之和难了一个档次。要求找出所有不重复的三元组,使得三个数之和为0。

我个人习惯先排序,再用双指针。为什么排序?因为排序后可以跳过重复元素,避免出现重复的三元组。

// Swift 实现 - 三数之和
func threeSum(_ nums: [Int]) -> [[Int]] {
    guard nums.count >= 3 else { return [] }
    let sorted = nums.sorted()
    var result = [[Int]]()
    
    for i in 0..<sorted.count - 2 {
        // 跳过重复的固定元素
        if i > 0 && sorted[i] == sorted[i - 1] { continue }
        
        var left = i + 1
        var right = sorted.count - 1
        
        while left < right {
            let sum = sorted[i] + sorted[left] + sorted[right]
            if sum == 0 {
                result.append([sorted[i], sorted[left], sorted[right]])
                // 跳过重复的左右指针
                while left < right && sorted[left] == sorted[left + 1] { left += 1 }
                while left < right && sorted[right] == sorted[right - 1] { right -= 1 }
                left += 1
                right -= 1
            } else if sum < 0 {
                left += 1
            } else {
                right -= 1
            }
        }
    }
    return result
}

避坑指南:我曾经在去重逻辑上栽过跟头。记住:固定元素去重用 i > 0 && sorted[i] == sorted[i-1];左右指针去重要在找到一组解之后进行,而不是每次循环都去重。

1.3 最长无重复子串:滑动窗口的实战

这道题在iOS开发中其实很实用。比如处理用户输入的搜索关键词,或者解析网络数据流时,都需要检测无重复的连续子串。

核心思路是滑动窗口 + 哈希表。窗口右边界不断右移,左边界根据重复字符的位置动态调整。

// Swift 实现 - 最长无重复子串
func lengthOfLongestSubstring(_ s: String) -> Int {
    var charIndexMap = [Character: Int]() // 字符 -> 最近一次出现的下标
    var maxLength = 0
    var left = 0
    
    for (right, char) in s.enumerated() {
        // 如果字符已存在且在窗口内,移动左边界
        if let lastIndex = charIndexMap[char], lastIndex >= left {
            left = lastIndex + 1
        }
        charIndexMap[char] = right
        maxLength = max(maxLength, right - left + 1)
    }
    return maxLength
}

面试加分点:可以提一下空间复杂度。虽然用了哈希表,但字符集有限(ASCII 128个或Unicode),所以空间是O(1)级别的。面试官听到这个细节通常会点头。

1.4 字符串反转与回文判断

字符串反转在iOS里太常见了。比如UILabel显示从右到左的文本,或者处理某些加密算法。

最简单的反转:用Swift的reversed()方法。但面试时最好手写一个双指针版本,展示你的基本功。

// 手写字符串反转 - 双指针
func reverseString(_ s: inout [Character]) {
    var left = 0
    var right = s.count - 1
    while left < right {
        s.swapAt(left, right)
        left += 1
        right -= 1
    }
}

回文判断就更直接了。双指针从两端向中间走,遇到非字母数字字符就跳过。

// 验证回文串(忽略大小写和非字母数字)
func isPalindrome(_ s: String) -> Bool {
    let chars = Array(s.lowercased())
    var left = 0
    var right = chars.count - 1
    
    while left < right {
        // 跳过非字母数字
        while left < right && !chars[left].isLetter && !chars[left].isNumber {
            left += 1
        }
        while left < right && !chars[right].isLetter && !chars[right].isNumber {
            right -= 1
        }
        if chars[left] != chars[right] {
            return false
        }
        left += 1
        right -= 1
    }
    return true
}

总结一下这四道题的核心套路:

题目 核心技巧 时间复杂度 空间复杂度
两数之和 哈希表存差值 O(n) O(n)
三数之和 排序 + 双指针 O(n²) O(1) 或 O(n) 排序空间
最长无重复子串 滑动窗口 + 哈希表 O(n) O(字符集大小)
字符串反转/回文 双指针 O(n) O(1)

你想想看,这四道题其实都在训练同一个能力——如何用更少的遍历次数解决问题。从暴力到哈希,从哈希到双指针,再到滑动窗口,每一步优化都是对数据结构的更深理解。

我个人建议,把这四道题当作热身练习。每天写一遍,坚持一周,你的编码手感会有明显提升。下一章我们会聊链表相关的题目,那又是另一番天地了。