(补) 算法训练Day18 | LeetCode513. 找树左下角的值(一路向左?);112/113. 路径总和I,II(回溯);106.从后序和中序遍历序列构造二叉树(105. 从前序和中序)

这篇博客详细介绍了LeetCode中的几个经典树形问题,包括找树左下角的值、路径总和I和II以及从遍历序列构造二叉树。博主通过递归和迭代两种方法解析了解题思路,重点讨论了如何判断树的最后一行以及如何在递归中处理回溯。此外,还分析了时间复杂度和空间复杂度,并分享了学习过程中的收获和思考。
摘要由CSDN通过智能技术生成

目录

 LeetCode513. 找树左下角的值

方法一:递归解法

1. 思路

2. 代码实现

3. 复杂度分析

4. 收获和思考

方法二: 迭代解法

1. 思路

2. 代码实现

3. 复杂度分析

4. 收获与思考

LeetCode112. 路径总和

方法一:递归解法

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

方法二:迭代解法

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

5. 拓展

LeetCode113 路径总和II

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode106. 从后序和中序遍历序列构造二叉树

leetcode105. 从前序和中序遍历序列构造二叉树

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


 LeetCode513. 找树左下角的值

链接: 513. 找树左下角的值 - 力扣(LeetCode)

 

方法一:递归解法

1. 思路

本地要找出树的最后一行找到最左边的值。此时大家应该想起用层序遍历是非常简单的了,反而用递归的话会比较难一点,我们依然还是先介绍递归法。

常见易错点

咋眼一看,这道题目用递归的话就就一直向左遍历,最后一个就是答案呗?没有这么简单,一直向左遍历到最后一个,它未必是最后一行啊。我们来分析一下题目:在树的最后一行找到最左边的值;

左下角的值,也可能是上级node的右孩子,不一定左下角的值一定是左孩子。

大体思路

首先要是最后一行,然后是最左边的值。如果使用递归法,如何判断是最后一行呢,其实就是深度最大的叶子节点一定是最后一行。所以要找深度最大的叶子节点。那么如何找最左边的呢?可以使用前序遍历(当然中序,后序都可以,因为本题没有 中间节点的处理逻辑,只要左优先于右就行),保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值。

2. 代码实现

递归三部曲:

  1. 确定递归函数的参数和返回值

参数必须有要遍历的树的根节点,还有就是一个int型的变量用来记录最长深度; 这里就不需要返回值了,所以递归函数的返回类型为void;本题还需要类里的两个全局变量,maxLen用来记录最大深度,result记录最大深度最左节点的数值;

class Solution:
    def findBottomLeftValue(self, root: TreeNode) -> int:
        max_depth = -float("INF")
        leftmost_val = 0

        def __traverse(root, cur_depth): 
  • 确定终止条件

当遇到叶子节点的时候,就需要统计一下最大的深度了,所以需要遇到叶子节点来更新最大深度;代码如下:

if not root.left and not root.right: 
                if cur_depth > max_depth: 
                    max_depth = cur_depth
                    leftmost_val = root.val  
  • 确定单层递归的逻辑

在找最大深度的时候,递归的过程中依然要使用回溯,代码如下:

if root.left: 
                cur_depth += 1
                __traverse(root.left, cur_depth)
                cur_depth -= 1
            if root.right: 
                cur_depth += 1
                __traverse(root.right, cur_depth)
                cur_depth -= 1

完整代码如下:

# 递归解法
# time:O(N);space:O(N)
class Solution(object):
    def findBottomLeftValue(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        # Python定义全局变量的方式
        global result
        result = 0
        global curMaxDepth 
        curMaxDepth = -float("inf")
        self.traversal(root,0)
        return result
        
    def traversal(self,node,depth):
        global result
        global curMaxDepth
    # base case 碰到叶子节点
        if node.left == None and node.right ==  None:
            # 深度大于现有的,才更新记录的最大深度和对应数值
            if depth > curMaxDepth:
                curMaxDepth = depth
                result = node.val
    # 单层递归逻辑
        # 前中后序都可以,因为没有中间节点的处理逻辑,左优先于右即可
        # 因为base case没有判断node是否为空,所以这里要加判断条件
        if node.left: 
            depth += 1
            self.traversal(node.left,depth)
            depth -= 1
            # 以上三行可以直接写成:(隐藏回溯)
            # self.traversal(node.left,depth+1)
        if node.right: 
            depth += 1
            self.traversal(node.right,depth)
            depth -= 1
            # 以上三行可以直接写成:(隐藏回溯)
            # self.traversal(node.right,depth+1)

3. 复杂度分析

时间复杂度:O(n)

其中 n 是二叉树的节点数目,需要遍历 n 个节点。

空间复杂度:O(n)

递归栈需要占用 O(n) 的空间。

4. 收获和思考

  1. 本题涉及到求深度,求深度应该从上到下去遍历,在LeetCode110. 平衡二叉树中有详细的讲到;

  2. 递归中其实隐藏了回溯,在LeetCode257. 二叉树的所有路径中讲解了究竟那里使用回溯,如何隐藏回溯;以下代码可以直接精简成一行,因为传入的参数depth+1并没有改变depth本身的值,相当于是调用递归的时候+1了,递归完成之后又-1了;

    if node.left: 
                depth += 1
                self.traversal(node.left,depth)
                depth -= 1
                # 以上三行可以直接写成:(隐藏回溯)
                # self.traversal(node.left,depth+1)
    
  3. 关于遍历顺序,是每道二叉树都要考虑的问题,本题中前中后序都OK,因为本题不涉及到中间节点的处理逻辑,只要左孩子优先于右孩子处理即可,如果二叉树的同一层,已经处理了一个节点之后,后面的节点就不会让curMaxDepth 和result更新了,除非到了更深的层;

  4. 如果在调用递归函数的过程中,需要维护几个全局变量,需要在主函数中定义全局变量,再赋值,递归函数中需要声明全局变量,然后就可以正常操作了;

    class Solution(object):
        def findBottomLeftValue(self, root):
            """
            :type root: TreeNode
            :rtype: int
            """
            # Python定义全局变量的方式
            global result
            result = 0
            global curMaxDepth 
            curMaxDepth = -float("inf")
        def traversal(self,node,depth):
            global result
            global curMaxDepth
    

方法二: 迭代解法

1. 思路

本题使用层序遍历再合适不过了,比递归要好理解的多!只需要记录最后一行第一个节点的数值就可以了,属于是模板题;

2. 代码实现

# 迭代解法
# 层序遍历之后,取最后一层的第一个元素返回即可
# time:o(n);space:O(n)
class Solution(object):
    def findBottomLeftValue(self, root):
        """
        :type root: TreeNode
        :rtype: int
        """
        if root == None: return None
        from collections import deque
        queue = deque([root])
        result = None
        while queue:
            size = len(queue)
            for i in range(size):
                cur = queue.popleft()
                # 用每一层的第一个元素迭代result
                if i == 0:
                    result = cur.val
                if cur.left: queue.append(cur.left)
                if cur.right: queue.append(cur.right)
        return result

3. 复杂度分析

时间复杂度:O(n)

其中 n 是二叉树的节点数目,每个节点几乎都需要遍历一次的;

空间复杂度:O(n)

如果二叉树是满完全二叉树,那么队列 queue 最多保存 n/2 个节点。

4. 收获与思考

  1. 本题的递归方法偏难,但是迭代简单,属于模板题;
  2. 做模板题的重点是,理解题意要求,然后在模板的遍历过程中作相应的处理;

Reference:

  1. 力扣
  2. 代码随想录 (programmercarl.com)

本题学习时间:80分钟。


LeetCode112. 路径总和

链接: 112. 路径总和 题解 - 力扣(LeetCode)

 113. 路径总和 II 题解 - 力扣(LeetCode)

方法一:递归解法

1. 思路

这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和,理解题意之后,首先第一个问题是遍历顺序是什么?

遍历顺序是什么?

可以使用深度优先遍历的方式来遍历二叉树,本题前中后序都可以,无所谓,因为中节点也没有处理逻辑。

2. 代码实现

  1. 确定递归函数的参数和返回类型

参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。

细节: 计数器count的取值

这里将计数器count直接赋值为target的值,遍历到一个node则减去value,最后判断是否为0即可;而直观的想法是计数器初始为0,遍历到一个node则+value,再看最后是否==value,这样也是可以的,但是代码更复杂一点。

再来看返回值,本题我们要找一条符合条件的路径,所以递归函数需要返回值,及时返回,那么返回类型是什么呢?

图中可以看出,遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。

所以代码如下:

class solution:
    def haspathsum(self, root: treenode, targetsum: int) -> bool:
				if root == none:
            return false  # 别忘记处理空treenode
        else:
            return isornot(root, targetsum - root.val)
			  def isornot(root, targetsum) -> bool:
  • 确定终止条件

碰到了叶子节点,就终止,此时判断count如果为0,返回True,否则false

递归终止条件代码如下:

def isornot(root, targetsum) -> bool:
  if (not root.left) and (not root.right) and targetsum == 0:
      return true  # 遇到叶子节点,并且计数为0
  if (not root.left) and (not root.right):
      return false  # 遇到叶子节点,计数不为0
  • 确定单层递归的逻辑

因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了;递归函数是有返回值的,如果递归函数返回true,说明找到了合适的路径,应该立刻返回,此时不能直接写 isornot(root.left, targetsum);因为这个递归函数是有返回值的,要判断返回值,处理返回值,再一层层向上返回,这样才能一层层返回给根节点。

if root.left:
    targetsum -= root.left.val  # 左节点
    if isornot(root.left, targetsum): return true  # 递归,处理左节点
    targetsum += root.left.val  # 回溯
		# 以上三行代码可以精简为以下一行
		# return isornot(root.left, targetsum-root.left.val)
if root.right:
    targetsum -= root.right.val  # 右节点
    if isornot(root.right, targetsum): return true  # 递归,处理右节点
    targetsum += root.right.val  # 回溯
		# 以上三行代码可以精简为以下一行
		# return isornot(root.right, targetsum-root.right.val)

以上的代码把回溯的细节清晰的展现出来了,熟练之后,也可以直接写注释里面的隐藏版。

总体代码如下:

# 递归解法
# time:O(N);space:O(H)
class Solution(object):
    def hasPathSum(self, root, targetSum):
        """
        :type root: TreeNode
        :type targetSum: int
        :rtype: bool
        """
        if root == None: return False # 处理空节点
        # 这里要记得把root自己的value减掉
        return self.hasPathSumHelper(root,targetSum-root.val)

    def hasPathSumHelper(self,node,targetSum):
        # base case 遇到叶子节点
        if node.left == None and node.right == None:
            if targetSum == 0: return True 
            else: return False
        # 处理单层递归逻辑
        if node.left: # 避免空节点进入递归,左
            targetSum -= node.left.val
            # 这里不可以直接写 return self.hasPathSumHelper(node.left,targetSum)
            # 因为只有找到了的时候,可以直接return true ,不需要遍历其他的路径了
            # 这里如果没找到,return false了,应该继续遍历其他路径
            if(self.hasPathSumHelper(node.left,targetSum)): return True
            targetSum += node.left.val
            # 以上3行代码可以直接隐藏回溯过程,精简为如下代码
            # if self.hasPathSumHelper(node.left,targetSum-node.left.val)): return True
        if node.right:
            targetSum -= node.right.val
          # 这里和上面同理,不可以直接写return self.hasPathSumHelper(node.right,targetSum)
            if (self.hasPathSumHelper(node.right,targetSum)): return True
            targetSum += node.right.val
          # 以上3行代码可以直接隐藏回溯过程,精简为如下代码
          # if (self.hasPathSumHelper(node.right,targetSum-node.right.val)): return True
        # 如果全部遍历完了一直没有返回true,那就是没有这样的路径,返回false
        return False

3. 复杂度分析

时间复杂度:O(N)

其中 N是树的节点数,对每个节点访问一次。

空间复杂度:O(H)

其中 H 是树的高度,空间复杂度主要取决于递归时栈空间的开销,最坏情况下,树呈现链状,空间复杂度为 O(N),平均情况下树的高度与节点数的对数正相关,空间复杂度为 O(logN)。

4. 思考与收获

  1. 本题可以写成超级精简的代码,如下:
class Solution(object):
    def hasPathSum(self, root, targetSum):
        if root == None: return False
        # base case 终止条件
        if root.left == None and root.right == None 
				   and targetSum == root.val:
            return True
        return self.hasPathSum(root.left,targetSum-root.val) 
								or self.hasPathSum(root.right,targetSum-root.val)

是不是发现精简之后的代码,已经完全看不出分析的过程了,所以我们要把题目分析清楚之后,在追求代码精简。

方法二:迭代解法

1. 思路

如果使用栈模拟递归的话,那么如果做回溯呢?**此时栈里一个元素不仅要记录该节点指针,还要记录从头结点到该节点的路径数值总和。**c++就我们用pair结构来存放这个栈里的元素。定义为:pair<TreeNode*, int> pair<节点指针,路径数值>这个为栈里的一个元素。

2. 代码实现

如下代码是使用栈模拟的层序遍历,如下:

# 迭代做法 层序遍历
# time:O(N);space:o(N)
class Solution(object):
    def hasPathSum(self, root, targetSum):
        """
        :type root: TreeNode
        :type targetSum: int
        :rtype: bool
        """
        if root == None: return False
        # 此时栈中放的是pair<节点指针,路径数值>
        stack = [(root,root.val)]
        while stack:
            node,curSum = stack.pop() 
            # 如果该节点为叶子节点,同时该路径数值等于Sum,则返回True
            if node.left == None and node.right == None and curSum == targetSum:
                return True
            # 前序遍历 先放右再放左
            # 右节点压入的时候,节点的路径数值也记录下来
            if node.right:
                stack.append((node.left,curSum+node.right.val))
            # 左节点压入的时候,节点的路径数值也记录下来
            if node.left:
                stack.append((node.left,curSum+node.left.val))
        return False

3. 复杂度分析

时间复杂度:O(N)

其中 N 是树的节点数。对每个节点访问一次;

空间复杂度:O(N)

其中 N 是树的节点数。空间复杂度主要取决于栈的开销,栈中的元素个数不会超过树的节点数。

4. 思考与收获

  1. 本题用迭代做法的话实际上是二叉树层序遍历的模板题,路径的个数其实就是整个树中叶子节点的个数,每个叶子节点都对应一条到根节点的路径,所以pop出节点pair的时候,只有叶子节点会判断其curSum值,无所谓二叉树的遍历顺序,只要每个叶子节点拿出来判断一下即可;
  2. 本题代码实现的数据结构用的是栈,实际上用队列也是一样的;

Reference:

  1. 力扣
  2. 力扣

5. 拓展

LeetCode113 路径总和II

LeetCode113 链接:113. 路径总和 II - 力扣(LeetCode)

1. 思路

113.路径总和ii要遍历整个树,找到所有路径,所以递归函数不要返回值!

2. 代码实现

# 递归解法
# time:O(N ^2);space:O(N)
class Solution(object):
    def pathSum(self, root, targetSum):
        """
        :type root: TreeNode
        :type targetSum: int
        :rtype: List[List[int]]
        """
        # 递归函数不需要返回值,因为我们要遍历整个树
        def traversal(node,remain):
            # base case
            # 遇到了叶子节点
            if node.left == None and node.right == None:
                # 找到了路径,则加进result里面去
                if remain == 0:
                    result.append(path[:])
                # 到了叶子节点但是不是我们要的路径,直接return
                return 
            # left 
            if node.left: 
                remain -= node.left.val
                path.append(node.left.val)
                traversal(node.left,remain) # 递归
                remain += node.left.val # 回溯
                path.pop() # 回溯
            # right
            if node.right:
                remain -= node.right.val
                path.append(node.right.val)
                traversal(node.right,remain) # 递归
                remain += node.right.val # 回溯
                path.pop() # 回溯
        path = []
        result = []
        if root == None: return result
        # 需要先把根节点加进去
        path.append(root.val)
        traversal(root,targetSum-root.val)
        return result

3. 复杂度分析

时间复杂度:O(N^2)

其中 N 是树的节点数。在最坏情况下,树的上半部分为链状,下半部分为完全二叉树,并且从根节点到每一个叶子节点的路径都符合题目要求。此时,路径的数目为 O(N),并且每一条路径的节点个数也为 O(N),因此要将这些路径全部添加进答案中,时间复杂度为 O(N^2);

空间复杂度:O(N)

其中 N是树的节点数。空间复杂度主要取决于栈空间的开销,栈中的元素个数不会超过树的节点数。

4. 思考与收获

  1. 迭代写法: 用第二个队列保存目前的总和与路径
# time:O(N);space:O(N)
class Solution:
    def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
        if not root:
            return []
        que, temp = deque([root]), deque([(root.val, [root.val])])
        result = []
        while que:
            for _ in range(len(que)):
                node = que.popleft()
                value, path = temp.popleft()
                if (not node.left) and (not node.right):
                    if value == targetSum:
                        result.append(path)
                if node.left:
                    que.append(node.left)
                    temp.append((node.left.val+value, path+[node.left.val]))
                if node.right:
                    que.append(node.right)
                    temp.append((node.right.val+value, path+[node.right.val]))
        return result

2. 相信很多同学都会疑惑,递归函数什么时候要有返回值,什么时候没有返回值,特别是有的时候递归函数返回类型为bool类型。

通过以上详细讲解的如下两道题,来回答这个问题:

再来看返回值,递归函数什么时候需要返回值?什么时候不需要返回值?这里总结如下三点:

  • 112.路径总和
  • 113.路径总和ii
  • 如果需要搜索整棵二叉树且不用处理递归返回值,递归函数就不要返回值。(这种情况就是本文下半部分介绍的113.路径总和ii)
  • 如果需要搜索整棵二叉树且需要处理递归返回值,递归函数就需要返回值。 (这种情况我们在**236. 二叉树的最近公共祖先 (opens new window)**中介绍)
  • 如果要搜索其中一条符合条件的路径,那么递归一定需要返回值,因为遇到符合条件的路径了就要及时返回。(本题的情况)

Reference:

  1. 力扣
  2. 代码随想录 (programmercarl.com)

本题学习时间:150分钟。


LeetCode106. 从后序和中序遍历序列构造二叉树

leetcode105. 从前序和中序遍历序列构造二叉树

leetcode106链接:106. 从中序与后序遍历序列构造二叉树 - 力扣(LeetCode)

LeetCode105. 链接:105. 从前序与中序遍历序列构造二叉树 - 力扣(LeetCode)

1. 思路

首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。如果让我们肉眼看两个序列,画一棵二叉树的话,应该分分钟都可以画出来。流程如图:

 那么代码应该怎么写呢?

说到一层一层切割,就应该想到了递归。

来看一下一共分几步:

  • 第一步:如果数组大小为零的话,说明是空节点了。
  • 第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
  • 第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
  • 第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
  • 第五步:切割后序数组,切成后序左数组和后序右数组
  • 第六步:递归处理左区间和右区间

2. 代码实现

不难写出如下代码:(先把框架写出来)

class Solution:
    def buildTree(self, inorder: List[int], postorder: List[int]) -> TreeNode:
        # 第一步: 特殊情况讨论: 树为空. (递归终止条件)
        if not postorder: 
            return None

        # 第二步: 后序遍历的最后一个就是当前的中间节点. 
        root_val = postorder[-1]
        root = TreeNode(root_val)

        # 第三步: 找切割点. 
        separator_idx = inorder.index(root_val)

        # 第四步: 切割inorder数组. 得到inorder数组的左,右半边. 
        # 第五步: 切割postorder数组. 得到postorder数组的左,右半边.
        

        # 第六步: 递归
        root.left = self.buildTree(inorder_left, postorder_left)
        root.right = self.buildTree(inorder_right, postorder_right)

        return root

难点大家应该发现了,就是如何切割,以及边界值找不好很容易乱套。

此时应该注意确定切割的标准,是左闭右开,还有左开右闭,还是左闭右闭,这个就是不变量,要在递归中保持这个不变量。在切割的过程中会产生四个区间,把握不好不变量的话,一会左闭右开,一会左闭右闭,必然乱套!

在数组的题目中都强调过循环不变量的重要性,在二分查找以及螺旋矩阵的求解中,坚持循环不变量非常重要,本题也是。

首先要切割中序数组,为什么先切割中序数组呢?

切割点在后序数组的最后一个元素,就是用这个元素来切割中序数组的,所以必要先切割中序数组。中序数组相对比较好切,找到切割点(后序数组的最后一个元素)在中序数组的位置,然后切割,如下代码中我坚持左闭右开的原则:

# 第三步: 找切割点. 
        separator_idx = inorder.index(root_val)

# 第四步: 切割inorder数组. 得到inorder数组的左,右半边. 
inorder_left = inorder[:separator_idx]
inorder_right = inorder[separator_idx + 1:]

接下来就要切割后序数组了。

首先后序数组的最后一个元素指定不能要了,这是切割点 也是 当前二叉树中间节点的元素,已经用了。后序数组的切割点怎么找?后序数组没有明确的切割元素来进行左右切割,不像中序数组有明确的切割点,切割点左右分开就可以了。

此时有一个很重要的点,就是中序数组大小一定是和后序数组的大小相同的(这是必然)。

中序数组我们都切成了左中序数组和右中序数组了,那么后序数组就可以按照左中序数组的大小来切割,切成左后序数组和右后序数组。

代码如下:


# 第五步: 切割postorder数组. 得到postorder数组的左,右半边.
# ⭐️ 重点1: 中序数组大小一定跟后序数组大小是相同的. 
postorder_left = postorder[:len(inorder_left)]
postorder_right = postorder[len(inorder_left): len(postorder) - 1]

递归逻辑

此时,中序数组切成了左中序数组和右中序数组,后序数组切割成左后序数组和右后序数组。接下来可以递归了,代码如下:

 # 第六步: 递归
root.left = self.buildTree(inorder_left, postorder_left)
root.right = self.buildTree(inorder_right, postorder_right)

Python完整代码如下;左闭右开区间 [x ,y )

# time:O(N^2);space:O(N)
class Solution(object):
    def buildTree(self, inorder, postorder):
        """
        :type inorder: List[int]
        :type postorder: List[int]
        :rtype: TreeNode
        """
        # 第一步,base case ,结束条件是中序和后序数组为空了
        if len(inorder) == 0: return None 
        # 第二步,找到并定义根节点,根节点是后序数组的最后一个元素
        rootValue = postorder[-1]
        root = TreeNode()
        root.val = rootValue
        # 多加一个返回条件,如果len(inorder) == 1,直接返回根节点即可
        if len(inorder) == 1: return root
        # 第三步 找到中序数组中切割点的位置
        # 记住Python中的这种写法
        cutPointIndex = inorder.index(rootValue)
        # 第四步,切割中序数组
        leftInorder = inorder[:cutPointIndex]
        rightInorder = inorder[cutPointIndex+1:]
        # 第五步 切割后序数组
        leftPostorder = postorder[:len(leftInorder)]
        rightPostorder = postorder[len(leftInorder):len(postorder)-1]
        # 第六步 递归
        root.left = self.buildTree(leftInorder,leftPostorder)
        root.right = self.buildTree(rightInorder,rightPostorder)
        return root

3. 复杂度分析

时间复杂度:O(n^2)

其中 n 是树中的节点个数,在最坏的情况下,树为链式的,需要递归N层,每次递归函数中,都有复杂度为O(N)的切片操作,总和的最差复杂度为O(N^2);

空间复杂度:O(n)

需要 O(h) 的空间表示递归时栈空间,还有几个数组变量储存切片的数组,总的空间复杂度为O(N);总体来说空间复杂度为O(N)

4. 思考与收获

  1. 这里又提到了区间不变量,如果不一开始就想好,定义好的话,后面的递归会非常乱;

  2. 上述代码的性能并不好,因为每层递归定义了新的数组,数组切片的复杂度为O(N),既耗时间又耗空间,但是以上代码很好理解,步骤清晰,实际上,我们还可以用下标分割数组,不用重新定义数组切片,效率更高,代码如下:

    # 递归方法,用下标切割数组
    # time:O(N);space:O(N)
    class Solution(object):
        def buildTree(self, inorder, postorder):
            """
            :type inorder: List[int]
            :type postorder: List[int]
            :rtype: TreeNode
            """
            def traversal(inorder,inorderBegin,inorderEnd,postorder,
    											postorderBegin,postorderEnd):
                # 第一步 结束条件
                if inorderBegin == inorderEnd : return None 
                # 第二步,找到并定义根节点,根节点是后序数组的最后一个元素
                rootValue = postorder[postorderEnd-1]
                root = TreeNode()
                root.val = rootValue
                # 多加一个返回条件
                if inorderEnd - inorderBegin == 1: return root
                # 第三步 找到中序数组中切割点的位置
                # 记住Python中的这种写法
                cutPointIndex = inorder.index(rootValue)
                # 第四步 切割中序数组 左闭右开 [inorderBegin,inorderEnd)
                leftInorderBegin = inorderBegin
                leftInorderEnd = cutPointIndex
                rightInorderBegin = cutPointIndex+1
                rightInorderEnd = inorderEnd
                # 第五步 切割后序数组
                leftPostorderBegin = postorderBegin
                    # 加上中序区间的长度
                leftPostorderEnd = postorderBegin + (cutPointIndex-inorderBegin)
                rightPostorderBegin = postorderBegin + (cutPointIndex-inorderBegin)
                rightPostorderEnd = postorderEnd -1
                # 第六步 递归
                root.left = traversal(inorder,leftInorderBegin,leftInorderEnd,
    																	postorder,leftPostorderBegin,leftPostorderEnd)
                root.right = traversal(inorder,rightInorderBegin,rightInorderEnd,
    																	postorder,rightPostorderBegin,rightPostorderEnd)
                return root
            if len(inorder) == 0: return None 
            return traversal(inorder,0,len(inorder),postorder,0,len(postorder))
    
  3. 既然中序和后序可以构造二叉树,那么前序和中序呢? 前序和后序呢?答案是前序和中序可以,但是前序和后序不行,先解释为什么不行?前序是 中左右,后序是 左右中,没有中序遍历就无法确定左右区间,即无法确定切割点;

    Example:

    tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。

    tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。

    那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树!所以前序和后序不能唯一确定一棵二叉树

  4.  

  5. 相信大家自己就算是思路清晰, 代码写出来一定是各种问题,所以一定要加日志来调试,看看是不是按照自己思路来切割的,不要大脑模拟,那样越想越糊涂;

  6. 给出前序和中序构造二叉树的代码:leetcode105

    # 递归解法
    class Solution(object):
        def buildTree(self, preorder, inorder):
            """
            :type preorder: List[int]
            :type inorder: List[int]
            :rtype: TreeNode
            """
            # 第一步 结束条件
            if len(preorder) == 0: return None 
            # 第二步 创建根节点
            rootValue= preorder[0]
            root = TreeNode()
            root.val = rootValue
             # 再加一个判断条件
            if len(preorder) == 1: return root
            # 第三步 寻找中序遍历切割点的位置
            cutPointIndex = inorder.index(rootValue)
            # 第四步 切割中序数组
            leftInorder = inorder[:cutPointIndex]
            rightInorder = inorder[cutPointIndex+1:]
            # 第五步 切割前序数组
            leftPreorder = preorder[1:len(leftInorder)+1]
            rightPreorder = preorder[1+len(leftPreorder):]
            # 第六步 递归
            root.left = self.buildTree(leftPreorder,leftInorder)
            root.right = self.buildTree(rightPreorder,rightInorder)
            return root
    
  7. Python可以直接根据值找到在数组中它的index:

    cutPointIndex = inorder.index(rootValue)
    

Reference: 代码随想录 (programmercarl.com)

本题学习时间: 130分钟。


本篇学习所用时间为6小时,总字数约15000字;三类题目,找树左下角的值,要正确理解题意;路径总和中用到了回溯;构造二叉树要体会其中的递归思想。(求推荐!)

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值