LeetCode99. 恢复二叉搜索树:morris遍历100%+99%(17张图超详细的解析!!!!)

本文探讨了一种特殊的二叉搜索树问题,其中两个节点的位置被错误交换。通过分析二叉搜索树的中序遍历特性,我们可以找到并交换这两个错位的节点。文中介绍了两种方法,包括常规的中序遍历和使用Morris遍历来找到并恢复这些节点,重点在于如何在常数空间复杂度内解决这个问题。

https://leetcode-cn.com/problems/recover-binary-search-tree/

题意

给你二叉搜索树的根节点 root ,该树中的两个节点被错误地交换。请在不改变其结构的情况下,恢复这棵树。

进阶:使用 O(n) 空间复杂度的解法很容易实现。你能想出一个只使用常数空间的解决方案吗?
。

输入:root = [1,3,null,null,2]
输出:[3,1,null,null,2]
解释:3 不能是 1 左孩子,因为 3 > 1 。交换 13 使二叉搜索树有效。

在这里插入图片描述

输入:root = [3,1,4,null,null,2]
输出:[2,1,4,null,null,3]
解释:2 不能在 3 的右子树中,因为 2 < 3 。交换 23 使二叉搜索树有效。

题解

这道题目的意思是,一颗二叉搜索树中两个节点的位置被调换了,要我们把他恢复原状。想来也简单,找到被调换的两个节点,将他们的元素值调换一下就解决了,但问题是,题目觉得O(n)的空间复杂度太简单了(把所有元素值找到用数组存起来,就能找到被调换的元素),非要整个O(1)的空间复杂度,所以就麻烦起来了了。那先不管,先看看怎么找被调换的元素。
大家都知道,二叉搜索树的中序遍历是升序,假设一棵二叉搜索树的中序遍历如下所示。
在这里插入图片描述

中序遍历顺序:1 2 3 4 5 6 7

此时我们交换1和4的位置。
在这里插入图片描述

此时中序遍历的结果:4 2 3 1 5 6 7

显然,在中序遍历中1和4的位置交换了。且中序遍历的结果中出现了两个逆序(大的在前,小的在后)
在这里插入图片描述
而两个交换元素显然就是第一个逆序的首个元素4(大的元素交换到前面,一定是第一个逆序的首个元素)和第二个逆序的末位元素1。
所以我们只要根据中序遍历顺序寻找其中的逆序就可以了。当然,还需注意一种情况也就是中序遍历序列中相邻元素的交换,比如,我们将元素2和3交换。

在这里插入图片描述

中序遍历的结果为:1 3 2 4 5 6 7

在这里插入图片描述

此时两个逆序合并成了一个,且交换元素就是逆序中的两个元素。
所以对于一个被交换过后的二叉搜索树,我们不得不遍历完所有的节点(当只找到一个逆序时),或者在找到两个逆序后结束遍历。

方法一:常规中序遍历,只记录逆转元素

该方法使用两个指针指向逆转元素,通过递归的中序遍历寻找逆序,看似完成了O(1)空间复杂度的要求,实际上,不管是使用迭代方法或者递归方法进行中序遍历,都需要O(h)的空间复杂度,因为中序遍历的时候栈的深度取决于二叉搜索树的高度,所以该方法其实并没有满足O(1)空间复杂度的要求。

//常规中序遍历解法,空间复杂度为O(h),h为树的高,中序遍历的时候栈的深度取决于二叉搜索树的高度。
class Solution {
    TreeNode reverOne=null,reverTwo=null;
    TreeNode pre=null;
    int times=0;//记录找到的逆序次数
    public void recoverTree(TreeNode root) {
        if(root==null)return;
        inorderTraversal(root);
        int tmp=reverOne.val;
        reverOne.val=reverTwo.val;
        reverTwo.val=tmp;
    }

    private void inorderTraversal(TreeNode root){
        if(root==null||times==2)return ;//最多2个逆序
        inorderTraversal(root.left);
        if(pre!=null&&pre.val>root.val){//被替换的后节点
            times++;
            if(null==reverOne)
                reverOne=pre;//记录第一个节点
            if(null!=reverOne)
                reverTwo=root;
        }
        pre=root;//跟新前节点
        inorderTraversal(root.right);
    }
}

在这里插入图片描述

方法二:morris遍历,真正实现O(1)空间复杂度

在递归或者迭代实现的中序遍历中,为什么需要O(h)的空间复杂度?这是因为一些父类节点到达后并不能直接访问,而要等左子树中元素访问后才能进行访问,所以需要将父节点通过栈存储起来(所以有左子树的节点到达后才需要存储起来之后访问),在恰当的时机再进行访问。那么什么才是恰当的时机?以中序遍历为例,当然是左子树访问完的时候。那左子树什么访问完?当然是父节点的前驱节点访问完的时候。既然一个父节点有左子树,那么他的前驱节点一定是左子树中,最右侧的元素(左子树中最大的元素),如下图中10的前驱节点就是8。

在这里插入图片描述
而前驱节点8是10左子树中最右侧的元素,所以一定没有右子节,既然在访问完8之后需要回到10访问,我们干脆利用8空闲的右节点,让它指向10,这样我们就不需要存储10到栈中,直接通过8就可以回到10,如下图所示。

在这里插入图片描述
对于有左子树的节点2和17也是同理,让他们前驱节点的右节点指向他们,如下图所示。

在这里插入图片描述
下面我们可以模拟一下流程,看看到底是怎么回事。

1.先到达根节点10,用一个指针cur(current简写)指向,发现有左子树所以不能立刻访问,所以需要先让它的前驱节点的右节点指向它,以便下次访问,所以从它的左节点开始不断向右找到后继节点8,让8的右节点指向它,这样10的任务就暂时完成了,也不再需要他了,所以进入它的左节点开始访问,即cur=cur.left。这是的结果如图所示,橙色代表当前到达的节点。

在这里插入图片描述

2.此时到达节点2,发现还是有左节点,所以故技重施,让2的前驱节点的右节点指向它,然后进入左子树。

在这里插入图片描述

3.此时进入1后发现没有左子树,所以终于可以安心地访问了(在此将访问过的节点标为灰色),然后再进入右节点,此时也就是节点2。

在这里插入图片描述

4.此时进入2,发现还有左节点,那我怎么知道它的左边有没有访问过呢,只能进入左子树找到前驱节点,看看前驱节点心里有没有他,结果找到前驱节点1后发现节点1的右节点就是它,于是节点2这才放了心,安心地按照中序遍历的顺序,访问自己,然后去往右子树,当然不要忘了修改前驱节点的右节点为空,也就是将回来的线索擦掉,否则二叉树有环LeetCode可不开心了。

在这里插入图片描述

5.进入了6后发现6没有左子树,这可把它开心坏了,终于可以把自己放在第一位了(唉,男人啊,有时候就是得对自己好点),于是乖乖访问自己,然后去往右子树。

在这里插入图片描述

6.进入节点8后,还是保持着乐观的心情,没有左子树,自己还是第一位,又乖乖访问自身,然后去往右节点。

在这里插入图片描述

7.到了节点10,发现有左子树,这心情又直转极下,不过还是故技重施,进入左子树的最右侧,找到前驱节点,发现这死鬼心里还是有他的,便消除线索,访问自身,进入右子树。

在这里插入图片描述

8.到达17,发现有左子树…,这说得俺都有点倦了,不过还是同样的套路,找到前驱节点13,设置线索,然后进入左子树。

在这里插入图片描述

9.到达节点13,没有左子树,访问自身,然后去往右节点。

在这里插入图片描述

10.到达节点17,找到前驱节点13,发现13指向着他,说明左子树访问过了,于是消除线索,访问自身,去往右节点。

在这里插入图片描述

11.到达节点20,发现没有左子树,于是访问自身,然后去往右节点。

在这里插入图片描述

此时节点cur为空,循环也就结束了,节点也已经访问完毕,终于可以上代码了。

morris遍历代码

这里用的节点是自定义的Node< E >,而非题目中的TreeNode。

private void morrisTraversal(Node<E> root){
    if(root==null)return;
    Node<E> cur=root,pre=null;//pre为前驱节点
    while(cur!=null){
        if(cur.left==null){//当没有左节点
            System.out.print(cur.element+" ");//直接访问当前节点
            cur=cur.right;//去往右节点
        }else{//有左节点,则需要先访问左节,并预先设置好回到当前节点的方法
            pre=cur.left;
            //寻找前驱节点
            while(null!=pre.right&&cur!=pre.right){//注意cur!=pre.right不能省略,否则将进入cur的右子树
                pre=pre.right;
            }
            //此时找到的是后继节点(判断是否设置右节点为当前访问节点)
            if(cur!=pre.right){//如果没有完成设置
                //System.out.println("set "+pre.element+" to "+cur.element);//打印线索的设置
                pre.right=cur;//设置前驱节点的右节点指向当前节点
                cur=cur.left;//当前的节点已经不再需要,直接去往左节点
            }else{//否则表明是从左边回来,左边不用再访问
                pre.right=null;//清空线索
                System.out.print(cur.element+" ");//访问自身
                cur=cur.right;//去往右子树
            }
        }
    }
}

99. 恢复二叉搜索树代码

由于使用morris遍历导致二叉树在未遍历完的过程中成环,所以不能在找到2次逆序直接退出,因为遍历没有完成,还会有线索没有清除完,二叉树可能仍然有环。

//morris遍历解法,空间复杂度为O(1)
class Solution {
	//用于记录错位的第一个、第二个节点
    TreeNode reverOne=null,reverTwo=null;
    TreeNode previous=null;//记录上一个节点
    public void recoverTree(TreeNode root) {
        //使用morris遍历
        morrisTraversal(root);
        //交换
        swap(reverOne,reverTwo);
    }

    //morris遍历
    private void morrisTraversal(TreeNode root) {
        TreeNode cur = root, pre = null;
        while (cur != null) {
            if (cur.left == null) {//当没有左节点
                process(cur);//处理当前节点
                cur = cur.right;//去往右节点
            } else {//有左节点,则需要先访问左节,并预先设置好回到当前节点的方法
                pre = cur.left;
                //寻找前驱节点
                //注意cur!=pre.right不能省略,否则将进入cur的右子树
                while (null != pre.right && cur != pre.right)
                    pre = pre.right;
                //此时找到的是后继节点(判断是否设置右节点为当前访问节点)
                if (cur != pre.right) {//如果没有完成设置
                    //System.out.println("set "+pre.element+" to "+cur.element);//打印线索的设置
                    pre.right = cur;//设置前驱节点的右节点指向当前节点
                    cur = cur.left;//当前的节点已经不再需要,直接去往左节点
                } else {//否则表明是从左边回来,左边不用再访问
                    pre.right = null;//清空线索
                    process(cur);//处理当前节点
                    cur = cur.right;//去往右子树
                }
            }
        }
    }
    //记录错位节点
    private void process(TreeNode cur){
        if(previous!=null&&previous.val>cur.val){
            if(reverOne==null)reverOne=previous;
            reverTwo=cur;
        }
        previous=cur;//更新前节点
    }
    //交换节点
    private void swap(TreeNode node1,TreeNode node2){
        int tmp=node1.val;
        node1.val=node2.val;
        node2.val=tmp;
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值