代码随想录 - Day19 - 二叉树
二叉树的种类
满二叉树
如果二叉树中除了叶子结点,每个结点的度都为 2,则此二叉树称为满二叉树。
满二叉树除了满足普通二叉树的性质,还具有以下性质:
- 满二叉树中第 i 层的节点数为 2(n-1) 个。
- 深度为 k 的满二叉树必有 2k-1 个节点 ,叶子数为 2(k-1)。
- 满二叉树中不存在度为 1 的节点,每一个分支点中都两棵深度相同的子树,且叶子节点都在最底层。
- 具有 n 个节点的满二叉树的深度为 log2(n+1)。
完全二叉树
在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~2(h-1) 个节点。
二叉搜索树
前面介绍的满二叉树和完全二叉树都没有数值,而二叉搜索树是有数值的,是一个有序树。
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
平衡二叉搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
题目分类
二叉树的遍历方式
二叉树主要有两种遍历方式:
- 深度优先遍历DFS:先往深走,遇到叶子节点再往回走。
- 广度优先遍历BFS:一层一层的去遍历。
这两种遍历是图论中最基本的两种遍历方式
从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式: - 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
这里前中后,其实指的就是中间节点的遍历顺序,前中后序指的就是中间节点的位置就可以了。
中间节点的顺序就是所谓的遍历方式
- 前序遍历:中左右
- 中序遍历:左中右
- 后序遍历:左右中
深度优先搜索主要使用递归的方式实现,也可以借助栈使用非递归的方式实现。
广度优先搜索一般使用队列来实现。
二叉树的递归遍历
递归算法的三个要素
- 确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
- 确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
- 确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
144. 二叉树的前序遍历
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
left = self.preorderTraversal(root.left)
right = self.preorderTraversal(root.right)
return [root.val] + left + right
94. 二叉树的中序遍历
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
left = self.inorderTraversal(root.left)
right = self.inorderTraversal(root.right)
return left + [root.val] + right
145. 二叉树的后序遍历
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
left = self.postorderTraversal(root.left)
right = self.postorderTraversal(root.right)
return left + right + [root.val]
二叉树的迭代遍历
144. 二叉树的前序遍历
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [root]
res = []
while stack:
node = stack.pop()
# 中结点先处理
res.append(node.val)
# 右孩子先入栈
if node.right:
stack.append(node.right)
# 左孩子后入栈
if node.left:
stack.append(node.left)
return res
94. 二叉树的中序遍历
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
cur = root
stack = [] # 不能提前将root结点加入stack中
res = []
while cur or stack:
if cur: # 先迭代访问最底层的左子树结点
stack.append(cur)
cur = cur.left
else: # 到达最左结点后处理栈顶结点
cur = stack.pop()
res.append(cur.val)
cur = cur.right # 取栈顶元素右结点
return res
145. 二叉树的后序遍历
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root:
return []
stack = [root]
res = []
while stack: # 以 中右左 的顺序遍历
node = stack.pop()
res.append(node.val) # 中结点先处理
if node.left: # 左孩子入栈
stack.append(node.left)
if node.right: # 右孩子入栈
stack.append(node.right)
return res[: :-1] # 返回反转之后的结果,反转后即为 左右中的顺序
前序和中序是完全两种代码风格,并不像递归写法那样代码稍做调整,就可以实现前后中序。 这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
二叉树的统一迭代法
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
144. 二叉树的前序遍历
class Solution:
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack = []
res = []
if root:
stack.append(root)
while stack:
node = stack.pop()
if node != None:
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
stack.append(node)
stack.append(None)
else:
node = stack.pop()
res.append(node.val)
return res
94. 二叉树的中序遍历
class Solution:
def inorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack = []
res = []
if root:
stack.append(root)
while stack:
node = stack.pop() # 更新节点
if node != None:
if node.right: # 添加右节点(空节点不入栈)
stack.append(node.right)
stack.append(node) # 添加中节点
stack.append(None) # 中节点访问过,但是还没有处理,加入空节点做为标记。
if node.left: # 添加左节点(空节点不入栈)
stack.append(node.left)
else: # 只有遇到空节点的时候,才将下一个节点放进结果集
node = stack.pop() # 重新取出栈中元素
res.append(node.val) # 加入到结果集
return res
145. 二叉树的后序遍历
class Solution:
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
stack = []
res = []
if root:
stack.append(root)
while stack:
node = stack.pop()
if node != None:
stack.append(node)
stack.append(None)
if node.right:
stack.append(node.right)
if node.left:
stack.append(node.left)
else:
node = stack.pop()
res.append(node.val)
return res
二叉树的属性
二叉树的修改与构造
求二叉搜索树的属性
二叉树公共祖先问题
二叉搜索树的修改与构造
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在各个地址的节点串联一起。
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是i
,那么它的左孩子就是 i * 2 + 1
,右孩子就是 i * 2 + 2
。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
二叉树的定义
class TreeNode:
def __init__(self, val, left = None, right = None):
self.val = val
self.left = left
self.right = right