算法—树
📟

算法—树

Created time
Aug 4, 2021 02:40 AM
Tags
blog
leetcode
algorithm
Priority
Status
Date
Aug 4, 2021

LeetCode

👨🏻‍💻 Github repo:
 

树的问题

这类问题有个大体的框架 👇:
def traverse_tree(root: Node):
	// 先序遍历代码位置
	traverse(root.left)
	// 中序遍历代码位置
	traverse(root.right)
	// 后序遍历代码位置
回到实际的问题中来,先序遍历和后序遍历的特点是:
  • 先序遍历的第一个节点为根结点 root
  • 后序遍历的最后一个节点为根节点 root
  • 中序遍历中,root 节点的左边部分为 root 的左子树,root 节点的右边部分为 root 的右子树
 

105 & 106

根据先序遍历和中序遍历还原一棵树 or 根据中序遍历和后续遍历还原一棵树。

掌握了上面说的树的规律,基本上就有如下解题步骤:
  1. 边界条件
    1. if not preorder or not inorder: 
      	return None
  1. 找到根结点 root
  1. 找到 root 在中序遍历中的索引
  1. 构造 root node
  1. 如果条件是 inorder + preorder ,则先遍历左子树再遍历右子树;如果条件是 inorder + postorder ,则先遍历右子树再遍历左子树。
    1. 原因:
      对于知道 preorder 的情况,preorder 第二个节点是 level 2 中左子树的根结点,因此递归的时候注意先递归左子树; 而对于知道 postorder 的情况,postorder 的倒数第二个节点是 level 2 中右子树的根结点,因此递归的时候注意先递归右子树;
       
  • 前序遍历和中序遍历
class Solution:
    def buildTree(self, preorder: List[int], inorder: List[int]) -> TreeNode:
        if not preorder or not inorder:
            return None

        # 这里的 pop 出第一个节点很关键,先序遍历第一个节点就是根结点
        root_val = preorder.pop(0)
        ind = inorder.index(root_val)
        root = TreeNode(root_val)
        root.left = self.buildTree(preorder, inorder[0:ind])
        root.right = self.buildTree(preorder, inorder[ind+1:])
        return root
  • 后序遍历和中序遍历
class Solution:
    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
        if not inorder or not postorder:
            return None

        root = TreeNode(postorder.pop())
        ind = inorder.index(root.val)
        # 这里的关键在于,先要递归生成右子树,原因:
        # postorder 倒数第二个节点是 level 2 右子树的根结点,而不是左子树,因此
        # 必须先生成右子树而非左子树
        root.right = self.buildTree(inorder[ind+1:], postorder)
        root.left = self.buildTree(inorder[:ind], postorder)
        return root

116

填充每一个节点的下一个右侧节点指针
notion image
 
还是使用递归的办法,但是会和简单的递归思维有一些改动的地方。
首先,我们最容易想到如下这种递归:
def connect(root):
	if not root or not root.left: return None
	root.left.next = root.right
	connect(root.left)
	connect(root.right)
	return root
但这样会有一个问题:
当两个节点拥有同一个父节点时,上面的做法是可行的。而仔细观察后我们发现,5 和 6 节点并没有连接同一个父节点,这时候,从 5 → 6 的操作就有问题了。
既然这个题递归时使用一个节点不行,那么我们换一种思维,使用两个节点来连接,是不是就可行了呢?于是,我们可以尝试以下方法,同时对两个节点进行递归操作:
def connect_two_nodes(node1, node2):
	# 基本条件,当节点不存在,直接返回
	if not node1 or not node2:
		return 
	# 第一步:对拥有同一父节点的两个兄弟节点进行直接连接
	node1.next = node2
	# 第二步:分别对两个节点的左右子节点进行递归连接
	connect_two_nodes(node1.left, node1.right)
	connect_two_nodes(node2.left, node2.right)
	# 第三步:左边节点的右子节点 -> 右边节点的左子节点
	connect_two_nodes(node1.right, node2.left)

def connect(root):
	if not root: return None
	connect(root.left, root.right)
	return root

除了递归,我们再使用层序遍历来解这道题。
对于上述问题,有两种连接方式:
  • 连接1 → 两个节点具有共同的父节点,我们可以使用以下方式对这种情况进行连接:
node.left.next = node.right
  • 连接2 → 两个节点没有共同的父节点,但是可以通过第一个父节点的 next 节点获取到第二个父节点,将第一个父节点的右孩子与第二个父节点的左孩子连接:
node.right.next = node.next.left
完成当前的连接后,进入下一层重复操作,知道所有节点都已连接。进入下一层后需要先更新最左节点,然后从最左节点开始遍历该层节点。因为是完美二叉树,所以当最左节点的左孩子不在时,表明该层已经是最后一层,完成了所有节点的连接。
def connect(self, root):
    if not root: return root

    # 从根结点开始
    leftmost = root

    while leftmost.left:
        # 遍历同一层节点链表
        head = leftmost
        while head:
            # 连接具有统一父节点的两个左右子节点
            head.left.next = head.right
            # 如果两个节点相邻且没有共同父节点,那么可以通过父节点的 next 节点来连接
            if head.next:
                head.right.next = head.next.left
            # 遍历到下一个节点,能运行到这里说明上一层操作中已经将本层的节点都已经连接好了
            head = head.next
        # 接下来对下一层进行操作
        leftmost = leftmost.left
    return root
 

114

将二叉树展开为链表
notion image
我们可以抽象出以下两个流程:
  1. 将 root 的左右子树拉平
  1. 将左子树接到右边,左子树置为 None
  1. 将新的右子树的最右子节点连接原来的右子树
代码如下:
def flatten(self, root):
    # base case
    if not root:
        return
    # 拉平左右子树
    self.flatten(root.left)
    self.flatten(root.right)

    # 暂存一下左右子树
    left = root.left
    right = root.right

    # 将左子树接到右子树
    root.right = left
    # 将左子树置为 None
    root.left = None

    # 先选取旧左子树进行操作
    tmp = root
    # 找到旧左子树的最右子节点
    while tmp.right:
        tmp = tmp.right

    # 将左子树的最右子节点连接右子树
    tmp.right = right
值得注意的是,我们需要对左右子树先进行 flatten 的递归操作,这一步的目的是拉平左右子树。
当拉平左右子树后,我们就可以根据逻辑对左右子树进行连接和置换操作。