力扣108. 将有序数组转换为二叉搜索树(中序遍历,递归)

本文详细解析了力扣108题:将有序数组转换为高度平衡的二叉搜索树。通过中序遍历和递归方法,探讨了不同选择根节点策略对生成树的影响,包括奇数和偶数元素数组的处理。提供了C++实现代码,深入分析了时间与空间复杂度。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

力扣108. 将有序数组转换为二叉搜索树(中序遍历,递归)

https://leetcode-cn.com/problems/convert-sorted-array-to-binary-search-tree/

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。

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

示例:

给定有序数组: [-10,-3,0,5,9],

一个可能的答案是:[0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树:

      0
     / \
   -3   9
   /   /
 -10  5

 

遍历树方法

中序遍历,递归

思路:

众所周知,二叉搜索树的中序遍历是一个升序序列。

将有序数组作为输入,可以把该问题看做 根据中序遍历序列创建二叉搜索树

下面是一些关于 BST 的知识。
只有中序遍历不能唯一确定一棵二叉搜索树。
先序+后序遍历不能唯一确定一棵二叉搜索树。
先序/后序遍历和中序遍历的关系:inorder = sorted(postorder) = sorted(preorder),
中序+后序、中序+先序可以唯一确定一棵二叉树。

注意:一个附件条件:树的高度应该是平衡的、例如:每个节点的两棵子树高度差不超过 1。

高度平衡意味着每次必须选择中间数字作为根节点。这对于奇数个数的数组是有用的,但对偶数个数的数组没有预定义的选择方案。

对于偶数个数的数组,要么选择中间位置左边的元素作为根节点,要么选择中间位置右边的元素作为根节点,不同的选择方案会创建不同的平衡二叉搜索树。

方法一:中序遍历:始终选择中间位置左边元素作为根节点;方法一始终选择中间位置左边的元素作为根节点。

int mid = (j + i) / 2

方法二:中序遍历:始终选择中间位置右边元素作为根节点;方法二始终选择中间位置右边的元素作为根节点。(i+j)奇数加一

int mid = (j + i) % 2 == 1 ? (j + i) / 2 + 1 : (j + i) / 2;

方法三:中序遍历:选择任意一个中间位置元素作为根节点;方法一和二会生成不同的二叉搜索树,这两种答案都是正确的。

如果 (i+j)奇数,随机选择加一或者加零

int mid = (j + i) % 2 == 1 ? (j + i) / 2 + randint(0, 1) : (j + i) / 2;

复杂度分析

时间复杂度:O(N),每个元素只访问一次。

空间复杂度:O(N),二叉搜索树空间 O(N),递归栈深度 O(logN)。

 

 

提供方法一解法:

#include "stdafx.h"
#include<queue>
#include<vector>
#include <iostream>
using namespace std;
struct TreeNode
{
	int val;
	TreeNode *left;
	TreeNode *right;
	TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};

class Solution
{
public:
	TreeNode* sortedArrayToBST(vector<int>& nums)
	{
		//鲁棒性
		if (nums.size() == 0)return nullptr;
		//根节点的数值下标
		int mid = (nums.size() - 1 + 0) / 2;
		//创建根节点
		TreeNode* head = new TreeNode(nums[mid]);
		//左子树,递归
		if (0 <= mid - 1)//防止越界
			head->left = recursive(nums, 0, mid - 1);
		//右子树,递归
		if (mid + 1 <= nums.size() - 1)
			head->right = recursive(nums, mid + 1, nums.size() - 1);
		return head;
	}
	TreeNode* recursive(vector<int>& nums, int i, int j)
	{
		int mid = (j + i) / 2;
		TreeNode* temp = new TreeNode(nums[mid]);
		if (i <= mid - 1)
			temp->left = recursive(nums, i, mid - 1);
		if (mid + 1 <= j)
			temp->right = recursive(nums, mid + 1, j);
		return temp;
	}
};

int main()
{
	vector<int>nums;
	nums.push_back(-10); nums.push_back(-3); nums.push_back(0); nums.push_back(5); nums.push_back(9);
	Solution s;
	auto result = s.sortedArrayToBST(nums);
	return 0;
}

 

 

附:二叉搜索树BST:Binary Search Trees

二叉排序树(二叉查找树、二叉搜索树)

1、二叉搜索树的特点

什么是二叉查找树:二叉搜索树的特点:对于树中的每个节点X,它的左子树中所有关键字值小于X的关键字值,而它的右子树中所有关键字值大于X的关键字值。

根据这个性质,对一个二叉树进行中序遍历,如果是单调递增的,则可以说明这个树是二叉搜索树

2、二叉搜索树的查找

过程:首先和根节点进行比较,如果等于根节点,则返回。如果小于根节点,则在根节点的左子树进行查找。如果大于根节点,则在根节点的右子树进行查找。

/* 查找以t为根节点的树中,是否包含x */
Position Find(ElementType x, SearchTree t)
{
    if (t == NULL) {
        return NULL;
    } else if (x < t->element) {
        return Find(x, t->left);
    } else if (x > t->element) {
        return Find(x, t->right);
    } else {
        return t;
    }
}

3、查找最大值和最小值

查找最大值:从根开始,如果有右儿子,则向右进行。直到右儿子为空,则当前节点为最大值。

迭代实现

Position FindMax(SearchTree t)
{
    if (t == NULL) {
        return NULL;
    }

    while (t->right != NULL)
    {
        t = t->right;
    }
    return t;  
}

查找最小值:从根开始,如果有左儿子,则向左进行。直到左儿子为空,则当前节点为最小值。

递归实现

Position FindMin(SearchTree t)
{
    if (t == NULL) {
        return NULL;
    } else if (t->left == NULL) {
        return t;
    } else {
        return FindMin(t->left);
    }
}

4、二叉搜索树的插入

二叉搜索树的插入过程和查找类似。新插入的节点一般在遍历的路径上的最后一点上,即叶子节点。如果待插入的数据比当前节点的数据大,并且当前节点的右儿子为空,则将待插入的节点插到右儿子位置上。如果右儿子不为空,则再递归的遍历右儿子。如果小于当前节点,则对左儿子做类似的处理就行。

SearchTree Insert(ElementType x, SearchTree t)
{
    if (t == NULL) {
        /* 插入第一个节点 */
        t = (SearchTree)malloc(sizeof(struct TreeNode));
        if (t == NULL) {
            return NULL;
        }
        t->element = x;
        t->left = NULL;
        t->right = NULL;
    } else if (x < t->element) {
        t->left = Insert(x, t->left);
    } else if (x > t->element) {
        t->right = Insert(x, t->right);
    }
    return t;
}

5、二叉搜索树的删除

二叉搜索树的删除分为以下几个情况:

1、待删除的节点是一个叶子节点,即它没有左右儿子。此时只要将它的父节点指向NULL即可。

2、如果节点有一个儿子,则该节点可以在其父节点调整指针绕过该节点后删除。

3、如果有两个儿子,一般的删除策略是用其右子树中最小的数据代替该节点的数据并递归地删除那个节点。因为右子树中最小地节点不可能有左儿子(如果有,则说明不是最小的),所以第二次删除更容易。(其实也就是将有两个儿子的情况转为容易处理的情况1或者2)。

/* 删除策略
 * 1、如果待删除节点只有一个儿子,则将该节点的父节点的儿子节点指针指向该节点的儿子节点,
 * 然后删除该节点.
 * 2、如果待删除节点有两个儿子,用右子树中最小的数据代替该节点的数据并递归地删除那个节点。
 * 因为右子树中最小地节点不可能有左儿子,所以第二次删除更容易.
 */
 SearchTree Delete(ElementType x, SearchTree t)
 {
     Position tmp;
     if (t == NULL) {  
         return NULL;
     }
     if (x < t->element) {
         t->left = Delete(x, t->left);
     } else if (x > t->element) {
         t->right = Delete(x, t->right);
     } else if (t->left && t->right) {
         tmp = FindMin(t->right);
         t->element = tmp->element;
         t->right = Delete(t->element, t->right);
     } else {
         tmp = t;
         if (t->left) {
             t = t->left;
         } else if (t->right) {
             t = t->right;
         }
         free(tmp);
         tmp = NULL;
     }
     return t;
 }

6、建立一棵空树

SearchTree MakeEmpty(SearchTree t)
{
    if (t != NULL) {
        MakeEmpty(t->left);
        MakeEmpty(t->right);
        free(t);
        t = NULL;
    }
    return NULL;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值