「剑指 Offer 36. 二叉搜索树与双向链表」
力扣题目链表[1]
输入一棵二叉搜索树,将该二叉搜索树转换成一个排序的循环双向链表。要求不能创建任何新的节点,只能调整树中节点指针的指向。
链表中的每个节点都有一个前驱和后继指针。对于双向循环链表,第一个节点的前驱是最后一个节点,最后一个节点的后继是第一个节点。
思路:
首先抛出一个结论:二叉搜索树的中序遍历的结果是增序。根据题目的要求,需要将二叉搜索树转换为排序的循环双向链表,因此这里需要对二叉搜索树进行中序遍历。
中序遍历
代码语言:javascript复制/**
* @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
。然后递归左子节点,处理当前节点,递归右子节点。处理当前节点的逻辑就是为了处理成双向链表。
接下来看处理成双向链表。如果前驱节点存在(这里的前驱节点就是回溯后的前一个遍历节点),将当前节点赋值给前驱节点的右指针。如果前驱节点不存在,意味着当前节点是第一个节点,所以赋值给头部节点。不论前驱节点是否存在,都将前驱节点赋值给当前节点的左指针。到现在为止,我们就建立了pre
和cur
的双向关系。最后将当前节点赋值给前驱节点,意味着前驱节点后移一步,等待着下一次递归的调用。
最后来看处理成循环链表。这里其实就是主函数的逻辑。如果二叉搜索树为空,则直接返回null
。然后递归二叉树,处理成双向链表。递归结束后,双向链表也处理好了。此时的head
指向链表的头部节点,pre
指向链表的尾部节点。我们只要将head
和pre
建立起双向关系,就可以形成循环双向链表。我们只需要将尾部节点赋值给头部节点的左指针,将头部节点赋值给尾部节点的右指针。如此便可以将头尾相连。
总结
本题是通过中序遍历来形成增序的链表。在中序遍历的内部将链表处理成双向链表。最后将首尾相连,形成循环双向链表。
复杂度方面,中序遍历需要访问所有节点。因此时间复杂度是O(n)
。最坏情况下,二叉树退化为链表,此时需要O(n)
的栈空间。因此空间复杂度是O(n)
。
参考资料
[1]
力扣题目链表: https://leetcode-cn.com/leetbook/read/illustration-of-algorithm/5dbies/