leetcode 450: 删除二叉搜索树中的节点

leetcode450:删除二叉搜索树中的节点


第二周的刷题博客开始了,这是第一篇。
首先总结一下,发现前面写的几篇还是不够思路清晰,总结不够,但是凡事总有个过程,慢慢来,慢慢进步。就像每天学半小时英语口语听力一样,现在已经比刚开始的时候进步很多了,很多句子听一次都大概能懂了,除非有一些生词,这也是坚持了近60天的成果。写博客的目的有两点,一是梳理思路,比较各种方法的思路与优缺点,二是方便自己以后回顾,减少复习时间。

聊完天了,说说这道题,这道题我没做出来,是看了题解才写出来的,所以很有必要记录一下。看了一下难度中等,通过率是36.9%(截止12.23日),算是挺低通过率的一题,所以难度还是有的。

题目描述

给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。

一般来说,删除节点可分为两个步骤:

首先找到需要删除的节点;
如果找到了,删除它.

说明: 要求算法时间复杂度为 O(h),h 为树的高度。

Given a root node reference of a BST and a key, delete the node with the given key in the BST. Return the root node reference (possibly updated) of the BST.

Basically, the deletion can be divided into two stages:

Search for a node to remove.
If the node is found, delete the node.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/delete-node-in-a-bst
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

解法

我最初的思路

首先肯定得找到要删除的目标节点吧,所以考虑的是中序遍历BST,找到目标节点就进行下一步,如果遍历完了都没找到,就直接返回根节点。但是肯定不能直接删除了就完了,得先记录目标节点的根节点吧,所以又考虑用一个Pre存储它的根节点,之后再重新拼接树。

想得挺好,但是实现起来好难哦…我还是太菜了。主要有以下几个难点:

  1. pre节点的记录也有特殊节点,当pre为空的时候,要区别是因为根节点就是目标 节点,还是因为再整棵树理都找不到目标节点。
  2. 遍历完的时候stack是空的,这时候不知道怎么区别是找不到目标节点的情况还是最后一个节点是目标节点的情况(比如树的结构为[1,2,3],目标节点为3).
  3. 后面怎么重新拼接树也没想好。

所以尝试了几次后,就放弃,去看题解了…

官方题解

leetcode的官方题解写得很详细了,这里就概括一遍思路,回顾一下我上面没解决的问题,它是怎么解决的。
首先是利用一个递归函数,每次排除1/2子树的节点,跟我上面的中序遍历相比,大大提高了效率。
递归函数,有两个要点要理解,一个是递归函数的作用,二是它返回的结果是什么。这道题里,这个递归函数的作用就是 删除一棵树里的目标节点,返回的是这棵修改后的树的根节点root。

递归过程:
deleteNode(root, key)

  1. 如果根节点的值大于目标节点key的值,说明key在左子树,所以递归调用(root.left, key)。key在左子树,也就说明 只用修改左子树就行了,右子树不用动,所以可以使 root.left = deleteNode(root.left, key).
  2. 如果根节点的值小于目标节点key的值,说明key在右子树,所以递归调用(root.right, key)。同理,rot.rght = deleteNode(root.right, key).
  3. 如果根节点的值等于目标节点key的值,说明找到key了,开始进行下一步处理。
    (启示:说到 二叉搜索树BST时,不仅要想到中序遍历的结果是排好序的,还要想到可以递归,有点像二分查找的模式寻找目标值,提高效率)

删除节点:
经过上一步的递归过程,找到了key,而且key是要调整的这个子树的根节点。

(思考1:竟然不用存储pre节点,是怎么做到连接两个部分的树的?)
当遍历到这个节点时,其实变量名只是起到一个指针的作用,直接修改它的值就行,直接令它的值等于它的继承节点的值。

调整子树:
这部分用到两个子函数:

def successor(root): # 代表中序遍历序列的后一个节点,即比当前节点大的最小节点
            root = root.right
            while root.left:
                root = root.left
            return root.val
        
def predecessor(root): # 代表中序遍历序列的前一个节点,即比当前节点小的最大节点
            root = root.left
            while root.right:
                root = root.right
            return root.val

要分三种情况:

  1. 整个子树就只有一个节点,也就是根节点是叶节点,这是直接令其等于None就行了。
  2. 根节点有右子树,继承节点就选择 它的后一个节点(比目标节点大的最小节点)。
root.val = successor(root) # 用它的后继节点代替它
root.right = self.deleteNode(root.right, root.val) # 修改右子树
  1. 根节点无右子树但有左子树,继承节点就选择 它的前一个节点(比目标节点小的最大节点)。
root.val = predecessor(root) # 用它的后继节点代替它
root.left= self.deleteNode(root.left, root.val) # 修改右子树

代码如下:

class Solution:
    def deleteNode(self, root: TreeNode, key: int) -> TreeNode:
        if not root:
            return None
        def successor(root): # 代表中序遍历序列的下一个节点,即比当前节点大的最小节点
            root = root.right
            while root.left:
                root = root.left
            return root.val
        
        def predecessor(root):
            root = root.left
            while root.right:
                root = root.right
            return root.val
        
        if key > root.val: # 如果key大于根节点,则从右子树寻找
            root.right = self.deleteNode(root.right, key)
        elif key < root.val: # 如果key小于根节点,则从左子树寻找
            root.left = self.deleteNode(root.left, key)
        else: # key == 根节点
            if not root.left and not root.right: # 如果根节点是叶节点,则直接删除该节点
                return  None
            if root.right: # 如果该节点有右节点,则要找比它大的下一个节点(后继节点)
                root.val = successor(root) # 用它的后继节点代替它
                root.right = self.deleteNode(root.right, root.val)
            else:
                root.val = predecessor(root)
                root.left = self.deleteNode(root.left, root.val)
        return root

运行结果:
450
这道题的递归用得很妙啊,以及复杂问题分情况讨论,之后也要更耐住性子分析呀~

写了3000多字…废话真多,明天起要开始写专利了…写完才能放假回家😭

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值