代码随想录——二叉树篇
在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
深度为k的满二叉树,拥有2^k-1个节点
完全二叉树的定义如下:
1.在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值;2.并且最下面一层的节点都集中在该层最左边的若干位置。
之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
(堆是一棵完全二叉树,树中每个结点的值都不小于(或不大于)其左右孩子的值。 如果父亲结点是大于等于左右孩子就是大顶堆,小于等于左右孩子就是小顶堆.)
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
左子结点.值 < 根节点.值 < 右子结点.值
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树
下面这两棵树都是搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过****1,并且左右两个子树都是一棵平衡二叉树。
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。
C++中****map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,
unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。
链式存储如图:
链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?
其实就是用数组来存储二叉树,顺序存储的方式如图:
用数组来存储二叉树如何遍历的呢?
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
所以大家要了解,用数组依然可以表示二叉树。
二叉树主要有两种遍历方式:
深度优先遍历dfs:先往深走,遇到叶子节点再往回走。
广度优先遍历bfs:一层一层的去遍历。
那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
深度优先遍历:
前序遍历(递归法,迭代法)
中序遍历(递归法,迭代法)
后序遍历(递归法,迭代法)
广度优先遍历:
层次遍历(迭代法)
在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。
这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。
看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中
大家可以对着如下图,看看自己理解的前后中序有没有问题。
我们来看看链式存储的二叉树节点的定义方式。二叉树的定义 和 链表 是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子。
二叉树的结点,有一个val, 有两个指针
class TreeNode:
def __init__(self, val):
self.val = val
self.left = None
self.right = None
递归怎么写:
前序
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = []
def dfs(node):
if not node: return
res.append(node.val)
dfs(node.left)
dfs(node.right)
dfs(root)
return res
中序
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = []
def dfs(node):
if not node: return
dfs(node.left)
res.append(node.val)
dfs(node.right)
dfs(root)
return res
后序
def preorderTraversal(self, root: TreeNode) -> List[int]:
res = []
def dfs(node):
if not node: return
dfs(node.left)
dfs(node.right)
res.append(node.val)
dfs(root)
return res
前序遍历是根左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
先把根节点加入栈内,只要栈不空,弹出栈顶元素,用curr记录一下,并把它的值加入Res数组内;接着判断一下右孩子是否存在,存在加入栈;判断左孩子是否在,存在加入栈,然后重复执行这个动作
def preorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root: return []
res, stack = [], []
stack.append(root)
while len(stack):
curr = stack.pop()
res.append(curr.val)
if curr.right:
stack.append(curr.right)
if curr.left:
stack.append(curr.left)
return res
中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
# 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 []
res, stack = [],[]
curr = root
while curr or len(stack):
if curr != None:
stack.append(curr)
curr = curr.left
else:
curr = stack.pop()
res.append(curr.val)
curr = curr.right
return res
前序: 根左右 ——> 根右左——>左右根
代码逻辑,只要把左子结点和右子结点的入栈顺序调个个儿,res 数组存储的就是根右左的遍历顺序,然后把res数组颠倒输出就行了
def postorderTraversal(self, root: Optional[TreeNode]) -> List[int]:
if not root: return []
res, stack = [], []
stack.append(root)
while len(stack):
curr = stack.pop()
res.append(curr.val)
if curr.left: stack.append(curr.left)
if curr.right: stack.append(curr.right)
res = res[::-1]
return res
这个我实在没有余力短时间吸收掌握
参考链接二叉树统一迭代写法
迭代方法和递归方法遍历二叉树其实都是dfs, 经常笔试题常用的dfs其实经常使用递归去做
前序,中序,后序
前序,后序,中序
在下一个博客