算法训练Day21 | LeetCode530.二叉搜索树的最小绝对差(双指针);501. 二叉树搜索树中的众数(双指针+一个技巧);236. 二叉树的最近公共祖先(回溯递归+返回值处理)

目录

LeetCode530.二叉搜索树的最小绝对差

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode501. 二叉树搜索树中的众数

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode236. 二叉树的最近公共祖先

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


LeetCode530.二叉搜索树的最小绝对差

链接: 530. 二叉搜索树的最小绝对差 - 力扣(LeetCode)

1. 思路

题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。注意是二叉搜索树,二叉搜索树可是有序的。

 💡 看到二叉树搜索树,就要立马想到BST的中序遍历是一个有序数组!

遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。所以最直观的想法,就是把二叉搜索树转换成有序数组,然后遍历一遍数组,就统计出来最小差值了,这种思路不再赘述;

以上这种创建数组的思路有点浪费空间储存,我们可以直接在递归的过程中完成计算和处理,不创建数组,用一个pre节点记录上一个节点的数值,用result全局变量记录我们需要的数,其实在二叉搜素树中序遍历的过程中,我们就可以直接计算了;

一些同学不知道在递归中如何记录前一个节点的指针,其实实现起来是很简单的,大家只要看过一次,写过一次,就掌握了;

2. 代码实现

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, val=0, left=None, right=None):
#         self.val = val
#         self.left = left
#         self.right = right
# 递归中序遍历,time:O(N);space:O(height)=O(N)
class Solution(object):
    def getMinimumDifference(self, root):
        global pre
        global result
        pre = None
        result = float("inf")
        self.traversal(root)
        return result

    def traversal(self,node):
        global pre
        global result
        if node == None: return None # base case
        self.traversal(node.left) # left
        if pre != None and node.val-pre.val < result:
            result = node.val -pre.val
        pre = node
        self.traversal(node.right)

3. 复杂度分析

时间复杂度:O(n)

其中 n 为二叉搜索树节点的个数。每个节点在中序遍历中都会被访问一次且只会被访问一次,因此总时间复杂度为 O(n);

空间复杂度:O(n)

递归函数的空间复杂度取决于递归的栈深度,而栈深度在二叉搜索树为一条链的情况下会达到 O(n)级别。

4. 思考与收获

  1. 这里附上递归创建数组的解法代码:

    # time:O(N);space:O(N)
    class Solution:
        def getMinimumDifference(self, root: TreeNode) -> int:
            res = []   
            r = float("inf")
            def buildaList(root):  //把二叉搜索树转换成有序数组
                if not root: return None
                if root.left: buildaList(root.left)  //左
                res.append(root.val)  //中
                if root.right: buildaList(root.right)  //右
                return res
                
            buildaList(root)
            for i in range(len(res)-1):  // 统计有序数组的最小差值
                r = min(abs(res[i]-res[i+1]),r)
            return r
    
  2. 迭代中序遍历的解法

    # time:O(n);space:O(N)
    class Solution(object):
        def getMinimumDifference(self, root):
            cur = root
            stack = []
            result = float("inf")
            prev = None
            while cur or stack:
                if cur:
                    stack.append(cur)
                    cur = cur.left
                else:
                    cur = stack.pop()
                    if prev != None:
                        result = min(result,cur.val-prev.val)
                    prev = cur
                    cur = cur.right
            return result
    
  3. 遇到在二叉搜索树上求什么最值,求差值之类的,都要思考一下二叉搜索树可是有序的,要利用好这一特点;

  4. 同时要学会在递归遍历的过程中如何记录前后两个指针,这也是一个小技巧,学会了还是很受用的;

  5. 关于递归函数的返回值,因为本题需要遍历整个二叉树,而且用全局变量记录我们需要的结果,所以不需要返回值。

Reference:

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

本题学习时间:50分钟。


LeetCode501. 二叉树搜索树中的众数

链接:  501. 二叉搜索树中的众数 - 力扣(LeetCode)

1. 思路

既然是搜索树,它中序遍历就是有序的。如图:

中序遍历代码如下:

def search_BST(self, cur: TreeNode) -> None:
    if not cur: return None
    self.search_BST(cur.left)
    # (处理节点)
    self.search_BST(cur.right)

遍历有序数组的元素出现频率,从头遍历,那么一定是相邻两个元素作比较,然后就把出现频率最高的元素输出就可以了。关键是在有序数组上的话,好搞,在树上怎么搞呢?这就考察对树的操作了。

在上一题中我们就使用了pre指针和cur指针的技巧,这次又用上了。弄一个指针指向前一个节点,这样每次cur(当前节点)才能和pre(前一个节点)作比较。而且初始化的时候pre = NULL,这样当pre为NULL时候,我们就知道这是比较的第一个元素;

代码如下:

 # 第一个节点
if not self.pre:
    self.count = 1
# 与前一个节点数值相同
elif self.pre.val == cur.val:
    self.count += 1 
# 与前一个节点数值不相同
else:
    self.count = 1
self.pre = cur # 更新上一个节点

此时又有问题了,因为要求最大频率的元素集合(注意是集合,不是一个元素,可以有多个众数),如果是数组上大家一般怎么办?

应该是先遍历一遍数组,找出最大频率(maxCount),然后再重新遍历一遍数组把出现频率为maxCount的元素放进集合。(因为众数有多个)这种方式遍历了两遍数组。那么我们遍历两遍二叉搜索树,把众数集合算出来也是可以的。

但这里其实只需要遍历一次就可以找到所有的众数。那么如何只遍历一遍呢?

如果 频率count 等于 maxCount(最大频率),当然要把这个元素加入到结果集中(以下代码为result数组),代码如下:

 if self.count == self.max_count:
            self.result.append(cur.val)

是不是感觉这里有问题,result怎么能轻易就把元素放进去了呢,万一,这个maxCount此时还不是真正最大频率呢。所以下面要做如下操作:

频率count 大于 maxCount的时候,不仅要更新maxCount,而且要清空结果集(以下代码为result数组),因为结果集之前的元素都失效了。

if self.count > self.max_count:
            self.max_count = self.count
            self.result = [cur.val]	
   # 清空self.result,确保result之前的的元素都失效

2. 代码实现

只需要遍历一遍二叉搜索树,就求出了众数的集合

# time:O(N) space:O(N)
# 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
class Solution:
    def __init__(self):
        self.pre = TreeNode()
        self.count = 0
        self.max_count = 0
        self.result = []

    def findMode(self, root: TreeNode) -> List[int]:
        if not root: return None
        self.search_BST(root)
        return self.result
        
    def search_BST(self, cur: TreeNode) -> None:
        if not cur: return None
        self.search_BST(cur.left)
        # 第一个节点
        if not self.pre:
            self.count = 1
        # 与前一个节点数值相同
        elif self.pre.val == cur.val:
            self.count += 1 
        # 与前一个节点数值不相同
        else:
            self.count = 1
        self.pre = cur

        if self.count == self.max_count:
            self.result.append(cur.val)
        
        if self.count > self.max_count:
            self.max_count = self.count
            self.result = [cur.val]	
            # 清空self.result,确保result之前的的元素都失效
        
        self.search_BST(cur.right)

3. 复杂度分析

时间复杂度:O(n)。即遍历这棵树的复杂度。

空间复杂度:O(n)。即递归的栈空间的空间代价。

4. 思考与收获

  1. 如果不是二叉搜索树呢?

    如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合;

    # 当做普通BT,遍历整个树,记录频率,输出其中众数
    # time:O(N) space:O(N)
    class Solution(object):
        def findMode(self, root):
            """
            :type root: TreeNode
            :rtype: List[int]
            """
            record = {}
            def preorderTraversal(node):
                if node == None: return 
                if node.val in record:
                    record[node.val] += 1
                else:
                    record[node.val] = 1
                preorderTraversal(node.left)
                preorderTraversal(node.right)
            preorderTraversal(root)
            maxNum = -float("inf")
            for key in record:
                maxNum = max(maxNum,record[key])
            result = []
            for key in record:
                if record[key] == maxNum:
                    result.append(key)
            return result
    
  2. 迭代写法

    只要把中序遍历转成迭代,中间节点的处理逻辑完全一样;代码如下:

    # time:O(N) space:O(N)
    class Solution:
        def findMode(self, root: TreeNode) -> List[int]:
            stack = []
            cur = root
            pre = None
            maxCount, count = 0, 0
            res = []
            while cur or stack:
                if cur:  # 指针来访问节点,访问到最底层
                    stack.append(cur)
                    cur = cur.left
                else:  # 逐一处理节点
                    cur = stack.pop()
                    if pre == None:  # 第一个节点
                        count = 1
                    elif pre.val == cur.val:  # 与前一个节点数值相同
                        count += 1
                    else:
                        count = 1
                    if count == maxCount:
                        res.append(cur.val)
                    if count > maxCount:
                        maxCount = count
                        res.clear()
                        res.append(cur.val)
    
                    pre = cur
                    cur = cur.right
            return res
    
  3. 在递归遍历二叉搜索树的过程中,我还介绍了一个统计最高出现频率元素集合的技巧, 要不然就要遍历两次二叉搜索树才能把这个最高出现频率元素的集合求出来。为什么没有这个技巧一定要遍历两次呢? 因为要求的是集合,会有多个众数,如果规定只有一个众数,那么就遍历一次稳稳的了。

Reference代码随想录 (programmercarl.com)

本题学习时间:60分钟。


LeetCode236. 二叉树的最近公共祖先

链接:  236. 二叉树的最近公共祖先 - 力扣(LeetCode)

1. 思路

首先思考遍历顺序?

遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。那么二叉树如何可以自底向上查找呢?

回溯啊,二叉树回溯的过程就是从低到上。后序遍历就是天然的回溯过程,最先处理的一定是叶子节点。

接下来就看如何判断一个节点是节点q和节点p的公共公共祖先呢?

情况一:

首先最容易想到的一个情况:如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。

情况二:

但是很多人容易忽略一个情况,就是节点本身p(q),它拥有一个子孙节点q(p)。

使用后序遍历,回溯的过程,就是从低向上遍历节点,一旦发现满足第一种情况的节点,就是最近公共节点了。但是如果p或者q本身就是最近公共祖先呢?

其实只需要找到一个节点是p或者q的时候,直接返回当前节点,无需继续递归子树。如果接下来的遍历中找到了后继节点满足第一种情况则修改返回值为后继节点,否则,继续返回已找到的节点即可。

2. 代码实现

递归三部曲:

  • 确定递归函数返回值以及参数

    需要递归函数返回值,来告诉我们是否找到节点q或者p,那么返回值为bool类型就可以了;但我们还要返回最近公共节点,可以利用上题目中返回值是TreeNode * ,那么如果遇到p或者q,就把q或者p返回,返回值不为空,就说明找到了q或者p;

    class Solution:
        def lowestCommonAncestor(self, root: 
    				'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
    
  • 确定终止条件

    如果找到了 节点p或者q,或者遇到空节点,就返回;代码如下:

    if not root or root == p or root == q:
          return root
    
  • 确定单层递归逻辑

    我们在LeetCode112.路径总和中总结了递归函数什么时候需要返回值,什么时候不需要:这里总结如下三点:

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

    递归函数有返回值就是要遍历某一条边,但有返回值也要看如何处理返回值!如果递归函数有返回值,如何区分要搜索一条边,还是搜索整个树呢?

    搜索一条边的写法:

    if (递归函数(root->left)) return ;
    if (递归函数(root->right)) return ;
    

    搜索整棵树的写法:

    left = 递归函数(root->left);
    right = 递归函数(root->right);
    left与right的逻辑处理;
    

    看出区别了没?

    在递归函数有返回值的情况下:如果要搜索一条边,递归函数返回值不为空的时候,立刻返回,如果搜索整个树,直接用一个变量left、right接住返回值,这个left、right后序还有逻辑处理的需要,也就是后序遍历中处理中间节点的逻辑(也是回溯)

    那么为什么要遍历整棵树呢?直观上来看,找到最近公共祖先,直接一路返回就可以了。

就像图中一样直接返回7,多美滋滋。

但事实上还要遍历根节点右子树(即使此时已经找到了目标节点了),也就是图中的节点4、15、20。因为在如下代码的后序遍历中,如果想利用left和right做逻辑处理, 不能立刻返回,而是要等left与right逻辑处理完之后才能返回;

left = 递归函数(root->left);
right = 递归函数(root->right);
left与right的逻辑处理;

所以此时大家要知道我们要遍历整棵树。知道这一点,对本题就有一定深度的理解了。那么先用left和right接住左子树和右子树的返回值,代码如下:

left = self.lowestCommonAncestor(root.left, p, q)
right = self.lowestCommonAncestor(root.right, p, q)

接下来处理中间节点:

如果left 和 right都不为空,说明此时root就是最近公共节点。这个比较好理解

如果left为空,right不为空,就返回right,说明目标节点是通过right返回的,反之依然。这里有的同学就理解不了了,为什么left为空,right不为空,目标节点通过right返回呢?

图中节点10的左子树返回null,右子树返回目标值7,那么此时节点10的处理逻辑就是把右子树的返回值(最近公共祖先7)返回上去!这里点也很重要,可能刷过这道题目的同学,都不清楚结果究竟是如何从底层一层一层传到头结点的。

那么如果left和right都为空,则返回left或者right都是可以的,也就是返回空。代码如下:

if left and right:  return root
if left != None and right == None: return left
elif left == None and right != None: return right
else: return None

那么寻找最小公共祖先,完整流程图如下:从图中,大家可以看到,我们是如何回溯遍历整棵二叉树,将结果返回给头结点的!

 

完整代码如下:

# Definition for a binary tree node.
# class TreeNode(object):
#     def __init__(self, x):
#         self.val = x
#         self.left = None
#         self.right = None
# 中序递归
# time:O(N);space:O(N)
class Solution(object):
    def lowestCommonAncestor(self, root, p, q):
        """
        :type root: TreeNode
        :type p: TreeNode
        :type q: TreeNode
        :rtype: TreeNode
        """
        # base case
        if root == None: return None
        if root == q or root == p: return root
        # 中序遍历
        left = self.lowestCommonAncestor(root.left,p,q)
        right = self.lowestCommonAncestor(root.right,p,q)
            # 开始处理中间节点
        if left and right: return root
        elif left == None and right != None: return right
        elif left != None and right == None: return left
        else: return None

情况二讨论:以上代码是按照情况的一的思路写的,此时再思考情况二,发现情况二的处理也已经包括在以上代码内了;分析如下:其实只需要找到一个节点是p或者q的时候,直接返回当前节点,无需继续递归子树。如果接下来的遍历中找到了后继节点满足第一种情况则修改返回值为后继节点,否则,继续返回已找到的节点即可。

比如遍历到1的时候,返回1,遍历到2,返回null,此时返回给4的是left=1,right=None,现在遍历到4,因为4 == q,满足边界条件,直接向上返回4了,最后返回的结果是4。

稍加精简,代码如下:

# time:O(N);space:O(N)
class Solution:
    def lowestCommonAncestor(self, root: 
				'TreeNode', p: 'TreeNode', q: 'TreeNode') -> 'TreeNode':
        if not root or root == p or root == q:
            return root
        left = self.lowestCommonAncestor(root.left, p, q)
        right = self.lowestCommonAncestor(root.right, p, q)
        if left and right:   return root
        if left:   return left
        return right

3. 复杂度分析

时间复杂度:O(N)

其中 N 是二叉树的节点数。二叉树的所有节点有且只会被访问一次,因此时间复杂度为 O(N);

空间复杂度:O(N)

其中 N 是二叉树的节点数。递归调用的栈深度取决于二叉树的高度,二叉树最坏情况下为一条链,此时高度为 N,因此空间复杂度为 O(N)。

4. 思考与收获

  1. 这道题目刷过的同学未必真正了解这里面回溯的过程,以及结果是如何一层一层传上去的;给大家归纳如下三点:
  2. 求最小公共祖先,需要从底向上遍历,那么二叉树,只能通过后序遍历(即:回溯)实现从低向上的遍历方式;
  3. 在回溯的过程中,必然要遍历整棵二叉树,即使已经找到结果了,依然要把其他节点遍历完,因为要使用递归函数的返回值(也就是代码中的left和right)做逻辑判断;
  4. 要理解如果返回值left为空,right不为空为什么要返回right,为什么可以用返回right传给上一层结果;
  5. 可以说这里每一步,都是有难度的,都需要对二叉树,递归和回溯有一定的理解;
  6. 本题没有给出迭代法,因为迭代法不适合模拟回溯的过程。理解递归的解法就够了;
  7. 情况二的处理是容易忽略的地方,虽然考虑之后代码仍然不变,但是想到这里才是全面的考虑了所有情况;

Reference:

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

本题学习时间:80分钟。


本篇学习所花时间3个多小时,总结字数约9000字;BST的最小绝对差和BST中的众数中学习到了BST中双指针的操作方法;二叉树的最近公共祖先是一道比较难理解的题,其中的回溯过程和返回值总结很值得思考!(求推荐)

  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值