1、二叉树基础概念
参考Carl哥的『代码随想录』
特殊二叉树
满二叉树
如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。如果深度为k,则有
2
k
−
1
2^{k}-1
2k−1个节点的二叉树。
完全二叉树
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~
2
h
−
1
2^{h-1}
2h−1 个节点
二叉搜索树
二叉搜索树是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉搜索树
平衡二叉搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn。
用数组存储并构建二叉树
def buildtree(self,nums):
def buildnums(index):
if index>len(nums)-1:
return
if nums[index] is not None:
root = Treenode(nums[index])
root.left = buildnums(2*index+1)
root.right = buildnums(2*index+2)
return root
return None
root = buildnums(0)
# print(root)
return root
2、遍历二叉树
2.1 递归法
2.1.1 前序遍历(中左右)
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def traversal(root):
if root==None:
return None
res.append(root.val) # 中
traversal(root.left) # 左
traversal(root.right) # 右
traversal(root)
return res
2.1.2 中序遍历(左中右)
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def traversal(root):
if root == None:
return None
traversal(root.left) # 左
res.append(root.val) # 中
traversal(root.right) # 右
traversal(root)
return res
2.1.3 后序遍历(左右中)
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
def traversal(root):
if root == None:
return None
traversal(root.left) # 左
traversal(root.right) # 右
res.append(root.val) # 中
traversal(root)
return res
2.2 迭代遍历【深度优先】
2.2.1 统一迭代法
2.2.1.1 前序遍历(中左右)
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
stack = [root]
while stack:
cur_tree = stack.pop()
if cur_tree:
# 遍历中结点且输出中结点,最早输出的
res.append(cur_tree.val)
# 右子树先进栈,所以后输出
if cur_tree.right:
stack.append(cur_tree.right)
# 左子树后进栈,所以先输出
if cur_tree.left:
stack.append(cur_tree.left)
# print(res)
return res
还有另一种写法,与下面的中序遍历和后序遍历统一。相比于上面的写法,让中结点先出栈再进栈,并通过一个None标记,之后再出栈时不需要再次遍历,直接输出。
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
res = []
stack = [root]
while stack:
cur = stack.pop()
if cur:
# 右子树先进栈,所以最后输出
if cur.right:
stack.append(cur.right)
# 左子树后进栈,所以其次输出
if cur.left:
stack.append(cur.left)
# 中结点最后进栈,所以最先输出,增加一个None,表示该结点已经被遍历过,不需要再遍历了,之后直接出栈
stack.append(cur)
stack.append(None)
else:
rt = stack.pop()
res.append(rt.val)
return res
2.2.1.2 中序遍历(左中右)
首先要明确,遍历该节点需要将其出栈并把左右子树入栈,而处理节点只需要输出即可,由于中序遍历时,需要先遍历中节点才能遍历子结点,但是输出顺序上又不能遍历中结点的时候就把它“处理”,要优先输出左子树。因此采用这种方法把中结点先出栈再入栈,并标记起来,表示已经遍历过了,直接处理即可。在这里是将其入栈后,再入栈一个NULL
,这样出栈时先遇到NULL
,再遇到遍历过的中结点,就直接把中结点出栈并加到输出里,见else部分。
唯一需要注意的就是入栈顺序,要与左中右反过来。
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
res = []
stack = [root]
while stack:
cur = stack.pop()
if cur:
if cur.right: # 右
stack.append(cur.right)
# cur_node 后面加了None,表示该节点已被遍历过(出过栈),后续直接输出
stack.append(cur) # 中
stack.append(None) # 左
if cur.left:
stack.append(cur.left)
else:
rt = stack.pop()
res.append(rt.val)
return res
2.2.1.2 后序遍历(左右中)
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
res = []
stack = [root]
while stack:
cur = stack.pop()
if cur:
stack.append(cur) # 中
stack.append(None)
if cur.right:
stack.append(cur.right) # 右
if cur.left:
stack.append(cur.left) # 左
else:
rt = stack.pop()
res.append(rt.val)
return res
每次遍历一层结点,把该层所有子节点存在下一层的队列中,循环遍历直到所有层遍历完
2.2.2 模板迭代法
该方法是先把所有左结点都入栈,然后再一个一个出栈,如果出栈的结点有右子树,那么再用同样的方法把他的所有左节点全部入栈。
2.2.2.1 前序遍历(中左右)
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = []
cur = root
stack = []
while stack or cur:
# 把所有当前根结点的左子树(可以理解为根节点)入栈
while cur:
res.append(cur.val) # 所有左子树的根结点入栈
stack.append(cur)
cur = cur.left # 这里是左子树
node = stack.pop() # 出栈
# 如果当前结点有右子树,cur指针就指向右子树,后续会把右子树的全部左子树入栈
# if node and node.right:
# 无论有没有右子树都可以用cur指向,即使是空,在下一轮循环中也会忽略
cur = node.right
return res
2.2.2.2 中序遍历(左中右)
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
cur = root
res, stack = [], []
while stack or cur:
# 遍历所有左子树,并入栈
while cur:
stack.append(cur)
cur = cur.left # 这里是左子树
# 取出栈顶元素
node = stack.pop()
res.append(node.val)
# 如果有右子树,cur指向右子树,后续会继续遍历该结点的所有左子树
cur = node.right
return res
2.2.2.3 后序遍历(左右中)
实际上是中右左,类似前序遍历的方式,最后再反转即可
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
cur = root
res, stack = [], []
while stack or cur:
while cur:
res.append(cur.val)
stack.append(cur)
cur = cur.right # 注意这里是右子树
node = stack.pop()
cur = node.left
return res[::-1]
总体上感觉模板迭代法没有统一迭代法好理解。
2.3 层序遍历【宽度优先】
每次遍历一层结点,把该层所有子节点存在下一层的队列中,循环遍历直到所有层遍历完。比迭代法好理解一些。
class Solution:
def levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if root is None:
return []
res = []
# 使用deque比list更快,pop(0)的时间复杂度:deque是O(1),list是O(n)
from collections import deque
que = deque([root])
while que:
n = len(que)
res_level = []
# 只遍历当前层的结点,一共有n个
for _ in range(n):
cur = que.popleft()
##########################
# 这步就是想要输出的东西,可以根据题意替换
# res_level.append(cur.val)
##########################
if cur.left:
que.append(cur.left)
if cur.right:
que.append(cur.right)
res.append(res_level)
return res
3、二叉树的高度和深度
3.1 概念区分
首先需要分清二叉树的深度和高度。这里借用Carl哥的图片来举例:
深度:指从根节点到该节点的最长简单路径边的条数
高度:指从该节点到叶子节点的最长简单路径边的条数
简单说深度是自顶向下
,起点是根节点
;高度是自底向上
,起点是最底层的叶子节点
。
3.2 计算深度
我一般在计算深度时,通常采用层序遍历的方法,每遍历一层深度就+1,思路比较简单。
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if root is None:
return 0
from collections import deque
que = deque([root])
depth = 0
while que:
n = len(que)
for _ in range(n):
cur = que.popleft()
if cur.left: que.append(cur.left)
if cur.right: que.append(cur.right)
# 每遍历一层,深度就加1
depth += 1
return depth
3.3 计算高度
高度计算一般采用递归法,从下层逐渐往上+1
一般用后序遍历,即左右中。
H
e
i
g
h
t
(
x
)
=
{
0
,
x
=
=
N
U
L
L
m
a
x
(
H
e
i
g
h
t
(
x
.
l
e
f
t
)
,
H
e
i
g
h
t
(
x
.
r
i
g
h
t
)
)
+
1
,
x
!
=
N
U
L
L
Height(x) = \begin{cases} 0, & x == NULL \\ max(Height(x.left),Height(x.right))+1, & x != NULL \\ \end{cases}
Height(x)={0,max(Height(x.left),Height(x.right))+1,x==NULLx!=NULL
def getheight(root):
if not root: return 0
# 先递归左子树,求左子树高度
leftheight = getheight(root.left)
# 再递归右子树,求右子树高度
rightheight = getheight(root.right)
####################################################################
# 这里可以加上剪枝的东西
# if leftheight<0 or rightheight<0 or abs(leftheight-rightheight)>1:
# return -1
# else:
####################################################################
# 最后中节点的高度则为左右子树的最大值+1(自身这层)
return max(leftheight,rightheight) + 1
rootheight = getheight(root)
4、深度优先搜索【递归法】
以leetcode113.路径总和Ⅱ为例,具体介绍应该怎么写,以及注意事项。
递归三部曲:
- 确定参数和返回值:中节点和targetSum-累加和的差,不需要返回值
- 确定终止条件:遍历到叶子节点(没有子节点),如果满足累加和==targetSum,就把结果存下来。存的时候必须保存值,不是地址!存的时候必须保存值,不是地址!存的时候必须保存值,不是地址!
必须要拷贝:stack.copy()
,再保存。找了好久的错误,发现是这里有问题T_T - 确定单层递归逻辑:如果有左/右子树就继续遍历左/右子树
递归法需要注意的是:
如果搜索时候,函数的参数包括状态参数
,比如累加和或者栈,那么就不需要回溯这部分状态了,因为在traversal的时候就已经记录了当前节点的状态,无论向下遍历还是向上回溯,这些状态量都与当前节点相关的;
反之,如果函数的参数不包括,这些状态以全局变量的形式存储,那么回溯的时候就要恢复到之前的状态,体现在出栈、累加和减子树的值。
Carl哥在代码随想录提到了回溯的隐藏,就与我上面说的有关,一开始不太理解,做了几道题之后才发现其中的奥妙所在~,借用Carl哥的图,举个例子。如果是包含了累加和的形式:def traversal(root,target)
,那么在不断遍历子树的过程中,每一个节点就对应了自己的状态。比如遍历根节点,他的状态时22,遍历到左子树4的时候,对应的状态为17。那么当我遍历完左子树后,想要遍历右子树的节点,右子树的节点的状态还是17,只与它本身的状态有关,而它的状态只取决于他的父节点。
【省流】:一个节点对应一个状态,回溯的时候不需要对这个状态进行修改,因为这个状态只取决于他的父节点,并且一直跟着该节点。
下面我提供了三个代码,分别对应没有状态参数,有一部分状态参数,包括所有状态参数。可以在traversal
函数的参数看出区别。
# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
stack = []
res = []
self.sum = 0
if not root:
return res
def traversal(root):
# 不包含任何状态参数,状态量以全局变量形式保存
stack.append(root.val)
self.sum += root.val
if not root.left and not root.right and self.sum==targetSum:
res.append(stack.copy())
if root.left:
traversal(root.left)
# 栈和累加和都要回溯
stack.pop()
self.sum -= root.left.val
if root.right:
traversal(root.right)
# 栈和累加和都要回溯
stack.pop()
self.sum -= root.right.val
traversal(root)
return res
# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
stack = []
res = []
if not root:
return res
def traversal(root,target):
# 包含累加和这个状态变量,栈需要回溯
stack.append(root.val)
target -= root.val
if not root.left and not root.right and target==0:
res.append(stack.copy())
if root.left:
traversal(root.left,target)
# 回溯栈
stack.pop()
if root.right:
traversal(root.right,target)
# 回溯栈
stack.pop()
traversal(root,targetSum)
return res
# 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 pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
stack = []
res = []
if not root:
return res
def traversal(root,target,st):
# 因为每一个节点对应一个target和栈,所以这些值不需要回溯,只需要维持自己的状态就可以
st.append(root.val)
target -= root.val
if not root.left and not root.right and target==0:
res.append(st.copy())
if root.left:
traversal(root.left,target,st.copy())
if root.right:
traversal(root.right,target,st.copy())
traversal(root,targetSum,stack)
return res
相信理解了上面三种写法之后,你肯定可以掌握二叉树的递归写法!
5、重建二叉树
5.1 思路
根据中序与前序(或后序)遍历的结果构造二叉树。
回忆一遍遍历顺序:
中序:左/中/右
前序:中/左/右
后序:左/右/中
通过上面的遍历顺序可以发现,前序或者后序遍历可以很容易找到根节点
,而中序遍历很容易分割左右子树
。
因此,构造二叉树的步骤就是:
(1)从前序/后序遍历中找到当前二叉树的根节点
(2)在中序遍历中找到根节点的下标,然后分割左右子树
(3)根据中序遍历的左子树的长度,分割出前序/后序遍历的左右子树
(.)剩下的就递归遍历左右子树咯
5.2 从中序与后序遍历序列构造二叉树
# 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 buildTree(self, inorder: List[int], postorder: List[int]) -> Optional[TreeNode]:
def rebuild(ino,posto):
if not ino or not posto:
return None
# if len(posto)==1:
# return TreeNode(posto.pop())
root = TreeNode(posto.pop())
i = ino.index(root.val)
in_left = ino[:i]
in_right = ino[i+1:]
n = len(in_left)
post_left = posto[:n]
post_right = posto[n:]
root.left = rebuild(in_left, post_left)
root.right = rebuild(in_right, post_right)
return root
root = rebuild(inorder, postorder)
return root
5.3 从前序与中序遍历序列构造二叉树
# 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 buildTree(self, preorder: List[int], inorder: List[int]) -> Optional[TreeNode]:
if not preorder:
return None
root = TreeNode(preorder[0])
i = inorder.index(root.val)
in_left = inorder[:i]
in_right = inorder[i+1:]
n = len(in_left)
pre_left = preorder[1:1+n]
pre_right = preorder[1+n:]
root.left = self.buildTree(pre_left, in_left)
root.right = self.buildTree(pre_right, in_right)
return root
很抱歉上面两道题没有写成统一的形式T_T
6、二叉搜索树
6.1 性质
二叉搜索树是一个有序树:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉搜索树
6.2 遍历
最佳方式是中序遍历
,左/中/右的遍历顺序恰好符合二叉搜索树的定义,因此遍历出来的结果一定是严格递增数组,利用这个有序数组我们就可以做很多事情啦!
def traversal(root):
if not root:
return True
traversal(root.left) # 左 - 小
res.append(root.val) # 中 - 中
traversal(root.right) # 右 - 大
traversal(root)