随便写写
这周末一共是做了5个题目,开始的时候是选了一个排序和一个链表的问题试试手,都是medium难度的,然后感觉并不是很难吧,就开始选作hard难度的题目。
emmm,难度明显有上升,这三道题分别是- Recover Binary Search Tree(树)
- Median of Two Sorted Arrays(数组)
- Redundant Connection II(图)
个人觉得Recover Binary Search Tree最有意思(学到了不使用递归和栈的遍历方法),同时也是比较有难度,所以下面主要讨论它的解题方法和复杂度分析
正文
- 题目描述
问题分析
题目大概意思是:一个二叉搜索树(已经是排好序的了)中的两个元素发生了错位,我们需要找出这两个错位的元素并且恢复二叉树为正确的形式。解题思路分析
很显然,只要找出发生错位的两个元素问题就解决了,自己先后想到如下两种方法:- 一开始想到的方法是直接在搜索树中进行递归,并在每一层中比较根节点和子节点大小关系然后判断出发生错误的节点,但是这样做非常麻烦。
- 另一个很简单清晰的做法是直接对搜索树进行递归的中序遍历(这个很简单,数据结构课程学过),所得到的一列数据本该是有序的,但是其中两个元素发生了交换,在这样的一列排好序的数据中,发生了错误的元素很容易被找到 —— 前后元素互换导致有一个元素比左右两个元素都小,另一个元素则比左右两个元素都大,然后拿到这两个元素的指针将他们的节点值进行交换即可。这个非常朴素的想法不知道为什么自己在一开始没想到orz。
算法步骤
- 对二叉树进行一次递归中序遍历,将需要输出的元素以指针的形式放到一个指针数组里面(排好序);
- 然后遍历指针数组比较数组中每一个元素的value与前后元素value之间的关系是否符合“前 < 本身 < 后”;
- 找出不符合的并将这两个不符合的进行value交换。
复杂度分析
空间复杂度是O(n)首先使用了递归,这是O(n),然后还使用了n个指针作为队列存放二叉树节点,也是O(n),所以总计是O(n)的空间复杂度
时间复杂度上主要是一个遍历和一个单层for循环,递归遍历是O(n)时间复杂度,for循环也是,所以总计O(n)的时间复杂度代码实现:
class Solution { public: void myTraverse( TreeNode* root, vector<TreeNode*>& list ) { if (root == NULL) return; myTraverse(root->left, list); list.push_back(root); myTraverse(root->right, list); } void recoverTree(TreeNode* root) { vector<TreeNode*> list; myTraverse(root, list); TreeNode * ex1 = NULL; TreeNode * ex2 = NULL; for ( auto i = list.begin(); i != list.end(); i++ ) { if ( i == list.begin() ) { if ( (i+1) != list.end() && (*i)->val > (*(i+1))->val ) { ex1 = *i; } } else if ( i+1 == list.end() ) { if ( i != list.begin() && (*i)->val < (*(i-1))->val ) { ex2 = *i; } } else { if ((*i)->val > (*(i+1))->val && (*i)->val > (*(i-1))->val && (*(i-1))->val < (*(i+1))->val ) { ex1 = *i; } else if ((*i)->val < (*(i-1))->val && (*i)->val < (*(i+1))->val && (*(i-1))->val < (*(i+1))->val ) { ex2 = *i; } } } int temp = ex1->val; ex1->val = ex2->val; ex2->val = temp; } };
另一种解法
这个题目做到上面本来可以算已经是结束了,但是题目的follow up里面提出了:使用O(n)空间复杂度方法做这个题是最直接的,但是还有更好的办法,仅仅使用O(1)的复杂度就能完成。怎么办呢,自己想了一段时间没想出来就去看讨论区了,讨论区给出的O(1)空间复杂度方法基本上都是用了Morris Traversal方法遍历二叉树(非递归,不用栈,O(1)空间)来做这个题。
因为之前学数据结构的时候感觉对这个遍历方法接触很少(可能老师讲了我没记住吧,反正印象不深,所以没想到也是情理之中),所以这篇博客重点就是讲一下这个moriss traversal方法,参考了这个博客- 为什么需要Morris Traversal
通常,实现二叉树的前序、中序、后序遍历的方法如下:递归,使用栈实现的迭代版本。这两种方法都是O(n)的空间复杂度,达不到我们的要求,想要达到O(1)复杂度的遍历,难度在于当子树遍历完成时如何回到父节点,在前面提到的两种方法中分别通过:递归的返回父节点调用处/栈弹出到父节点来实现,这都需要花费O(n)空间,如何节省这个空间达到相同目的,这需要Morris Traversal - Morris Traversal如何做到O(1)空间复杂度(同时时间复杂度不变为O(n))
在Morris Traversal方法中我们利用某些节点(多为叶节点)的左/右空指针指向父节点(这将在初到父节点时设置好,它是中序遍历下父节点的前驱结点,即它在排序中正好在父节点的前一位),当遍历达到这个子树的终点时将通过这个指针返回父节点并在返回之后重置它为NULL。这种方法
- 空间复杂度为O(1),因为只用了当前指针,前驱指针,前一个节点的value,当前结点的value等常数个变量。
- 时间复杂度,看起来是增加了,好像是O(nln(n)) —— 一共n个节点,每个节点找到前驱所需要的时间与节点高度有关系,即是ln(n)。但事实上,寻找所有节点的前驱节点只需要O(n)时间。n个节点的二叉树中一共有n-1条边,整个过程中每条边最多只走3次,一次是为了定位到某个节点,另外两次是为了寻找上面某个节点的前驱节点(一次是设置前驱结点,一次是根据前驱结点返回父节点后重置前驱结点的左/右指针为空),如下图所示,其中红色是为了定位到某个节点,黑色线是为了找到前驱节点。所以复杂度为O(3n-3),也就是O(n)。
算法步骤(这道题只需要中序遍历,实际上后序遍历和前序遍历做法类似):
- 如果当前节点的左孩子为空,则处理当前节点并将其右孩子作为当前节点。
- 如果当前节点的左孩子不为空,在当前节点的左子树中找到当前节点在中序遍历下的前驱节点。
a) 如果前驱节点的右孩子为空,将它的右孩子设置为当前节点。当前节点更新为当前节点的左孩子。
b) 如果前驱节点的右孩子为当前节点,将它的右孩子重新设为空(恢复树的形状)。处理当前节点。当前节点更新为当前节点的右孩子。 - 重复以上1、2直到当前节点为空。
ps: 这里的处理指的是将当前结点值存入临时变量nextNum并且将它与preNum比较,如果后者小于前者说明这两个节点中有一个出错,这个结合当前已经判断出多少出错节点可以判断是谁出错了
代码实现
class Solution { public: void recoverTree(TreeNode* root) { TreeNode * cur = root; TreeNode * preNode = NULL; TreeNode * ex1 = NULL; TreeNode * ex2 = NULL; int num = 0, nextNum = 0; while (cur != NULL) { if (cur->left == NULL) { num = nextNum; nextNum = cur->val; if (nextNum < num) { if (ex1 == NULL) { ex1 = preNode; ex2 = cur; } else { ex2 = cur; } } preNode = cur; cur = cur->right; } else { TreeNode * prev = cur->left; while (prev->right != NULL && prev->right != cur) { prev = prev->right; } if (prev->right == NULL) { prev->right = cur; cur = cur->left; } else { num = nextNum; nextNum = cur->val; if (nextNum < num) { if (ex1 == NULL) { ex1 = preNode; ex2 = cur; } else { ex2 = cur; } } preNode = cur; prev->right = NULL; cur = cur->right; } } } int temp = ex1->val; ex1->val = ex2->val; ex2->val = temp; } };
- 为什么需要Morris Traversal
总结
这周的实验主要收获就是从这个题目中学到的moriss traversal遍历方法,它在相比其他常用遍历二叉树方法同级别时间复杂度(O(n)的时间复杂度)情况下能够少用很多空间(常数空间复杂度),所以是一种很好的方法,尤其在搜索树非常大的情况下这种方法可以节省很多的开销,下面给出使用两种不同方法解这道题的相关参数:
使用递归方法
使用moriss traversal
显然后者好上不少
关于其他题目中我认为比较有价值的题目还有Merge Intervals和Redundant Connection II两道题在另外两个博客中给出。