满二叉树:节点都是满的,节点个数2^k - 1
完全二叉树:除最后一层外都是满的,底部可以空,但是有值的地方必须从左到右连续,
平衡二叉搜索树:(搜索树有序)平衡☞左子树和右子树的深度差不能> 1
链式
.left 指向左儿子 .right指向右儿子(指针啦~)
线式
数组1 2 3 4 5 6顺序着标,对于某个节点i,i * 2+1是他的左孩子 i * 2+2是右孩子
一般是用链式也就是链表去构造,自己封装好,把头节点传入待处理的函数
深度优先搜索
前中后、迭代、递归(能递归的一定能迭代)
广度优先搜索 层序
前序遍历: 中左右
中序:左中右
后序:左右中(发现是先左后右,前中后是相对于中而言)
栈其实就是递归的一种实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
面试传入二叉树需要自己定义数据结构 初始值赋值None
#
class TreeNode:
def __init__(self, val, left = None, right = None):
self.val = val
self.left = left
self.right = 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
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
res = [] # 全局变量
# 递归参数
def dfs(node):
# 终止条件
if node == None:
return
res.append(node.val)
dfs(node.left)
dfs(node.right)
# 入口
dfs(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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
def dfs(node):
if node == None:
return
dfs(node.left)
dfs(node.right)
ans.append(node.val)
dfs(root)
return ans
左中右
# 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 inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
def dfs(node):
if node == None:
return
dfs(node.left)
ans.append(node.val)
dfs(node.right)
dfs(root)
return ans
递归和迭代的主要区别:
递归:一个函数直接或间接地调用自身来解决问题的方式。递归通常将问题分解为一个或多个较小的子问题,直到这些子问题足够简单以直接解决。
示例:
def factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)
迭代:使用循环结构(如 for
循环或 while
循环)反复执行一段代码直到满足某个条件来解决问题。
示例:
def factorial(n):
result = 1
for i in range(1, n + 1):
result += i
return result
实现方式:
for
或 while
循环)实现。终止条件:
空间复杂度:
可读性和简洁性:
性能:
递归:适用于自然分解为更小子问题的问题,如树遍历、图遍历、分治算法(如归并排序、快速排序)、计算斐波那契数列等。
示例:计算斐波那契数列
def fibonacci(n):
if n <= 1:
return n
else:
return fibonacci(n - 1) + fibonacci(n - 2)
迭代:适用于需要重复执行某段代码直到满足特定条件的问题,如遍历数组、链表等线性数据结构、简单的数学计算等。
示例:计算斐波那契数列
def fibonacci(n):
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
return a
用栈模拟+while 循环
前序是中左右,所以每一个节点弹出保存之前,先入栈右节点,再入栈左节点,右节点先保存,在左节点的子树 没有全部弹出之前,会一直在,所以很好的实现了前序
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
if root == None:
return ans
stack = [root]
while stack:
temp = stack.pop()
ans.append(temp.val) # 进去之前要考虑为None
if temp.right:
stack.append(temp.right)
if temp.left:
stack.append(temp.left)
return ans
左中右,有一个node,先放入右节点,它,左节点,啥时候弹出?得先找到最左边那个节点。所以我们得按拿的顺序去放,先放入节点5,再放他的左节点4,如果还有左节点,一直放左节点,直到最左边1,弹出左1,弹出中4,如果有右2,放入右,再弹出右2,在弹出上一层的中5
注意:while cur or stack: # 循环条件# 怎么更新cur
# 假设这个最左节点是一个中节点,它还有一个右节点
cur = cur.right
# 去考虑右节点为根的小树
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
if not root:
return ans
# 不能提前将root放入,
stack = []
cur = root
while cur or stack: # 循环条件
# if node.left: # 这样会反复添加
# stack.append(node.left) 所以用一个指针来找
if cur:
stack.append(cur)
cur = cur.left
# 当左线找到最左点or 右节点为none
else:
cur = stack.pop() #为空,
ans.append(cur.val)
# 怎么更新cur
# 假设这个最左节点是一个中节点,它还有一个右节点
cur = cur.right
# 去考虑右节点为根的小树
return ans
而且当到中的时候,stack是为空的巧的是cur指向右子树,指针表示了查询的顺序
左右中,可以通过先序遍历中左右改成 中右左,在ans[::-1]结果反转一下
# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
if not root:
return ans
cur = root
stack = []
last_visited = None
while cur or stack:
if cur: # 左
stack.append(cur)
cur = cur.left
# 当cur为空
else:
cur = stack[-1]
# 会重复访问
if cur.right and last_visited != cur.right: # 右节点重复访问
cur = cur.right
else:
ans.append(cur.val) # 中
last_visited = stack.pop()
cur = None # 右还没遍历 当 cur 为 None 时,查看栈顶节点的右子树
return ans
发现是用cur记录是否遍历过,cur有值就入栈,中序的顺序是左 中 右
后序遍历和中序遍历在实现上有一些相似之处,但它们在访问节点的顺序和处理方式上有显著的区别。以下是对这两种遍历方法的详细总结,以及它们的不同之处:
中序遍历的顺序是:左子树 -> 根节点 -> 右子树。其实现方式如下:
重点是我要用一个cur去找通过中 左 右(新头) 左的顺序
为什么?因为我的访问顺序和处理顺序是不一样的,前序中左右,一层一层都是先中,但中序是先左,所以得找到最左边。访问过的放到stack里面,没访问的用cur去更新,访问顺序也是中左右的顺序,符合我们寻找的逻辑
此时重复的是找最左点,cur空时(左点到头 or右点空)时弹出,右点又为新的数的头一直找左
# 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 inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
# 左中右 找左 压入右,中, 中作为新的左
ans = []
stack = []
if not root:
return []
cur = root
while cur or stack:
# 去找头和左,要一直找到最左点,才能看他对应的右,这个右又是新的头
while cur:
stack.append(cur)
cur = cur.left
# if not cur: 执行到下面应该就是cur为空
node = stack.pop()
ans.append(node.val)
# if node.right: 如果存在,cur没毛病,如果不存在,反正cur之前也是空
cur = node.right
return ans
后序遍历的顺序是:左子树 -> 右子树 -> 根节点。其实现方式如下:
# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
if not root:
return ans
cur = root
stack = []
last_visited = None
while cur or stack:
if cur: # 遍历左子树
stack.append(cur)
cur = cur.left
else:
cur = stack[-1] # 查看栈顶节点
# 如果右子树存在且未被访问过,遍历右子树
if cur.right and last_visited != cur.right:
cur = cur.right
else:
# 访问根节点
ans.append(cur.val)
last_visited = stack.pop() # 弹出栈顶节点并更新 last_visited
cur = None # 重置 cur 为 None
return ans
把后续左右中看做中右左的逆,一层一层的,类似前序迭代
# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
if not root:
return []
stack = [root]
# 左右中 中右左
# 为什么能直接就出来,要出现中右左 可以是栈中,左右中,也可以是先中 弹出,再左右
# 先中,弹出有什么好,类似一层一层的考虑,下一层就是他的左子和右子
while stack:
node = stack.pop()
ans.append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
return ans[::-1]
迭代到底在重复什么?
看前序迭代,从根节点开始,每次弹出一个,是中,再弹出左,左作为中节点,还有他的左和右,就这样一层一层的找左右,通过栈,把右压在底下,左先弹出来。我们从逻辑上看前序也是中,左(作为中还有左),左,左,一直到头,看到头的左对应的右,也就是刚刚被压在底下的右。右可能也是作为中去找左和右,所以有有了一场迭代,去找左和右
曾经的左作为新的中,曾经的右作为新的中,先进右后进左以便先出左
访问顺序:
辅助变量:
cur
和栈来遍历树。last_visited
来跟踪上一次访问的节点,以避免重复访问右子树。栈的处理:
last_visited
变量。终止条件:
cur
为 None
且栈为空。前面用迭代来实现前中后序,后面通过栈用递归来实现。
针对三种遍历方式,使用迭代法是可以写出统一风格的代码!
访问放入栈,要处理的也放入,但是做一个标记。
标记什么?标记已经展开过left right的点,所以不会重复添加
对于前序来说,中左右。遇到中,我们放进去,但是他先弹出,所以我们放在最外面,拿出来5,放入他的右6和左4,再放入中5,由于我们要弹出的数后面做个记号放一个none,如果弹出一个数是none,那他前一个数就是要处理的,弹出放入ans。
那怎么处理这个逻辑呢,普通的要处理的数没有左右,后面依旧添加一个None表示他要弹出
# 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 preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
ans = []
st = []
if not root:
return []
st = [root]
while st:
node = st.pop()
if node:
if node.right:
st.append(node.right)
if node.left:
st.append(node.left)
# 拿出来看了一下,再放回
st.append(node)
st.append(None) # 标记要处理,
# 不管是中节点(有儿子的),还是最后要弹出的节点(没儿子的),轮到他后面都有一个标记
else:
# 待处理
ans.append(st.pop().val)
return ans
对于中序,左中右。右边先进,中后进,左最后,左作为新的中,none加在哪里?其实我们发现,对于一个中节点去访问他的左右节点这个操作,是不断重复的,也就是每个节点,都会有机会被当做中去看有没有左右。什么中这个节点访问过,所以在他后面标记一个None,每个节点都有机会被当做中,所以都会被标记,也就都会输出。至于输出的顺序,受进栈的顺序影响,只要我一直是右中左的顺序,就不会出错
# 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 inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
ans = []
st = [root] # 存放访问和标记 标记法
while st:
node = st.pop()
if node:
# 访问,展开左右
# 左中右, 右中左
if node.right:
st.append(node.right)
st.append(node)
st.append(None)
if node.left:
st.append(node.left)
# 如果是要弹出的叶子节点,等到访问他们的时候自然会加上NONE
else:
ans.append(st.pop().val)
return ans
后序,左右中。同样标记访问展开过的点
# 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 postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
ans = []
st = [root] # 存放访问和标记 标记法
while st:
node = st.pop()
if node:
st.append(node)
st.append(None)
if node.right:
st.append(node.right)
if node.left:
st.append(node.left)
else:
ans.append(st.pop().val)
return ans
标记法太好用了!标记展开的点,迭代重复展开,遇到叶子节点或者展开过的点就回有none,直接输出即可。不管是递归还是迭代还是标记法都是深度优先下面考虑广度优先的层序遍历。
深度优先是用栈来实现,(一路一路)。广度优先是用队列,(一层一层)
需要哪些变量。ans存结果是一个二维的列表 queue放拿出来的值 node是拿出来之后要left right展开的点 level是一层的一维的列表
102.二叉树的层序遍历
注意deque初始化应该是一个iterable的queue = collections.deque([root])
# 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 levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
if not root:
return []
ans = []
queue = collections.deque([root])
while queue:
# 后面queue会随着node展开增加,怎么才能知道我的node是在这一层的呢?
# 用queue的长度 每次queue进入循环都只存了一层的信息
level = [] # 应该在每层初始化
for _ in range(len(queue)):
node = queue.popleft()
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
level.append(node.val)
ans.append(level)
return ans
递归
# 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 levelOrder(self, root: Optional[TreeNode]) -> List[List[int]]:
# 递归,参数:节点,层数
if not root:
return []
ans = []
def dfs(node, level):
if not node:
return
# ans虽然是[[]]但是如果level 1 就超出范围了
# 所以应该每层开始都要初始化一下
if len(ans) == level:
ans.append([])
ans[level].append(node.val)
dfs(node.left, level+1)
dfs(node.right, level+1)
dfs(root, 0)
return ans
107.二叉树的层次遍历II
199.二叉树的右视图
637.二叉树的层平均值
429.N叉树的层序遍历
515.在每个树行中找最大值
116.填充每个节点的下一个右侧节点指针
117.填充每个节点的下一个右侧节点指针II
104.二叉树的最大深度
111.二叉树的最小深度