iOS算法刷题之二叉树

95 阅读6分钟

二叉树.png

二叉树从数据结构上来说和链表是很像的,链表有一个子树,二叉树有两个子树。

二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 回溯算法核心框架 和 动态规划核心框架。

无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做。 所以二叉树的前序、中序、后序排序也非常重要。

二叉树中所涉及到的题型有:BFS、DFS递归、构造二叉树、序列化、二叉搜索树、Trie树等。

DFS递归

DFS举例: 104. 二叉树的最大深度

题解:二叉树问题虽然简单,但是暗含了动态规划和回溯算法等高级算法的思想。本题使用DFS方式解题,也可以尝试使用动态规划方式解题。

class Solution {
    var result = 0
    var depth = 0
    func maxDepth(_ root: TreeNode?) -> Int {
        traverse(root: root)
        return result
    }
    func traverse(root: TreeNode?){
        if root == nil {
            return 
        }
        // 前序遍历位置
        depth += 1
        // 遍历的过程中记录最大深度
        result = max(result, depth)
        traverse(root: root?.left)
        traverse(root: root?.right)
        // 后续遍历位置
        depth -= 1
    }
}

其他DFS经典题还有:144. 二叉树的前序遍历94. 二叉树的中序遍历145. 二叉树的后序遍历222. 完全二叉树的节点个数100. 相同的树116. 填充每个节点的下一个右侧节点指针101. 对称二叉树543. 二叉树的直径110. 平衡二叉树226. 翻转二叉树114. 二叉树展开为链表113. 路径总和 II513. 找树左下角的值

BFS

BFS相对DFS的主要区别是:BFS找到的路径一定是最短的,但代价就是空间复杂度可能比DFS大很多。 BFS框架:

// 计算从起点 start 到终点 target 的最近距离 
int BFS(Node start, Node target) { 
    Queue<Node> q; // 核心数据结构 
    Set<Node> visited; // 避免走回头路 
    
    q.offer(start); // 将起点加入队列 
    visited.add(start); 
    int step = 0; // 记录扩散的步数 
    
    while (q not empty) { 
        int sz = q.size(); 
        /* 将当前队列中的所有节点向四周扩散 */ 
        for (int i = 0; i < sz; i++) { 
            Node cur = q.poll(); 
            /* 划重点:这里判断是否到达终点 */ 
            if (cur is target) 
                return step; 
            /* 将 cur 的相邻节点加入队列 */ 
            for (Node x : cur.adj()) { 
                if (x not in visited) { 
                    q.offer(x); 
                    visited.add(x); 
                } 
            } 
        } 
        /* 划重点:更新步数在这里 */ 
        step++; 
    } 
}

二叉树BFS举例: 111. 二叉树的最小深度

题解:请看上面的题解。

class Solution {
    func minDepth(_ root: TreeNode?) -> Int {
        if root == nil {
            return 0
        }
        var queue: [TreeNode?] = []
        queue.append(root)
        var depth = 1

        while !queue.isEmpty {
            let size = queue.count 
            for _ in 0..<size {
                let cur = queue.removeFirst()
                // 判断是否到达终点
                if cur?.left == nil && cur?.right == nil {
                    return depth
                }
                // 将 cur 相邻的节点加入队列
                if cur?.left != nil {
                    queue.append(cur?.left)
                }
                if cur?.right != nil {
                    queue.append(cur?.right)
                }
            }
            depth += 1
        }
        return depth
    }
}

其他BFS经典题还有:102. 二叉树的层序遍历103. 二叉树的锯齿形层序遍历107. 二叉树的层序遍历 II515. 在每个树行中找最大值

构造二叉树

二叉树的构造问题一般都是使用“分解问题”的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树 构造二叉树举例: 654. 最大二叉树

题解:函数 build 的定义是根据输入的数组构造最大二叉树,那么只要先要找到根节点,然后让 build 函数递归生成左右子树即可。

class Solution {
    func constructMaximumBinaryTree(_ nums: [Int]) -> TreeNode? {
    return build(nums, lo: 0, hi: nums.count-1)
}

// 将 nums[lo..hi]构造成符合条件的树,返回根节点
func build(_ nums: [Int], lo: Int, hi: Int) -> TreeNode? {
    // base case
    if (lo > hi) {
        return nil
    }
    // 找到数组中的最大值和对应的索引
    var index = -1
    var maxVal = Int.min
    for i in lo...hi {
        if maxVal < nums[i] {
            index = i
            maxVal = nums[i]
        }
    }
    let root = TreeNode(maxVal)
    root.left = build(nums, lo: lo, hi: index-1)
    root.right = build(nums, lo: index+1, hi: hi)
    return root
}
}

其他经典的构造二叉树的题有:105. 从前序与中序遍历序列构造二叉树106. 从中序与后序遍历序列构造二叉树889. 根据前序和后序遍历构造二叉树

序列化

序列化和反序列化的目的,以某种特定格式组织数据,使得数据可以独立与编程语言。 序列化举例: 297. 二叉树的序列化与反序列化

序列化问题其实就是遍历问题,你能遍历,顺手把遍历的结果转化成字符串的形式,不就是序列化了么? 这里我就简单说说用前序遍历的思路,前序遍历的特点是根节点在开头,然后接着左子树的前序遍历结果,然后接着右子树的前序遍历结果。所以如果按照前序遍历顺序进行序列化,反序列化的时候,就知道第一个元素是根节点的值,然后递归调用反序列化左右子树,接到根节点上即可,上述思路翻译成代码即可解决本题。当然,这题也可以尝试使用二叉树的中序、后序、层序的遍历方式来做。

class Codec {
    let SEP = ","
    let NULL = "#"

    func serialize(_ root: TreeNode?) -> String {
        var str = ""
        serialize(root: root, str: &str)
        return str
    }
    // 辅助函数
    func serialize(root: TreeNode?, str: inout String) {
        if root == nil {
            str += NULL
            str += SEP
            return
        }
        // 前序遍历位置
        str += "\(root!.val)"
        str += SEP
        serialize(root: root?.left, str: &str)
        serialize(root: root?.right, str: &str)
    }
    
    func deserialize(_ data: String) -> TreeNode? {
        var nodes = data.split(separator: ",")
        return deserialize(nodes: &nodes)
    }

    func deserialize(nodes: inout [Substring]) -> TreeNode? {
        if nodes.isEmpty {
            return nil
        }
        // 前序遍历位置
        let first = nodes.removeFirst()
        if first == NULL {
            return nil
        }
        let root = TreeNode(Int(first)!)
        root.left = deserialize(nodes: &nodes)
        root.right = deserialize(nodes: &nodes)
        return root
    }
}

其他序列化经典题有:572. 另一颗树的子树652. 寻找重复的子树449. 序列化和反序列化二叉搜索树

二叉搜索树

二叉搜索树并不算复杂,但它可以算是数据结构领域的半壁江山,直接基于BST的数据结构有AVL树、红黑树等等,拥有了自平衡性质,可以提供logN级别的增删改效率。

从算法题的角度来看BST,除了它的定义,还有一个重要的性质:BST的中序遍历结果是有序的(升序)。 二叉搜索树举例: 700. 二叉搜索树中的搜索

题解:利用 BST 左小右大的特性,可以避免搜索整棵二叉树去寻找元素,从而提升效率。

class Solution {
    func searchBST(_ root: TreeNode?, _ val: Int) -> TreeNode? {
        if root == nil || root!.val == val {
            return root
        }
        if root!.val > val {
            return searchBST(root?.left, val)
        }else {
            return searchBST(root?.right, val)
        }
    }
}

其他经典的二叉搜索树题有: 450. 删除二叉搜索树中的节点230. 二叉搜索树中第K小的元素96. 不同的二叉搜索树98. 验证二叉搜索树701. 二叉搜索树中的插入操作95. 不同的二叉搜索树 II

Trie树

Trie树也叫前缀树、字典树、单词查找树,是一种二叉树衍生出来的高级数据结构,主要应用场景是处理字符串前缀相关的操作。 Trie树用“树枝”存储字符串(键),用“节点”存储字符串(键)对应的数据(值)。 Trie树举例: 208. 实现Trie(前缀树)

题解:TrieNode为一个节点,childre的key位字符,value为TrieNode,isword是一个标记位,标记是否为单词。插入一个元素时,有该节点就接续递归遍历、无该节点则创建节点。

class TrieNode {
    var children: [Character: TrieNode] = [:]
    //    var children = [TrieNode?](repeating: nil, count: 26)
    var isWord = false
    init() {
        self.children = [:]
        self.isWord = false
    }
}

class Trie {
    let root: TrieNode
    init() {
        root = TrieNode()
    }
    
    // 插入一个元素
    func insert(_ word: String) {
        var cur = root
        for c in word {
        //            let asciiValueIndex = Int(char.asciiValue!) - 97
//            let subNode = currentNode.children[asciiValueIndex]
            if let childNode = cur.children[c] {
                // 有该节点就继续递归遍历
                cur = childNode
            } else {
                // 无该节点则创建该节点
                cur.children[c] = TrieNode()
                cur = cur.children[c]!
            }
        }
        cur.isWord = true
    }
    
    // 判断元素是否在集合中
    func search(_ word: String) -> Bool {
        var cur = root
        for c in word {
            if let childNode = cur.children[c] {
                // 有该节点则继续递归遍历
                cur = childNode
            } else {
                // 无该节点则表示元素不在集合中
                return false
            }
        }
        return cur.isWord
    }
    
    // 判断集合中是否有前缀为 prefix 的元素
    func startsWith(_ prefix: String) -> Bool {
        var cur = root
        for c in prefix {
            if let childNode = cur.children[c] {
                // 有该节点则继续递归遍历
                cur = childNode
            } else {
                // 无该节点则表示元素不在,无此前缀
                return false
            }
        }
        return true
    }
}

其他经典Trie树的题: 648. 单词替换211. 添加与搜索单词 - 数据结构设计677. 键值映射212. 单词搜索 II