想象你走进一座大型游乐场:热门项目门前,游客排成一条“单人链”,每个人都只牵着自己前面那个人的衣角。想知道队伍总长多少?只能从队首开始,一个接一个地数过去——这就是链表。游乐场的员工组织架构图则是一棵树:总经理坐在“树根”的位置,往下分出运营部、设备部、演艺部,每个部门经理下面又带着一群员工。AI 面试中反复出现的反转链表、合并有序链表、二叉树遍历、最近公共祖先,本质上不过是“给队伍掉头”“把两条有序队伍合成一条”“按固定路线把所有景点逛一遍”“帮两名员工找到共同的上司”。

岗位关联

对 AI 从业者而言,链表与树是算法面试的“入场券”,也是理解 Transformer 位置编码、决策树分裂等模型结构的底层直觉。掌握本文的六步拆解,你就能在白板上稳定写出无 bug 的链表操作与二叉树递归。

目录
  1. 可视化演示
  2. 链表结构与类型:认识排队线的四种姿态
  3. 反转链表家族:给队伍掉头
  4. 合并与双指针:两条队伍合成一条
  5. 二叉树遍历:按规则逛遍组织架构
  6. 递归进阶与最近公共祖先:帮两人找共同上司
  7. 边界与最优解:掉头前先看看队伍空不空
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示链表反转与快慢指针操作、二叉树前中后序遍历与BFS层序、二叉搜索树有序性质与平衡操作。切换模式掌握数据结构面试核心题型。

本节配有交互动效(进入视口后自动加载,约 3KB)。

链表反转与快慢指针 迭代反转:三指针prev/curr/next逐步翻转,时间O(n)空间O(1) 快慢指针:fast走两步slow走一步,找中点或检测环,是链表高频技巧 reverse: prev→curr→next, while(curr) next=curr.next, curr.next=prev 指针操作是链表面试题的基石

链表结构与类型:认识排队线的四种姿态

游乐场的排队线有几种玩法:最常见的“单人链”里,每个游客只认识自己前面的那一位,想找队尾只能一路问过去,这叫单链表。有的 VIP 通道让游客前后都认识,方便随时换位置,这叫双向链表。还有的花车巡游队伍首尾相接、绕成一个圆圈,这叫循环链表。而在每支队伍最前方,都站着一位穿制服的引导员——他不参与游玩,只负责让“第一个人”更好管理,这就是哨兵节点。

把导游的比喻翻译成代码,链表的最小单元叫“节点”(Node),一个节点包含两块信息:货物区 val 存放数据,指针区 next 存放“下一个人在哪里”。单链表只能从 head 开始单向走,尾节点把 next 指向空 null。双向链表则多一个 prev 指向前一个节点,代价是每个节点多占一份内存。而哨兵节点 dummy 就像引导员站在队首,让“头节点被删除”这类麻烦事不再需要单独开分支处理。

为什么面试爱考链表?因为它的“随机访问”能力很弱。数组按下标能瞬间跳到第 k 个位置,链表却必须从队首开始一步一个脚印。这正是下面的公式要说明的事。

所以面试官看到你写链表第 k 个访问时,期待的从来不是“数组式跳跃”,而是老老实实从头遍历。就像排队引导员从不抄近道,他只会逐个数过去——这种“只能认下一个”的朴素约束,正是链表所有经典题目的出发点。

T(k)=k×O(1)=O(k)\displaystyle T(k) = k \times O(1) = O(k)
这句话的意思是:在链表中找到第 k 个节点,时间随 k 线性增长,没有捷径可走。
  1. 符号 T(k)T(k) 表示“从链表头走到第 k 个节点”所需的时间成本。
    T(k)T(k)
  2. 一步 O(1)每次只能通过当前节点的 next 指针跳到下一个位置,跳一步是常数时间。
    k×O(1)k \times O(1)
  3. 化简 O(k)k 个常数时间累加,结果与 k 成线性关系,记为 O(k)。
    T(k)=O(k)T(k) = O(k)

反转链表家族:给队伍掉头

游乐场广播响起:“所有游客向后转!”原本排在第一位的 1 号想去队尾,只能让整条队伍掉头。掉头的方法很朴素:从队首开始,让每个人松开原来牵着的手,改牵“原来排在自己前面那个人”的手,一个也不能漏。这就是反转链表。

换成程序员的话说,反转链表要做三件事:先记住当前节点的后继(否则一改指向就迷路),再把当前节点的 next 改指向前驱,最后三个指针整体前移。三个指针分别叫 prev(前驱)、cur(当前)、nxt(后继),这是链表面试里最经典的三指针套路。除了整条反转,还有“反转区间”的变体:先走到区间起点,再用同样的三指针逻辑反转区间内部。

迭代版空间无敌;递归版则把“反转 head 之后的整条链”看成子问题,代码短但每层递归都会压栈。递归的直觉是:先让后面的队伍自己掉头,回头再来处理当前节点。链表一长,递归栈就容易爆。

掉头后的队伍里,原来的队首 1 号站到了队尾,头指针指向了 5 号。面试中的“判断回文链表”也是同一招:快慢指针找到中点,反转后半段,再从头逐位比较——相当于让半支队伍掉头,看它是否和另半支一模一样。

T(n)=O(n),S迭代(n)=O(1),S递归(n)=O(n)\displaystyle T(n) = O(n), \quad S_{\text{迭代}}(n) = O(1), \quad S_{\text{递归}}(n) = O(n)
时间上两者都只走一遍,但迭代只占常数额外空间,递归的额外空间会随链表长度线性增长。
  1. 遍历 n 次无论迭代还是递归,每个节点都只被处理一次。
    T(n)=O(n)T(n) = O(n)
  2. 迭代空间迭代只维护三个指针,不随 n 增长。
    S迭代(n)=O(1)S_{\text{迭代}}(n) = O(1)
  3. 递归空间递归深度等于链表长度,每层都要保存现场。
    S递归(n)=O(n)S_{\text{递归}}(n) = O(n)

合并与双指针:两条队伍合成一条

游乐场两个项目同时散场,两条队伍都按身高从矮到高排好了。工作人员要快速合成一条仍然按身高有序的新队伍:每次只看两条队最前面的游客,谁矮谁先过来。谁先来?永远是对比两队队首。

技术实现上,这题的黄金搭档是“哨兵节点 + 尾指针”。用 dummy 节点做新队伍的假队首,tail 指针始终指向新队伍最后一个人。每轮比较两条链的当前节点,把较小的接在 tail 后面,tail 再前移一步。最后哪条链还有剩余,整体接上即可——就像两条队伍合流时,某条队先走完了,把剩下那条整段搬过来。

与双指针同族的还有快慢指针:慢的每次走 1 步,快的每次走 2 步。在环形跑道上,跑得快的迟早会追上跑得慢的——用这个速度差,可以判断链表是否有环、定位环的入口,还能巧妙地找到链表中点。

就像两条队伍合流时,工作人员只需要记住“下一个该谁”,不需要复印整支队伍的名册。面试官要看到的就是这种“原地、线性”的答案。

T(m,n)=O(m+n),S(m,n)=O(1)\displaystyle T(m, n) = O(m + n), \quad S(m, n) = O(1)
合并两条分别长 m、n 的有序链表,每个节点最多被比较一次,额外空间只有常数个指针。
  1. 总节点数两条链合计 m+n 个节点,每个节点至多被接走一次。
    m+nm + n
  2. 比较次数每次比较消耗一个节点,因此总比较次数也是 m+n 量级。
    T(m,n)=O(m+n)T(m, n) = O(m + n)
  3. 额外空间只新建一个 dummy 节点和少数几个指针,与规模无关。
    S(m,n)=O(1)S(m, n) = O(1)

二叉树遍历:按规则逛遍组织架构

游乐场的员工架构是一棵树:总经理在顶端,下面分部门经理,经理下面带员工。如果总部要发全园通知,有三种下发顺序:先让领导知道再逐级往下(前序);先问完左部门、再问领导、再去右部门(中序);先让基层员工都知道了再向经理汇报(后序)。这三种统称为深度优先遍历。

在树上递归时,每个节点其实会被“经过”三次:进入时一次、左子树回来后一次、右子树回来后一次。前序 = 第一次经过时就记录;中序 = 第二次经过时记录;后序 = 第三次经过时记录。这就是递归序,理解了它,三种遍历只是改一行打印位置的事。层序遍历则像坐电梯,从上到下、从左到右一层层扫过去,需要借助队列实现。

树的遍历几乎是所有树形题——最大深度、子树求和、序列化与反序列化——的地基。很多看起来很难的题,最后都归结为“你选择在递归序的哪一次经过时处理节点”。

就像乐园通知发遍所有员工,无论组织架构多复杂,每个人总会收到且只收到一次。递归序是树递归的灵魂:你不需要死背模板,只需要问自己“这次经过节点时,我该做什么”。

T(n)=T(nL)+T(nR)+O(1),T(n)=O(n)\displaystyle T(n) = T(n_L) + T(n_R) + O(1), \quad T(n) = O(n)
遍历一棵有 n 个节点的二叉树,每个节点恰好被访问常数次,总体仍是线性时间。
  1. 递归式以 root 为根的子树耗时,等于左子树耗时加右子树耗时,再加上访问 root 自身的常数时间。
    T(n)=T(nL)+T(nR)+O(1)T(n) = T(n_L) + T(n_R) + O(1)
  2. 归并求和把递归式一路展开,每个节点的 O(1) 累加,总代价就是 n 个常数的和。
    T(n)=O(n)T(n) = O(n)
  3. 线性下界无论树多歪、多平衡,遍历都必须访问所有节点,线性时间是最优。

递归进阶与最近公共祖先:帮两人找共同上司

设备部的 7 号员工和演艺部的 4 号员工想找个共同上司审批方案。他们在组织架构图里分别往上走,两条汇报线第一个交汇的节点,就是最近公共祖先(LCA)。注意不是总经理——总经理虽然是共同的上级,但“最近”才是答案。

递归解法的思路建立在子树划分上:从根开始,如果当前节点就是 p 或 q,直接返回;否则分别去左子树、右子树里找。若左子树找到了一个、右子树也找到了一个,说明当前节点就是两条汇报线的分叉点,也就是 LCA;若只有一边找到,就把那一边的答案继续向上传。递归三要素在这里体现得很完整:大问题拆成左右两个子问题、终止条件是遇到空节点或目标节点、返回值用“非空即答案”来统一。

用 h(u) 表示“以 u 为根的子树是否包含 p 或 q”这个布尔值,LCA 的判定就可以写成下面的逻辑式。

面试官一眼就能看出你是否有真懂 LCA:就看你是否明白“两边都命中即分叉点”的判断。这正如在组织架构里,只有当两个人的汇报线在某个节点分叉,那个节点才是他们各自的最近共同上司。

h(u)=h(left(u))∨h(right(u))∨(u∈{p,q})\displaystyle h(u) = h(\text{left}(u)) \vee h(\text{right}(u)) \vee (u \in \{p, q\})
三个条件里,左子树命中、右子树命中、或节点本身是目标,任一成立即说明子树含有目标。当左右同时命中时,u 就是最近公共祖先。
  1. h(u) 的含义h(u) 是一个布尔值,“u 的子树里存在 p 或 q”为真。
    h(u)h(u)
  2. 左子树贡献h(left(u)) 为真,说明目标从左侧出现。
    h(left(u))h(\text{left}(u))
  3. 右子树贡献同理,右边也可能藏着目标。
    h(right(u))h(\text{right}(u))
  4. 自身贡献u 自己恰好是 p 或 q,同样算命中。
    u∈{p,q}u \in \{p, q\}
  5. 三路或三个条件用“或”连起来;当左右同时为真时,u 就是答案。
    h(u)=h(left(u))∨h(right(u))∨(u∈{p,q})h(u) = h(\text{left}(u)) \vee h(\text{right}(u)) \vee (u \in \{p, q\})

边界与最优解:掉头前先看看队伍空不空

掉头操作开始前,有经验的引导员会先检查三件事:队伍是否空无一人?只有一个人要不要掉?两个人掉头后谁站前面?这些在代码里对应的就是空指针、单节点、双节点这些边界条件。九成链表 bug 都藏在边界里——while cur 忘了判空、nxt 没有先保存、两个节点时头尾指向混乱。

面试最优解还有一条铁律:复杂度先说清,迭代优先于递归。递归虽美,但在链表这种“深度可能等于长度”的结构上,递归栈占用 O(n),而迭代只需 O(1)。轮到树时则相反:树高决定递归深度,只有树退化成链的时候才需要警惕。

于是面试官眼里最有经验的候选人,落笔前先把空链表、单节点、双节点在脑子里过一遍,落笔时先报复杂度再写代码,写完再用手工例子验证一遍。就像引导员永远先确认“队里还有人、人还认得邻居”,才喊出“向后转”。

h={O(log⁡n),平衡二叉树O(n),退化为链表\displaystyle h = \begin{cases} O(\log n), & \text{平衡二叉树} \\ O(n), & \text{退化为链表} \end{cases}
递归的空间开销由树高 h 决定。树越平衡越省空间,越像链表越危险。
  1. 树高 h从树根到最远叶子的边数,决定了递归栈的最大深度。
    hh
  2. 平衡情形每一层节点数翻倍,n 个节点的高度只有对数级别。
    h=O(log⁡n)h = O(\log n)
  3. 退化情形每个节点只有一个孩子,树就变成了一条“竖起来的排队线”。
    h=O(n)h = O(n)
  4. 结论在退化树上用递归,等于让排队队伍去上下跳,必然爆栈。

AI 应用场景

场景 1:AI 模拟面试官帮你刷链表题

阶段:面试准备与能力评估

解决的问题:小白刷题常陷入“看答案会、合上答案不会”的假性掌握。把 ChatGPT/Claude 当作模拟面试官,让它像真人面试官一样先让你在白板上讲思路、再写代码,最后按“边界处理、复杂度分析、沟通清晰度”三个维度打分,能精准暴露假性掌握。

具体案例:你告诉 AI“我要面 AI 算法岗,请用中文模拟一道反转链表题”。AI 会先问思路,你回答“用三指针迭代”,它继续追问“如果链表有空指针怎么办”“递归版本空间复杂度是多少”。这种追问式反馈和真实面试节奏几乎一致。

场景 2:AI 代码审查自动检查链表边界

阶段:代码生成后的静态审查

解决的问题:Copilot 等 AI 编程助手生成链表操作代码时,经常“看起来对但边界全错”——空指针解引用、漏掉 nxt 保存导致断链。把链表反转与合并的模板和边界规则写入审查提示词,AI 能在提交前自动标注风险行。

具体案例:数据团队用 AI 助手生成链表合并代码,审查提示词包含“检查 head 为空、单节点、双节点三种输入”。AI 自动指出 while 循环条件缺少对两条链同时为空的保护,并给出加哨兵节点的修正建议。

场景 3:用经典链表题评估大模型编程能力

阶段:模型评测与基准构建

解决的问题:大模型是否真会写代码?评测基准常以 LeetCode 经典题为样本,因为链表反转、二叉树遍历这些题目答案固定、判定客观。用本讲的复杂度与边界标准作为“参考答案生成器”,可以自动化批量评估候选模型的代码能力。

具体案例:某团队构建评测集时,用“给定单链表头节点,请用 O(1) 额外空间反转”作为 prompt 让模型写代码,再用包含空链表、单节点、双节点的测试用例批量判分,最终筛选出编程能力稳定的基座模型。

Python 演示

python100 行
class ListNode:    def __init__(self, val=0, nxt=None):        self.val = val        self.next = nxt  def build_list(values):    dummy = ListNode()    tail = dummy    for v in values:        tail.next = ListNode(v)        tail = tail.next    return dummy.next  def to_list(head):    out = []    while head:        out.append(head.val)        head = head.next    return out  def reverse_list(head):    prev = None    cur = head    while cur:        nxt = cur.next        cur.next = prev        prev = cur        cur = nxt    return prev  def merge_two(a, b):    dummy = ListNode()    tail = dummy    while a and b:        if a.val <= b.val:            tail.next = a            a = a.next        else:            tail.next = b            b = b.next        tail = tail.next    tail.next = a if a else b    return dummy.next  class TreeNode:    def __init__(self, val=0, left=None, right=None):        self.val = val        self.left = left        self.right = right  def build_sample_tree():    root = TreeNode(3)    root.left = TreeNode(5)    root.right = TreeNode(1)    root.left.left = TreeNode(6)    root.left.right = TreeNode(2)    root.right.left = TreeNode(0)    root.right.right = TreeNode(8)    root.left.right.left = TreeNode(7)    root.left.right.right = TreeNode(4)    return root  def preorder(root):    if root is None:        return []    return [root.val] + preorder(root.left) + preorder(root.right)  def lowest_common_ancestor(root, p, q):    if root is None or root.val in (p, q):        return root    left = lowest_common_ancestor(root.left, p, q)    right = lowest_common_ancestor(root.right, p, q)    if left and right:        return root    return left if left else right  def main():    a = build_list([1, 3, 5])    b = build_list([2, 4, 6])    merged = merge_two(a, b)    print("merge:", to_list(merged))    rev = reverse_list(merged)    print("reverse:", to_list(rev))    root = build_sample_tree()    print("preorder:", preorder(root))    node = lowest_common_ancestor(root, 5, 4)    print("lca(5,4):", node.val if node else None)  if __name__ == "__main__":    main()
目的:把本文的核心算法串成一份可运行脚本——合并两条有序链表、反转合并结果、构建二叉树并做前序遍历、求两个节点的最近公共祖先。
  1. dummy = ListNode()哨兵节点:给新队伍装一个“假队首”,避免特判头节点,任何情况下 tail 都有东西可接。
  2. nxt = cur.next掉头第一步:先记住后面的游客,否则改完指向就找不到他了。
  3. cur.next = prev掉头核心一步:让当前游客改牵“原来排在自己前面的人”的手。
  4. tail.next = a if a else b某条队先走完时,把另一条剩下的全部接上,像合并队伍时“整段搬移”。
  5. if left and right: return rootLCA 的分叉判定:左右子树各命中一个目标,当前节点就是共同上司。

常见误区

误区“递归反转只要几行,看起来更优雅,面试写递归一定加分,反正链表也长不到哪去。”

正解递归反转的空间是 O(n),链表长到十万节点时直接爆栈;迭代三指针时间同样是 O(n),但空间只有 O(1)。面试官喜欢“先讲清两种方案、再按约束选择”的候选人,而不是无脑炫技。

为何常见:递归代码短、教程爱写、人也容易背下来,于是产生了“短 = 好”的错觉。

误区“两条链都不能动,必须 new 一堆新节点复制出来,否则会破坏原链表数据。”

正解用哨兵节点加尾指针可以原地合并,不复制任何节点,只是不断调整 next 指向。算法题中“破坏原链表”通常无所谓,面试更在意合并是否做到 O(1) 额外空间。

为何常见:很多初学者第一次写链表就是“复制后再操作”,形成了安全却浪费的习惯。

误区“前序中序后序就是三个模板,背下来,遇到遍历题直接套就完事了。”

正解遍历的真正考点是递归序——每个节点被经过三次,打印时机不同而已。只背模板的人,遇到“改造遍历求最大深度”“用遍历序列还原二叉树”这类变体会立刻束手无策。

为何常见:模板背诵成本低、见效快,让人误以为记住了模板就等于理解了本质。

误区“快慢指针追上就完事了,能判断出有环,这道题就算做完了。”

正解判断有环只是第一步;更进一步,从相遇点和头节点各放一个慢指针同步走,两者必然在环入口相遇,从而定位环的入口。面试官问“链表有没有环”时,几乎总会紧跟一句“环入口在哪”。

为何常见:多数教程只讲“判断有环”,很少推导入环点,导致知识链断在最容易被追问的地方。

误区“最近公共祖先听起来很难,面试肯定要上倍增、树链剖分,不然显得不够厉害。”

正解普通二叉树(非 BST)的 LCA,O(n) 递归解就是标准答案,“左右都命中即当前节点”三行核心逻辑足以说清边界;倍增只是为了应对“多次查询”的优化。面试官不要求时,别主动加戏。

为何常见:网上题解总爱卷“高级算法”,让小白误以为基础解法上不了台面。

面试题

Q1. 在一个长度为 n 的单链表中,定位到尾节点的时间复杂度是()

单选题 · easy

  • A.常数时间 O(1)
  • B.对数时间 O(logn)
  • C.线性时间 O(n)← 正确答案
  • D.平方时间 O(n²)

答案:C。链表每个节点只知道下一位,像排队数人数,只能从队首一路走到队尾,n 个节点要走 n 步,所以是 O(n)。数组按下标跳转才是 O(1),那属于“点名册式查询”,链表没有这份特权。

Q2. 二叉树的前序遍历的正确顺序是()

单选题 · easy

  • A.左根右的中序
  • B.根左右的先序← 正确答案
  • C.左右根的后序
  • D.逐层扫描的层序

答案:B。前序遍历就是“先经过根、再进左子树、最后进右子树”,对应乐园通知里“先领导再下属”。A 是中序,C 是后序,D 是层序,都不符合“前序”。

Q3. 迭代反转单链表时,需要同时维护的指针个数是()

单选题 · easy

  • A.0 个,原地交换即可
  • B.1 个,只需 head
  • C.2 个,当前与后继
  • D.3 个,前驱当前后继← 正确答案

答案:D。掉头需要 prev 记住“前一个游客”、cur 指向当前、nxt 预先保存“后一个游客”,三个缺一不可。少了 nxt,修改 cur 的指向后就再也找不回队伍了。

Q4. 用快慢指针检测链表是否存在环,下列说法正确的是()

单选题 · mid

  • A.快指针永远追不上慢指针
  • B.两者一定在入环点相遇
  • C.存在环时两者必在环内相遇← 正确答案
  • D.必须借助哈希表才能判断

答案:C。慢指针每次走 1 步、快指针每次走 2 步,进入环后每轮距离缩小 1,必然相遇。相遇点在环内任意位置,不一定是入环点,所以 B 错、C 对。哈希表能判断但额外空间 O(n),不是最优。

Q5. 合并两条长度分别为 m、n 的有序单链表的时间与额外空间为()

单选题 · mid

  • A.O(m×n) 与 O(m+n)
  • B.O(m+n) 与 O(1)← 正确答案
  • C.O(max(m,n)) 与 O(m+n)
  • D.O(m²+n²) 与 O(1)

答案:B。每次比较取走一个节点,m+n 个节点各处理一次,时间 O(m+n);用哨兵节点原地接链,额外空间只有常数个指针,即 O(1)。B 正确。

Q6. 下列属于二叉树深度优先遍历的有()

多选题 · mid

  • A.前序遍历← 正确答案
  • B.中序遍历← 正确答案
  • C.后序遍历← 正确答案
  • D.层序遍历

答案:ABC。深度优先就是“一条路走到黑再回头”,前序、中序、后序都属于这一类,它们只是递归序中记录时机不同;层序遍历按层横扫,属于广度优先。

Q7. 某二叉树前序遍历为 [3,9,20,15,7],中序遍历为 [9,3,15,20,7],则根节点是()

单选题 · hard

  • A.节点 3← 正确答案
  • B.节点 9
  • C.节点 20
  • D.节点 7

答案:A。前序第一个元素一定是根,所以根是 3。验证:中序里 3 左边只有 9,说明左子树只有一个节点;右侧 [15,20,7] 继续由 20 作根划分。这是“由遍历序列还原树”的第一步,也是面试高频变体。

Q8. 用 O(1) 额外空间判断链表 1→2→2→1 是否为回文,最优做法是()

单选题 · extreme

  • A.递归反转整条链表再比较
  • B.快慢指针找中点并反转后半段← 正确答案
  • C.用栈存下所有节点再比较
  • D.复制到数组后用双指针

答案:B。快慢指针先找到中点,再反转后半段,最后从两头同步比较,全程只用常数指针,空间 O(1)。栈和数组都是 O(n) 空间,不满足要求;整条反转会丢掉与原队首比较的基准。

Q9. 关于哨兵节点 dummy 的说法,正确的有()

多选题 · mid

  • A.节点值任意,不参与业务数据← 正确答案
  • B.能简化头节点被删除的逻辑← 正确答案
  • C.只有放在链表尾部才有意义
  • D.合并有序链表时免去空头特判← 正确答案

答案:ABD。dummy 是“假队首”,值随便填,真正有用的是它的 next 指向真实链首;用它做头节点后,删除、插入头节点都不再需要为“头节点为空”单独开分支。C 说反了,dummy 的价值在头部而非尾部。

Q10. 在最近公共祖先的递归解法中,返回值可能表示()

多选题 · hard

  • A.命中的目标节点 p 或 q← 正确答案
  • B.已找到的最近公共祖先← 正确答案
  • C.null 表示该子树不含目标← 正确答案
  • D.总是返回树的最大深度

答案:ABC。该递归把三种状态编码进一个节点指针:命中 p 或 q 就返回那个节点;左右子树都命中时返回当前根,即 LCA;两边都没找到就返回 null。D 与 LCA 完全无关,返回深度属于“最大深度”题的职责。