二叉树理论基础
二叉树种类
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
一棵满二叉树的节点数与节点深度的关系:深度为k的一颗满二叉树有2^k-1个节点。
完全二叉树: 在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
满二叉树与完全二叉树的区别
满二叉树与完全二叉树的最大区别在于叶子节点是否填满,完全二叉树的叶子节点是未填满的,且叶子节点集中于左边,即叶子节点在最后一层顺序中,是从左往右的一个顺序进行填补的。
二叉搜索树
二叉搜索树是有数值的,是一个有序树。具有以下特点:
- 若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
- 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
- 它的左、右子树也分别为二叉排序树
基于上述特点,二叉搜索树的中序遍历是一个有序数组。
平衡二叉搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
二叉树的存储方式
链式存储(用的较多):链式存储通过指针把分布在各个地址的节点串联一起。
顺序存储:顺序存储通过数组来实现,其在内存中是连续分布的。
如何用数组来遍历二叉树?
需要利用这个特性:如果父节点的数组下标是 i,那么它的左孩子就是 2i + 1,右孩子就是 2i + 2。
二叉树的遍历方式
深度优先遍历:先往深走,遇到叶子节点再往回走
- 前序遍历(递归法,迭代法)—— 根左右
- 中序遍历(递归法,迭代法)—— 左根右
- 后序遍历(递归法,迭代法)—— 左右根
前、中、后的名称用于说明根节点是在哪个顺序被遍历到。
广度优先遍历:一层一层的去遍历
- 层次遍历(迭代法)
二叉树数据结构的定义(代码)
class TreeNode:
def __init__(self, val, left = None, right = None):
self.val = val
self.left = left
self.right = right
递归遍历(需要重点掌握)
需要重点关注三个部分
- 确定递归函数的参数返回值
- 确定终止条件
- 确定单层递归的逻辑
LeetCode 144.二叉树的前序遍历
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def preorderTraversal(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
## 前序遍历:根左右
result = [] ### 存放遍历结果
result = self.dfs(root, result)
return result
def dfs(self, node, result): ### 定义单层递归函数,dfs表示深度优先遍历
if node is None: ### 一、终止条件。在递归的过程中,栈是不断叠加的,当搜索到空节点时,直接return,释放栈的递归过程,从而进行新一轮的搜索。
return result ### 在前序遍历中,为左子树遍历完后,释放栈的左子树遍历,随后进入到右子树的遍历。
### 二、单层递归逻辑:下面三句代码分别是根左右
result.append(node.val) ### 将当前节点的值添加到遍历结果中
self.dfs(node.left, result)
self.dfs(node.right, result)
return result ### 三、确定递归参数返回值
思路:
- 确定递归函数的参数返回值 —— 返回前序遍历结果
- 确定终止条件 —— 当遍历到空节点时,释放栈
- 确定单层递归的逻辑 —— 前序遍历是根左右,因此是先将node存储到遍历结果,再去依次遍历左右节点。
递归可视化:
如左子树的遍历过程:先遍历根节点dfs(1),根节点有左节点,因此dfs(2);值为2的左节点有左节点,因此dfs(4);由于值为4的节点无左右节点,因此不再遍历,退到dfs(2)中,此时dfs(2)还有dfs(5)没遍历,因此dfs(5).....
LeetCode 145.二叉树的后序遍历
# Definition for a binary tree node.
class TreeNode(object):
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
class Solution(object):
def postorderTraversal(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
result = []
result = self.dfs(root, result)
return result
def dfs(self, node, result):
if node == None: ### 终止条件。
return result
self.dfs(node.left, result) ### 单次递归逻辑。后续遍历是左右根
self.dfs(node.right, result)
result.append(node.val)
return result ### 返回参数
LeetCode 94.二叉树的中序遍历
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def inorderTraversal(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
result = []
result = self.dfs(root, result)
return result
def dfs(self, node, result):
if node == None: ### 终止条件。
return result
self.dfs(node.left, result) ### 单次递归逻辑。中序遍历是左中右
result.append(node.val)
self.dfs(node.right, result)
return result ### 返回参数。中序遍历结果
非递归遍历
非递归遍历与递归遍历,最大的区别在于递归遍历只要换下代码顺序就能实现前中后序的遍历效果。但非递归方法不能。
迭代遍历
迭代法实现二叉树的前、后序遍历。
思路:
- 栈所pop出的元素刚好是遍历的顺序
如前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。因此在单次遍历中,首先对根节点进行push操作,随后再pop出,将值存储到遍历结果result中;之后依次将右节点和左节点进行push操作,当进行栈的pop操作,并将元素依次放到遍历结果result中后,此时遍历结果result == [ 根, 左, 右]。
LeetCode 144.二叉树的前序遍历
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def preorderTraversal(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
result = []
if not root: ### 空树
return result
### 空树的话,stack仍为True,后续循环会报错。
stack = [root] ### 先把树的根节点存储到栈中,通过pop操作来获得当前操作的节点
while stack:
node = stack.pop() ### 根节点
result.append(node.val) ### 先存储根节点。遍历顺序是根左右,因此stack栈的推入元素是根右左,推出才会是根左右
if node.right:
stack.append(node.right) ### 右节点
if node.left:
stack.append(node.left) ### 左节点
### 随后继续循环的话就是将stack栈顶的上一轮左节点当为根节点,来继续判断。所以我们只需将每次迭代根节点的遍历存储到result中就行
return result
在root中,唯一可以确定的就是第一个元素是根节点。因此如何利用这个根节点和遍历顺序来获得整颗树的遍历结果是迭代遍历的核心。而迭代遍历利用的是栈“先进后出”特性,由于前序遍历是根左右,那么当栈的推入顺序为根(先push再pop)右左时,栈的推出顺序就成为左右,并且每一轮所pop的第一个元素会优先是左节点(如果有的话),即是当前处理的根节点。这样就满足了迭代遍历左边完后再遍历右边。两步:访问节点+处理节点 代码的实现思路:要访问的元素和要处理的元素顺序是一致的,都是中间节点。
LeetCode 145.二叉树的后序遍历
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def postorderTraversal(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
result = []
if not root:
return result
stack = [root]
while stack:
node = stack.pop() ### 根节点
result.append(node.val) ### 先存储根节点。遍历顺序是根右左,因此stack栈的推入元素是根左右
if node.left:
stack.append(node.left) ### 左节点
#### 将这两个push操作顺序进行了更换
if node.right:
stack.append(node.right) ### 右节点
result.reverse() ### 对结果进行反转
return result
对于后序遍历,其遍历顺序为左右根。而前序遍历为根左右,那么我们可以将前序遍历中栈的推入更改为先推入左节点再推入右节点,这样的话得到的遍历顺序为根右左,随后将这个遍历顺序进行反转就能得到左右根,即后序遍历的遍历顺序。———— 后续遍历可以基于前序遍历实现。
而对于中序遍历,就不能基于前序遍历使用迭代法实现。因为中序遍历是左根右,第一个处理的节不等于访问节点,处理节点需要通过向左遍历获得先。处理顺序和访问顺序是不一致的。
迭代法实现二叉树的中序遍历
思路:
- 用栈记录遍历过的节点,然后再从栈里弹出元素
针对一颗树,先不断向左遍历得到第一个处理节点,并将访问的节点push到栈中。中序遍历是左根右。
- 到达最左侧节点时,需要对stack进行pop操作,并将pop的值去添加到遍历结果result中,和更新访问节点的指针,pop是为了获取根节点的值,更新操作是为了后续判断右子树———— 无左子节点时,更新遍历结果,并判断当前处理节点有无右子节点。
- 若有右子节点,则stack push当前节点指针,并将指针指向该节点的左指针,让其向左遍历判断有无左子节点。如果有,则更新指针;如果没有,则执行1操作
- 当指针和stack均为空时,此时表明整颗树已遍历完毕。
到达第一个处理节点后,此时pop出栈顶元素,去处理下个节点。如下面这颗二叉树,遍历到第一个处理节点时,此时的栈为[1,2,4], 随后pop出栈顶,加入到遍历结果result中,即result == [4];处理并判断当前栈的栈顶元素,如果right指针不为空,则向右遍历进去后不断向左遍历得到该右子树最左侧节点,此时stack == [1,2,5,6],由于6无左子节点,因此pop栈,得到result == [4,6]。相同的思路继续去遍历节点5的右子树,到访问到7时,stack == [1,2,5,7],7无左子节点,因此执行pop操作, result == [4,6,7],并且7无右子节点,因此再继续pop操作,此时stack == [1, 2], result == [4,6,7,5]。... 当最后stack和指针为空时,则表示遍历完毕。
算法思路:先左遍历得到最左侧的节点,并且stack存储这个过程中遍历过的节点。在处理节点时,如果无左节点,则stack进行pop操作,遍历结果result存储这个pop的结果;而对于右子节点,其需要额外判断有无右节点,如果无右节点,cur仍为None,需要额外进行一次pop操作,遍历结果result存储这个pop的结果。
LeetCode 94. 二叉树的中序遍历
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def inorderTraversal(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
stack = []
result = []
if not root: ## 空树的话
return result
cur = root ## 访问指针从根节点出发
while cur or stack:
if cur: ## 如果指针存在,则向左寻找最左侧节点
stack.append(cur)
cur = cur.left
else: ## 指针不存在,则表明到达最左侧节点。
cur = stack.pop() ### 将指针更新到当前这个最左侧的节点
result.append(cur.val) ### 更新遍历结果
cur = cur.right ### 寻找这个最左侧节点有无右节点。如果没有,则会继续执行这个else下的语句
return result
总结:迭代法中前序遍历和后序遍历思路是相同的,后序遍历可以基于前序遍历实现。这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进result数组中)可以同步处理,但是中序就无法做到同步!
统一迭代
后序遍历可以基于前序遍历的迭代法进行优化得出,而中序遍历则是完全另外一种风格了。在这部分是为了使用统一的迭代遍历方法来实现前中后的遍历操作。
无法统一的原因:无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
解决思路:将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记
层序遍历
关键:如何层序保存节点。
解决方法:通过队列实现(队列先进先出,符合一层一层遍历的逻辑),并且通过一个全局变量size来记录每一层的节点数目。
算法思路:通过一个队列来push当前层的元素,并通过一个size来记录当前层的节点数目,当size减为0时,则表示当前层的节点数目已经被对列pop出去了。另外,当当前层节点pop出去时,要保留当前层节点的指针,来看该节点是否还有子节点,有的话,则将节点push进队列中。
需要通过两个数组来保存遍历结果,一个是一维数组,分别保存每一层的遍历结果,另一个是二维数组,来保存全部层的遍历结果,每一行表示每一层的遍历结果。
过程:
① 队列 :[ 3 ] size = 1
[ 9 20 ] size = 0 level = [ 3 ]
② 队列:[ 9 20 ] size = 2
[ 20 ] size = 1
[15, 7] size = 0 level = [ 9, 20 ]
③ 队列:[15, 7] size = 2
[ 15 ] size =1
[ 7 ] size = 0 level = [15, 7]
result = [
[ 3 ] ,
[ 9, 20 ] ,
[ 15, 7 ],
]
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def levelOrder(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[List[int]]
"""
result = []
if not root: ## 空树
return result
queue = deque([root]) ## 先将根节点存储到队列中
while queue: ## 队列为空时,则遍历完毕。
level = [] ## 存储每一层的遍历结果
size = len(queue)
# for _ in range(len(queue)): ### _ 表示这个变量不会被使用;使用for循环的len(queue)虽在后续添加子节点时会变化,当其循环次数取决于刚开始的len(queue),当len(queue)等于2时,使用for循环的话,就只会循环两次,即使循环结束,后面len(queue)变为3了,也仍不影响其只循环两次。
while size: ### len(queue) 记录的是当前层的节点数目
cur = queue.popleft() ### 将当前层的节点pop出,并且保留指向该节点的指针,来将其子节点添加于队列中
level.append(cur.val) ### 存储当前层遍历节点的值
if cur.left: ### 上述在添加节点后,size的大小没有变,而是得当前层遍历完后, size 到下一轮循环时才会更新
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
result.append(level) ### result 是一个二维数组,里面存储了N个一维数组,N是二叉树的深度,一维数组是每一层的遍历结果。
return result
注意的点:
- for _ in range(len(queue)): ### _ 表示这个变量不会被使用;使用for循环的len(queue)虽在后续添加子节点时会变化,当其循环次数取决于刚开始的len(queue),当len(queue)等于2时,使用for循环的话,就只会循环两次,即使循环结束,后面len(queue)变为3了,也仍不影响其只循环两次。
- 而使用while循环时,就不能这样使用,因为len(queue)的动态变化,会影响while的循环次数。因此不能是while len(queue)
LeetCode 关于层序遍历二叉树的题目。
-
102.二叉树的层序遍历
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def levelOrder(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[List[int]]
"""
result = []
if not root: ## 空树
return result
queue = deque([root]) ## 先将根节点存储到队列中
while queue: ## 队列为空时,则遍历完毕。
level = [] ## 存储每一层的遍历结果
size = len(queue)
# for _ in range(len(queue)): ### _ 表示这个变量不会被使用;使用for循环的len(queue)虽在后续添加子节点时会变化,当其循环次数取决于刚开始的len(queue),当len(queue)等于2时,使用for循环的话,就只会循环两次,即使循环结束,后面len(queue)变为3了,也仍不影响其只循环两次。
while size: ### len(queue) 记录的是当前层的节点数目
cur = queue.popleft() ### 将当前层的节点pop出,并且保留指向该节点的指针,来将其子节点添加于队列中
level.append(cur.val) ### 存储当前层遍历节点的值
if cur.left: ### 上述在添加节点后,size的大小没有变,而是得当前层遍历完后, size 到下一轮循环时才会更新
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
result.append(level) ### result 是一个二维数组,里面存储了N个一维数组,N是二叉树的深度,一维数组是每一层的遍历结果。
return result
-
107.二叉树的层次遍历II
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def levelOrderBottom(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[List[int]]
"""
result = []
if not root: ## 判断是否空树
return result
queue = deque([root]) ## 先从根节点开始
while queue:
level = []
size = len(queue)
while size:
cur = queue.popleft()
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
result.append(level)
### 反转实现版本一
# slow, right = 0, len(result) - 1
# while slow < right:
# result[slow], result[right] = result[right], result[slow]
# slow += 1
# right -= 1
### 反转实现版本二
# result = result[::-1]
### 反转实现版本三
result.reverse()
return result
-
199.二叉树的右视图
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def rightSideView(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
result = []
if not root:
return result
queue = deque([root])
while queue:
level = []
size = len(queue)
while size:
cur = queue.popleft()
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
right_level = level[-1] ## 每一层的最后一个元素
result.append(right_level)
return result
-
637.二叉树的层平均值
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def averageOfLevels(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[float]
"""
result = []
if not root:
return result
queue = deque([root])
while queue:
size = len(queue)
level = []
while size:
cur = queue.popleft()
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
level_avg = self.avg_caculate(level)
result.append(level_avg)
return result
def avg_caculate(self, level):
size = len(level)
sum = 0
for i in range(len(level)):
sum += level[i]
avg = float(sum) / float(size)
return avg
-
429.N叉树的层序遍历
"""
# Definition for a Node.
class Node(object):
def __init__(self, val=None, children=None):
self.val = val
self.children = children
"""
class Solution(object):
def levelOrder(self, root):
"""
:type root: Node
:rtype: List[List[int]]
"""
result = []
if not root:
return result
queue = deque([root])
while queue:
level = []
size = len(queue)
while size:
cur = queue.popleft()
level.append(cur.val)
for child in cur.children: ### 关键:如何去获得下一层节点?
if child:
queue.append(child)
size -= 1
result.append(level)
return result
如何获得N叉树下的子节点。当前节点下的字节点用node.children可以获取,若要获取其中的每一个子节点,需要通过循环实现。
-
515.在每个树行中找最大值
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def largestValues(self, root):
"""
:type root: Optional[TreeNode]
:rtype: List[int]
"""
result = []
if not root:
return result
queue =deque([root])
while queue:
level = []
size = len(queue)
while size:
cur = queue.popleft()
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
max_level = max(level)
result.append(max_level)
return result
-
116.填充每个节点的下一个右侧节点指针/ 117.填充每个节点的下一个右侧节点指针II
"""
# Definition for a Node.
class Node(object):
def __init__(self, val=0, left=None, right=None, next=None):
self.val = val
self.left = left
self.right = right
self.next = next
"""
class Solution(object):
def connect(self, root):
"""
:type root: Node
:rtype: Node
"""
if not root:
return root
queue = deque([root])
while queue:
size = len(queue)
prev = None ### 当前层处理节点的上一个节点,初始化为None
while size:
cur = queue.popleft()
if prev: ### prev 用于记录当前层的上一个节点,cur表示当前处理节点
prev.next = cur
prev = cur ### 获得当前节点后,将其作为下一个处理节点的上一个节点
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
return root
题目要求返回的只是一个 Node 对象,然后力扣后台将返回的这个 Node 所代表的整棵树结构进行了序列化。题目的含义是将 # 当成每一层的结束,也只是力扣后台的约定,用其他的标识也都可以。
-
104.二叉树的最大深度
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def maxDepth(self, root):
"""
:type root: Optional[TreeNode]
:rtype: int
"""
result = []
if not root:
return 0
queue = deque([root])
while queue:
size = len(queue)
level = []
while size:
cur = queue.popleft()
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
size -= 1
result.append(level)
return len(result) ### result的长度就是层序遍历的最大长度,即最大深度。
-
111.二叉树的最小深度
# Definition for a binary tree node.
# class TreeNode(object):
# def __init__(self, val=0, left=None, right=None):
# self.val = val
# self.left = left
# self.right = right
class Solution(object):
def minDepth(self, root):
"""
:type root: Optional[TreeNode]
:rtype: int
"""
result = []
if not root:
return 0
queue = deque([root])
min_depth = 1 ### 根节点的深度
while queue:
size = len(queue)
level = []
while size:
cur = queue.popleft()
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
if not cur.left and not cur.right: ### 找到了非叶子节点
return min_depth ### 当前正在遍历的层数,如果存在节点没有子节点,那当前该节点到根节点的路径就是最短路径。
size -= 1
min_depth += 1 ### 每遍历完一层,最小深度+1。此时min_depth的深度表示的是下一轮正在遍历查找的层数。
result.append(level)
return len(result) ## 不存在最短路径,最短路径 == 最长路径