二叉树从数据结构上来说和链表是很像的,链表有一个子树,二叉树有两个子树。
二叉树题目的递归解法可以分两类思路,第一类是遍历一遍二叉树得出答案,第二类是通过分解问题计算出答案,这两类思路分别对应着 回溯算法核心框架 和 动态规划核心框架。
无论使用哪一种思维模式,你都要明白二叉树的每一个节点需要做什么,需要在什么时候(前中后序)做。 所以二叉树的前序、中序、后序排序也非常重要。
二叉树中所涉及到的题型有: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. 路径总和 II、513. 找树左下角的值
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. 二叉树的层序遍历 II、515. 在每个树行中找最大值
构造二叉树
二叉树的构造问题一般都是使用“分解问题”的思路:构造整棵树 = 根节点 + 构造左子树 + 构造右子树 构造二叉树举例: 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