897.递增顺序搜索树 递归思考

897. 递增顺序搜索树

题目:

给你一棵二叉搜索树,请你 按中序遍历 将其重新排列为一棵递增顺序搜索树,使树中最左边的结点成为树的根结点,并且每个结点没有左子结点,只有一个右子结点。
github题解仓库

解法一:

思路:

函数最终要返回该二叉搜索树最左边的结点,也是以该结点为根结点的递增顺序搜索树,所以设计的递归函数的返回值应该是所给搜索树的最左边的结点ans。中序遍历中引入pos(后继)这个参数可以很方便的处理前后继之间的关系,pos参数表示当前结点在中序遍历中的后继结点。(突然发现这与线索树的建立有异曲同工之妙啊啊啊!(咦,这不就是在考察遍历时前后继之间的关系吗!所以,就得引入后继这个参数!

class Solution{
public:
	TreeNode* increasingBST(TreeNode* root, TreeNode* pos = nullptr) {
        if(!root) return pos;
        TreeNode* ans = incresaingBST(root->left, root);//当前结点左子树的后继结点就是该结点本身
        root->right = increasingBST(root->right, pos);//pos是中序遍历下当前结点的后继结点
        							//那它是怎么确定的呢?在此之前某次调用root->left时,赋予的root
        root->left = nullptr;
        return ans;
    }   
}

带你跑:

大体上是左子树进入递归设置好后继结点,当前结点的右指针指向 右子树进入递归调出的后继结点,这两个动作嵌套完成。

在遍历左子树的过程中传入的参数(root->left, root),root为后面的结点确定了的pos,也就是其左子树的最右边结点的后继。

当遇到任一结点T的左孩子为空时,结束判断语句就返回该结点T,而第一次遇到的左孩子为空的结点就是原搜索树最左边的结点,所以函数结尾需返回它,在此之前需声明一个ans变量保存它。

当遇到任一结点T的右孩子为空时,就直接返回它所在子树的后继或者是最后的nullptr

所以root->right = increasingBST(root->right,pos)这段代码既能使当前结点指向其右子树中最左边的结点,还能使其指向祖先结点中的某个后继结点。

总结就是前者铺路,后者搭桥。

解法二:

思路:

反过来按照右、中、左的顺序递归处理该搜索树(咦!突然发现题目要求中序遍历,但这不算,但这种解法值得一见!

class Solution{
public:
    void fun(TreeNode* root, TreeNode* ans) {//ans是始终指向拼接中的递增顺序搜索树的根结点的指针。
        if(!root) return;
        fun(root->right, ans)
        root->rigth = ans;
        ans = root;
        fun(root->left, ans)
        root->left = nullptr;
    }
	TreeNode* increasingBST(TreeNode* root){
        TreeNode* ans = nullptr;
        fun(root, ans);
        return tail;
    }
}

添加默认参数,合并为一个函数的写法:

递归函数返回递增顺序搜索树的根结点,那么赋值时抓住这点来读代码。(刚开始不太好读明白的。慢慢来(:

class Solution{
public:
    TreeNode* increasingBST(TreeNode* root, TreeNode* ans = nullptr) {
		if(!root) return ans;//第一次返回nullptr,之后返回传进来的ans(目标搜索树的根结点)。
        ans = increasingBST(root->right, ans);
        root->right = ans;
        ans = root;
        ans = increasingBST(root->left, ans);//这里注意上面的ans要传入到函数中,而被赋值的ans已改变
        root->left = nullptr;//先处理完左分支后才能设为空指针
        return ans;
    }
}

进一步精简:

函数功能也可以理解为root后面拼接ans

class Solution{
public:
	TreeNode* increasingBST(TreeNode* root, TreeNode* ans = nullptr) {
        if(!root) return ans;
        root->right = increasingBST(root->right, ans);
        ans = increasing(root->left, root);
        root->left = nullptr;
        return ans;
	}   
}
参考:https://leetcode-cn.com/problems/increasing-order-search-tree/solution/c-yi-ci-bian-li-yuan-di-xiu-gai-by-meng-fzstk/

细心读一读还是好理解的。

其他:

看到了个有点怪怪的写法:(其实跟解法二一样(效果),但又不太一样(编码思路?),殊途同归吧

class Solution {
public:
    TreeNode* solve(TreeNode* root, TreeNode* tail) {
        if (!root) return tail;
        auto rTree = solve(root->right, tail);
        auto lTree = solve(root->left, root);
        root->right = rTree;
       	root->left = NULL;
        return lTree;
    }
    TreeNode* increasingBST(TreeNode* root) {
        return solve(root, NULL);
    }
};
//参考:https://leetcode-cn.com/problems/increasing-order-search-tree/solution/c-di-gui-2chong-jie-fa-fei-di-gui-1chong-r2jv/

思考:

最后有些对递归的思考,或者说对递归函数参数的思考,写递归函数时,参数大体有两种用法或者说应用场景:

  • 一种是以引用或者指针的形式进入函数,在函数的作用下来改变传入参数的值,这样的好处是可以减少全局变量的使用,避免造成变量污染。
  • 另一种可以为递归服务(当然也可以是指针或引用的形式),在函数中调用函数本身时可以传入不同含义的参数,达到期望的作用。

以上是对递归函数从应用上概念的划分,在写或者分析递归函数时,除了自上而下思考解决方案,需对各参数有个以上初步的定义和判断,当然这是很基本的,但脑子要清晰。

随后不管是写递归还是分析递归代码,核心就是分析每个参数的含义,有时一个参数会有多个含义,这也是需要注意的,分析参数的含义是真正理解一段递归代码的必要条件,比如树上递归时,该参数在不同结点上的传入浅层有什么含义(比如有时是当前结点的父结点),或者更本质的含义是什么(比如是遍历的后继结点),或者在终止条件语句中,为什么return的是这个变量而不是其他的,他在不同结点、不同分支上的return的都是什么,函数结尾的return又都返回的是什么。

我觉得分析递归函数的首要原则就是事实求是,函数调用中各参数在位置上的对应关系要分清,各参数的含义要分清。在分清或者试图分清以上两点的基础上去理解递归的含义时,虽然不太容易,但对内功的提升是很有帮助的,同时在纸上或者脑子里跑几一遍,能加深感性上的认识。

反问总结:

从整体递归解决方案上,该参数有什么含义?

从各个结点上对该结点的处理上,该参数有什么含义?

各处return在整体意义上、不同结点、不同分支上都返回的是什么?

这里为什么是xxx,为什么不是其他,还可以用什么代替?

能精简吗,或许有更优雅的写法?

最后,用非递归写法试试?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值