【算法--链表】109.有序链表转换二叉搜索树--通俗讲解

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

给你一个升序排列的单链表,将其转换为高度平衡的二叉搜索树(BST),即每个节点的左右子树高度差不超过1。

示例:

  • 输入:head = [-10, -3, 0, 5, 9]
  • 输出:一个平衡BST,例如 [0, -3, 9, -10, null, 5](树的形式,根节点为0,左子树为-3,右子树为9,等等)

二、解题核心

使用快慢指针找到链表的中间节点作为二叉搜索树的根节点,然后递归构建左子树和右子树。 这就像找到一群按身高排序的人中的中间那个人作为队长,然后让左边的人组成左队,右边的人组成右队,递归进行直到所有人都安排到位。

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

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

1. 快慢指针找中间节点

  • 是什么:快指针每次走两步,慢指针每次走一步,当快指针到达链表末尾时,慢指针指向中间节点。
  • 为什么重要:中间节点作为BST的根节点,可以确保树是平衡的,因为左右子树的节点数大致相等。

2. 递归构建子树

  • 是什么:找到中间节点后,将链表分为左半部分和右半部分,递归构建左子树和右子树。
  • 为什么重要:递归是构建树结构的关键,每次递归处理子链表,确保每个子树都是平衡的。

3. 链表断开处理

  • 是什么:在找到中间节点后,需要将左半部分的链表末尾断开,以便递归处理左半部分时不会包含中间节点及其后的节点。
  • 为什么重要:如果不断开链表,递归时会处理错误的节点范围,导致树结构错误。

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

让我们用链表 [-10, -3, 0, 5, 9] 的例子来可视化过程:

  1. 找到中间节点

    • 快慢指针初始化:快指针和慢指针都指向头节点 -10。
    • 快指针移动两步(到0),慢指针移动一步(到-3)。
    • 快指针移动两步(到9),慢指针移动一步(到0)。
    • 快指针到达末尾,慢指针指向0,即中间节点。
  2. 构建根节点

    • 以中间节点0的值创建根节点。
  3. 断开链表

    • 左半部分链表:从头节点 -10 到中间节点0的前一个节点 -3(需要断开,使 -3 的 next 为 null)。
    • 右半部分链表:中间节点0的下一个节点5开始到末尾。
  4. 递归构建左子树

    • 左半部分链表:[-10, -3]
    • 找到中间节点:快慢指针找到 -3 作为中间节点(因为链表较短,-3 是中间)。
    • 以 -3 为根,左子树为 -10,右子树为 null。
  5. 递归构建右子树

    • 右半部分链表:[5, 9]
    • 找到中间节点:快慢指针找到 5 作为中间节点(但实际中间应该是9?需要正确处理)。
    • 以 5 为根,左子树为 null,右子树为 9。
  6. 组合成树

    • 根节点0的左子树指向 -3,右子树指向 5。
    • 最终树结构: 0 /
      -3 5 /
      -10 9

注意:实际过程中,递归会正确处理链表长度,确保树平衡。

五、C++ 代码实现(附详细注释)

cpp 复制代码
#include <iostream>
using namespace std;

// 链表节点定义
struct ListNode {
    int val;
    ListNode *next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode *next) : val(x), next(next) {}
};

// 二叉树节点定义
struct TreeNode {
    int val;
    TreeNode *left;
    TreeNode *right;
    TreeNode() : val(0), left(nullptr), right(nullptr) {}
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
};

class Solution {
public:
    TreeNode* sortedListToBST(ListNode* head) {
        // 如果链表为空,返回空树
        if (head == nullptr) {
            return nullptr;
        }
        // 如果只有一个节点,直接返回该节点构成的树
        if (head->next == nullptr) {
            return new TreeNode(head->val);
        }
        
        // 使用快慢指针找到中间节点的前一个节点
        ListNode* slow = head;
        ListNode* fast = head;
        ListNode* prev = nullptr; // 用于记录中间节点的前一个节点
        
        while (fast != nullptr && fast->next != nullptr) {
            prev = slow;
            slow = slow->next;
            fast = fast->next->next;
        }
        
        // 此时slow指向中间节点,prev指向中间节点的前一个节点
        // 断开链表:将prev的next置为null,形成左半部分链表
        if (prev != nullptr) {
            prev->next = nullptr;
        }
        
        // 创建根节点,值为中间节点的值
        TreeNode* root = new TreeNode(slow->val);
        
        // 递归构建左子树:左半部分链表从head到prev
        root->left = sortedListToBST(head);
        
        // 递归构建右子树:右半部分链表从slow->next开始
        root->right = sortedListToBST(slow->next);
        
        return root;
    }
};

// 辅助函数:打印二叉树(中序遍历,用于验证)
void inOrder(TreeNode* root) {
    if (root == nullptr) {
        return;
    }
    inOrder(root->left);
    cout << root->val << " ";
    inOrder(root->right);
}

// 测试代码
int main() {
    // 创建示例链表:[-10, -3, 0, 5, 9]
    ListNode* head = new ListNode(-10);
    head->next = new ListNode(-3);
    head->next->next = new ListNode(0);
    head->next->next->next = new ListNode(5);
    head->next->next->next->next = new ListNode(9);
    
    Solution solution;
    TreeNode* root = solution.sortedListToBST(head);
    
    inOrder(root); // 输出:-10 -3 0 5 9 (中序遍历结果应为升序)
    cout << endl;
    
    // 释放内存(实际面试中可能不需要完整释放)
    return 0;
}

六、注意事项

  • 递归终止条件:当链表为空或只有一个节点时,直接返回,这是递归的基础情况。
  • 快慢指针的正确性:快慢指针需要正确处理,确保慢指针指向中间节点。当链表长度为偶数时,中间节点有两个,通常取第二个中间节点(即慢指针指向的位置)以保证树平衡。
  • 链表断开:在递归前必须断开链表,否则左半部分递归会包含整个链表,导致无限递归或错误。
  • 内存管理:在C++中,创建了新的树节点,需要确保在适当的时候释放内存,但面试中通常更关注算法逻辑。
  • 时间复杂度:每次递归都需要找到中间节点,总时间复杂度为 O(n log n),因为链表每次被分成两半,但找中间节点需要线性时间。
相关推荐
寂静山林11 分钟前
UVa 12991 Game Rooms
算法·1024程序员节
Dream it possible!1 小时前
LeetCode 面试经典 150_链表_合并两个有序链表(58_21_C++_简单)
leetcode·链表·面试·1024程序员节
余俊晖1 小时前
RLVR训练多模态文档解析模型-olmOCR 2技术方案(模型、数据和代码均开源)
人工智能·算法·ocr·grpo
凉虾皮1 小时前
2024包河初中组
学习·算法·1024程序员节
m0_748233642 小时前
C++ 模板初阶:从函数重载到泛型编程的优雅过渡
java·c++·算法·1024程序员节
以己之2 小时前
11.盛最多水的容器
java·算法·双指针·1024程序员节
初级炼丹师(爱说实话版)3 小时前
算法面经常考题整理(3)大模型
算法
Neil今天也要学习4 小时前
永磁同步电机无速度算法--基于相位超前校正的LESO
算法·1024程序员节
码农多耕地呗4 小时前
力扣226.翻转二叉树(java)
算法·leetcode·职场和发展
韭菜炒大葱5 小时前
Git入门指南:掌握版本控制的核心工作流程
git·面试