一、概念
树是一种非线性结构,就像真实的树倒挂,具体定义:树是包含n(n>=0)个节点的有穷集,其中:
1、每个元素称为节点
2、有一个特定的节点被称为跟节点或树根
3、除根节点之外的其余元素被分为m(m>=0)个互不相交的集合T1,T2,T3...... Tm-1, 其中每一个集合Ti(1 <=i<=m)本身也是一颗树,被称作原树的子树
首先介绍一下二叉树的结构(如下图):
A为根节点(root), D为H,I的父节点(Parent Node), H,I为D的子节点(Child Node), H和I之间互为兄弟节点(Sliblings)。树的深度指的是树中有多少个level,图中,树的深度为4。
二、二叉树的存储
二叉树的存储分为两种:
1、基于指针或者引用的二叉链表存储法
2、基于数组的顺序存储法,其中完全二叉树使用数组存储是最节省内存的一种方式
三、遍历方式
广度优先搜索:也成为层次遍历。
深度优先搜索::分为前序,中序,后序遍历。
前序遍历:对于树中的任意结点来说,先打印这个结点,然后再打印它的左子树,最后打印右子树。
中序遍历:对于树中的任意结点来说,先打印它的左子树,然后再打印这个结点,最后打印右子树。
后续遍历:对于树中的任意结点来说,先打印右子树,然后再打印它的左子树,最后打印结点本身。
总结一下,就是前中后指的都是根的位置在前中后。
四、二叉树的代码定义
对于树的代码定义:说明,每个节点都是一个类(class), 每个类中包括了节点的值,和节点的左节点(left)和右节点(right)。
# Definition for a binary tree node.
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
看一个具体例子(如图):[1,null,2,3]
二叉树的结构打印:
TreeNode{val: 1, left: None, right: TreeNode{val: 2, left: TreeNode{val: 3, left: None, right: None}, right: None}}
五、常见面试题:
下面看具体的问题:
关于树的算法一般有两种,递归(Recursion)和迭代(Iteration),下面我会分别列举几种常见的关于树的例子。
问题一:二叉树的前序遍历
例题1:https://leetcode-cn.com/problems/binary-tree-preorder-traversal/ 144. 二叉树的前序遍历
思路:
思路一: 迭代(Iteration), T:O(n), n为二叉树的节点数; S:O(n), n为栈的开销
思路二: 递归(Recursion),T:O(n), n为二叉树的节点数; S:平均情况下为 O(logn),最坏情况下树呈现链状,为 O(n)。
代码实现:
# 迭代
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
re, stack = [], [root]
while stack:
node = stack.pop()
if node:
re.append(node.val)
stack.append(node.right)
stack.append(node.left)
return re
# 递归:
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
def dfs(root, res):
if root:
res.append(root.val)
dfs(root.left, res)
dfs(root.right, res)
res = []
dfs(root, res)
return res
问题二、求二叉树的中序遍历
例题2: https://leetcode-cn.com/problems/binary-tree-inorder-traversal/ 94. 二叉树的中序遍历
给定一个二叉树的根节点 root ,返回它的 中序 遍历。
示例 1:
输入:root = [1,null,2,3]
输出:[1,3,2]
示例 2:
输入:root = []
输出:[]
示例 3:
输入:root = [1]
输出:[1]
示例 4:
输入:root = [1,2]
输出:[2,1]
示例 5:
输入:root = [1,null,2]
输出:[1,2]
思路:
递归和迭代。时间复杂度和空间复杂度分析和前序遍历一样。
思路一:递归(Recursion),左根右, 时间复杂度为O(n),其中n为树的节点数 , 空间复杂度为O(n),其中n为树的深度。
思路二:迭代(Iteration),遍历root, stack中依次装入root的left的节点; stack中为空时,为迭代终止条件;stack中弹出结点,将值加入返回的结果集中。开始遍历右节点。
所以,最先加入根,然后左节点;弹出时变成,左节点,根。然后压入右节点,弹出右节点;
时间复杂度为O(n),其中n为树的节点数 , 空间复杂度为O(n),其中n为树的深度。
代码实现:
# 递归
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
def dfs(root, res):
if root:
dfs(root.left, res)
res.append(root.val)
dfs(root.right, res)
res = []
dfs(root, res)
return res
# 迭代
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res, stack = [], []
while True:
while root:
stack.append(root)
root = root.left
if not stack:
return res
node = stack.pop()
res.append(node.val)
root = node.right
问题三、求二叉树的最大深度
例题3: https://leetcode-cn.com/problems/maximum-depth-of-binary-tree/ 104. 二叉树的最大深度
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
3
/ \
9 20
/ \
15 7
思路:
按照总结,常见的思路有两种:递归和迭代。
思路1:递归,二叉树的最大深度,等于左右子节点中的较大者 + 1(1为根节点自己)
思路2:迭代,借助辅助栈stack,借助辅助栈,每次遍历左右节点完后,压入栈中,清空栈中之前的元素,计数+1。
T:O(n), n为节点的个数, S:O(n),n为树的深度
第1次迭代后的stack中为:
[TreeNode{val: 3, left: TreeNode{val: 9, left: None, right: None}, right: TreeNode{val: 20, left: TreeNode{val: 15, left: None, right: None}, right: TreeNode{val: 7, left: None, right: None}}}]
此时是把整个root都压入栈中,深度计数+1,根节点本身就是一层。
第2次迭代后的stack中为:
[TreeNode{val: 9, left: None, right: None}, TreeNode{val: 20, left: TreeNode{val: 15, left: None, right: None}, right: TreeNode{val: 7, left: None, right: None}}]
此时把根结点中的右节点的左右节点压入了栈中,深度计数+1。
第3次迭代后的stack中为:
[TreeNode{val: 15, left: None, right: None}, TreeNode{val: 7, left: None, right: None}]
此时把上一次的节点中的右节点的左右节点压入栈中,深度计数+1。
第4次迭代后的stack中为:
[],计数不变。
返回计数结果,即树的最大深度。
代码实现:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
# 递归:
class Solution:
def maxDepth(self, root: TreeNode) -> int:
return 1 + max(self.maxDepth(root.left), self.maxDepth(root.right)) if root else 0
# 迭代:
class Solution:
def maxDepth(self, root: TreeNode) -> int:
depth = 0
stack = [root] if root else []
while stack:
depth += 1
queue = []
for node in stack:
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
stack = queue
return depth
问题四、N叉树的前序遍历
例题4: 589. N叉树的前序遍历 https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/
思路:
思路一:递归,相对简单。先装入根的值,然后依次装入children节点中的值。
T:O(n), n为节点的个数; S:O(n), n为树的最大深度
思路二:迭代, 将root装入stack中,循环stack,先弹出栈,结果集中装入当前节点的值,栈中反方向装入children的节点。
注意:借助辅助栈stack,每次往stack中追加children的倒序值(根据LIFO,后进先出原则),注意用extend,不用append,extend是直接拼接内容的值,而append会拼接整块内容,包括值和[],例如append会把[1,2,3]直接追加,而extend只追加1,2,3的值
T:O(n), n为节点的个数; S:O(n), n为树的最大深度
代码实现:
# 迭代:
class Solution:
def preorder(self, root: 'Node') -> List[int]:
if not root:
return []
res = [root.val]
for child in root.children:
res += self.preorder(child)
return res
# 递归:
class Solution:
def preorder(self, root: 'Node') -> List[int]:
if not root:
return []
res, stack = [], [root]
while stack:
node = stack.pop()
if node:
res.append(node.val)
stack.extend(node.children[::-1])
return res
问题5: 105. 从前序与中序遍历序列构造二叉树 https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/
根据一棵树的前序遍历与中序遍历构造二叉树。
注意:
你可以假设树中没有重复的元素。
例如,给出
前序遍历 preorder = [3,9,20,15,7]
中序遍历 inorder = [9,3,15,20,7]
返回如下的二叉树:
3
/ \
9 20
/ \
15 7
思路:
递归,前序中的第一个元素为根节点的值,获取它,可以构建树节点的value值,然后递归遍历树的左节点和右节点
时间复杂度:O(n),n为节点的个数
空间复杂度:O(n),n为树的深度
代码实现:
class Solution:
def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
if inorder:
ind = inorder.index(preorder.pop(0))
root = TreeNode(inorder[ind])
root.left = self.buildTree(preorder, inorder[0:ind])
root.right = self.buildTree(preorder, inorder[ind+1:])
return root
问题6: 二叉树的最近公共祖先https://leetcode-cn.com/problems/er-cha-shu-de-zui-jin-gong-gong-zu-xian-lcof/
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1
输出: 3
解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2:
输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4
输出: 5
解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
思路:
二叉树的最近公共祖先有两种情况,1,p和q在根节点的左右两侧。 2、p或q其中一个为根节点。递归左右子树查找p和q,如果左节点为空,则返回右节点,如果右节点为空,则返回左节点。说明p和q其中一个为根节点。 如果都没找到,说明p和q在根节点的两侧,返回root根节点。
时间复杂度:O(n),n为节点的个数
空间复杂度:O(n),n为树的深度
代码实现:
class Solution:
def lowestCommonAncestor(self, root: TreeNode, p: TreeNode, q: TreeNode) -> TreeNode:
if not root or root == p or root == q:
return root
left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)
if not left:
return right
if not right:
return left
return root
撰写记录
2020.12.11-06:11:01-第一次撰写
2020.12.13-08:20:19-第二次撰写
2020.12.13-11:38:00-第三次撰写
2020.12.21-06:26:10-第四次撰写
2021.01.30-20:22:00-第五次撰写
2021.02.16-08:28:00-第六次撰写