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

674 阅读3分钟

「这是我参与2022首次更文挑战的第33天,活动详情查看:2022首次更文挑战」。

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

力扣题目链表

输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。

链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。

思路:

首先抛出一个结论:二叉搜索树的中序遍历的结果是增序。根据题目的要求,需要将二叉搜索树转换为排序的循环双向链表,因此这里需要对二叉搜索树进行中序遍历。

中序遍历

/**
 * @param {Node} root
 * @return {Node}
 */
const treeToDoublyList = (root) => {
    let pre = null; // 初始化前驱节点
    let head = null; // 初始化头部节点
    const dfs = (cur) => {
        if (!cur) return;
        dfs(cur.left);
        if (pre) pre.right = cur;
        else head = cur;
        cur.left = pre;
        pre = cur;
        dfs(cur.right);
    }
    if (!root) return null;
    dfs(root);
    head.left = pre;
    pre.right = head;
    return head;
};
  • 时间复杂度 O(n)
  • 空间复杂度 O(n)

分析:

代码一共分为三个部分。第一部分是中序遍历二叉搜索树;第二部分是处理成双向链表;第三部分是处理成循环链表。下面逐步分析。

首先我们声明了两个指针,分别指向当前节点的前驱节点和当前链表的头部节点。记录前驱节点是因为我们要处理成双向链表,记录头部节点是因为最终返回结果需要返回链表的头部节点。

接下来看中序遍历。既然是递归,那么首先需要写好递归的终止条件。这里的终止条件就是节点为null。然后递归左子节点,处理当前节点,递归右子节点。处理当前节点的逻辑就是为了处理成双向链表。

接下来看处理成双向链表。如果前驱节点存在(这里的前驱节点就是回溯后的前一个遍历节点),将当前节点赋值给前驱节点的右指针。如果前驱节点不存在,意味着当前节点是第一个节点,所以赋值给头部节点。不论前驱节点是否存在,都将前驱节点赋值给当前节点的左指针。到现在为止,我们就建立了precur的双向关系。最后将当前节点赋值给前驱节点,意味着前驱节点后移一步,等待着下一次递归的调用。

最后来看处理成循环链表。这里其实就是主函数的逻辑。如果二叉搜索树为空,则直接返回null。然后递归二叉树,处理成双向链表。递归结束后,双向链表也处理好了。此时的head指向链表的头部节点,pre指向链表的尾部节点。我们只要将headpre建立起双向关系,就可以形成循环双向链表。我们只需要将尾部节点赋值给头部节点的左指针,将头部节点赋值给尾部节点的右指针。如此便可以将头尾相连。

总结

本题是通过中序遍历来形成增序的链表。在中序遍历的内部将链表处理成双向链表。最后将首尾相连,形成循环双向链表。

复杂度方面,中序遍历需要访问所有节点。因此时间复杂度是O(n) 。最坏情况下,二叉树退化为链表,此时需要O(n)的栈空间。因此空间复杂度是O(n)