题目描述

✅ 剑指 Offer 36. 二叉搜索树与双向链表

image-20261001230752561

题意分析

将二叉搜索树转换为按节点值从小到大排列的循环双向链表,返回最小值节点。直接复用树中的节点:left 改作前驱指针,right 改作后继指针,不能另建一批节点代替。

双向要求相邻节点能互相到达,循环还要求最小节点的前驱指向最大节点、最大节点的后继指回最小节点。空树返回空;只有一个节点时,它的左右指针都应指向自己。

解法:迭代中序原地串联

核心思路

[!blue]

二叉搜索树的中序遍历顺序就是从小到大的顺序,因此可以边遍历边把相邻节点串起来,不需要收集全部节点后再排序。用显式栈保存尚未处理的祖先,先不断向左入栈,弹出节点时再处理它,随后转入右子树。

用 pre 指向刚刚访问过的中序节点,用 head 保存第一个被访问的节点。第一次弹出时还没有前驱,这个节点就是最小节点,记录为 head;以后每次弹出 cur,都令 pre.right = cur、cur.left = pre,在两个方向连接相邻节点,然后令 pre = cur。

原地重连不会破坏剩余遍历:访问 cur 时,它的左子树已经处理完,所以可以把 cur.left 改为前驱;当前节点原来的 right 尚未覆盖,仍能用它进入未遍历的右子树。若前驱原来有右子树,算法此时已经沿那棵子树下行,待处理位置由当前节点与栈共同保存,因此改写前驱的右指针也不会丢失入口。

这些连接逐步形成从 head 到 pre 的有序双向前缀。中序结束后,pre 就是最大节点,再执行 head.left = pre 与 pre.right = head,补上首尾两条循环连接。闭环必须留到最后,否则沿原树指针继续遍历时可能重新进入已经处理过的节点。

每个节点只在中序弹出时接入一次,节点顺序与 BST 的有序顺序一致。单节点时 head 与 pre 相同,最后两次赋值自然形成自环,也不需要额外分支。

解题步骤

  1. 空树直接返回空,非空时初始化栈、当前指针 cur,以及空的 head、pre。
  2. 从 cur 不断向左入栈,直到没有左孩子,再弹出栈顶作为当前访问节点。
  3. 若 pre 为空,记录 head = cur;否则将 pre 与 cur 双向相连。
  4. 更新 pre = cur,再进入当前节点原来的右子树,重复中序过程。
  5. 当前指针与栈都为空后,将 head、pre 首尾相连并返回 head。

代码实现

class Solution {
    public Node treeToDoublyList(Node root) {
        if (root == null) {
            return null;
        }

        Deque<Node> stack = new ArrayDeque<>();
        Node cur = root;
        Node head = null;
        Node pre = null;

        while (cur != null || !stack.isEmpty()) {
            while (cur != null) {
                stack.push(cur);
                cur = cur.left;
            }

            cur = stack.pop();

            if (pre == null) {
                head = cur;
            } else {
                // 连接中序前驱与当前节点,当前节点的原右子树仍保留。
                pre.right = cur;
                cur.left = pre;
            }

            pre = cur;
            cur = cur.right;
        }

        // 全部节点处理完后才闭环,避免破坏尚未完成的遍历。
        head.left = pre;
        pre.right = head;

        return head;
    }
}
func treeToDoublyList(root *Node) *Node {
    if root == nil {
        return nil
    }

    stack := make([]*Node, 0)
    cur := root
    var head, pre *Node

    for cur != nil || len(stack) > 0 {
        for cur != nil {
            stack = append(stack, cur)
            cur = cur.Left
        }

        cur = stack[len(stack)-1]
        stack = stack[:len(stack)-1]
        if pre == nil {
            head = cur
        } else {
            // 连接中序前驱与当前节点,当前节点的原右子树仍保留。
            pre.Right = cur
            cur.Left = pre
        }
        pre = cur
        cur = cur.Right
    }

    // 全部节点处理完后才闭环,避免破坏尚未完成的遍历。
    head.Left = pre
    pre.Right = head
    return head
}

复杂度分析

  • 时间复杂度:$O(n)$。每个节点恰好入栈、出栈并连接一次。
  • 空间复杂度:$O(h)$,其中 h 是树高。显式栈最多保存一条根到叶的路径;平衡树为 $O(\log n)$,链状树最坏为 $O(n)$。

关键点总结

[!green]

  • “BST + 有序输出”优先利用中序遍历,不需要额外排序。
  • pre 是中序序列中的前驱,不一定是树结构中的父节点。
  • 相邻节点要同时设置 pre.right 和 cur.left,不能只连一个方向。
  • 中序只得到开链;遍历结束后还必须用最小、最大节点闭环。

易错点总结

[!yellow]

  • 返回原根节点:BST 的根未必最小,应返回中序第一个访问到的 head。
  • 提前覆盖 cur.right:当前节点原来的右子树还需要遍历,本步只改前驱的右指针与当前节点的左指针。
  • 只连接一个方向:必须同时设置前驱的后继与当前节点的前驱,才能形成双向链表。
  • 忽略循环要求:普通的有序双向链还不够,遍历后必须补上头尾连接,单节点也需要自环。
  • 遍历中途闭环:会让仍在使用的树指针形成回路,闭环应放在中序处理全部结束之后。

相似题目

题目 难度 关联与区别
897. 递增顺序搜索树 简单 同样按BST中序重连节点,原题只连递增右链,本题还要连接前驱及首尾成环。
114. 二叉树展开为链表 中等 同样原地重连树节点,原题按前序展开普通二叉树,本题按中序生成有序双向链。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/61654325
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!