LeetCode题108“有序数组转二叉搜索树”总结

73 阅读5分钟

原题链接:108. 将有序数组转换为二叉搜索树 - 力扣(LeetCode)

image.png

这道题刚看的时候一脸懵逼,不知道怎么下手。二叉搜索树指的是平衡二叉搜索树,也就是任意节点的左右子树高度只差不超过1。当时做的时候直觉是二分加递归,可是只知道大概思路,不知道代码怎么实现。尤其是涉及到递归的算法,总是感觉理解不够透彻。因此这篇文章就是总结一下这道题里面出现过的知识点,做一个复盘。

1.算法题方法论

虽然我的算法水平很一般,但是这几年也陆陆续续做了许多算法题。我对解题的理解是:先想清楚思路,再下笔。

以python为例,我会在输入框的最上面用'''text'''把思路部分框起来,说白了就是用多行注释的方式整理思路。

image.png 不能一上来就写代码,这样很容易写到一半思维混乱,然后无法动笔。 最好就是用注释的方式理清思路,分析到各种情况,逻辑上成立,代码才有可能成立。

2. 算法分析

涉及到树的算法题很多都要用递归,这题也不例外。如果本题跟标题一样只要求就是二叉搜索树,那就很简单,直接就从有序数组下标为0的位置不断往后走,后一个结点是前一个结点的右儿子,构成一颗只含右子树不含左子树的树,就是二叉搜索树。

可惜的是题干要求是平衡二叉搜索树,如果按照刚刚的方法构建,左右子树的高度之差肯定超过1,因为根节点的左子树高度为0,右子树的高度可以是一个很大的之,差远大于1。所以这种思路不行

正确的思路应该是,既然要求是平衡二叉搜索树,那么根节点的左右子树要尽可能对称,不要出现一边很多一边很少的情况。搜索树要求结点的大小关系满足左子树<根节点<右子树,所以根节点应该在原有序数组的中间位置。

依此类推,根节点的左子树和右子树也都应该满足上述条件,也就是说根节点的左儿子是前半截有序数组的中间点,根节点的右儿子是有序数组后半截的中间点。分析到这里,基本就可以确定是用二分+递归实现了。

代码如下

'''
只能说毫无思路。直觉是二分加递归,构建好树后再用宽搜输出这棵树。
如果数组中的元素是奇数个,那么左右子树高度差为0;如果元素个数为偶数个,那么左右子树高度差为1

构建好树以后再以层序输出,也就是bfs遍历。

思路有了,代码写的出来吗?怎样用二分的递归去构建这棵树?

-------------------------------------------------------
最后返回的是一棵树,不是数组,没必要最后补一个宽搜。
到目前为止,我对递归的体会就是思路想通,以人类的思路解题
'''

# Definition for a binary tree node.
# class TreeNode:
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right

import collections
class Solution:
    def sortedArrayToBST(self, nums: List[int]) -> Optional[TreeNode]:
        len_nums = len(nums)
        if len_nums == 0:
            return []

        def digui(self, nums, l, r):
            if l > r:
                return None
            mid = int((l + r) / 2)
            root = TreeNode(nums[mid])
            root.left = digui(self, nums, l, mid-1)
            root.right = digui(self, nums, mid+1, r)
            return root
        root = digui(self, nums, 0, len_nums-1)  
        return root


3. 递归算法应该怎么写

递归最直观的理解是函数自己调用自己。就我的经验而言,写递归最重要的是不去纠结底层实现,只是按照前面分析过的逻辑顺序写就好了。

比如这道题,根结点的左右子树都是平衡二叉搜索树,所以左右子树都应该用一遍digui函数。这句话就对应着最关键的三行代码:

root = TreeNode(nums[mid])
root.left = digui(self, nums, l, mid-1)
root.right = digui(self, nums, mid+1, r)

能根据上面的理解写出对应的代码就实现大半了,剩下的就是写好边界条件。当l>r时,说明当前处理片段中已经没有结点了,所以返回None。最后题目要求的返回类型是二叉树,所以return root.

4. python嵌套函数以及对self的理解

为什么这题要在sortArrayToBST函数内部再定义一个函数digui,而不是直接用sortArrayToBST实现递归?

直观的理解是,递归函数的参数肯定涉及左边界和右边界,也就是l和r,l和r不断变化,自己调用自己,以实现递归。而原函数的参数已经定义好了,是不含l和r的,因此需要重新在原函数内部再定义一个递归函数。


为什么函数的参数里面都得带一个self? 函数体内并没有使用self这个参数。

一定程度上,这是python的语法规定。如果不是在类的里面定义函数,是可以不写self的。但是如果是在class的内部定义的函数,python规定了第一个参数必须是self, 用它可以修改类的属性,调用类的方法。