本系列是算法通关手册LeeCode的学习笔记
算法通关手册(LeetCode) | 算法通关手册(LeetCode) (itcharge.cn)
目录
一,树(Tree)
树的相关术语
节点间关系
树的其他术语
二,二叉树(Binary Tree)
满二叉树(Full Binary Tree)
完全二叉树(Complete Binary Tree)
二叉搜索树(Binary Search Tree)
平衡二叉搜索树(Balanced Binary Tree)
三,二叉树的存储结构
顺序存储结构
链式存储结构
四,二叉树的遍历
二叉树的前序遍历
二叉树的中序遍历
二叉树的后序遍历
二叉树的层序遍历
五,二叉树的还原
从前序和中序序列构造二叉树:
从中序和后序序列构造二叉树
从前序和后序序列构造二叉树
一,树(Tree)
由 n >= 0 个节点与节点之间的关系组成的有限集合。 n = 0 时称空树,n > 0 时称非空树。
树 具有以下特点:
有且仅有一个节点没有前驱节点,该节点称为树的【根节点(Root)】;
除了根节点之外,每个节点有且仅有一个直接前驱节点;
包括根节点在内,每个节点可以有多个后继节点(一对多的关系);
当 n > 1 时,除了根节点之外的其他节点与其后继节点组成的也是一棵树,称为根的
【子树(Sub Tree)】;
树的相关术语
树的节点:由一个数据元素和若干个指向其子树的分支组成(节点 加 指向子树的箭头);
节点的度:一个节点所含有的子树个数;
叶子节点:度为 0 的节点,如图中的 C、F、H、I、G、K;
分支节点:度不为 0 的节点,如图中的 A、B、D、E、G;
数的度:树中节点的最大度数,如图中树的度为 3;
节点间关系
子节点:一个节点含有的【子树的根节点】称为该节点的子节点;
父节点:如果一个节点含有子节点,则称该节点为其子节点的父节点;
兄弟节点:具有相同父节点的节点互称为兄弟节点;
树的其他术语
节点的层次:从根节点开始定义,根为第 1 层,根的子节点为第 2 层,以此类推;
树的深度(高度):所有节点中最大的层数;
堂兄弟节点:父节点在同一层的节点互为堂兄弟;
路径:树中两个节点之间所经过的节点序列。如图中 E 到 G 的路径为 E-B-A-D-G;
路径长度:两个节点之间路径上经过的边数。如图中 E 到 G 的路径长度为 4;
节点的祖先:从该节点到根节点所经过的所有节点,被称为该节点的祖先。如图中 H 的祖先为 E、B、A;
节点的子孙:节点的子树中所有节点被称为该节点的子孙。例如图中 D 的子孙为 F、G、K
有序树:节点的各个子树从左至右有序,不能呼唤位置;
无序树:节点的各个子树可互换位置。
二,二叉树(Binary Tree)
树中各个节点的度不大于 2 个的有序树,称为二叉树。通常树中的分支节点被称为【左子树】或【右子树】。二叉树的分支具有左右次序,不能随意互换位置。
二叉树可以用递归的方式来定义,即二叉树满足以下两个要求之一:
空树:二叉树是一个空树;
非空树:二叉树是有一个根节点和两棵互不相交的子树,分别称为根节点的左子树、右
子树组成的非空树,并且其本身都是二叉树。
二叉树是种特殊的树,它最多有两个子树,分别为左子树和右子树,并且两个子树是有序的,不可以互换。也就是说,在二叉树种不存在度大于 2 的节点。
二叉树在逻辑上可以分为 5 种基本形态
满二叉树(Full Binary Tree)
如果所有分支节点都存在左子树和右子树,并且所有叶子节点都在同一层上,则称该二叉树为满二叉树。
满二叉树满足:
叶子节点只出现在最下面一层;
非叶子节点的度一定为 2 ;
在同等深度的二叉树中,满二叉树的节点个数最多,叶子节点个数最多。
如果我们对满二叉树的节点进行编号。根节点编号为 1,然后按照层次依次向下,每一层从左到右的顺序进行编号,则深度为 k 的满二叉树最后一个节点的编号为 2^k - 1。
完全二叉树(Complete Binary Tree)
如果叶子节点只能出现在最下面两层,并且最下层的叶子节点都依次排列在该层最左边的位置上,称为完全二叉树。
完全二叉树满足:
叶子节点只能出现在最下面两层;
最下层的叶子节点一定集中在该层最左边的位置上;
倒数第二层如果有叶子节点,则一定集中在右边位置上;
如果节点的度为一,则该节点只有做孩子节点;
二叉搜索树(Binary Search Tree)
也叫二叉查找树,是指一棵空树或具有以下性质的二叉树:
如果任意节点的左子树不为空,则左子树上所有节点的值均小于它根节点的值;
如果任意节点的右子树不为空,则右子树上所有节点的值均大于它根节点的值;
任意节点的左子树、右子树均为二叉搜索树
平衡二叉搜索树(Balanced Binary Tree)
一种结构平衡的二叉搜索树。即叶节点的高度差绝对值不超过 1,并且左右两个子树都是一棵平衡二叉搜索树。
三,二叉树的存储结构
二叉树的存储结构分为两种:【顺序存储结构】和【链式存储结构】。
顺序存储结构
其实,堆排序、优先队列中的二叉堆结构,采用的就是二叉树的顺序存储结构。
二叉树的顺序存储结构使用一维数组在存储二叉树中的节点,节点存储位置则采用完全二叉树的节点层次编号,按照层次从上到下,每一层从左到右的顺序依次存放二叉树的数据元素。在进行顺序存储时,如果对应的二叉树节点不存在,则设置为空节点。
可以看出节点之间的逻辑关系:
如果某二叉树节点(非叶子节点)的下标为 i,那么其左孩子节点的下标为 i * i + 1,右
孩子节点的下标为 2 * i + 2;
如果某二叉树节点(非根节点)的下标为 i,那么其根节点的下标为 (i - 1) // 2
对于完全二叉树(尤其是满二叉树)来说,采用顺序存储结构比较合适,它能充分利用存储空间,而对于一般二叉树,如果需要设置很多的【空节点】,则采用顺序存储结构就会浪费很多存储空间,并且由于顺序存储结构固有的一些缺陷,会使得二叉树的插入,删除等操作不方便,效率也比较低,对于二叉树来说,当数的形态和大小经常发生动态变化时,更适合采用链式存储结构。
链式存储结构
二叉树采用链式存储结构时,每个节点包含一个用于数据域 val ,存储节点信息,还包含两个指针域 left 和 right ,分别指向左右两个孩子节点,当左孩子或右孩子不存在,相应指针域为空。
对应代码:
class TreeNode:
def __init__(self, val=0, left=None, right=None):
self.val = val
self.left = left
self.right = right
二叉树的链表存储结构具有灵活、方便的特点。节点的最大数目只受系统最大可存储空间的限制。一般情况下,二叉树的链表存储结构比顺序存储结构更省空间,而对于二叉树实施相关操作也很方便,因此,一般使用链式存储结构来存储二叉树。
四,二叉树的遍历
从根节点出发,按照某种次序依次访问二叉树中的所有节点,使得每个节点被访问依次且仅被访问一次。
使用深度优先的方式遍历二叉树,分为二叉树的前、中、后序遍历。
使用广度优先遍历二叉树,则是二叉树的层序遍历。
二叉树的前序遍历
如果二叉树为空,则返回;
如果二叉树不为空,则:
访问根节点;
以前序遍历的方式遍历根节点的左子树;
以前序遍历的方式遍历根节点的右子树;
可见前序遍历过程是一个递归过程,在遍历任何一棵子树时,仍然是按照先访问根节点,然后遍历子树根节点的左子树,最后遍历根节点的右子树的顺序进行遍历。
注意这里的访问,指的是得到该节点的 val 值,而不是进入该节点。
递归实现
class Solution:
def preorderTraversal(self, root: TreeNode):
res = []
def preorder(root):
if not root:
return
res.append(root.val)
preorder(root.left)
preorder(root.right)
preorder(root)
return res
前序遍历的显示栈实现:
根据栈先入后出的特点,先放入右子树,再放入左子树。
判断二叉树是否为空,为空则直接返回;
初始化维护一个栈,将根节点入栈;
当栈不为空时:
弹出栈顶元素 node,并访问该元素;
如果 node 的右子树不为空,则将 node 的右子树入栈;
如果 node 的左子树不为空,则将 node 的左子树入栈;
def preorderTraversalStack(self, root):
if not root:
return []
res = []
stack = [root]
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
二叉树的中序遍历
如果二叉树为空,则返回;
如果二叉树不为空,则:
以中序遍历的方式遍历根节点的左子树;
访问根节点;
以中序遍历的方式遍历根节点的右子树;
递归实现:
def inorderTraversal(self, root: TreeNode):
res = []
def inorder(root):
if not root:
return
inorder(root.left)
res.append(root.val)
inorder(root.right)
inorder(root)
return res
中序遍历的显式栈实现:
访问根节点要在左子树遍历完之后,因此我们应该从根节点开始,循环遍历左子树,不断将当前子树的根节点放入栈中,直到当前节点无左子树时,从栈中弹出该节点并处理;
判断二叉树是否为空,为空则直接返回;
初始化维护一个空栈;
当根节点或者栈不为空时,
如果当前节点不为空,则遍历左子树,并不断把当前子树的根节点入栈;
如果当前节点为空,则说明当前节点无左子树,则弹出栈顶元素 node,并访问,然后尝
试访问该节点的右子树;
def inorderTraversalStack(self, root):
if not root:
return []
res = []
stack = []
while root or stack:
while root:
stack.append(root)
root = root.left
node = stack.pop()
res.append(node.val)
root = node.right
return res
二叉树的后序遍历
如果二叉树为空,则返回;
如果二叉树不为空,则
以后序遍历的方式遍历根节点的左子树;
以后序遍历的方式遍历根节点的右子树;
访问根节点
递归实现:
def postorderTraversal(self, root: TreeNode):
res = []
def postorder(root):
if not root:
return
postorder(root.left)
postorder(root.right)
res.append(root.val)
postorder(root)
return res
后序遍历的显式栈实现
后序遍历中,根节点的访问在左右子树访问之后。
判断二叉树是否为空,为空则直接返回;
初始化维护一个空栈,使用 prev 保存前一个访问的节点,用于确定当前节点的右子树是否已经访问完毕;
当根节点或者栈不为空时,从当前节点开始:
如果当前节点右左子树,则不断遍历左子树,并将当前根节点压入栈中;
如果当前节点无左子树,则弹出栈顶元素 node;
如果栈顶元素 node 无右子树或者右子树已经访问完毕,则访问该元素,然后记录前一
节点,并将当前节点标记为空节点,意为需要栈弹出一个栈顶元素;
如果栈顶元素 node 有右子树,则将栈顶元素重新压入栈中,继续访问栈顶元素的右子
树。
def postorderTraversalStack(self, root):
res = []
stack = []
prev = None
while root or stack:
while root:
stack.append(root)
root = root.left
node = stack.pop()
if not node.right or node.right == prev:
res.append(node.val)
prev = node
root = None # 需要弹出新的栈顶元素
else:
stack.append(node)
root = node.right
return res
二叉树的层序遍历
如果二叉树为空,则返回;
如果二叉树不为空,则:
先依次访问二叉树第一层的节点;
然后依次访问二叉树第二层的节点;
...
层序遍历过程是一个广度优先搜索的过程
二叉树的层序遍历通过队列实现:
判断二叉树是否为空,为空则直接返回;
令根节点入队;
当队列不为空时,求出当前队列长度 s;
依次从队列中取出这 s 个元素,并对这 s 个元素依次进行访问,然后将其左右孩子节点入队,然后继续遍历下一层节点;
当队列为空时,结束遍历;
实现代码:
def levelOrder(self, root: TreeNode):
if not root:
return []
queue = [root]
order = []
while queue:
level = []
size = len(queue)
for _ in range(size):
cur = queue.pop(0)
level.append(cur.val)
if cur.left:
queue.append(cur.left)
if cur.right:
queue.append(cur.right)
if level:
order.append(level)
return order
104. 二叉树的最大深度
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
if not root :
return 0
return max(self.maxDepth(root.left), self.maxDepth(root.right)) + 1
103. 二叉树的锯齿形层序遍历
import collections
class Solution:
def zigzagLevelOrder(self, root: TreeNode) -> List[List[int]]:
if not root:
return []
queue = [root]
order = []
odd = True
while queue:
level = collections.deque()
size = len(queue)
for _ in range(size):
curr = queue.pop(0)
if odd:
level.append(curr.val)
else:
level.appendleft(curr.val)
if curr.left:
queue.append(curr.left)
if curr.right:
queue.append(curr.right)
if level:
order.append(list(level))
odd = not odd
return order
111. 二叉树的最小深度
# 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 minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
queue = [root]
dep = 1
while queue:
n = len(queue)
for i in range(n):
node = queue.pop(0)
if node.left == None and node.right == None:
return dep
if node.left:
queue.append(node.left)
if node.right:
queue.append(node.right)
dep += 1
return dep
与最大不同,找树的最小深度算是广度遍历的经典应用场景,逐层的排查节点是否是叶子节点,能够找到树的最小深度。
五,二叉树的还原
指的是通过二叉树的遍历序列,还原出对应的二叉树。
这个部分比较抽象,所以建议找视频资源进行学习:
比如:【已知二叉树中序、后序序列,如何还原二叉树?一道题搞定】 https://www.bilibili.com/video/BV1624y1t7Cg/share_source=copy_web&vd_source=87ace2077a752fc4561aff19bae87dac
简单来说就是根据遍历过程中的规则,不断查找两个遍历结果,实现二叉树的还原,我们的重心在代码实现。而代码实现的重点在于充分明白所知序列能够发挥的作用。
前序序列作用,前序的遍历顺序是【根左右】,因此我们可以从中知道根节点的位置;
中序序列作用,中序遍历的顺序是【左根右】,因此我们知道根节点后,可以知道左子树与右子树的中序序列
后序序列作用,后序遍历的顺序是【左右根】,我们可以知道根节点的位置。
开始代码实现之前,我们要明确的是,只有前序和后序无法唯一确定二叉树。
从前序和中序序列构造二叉树:
从前序遍历序列中得到当前根节点位置在 preorder[ 0 ];
通过在中序遍历中查找上一步根节点对应位置 inorder[ k ],从而将二叉树左右子树分隔开,并得到左右子树节点个数;
从上一步得到的左右子树个节点个数,将前序序列中的左右子树分开;
构造当前节点,并递归建立左右树,在左右树的对应位置继续递归遍历并执行上述三步,直到节点为空
def buildTreeFromPreIn(self, preorder, inorder):
def createTree(preorder, inorder, n):
if n == 0:
return None
k = 0
while preorder[0] != inorder[k]:
k += 1 # 找到中序遍历的分隔位置
node = TreeNode(inorder[k])
node.left = createTree(preorder[1: k + 1], inorder[0: k], k)
node.right = createTree(preorder[k + 1: ], inorder[k + 1: ], n - k - 1)
return node
return createTree(preorder, inorder, len(inorder))
从中序和后序序列构造二叉树
从后序序列中得到当前根节点的位置在 postorder[ n - 1 ];
通过在中序遍历中查找根节点对应的位置 inorder[k],从而将二叉树的所有子树分隔开,并得到左右子树的节点个数;
从上一步得到的左右子树节点个数,将后序序列中的左右子树分开;
构造当前节点,并递归建立左右子树,并在左右子树对应位置继续递归执行上述三步,直到节点为空
def buildTreeFromInPost(self, inorder, postorder):
def createTree(inorder, postorder, n):
if n == 0:
return None
k = 0
while postorder[n - 1] != inorder[k]:
k += 1
node = TreeNode(inorder[k])
node.right = createTree(inorder[k + 1: n], postorder[k: n - 1], n - k - 1)
node.left = createTree(inorder[0: k], postorder[0: k], k)
return node
return createTree(inorder, postorder, len(postorder))
从前序和后序序列构造二叉树
我们可以默认指定前序序列的第 2 个值为左子树的根节点,由此递归划分左右子树。
从前序遍历序列中可知当前根节点的位置在 preorder[ 0 ];
从前序序列的第二个值为左子树的根节点,通过在后序序列中查找上一步根节点对应的位置,postorder[ k ]。从而将二叉树的左右子树分隔开,从得到左右子树节点的个数.
从上一步得到的左右子树个数,将后序序列结果中的左右子树分开;
构造当前节点,并递归建立左右子树,在左右子树对应位置继续递归遍历并执行上述三步,直到节点为空。
def constructFromPrePost(self, preorder: List[int], postorder: List[int]) -> TreeNode:
def createTree(preorder, postorder, n):
if n == 0:
return None
node = TreeNode(preorder[0])
if n == 1:
return node
k = 0
while postorder[k] != preorder[1]:
k += 1
node.left = createTree(preorder[1: k + 2], postorder[: k + 1], k + 1)
node.right = createTree(preorder[k + 2: ], postorder[k + 1: -1], n - k - 2)
return node
return createTree(preorder, postorder, len(preorder))
算法通关手册(LeetCode) | 算法通关手册(LeetCode)
原文内容在这里,如有侵权,请联系我删除。