109. 有序链表转换二叉搜索树 ●●

10 篇文章 0 订阅
9 篇文章 2 订阅

109. 有序链表转换二叉搜索树 ●●

描述

给定一个单链表的头节点 head ,其中的元素 按升序排序 ,将其转换为高度平衡的二叉搜索树

本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差不超过 1。

示例

输入: head = [-10,-3,0,5,9]
输出: [0,-3,9,-10,null,5]
解释: 一个可能的答案是[0,-3,9,-10,null,5],它表示所示的高度平衡的二叉搜索树。

题解

1. 分治 链表中位数

108. 将有序数组转换为二叉搜索树 ● 类似,但是此题涉及到链表的操作问题,因为链表不能像数组一样随机存取,需要通过顺序访问来获取到中位数的位置。

此处通过快慢指针来得到中位数,快指针比慢指针快一倍,当快指针到达终点时,慢指针则指向中位数

然后通过分治递归,构造左右子树。

由于题目中给定的链表为单向链表,访问后继元素十分容易,但无法直接访问前驱元素。因此在找出链表的中位数节点 mid 之后,如果设定「左闭右开」的关系,我们就可以直接用 (left,mid) 以及(mid->next,right) 来表示左右子树对应的列表了。并且,初始的列表也可以用 (head,null) 方便地进行表示,其中 null表示空节点。

  • 时间复杂度: O ( n log ⁡ n ) O(n \log n) O(nlogn),其中 n 是链表的长度。
    设长度为 n 的链表构造二叉搜索树的时间为 T ( n ) T(n) T(n),递推式为 T ( n ) = 2 ⋅ T ( n / 2 ) + O ( n ) T(n) = 2 \cdot T(n/2) + O(n) T(n)=2T(n/2)+O(n),根据主定理, T ( n ) = O ( n log ⁡ n ) T(n) = O(n \log n) T(n)=O(nlogn)

  • 空间复杂度: O ( log ⁡ n ) O(\log n) O(logn),这里只计算除了返回答案之外的空间。平衡二叉树的高度为 O ( log ⁡ n ) O(\log n) O(logn),即为递归过程中栈的最大深度,也就是需要的空间。

class Solution {
public:
    ListNode* getMid(ListNode* head, ListNode* end){
        ListNode* slow = head;                          // 快慢指针
        ListNode* fast = head;
        while(fast != end && fast->next != end){
            slow = slow->next;
            fast = fast->next->next;                    // 快指针是慢指针的两倍,因此慢指针为中位数
        }
        return slow;
    }

    TreeNode* bulidTree(ListNode* head, ListNode* end){     // 左闭右开
        if(head == end) return nullptr;                     // 当前节点以遍历过
        ListNode* mid = getMid(head, end);                  // 找到链表中位数
        TreeNode* root = new TreeNode(mid->val);            // 建立树根节点
        root->left = bulidTree(head, mid);                  // 左子树,左闭右开
        root->right = bulidTree(mid->next, end);            // 右子树
        return root;
    }

    TreeNode* sortedListToBST(ListNode* head) {
        return bulidTree(head, nullptr);
    }
};
2. 分治 + 中序遍历优化

方法一的时间复杂度的瓶颈在于寻找中位数节点

由于构造出的二叉搜索树的中序遍历结果就是链表本身,因此我们可以将分治和中序遍历结合起来,减少时间复杂度。

具体地,设当前链表的左端点编号为 left,右端点编号为 right,包含关系为「双闭」,即 left 和 right 均包含在链表中。

链表节点的编号为 [0,n)。中序遍历的顺序是「左子树 - 根节点 - 右子树」,那么在分治的过程中,我们不用急着找出链表的中位数节点,而是使用一个占位节点,等到中序遍历到该节点时,再填充它的值(更新 head 节点)。

我们可以通过计算编号范围来进行中序遍历:

  • 中位数节点对应的编号为 m i d = ( l e f t + r i g h t + ) / 2 mid=(left+right+)/2 mid=(left+right+)/2
  • 左右子树对应的编号范围分别为 [ l e f t , m i d − 1 ] [left,mid−1] [left,mid1] [ m i d + 1 , r i g h t ] [mid+1,right] [mid+1,right]

如果 l e f t > r i g h t left>right left>right,那么遍历到的位置对应着一个空节点,否则对应着二叉搜索树中的一个节点。

这样一来,我们其实已经知道了这棵二叉搜索树的结构,并且题目给定了它的中序遍历结果,那么我们只要对其进行中序遍历,就可以还原出整棵二叉搜索树了。

  • 时间复杂度: O ( n ) O(n) O(n),其中 n 是链表的长度。
    设长度为 n 的链表构造二叉搜索树的时间为 T(n),递推式为 T ( n ) = 2 ⋅ T ( n / 2 ) + O ( 1 ) T(n) = 2 \cdot T(n/2) + O(1) T(n)=2T(n/2)+O(1),根据主定理, T ( n ) = O ( n ) T(n) = O(n) T(n)=O(n)
  • 空间复杂度: O ( l o g n ) O(logn) O(logn),这里只计算除了返回答案之外的空间。平衡二叉树的高度为 O ( log ⁡ n ) O(\log n) O(logn),即为递归过程中栈的最大深度,也就是需要的空间。

注意:
递归函数中用head = head -> next;来表示中序遍历时链表节点的更新过程;
head 传参需要加 &,才能真正改变链表节点的位置。

class Solution {
public:
    TreeNode* buildTree(ListNode*& head, int left, int right){     // 双闭
        if(left > right) return nullptr;                // 空节点
        int mid = (left + right) >> 1;                  // 中位数节点
        TreeNode* root = new TreeNode();                
        root->left = buildTree(head, left, mid - 1);    // 左
        root->val = head->val;                          // 中,实际是从最左节点开始赋值,即中序遍历得到的链表
        head = head->next;                              // 中,更新链表节点,head 传参需要加 &
        root->right = buildTree(head, mid + 1, right);  // 右
        return root;
    }

    TreeNode* sortedListToBST(ListNode* head){
        int len = 0;
        ListNode* curr = head;
        while(curr != nullptr){
            ++len;                                      // 链表长度
            curr = curr->next;
        }
        return buildTree(head, 0, len-1);
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值