快上车,一文带你读懂二叉树
很开心今天又打开电脑写文章啦,今天依然是二叉树的主题,跟着 Chris 一起走进二叉树的奇妙森林吧。
一、二叉树
在数据结构中有很多种类的树结构,例如,普通二叉树、完全二叉树、满二叉树、线索二叉树、哈夫曼树、二叉搜索树(排序树)、平衡二叉树、AVL 平衡二叉树、红黑树、B 树、B+树、堆。本次我们先从基础入手,主要带大家了解普通二叉树、满二叉树和完全二叉树。
普通二叉树:是由 n(n>=0) 个有限元素的集合,当集合为空时,该二叉树为空树,当集合不为空时,由根 (root) 节点和两个不相交的左子树和右子树组成,如图。
在二叉树中,一个元素被称为节点,二叉树的递归定义为:二叉树或者是一颗空树,或者是一颗由根节点和两颗互不相交的左子树和右子树所组成的非空树,左子树和右子树同样也是一颗二叉树。
满二叉树:在一颗二叉树中,如果所有的分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,如图。
完全二叉树:叶子节点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左边,如图。
注意:满二叉树肯定是完全二叉树,而完全二叉树不一定是满二叉树。
二、二叉树遍历
当我们了解二叉树的基本结构后,二叉树在我们实际应用中发挥着什么作用呢?数据结构遍布我们的生活,比如 MySQL 的 innodb 引擎,其索引结构就是使用的 b+ 树。
二叉树有两种通用的遍历策略,分别为深度优先搜索 (DFS) 和广度优先搜索 (BFS)。
深度优先搜索,在进行遍历或者说搜索的时候,选择一个没有被搜过的节点(一般选择根节点),按照深度优先,一直往该节点的后续路径节点进行访问,直到该路径的最后一个节点,即二叉树的叶子节点,然后再从未被访问的邻节点进行深度优先搜索,重复以上过程,直至所有节点都被访问,遍历结束。
深度优先搜索可以根据根节点、左孩子和右孩子的相对顺序遍历的不同被细分为前序遍历,中序遍历和后序遍历。以满二叉树为例:
前序遍历:1,2,4,5,3,6,7
中序遍历:4,2,5,1,6,3,7
后序遍历:4,5,2,6,7,3,1
广度优先搜索简言之就是按照高度顺序一层一层的访问整棵树,高层次的节点将会比低层次的节点先被访问到。首先访问根节点,然后广度优先访问其邻节点,然后再依次进行被访问点的邻节点,一层一层访问,直至访问完所有节点,遍历结束。
广度优先搜索即层次遍历:1,2,3,4,5,6,7
接下来,Chris 将通过具体的代码带大家领略深度优先搜索和广度优先搜索的魅力,其中我们的代码将会通过递归和迭代两种方式来进行讨论,而迭代和递归的的区别如下:
迭代:循环结构,使用 for 或者 while 循环来实现。
递归:选择结构,使用 if 及 else 调用自己,并在合适的时机退出。
三、深度搜索遍历
3.1 二叉树的前序遍历
二叉树的前序遍历需要按照根左右的顺序对节点进行遍历。
1、递归
如果根节点不存在,直接返回;
如果根节点存在,则把根节点值插入 ans 中,之后递归遍历根节点的左子树和右子树。
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
ans=[]
if root:
ans.append(root.val)
ans+=self.preorderTraversal(root.left)
ans+=self.preorderTraversal(root.right)
return ans
2、迭代
在二叉树的迭代中,我们利用了一个辅助栈,我们知道队列是先进先出的性质,而栈则是后进先出。
如果根节点不存在,直接返回;
把二叉树的根节点 root 推进栈;
循环判断栈是否为空,如果栈不为空,从栈中取出一个节点 curr,把 curr 节点推进栈;
判断 curr 节点的右节点是否存在,如果存在则把 curr 的右节点推入栈;
判断 curr 节点的左节点是否存在,如果存在则把 curr 的左节点推入栈;
之后循环遍历栈,直到栈为空,此时遍历结束。
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
res=[]
if root is None:
return res
stack=[]
stack.append(root)
while stack:
curr=stack.pop()
res.append(curr.val)
if curr.right: # 栈后进先出,所以先放右节点
stack.append(curr.right)
if curr.left:
stack.append(curr.left)
return res
前序遍历我们也可以使用下面这种思想,可以每次先输出当前结点 curr,并将右子节点推进栈中,然后将左子结点设为当前节点。入栈和出栈条件(当前结点 curr 不为 None 时,每一次循环将当前结点 curr 入栈;当前结点 curr 为 None 时,则出栈一个结点)以及循环结束条件(整个循环在 stack 和 curr 皆为 None 的时候结束),与中序遍历一模一样。
class Solution:
def preorderTraversal(self, root: TreeNode) -> List[int]:
res=[]
stack=[]
curr=root
while stack or curr:
if curr:
res.append(curr.val)
stack.append(curr.right)
curr=curr.left
else:
curr=stack.pop()
return res
3.2 二叉树的中序遍历
二叉树的中序遍历需要按照左根右的顺序对节点进行遍历。
1、递归
和前序遍历代码类似,只是把存储根节点的位置换到了中间,变成中序遍历的顺序。
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res=[]
if root:
res+=self.inorderTraversal(root.left) # 注意不能使用append,否则结果是[1,[3],2],而不是[1,3,2]
res.append(root.val)
res+=self.inorderTraversal(root.right)
return res
更加简洁:
class Solution:
def inorderTraversal(self, root):
if not root:
return []
return self.inorderTraversal(root.left) + [root.val] + self.inorderTraversal(root.right)
2、迭代
思想:当前结点 curr 不为 None 时,每一次循环将当前结点 curr 入栈;当前结点 curr 为 None 时,则出栈一个结点,且在结果中存储出栈结点的 val 值。整个循环在 stack 和 curr 皆为 None 的时候结束。
在 while 循环中,当 curr 节点存在时,把根节点及其所有左子节点入栈;
当 curr 节点不存在时,从栈中取出一个节点,把该节点存储到容器中,同时把 curr 节点转向右子节点;
重复以上步骤,直到栈为空并且当前节点也为空,则退出循环。
class Solution:
def inorderTraversal(self, root: TreeNode) -> List[int]:
res=[]
stack=[]
curr=root
while stack or curr:
if curr:
stack.append(curr)
curr=curr.left
else:
curr=stack.pop()
res.append(curr.val)
curr=curr.right
return res
3.3 二叉树的后序遍历
二叉树的后序遍历需要按照左右根的顺序对节点进行遍历。
1、递归
和前序遍历、中序遍历代码类似,只是把存储根节点的位置换到了最后,变成后序遍历的顺序。
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
res=[]
if root:
res=self.postorderTraversal(root.left)
res+=self.postorderTraversal(root.right)
res.append(root.val)
return res
2、迭代
把二叉树的根节点 root 入栈;
如果栈不为空,从栈中取出一个节点,把该节点插入到容器的头部;如果该节点的左子树不为空,则把该左子树放入栈中;如果该节点的右子树不为空,则把右子树放入栈中。
一直重复第二个步骤 ,直到栈为空,此时遍历结束。
注意:前序遍历和中序遍历是把节点插入到容器的尾部,而此处的后序遍历则是把节点插入头部。
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
res=[]
stack=[]
if root is None:
return res
stack.append(root)
while stack:
curr=stack.pop()
res.insert(0,curr.val)
if curr.left:
stack.append(curr.left)
if curr.right:
stack.append(tmp.right)
return res
如果当前节点的右节点和上一次遍历的节点相同,那就表明当前是从右节点过来的了。
class Solution:
def postorderTraversal(self, root: TreeNode) -> List[int]:
res=[]
stack=[]
cur=root
last=None
while cur or stack:
if cur:
stack.append(cur)
cur=cur.left
else:
tmp=stack[-1]
# 是否变到右子树
if tmp.right and tmp.right !=last:
cur=tmp.right
else:
res.append(tmp.val)
last=tmp
stack.pop()
return res
四、广度优先搜索
广度优先搜索,即逐层地,从左到右访问所有节点。
4.1 二叉树的层次遍历
1、递归:
如果当前节点 node 不存在,直接返回;
如果当前容器 res 的长度等于深度,初始深度为0,则向 res 中插入下一层;
最后在当前深度层插入 node的节点值,同时递归遍历 node 节点的左节点和右节点。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
res=[]
def helper(node,depth):
if not node:
return
if len(res)==depth:
res.append([])
res[depth].append(node.val)
helper(node.left,depth+1)
helper(node.right,depth+1)
helper(root,0)
return res
2、迭代
把二叉树的根节点 root 放进栈。
如果栈不为空,取出栈的头节点 curr,存储curr节点值,同时把 curr 节点的左右子节点入栈中;
一直重复第二个步骤 ,直到队列为空,此时遍历结束。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
res=[]
stack=[]
if root is None:
return res
stack.append(root)
while stack:
curr=stack[0]
res.append(curr.val)
if curr.left:
stack.append(curr.left)
if curr.right:
stack.append(curr.right)
return res
以上代码输出的节点值都在同一个数组中,如[3, 9, 20, 15, 7],如果想输出[[3],[9,20],[15,7]]这样的格式,就需要增加深度。
class Solution:
def levelOrder(self, root: TreeNode) -> List[List[int]]:
if root is None:
return
res=[]
stack=[root]
depth=0
while stack:
res.append([])
for i in range(len(stack)):
node=stack.pop(0)
res[depth].append(node.val)
if node.left:
stack.append(node.left)
if node.right:
stack.append(node.right)
depth+=1
return res
好了,今天就到这里结束啦,希望大家喜欢。
点赞、收藏、关注随意,祝我们终将自由。