LC-257. 二叉树的所有路径

这篇博客介绍了如何使用递归和迭代法解决二叉树的所有路径问题。递归法中,关键在于只在到达叶子节点时返回路径,而迭代法则利用两个栈保存节点和路径,确保正确组合所有可能的路径。两种方法都基于前序遍历,确保了路径的正确构造。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

LC-257. 二叉树的所有路径

递归法

这道题是一个简单题,虽然经过一番思考做了上来,但是确实是思维不清晰,所以还是要总结一下。

这道题是记录根节点到叶子节点的路径,最重要的是想明白返回条件

  • 这道题是记录到叶子节点,自然是只有到叶子节点的时候,才会返回,也就是说左右子树都不存在的时候 not root.left and not root.right。不要对本层节点进行判断,那样会导致对叶子节点的左右子树判断,最后会同一个路径会添加两次。

  • 这道题我采用的时先序遍历,体现在最开始遍历的时候就先把root.val加入到答案中啦~然后才进行的递归。

  • 因为上面是判断了一个节点的左右孩子的存在性,没有判断这个节点是否存在,会出现空节点报错,所以在递归的时候,就先应该判断节点是否存在。**重点:**通过传参的时候用+拼接的方式传参,避免了后面还需要回退的问题,这是个小技巧。也就是说避免了先递归再回溯的问题。

    if root.left: func(root.left, tmp + '->' + str(root.left.val))
    if root.right: func(root.right, tmp + '->' + str(root.right.val))
    
  • 有人会好奇,如果没有左右孩子都不存在,那么本层的怎么返回呢?其实不会的,因为别忘记了我们最开始就判断了这个节点是不是叶子节点,如果是叶子节点直接添加,如果不是,则必然一个孩子。

代码如下:

class Solution:
    def binaryTreePaths(self, root: Optional[TreeNode]) -> List[str]:
        res = []
        
        def func(root, tmp):
            if not root.left and not root.right: 
                res.append(tmp)
                return
            if root.left: func(root.left, tmp + '->' + str(root.left.val))
            if root.right: func(root.right, tmp + '->' + str(root.right.val))

        func(root, str(root.val))
        return res

迭代法

迭代法依然使用前序遍历的方法。这里的主要思想是需要两个栈,同时讲节点和该节点对应的路径保存。什么叫该节点对一个的路径保存呢,意思就是说到这个节点为止,已经路径到哪了,这样如果它还有孩子直接把 这个路径取出,添加上孩子就好了。

image-20220404153029677

以示例为了,假如stack[1] = 2,那么path_st[2] = “1->2”,这样如果到节点5了,只需要取出path_st[2] = "1->2"加上5,并且5是叶子节点,直接添加到最后的result里面就可以了。

代码如下:

from collections import deque

class Solution:
    """二叉树的所有路径 迭代法"""

    def binaryTreePaths(self, root: TreeNode) -> List[str]:
        # 题目中节点数至少为1
        stack, path_st, result = deque([root]), deque(), []
        path_st.append(str(root.val))

        while stack:
            cur = stack.pop()
            path = path_st.pop()
            # 如果当前节点为叶子节点,添加路径到结果中
            if not (cur.left or cur.right):
                result.append(path)
            if cur.right:
                stack.append(cur.right)
                path_st.append(path + '->' + str(cur.right.val))
            if cur.left:
                stack.append(cur.left)
                path_st.append(path + '->' + str(cur.left.val))

        return result
<think>嗯,用户让我介绍一下ThreadLocal,包括它存的是什么,数据结构是怎样的,还要比较rehash和链表/红黑树的优缺点。另外还有LeetCode 236题,二叉树的最近公共祖先。我需要先理清楚每个部分的知识点,然后组织语言回答。 首先,ThreadLocal。ThreadLocal是Java中的线程局部变量,每个线程都有独立的副本,用来解决多线程的并发问题。它存的是线程本地的变量,每个线程通过ThreadLocalMap来存储数据,这个Map的key是ThreadLocal实例本身,value是设置的值。数据结构方面,ThreadLocalMap使用了一个Entry数组,Entry继承自WeakReference,key是弱引用,value是强引用。这样设计可能导致内存泄漏,需要调用remove方法清理。 关于rehash,当哈希表负载因子达到阈值时,会进行扩容,重新计算所有元素的位置。ThreadLocalMap的rehash是线性的探测,遇到冲突就找下一个空槽,而像HashMap在Java8之后用链表转红黑树。比较的话,线性探测的优点是内存连续,缓存友好,但扩容可能更频繁,链表/红黑树处理冲突更高效,尤其数据量大时红黑树能保持O(logn)查询,但内存不连续,节点需要额外空间。可能用户想了解不同冲突解决方式的优劣,以及为什么ThreadLocal采用线性探测而不是其他结构。 然后是LeetCode 236题,二叉树的最近公共祖先。需要找到两个节点的最低公共祖先。通常解法是后序遍历,递归查找左右子树是否包含p或q。如果左右都有,当前节点就是LCA。如果一边有,另一边没有,则返回有的那边。需要处理当前节点等于p或q的情况,直接返回当前节点。可能还要考虑边界情况,比如其中一个节点是另一个的祖先。 现在需要把这些内容有条理地整理出来,分点回答,确保准确且易懂。可能用户是在准备面试,所以需要详细但不过于冗长的解释。注意ThreadLocal的内存泄漏问题和解决方案,以及在数据结构选择上的权衡。对于算法题,可能需要给出思路、关键点以及可能的代码实现。 检查是否有遗漏点,比如ThreadLocalMap的哈希函数,或者线性探测的具体步骤。在比较rehash方法时,要强调不同场景下的适用性。对于算法题,确认是否最优解,是否有其他方法比如存储父节点然后比较路径等。确保回答覆盖所有问题点,并且逻辑清晰。</think>### 一、ThreadLocal详解 --- #### 1. **ThreadLocal存的是什么?** ThreadLocal 存储的是**线程本地变量**,每个线程通过 ThreadLocal 可以持有独立的变量副本,避免多线程竞争。数据实际存储在 `ThreadLocalMap` 中(每个线程的 `Thread` 对象内部有一个 `threadLocals` 字段)。 #### 2. **数据结构:ThreadLocalMap** - **实现方式**:`ThreadLocalMap` 是 ThreadLocal 的静态内部类,使用一个 `Entry` 数组存储键值对。 - **Entry 设计**: ```java static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); // Key 是弱引用(避免内存泄漏) value = v; // Value 是强引用 } } ``` - **哈希冲突解决**:使用**开放地址法(线性探测)**,而非链表或红布树。冲突时依次向后查找空槽。 #### 3. **Rehash 机制与链表/红布树的对比** | **特性** | **ThreadLocalMap(线性探测)** | **HashMap(链表/红布树)** | |-------------------|--------------------------------------|------------------------------------| | **内存占用** | 连续内存,缓存友好 | 链表/红布树需要额外指针,内存碎片化 | | **冲突解决效率** | 简单但可能频繁扩容(负载因子 2/3) | 链表 O(1)/红布树 O(logn) | | **适用场景** | 预期数据量小(线程隔离变量通常少) | 通用场景,支持大数据量 | **为什么 ThreadLocal 使用线性探测?** - **轻量级**:ThreadLocal 通常存储少量线程本地变量,线性探测简单高效。 - **避免内存泄漏**:弱引用 Key 需及时清理,开放地址法更容易触发清理逻辑(`expungeStaleEntry`)。 --- ### 二、LeetCode 236:二叉树的最近公共祖先(LCA) --- #### 1. **问题描述** 给定一个二叉树和两个节点 `p`、`q`,找到它们的最近公共祖先(最低层级的共同祖先)。 #### 2. **递归解法** ```python def lowestCommonAncestor(root, p, q): if not root or root == p or root == q: return root left = lowestCommonAncestor(root.left, p, q) right = lowestCommonAncestor(root.right, p, q) if left and right: # p 和 q 分布在两侧 return root return left if left else right # 返回非空的一侧 ``` #### 3. **关键点** - **后序遍历**:自底向上查找,先处理子树再判断当前节点。 - **终止条件**:当前节点是 `p` 或 `q`,或为空。 - **合并结果**:若左右子树均找到目标,当前节点是 LCA;否则返回非空子树的结果。 #### 4. **复杂度** - 时间复杂度:O(n),每个节点遍历一次。 - 空间复杂度:O(h),递归栈深度(h 为树高)。 --- ### 总结 - **ThreadLocal**:通过线性探测的轻量级设计实现线程隔离变量存储。 - **LCA 问题**:递归后序遍历是典型解法,需理解“合并子树结果”的逻辑。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我哈士奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值