leetcode刷题记录:二叉树1(纲领篇)

本文详细介绍了二叉树的前序、中序、后序遍历方法,探讨了遍历的递归实现以及回溯框架在动态规划中的应用。着重分析了如何处理二叉树问题,如最大深度和直径计算,强调了辅助函数在解决问题中的角色,并分享了LeetCode上的相关实例。
摘要由CSDN通过智能技术生成

摘自labuladong的算法小抄:https://labuladong.online/algo/essential-technique/binary-tree-summary-2/

1. 二叉树的遍历框架

void traverse(TreeNode* root){
    if (!root) { return; }
    // 前序位置
    traverse(root->left);
    // 中序位置
    traverse(root->right);
    // 后序位置
}

traverse是一个遍历函数,作用就是遍历二叉树的所有节点,前中后序决定了遍历的顺序。数组和链表也可以用类似的方法来遍历。

因为二叉树不容易写成迭代的形式,所以一般我们说二叉树的遍历,就是指递归遍历。

前序位置:刚进入一个结点的时候。(类比三国杀,回合开始的时候)

中序位置:遍历完左子树,刚要开始遍历右子树的时候。所以多叉树没有中序遍历

后序位置:要离开一个结点的时候。(类比三国杀,回合结束的时候)

后序遍历的应用:倒序打印链表

void print_list_node(ListNode* head) {    
    if(head == NULL) {
        return;       
    } 
    print_list_node(head->next);
    print(head->val);
}

每个节点都有自己的前中后序遍历的位置

这里的前序不仅仅是教科书上说的前序遍历(中序、后序同理)。你可以在遍历二叉树时在前序位置往列表里插入元素,这就是教科书上的前序遍历。但不代表你不能在前序位置做更复杂的事情,实现更复杂的功能。

2. 两种解题思路

  1. 回溯框架:遍历二叉树
  2. 动态规划框架:分解问题

2.1 最简单的例子:二叉树的前序遍历

遍历思路解法:用一个外部变量记录res遍历结果,新建一个辅助函数traverse,返回值为空

def preOrder(root):
    traverse(root)
    return res
def traverse(root):
    if (root == None):
        return []
    res.append(root.val)
    traverse(root.left)
    traverse(root.right)

分解思路:不用辅助函数,不用外部变量,preOrder函数返回的是以root为根节点子树的前序遍历结果。这种方法一般不常用,因为各个语言list add的操作的复杂度不可控。

def preOrder2(root):
    if root == None:
        return []
    res = []
    res.append(root.val)
    res.extend(preOrder2(root.left))
    res.extend(preOrder2(root.right))
    return res

这里引出一个重点,什么时候要用辅助函数traverse,什么时候直接用原函数递归?

结论:如果你的问题需要用一个外部函数记录,一般都需要用辅助函数traverse;如果题目要求的答案恰好就是函数本身的返回值,就不用外部函数traverse.

2.2 二叉树的最大深度

leetcode 204
分解的思路,先算出左右子树的最大深度,再+1,因此逻辑要放在后序位置。

class Solution {
public:
    int maxDepth(TreeNode* root) {
        if (root == NULL){
            return 0;
        }
        int depth = max(maxDepth(root->left), maxDepth(root->right));
        depth ++;
        return depth;
    }
};

其实这道题也可以用遍历的思路来解,代码写起来比较麻烦. 核心思路就是遍历整个二叉树,用一个外部变量depth记录当前结点的深度。如果到达叶子结点则更新最终的结果res.

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
class Solution(object):
    def __init__(self, res=0, depth=0):
        self.res = res
        self.depth = depth
    def maxDepth(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        self.traverse(root)
        return self.res
    def traverse(self, root):
        if root == None:
            self.res = max(self.res, self.depth)
            return self.res
        self.depth += 1
        self.traverse(root.left)
        self.traverse(root.right)
        self.depth -= 1

提炼一个通用的解法

  1. 是否可以遍历一遍二叉树得到答案?如果可以,用一个traverse+一个外部变量即可
  2. 是否可以定义一个递归函数,通过子树的答案推导原问题的答案?如果可以,写出这个递归函数的定义,并利用好其返回值

3 后序位置的特殊之处

  1. 中序:bst的中序遍历就是遍历有序数组。BST的特点:每个节点最多有两个子节点,左子节点的值小于父节点的值,右子节点的值大于父节点的值
  2. 前序和后序:前序是自顶向下,后序是自底向上

例子1:把根节点看做第一层打印每个节点所在的层数

void solution(TreeNode* root){
    print_node(root, 1);
}
void print_node(TreeNode* root, int pos){
    if(root == NULL) {       
        return;
    }
    cout << pos << endl;
    print_node(root->left, pos+1);
    print_node(root->right, pos+1);

}

例子2:打印每个节点的左右子树各有多少节点?

int solution(TreeNode* root) {
    if(root == NULL){      
        return 0;
    }
    int left = solution(root->left);
    int right = solution(root->right);
    return left + right + 1;
}

这两个问题的根本区别就是前序和后序。

  • 第一个问题,一个节点在第几层,从根节点遍历过来的时候就可以记录,需要用一个参数传递下去;
  • 第二个问题,需要遍历完子树之后才能弄清楚,需要后序遍历,因为只有后序遍历才能返回子树的信息。
    实际上第一个问题,也可以通过后序来实现,只是我们习惯于写成前序的方式。

换句话说只要问题和子树有关,大概率是要给函数设置合理的定义和返回值,在后序的位置写代码。

leetcode 543 diameter of binary tree二叉树的直径

重要:每一条二叉树的直径,就是某个节点的左右子树深度之和。
需要一个全局变量来记录。

class Solution(object):
    def diameterOfBinaryTree(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        self.res = 0
        self.traverse(root)
        return self.res
    def traverse(self, root):
        if root == None:
            return 0
        leftMax = self.traverse(root.left)
        rightMax = self.traverse(root.right)
        self.res = max(self.res, leftMax + rightMax)
        return max(leftMax, rightMax) + 1

这里traverse函数的作用,是返回root的最大深度。时间复杂度O(n), n是二叉树的所有节点数。

这道题必须增加辅助函数,无法用分解的方式来做。因为直径无法分解为子树的问题。就算求出了左右子树的直径,也无法求出当前数的直径。需要用外部变量来记录res,用遍历的方式来做。

一些有趣的:https://leetcode.com/problems/diameter-of-binary-tree/solutions/575172/worst-solution-ever-worse-than-100-of-submissions-in-both-time-and-memory/
leetcode上这个老哥写了个巨复杂的解法,居然是点赞最高的答案。评论区:Once you start writing this in an interview you are no longer in danger, you are the danger.
Thanos had to snap twice to wipe out this code

4. 二叉树的层序遍历

https://leetcode.cn/problems/diameter-of-binary-tree/

class Solution(object):
    def levelOrder(self, root):
        """
        :type root: TreeNode
        :rtype: List[List[int]]
        """
        if not root:
            return
        res = []
        q = deque()
        q.append(root)
        while q:
            sz = len(q)
            level = []
            for i in range(sz):
                cur = q.popleft()                
                level.append(cur.val)
                if cur.left:
                    q.append(cur.left)
                if cur.right:
                    q.append(cur.right)
            res.append(level)
        return res        
  • 28
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值