總結類型:
- 完全子樹(#222)
- BST(左右子樹值的性質,注意不僅要滿足parent-child relation,還要滿足ancestor-grandchild relation,可以通過傳入small/large變量,或者通過右節點的最左節點和左節點的最右節點來判斷)(#333)
- Inorder(sorted), preorder([root][small][large]), postorder traversal(recursively & iteratively)
- Height: The number of edges from the node to the deepest leaf. So the leaf node has height 0(#366,#572)
- Depth: The depth of a node is the number of edges from the node to the tree's root node. The root node has depth 0.
- stack & queue -> in preorder
Algorithm Crush
- Morris Traversal (change pointer, link the rightmost node of the left node to the root. we can apply this pattern to some other problems.)
124. Binary Tree Maximum Path Sum
Given a binary tree, find the maximum path sum.
For this problem, a path is defined as any sequence of nodes from some starting node to any node in the tree along the parent-child connections. The path must contain at least one node and does not need to go through the root.
這道題的一個難度其實在于要考慮負數。如果不用考慮負數,那么遞歸很好寫。如果有了負數,那么對根,左孩子,右孩子有這么幾種情況:
- 根
- 左孩子
- 右孩子
- 根和左孩子
- 根和右孩子
- 根和左孩子和右孩子
最大路徑值不外乎這些情況。
在遞歸中,我們算完了這一層的最大值,要返回給上一層當左/右孩子時,因為必須與上一層無縫對接,就必須返回這幾種情況中的一種:
- 根
- 根和左孩子
- 根和右孩子
對比當前層和上一層,兩者的關系是:這一層返回的情況會成為下一層的左孩子或者右孩子。也就是說,我們這一層對根,左孩子,右孩子的討論可以簡化成下面這三種情況:
- 左孩子
- 右孩子
- 根和左孩子和右孩子
因為max(根,根和左孩子,根和右孩子)會返回到上一層變為上一層的左孩子/右孩子跟另一個子樹去討論,就是說這一層和上一層其實討論了六種情況。
當然,我們要用一個全局變量來記錄全局最大值。當根返回的時候,我們要比多一次全局變量以及返回值。在具體寫code時,對于葉來說,要考慮到如何處理nullptr所代表的值。我這里先用INT_MIN表示了,設置了兩個flag來進行0和INT_MIN之間的轉換。
class Solution {
private:
int gl_max = INT_MIN;
int helper(TreeNode* root)
{
if (!root) return INT_MIN;
int left = helper(root->left);
int right = helper(root->right);
bool left_null = false;
bool right_null = false;
if (left == INT_MIN) { left_null = true; left = 0; }
if (right == INT_MIN) { right_null = true; right = 0; }
gl_max = max(gl_max, max(root->val + left + right, max(left_null ? INT_MIN : left, right_null ? INT_MIN : right)));
return max(root->val, max(root->val + left, root->val + right));
}
public:
int maxPathSum(TreeNode* root)
{
return max(gl_max, helper(root));
}
};
108. Convert Sorted Array to Binary Search Tree
Given an array where elements are sorted in ascending order, convert it to a height balanced BST.
這道題用binary search就可以做了,我比較想讓自己記住的一點是binary search的終止條件和start、end值,因為有時候會搞混:
- start = 0
- end = size
- mid = (start + end) / 2
- end condition: start == end
236. Lowest Common Ancestor of a Binary Tree
Given a binary tree, find the lowest common ancestor (LCA) of two given nodes in the tree.
這道題真的是醉了~想了挺久的但想出的方法巨繁瑣,一看評論區發現大神不愧是大神TAT
Recursion有時候真的可以很巧妙!!
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
if (!root || root == p || root == q) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
// 如果左右子樹都是nullptr,就return nullptr
// 如果左子樹是null,右子樹不是,就return左子樹的值
// 如果右子樹是null,左子樹不是,就return右子樹的值
// 如果左右子樹都不是null,就return root
return !left ? right : !right ? left : root;
}
大神的思路翻譯一下的話就是:如果當前根的左右子樹都包含著p或者q,那就說明當前根才是ancestor;如果當前根的左右子樹都不包含p或者q,那就以null來說明不包括;如果當前根只有左或者右子樹包含p或者q,那ancestor就是那個左或者右子樹的根。理解容易,難以想出來啊~
我自己想思路的時候用了非常多的假設和條件,看到這么簡潔的思路真是驚呆了。我想自己還是思路犯了根本性的錯誤,鉆進死胡同里去了。。。
222. Count Complete Tree Nodes
Given a complete binary tree, count the number of nodes.
Definition of a complete binary tree from Wikipedia:In a complete binary tree every level, except possibly the last, is completely filled, and all nodes in the last level are as far left as possible. It can have between 1 and 2h
nodes inclusive at the last level h.
做到這題感覺樹它就是recursion,recursion它就是樹。完全樹的題目沒怎么碰到過,這次知道了它方便利用的一些性質:
- 對于一個完全子樹,高度多一,則子樹節點個數翻倍
- 對于一個完全子樹,它的高度由左子樹外葉節點的深度決定
具體將這個性質應用到題目中來:
- 如果當前根的左子樹和右子樹的高度一致,那么說明,至少左子樹是完全的。我們可以用1<<h的方法,來算出左子樹的節點個數,對于右子樹,進行recursion的操作。
- 如果當前根的左子樹和右子樹的高度不是一致的,右子樹是完全的。我們也可以進行同樣的方法算出右子樹的節點個數,對左子樹進行recursion操作。只不過這個情況和上一點的情況相比,h的高度要-1。
時間復雜度上,我們每次找深度用了logn的時間,最壞情況下要找logn次(比如該樹離完全樹只有一個node之差)。所以時間復雜度是O((logn)^2)。
class Solution {
private:
int height(TreeNode* root, int h)
{
if (!root) return h;
return height(root->left, h + 1);
}
public:
int countNodes(TreeNode* root)
{
if (!root) return 0;
int left_h = height(root->left, 0);
int right_h = height(root->right, 0);
if (left_h == right_h) return (1 << left_h) + countNodes(root->right);
else return (1 << right_h) + countNodes(root->left);
}
};
114. Flatten Binary Tree to Linked List
Given a binary tree, flatten it to a linked list in-place.
我在思考這個問題的時候,想,如果要把左子樹變到右子樹去,那么右子樹的信息不是要丟失嗎?這么一來就要把左右子樹交換,這么一來還不如先翻轉樹來得方便。于是我用遞歸的方式做了,方法是先翻轉樹,然后再用先序遍歷。
其實用迭代的方式也可以做。當我們將左右子樹連起來的時候,右子樹消失是沒有關系的,因為可以用左子樹得到嘛。這與morris traversal有異曲同工之妙,看來讓我們改變樹的結構的時候,經常可以用得到這一招。
那怎么去連呢?扁平化后的樹顯然是先序遍歷的結果。先序遍歷是深度搜索中的一種,也就是說先將左子樹先搜索完再搜索右子樹。所以我們可以將右子樹連在左子樹的最后一個節點,也就是左節點的最右節點。
class Solution {
public:
void flatten(TreeNode* root)
{
while (root)
{
while (root->left)
{
TreeNode* left = root->left;
while (left->right) left = left->right;
left->right = root->right;
root->right = root->left;
root->left = nullptr;
root = root->right;
}
root = root->right;
}
}
};
366. Find Leaves of Binary Tree
Given a binary tree, collect a tree's nodes as if you were doing this: Collect and remove all leaves, repeat until the tree is empty.
這道題的insight在于發現,所有的葉節點的高度都一致。高度的定義是,從該節點到外葉節點經過的邊數量。我們只要把所有高度為H的節點放進vector[H]中,就是結果。
class Solution {
private:
vector<vector<int>> res;
int helper(TreeNode* root)
{
if (!root) return -1;
int height = 1 + max(helper(root->left), helper(root->right));
if (res.size() == height) res.push_back({});
res[height].push_back(root->val);
return height;
}
public:
vector<vector<int>> findLeaves(TreeNode* root)
{
helper(root);
return res;
}
};
654. Maximum Binary Tree
這題O(n^2)的方法就不去想了吧,可以做到更好的O(n):
1、假如當前數比上一個數小,就把當前樹做成上一個樹的右子樹。這很明顯,因為右子樹總是遞減的。
2、關鍵當當前數比前面一個數大的時候,我們要尋找第一個比當前數大的樹T,讓T右子樹等于當前樹,當前樹的左子樹等于T的原右子樹。我們要保證的是,對于當前數,它能將T以后,和當前數以前的這些數,作為當前樹的左子樹。這個idea不難發現,但是在寫code的時候,我自己寫的work but too compliated,因為我們的關注點很大程度上在于“上一個”數,我們可以采用stack的思想。感謝評論區的幫助。
class Solution {
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums)
{
if (nums.empty()) return nullptr;
vector<TreeNode*> stk;
for (int num: nums)
{
TreeNode* cur = new TreeNode(num);
while (stk.size() && stk.back()->val < num)
{
cur->left = stk.back();
stk.pop_back();
}
if (stk.size())
{
stk.back()->right = cur;
}
stk.push_back(cur);
}
return stk.front();
}
};
我自己寫code的時候把上述兩種情況分開討論,寫的比較長,但是這個方法說明,我們遇到(2)情況的時候,其實是先遇到了(2)情況,然后因為pop掉了一些數,所以又會回到(1)情況去了。這樣的code更加精煉。
538. Convert BST to Greater Tree
Given a Binary Search Tree (BST), convert it to a Greater Tree such that every key of the original BST is changed to the original key plus sum of all keys greater than the original key in BST.
分析清楚就很好做!可惜我太快開始寫了,直到重新理思路才發現很簡單。
我們從最大的節點開始更新,以從大到小的順序來中序遍歷這棵樹。
- 右節點是空指針的時候,當前節點 += 比它大的節點和(需要傳入一個變量sum來記錄)
- 右節點非空的時候,當前節點 += 比它大的節點和(由遞歸返回)
- 左節點空的時候,返回sum
- 左節點非空的時候,先遍歷左節點再返回,傳入變量sum為當前節點的值
class Solution {
private:
int helper(TreeNode* root, int right_sum)
{
if (!root) return right_sum;
root->val += helper(root->right, right_sum);
return helper(root->left, root->val);
}
public:
TreeNode* convertBST(TreeNode* root)
{
if (!root) return nullptr;
helper(root, 0);
return root;
}
};
637. Average of Levels in Binary Tree
這道題不難,主要是想比較一下dfs和bfs做這道題的這兩種方法。
1、DFS:用兩個array來存放此層的總和和節點數量,用height來儲存是第幾層。
class Solution {
private:
vector<double> sum;
vector<int> count;
vector<double> res;
void dfs(TreeNode* root, int height)
{
if (!root) return;
if (sum.size() == height)
{
sum.push_back(root->val);
count.push_back(1);
}
else
{
sum[height] += root->val;
count[height]++;
}
dfs(root->left, height + 1);
dfs(root->right, height + 1);
}
public:
vector<double> averageOfLevels(TreeNode* root)
{
dfs(root, 0);
for (int i = 0; i < sum.size(); ++i)
{
res.push_back(sum[i] / count[i]);
}
return res;
}
};
2、BFS:標準bfs方法。
class Solution {
private:
vector<double> res;
public:
vector<double> averageOfLevels(TreeNode* root)
{
if (!root) return {};
queue<TreeNode*> q;
q.push(root);
while (q.size())
{
double sum = 0;
double count = 0;
queue<TreeNode*> q_next;
while (true)
{
TreeNode* top = q.front();
q.pop();
if (top->left) q_next.push(top->left);
if (top->right) q_next.push(top->right);
sum += top->val;
count++;
if (q.empty()) { res.push_back(sum / count); break; }
}
q = q_next;
}
return res;
}
};
285. Inorder Successor in BST
這道題可以不利用BST的性質,用inorder traversal來做。利用BST性質來做是我更喜歡的。要找到一個數的inorder successor,也就是找到比這個節點大,但又大得最少的數。我們可以從根出發:
1、如果我們發現當前節點的數比所要找節點p的數大的話,它是一個潛在的中序后續節點,把當前節點往左挪
2、如果當前節點比所要找的節點p的數小的話,無需更新中序后續節點,把當前節點往右挪
3、如果當前節點等于索要找的節點p的數的話,無需更新中序后續節點,把當前節點往右挪
4、如果當前節點是空指針的話,說明尋找結束了
可以想三種情況:
- p為左外葉節點,那么后續節點應該是它的parent。滿足了第三個條件,由于觸發了第四個條件,我們可以返回它的parent
- p的后續節點在其右子樹的最左子樹。此時第三個條件滿足后,我們把當前節點往右挪,會一直觸發第一個條件,直到我們找到右子樹的最左子樹
- p是某右子樹外葉,它后續節點在ancestor上
class Solution {
public:
TreeNode* inorderSuccessor(TreeNode* root, TreeNode* p)
{
TreeNode* res = nullptr;
if (!p) return res;
while (root)
{
if (root->val > p->val)
{
res = root;
root = root->left;
}
else root = root->right;
}
return res;
}
};
BST的題目肯定可以利用它的性質做的,以后盡量優先考慮用它的性質來做。我覺得這是一道簡單但犀利的題目。
572. Subtree of Another Tree
Given two non-empty binary trees s and t, check whether tree t has exactly the same structure and node values with a subtree of s. A subtree of s is a tree consists of a node in s and all of this node's descendants. The tree s could also be considered as a subtree of itself.
比較喜歡這道題是因為又可以利用height來避免討論一些情況。
class Solution {
private:
vector<TreeNode*> nodes;
int get_h(TreeNode* n)
{
if (!n) return -1;
return max(get_h(n->left), get_h(n->right)) + 1;
}
int find_nodes(TreeNode* s, int h)
{
if (!s) return -1;
int height = max(find_nodes(s->left, h), find_nodes(s->right, h)) + 1;
if (height == h) nodes.push_back(s);
return height;
}
bool isSubtreeHelper(TreeNode* root1, TreeNode* root2)
{
if (!root1 && !root2) return true;
if (root1 && !root2 || !root1 && root2) return false;
if (root1->val != root2->val) return false;
return isSubtreeHelper(root1->left, root2->left) &&
isSubtreeHelper(root1->right, root2->right);
}
public:
bool isSubtree(TreeNode* s, TreeNode* t)
{
int t_h = get_h(t);
find_nodes(s, t_h);
for (auto n: nodes)
{
if (isSubtreeHelper(n, t)) return true;
}
return false;
}
};
449. Serialize and Deserialize BST
這道題利用preorder在BST中的性質。之前常做的是BST的中序遍歷,結果是一個排序好的遞增數列。而前序遍歷的結果是,數列的結果會像這樣:root(small)(large),因為我們遍歷的順序是先根,再左子樹,最后右子樹。利用這個性質,我們可以在serialize的步驟,做一次前序遍歷,而在deserialize的步驟,利用一個container來分割當前根的左右子樹,根據是將數與根的值做比較。
時間上來說,與merge sort的思想類似:每一層找左右子樹的時候都要用到O(n)的時候,樹深logN層,最壞情況N層,所以O(n^2)的復雜度。
我嘗試了用vector來implement this idea, but holy shit, C++竟然沒有在未排序數列中找到第一個比目標值更大數這樣的helper function,一口老血。最近覺得C++越來越不方便了。
class Codec {
private:
string s;
void dfs(TreeNode* root)
{
if (!root) return;
s += to_string(root->val);
s += ',';
dfs(root->left);
dfs(root->right);
}
queue<int> parse_int(string data)
{
queue<int> q;
istringstream ss(data);
string s;
while (getline(ss, s, ',')) q.push(atoi(s.c_str()));
return q;
}
TreeNode* construct_tree(queue<int> &q)
{
if (q.empty()) return nullptr;
queue<int> left;
TreeNode* root = new TreeNode(q.front());
q.pop();
while (q.size() && q.front() <= root->val)
{
left.push(q.front());
q.pop();
}
root->left = construct_tree(left);
root->right = construct_tree(q);
return root;
}
public:
// Encodes a tree to a single string.
string serialize(TreeNode* root)
{
dfs(root);
if (s.size()) s.pop_back();
return s;
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data)
{
queue<int> q = parse_int(data);
return construct_tree(q);
}
};
// Your Codec object will be instantiated and called as such:
// Codec codec;
// codec.deserialize(codec.serialize(root));
255. Verify Preorder Sequence in Binary Search Tree
Given an array of numbers, verify whether it is the correct preorder traversal sequence of a binary search tree. You may assume each number in the sequence is unique.
挺好的題,又是BST的前序序列的規律。可以用recursion做,也可以用一個stack做(本質上都是深搜嘛)。當前數如果比stack上最后一個數小的話,我們繼續push上stack,等于還是在左子樹。如果比stack上最后一個數大的話(按題意不考慮duplicate),說明我們到某個子樹的右子樹了。我們要找是哪個子樹的右子樹,就一直pop stack,一直到stack空了或者stack的數K大于當前數了,我們知道K之前被pop的那個數K_PREV其實就是樹根。由于前序數列按照中、左、右的順序遍歷,而我們已經處理好了中和左,右子樹的所有數都要比K_PREV大。所以記住K_PREV,再做同樣的遍歷,一直到數列最后。
class Solution {
public:
bool verifyPreorder(vector<int>& preorder)
{
stack<int> s;
int low = INT_MIN;
for (int num: preorder)
{
if (low > num) return false;
while (s.size() && num > s.top())
{
low = s.top();
s.pop();
}
s.push(num);
}
return true;
}
};
549. Binary Tree Longest Consecutive Sequence II
Given a binary tree, you need to find the length of Longest Consecutive Path in Binary Tree. Especially, this path can be either increasing or decreasing. For example, [1,2,3,4] and [4,3,2,1] are both considered valid, but the path [1,2,4,3] is not valid. On the other hand, the path can be in the child-Parent-child order, where not necessarily be parent-child order.
我一開始想的思路:最大值無非三種情況
- 根 + 左樹連續值(根與左樹差1,并且記下與左樹是遞增還是遞減關系)
- 根 + 右樹連續值(根與右樹差1,并且記下與右樹是遞增還是遞減關系)
- 根 + 左樹連續值 + 右樹連續值(根與左右樹都差一,并且左右樹根值不同)
總體思路是對的,但是討論的情況有點偏復雜。我用一個Result struct來記錄當前節點究竟是遞增還是遞減的情況的話,對于左右樹的遞增/遞減,我都只要選取最大的來作為當前節點的遞增/遞減,并返回這個結果。而對于第三種情況,我們其實無需再比較根是否與左右樹都差一,并且左右樹根值不同——當前節點的(遞增+遞減-1)就是可能是最大值情況。為什么呢?
- 遞增和遞減值肯定是從左和右兩個不同的子樹上得來,如果說滿足了(根與左右樹都差一,并且左右樹根值不同)這個條件,(遞增 + 遞減 - 1)就是可能是最大值情況這個很直觀
- 如果說根與左樹差一而右樹不差一,那么右樹給的遞增/遞減結果是0,當前節點從中得到的遞增/遞減結果是1。(遞增 + 遞減 - 1)的(-1)將這個情況給減去了,剩下的就是根與左樹的遞增/遞減值
- 如果說根與右樹差一而左樹不差一,同理
- 如果說根與左右樹都不差一,那么當前節點的遞增/遞減節點都分別是1,那么(遞增 + 遞減 - 1)= (1 + 1 - 1) = 1,說明當前節點的最大連續遞增/遞減節點是1,這也符合情況
我覺得三種情況是比較好分析出來的,但是第三種情況其實可以用(遞增+遞減-1)這一句話來表達,是比較隱含的insight。
class Solution {
private:
int gl_max = 0;
struct Result
{
int val;
int inc;
int dec;
Result(int v, int i, int d) : val(v), inc(i), dec(d) {}
};
Result* helper(TreeNode* root)
{
if (!root) return nullptr;
Result* left = helper(root->left);
Result* right = helper(root->right);
int inc = 1, dec = 1;
if (left && root->val - left->val == 1) inc = left->inc + 1;
else if (left && left->val - root->val == 1) dec = left->dec + 1;
if (right && root->val - right->val == 1) inc = max(inc, right->inc + 1);
else if (right && right->val - root->val == 1) dec = max(dec, right->dec + 1);
gl_max = max(gl_max, inc + dec - 1);
return new Result(root->val, inc, dec);
}
public:
int longestConsecutive(TreeNode* root)
{
helper(root);
return gl_max;
}
};
272. Closest Binary Search Tree Value II
Given a non-empty binary search tree and a target value, find k values in the BST that are closest to the target.
這道題,我是這么想。recursion中,將比target更小的數放在stack里,一旦找到比target更大的數了,就將之和之后遍歷的數與stack中比較,取離target更近的那個數放進result vector。挺直觀的,寫的時候注意終止條件:
- 我們放到k==res.size()為止
- 如果比target小的數先都放進res了(甚至沒有比target小的數),但是還沒終止的話,要注意之后的遍歷每一個數都要放進res
- 如果比target大的數先都放進res了(甚至沒有比target大的數),但是還沒終止的話,要把stack里的數推到res里
class Solution {
private:
void inorder_add_k(TreeNode* root, double target, stack<int> &inorder, bool &end, vector<int> &res, int k)
{
if (!root) return;
inorder_add_k(root->left, target, inorder, end, res, k);
if (target <= root->val) end = true;
if (!end) inorder.push(root->val);
else
{
while (inorder.size() && res.size() < k)
{
int last = inorder.top();
if (target - last <= root->val - target)
{ res.push_back(last); inorder.pop();}
else
{ res.push_back(root->val); break; }
}
if (res.size() == k) return;
else if (inorder.empty()) res.push_back(root->val);
}
inorder_add_k(root->right, target, inorder, end, res, k);
}
public:
vector<int> closestKValues(TreeNode* root, double target, int k)
{
stack<int> inorder;
bool end = false;
vector<int> res;
inorder_add_k(root, target, inorder, end, res, k);
// if we've exhausting elements > target
while (res.size() != k)
{
res.push_back(inorder.top());
inorder.pop();
}
return res;
}
};
這樣的解法最壞情況也是O(n+k)的。另外參考評論區,還有一種解法,可以先找到離target最近的那個值,需要O(h)。然后用兩個pointer,一個往小了去,一個往大了去,每次pointer的遍歷時間也是O(h)。這樣就一共是O(kh)時間。這樣的想法的本質其實就是在一個sorted array找到k nearest elements,不過由于這是樹,這種iterator的操作比較少見,因為不是O(1)的。讓我來寫寫看: