写在题目前的话:
第一次写这个博客以后我发现我理解错了题目,但是我的问题更具有一般性,更复杂,所以文章就不改了
题目:只有两个结点被错误的交换。
我的:有任意多个结点被错误的交换。
题目:
先分析题目:使用O(n)空间复杂度的解法很容易实现,那么我们先看看很容易实现是怎实现的。
算法1:
1、中序遍历二叉树,并将中序序列保存在一个数组Numbers中
2、对Numbesr进行排序
3、中序遍历二叉树,并用Numbers序列按遍历顺序覆盖掉二叉树中的每一个值
此算法即是我想到的空间复杂度为O(n)容易的解法
123的时间复杂度分别为O(N), O(NlogN), O(N),因此上述算法时间复杂度为O(NlogN)
接下来,看一下使用常数空间怎么解决这个问题。
先注意一点
void recoverTree(TreeNode* root)
通过题目给的接口函数可以看出,是值传递。因此通过对原树进行遍历并重新构建一个新树,再把新树根结点赋值给root的方法是不成立的,而且题目说明,树的结构不发生改变,也能够理解到,这个题目是要直接交换树结点的值。
根据之前算法1的启发,其实要做的就是对二叉树进行排序,使得其中序遍历序列为有序序列即可。而中序遍历能够得到什么?
能得到一个按次序访问的结点序列,如果每次访问下一个结点之前,都记录一个之前访问的结点,那么就可以得到一对相邻结点。
然而思考的顺序有些颠倒,设置一个之前访问的结点并不是突发奇想,而在于对排序算法知识的积累。题目核心在于排序,因此需要思考排序算法与此题目的关联性,哪些排序算法能够比较容易套进来?常见的有选择排序,冒泡排序,插入排序,快速排序。快速排序不考虑因为比较复杂,不论是选轴值和从两端遍历都不是那么好实现。选择排序每次需要从第K个位置去更新,也不太舒服。因此冒泡和插入排序是值得考虑的点。虽然是在用排除法,其实最开始就想到了冒泡排序,只是在总结时候强行加了这么一个思考方式,因为冒泡排序是比较相邻元素,中序遍历也比较容易得到一个相邻序列。
算法2:
每次比较相邻的访问结点,并将数值较大的结点往后冒泡,最终就能得到有序的序列,在这个题目中,需要一个改进的冒泡排序,当任何一次冒泡过程没有发生元素交换,则冒泡过程结束,排序完成,否则就需要先统计树的结点数目n,然后再进行n次冒泡,多次一举。
具体算法可以结合代码理解:
class Solution {
public:
void recoverTree(TreeNode* root) {
function<bool(TreeNode*)> sortTree;
TreeNode* pre = NULL;
sortTree = [&sortTree, &pre](TreeNode* root)->bool{
if (!root) { return true; }
bool ok = true;
ok = ok && sortTree(root->left);
if (pre != NULL && root->val < pre->val) {
swap(root->val, pre->val);
ok = false;
}
pre = root;
ok = ok && sortTree(root->right);
return ok;
};
do {
pre = NULL;
}
while (!sortTree(root));
}
};
这里用到了C++的lambda表达式,因为匿名参数的闭包特点,能够将算法封闭在函数内部,所以很优雅。效率仍未考知。
总结下这个题目的知识点:
1、二叉排序树的定义。左子树结点都小于根结点的值,右子树的结点都大于根结点的值,子树也是二叉排序树。
2、二叉排序树可以通过中序遍历得到有序序列,中序序列的逆序列可以得到降序序列。
3、基础排序算法的流程,特点。
4、递归函数的技巧,中序遍历二叉搜索树过程中,得到序列需要一个递归函数外的变量来保持索引过程。因此递归函数并不是完全封闭的。很多递归函数都用到了类似的技巧,通过访问外部全局或成员函数来获得快捷访问,也避免了递归函数参数传递过多的问题,但从封装的角度上来说,这种做法是并不优雅的。
5、*lambda(非必要)
额外的话
虽然通过算法二解决了这个挑战,也能成功AC,但冒泡排序的时间复杂度为O(N^2),并不是非常优秀,通过算法一可以看到,这个题目的时间复杂度是可以达到理论最优的O(N*logN)的,因此该算法也只能说解决了问题,而没有较完美的解决问题,如果空间复杂度能控制在O(1),时间复杂度控制在O(N*logN),那么应该是对这个题目一个完美答卷了。感兴趣的同学可以自己思考思考,是否能做的更好。
来源:CSDN
作者:Moyiii
链接:https://blog.csdn.net/Moyiii/article/details/81354208