0
点赞
收藏
分享

微信扫一扫

算法学习笔记——数据结构:二叉树(结点数、反序列化、最近公共祖先LCA)

穆风1818 2022-04-03 阅读 102

二叉树BinaryTree

总体框架:

  • 二叉树算法设计的框架:关注当前节点要做什么,左右子树交给递归框架,不用当前节点操心
  • 一旦涉及建树、修改等操作,函数最终应该返回TreeNode节点,并且接收递归调用的返回值(赋值给节点的左右子树)

递归问题,如何写函数

二叉树的问题多为递归问题,任何递归型的问题弄清“灵魂三问”:
1. 这个函数是干什么的?
2. 函数参数是什么?
3. 得到函数的递归结果,应该干什么?

实际上,动态规划中要明确函数的“定义”、“状态”、“选择”,其本质就是上面的“灵魂三问”
可见,二叉树、递归与动态规划,内在套路是有一致性的、是相通的

完全二叉树、满二叉树

求节点个数:

  • 对于一般二叉树,遍历所有节点
  • 对于满二叉树,求出深度d,节点总数为2^d-1
  • 对于完全二叉树,分别处理左右子树结点数(其两棵子树中至少有一棵是满二叉树)
    利用特性:对于迭代中的每棵子树,向左走到底得到深度dl、向右走到底得到深度dr,如果dl==dr,该子树为完全二叉树,按照技巧2处理,否则按照一般二叉树的方式处理

遍历二叉树

遍历二叉树的方式有两种:递归遍历和迭代(层次)遍历

1. 递归遍历(DFS),即前/中/后序遍历

  • 应用:求二叉树的深度/结点总数/结点值的总和

前序遍历,处理顺序大致是“从上往下”;后序遍历,大致是“从下往上”
框架:

class TreeNode(object):
    """二叉树的结点"""
    def __init__(self, val):
        self.val = val
        self.left = None
        self.right = None

def traverse(root: TreeNode):
    """二叉树的递归遍历"""
    # 在此前序遍历
    traverse(root.left)
    # 在此中序遍历
    traverse(root.right)
    # 在此后序遍历

2. 迭代遍历(BFS,用队列实现),即层次遍历

  • 应用:求二叉树的宽度、逐行打印二叉树

框架:

from queue import Queue
def traverse(root: TreeNode):
    """二叉树的递归遍历"""
    if root is None:
        return
    q = Queue()
    q.put(root)

    while not q.empty():
        node = q.get()
        # 层次遍历代码,在此处访问节点
		# 每取出一个节点,将其左右子节点入队
        if node.left:
            q.put(node.left)
        if node.right:
            q.put(node.right)

具体实现在下面“序列化和反序列化二叉树”部分中介绍

序列化和反序列化二叉树

序列化

  • 序列化理解为二叉树的前/中/后序遍历的结果
  • 因此序列化的本质就是遍历二叉树

反序列化

  • 反序列化理解为根据遍历结果还原二叉树
    核心是:在遍历结果中确定根、左右子树的下标范围,然后建立根、递归建立其左右子树

  • 算法框架:大多情况下反序列化的算法框架与序列化基本相同,即遍历二叉树的框架

前序遍历实现

序列化:就是前序遍历,在遍历同时注意打印值即可(None打印为#)

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Codec:
    def serialize(self, root):
        """前序遍历的序列化
        在遍历同时注意打印值即可(None打印为#)"""
        ans = []

        def traverse(node):
            nonlocal ans
            if node is None:
                ans.append('#')
                return
            # 前序遍历
            ans.append(str(node.val))

            traverse(node.left)
            traverse(node.right)
            
        traverse(root)
        return ','.join(ans)

反序列化遵循前序遍历的规则,先确定根,然后递归生成左右子树

class Codec:
    def deserialize(self, data):
        """前序遍历的反序列化
        整体框架同序列化"""
        nodes = data.split(',')  # 节点的列表
        cur = 0
        length = len(nodes)

        def buildTree():
            """使用[cur,末尾]范围构造子树"""
            nonlocal cur, length
            if cur >= length:
                return None

            # 遵循前序遍历的规则,先确定根,然后递归生成左右子树

            first = nodes[cur]  # 列表最左侧就是根节点
            cur += 1  # 处理完一个节点
            
            # 空节点,直接返回
            if first == '#':
                return None
            # 非空节点,需要建立左右子树
            root = TreeNode(int(first))
            root.left = buildTree()
            root.right = buildTree()
            return root

        return buildTree()

中/后序遍历实现

序列化:方式与前序遍历类似,改变打印节点值的位置即可
反序列化

  • 中序遍历的反序列化:单靠中序遍历无法反序列化(因为不知道根节点在中间的那个位置)
  • 后序遍历的反序列化:
    显然不能继续套用后序遍历框架,因为找不到左右子树的根,仍应先确定根,然后递归生成左右子树。应该从后往前处理,即(后序遍历结果)根在首字符,前一个字符就是右子树的根,(处理完右子树后)再前一个字符就是左子树的根

层次遍历实现

层次遍历需要用队列实现(BFS)
序列化:

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
from queue import Queue
class Codec:

    def serialize(self, root):
        """中序遍历的序列化"""
        if root is None:
            return '#'
        ans = []  # 保存序列化的结果
        q = Queue()
        q.put(root)
        while not q.empty():
            node = q.get()
            if node is None:
                ans.append('#')
                continue
            ans.append(str(node.val))
            q.put(node.left)
            q.put(node.right)
        return ','.join(ans)

反序列化:
维护队列,只有非空节点入队
维护独立的指针ii依次指向[中序遍历结果序列]中的每个节点,
每次取出队首节点,指针i同步处理序列中的两个节点,分别作为队首父节点的左右儿子,得到左右儿子后继续将他们入队(符合层次遍历顺序)

from queue import Queue
class Codec:
    def deserialize(self, data):
        """层次遍历的反序列化
        同样用队列实现(队列中保存非空节点),
        每次从队首取出父节点,同时指针i依次遍历节点序列,作为父节点的左右儿子,并入队
        """
        nodes = data.split(',')  # 节点的列表
        if nodes[0] == '#':
            return None
        # 根节点
        root = TreeNode(nodes[0])
        q = Queue()
        q.put(root)  # 根节点入队

        i = 1
        length = len(nodes)
        while i < length:  # 迭代,每次从队列中取出一个节点,左右儿子根据指针i获得
            # 取出父节点
            father = q.get()

            nextVal = nodes[i]  # 下一个节点的值,若为#则代表空节点
            i += 1  # 后移指针
            # 左儿子
            if nextVal != '#':
                father.left = TreeNode(int(nextVal))
                q.put(father.left)
            else:
                father.left = None

            nextVal = nodes[i]  # 下一个节点的值,若为#则代表空节点
            i += 1  # 后移指针
            # 右儿子
            if nextVal != '#':
                father.right = TreeNode(int(nextVal))
                q.put(father.right)
            else:
                father.right = None
        return root

最近公共祖先LCA

求二叉搜索树的最近公共祖先

从根节点开始,利用BST的特性,根据当前节点值和p、q节点值判断:

  • 如果两个节点都在当前节点的左/右子树,则进入左/右子树
  • 否则,两节点一定不在同一子树,当前节点就是“分叉点”,返回当前结点

求二叉树的最近公共祖先

普通的二叉树,无法像BST那样直接判断某节点是否在子树中

  1. 首先,二叉树的问题,先把遍历框架写出来准没错
  2. 类似于上面的思路,站在当前的节点上,我们需要看其左/右子树的情况,若当前根节点就是p/q,或若p、q节点分别在其左/右子树中,则当前节点就是公共祖先
  3. 更进一步,如何保证是最近公共祖先?从树的叶子节点往上走,第一个符合上述条件的节点,就是最近公共祖先,i.e.应采用后序遍历
  4. 当前的根节点是最近公共祖先的情况有:
  • root为p,q中的一个,最近公共祖先为root
  • p,q分别在root的左右子树上,最近公共祖先为root
  • p和q同时在root的左子树或同时在右子树,则root虽然是公共祖先,但是真正的最近公共祖先在root的下层(确定最近公共祖先需要遍历子树来进行递归求解)
  1. 递归函数的“灵魂三问”:
    ①这个函数是干什么的:最终要返回最近公共祖先,递归过程中返回None或p/q/最近公共祖先,用于判断p、q是否在左右子树中
    ②函数参数是什么:当前所在树的根节点
    ③得到函数的递归结果,应该干什么:用于判断p、q节点是否在当前结点的左/右子树中
    返回结果:
    若p、q分别位于左、右子树中,返回root;
    左、右子树都没有p、q,返回None;
    左、右子树中只有一个含有p/q/p和q,返回那个子树的递归结果(从而保证能返回最近公共祖先)
# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None

class Solution:
    def lowestCommonAncestor(self, root: 'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        """给出根、两个节点,返回最近公共祖先
        base case:root就是p、q,直接返回最近公共祖先root
        1.(排除base case)root是最近公共祖先的情况,一定是p、q分别位于左、右子树中
        2.若左、右子树中都没有p、q,返回None
        3.剩下的情况是,左、右子树中的一个含有p/q/p和q,返回那个子树的递归结果(不能返回root,否则不是最近公共祖先)"""
        if root is None:
            return None
        if root == p or root == q:
            return root

        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)

        # 后序遍历位置(从下往上),判断若当前节点左右子树包含p、q,则说明是最近公共祖先
        if left and right:  # 情况1
            return root
        if not left and not right:  # 情况2
            return None
        return left if left else right  # 情况3

举报

相关推荐

0 条评论