【算法】426.将二叉搜索树转化为排序的双向链表--通俗讲解

80 阅读6分钟

一、题目是啥?一句话说清

给定一个二叉搜索树(BST),将其就地转换为一个排序的双向循环链表,其中左指针指向前驱,右指针指向后继,并且头尾相连。

示例:

  • 输入:二叉搜索树(例如:根节点为4,左子树有2(左1右3),右子树有5)
  • 输出:双向循环链表 1 ↔ 2 ↔ 3 ↔ 4 ↔ 5 ↔(回到1)

二、解题核心

利用二叉搜索树的中序遍历特性(遍历结果有序),在遍历过程中调整节点指针,将左指针指向前驱节点,右指针指向后继节点,最后将头尾节点连接形成循环链表。

这就像把一棵树变成一条环形的队伍,每个人左手拉着前一个人,右手拉着后一个人,队伍从小到大排列,并且队首和队尾相连。

三、关键在哪里?(3个核心点)

想理解并解决这道题,必须抓住以下三个关键点:

1. 中序遍历的有序性

  • 是什么:二叉搜索树的中序遍历结果是一个升序序列。
  • 为什么重要:这保证了转换后的链表是有序的,无需额外排序。

2. 指针调整

  • 是什么:在遍历过程中,维护一个指向前一个节点的指针,将当前节点的左指针指向前一个节点,前一个节点的右指针指向当前节点。
  • 为什么重要:这是构建双向链表的核心操作,通过调整指针实现节点之间的连接。

3. 循环链表的形成

  • 是什么:遍历完成后,将头节点(最小节点)的左指针指向尾节点(最大节点),尾节点的右指针指向头节点。
  • 为什么重要:这完成了循环链表的构建,确保链表首尾相连,满足题目要求。

四、看图理解流程(通俗理解版本)

假设二叉搜索树如下:

        4
       / \
      2   5
     / \
    1   3
  1. 中序遍历顺序:1 → 2 → 3 → 4 → 5
  2. 遍历过程
    • 访问节点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。
  3. 形成循环:遍历完成后,头节点是1,尾节点是5。将1的左指针指向5,5的右指针指向1。
  4. 最终链表: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.链表随机节点--通俗讲解


关注公众号,获取更多底层机制/ 算法通俗讲解干货!