【JAVA算法】简单-将有序数组转换为二叉搜索树

算法虽难,循序渐进,督促自己,总有进步;
本博文仅为了督促自己学习算法,如有遗漏或错误之处,请不吝指教;

题目

将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。

示例 :

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

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

      0
     / \
   -3   9
   /   /
 -10  5

解题思路:

题目中给出了两个条件:一个升序排序的有序数组,转换为一棵高度平衡二叉搜索树

我们先了解下,什么是二叉搜索树:

二叉搜索树
二叉搜索树(Binary Search Tree)是指一棵空树或具有如下性质的二叉树:

  • 若任意节点的左子树不空,则左子树上所有节点的值均小于它的根节点的值
  • 若任意节点的右子树不空,则右子树上所有节点的值均大于它的根节点的值
  • 任意节点的左、右子树也分别为二叉搜索树
  • 没有键值相等的节点

基于以上性质,我们可以得出一个二叉搜索树的特性:二叉搜索树的中序遍历结果为递增序列

中序遍历的顺序为:左节点 → 根节点 → 右节点

遍历树的方法有哪些:

DFS(深度优先),又细分为先序遍历,中序遍历,后序遍历
BFS(广度优先)

  • 深度优先遍历(DFS)
    这种方法以深度 depth 优先为策略,从根节点开始一直遍历到某个叶子节点,然后回到根节点,在遍历另外一个分支。
    根据根节点,左孩子节点和右孩子节点的访问顺序又可以将 DFS 细分为先序遍历 preorder,中序遍历 inorder 和后序遍历 postorder。
  • 广度优先遍历(BFS)
    按照高度顺序,从上往下逐层遍历节点。
    先遍历上层节点再遍历下层节点。

下图中按照不同的方法遍历对应子树,得到的遍历顺序都是 1-2-3-4-5。根据不同子树结构比较不同遍历方法的特点
【JAVA算法】简单-将有序数组转换为二叉搜索树_第1张图片
将有序数组转换为二叉搜索树的结果为什么 不唯一 ?
二叉搜索树的中序遍历是一个升序序列。
将有序数组作为输入,可以把该问题看做 根据中序遍历序列创建二叉搜索树。

这个问题的答案唯一吗。例如:是否可以根据中序遍历序列和二叉搜索树之间是否一一对应,答案是 否定的

下面是一些关于 BST 的知识。

  • 中序遍历不能唯一确定一棵二叉搜索树。
  • 先序和后序遍历不能唯一确定一棵二叉搜索树。
  • 先序/后序遍历和中序遍历的关系: inorder = sorted(postorder) = sorted(preorder)
  • 中序+后序、中序+先序可以唯一确定一棵二叉树。

因此,“有序数组 -> BST”有多种答案。
【JAVA算法】简单-将有序数组转换为二叉搜索树_第2张图片
因此,添加一个附件条件:树的高度应该是平衡的、例如:每个节点的两棵子树高度差不超过 1

这种情况下答案唯一吗?仍然没有。

【JAVA算法】简单-将有序数组转换为二叉搜索树_第3张图片
高度平衡意味着每次必须选择中间数字作为根节点。这对于奇数个数的数组是有用的,但对偶数个数的数组没有预定义的选择方案。

【JAVA算法】简单-将有序数组转换为二叉搜索树_第4张图片
对于偶数个数的数组,要么选择中间位置左边的元素作为根节点,要么选择中间位置右边的元素作为根节点,不同的选择方案会创建不同的平衡二叉搜索树。
方法一:始终选择中间位置左边的元素作为根节点,
方法二:始终选择中间位置右边的元素作为根节点。
方法一和二会生成不同的二叉搜索树,这两种答案都是正确的。

如何构造树

构造一棵树的过程可以拆分成无数个这样的子问题:构造树的每个节点以及节点之间的关系。对于每个节点来说,都需要:

  • 选取节点
  • 构造该节点的左子树
  • 构造该节点的右子树

因题目要求构造一棵「高度平衡」的树,所以我们在选取节点时选择数组的中点作为根节点,以此来保证平衡性。

以题目给出的 [-10,-3,0,5,9] 为例。

我们选取数组中点,即数字 0 作为根节点。此时,以 0 为分界点将数组分为左右两个部分,左侧为 [-10, -3],右侧为 [5, 9]。
因该数组为升序排列的有序数组,所以左侧数组值均小于 0,可作为节点 0 的左子树;右侧数组值均大于 0,可作为节点 0 的右子树。

【JAVA算法】简单-将有序数组转换为二叉搜索树_第5张图片
同上述步骤,将 [-10, -3] 和 [5, 9] 单独看作两棵树,从而继续为他们构造左右子树。

对于左侧数组 [-10, -3],我们选取 -3 作为根节点;对于右侧数组 [5, 9],选取 9 作为根节点。

最终构造结果如下:

【JAVA算法】简单-将有序数组转换为二叉搜索树_第6张图片

递归设计

函数作用
通过上述解题过程我们可以明确该问题的子问题是:构造树的每个节点以及该节点的左右子树。因此,递归函数的作用很明显:

  • 选取要构造关系的节点并创建它
  • 构造该节点的左子树
  • 构造该节点的右子树
  • 函数的输入为递增数组,函数的返回为完成构造的节点。

何时结束
当输入的递增数组为空时,只能构成一棵空树,此时返回空节点。

何时调用
当构造节点的左右子树时,对递增数组进行拆分并进行递归调用。

解题方案:

方法一:中序遍历

算法思路

始终选择中间位置左边元素作为根节点
【JAVA算法】简单-将有序数组转换为二叉搜索树_第7张图片

  • 方法 helper(left, right) 使用数组 numsnums 中索引从 left 到 right 的元素创建 BST:
    如果 left > right,子树中不存在元素,返回空。
    找出中间元素:p = (left + right) // 2。
    创建根节点:root = TreeNode(nums[p])。
    递归创建左子树 root.left = helper(left, p - 1) 和右子树 root.right = helper(p + 1, right)。
  • 返回 helper(0, len(nums) - 1)。

JAVA代码

class Solution {
  int[] nums;

  public TreeNode helper(int left, int right) {
    if (left > right) return null;

    // always choose left middle node as a root
    int p = (left + right) / 2;

    // inorder traversal: left -> node -> right
    TreeNode root = new TreeNode(nums[p]);
    root.left = helper(left, p - 1);
    root.right = helper(p + 1, right);
    return root;
  }

  public TreeNode sortedArrayToBST(int[] nums) {
    this.nums = nums;
    return helper(0, nums.length - 1);
  }
}

方法二.中序遍历

始终选择中间位置右边元素作为根节点

【JAVA算法】简单-将有序数组转换为二叉搜索树_第8张图片

JAVA代码

class Solution {
  int[] nums;

  public TreeNode helper(int left, int right) {
    if (left > right) return null;

    // always choose right middle node as a root
    int p = (left + right) / 2;
    if ((left + right) % 2 == 1) ++p;

    // inorder traversal: left -> node -> right
    TreeNode root = new TreeNode(nums[p]);
    root.left = helper(left, p - 1);
    root.right = helper(p + 1, right);
    return root;
  }

  public TreeNode sortedArrayToBST(int[] nums) {
    this.nums = nums;
    return helper(0, nums.length - 1);
  }
}

方法三:中序遍历

算法思路

选择任意一个中间位置元素作为根节点

不做预定义选择,每次随机选择中间位置左边或者右边元素作为根节点。每次运行的结果都不同,但都是正确的
【JAVA算法】简单-将有序数组转换为二叉搜索树_第9张图片

JAVA代码

class Solution {
    int[] nums;
    Random rand = new Random();
    
    public TreeNode helper(int left, int right) {
        if (left > right) return null;
        
        // choose random middle node as a root
        int p = (left + right) / 2; 
        if ((left + right) % 2 == 1) p += rand.nextInt(2);

        // inorder traversal: left -> node -> right
        TreeNode root = new TreeNode(nums[p]);
        root.left = helper(left, p - 1);
        root.right = helper(p + 1, right);
        return root;
    }
    
    public TreeNode sortedArrayToBST(int[] nums) {
        this.nums = nums;
        return helper(0, nums.length - 1);    
    }
}

三种方法复杂度

  • 时间复杂度:O(N),每个元素只访问一次
  • 空间复杂度:O(N),二叉搜索树空间 O(N),递归栈深度O(logN)

欢迎关注我的公众号,每日更新,督促自己进步

你可能感兴趣的:(算法)