一、题目是啥?一句话说清
给定一个二叉搜索树(BST),将其就地转换为一个排序的双向循环链表,其中左指针指向前驱,右指针指向后继,并且头尾相连。
示例:
- 输入:二叉搜索树(例如:根节点为4,左子树有2(左1右3),右子树有5)
- 输出:双向循环链表 1 ↔ 2 ↔ 3 ↔ 4 ↔ 5 ↔(回到1)
二、解题核心
利用二叉搜索树的中序遍历特性(遍历结果有序),在遍历过程中调整节点指针,将左指针指向前驱节点,右指针指向后继节点,最后将头尾节点连接形成循环链表。
这就像把一棵树变成一条环形的队伍,每个人左手拉着前一个人,右手拉着后一个人,队伍从小到大排列,并且队首和队尾相连。
三、关键在哪里?(3个核心点)
想理解并解决这道题,必须抓住以下三个关键点:
1. 中序遍历的有序性
- 是什么:二叉搜索树的中序遍历结果是一个升序序列。
- 为什么重要:这保证了转换后的链表是有序的,无需额外排序。
2. 指针调整
- 是什么:在遍历过程中,维护一个指向前一个节点的指针,将当前节点的左指针指向前一个节点,前一个节点的右指针指向当前节点。
- 为什么重要:这是构建双向链表的核心操作,通过调整指针实现节点之间的连接。
3. 循环链表的形成
- 是什么:遍历完成后,将头节点(最小节点)的左指针指向尾节点(最大节点),尾节点的右指针指向头节点。
- 为什么重要:这完成了循环链表的构建,确保链表首尾相连,满足题目要求。
四、看图理解流程(通俗理解版本)
假设二叉搜索树如下:
4
/ \
2 5
/ \
1 3
- 中序遍历顺序:1 → 2 → 3 → 4 → 5
- 遍历过程:
- 访问节点1:前一个节点为null,所以节点1是头节点。prev指向1。
- 访问节点2:prev是1,将1的右指针指向2,2的左指针指向1。prev指向2。
- 访问节点3:prev是2,将2的右指针指向3,3的左指针指向2。prev指向3。
- 访问节点4:prev是3,将3的右指针指向4,4的左指针指向3。prev指向4。
- 访问节点5:prev是4,将4的右指针指向5,5的左指针指向4。prev指向5。
- 形成循环:遍历完成后,头节点是1,尾节点是5。将1的左指针指向5,5的右指针指向1。
- 最终链表:1 ↔ 2 ↔ 3 ↔ 4 ↔ 5 ↔(回到1)
五、C++ 代码实现(附详细注释)
#include <iostream>
using namespace std;
// 二叉树节点定义
class Node {
public:
int val;
Node* left;
Node* right;
Node() : val(0), left(nullptr), right(nullptr) {}
Node(int _val) : val(_val), left(nullptr), right(nullptr) {}
Node(int _val, Node* _left, Node* _right)
: val(_val), left(_left), right(_right) {}
};
class Solution {
private:
Node* prev; // 用于记录中序遍历中的前一个节点
Node* head; // 链表的头节点(最小节点)
// 中序遍历递归函数
void inorder(Node* curr) {
if (curr == nullptr) return;
// 遍历左子树
inorder(curr->left);
// 处理当前节点
if (prev == nullptr) {
// 第一个节点,设置为头节点
head = curr;
} else {
// 连接前一个节点和当前节点
prev->right = curr;
curr->left = prev;
}
prev = curr; // 更新prev为当前节点
// 遍历右子树
inorder(curr->right);
}
public:
Node* treeToDoublyList(Node* root) {
if (root == nullptr) return nullptr;
prev = nullptr;
head = nullptr;
// 中序遍历调整指针
inorder(root);
// 连接头尾节点形成循环链表
head->left = prev;
prev->right = head;
return head;
}
};
// 辅助函数:打印双向循环链表(向前打印)
void printList(Node* head) {
if (head == nullptr) return;
Node* current = head;
do {
cout << current->val << " ";
current = current->right;
} while (current != head);
cout << endl;
}
// 测试代码
int main() {
// 构建示例二叉搜索树
Node* root = new Node(4);
root->left = new Node(2);
root->right = new Node(5);
root->left->left = new Node(1);
root->left->right = new Node(3);
Solution solution;
Node* head = solution.treeToDoublyList(root);
printList(head); // 输出:1 2 3 4 5
// 释放内存(简单示例,实际中可能需要更完整的释放)
// 注意:由于是循环链表,需要小心释放以避免无限循环
// 这里为了简单,不完整释放
return 0;
}
六、时间空间复杂度
- 时间复杂度:O(n),其中n是节点数。每个节点被访问一次。
- 空间复杂度:O(h),其中h是树的高度。这是由于递归栈的空间。最坏情况下(树为链状),h=n;平均情况下(树平衡),h=log n。
七、注意事项
- 递归深度:对于大型树,递归可能导致栈溢出。可以考虑使用迭代中序遍历来避免递归。
- 空树处理:如果输入是空树,直接返回nullptr。
- 指针操作:在调整指针时,确保不会丢失对节点的引用。
- 循环连接:最后一定要连接头尾节点,否则链表不是循环的。
- 内存管理:在C++中,如果需要释放内存,要注意循环链表可能导致无限循环,需要先断开循环再释放。
算法通俗讲解推荐阅读
【算法--链表】83.删除排序链表中的重复元素--通俗讲解
【算法--链表】删除排序链表中的重复元素 II--通俗讲解
【算法--链表】86.分割链表--通俗讲解
【算法】92.翻转链表Ⅱ--通俗讲解
【算法--链表】109.有序链表转换二叉搜索树--通俗讲解
【算法--链表】114.二叉树展开为链表--通俗讲解
【算法--链表】116.填充每个节点的下一个右侧节点指针--通俗讲解
【算法--链表】117.填充每个节点的下一个右侧节点指针Ⅱ--通俗讲解
【算法--链表】138.随机链表的复制--通俗讲解
【算法】143.重排链表--通俗讲解
【算法--链表】146.LRU缓存--通俗讲解
【算法--链表】147.对链表进行插入排序--通俗讲解
【算法】【链表】148.排序链表--通俗讲解
【算法】【链表】160.相交链表--通俗讲解
【算法】【链表】203.移除链表元素--通俗讲解
【算法】【链表】206.反转链表--通俗讲解
【算法】234.回文链表--通俗讲解
【算法】【链表】237.删除链表中的节点--通俗讲解
【算法】【链表】328.奇偶链表--通俗讲解
【算法】【链表】给单链表加一--通俗讲解
【算法】【链表】382.链表随机节点--通俗讲解
关注公众号,获取更多底层机制/ 算法通俗讲解干货!