iOS算法刷题之字符串与数组

238 阅读7分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情 字符串与数组.png

数组是一种基础的数据结构,基本的操作就是增删改查,遍历的方式无非迭代和递归。

数组是顺序存储,链表是链式存储,其他的数据结构如栈、队列、堆、树、图等也都是从数组和链表的数据结构演化出来的,字符串类型一般也会拆分成数组来进行处理或者也按照数组的思路处理,一定意义上来说也是数组的一种(字符串一些特有的算法不在此列,这里说的字符串类的题型都是顺序处理类型的)。

排序

说起数组排序,可能很多人都被问过十大排序算法,问的较多的比如快速排序、归并排序等,虽然数组排序只是LeetCode当中的一道题,但这一道题却有很多种解法。这里推荐一个大佬的文章,讲的非常细致全面 十大排序从入门到入赘

912.数组排序具体的解法这里就只写快排一种了:

class Solution {
    func sortArray(_ nums: [Int]) -> [Int] {
        var sortNums = nums
        quickSort(list: &sortNums, left: 0, right: sortNums.count-1)
        return sortNums
    }

    func quickSort(list: inout [Int], left: Int, right: Int) {
    if left > right {
        // 左边往左边移动,右边往右边移动,最后过了就停止
        return
    }
    var l, r, pivot: Int
    
    l = left
    r = right
    pivot = list[left]
    
    while l != r {
        while list[r] >= pivot && l < r {
            // 左边大的往右边移动
            r -= 1
        }
        list[l] = list[r]
        while list[l] <= pivot && l < r {
            // 右边小的往左边移动
            l += 1
        }
        list[r] = list[l]
    }
    list[l] = pivot
    quickSort(list: &list, left: left, right: l-1)
    quickSort(list: &list, left: l+1, right: right)
}
}

归并排序还有另外一个题315. 计算右侧小于当前元素的个数

遍历

数组的遍历也是常见题型,对于数组来说是可以通过位置来找到相应的值了,遍历也是通过特定指针来进行遍历的。 下面看一下 54. 螺旋矩阵:

题解:创建四个指针:top、bottom、left、right,以及一个result数组用来存放结果。通过left top -> right top, right top -> right bottom, right bottom -> left bottom, left bottom -> left top四个方向不断地向内部遍历,直到遍历完成为止。

class Solution {
    func spiralOrder(_ matrix: [[Int]]) -> [Int] {
    if matrix.count == 0 {
        return []
    }
    var result: [Int] = []
    var top = 0
    var bottom = matrix.count-1
    var left = 0
    var right = matrix[0].count-1
    
    while top <= bottom && left <= right {
        // left top -> right top
        for i in left...right {
            result.append(matrix[top][i])
        }
        top += 1
        if top > bottom {
            break
        }
        // right top -> right bottom
        for i in top...bottom {
            result.append(matrix[i][right])
        }
        right -= 1
        if left > right {
            break
        }
        // right bottom -> left bottom
        for i in (left...right).reversed() {
            result.append(matrix[bottom][i])
        }
        bottom -= 1
        if top > bottom {
            break
        }
        // left bottom -> left top
        for i in (top...bottom).reversed() {
            result.append(matrix[i][left])
        }
        left += 1
        if left > right {
            break
        }
    }
    return result
}
}

数组遍历的其他景点题还有: 48. 旋转图像 59. 螺旋矩阵II

双指针

双指针可以分为快慢指针和左右指针两种,通过移动指针来完成解题。 快慢指针举例: 26. 删除有序数组中的重复项

题解:创建slow、fast两个指针,fast不断向后遍历,当nums[fast] != nums[slow]时将nums[fast]赋值给nums[slow],然后slow++,直到fast指针遍历到最后为止。这时,slow指针指向的是最后一个不重复的元素,所以返回slow+1即可。

class Solution {
    func removeDuplicates(_ nums: inout [Int]) -> Int {
        var slow = 0
        var fast = 0
        while fast < nums.count {
            if nums[fast] != nums[slow] {
                slow += 1
                nums[slow] = nums[fast]
            }
            fast += 1
        }
        return slow+1
    }
}

左右指针举例: 5. 最长回文子串

题解:寻找回文串的问题核心思想是:从中间开始向两边扩散来判断回文串。找回文串的关键技巧是传入两个指针 l 和 r 向两边扩散,因为这样实现可以同时处理回文串长度为奇数和偶数的情况。

class Solution {
    func expandAroundCenter(_ strArr: [String.Element], l: Int, r: Int) -> (l: Int, r: Int, len: Int) {
        var left = l
        var right = r
        while left >= 0 && right < strArr.count && strArr[left] == strArr[right] {
            left -= 1
            right += 1
        }
        return (left+1, right-1, right-left-1)
    }
    func longestPalindrome(_ s: String) -> String {
        if s.count == 0 {
            return ""
        }
        let strArr = Array(s)
        var start = 0, end = 0
        for i in 0..<s.count {
            let len1 = expandAroundCenter(strArr, l: i, r: i)
            let len2 = expandAroundCenter(strArr, l: i, r: i+1)
            if max(len1.len, len2.len) > end - start + 1 {
                if len1.len > len2.len {
                    start = len1.l
                    end = len1.r
                }else {
                    start = len2.l
                    end = len2.r
                }
            }
        }
        return String(strArr[start...end])
    }
}

双指针其他经典题型还有: 83. 删除排序链表中的重复元素 27. 移除元素 283. 移动零 11. 盛最多水的容器 167. 两数之和II-输入有序数组 344. 反转字符串 392. 判断子序列

滑动窗口

滑动窗口的题是有技巧的,思路就是维护一个窗口,不断滑动,然后更新答案。 框架代码如下:

/* 滑动窗口算法框架 */ 
void slidingWindow(string s) { 
    unordered_map<char, int> window;
    
    int left = 0, right = 0; 
    while (right < s.size()) { 
    // c 是将移入窗口的字符 
    char c = s[right]; 
    // 增大窗口 right++; 
    // 进行窗口内数据的一系列更新 
    ... 
    
    /*** debug 输出的位置 ***/ 
    // 注意在最终的解法代码中不要 print 
    // 因为 IO 操作很耗时,可能导致超时 
    printf("window: [%d, %d)\n", left, right); 
    /********************/ 
    
    // 判断左侧窗口是否要收缩 
    while (window needs shrink) { 
        // d 是将移出窗口的字符 
        char d = s[left]; 
        // 缩小窗口 
        left++; 
        // 进行窗口内数据的一系列更新 
        ... 
     } 
  } 
}

滑动窗口举例: 76. 最小覆盖子串:

题解:请看上面的框架。

class Solution {
    func minWindow(_ s: String, _ t: String) -> String {
        // 为了方便处理,这里将字符串转成数组
        let sArray = Array(s)
        var left = 0
        var right = 0
        var need: [Character: Int] = [:]
        var window: [Character: Int] = [:]
        for c in t {
            need[c, default: 0] += 1
        }
        // valid表示窗口中满足need条件的字符,如果vaild和need大小相同,则说明窗口已经满足条件,已经覆盖了串t
        var valid = 0
        // 记录最小覆盖子串的起始索引及长度
        var start = 0
        var len = Int.max
        while right < sArray.count {
            // 开始滑动
            let c = sArray[right]
            //右移窗口
            right += 1
            // 进行窗口内数据的一系列更新
            if need[c] != nil {
                window[c, default: 0] += 1
                if window[c] == need[c] {
                    valid += 1
                }
            }
            // 判断左侧窗口是否要收缩
            while valid == need.count {
                if right - left < len {
                    start = left
                    len = right - left
                }
                // d 是将移除窗口的字符
                let d = sArray[left]
                // 左移窗口
                left += 1
                // 进行窗口内数据的一系列更新
                if need[d] != nil {
                    if window[d] == need[d] {
                        valid -= 1
                    }
                    window[d]! -= 1
                }
            }
        }
        return len == Int.max ? "" : String(sArray[start..<start+len])
    }
}

其他经典滑动窗口的题有: 567. 字符串的排列 438. 找到字符串中所有字母异位词 3. 无重复字符的最长子串

二分搜索

二分搜索思维的精髓是通过已知信息尽可能多地收缩(折半)搜索空间。 二分搜索的框架并不是很复杂,但二分搜索的题的细节很多,边界处理不好,不容易做对。可以看一下我写了首诗,让你闭着眼睛也能写对二分搜索,还有二分查找从入门到入睡。 框架:

int binarySearch(int[] nums, int target) { 
    int left = 0, right = ...; 

    while(...) { 
        int mid = left + (right - left) / 2; 
        if (nums[mid] == target) {
            ... 
        } else if (nums[mid] < target) { 
            left = ... 
        } else if (nums[mid] > target) { 
            right = ... 
        } 
    } 
    return ...;
}

二分搜索题举例: 704. 二分查找

题解:请看上面的框架。

class Solution {
    func search(_ nums: [Int], _ target: Int) -> Int {
        var left = 0
        var right = nums.count - 1
        while left <= right {
            let mid = left + (right - left)/2
            if nums[mid] == target {
                return mid
            }else if nums[mid] > target {
                right = mid - 1
            }else {
                left = mid + 1
            }
        }
        return -1
    }
}

其他经典二分搜索题:34. 在排序数组中查找元素的第一个和最后一个位置

最长递增子序列

最长递增子序列,简称为LIS,是非常经典的一类算法题,可以使用动态规划来解,动态规划后面会单独讲一下。 最长递增子序列举例: 300. 最长递增子序列

dp 数组的定义:dp[i] 表示以 nums[i] 这个数结尾的最长递增子序列的长度。那么 dp 数组中最大的那个值就是最长的递增子序列长度。

class Solution {
    func lengthOfLIS(_ nums: [Int]) -> Int {
        let n = nums.count 
        if n == 0 {
            return 0
        }
        var dp: [Int] = Array(repeating: 1, count: n)
        for i in 1..<n {
            for j in 0..<i {
                if nums[i] > nums[j] {
                    dp[i] = max(dp[i], dp[j]+1)
                }
            } 
        }
        var res = Int.min
        for i in 0..<dp.count {
            res = max(res, dp[i])
        }
        return res
    }
}

其他LIS题还有:354. 俄罗斯套娃信封问题

区间问题

区间问题就是线段问题,让你合并所有线段、找出线段的交集等,一般会用到排序。 区间问题举例: 1288. 删除被覆盖区间

按照区间的起点进行升序排序。排序后会有三种情况:1,两个元素刚好全部覆盖;2,两个元素相交,形成更大的空间;3,两个元素完全不相交。

class Solution {
 func removeCoveredIntervals(_ intervals: [[Int]]) -> Int {
    // 按照起点升序排列,起点相同时降序排列
    let list = intervals.sorted(by: {
        if $0[0] == $1[0]{
            return $1[1] - $0[1] < 0
        }
        return $0[0] - $1[0] < 0
    })
    // 记录合并区间的起点和终点
    var left = list[0][0]
    var right = list[0][1]
    var res = 0
    
    for i in 1..<list.count {
        let temp = list[i]
        // 情况一,找到覆盖区间
        if left <= temp[0] && right >= temp[1] {
            res += 1
        }
        // 情况二,找到相交区间,合并
        if right >= temp[0] && right <= temp[1] {
            right = temp[1]
        }
        // 情况三,完全不相交,更新起点和终点
        if right < temp[0] {
            left = temp[0]
            right = temp[1]
        }
    }
    return list.count - res
}
}

其他经典区间问题的题有:56. 合并区间986. 区间列表的交集435. 无重叠区间452. 用最少数量的箭引爆气球

前缀和

前缀和技巧适用于快速、频繁地计算一个索引区间内的元素之和。 前缀和题距离: 303. 区域和检索 - 数组不可变

题解:标准的前缀和问题,核心思路是用一个新的数组 preSum 记录 nums[0..i-1] 的累加和,如果我想求索引区间 [1, 4] 内的所有元素之和,就可以通过 preSum[5] - preSum[1] 得出。这样,sumRange 函数仅仅需要做一次减法运算,避免了每次进行 for 循环调用,最坏时间复杂度为常数 O(1)。

class NumArray {
    var preSum: [Int] = []
    init(_ nums: [Int]) {
        preSum = Array(repeating: 0, count: nums.count + 1)
        // 计算前缀和
        for i in 1..<preSum.count {
            preSum[i] = preSum[i-1] + nums[i-1]
        }
    }
    
    func sumRange(_ left: Int, _ right: Int) -> Int {
        return preSum[right+1] - preSum[left]
    }
}

其他前缀和题:304. 二维区域和检索 - 矩阵不可变

差分数组

差分数组主要适用场景是频繁对原始数组的某个区间元素进行增减 差分数组举例:1109. 航班预计统计

题解:1、构造差分数组;2、还原原始数组;3、进行区间增减,如果你想对区间 nums[i..j] 的元素全部加 3,那么只需要让 diff[i] += 3,然后再让 diff[j+1] -= 3 即可。本题就相当于给你输入一个长度为 n 的数组 nums,其中所有元素都是 0,然后让你进行一系列区间加减操作,可以套用差分数组技巧。

class Solution {
func corpFlightBookings(_ bookings: [[Int]], _ n: Int) -> [Int] {

    let nums: [Int] = Array(repeating: 0, count: n)
    // 构造差分解法
    let df: Difference = Difference(nums: nums)
    for book in bookings {
        // 注意转成数组索引要减一
        let i = book[0] - 1
        let j = book[1] - 1
        let val = book[2]
        // 对区间 nums[i..j]增加val
        df.increment(i: i, j: j, val: val)
    }
    return df.result()
}
}
class Difference {
    // 差分数组
    private var diff: [Int] = []
    
    // 输入一个初始数组,区间操作将在这个数组上进行
    init(nums: [Int]) {
        if nums.count < 0 {
            return
        }
        diff = Array(repeating: 0, count: nums.count)
        // 根据初始数组构造差分数组
        diff[0] = nums[0]
        for i in 1..<nums.count {
            diff[i] = nums[i] - nums[i-1]
        }
    }
    
    // 给闭区间[i,j]增加val(可以是负数)
    public func increment(i: Int, j: Int, val: Int) {
        diff[i] += val
        if j + 1 < diff.count {
            diff[j + 1] -= val
        }
    }
    
    // 返回结果数组
    public func result() -> [Int] {
        var res = Array(repeating: 0, count: diff.count)
        // 根据差分数组构造结果数组
        res[0] = diff[0]
        for i in 1..<diff.count {
            res[i] = res[i-1] + diff[i]
        }
        return res
    }
}

其他差分数组题:1094. 拼车

总结

以上就是对于数组提醒的一个简单分类了,当然也有很多数组题是不好分类的或者上面举例的题也能分类到其他类型里,这都不是最重要的,重要的是把题解出来然后总结出规律。 还有其他一些经典题,包括但不限于: 1. 两数之和674. 最长连续递增序列128. 最长连续序列4. 寻找两个正序数组的中位数