題目來源
基礎:給你二叉搜索樹的根節點 root ,該樹中的兩個節點被錯誤地交換。請在不改變其結構的情況下,恢復這棵樹。
進階:使用 O(n) 空間復雜度的解法很容易實現。你能想出一個只使用常數空間的解決方案嗎?
示例1:
輸入:root = [1,3,null,null,2] 輸出:[3,1,null,null,2] 解釋:3 不能是 1 左孩子,因為 3 > 1 。交換 1 和 3 使二叉搜索樹有效。
示例2:
輸入:root = [3,1,4,null,null,2] 輸出:[2,1,4,null,null,3] 解釋:2 不能在 3 的右子樹中,因為 2 < 3 。交換 2 和 3 使二叉搜索樹有效。
來源:力扣(LeetCode) 鏈接:https://leetcode-cn.com/problems/recover-binary-search-tree 著作權歸領扣網絡所有。商業轉載請聯系官方授權,非商業轉載請注明出處。
題目解析
什么意思呢?這是其實是兩道題,第一道是基礎的,就是用基本的解法即可,關鍵是第二種如何優化你的算法。
好,我們先說第一種,大眾思維。
既然題目上說了錯誤地交換了搜索二叉樹(BST)的兩個節點,那么BST又有什么特點呢?
我們知道中序遍歷搜索BST會得到一組升序的數組(比如:[1,2,3,4,5,6,7,8]),那好,按照題意我們交換兩組節點2和6,數組變成[1,6,3,4,5,2,7,8],此時 可發現數組的升序被打破了,因為6>3,5>2。沒錯,我們就利用該性質是不是就可以找出交換的兩個節點位置,然后做交換就好了?
解析方法歸納:
1、先得到BST中序遍歷的數組序列;
2、找到不滿足條件的位置;
3、看節點數有幾個:
3.1、如果有兩個,即[1,6,3,4,5,2,7,8]中6>3,5>2,那么就將位置分別記為 i 和 j(i < j,其中i是6,j是5,特別提醒並不是2哦),對應的交換錯的節點為 Ai 和 Aj+1 (Ai > Ai+1 && Aj > Aj+1),我們分別記為x,y;
3.2、如果有一個,即[1,2,3,5,4,6,7,8]中的5>4,那么就將位置記為 i ,交換錯的位置就是Ai 和 Ai+1,我們分別記為x,y;
4、遍歷樹,交換節點 x , y 。
好了。思路也很清楚了,關鍵是如何實現。
第一步:先序遍歷BST這應該挺簡單的,遞歸嘛,我們將數組定位為nums來記錄:
//c++
第二步:找到不滿足條件的位置;但是該位置可能有一個,也可能有兩個,所以,得要遍歷數組一次。
vector<int> find2val(TreeNode* root, vector<int>& nums){ int n1 = 0; int n2 = 0; bool sec = false; for(int i = 0; i<nums.size()-1; i++){ if(nums[i]>nums[i+1]){ if(!sec){ n1 = nums[i]; n2 = nums[i+1]; sec = true; } else n2 = nums[i+1]; } } return {n1,n2}; }
第三步:看數組中到底有幾次
我們這里在主函數中直接就寫成2了,因為最多為兩次,當然也可以將這個次數記錄下來;
第四步:遍歷樹,換位置
void reverse(TreeNode * root, int count, int x, int y){ if(root!=nullptr){ if(root->val == x || root->val == y){ root->val = (root->val == x) ? y : x;//swap (x,y) if(--count == 0){//來計數是第幾次如果是第二次了后面的就不用再遍歷了; return; } } reverse(root->left,count,x,y); reverse(root->right,count,x,y); } }
第五步:主函數
void recoverTree(TreeNode* root) { vector<int> nums; inOrder(root, nums);//第一步中序遍歷得到升序數組 vector<int> swap_vals = find2val(root, nums);//找到兩個被錯誤交換的值 reverse(root,2,swap_vals[0],swap_vals[1]);//遍歷樹,進行交換; }
算法分析:
- 時間復雜度:O(N),其中N為BST的節點數。中序遍歷要O(N)的時間,而判斷交換節點在哪里,最好的情況是O(1),最壞的情況是O(N),所以是O(N);
- 空間復雜度:O(N),因為用到了一個數組來存放升序數列;
以上是一般大眾思維,那么如何進行優化呢?優化的點在哪里呢?
其實,我們沒有必要去引入這個nums數組,因為我們在中序遍歷樹時,如果去維護一個前節點變量,那么我們就可以在遍歷過程中直接進行比較,我們在這里引入一個棧,並且迭代實現中序遍歷,並不是遞歸。具體用法看下面;
如:
3 / \ 1 4 / 2
第一步:中序遍歷,先找到最左節點,中途所有的節點都入棧;
第二步:繼續;
第三步:取棧頂元素,並賦予前一個節點變量pred,並向彈出的節點的右子樹走;
第四步:繼續,因為1的右節點,也是NULL,故繼續彈棧,彈出來也就是1的父節點3;賦予前一個節點變量pred=3;
第五步:遍歷右子樹,有值,尋找右子樹中的最左節點,並將沿途所遍歷的節點都入棧;
第六步:入棧,找到右子樹的最左節點;
第七步:彈出棧頂,此時相當於,找到了以 3 為根節點的,中序遍歷中左子樹的最后一個節點值,和右子樹中的第一個值。這句話的意思是,當你中序遍歷時,3的前后值分別為 1 和 2 ;
看好這里,發現了前后節點值大小異常:2 < 3,記錄下這兩個節點Node1,Node2;
第八步:繼續;
第九步:遍歷完畢,找到了交換的點Node1,Node2,進行交換即可;
代碼實現:
class Solution { public: void recoverTree(TreeNode* root) { stack<TreeNode*> stk; TreeNode* x = nullptr; TreeNode* y = nullptr; TreeNode* pred = nullptr; while (!stk.empty() || root != nullptr) { while (root != nullptr) { stk.push(root); root = root->left; } root = stk.top(); stk.pop(); if (pred != nullptr && root->val < pred->val) { y = root; if (x == nullptr) { x = pred; } else break; } pred = root; root = root->right; } swap(x->val, y->val); } };
這部分圖來自:
作者:LeetCode-Solution 鏈接:https://leetcode-cn.com/problems/recover-binary-search-tree/solution/hui-fu-er-cha-sou-suo-shu-by-leetcode-solution/ 來源:力扣(LeetCode) 著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。
復雜度分析:
- 時間復雜度:最壞情況下是需要遍歷整棵樹(即交換節點為BST的最右側的兩個節點),時間復雜度為O(N),N為節點個數;
- 空間復雜度:O(H),H為BST的高度;注意:中序遍歷的時候,棧的深度取決於樹的高度噢!!!
親愛的,你們以為到這里就結束了嗎?
錯,大錯特錯,在這里突然冒出一個Morris中序遍歷算法,這個算法之前是真的不知道。無知了...