iOS 算法整理

128 阅读9分钟

参考来源:iOS面试之道 (豆瓣) (douban.com)

推荐阅读:Hello 算法 (hello-algo.com)

数组

1. 两数之和 - 力扣(LeetCode)

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target  的那 两个 整数,并返回它们的数组下标。

func twoSum(_ nums: [Int], _ target: Int) -> [Int] {
    var dict = [Int: Int]()
    for (i, num) in nums.enumerated() {
        if let lastIndex = dict[target - num] {
            return [lastIndex, i]
        } else {
            dict[num] = i
        }
    }
    fatalError("No valid output")
}
/*
* 哈希表
* 时间复杂度:O(N),其中 N 是数组中的元素数量。对于每一个元素 x,我们可以 O(1) 地寻找 target - x。
* 空间复杂度:O(N),其中 N 是数组中的元素数量。主要为哈希表的开销。
*/

字符串

344. 反转字符串 - 力扣(LeetCode)

编写一个函数,其作用是将输入的字符串反转过来。输入字符串以字符数组 s 的形式给出。 不要给另外的数组分配额外的空间,你必须原地修改输入数组、使用 O(1) 的额外空间解决这一问题。

func reverseString1(_ s: inout [Character]) {
     // 双指针 - 元组
     var l = 0
     var r = s.count - 1
     while l < r {
         (s[l], s[r]) = (s[r], s[l])
         l += 1
         r -= 1
     }
}
func reverseString2(_ s: inout [Character]) {
    // 双指针 - 库函数
    var j = s.count - 1
    for i in 0 ..< Int(Double(s.count) * 0.5) {
        s.swapAt(i, j)
        j -= 1
    }
}
/*
* 双指针
* 时间复杂度:O(N),其中 N 为字符数组的长度。一共执行了 N/2 次的交换。
* 空间复杂度:O(1)。只使用了常数空间来存放若干变量。
*/

链表

141. 环形链表 - 力扣(LeetCode)

给你一个链表的头节点 head ,判断链表中是否有环。

func hasCycle(_ head: ListNode?) -> Bool {
    var slow = head
    var fast = head
    while fast != nil && fast!.next != nil {
        slow = slow!.next
        // 快指针两倍速前进
        fast = fast!.next!.next
        if slow === fast {
            return true
        }
    }
    return false
}
/*
* 快慢指针
* 时间复杂度:O(N),其中 N 是链表中的节点数
* - 当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。
* - 当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N 轮
* 空间复杂度:O(1)。我们只使用了两个指针的额外空间。
*/

86. 分隔链表 - 力扣(LeetCode)

给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。 你应当 保留 两个分区中每个节点的初始相对位置。

func partition(_ head: ListNode?, _ x: Int) -> ListNode? {
    // 引入dummy节点
    let pervDummy = ListNode(0), postDummy = ListNode(0)
    var prev = pervDummy, post = postDummy
    var node = head
    // 使用尾插法处理左边和右边
    while node != nil {
        if node!.val < x {
            prev.next = node
            prev = node!
        } else {
            post.next = node
            post = node!
        }
        node = node!.next
    }
    // 防止构成环
    post.next = nil
    // 左右拼接
    prev.next = postDummy.next
    return pervDummy.next
}
/*
* 快行指针(两个指针相差2倍速)
* 时间复杂度: O(n),其中 n 是原链表的长度。我们对该链表进行了一次遍历。
* 空间复杂度:O(1)。
*/

19. 删除链表的倒数第 N 个结点 - 力扣(LeetCode)

给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。

func removeNthFromEnd(_ head: ListNode?, _ n: Int) -> ListNode? {
    guard let head = head else {
        return nil
    }
    let dummy = ListNode(0)
    dummy.next = head
    var prev: ListNode? = dummy
    var post: ListNode? = dummy
    // 设置后一个节点的初始位置
    for _ in 0..<n {
        if post == nil {
            break
        }
        post = post!.next
    }
    // 同时移动前后节点
    while post != nil && post!.next != nil {
        prev = prev!.next
        post = post!.next
    }
    // 删除节点
    prev!.next = prev!.next!.next
    return dummy.next
}
/*
* 快行指针(两个指针相差n个节点)
* 时间复杂度: O(n),其中 n 是原链表的长度。
* 空间复杂度:O(1)。 
*/

栈和队列

71. 简化路径 - 力扣(LeetCode)

func simplifyPath(_ path: String) -> String {
    // 用数组实现栈的功能
    var pathStack = [String]()
    // 拆分原路径
    let paths = path.components(separatedBy: "/")
    for path in paths {
        // 对于"."直接跳过
        guard path != "." else {
            continue
        }
        // 对于 ".." 使用 pop 操作
        if path == ".." {
            if (pathStack.count > 0) {
                pathStack.removeLast()
            }
        // 对于空数组的特殊情况
        } else if path != "" {
            pathStack.append(path)
        }
    }
    // 将栈中的内容转化为优化后的路径
    let res = pathStack.reduce("") { total, dir in "\(total)/\(dir)" }
    // 注意空路径的结果是 "/"
    return res.isEmpty ? "/" : res
}
/*
* 栈
* 时间复杂度:O(n),其中 n 是字符串 path 的长度。
* 空间复杂度:O(n)。我们需要 O(n) 的空间存储 paths 中的所有字符串。
*/

二叉树

二叉树

// ----- 递归方式
//前序遍历  根节点 -> 左节点 -> 右节点
func preOrder(_ root: TreeNode?) -> Void {
    guard root != nil else { return }
    print("\(root!.val)\t", terminator: "")
    preOrder(root!.left)
    preOrder(root!.right)
}

//中序遍历 左节点 -> 根节点 -> 右节点
func midOrder(_ root: TreeNode?) -> Void {
    guard root != nil else { return }
    midOrder(root!.left)
    print("\(root!.val)\t", terminator: "")
    midOrder(root!.right)
}

//后序遍历  左节点  -> 右节点 -> 根节点
func afterOrder(_ root: TreeNode?) -> Void {
    guard root != nil else { return }
    afterOrder(root!.left)
    afterOrder(root!.right)
    print("\(root!.val)\t", terminator: "")
}

104. 二叉树的最大深度 - 力扣(LeetCode)

给定一个二叉树,找出其最大深度。 二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。 说明: 叶子节点是指没有子节点的节点。

func maxDepth(_ root: TreeNode?) -> Int {
    guard let root = root else { return 0 }
    return max(maxDepth(root.left), maxDepth(root.right)) + 1
}
/*
* 递归(深度优先搜索)
* 时间复杂度:O(n),n为二叉树节点的个数。每个节点在递归中只被遍历一次。
* 空间复杂度:O(height),其中height表示二叉树的高度。递归函数需要栈空间,而栈空间取决于递归的深度,因此空间复杂度等价于二叉树的高度。
*/

98. 验证二叉搜索树 - 力扣(LeetCode)

给你一个二叉树的根节点 root ,判断其是否是一个有效的二叉搜索树。

有效 二叉搜索树定义如下:

  • 节点的左子树只包含 小于 当前节点的数。
  • 节点的右子树只包含 大于 当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。
func isValidBST(_ root: TreeNode?) -> Bool {
    return _help(root, Int.min, Int.max)
}
func _help(_ node: TreeNode?, _ low: Int, _ upper: Int) -> Bool {
    guard let node = node else { return true }
    // 所有右子树节点的值都必须大于根节点的值
    if low != Int.min && node.val <= low {
        return false
    }
    // 所有左子树节点的值都必须小于根节点的值
    if upper != Int.max && node.val >= upper {
        return false
    }
    return _help(node.left, low, node.val) && _help(node.right, node.val, upper)
}
/*
* 递归
* 时间复杂度:O(n),其中 n 为二叉树的节点个数。在递归调用的时候二叉树的每个节点最多被访问一次,因此时间复杂度为 O(n)。
* 空间复杂度:O(n),其中 n 为二叉树的节点个数。递归函数在递归过程中需要为每一层递归函数分配栈空间,所以这里需要额外的空间且该空间取决于递归的深度,即二叉树的高度。最坏情况下二叉树为一条链,树的高度为 n ,递归最深达到 n 层,故最坏情况下空间复杂度为 O(n) 。
*/

二叉树遍历

二叉树遍历

144. 二叉树的前序遍历 - 力扣(LeetCode)

// 抄自书
func preorderTraversal(_ root: TreeNode?) -> [Int] {
    var res = [Int]()
    var stack = [TreeNode]()
    var node = root
    while !stack.isEmpty || node != nil {
        if node != nil {
            res.append(node!.val)
            stack.append(node!)
            node = node!.left
        } else {
            node = stack.removeLast().right
        }
    }
    return res;
}
// 力扣作者:huang-ye-v
func preorderTraversal(_ root: TreeNode?) -> [Int] {
    guard root != nil else { return [] }//空拦截
    var preOrderAry = [Int]()
    var stack = [TreeNode]()//辅助栈
    var cur = root
    while stack.isEmpty == false || cur != nil {
        while cur != nil {//左子树探底
            stack.append(cur!)
            preOrderAry.append(cur!.val)
            cur = cur!.left
        }
        let tmp = stack.popLast()!//栈顶元素出栈
        cur = tmp.right// 转到右子树继续循环
    }
    return preOrderAry
}
/*
* 迭代「DFS」
* 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
* 空间复杂度:O(n),为迭代过程中显式栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
*/

94. 二叉树的中序遍历 - 力扣(LeetCode)

// 111
func inorderTraversal(_ root: TreeNode?) -> [Int] {
    var res = [Int]()
    var stack = [TreeNode]()
    var node = root
    while !stack.isEmpty || node != nil {
        if node != nil {
            stack.append(node!)
            node = node!.left
        } else {
            node = stack.removeLast()
            res.append(node!.val)
            node = node!.right
        }
    }
    return res;
}
// 力扣作者:huang-ye-v
func inorderTraversal(_ root: TreeNode?) -> [Int] {
    guard root != nil else { return []}
    var inorderAry = [Int]()
    var stack = [TreeNode]()
    var cur = root
    while stack.isEmpty == false || cur != nil {
        while cur != nil {
            stack.append(cur!)
            cur = cur!.left
        }
        let tmp = stack.popLast()!
        inorderAry.append(tmp.val)//访问节点的操作放在出栈的时候
        cur = tmp.right
    }
    return inorderAry
}
/*
* 迭代「DFS」
* 时间复杂度:O(n),其中 n 为二叉树节点的个数。二叉树的遍历中每个节点会被访问一次且只会被访问一次。
* 空间复杂度:O(n)。空间复杂度取决于栈深度,而栈深度在二叉树为一条链的情况下会达到 O(n) 的级别。
*/

145. 二叉树的后序遍历 - 力扣(LeetCode)

func postorderTraversal(_ root: TreeNode?) -> [Int] {
    var res = [Int]()
    var stack = [TreeNode]()
    var node = root
    var prev: TreeNode? = nil
    while !stack.isEmpty || node != nil {
        while node != nil {
            stack.append(node!)
            node = node!.left
        }
        node = stack.removeLast()
        if node!.right == nil || node!.right === prev {
            res.append(node!.val)
            prev = node!
            node = nil
        } else {
            stack.append(node!)
            node = node!.right
        }
    }
    return res;
}
// 力扣作者:huang-ye-v
 func postorderTraversal(_ root: TreeNode?) -> [Int] {
    guard root != nil else { return []}
    var postorderAry = [Int]()
    var stack = [TreeNode]()
    var cur = root
    while stack.isEmpty == false || cur != nil {
        while cur != nil {
            stack.append(cur!)
            postorderAry.append(cur!.val)
            cur = cur!.right
        }
        let tmp = stack.popLast()!
        cur = tmp.left
    }
    return postorderAry.reversed()
}

102. 二叉树的层序遍历 - 力扣(LeetCode)

// 代码随想
func levelOrder(_ root: TreeNode?) -> [[Int]] {
    var result = [[Int]]()
    guard let root = root else { return result }
    // 表示一层
    var queue = [root]
    while !queue.isEmpty {
        let count = queue.count
        var subarray = [Int]()
        for _ in 0 ..< count {
            // 当前层
            let node = queue.removeFirst()
            subarray.append(node.val)
            // 下一层
            if let node = node.left { queue.append(node) }
            if let node = node.right { queue.append(node) }
        }
        result.append(subarray)
    }
    return result
}
// 作者:huang-ye-v
func BFSOrder(root:TreeNode?) -> [Int] {
    guard root != nil else { return [] }
    var queue:[TreeNode] = [root!]// 队列辅助,根节点入队
    var rst = [Int]()
    while queue.isEmpty == false {
        let node = queue.removeFirst()
        rst.append(node.val)
        if node.left != nil {
            queue.append(node.left!)
        }
        if node.right != nil {
            queue.append(node.right!)
        }
    }
    return rst
}
/* 广度优先搜索「BFS」
* 时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
* 空间复杂度:队列中元素的个数不超过 n 个,故渐进空间复杂度为 O(n)。
*/

排序和搜索

剑指 Offer II 074. 合并区间 - 力扣(LeetCode)

// 合并区间 作者:shen-jing-wa-45n  时间O(nlogn)  空间O(1)
func merge(_ intervals: [[Int]]) -> [[Int]] {
    // 先排序 按照 首位小的在前
    let intervals = intervals.sorted { $0[0] < $1[0] }
    // 存放结果,先把 0 位放进去
    var ans: [[Int]] = [intervals[0]]
    // 从 1 位开始和结果比较
    for j in 1 ..< intervals.count {
        // 待处理数据
        var row = intervals[j]
        for i in 0 ..< ans.count {
            // 已有结果
            var list = ans[i]
            // 待处理最小值 在 已有结果 区间内部,则有重叠
            if row[0] <= list[1] {
                // 因为一开始已经排序了,只需更新最大区间
                list[1] = max(list[1], row[1])
                ans[i] = list
                // 清空当前待处理数据
                row = []
                break
            }
        }
        // 当前待处理数据不在区间内 加入结果
        if !row.isEmpty {
            ans.append(row)
        }
    }
    return ans
}
let intervals = [[1,3],[2,6],[8,10],[15,18]]
print(merge(intervals))
/* 
 * 时间复杂度:O(nlogn),其中 n 为区间的数量。除去排序的开销,
 * 我们只需要一次线性扫描,所以主要的时间开销是排序的 O(nlogn)。
 * 空间复杂度:O(logn),其中 n 为区间的数量。这里计算的是存储答案之外,
 * 使用的额外空间。O(logn) 即为排序所需要的空间复杂度。
 */

704. 二分查找 - 力扣(LeetCode)

给定一个 n 个元素有序的(升序)整型数组 nums 和一个目标值 target  ,写一个函数搜索 nums 中的 target,如果目标值存在返回下标,否则返回 -1。

// 代码随想
// (版本一)左闭右闭区间
func search1(_ nums: [Int], _ target: Int) -> Int {
    // 1. 先定义区间。这里的区间是[left, right]
    var left = 0
    var right = nums.count - 1
    while left <= right {// 因为taeget是在[left, right]中,包括两个边界值,所以这里的left == right是有意义的
        // 2. 计算区间中间的下标(如果left、right都比较大的情况下,left + right就有可能会溢出)
        // let middle = (left + right) / 2
        // 防溢出:
        let middle = left + (right - left) / 2
        // 3. 判断
        if target < nums[middle] {
            // 当目标在区间左侧,就需要更新右边的边界值,新区间为[left, middle - 1]
            right = middle - 1
        } else if target > nums[middle] {
            // 当目标在区间右侧,就需要更新左边的边界值,新区间为[middle + 1, right]
            left = middle + 1
        } else { 
            // 当目标就是在中间,则返回中间值的下标
            return middle
        }
    }
    // 如果找不到目标,则返回-1
    return -1
}  
/*
 * 时间复杂度:O(logn),其中 n 是数组的长度。
 * 空间复杂度:O(1)
 */
    
// (版本二)左闭右开区间
func search2(_ nums: [Int], _ target: Int) -> Int {
    var left = 0
    var right = nums.count
    while left < right {
        let middle = left + ((right - left) >> 1)
        if target < nums[middle] {
            right = middle
        } else if target > nums[middle] {
            left = middle + 1
        } else {
            return middle
        }
    }
    return -1
}

278. 第一个错误的版本 题解 - 力扣(LeetCode)

假设你有 n 个版本 [1, 2, ..., n],你想找出导致之后所有版本出错的第一个错误的版本。

func firstBadVersion(_ n: Int) -> Int {
    // 处理特殊情况
    guard n >= 1 else { return -1 }
    var left = 1, right = n, mid = 0
    while left < right {
        mid = (right - left) / 2 + left
        if isBadVersion(mid) {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return left // return right 同样正确
}
/*
 * 时间复杂度:O(logn),其中 n 是给定版本的数量。
 * 空间复杂度:O(1)。我们只需要常数的空间保存若干变量。
 */

33. 搜索旋转排序数组 - 力扣(LeetCode)

给你 旋转后 的数组 nums 和一个整数 target ,如果 nums 中存在这个目标值 target ,则返回它的下标,否则返回 -1 。

func search(_ nums: [Int], _ target: Int) -> Int {
    var (left, mid, right) = (0, 0, nums.count - 1)
    while left <= right {
        mid = (right - left) / 2 + left
        if nums[mid] == target {
            return mid
        }
        // 旋转位置特别靠前
        if nums[mid] >= nums[left] {
            if nums[mid] > target && target >= nums[left] {
                right = mid - 1
            } else {
                left = mid + 1
            }
        } else {// 旋转位置特别靠后
            if nums[mid] < target && target <= nums[right] {
                left = mid + 1
            } else {
                right = mid - 1
            }
        }
    }
    return -1
}
/*
 * 时间复杂度:O(logn),其中 n 为 nums 数组的大小。整个算法时间复杂度即为二分查找的时间复杂度 O(logn)。
 * 空间复杂度:O(1)。我们只需要常数级别的空间存放变量。
 */

35. 搜索插入位置 - 力扣(LeetCode)

给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引。如果目标值不存在于数组中,返回它将会被按顺序插入的位置。

// 作者-代码随想
// 暴力法
func searchInsert1(_ nums: [Int], _ target: Int) -> Int {
    for i in 0..<nums.count {
        if nums[i] >= target {
            return i
        }
    }
    return nums.count
}

// 二分法
func searchInsert(_ nums: [Int], _ target: Int) -> Int {
    var left = 0
    var right = nums.count - 1

    while left <= right {
        let middle = left + ((right - left) >> 1)

        if nums[middle] > target {
            right = middle - 1
        }else if nums[middle] < target {
            left = middle + 1
        }else if nums[middle] == target {
            return middle
        }
    }

    return right + 1
}
/*
 * 时间复杂度:O(logn),其中 n 为数组的长度。二分查找所需的时间复杂度为 O(logn)。
 * 空间复杂度:O(1)。只需要常数的空间保存若干变量。
 */

深度优先搜索和广度优先搜索

542. 01 矩阵 - 力扣(LeetCode)

给定一个由 0 和 1 组成的矩阵 mat ,请输出一个大小相同的矩阵,其中每一个格子是 mat 中对应位置元素到最近的 0 的距离。

func updateMatrix(_ mat: [[Int]]) -> [[Int]] {
    let m = mat.count
    let n = mat[0].count
    var ans = Array(repeating: Array(repeating: 0, count: n), count: m)
    var visited = Array(repeating: Array(repeating: false, count: n), count: m)
    var queue: ArraySlice<(Int, Int)> = []

    for i in 0 ..< m {
        for j in 0 ..< n where mat[i][j] == 0 {
            queue.append((i, j))
            visited[i][j] = true
        }
    }
    while !queue.isEmpty {
        let pos = queue.removeFirst()
        let next = [(pos.0 + 1, pos.1), (pos.0 - 1, pos.1), (pos.0, pos.1 + 1), (pos.0, pos.1 - 1)]

        for (i, j) in next {
            if (0 ..< m).contains(i), (0 ..< n).contains(j), !visited[i][j] {
                queue.append((i, j))
                visited[i][j] = true
                ans[i][j] = ans[pos.0][pos.1] + 1
            }
        }
    }
    return ans
}
/*
 * 广度优先搜索
 * 时间复杂度:O(mn),其中 m 为矩阵行数,n 为矩阵列数,即矩阵元素个数。
 * 广度优先搜索中每个位置最多只会被加入队列一次,因此只需要 O(mn) 的时间复杂度。
 * 空间复杂度:O(mn),其中 m 为矩阵行数,n 为矩阵列数,即矩阵元素个数。
 * 除答案数组外,最坏情况下矩阵里所有元素都为 0,全部被加入队列中,此时需要 O(mn) 的空间复杂度。
 */

79. 单词搜索 - 力扣(LeetCode)

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

func exist(_ board: [[Character]], _ word: String) -> Bool {
    guard board.count > 0 && board[0].count > 0 else {
        return false
    }
    let (m, n) = (board.count, board[0].count)
    var visited = Array(repeating: Array(repeating: false, count: n), count: m)
    let wordContent = [Character](word)

    for i in 0 ..< m {
        for j in 0 ..< n {
            if dfs(board, wordContent, m, n, i, j, &visited, 0) {
                return true
            }
        }
    }
    return false
}
func dfs(_ board: [[Character]], _ wordContent: [Character], _ m: Int, _ n: Int, _ i: Int, _ j: Int, _ visited: inout [[Bool]], _ index: Int) -> Bool{
    if index == wordContent.count {
        return true
    }
    guard i >= 0 && i < m && j >= 0 && j < n else {
        return false
    }
    guard !visited[i][j] && board[i][j] == wordContent[index] else {
        return false
    }
    visited[i][j] = true
    if dfs(board, wordContent, m, n, i + 1, j, &visited, index + 1) || dfs(board, wordContent, m, n, i - 1, j, &visited, index + 1) || dfs(board, wordContent, m, n, i, j + 1, &visited, index + 1) || dfs(board, wordContent, m, n, i, j - 1, &visited, index + 1) {
        return true
    }
    visited[i][j] = false
    return false
}
/*
 * 深度优先搜索 + 回溯
 * 时间复杂度:一个非常宽松的上界为 O(MN⋅3 L),其中 M,N 为网格的长度与宽度,L 为字符串 word 的长度。
 * 在每次调用函数 check 时,除了第一次可以进入 4 个分支以外,其余时间我们最多会进入 3 个分支(因为每个位置只能使用一次,所以走过来的分支没法走回去)。
 * 由于单词长为 L,故 check(i,j,0) 的时间复杂度为 O(3 L),而我们要执行 O(MN) 次检查。
 * 然而,由于剪枝的存在,我们在遇到不匹配或已访问的字符时会提前退出,终止递归流程。
 * 因此,实际的时间复杂度会远远小于 Θ(MN⋅3 L)。
 * 空间复杂度:O(MN)。我们额外开辟了 O(MN) 的 visited 数组,同时栈的深度最大为 O(min(L,MN))。
 */

动态规划

72. 编辑距离 - 力扣(LeetCode)

给你两个单词 word1 和 word2, 请返回将 word1 转换成 word2 所使用的最少操作数  。

func minDistance(_ word1: String, _ word2: String) -> Int {
    if word1.count == 0 {return word2.count}
    if word2.count == 0 {return word1.count}
    var dp:[Int] = Array(repeating: 0, count: word2.count + 1)
    let ary1 = Array(word1)
    let ary2 = Array(word2)
    for j in 0...ary2.count{
        dp[j] = j
    }
    for i in 1...ary1.count{
        var cur = i - 1
        dp[0] = i - 1
        for j in 1...ary2.count{
            var leftTop = cur
            cur = dp[j]
            let left = dp[j - 1] + 1
            let top = dp[j] + 1
            if ary1[i - 1] != ary2[j - 1] {
                leftTop = leftTop + 1
            }
            dp[j] = min(min(left, top), leftTop)
        }
    }
    return dp[word2.count]
}
/*
 * 动态规划
 * 时间复杂度:O(mn),其中 m 为 word1 的长度,n 为 word2 的长度。
 * 空间复杂度:O(mn),我们需要大小为 O(mn) 的 D 数组来记录状态值。
 */