【leetcode】二叉树与经典问题
文章目錄
- 筆記
- leetcode [114. 二叉樹展開為鏈表](https://leetcode-cn.com/problems/flatten-binary-tree-to-linked-list/)
- 解法一: 后序遍歷、遞歸
- leetcode [226. 翻轉二叉樹](https://leetcode-cn.com/problems/invert-binary-tree/)
- 思路與算法
- 復雜度分析
- leetcode [劍指 Offer 32 - I. 從上到下打印二叉樹](https://leetcode-cn.com/problems/cong-shang-dao-xia-da-yin-er-cha-shu-lcof/)
- BFS算法流程:
- 復雜度分析:
- leetcode [107. 二叉樹的層序遍歷 II](https://leetcode-cn.com/problems/binary-tree-level-order-traversal-ii/)
- 學會二叉樹的層序遍歷,可以一口氣擼完leetcode一下八道題目:
- 102.二叉樹的層序遍歷
- 199.二叉樹的右視圖
- 637.二叉樹的層平均值
- leetcode [589. N 叉樹的前序遍歷](https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/)
- 樹的遍歷(Traversal)
- 遞歸算法的三個要素。每次寫遞歸,都按照這三要素來寫,可以保證大家寫出正確的遞歸算法!
- 確定遞歸函數的參數和返回值:
- 確定終止條件:
- 確定單層遞歸的邏輯:
- 前序遍歷(preorder, 按照先訪問根節點的順序)
- 中序遍歷:(inorder, 按照先訪問順序,左 根 右)
- 后序遍歷:(lastorder, 按照先訪問順序,左 右 根 )
- 144.二叉樹的前序遍歷 迭代法
- 94.二叉樹的中序遍歷 中序遍歷(迭代法)
- 145.二叉樹的后序遍歷(迭代法)
- 二叉樹前中后迭代方式統一寫法
- N叉樹的前序遍歷
- N叉樹的后序遍歷
- 總結
- leetcode [589. N 叉樹的前序遍歷](https://leetcode-cn.com/problems/n-ary-tree-preorder-traversal/)
- 429. N叉樹的層序遍歷
- 思路:這道題依舊是模板題**,**只不過一個節點有多個孩子了**
- 515.在每個樹行中找最大值
- 思路:層序遍歷,取每一層的最大值
- 116.填充每個節點的下一個右側節點指針
- 思路:本題依然是層序遍歷,只不過在單層遍歷的時候記錄一下本層的頭部節點,然后在遍歷的時候讓前一個節點指向本節點就可以了
- 117.填充每個節點的下一個右側節點指針II
- 思路:這道題目說是二叉樹,但116題(上題)目說是完整二叉樹,其實沒有任何差別,一樣的代碼一樣的邏輯一樣的味道
- 總結
- leetcode [103. 二叉樹的鋸齒形層序遍歷](https://leetcode-cn.com/problems/binary-tree-zigzag-level-order-traversal/)
- leetcode [110. 平衡二叉樹](https://leetcode-cn.com/problems/balanced-binary-tree/)
- leetcode [112. 路徑總和](https://leetcode-cn.com/problems/path-sum/)
- **方法一:深度優先搜索遞歸**
- 方法二:廣度優先搜索
- 方法三:棧模擬遞歸(回溯)
- leetcode [105. 從前序與中序遍歷序列構造二叉樹](https://leetcode-cn.com/problems/construct-binary-tree-from-preorder-and-inorder-traversal/)
- 遞歸的解法 1 找根位置 2 遞歸建立左子樹 3遞歸建立右子樹
- 2.使用指針解決
- leetcode [222. 完全二叉樹的節點個數](https://leetcode-cn.com/problems/count-complete-tree-nodes/)
- 方法一:適合所有類型的樹的節點計算
- 方法二:dfs
- 1.如果根節點的左子樹深度等于右子樹深度,則說明左子樹為滿二叉樹。
- 如果根節點的左子樹深度大于右子樹深度,則說明右子樹為滿二叉樹
- 三種做法的代碼
- 1.暴力求解,深度遞歸dfs
- 2.運用完全二叉樹的性質(和滿二叉樹結合)
- 3.方法三:二分查找 + 位運算
- leetcode [劍指 Offer 54. 二叉搜索樹的第k大節點](https://leetcode-cn.com/problems/er-cha-sou-suo-shu-de-di-kda-jie-dian-lcof/)
- 方法一:遞歸的方式
- 方法二:先求前序遍歷數組,在根據數組求第k大的值
- leetcode [劍指 Offer 26. 樹的子結構](https://leetcode-cn.com/problems/shu-de-zi-jie-gou-lcof/)
- 方法1:遞歸
- leetcode [662. 二叉樹最大寬度](https://leetcode-cn.com/problems/maximum-width-of-binary-tree/)
- 方法 0:寬度優先搜索 [Accepted]
- 方法 1:寬度優先搜索 [Accepted]
- 方法 2:深度優先搜索 [Accepted]
- leetcode [968. 監控二叉樹](https://leetcode-cn.com/problems/binary-tree-cameras/)
- 第一種解法
- 情況1:左右節點都有覆蓋
- 情況2:左右節點至少有一個無覆蓋的情況
- 情況3:左右節點至少有一個有攝像頭
- 情況4:頭結點沒有覆蓋
- 情況3:左右節點至少有一個有攝像頭
- 情況4:頭結點沒有覆蓋
筆記
樹
節點:集合
邊: 關系
1對多映射
leetcode 114. 二叉樹展開為鏈表
解法一: 后序遍歷、遞歸
依據二叉樹展開為鏈表的特點,使用后序遍歷完成展開。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/TreeNode* last = nullptr;
class Solution {
public:void flatten(TreeNode* root) {if (root == nullptr) return;flatten(root->left);flatten(root->right);if (root->left != nullptr) {auto pre = root->left;while (pre->right != nullptr) pre = pre->right;pre->right = root->right;root->right = root->left;root->left = nullptr;}root = root->right;return;}
};
解法二: 非遞歸,不使用輔助空間及全局變量
前面的遞歸解法實際上也使用了額外的空間,因為遞歸需要占用額外空間。下面的解法無需申請棧,也不用全局變量,是真正的 In-Place 解法。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {
public:void flatten(TreeNode* root) {while (root != nullptr) {if (root->left != nullptr) {auto most_right = root->left; // 如果左子樹不為空, 那么就先找到左子樹的最右節點while (most_right->right != nullptr) most_right = most_right->right; // 找最右節點most_right->right = root->right; // 然后將跟的右孩子放到最右節點的右子樹上root->right = root->left; // 這時候跟的右孩子可以釋放, 因此我令左孩子放到右孩子上root->left = nullptr; // 將左孩子置為空}root = root->right; // 繼續下一個節點}return;}
};
leetcode 226. 翻轉二叉樹
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-M5Zo096g-1619446816610)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1619309137259.png)]
//根 左 右
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:TreeNode* invertTree(TreeNode* root) {if(root==NULL) return root;swap(root->left,root->right);invertTree(root->left);invertTree(root->right);return root;}
};
思路與算法
這是一道很經典的二叉樹問題。顯然,我們從根節點開始,遞歸地對樹進行遍歷,并從葉子結點先開始翻轉。如果當前遍歷到的節點 root 的左右兩棵子樹都已經翻轉,那么我們只需要交換兩棵子樹的位置,即可完成以 root 為根節點的整棵子樹的翻轉。
// 左 右 根
class Solution {
public:TreeNode* invertTree(TreeNode* root) {if (root == nullptr) {return nullptr;}TreeNode* left = invertTree(root->left);TreeNode* right = invertTree(root->right);root->left = right;root->right = left;return root;}
};
復雜度分析
時間復雜度:O(N),其中 N 為二叉樹節點的數目。我們會遍歷二叉樹中的每一個節點,對每個節點而言,我們在常數時間內交換其兩棵子樹。
空間復雜度:O(N)。使用的空間由遞歸棧的深度決定,它等于當前節點在二叉樹中的高度。在平均情況下,二叉樹的高度與節點個數為對數關系,即O(logN)。而在最壞情況下,樹形成鏈狀,空間復雜度為 O(N)。
leetcode 劍指 Offer 32 - I. 從上到下打印二叉樹
BFS算法流程:
特例處理: 當樹的根節點為空,則直接返回空列表 [] ;
初始化: 打印結果列表 res = [] ,包含根節點的隊列 queue = [root] ;
BFS 循環: 當隊列 queue 為空時跳出;
出隊: 隊首元素出隊,記為 node;
打印: 將 node對應val 添加至列表 ans尾部;
添加子節點: 若 node 的左(右)子節點不為空,則將左(右)子節點加入隊列 queue ;
返回值: 返回打印結果列表 ans即可。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
//BFS
class Solution {
public:vector<int> levelOrder(TreeNode* root){vector<int> ans;if(root == NULL) return ans;queue<TreeNode*> q;q.push(root);while(!q.empty()){TreeNode* node=q.front();q.pop();ans.push_back(node->val);if(node->left) q.push(node->left);if(node->right) q.push(node->right);}return ans;}//DFS// void getResult(TreeNode *root, int k,vector<vector<int>> &ans){// if(root == NULL) return;// if(k == ans.size()) ans.push_back(vector<int>());// ans[k].push_back(root->val);// getResult(root->left, k + 1, ans);// getResult(root->right, k + 1, ans);// return;// }// vector<vector<int>> levelOrder(TreeNode* root) {// vector<vector<int>> ans;// getResult(root , 0 , ans);// return ans;// }
};
復雜度分析:
時間復雜度 O(N) : N 為二叉樹的節點數量,即 BFS 需循環 N 次。
空間復雜度 O(N) : 最差情況下,即當樹為平衡二叉樹時,最多有 N/2 個樹節點同時在 queue 中,使用 O(N) 大小的額外空間。
leetcode 107. 二叉樹的層序遍歷 II
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:void getResult(TreeNode *root,int k, vector<vector<int>> &ans){if(root==NULL) return;if(k == ans.size()) ans.push_back(vector<int>());ans[k].push_back(root->val);getResult(root->left,k+1,ans);getResult(root->right,k+1,ans);return;}//前序遍歷 //根 左 右 [3],[9,20], [15,7]vector<vector<int>> levelOrderBottom(TreeNode* root) {vector<vector<int>> ans;getResult(root,0,ans);for(int i=0,j=ans.size()-1;i<j;i++,j--){swap(ans[i],ans[j]);}return ans;}
};
學會二叉樹的層序遍歷,可以一口氣擼完leetcode一下八道題目:
102.二叉樹的層序遍歷
107.二叉樹的層次遍歷II
199.二叉樹的右視圖
637.二叉樹的層平均值
429.N叉樹的前序遍歷
515.在每個樹行中找最大值
116.填充每個節點的下一個右側節點指針
117.填充每個節點的下一個右側節點指針II
102.二叉樹的層序遍歷
給你一個二叉樹,請你返回其按 層序遍歷 得到的節點值。 (即逐層地,從左到右訪問所有節點)。
思路
我們之前講過了三篇關于二叉樹的深度優先遍歷的文章:
接下來我們再來介紹二叉樹的另一種遍歷方式:層序遍歷。
層序遍歷一個二叉樹。就是從左到右一層一層的去遍歷二叉樹。這種遍歷的方式和我們之前講過的都不太一樣。
需要借用一個輔助數據結構即隊列來實現,隊列先進先出,符合一層一層遍歷的邏輯,而用棧先進后出適合模擬深度優先遍歷也就是遞歸的邏輯。
而這種層序遍歷方式就是圖論中的廣度優先遍歷,只不過我們應用在二叉樹上。
使用隊列實現二叉樹廣度優先遍歷,動畫如下:
這樣就實現了層序從左到右遍歷二叉樹。
代碼如下:這份代碼也可以作為二叉樹層序遍歷的模板,以后再打后面的題目就靠它了。
BFS樹/圖的層序遍歷,或者廣度優先搜索,基于queue數據結構實現
class Solution {
public:vector<vector<int>> levelOrder(TreeNode* root) {queue<TreeNode*> que;if (root != NULL) que.push(root);vector<vector<int>> result;while (!que.empty()) {int size = que.size();vector<int> vec;// 這里一定要使用固定大小size,不要使用que.size(),因為que.size是不斷變化的for (int i = 0; i < size; i++) {TreeNode* node = que.front();que.pop();vec.push_back(node->val);if (node->left) que.push(node->left);if (node->right) que.push(node->right);}result.push_back(vec);}return result;}
};
199.二叉樹的右視圖
給定一棵二叉樹,想象自己站在它的右側,按照從頂部到底部的順序,返回從右側所能看到的節點值。
思路
層序遍歷的時候,判斷是否遍歷到單層的最后面的元素,如果是,就放進result數組中,隨后返回result就可以了。
class Solution {
public:vector<int> rightSideView(TreeNode* root) {queue<TreeNode*> que;if (root != NULL) que.push(root);vector<int> result;while (!que.empty()) {int size = que.size();for (int i = 0; i < size; i++) {TreeNode* node = que.front();que.pop();if (i == (size - 1)) result.push_back(node->val);// 將每一層的最后元素放入result數組中if (node->left) que.push(node->left);if (node->right) que.push(node->right);}}return result;}
};
637.二叉樹的層平均值
給定一個非空二叉樹, 返回一個由每層節點平均值組成的數組。
思路 本題就是層序遍歷的時候把一層求個總和在取一個均值。
class Solution {
public:vector<double> averageOfLevels(TreeNode* root) {queue<TreeNode*> que;if (root != NULL) que.push(root);vector<double> result;while (!que.empty()) {int size = que.size();double sum = 0; // 統計每一層的和for (int i = 0; i < size; i++) { TreeNode* node = que.front();que.pop();sum += node->val;if (node->left) que.push(node->left);if (node->right) que.push(node->right);}result.push_back(sum / size); // 將每一層均值放進結果集}return result;}
};
leetcode 589. N 叉樹的前序遍歷
/*// Definition for a Node.
class Node {
public:int val;vector<Node*> children;Node() {}Node(int _val) { val = _val;}Node(int _val, vector<Node*> _children) {val = _val;children = _children;}
};
*/
class Solution {
public:void __preorder(Node* root,vector<int> &ans){if(root==NULL) return ;ans.push_back(root->val);for(auto x:root->children){__preorder(x,ans);}}vector<int> preorder(Node* root) {vector<int> ans;__preorder(root,ans);return ans;}
};
樹的遍歷(Traversal)
如下圖, 三種遍歷方式, 可用同一種遞歸思想實現
遞歸算法的三個要素。每次寫遞歸,都按照這三要素來寫,可以保證大家寫出正確的遞歸算法!
確定遞歸函數的參數和返回值:
確定哪些參數是遞歸的過程中需要處理的,那么就在遞歸函數里加上這個參數, 并且還要明確每次遞歸的返回值是什么進而確定遞歸函數的返回類型。
確定終止條件:
寫完了遞歸算法, 運行的時候,經常會遇到棧溢出的錯誤,就是沒寫終止條件或者終止條件寫的不對,操作系統也是用一個棧的結構來保存每一層遞歸的信息,如果遞歸沒有終止,操作系統的內存棧必然就會溢出。
確定單層遞歸的邏輯:
確定每一層遞歸需要處理的信息。在這里也就會重復調用自己來實現遞歸的過程。
以下以前序遍歷為例:
確定遞歸函數的參數和返回值:因為要打印出前序遍歷節點的數值,所以參數里需要傳入vector在放節點的數值,除了這一點就不需要在處理什么數據了也不需要有返回值,所以遞歸函數返回類型就是void,代碼如下:
void traversal(TreeNode* cur, vector<int>& vec)
確定終止條件:在遞歸的過程中,如何算是遞歸結束了呢,當然是當前遍歷的節點是空了,那么本層遞歸就要要結束了,所以如果當前遍歷的這個節點是空,就直接return,代碼如下:
if (cur == NULL) return;
確定單層遞歸的邏輯:前序遍歷是中左右的循序,所以在單層遞歸的邏輯,是要先取中節點的數值,代碼如下:
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
單層遞歸的邏輯就是按照中左右的順序來處理的,這樣二叉樹的前序遍歷,基本就寫完了,在看一下完整代碼:
前序遍歷(preorder, 按照先訪問根節點的順序)
class Solution {
public:void traversal(TreeNode* cur, vector<int>& vec) {if (cur == NULL) return;vec.push_back(cur->val); // 根traversal(cur->left, vec); // 左traversal(cur->right, vec); // 右}vector<int> preorderTraversal(TreeNode* root) {vector<int> result;traversal(root, result);return result;}
};
中序遍歷:(inorder, 按照先訪問順序,左 根 右)
void traversal(TreeNode* cur, vector<int>& vec) {if (cur == NULL) return;traversal(cur->left, vec); // 左vec.push_back(cur->val); // 根traversal(cur->right, vec); // 右
}
后序遍歷:(lastorder, 按照先訪問順序,左 右 根 )
void traversal(TreeNode* cur, vector<int>& vec) {if (cur == NULL) return;traversal(cur->left, vec); // 左traversal(cur->right, vec); // 右vec.push_back(cur->val); // 根
}
此時大家可以做一做leetcode上三道題目,分別是:
144.二叉樹的前序遍歷 迭代法
為什么可以用迭代法(非遞歸的方式)來實現二叉樹的前后中序遍歷呢?
我們在棧與隊列:匹配問題都是棧的強項中提到了,遞歸的實現就是:每一次遞歸調用都會把函數的局部變量、參數值和返回地址等壓入調用棧中,然后遞歸返回的時候,從棧頂彈出上一次遞歸的各項參數,所以這就是遞歸為什么可以返回上一層位置的原因。
此時大家應該知道我們用棧也可以是實現二叉樹的前后中序遍歷了。
前序遍歷(迭代法)
我們先看一下前序遍歷。
前序遍歷是中左右,每次先處理的是中間節點,那么先將跟節點放入棧中,然后將右孩子加入棧,再加入左孩子。
為什么要先加入 右孩子,再加入左孩子呢? 因為這樣出棧的時候才是中左右的順序。
動畫如下:
class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {stack<TreeNode*> st;vector<int> result;if (root == NULL) return result;st.push(root);while (!st.empty()) {TreeNode* node = st.top(); // 中st.pop();result.push_back(node->val);if (node->right) st.push(node->right); // 右(空節點不入棧)if (node->left) st.push(node->left); // 左(空節點不入棧)}return result;}
};
94.二叉樹的中序遍歷 中序遍歷(迭代法)
為了解釋清楚,我說明一下 剛剛在迭代的過程中,其實我們有兩個操作:
處理:將元素放進result數組中
訪問:遍歷節點
分析一下為什么剛剛寫的前序遍歷的代碼,不能和中序遍歷通用呢,因為前序遍歷的順序是中左右,先訪問的元素是中間節點,要處理的元素也是中間節點,所以剛剛才能寫出相對簡潔的代碼,因為要訪問的元素和要處理的元素順序是一致的,都是中間節點。
那么再看看中序遍歷,中序遍歷是左中右,先訪問的是二叉樹頂部的節點,然后一層一層向下訪問,直到到達樹左面的最底部,再開始處理節點(也就是在把節點的數值放進result數組中),這就造成了處理順序和訪問順序***是不一致的。****
那么在使用迭代法寫中序遍歷,就需要借用指針的遍歷來幫助訪問節點,棧則用來處理節點上的元素。
動畫如下:
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {vector<int> result;stack<TreeNode*> st;TreeNode* cur = root;while (cur != NULL || !st.empty()) {if (cur != NULL) { // 指針來訪問節點,訪問到最底層st.push(cur); // 將訪問的節點放進棧cur = cur->left; // 左} else {cur = st.top(); // 從棧里彈出的數據,就是要處理的數據(放進result數組里的數據)st.pop();result.push_back(cur->val); // 中cur = cur->right; // 右}}return result;}
};
145.二叉樹的后序遍歷(迭代法)
再來看后序遍歷,先序遍歷是中左右,后續遍歷是左右中,那么我們只需要調整一下先序遍歷的代碼順序,就變成中右左的遍歷順序,然后在反轉result數組,輸出的結果順序就是左右中了,如下圖:
class Solution {
public:vector<int> postorderTraversal(TreeNode* root) {stack<TreeNode*> st;vector<int> result;if (root == NULL) return result;st.push(root);while (!st.empty()) {TreeNode* node = st.top();st.pop();result.push_back(node->val);if (node->left) st.push(node->left); // 相對于前序遍歷,這更改一下入棧順序 (空節點不入棧)if (node->right) st.push(node->right); // 空節點不入棧}reverse(result.begin(), result.end()); // 將結果反轉之后就是左右中的順序了return result;}
};
此時我們實現了前后中遍歷的三種迭代法,是不是發現迭代法實現的先中后序,其實風格也不是那么統一,除了先序和后序,有關聯,中序完全就是另一個風格了,一會用棧遍歷,一會又用指針來遍歷。
二叉樹前中后迭代方式統一寫法
之后我們發現迭代法實現的先中后序,其實風格也不是那么統一,除了先序和后序,有關聯,中序完全就是另一個風格了,一會用棧遍歷,一會又用指針來遍歷。
實踐過的同學,也會發現使用迭代法實現先中后序遍歷,很難寫出統一的代碼,不像是遞歸法,實現了其中的一種遍歷方式,其他兩種只要稍稍改一下節點順序就可以了。
其實針對三種遍歷方式,使用迭代法是可以寫出統一風格的代碼!
重頭戲來了,接下來介紹一下統一寫法。
那我們就將訪問的節點放入棧中,把要處理的節點也放入棧中但是要做標記。
如何標記呢,就是要處理的節點放入棧之后,緊接著放入一個空指針作為標記。 這種方法也可以叫做標記法。
迭代法中序遍歷
中序遍歷代碼如下:(詳細注釋)
class Solution {
public:vector<int> inorderTraversal(TreeNode* root) {vector<int> result;stack<TreeNode*> st;if (root != NULL) st.push(root);while (!st.empty()) {TreeNode* node = st.top();if (node != NULL) {st.pop(); // 將該節點彈出,避免重復操作,下面再將右中左節點添加到棧中if (node->right) st.push(node->right); // 添加右節點(空節點不入棧)st.push(node); // 添加中節點st.push(NULL); // 中節點訪問過,但是還沒有處理,加入空節點做為標記。if (node->left) st.push(node->left); // 添加左節點(空節點不入棧)} else { // 只有遇到空節點的時候,才將下一個節點放進結果集st.pop(); // 將空節點彈出node = st.top(); // 重新取出棧中元素st.pop();result.push_back(node->val); // 加入到結果集}}return result;}
};
迭代法前序遍歷
迭代法前序遍歷代碼如下: (注意此時我們和中序遍歷相比僅僅改變了兩行代碼的順序)
class Solution {
public:vector<int> preorderTraversal(TreeNode* root) {vector<int> result;stack<TreeNode*> st;if (root != NULL) st.push(root);while (!st.empty()) {TreeNode* node = st.top();if (node != NULL) {st.pop();if (node->right) st.push(node->right); // 右if (node->left) st.push(node->left); // 左st.push(node); // 中st.push(NULL);} else {st.pop();node = st.top();st.pop();result.push_back(node->val);}}return result;}
};
迭代法后序遍歷
后續遍歷代碼如下: (注意此時我們和中序遍歷相比僅僅改變了兩行代碼的順序)
class Solution {
public:vector<int> postorderTraversal(TreeNode* root) {vector<int> result;stack<TreeNode*> st;if (root != NULL) st.push(root);while (!st.empty()) {TreeNode* node = st.top();if (node != NULL) {st.pop();st.push(node); // 中st.push(NULL);if (node->right) st.push(node->right); // 右if (node->left) st.push(node->left); // 左} else {st.pop();node = st.top();st.pop();result.push_back(node->val);}}return result;}};
N叉樹的前序遍歷
和二叉樹的道理是一樣的,直接給出代碼了
遞歸C++代碼
class Solution {
private:vector<int> result;void traversal (Node* root) {if (root == NULL) return;result.push_back(root->val);for (int i = 0; i < root->children.size(); i++) {traversal(root->children[i]);}}public:vector<int> preorder(Node* root) {result.clear();traversal(root);return result;}
};
迭代法C++代碼
class Solution {public:vector<int> preorder(Node* root) {vector<int> result;if (root == NULL) return result;stack<Node*> st;st.push(root);while (!st.empty()) {Node* node = st.top();st.pop();result.push_back(node->val);// 注意要倒敘,這樣才能達到前序(中左右)的效果for (int i = node->children.size() - 1; i >= 0; i--) {if (node->children[i] != NULL) {st.push(node->children[i]);}}}return result;}
};
N叉樹的后序遍歷
遞歸C++代碼o
class Solution {
private:vector<int> result;void traversal (Node* root) {if (root == NULL) return;for (int i = 0; i < root->children.size(); i++) { // 子孩子traversal(root->children[i]);}result.push_back(root->val); // 中}public:vector<int> postorder(Node* root) {result.clear();traversal(root);return result;}};
迭代法C++代碼
class Solution {
public:vector<int> postorder(Node* root) {vector<int> result;if (root == NULL) return result;stack<Node*> st;st.push(root);while (!st.empty()) {Node* node = st.top();st.pop();result.push_back(node->val);for (int i = 0; i < node->children.size(); i++) { // 相對于前序遍歷,這里反過來if (node->children[i] != NULL) {st.push(node->children[i]);}}}reverse(result.begin(), result.end()); // 反轉數組return result;}
};
總結
對于二叉樹,我們寫出了前中后序的遞歸,以及對應的迭代法,然后分析出為什么寫出統一風格的迭代法比較難。
進而給出了前中后序統一風格的迭代法代碼。
我們可以寫出了統一風格的迭代法,不用在糾結于前序寫出來了,中序寫不出來的情況了。
但是統一風格的迭代法并不好理解,而且想在面試直接寫出來還有難度的。
所以大家根據自己的個人喜好,對于二叉樹的前中后序遍歷,選擇一種自己容易理解的遞歸和迭代法。
最后在給出N叉樹的前后序遍歷的遞歸與迭代。理解了以上二叉樹的遍歷方式,N叉樹就容易很多了,都是一個套路。
leetcode 589. N 叉樹的前序遍歷
/*
// Definition for a Node.
class Node {
public:int val;vector<Node*> children;Node() {}Node(int _val) { val = _val;}Node(int _val, vector<Node*> _children) {val = _val;children = _children;}
};
*/
class Solution {
public:void __preorder(Node* root,vector<int> &ans){if(root==NULL) return ;ans.push_back(root->val);for(auto x:root->children){__preorder(x,ans);}}vector<int> preorder(Node* root) {vector<int> ans;__preorder(root,ans);return ans;}
};
429. N叉樹的層序遍歷
給定一個 N 叉樹,返回其節點值的層序遍歷。 (即從左到右,逐層遍歷)。
例如,給定一個 3叉樹 :
返回其層序遍歷:
[
[1],
[3,2,4],
[5,6]
]
思路:這道題依舊是模板題**,只不過一個節點有多個孩子了
class Solution {
public:vector<vector<int>> levelOrder(Node* root) {queue<Node*> que;if (root != NULL) que.push(root);vector<vector<int>> result;while (!que.empty()) {int size = que.size();vector<int> vec;for (int i = 0; i < size; i++) { Node* node = que.front();que.pop();vec.push_back(node->val);for (int i = 0; i < node->children.size(); i++) { // 將節點孩子加入隊列if (node->children[i]) que.push(node->children[i]);}}result.push_back(vec);}return result;}
};
515.在每個樹行中找最大值
您需要在二叉樹的每一行中找到最大的值。
思路:層序遍歷,取每一層的最大值
class Solution {
public:vector<int> largestValues(TreeNode* root) {queue<TreeNode*> que;if (root != NULL) que.push(root);vector<int> result;while (!que.empty()) {int size = que.size();int maxValue = INT_MIN; // 取每一層的最大值for (int i = 0; i < size; i++) {TreeNode* node = que.front();que.pop();maxValue = node->val > maxValue ? node->val : maxValue;if (node->left) que.push(node->left);if (node->right) que.push(node->right);}result.push_back(maxValue); // 把最大值放進數組}return result;}
};
116.填充每個節點的下一個右側節點指針
給定一個完美二叉樹,其所有葉子節點都在同一層,每個父節點都有兩個子節點。二叉樹定義如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每個 next 指針,讓這個指針指向其下一個右側節點。如果找不到下一個右側節點,則將 next 指針設置為 NULL。
初始狀態下,所有 next 指針都被設置為 NULL。
思路:本題依然是層序遍歷,只不過在單層遍歷的時候記錄一下本層的頭部節點,然后在遍歷的時候讓前一個節點指向本節點就可以了
class Solution {
public:Node* connect(Node* root) {queue<Node*> que;if (root != NULL) que.push(root);while (!que.empty()) {int size = que.size();vector<int> vec;Node* nodePre;Node* node;for (int i = 0; i < size; i++) {if (i == 0) {nodePre = que.front(); // 取出一層的頭結點que.pop();node = nodePre;} else {node = que.front();que.pop();nodePre->next = node; // 本層前一個節點next指向本節點nodePre = nodePre->next;}if (node->left) que.push(node->left);if (node->right) que.push(node->right);}nodePre->next = NULL; // 本層最后一個節點指向NULL}return root;}
};
117.填充每個節點的下一個右側節點指針II
思路:這道題目說是二叉樹,但116題(上題)目說是完整二叉樹,其實沒有任何差別,一樣的代碼一樣的邏輯一樣的味道
class Solution {
public:Node* connect(Node* root) {queue<Node*> que;if (root != NULL) que.push(root);while (!que.empty()) {int size = que.size();vector<int> vec;Node* nodePre;Node* node;for (int i = 0; i < size; i++) {if (i == 0) {nodePre = que.front(); // 取出一層的頭結點que.pop();node = nodePre;} else {node = que.front();que.pop();nodePre->next = node; // 本層前一個節點next指向本節點nodePre = nodePre->next;}if (node->left) que.push(node->left);if (node->right) que.push(node->right);}nodePre->next = NULL; // 本層最后一個節點指向NULL}return root;}
};
總結
二叉樹的層序遍歷,就是圖論中的廣度優先搜索在二叉樹中的應用,需要借助隊列來實現(此時是不是又發現隊列的應用了)。(雖然不能一口氣打十個,打八個也還行。)
leetcode 103. 二叉樹的鋸齒形層序遍歷
- 先進行層序遍歷,將遍歷結果存儲到二維數組中,
- 依次遍歷存儲所有結果,
- 若對應行數為偶數,倒序該數組,反之,正常存儲。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:void getResult(TreeNode* root,int k,vector<vector<int>> &ans){if(root==NULL) return;if(k==ans.size()) ans.push_back(vector<int>());ans[k].push_back(root->val);getResult(root->left,k+1,ans);getResult(root->right,k+1,ans);return;}void reverse(vector<int> &ans){for(int i=0,j=ans.size()-1;i<j;i++,j--){swap(ans[i],ans[j]);}return;}vector<vector<int>> zigzagLevelOrder(TreeNode* root) {vector<vector<int>> ans;getResult(root,0,ans);for(int i=1;i<ans.size();i+=2){reverse(ans[i]);}return ans;}
};
leetcode 110. 平衡二叉樹
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:int getHeight(TreeNode* root){if(root==NULL) return 0;int l=getHeight(root->left);//遞歸定義,當樹不平衡時候,返回負值int r=getHeight(root->right);if(l<0||r<0) return -2; //不平衡返回負值-2if(abs(l-r)>1) return -2;//平衡>=0return max(l,r)+1;}bool isBalanced(TreeNode* root) {return getHeight(root)>=0;}
};
討論:遞歸函數的意義? 可以表示樹高 可表示樹平衡。。。
leetcode 112. 路徑總和
方法一:深度優先搜索遞歸
注意到本題的要求是,詢問是否有從「根節點」到某個「葉子節點」經過的路徑上的節點之和等于目標和。核心思想是對樹進行一次遍歷,在遍歷時記錄從根節點到當前節點的路徑和,以防止重復計算。
需要特別注意的是,給定的 root 可能為空
思路及算法
觀察要求我們完成的函數,我們可以歸納出它的功能:詢問是否存在從當前節點 root 到葉子節點的路徑,滿足其路徑和為 sum。
假定從根節點到當前節點的值之和為 val,我們可以將這個大問題轉化為一個小問題:是否存在從當前節點的子節點到葉子的路徑,滿足其路徑和為 sum - val。
不難發現這滿足遞歸的性質,若當前節點就是葉子節點,那么我們直接判斷 sum 是否等于 val 即可(因為路徑和已經確定,就是當前節點的值,我們只需要判斷該路徑和是否滿足條件)。若當前節點不是葉子節點,我們只需要遞歸地詢問它的子節點是否能滿足條件即可。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:bool hasPathSum(TreeNode* root, int targetSum) {if(root==NULL) return false;if(root->left == NULL && root->right ==NULL) return root->val==targetSum;if(root->left && hasPathSum(root->left, targetSum- root->val)) return true;//左節點不為空,返回該節點的值與if(root->right && hasPathSum(root->right,targetSum- root->val)) return true;return false;}
};
復雜度分析
時間復雜度:O(N),其中 N 是樹的節點數。對每個節點訪問一次。
空間復雜度:O(H),其中 H 是樹的高度。空間復雜度主要取決于遞歸時棧空間的開銷,最壞情況下,樹呈現鏈狀,空間復雜度為 O(N)。平均情況下樹的高度與節點數的對數正相關,空間復雜度為 O(log N)。
‘’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’’
方法二:廣度優先搜索
思路及算法
首先我們可以想到使用廣度優先搜索的方式,記錄從根節點到當前節點的路徑和,以防止重復計算。
這樣我們使用兩個隊列,分別存儲將要遍歷的節點,以及根節點到這些節點的路徑和即可。
bool hasPathSum(TreeNode* root, int targetSum) {if(root==NULL) return false;queue<TreeNode*> node;queue<int> val;node.push(root);val.push(root->val);while(!node.empty()){TreeNode* temp_node=node.front();int temp_val =val.front();node.pop();val.pop();if(temp_node->left == NULL && temp_node->right == NULL){if(temp_val == targetSum){return true;}continue;}if (temp_node->left != NULL){node.push(temp_node->left);val.push(temp_node->left->val + temp_val);}if (temp_node->right != NULL){node.push(temp_node->right);val.push(temp_node->right->val +temp_val);}}return false;}
復雜度分析
時間復雜度:O(N),其中 N 是樹的節點數。對每個節點訪問一次。
空間復雜度:O(N),其中 N 是樹的節點數。空間復雜度主要取決于隊列的開銷,隊列中的元素個數不會超過樹的節點數。
方法三:棧模擬遞歸(回溯)
如果使用棧模擬遞歸的話,那么如果做回溯呢?
此時棧里一個元素不僅要記錄該節點指針,還要記錄從頭結點到該節點的路徑數值總和。
C++就我們用pair結構來存放這個棧里的元素。
定義為:pair<TreeNode*, int> pair<節點指針,路徑數值> , 這個為棧里的一個元素。
如下代碼是使用棧模擬的前序遍歷,如下: 根左右,棧中順序,相反
bool hasPathSum(TreeNode* root, int sum) {if(root ==NULL) return false;// 棧里要放的是pair<節點指針,路徑數值>stack<pair<TreeNode*,int>> node;node.push(pair<TreeNode*,int>(root,root->val));//初始化while( !node.empty()){pair<TreeNode*,int> temp=node.top();node.pop();//如果當前節點為葉子節點,p判斷該節點的路徑數就等于sum,滿足 返回trueif(!temp.first->left &&!temp.first->right&& sum == temp.second) return true;// 右節點,壓進去一個節點的時候,將該節點的路徑數值也記錄下來if(temp.first->right){int a = temp.first->right->val + temp.second;node.push(pair<TreeNode*,int>(temp.first->right,a));}// 左節點,壓進去一個節點的時候,將該節點的路徑數值也記錄下來if(temp.first->left){int b = temp.first->left->val + temp.second;node.push(pair<TreeNode*,int>(temp.first->left,b));}}wsx return false;}
leetcode 105. 從前序與中序遍歷序列構造二叉樹
遞歸的解法 1 找根位置 2 遞歸建立左子樹 3遞歸建立右子樹
我們知道前序遍歷的第一個元素肯定是根節點,那么前序遍歷的第一個節點在中序位置之前的都是根節點的左子節點,之后的都是根節點的右子節點,我們來簡單畫個圖看一下
這里是隨便舉個例子,我們看到前序遍歷的3肯定是根節點,那么在中序遍歷中,3前面的都是3左子節點的值,3后面的都是3右子節點的值,他真正的結構是這樣的
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {if(preorder.size()==0) return NULL;int pos=0;while(inorder[pos]!=preorder[0]) ++pos;vector<int> l_pre,l_in,r_pre,r_in;for(int i=0;i<pos;i++){l_pre.push_back(preorder[i+1]);l_in.push_back(inorder[i]);}for(int i=pos+1;i<preorder.size();i++){r_pre.push_back(preorder[i]);r_in.push_back(inorder[i]);}TreeNode *root=new TreeNode(preorder[0]);root->left=buildTree(l_pre,l_in);root->right=buildTree(r_pre,r_in);return root;}
2.使用指針解決
我們只需要使用3個指針即可。一個是preStart,他表示的是前序遍歷開始的位置,一個是inStart,他表示的是中序遍歷開始的位置。一個是inEnd,他表示的是中序遍歷結束的位置,我們主要是對中序遍歷的數組進行拆解,下面就以下面的這棵樹來畫個圖分析下
他的前序遍歷是:[3,9,8,5,2,20,15,7]
他的中序遍歷是:[5,8,9,2,3,15,20,7]
這里只要找到了前序遍歷的結點在中序遍歷的位置,我們就可以把中序遍歷數組分解為兩部分了。如果index是前序遍歷的某個值在中序遍歷數組中的索引,以index為根節點劃分的話,那么中序遍歷中
[0,index-1]就是根節點左子樹的所有節點,
[index+1,inorder.length-1]就是根節點右子樹的所有節點。
中序遍歷好劃分,那么前序遍歷呢,如果是左子樹:
preStart=index+1;
如果是右子樹就稍微麻煩點,
preStart=preStart+(index-instart+1);
preStart是當前節點比如m先序遍歷開始的位置,index-instart+1就是當前節點m左子樹的數量加上當前節點的數量,所以preStart+(index-instart+1)就是當前節點m右子樹前序遍歷開始的位置,我們來看下完整代碼
public TreeNode buildTree(int[] preorder, int[] inorder) {return helper(0, 0, inorder.length - 1, preorder, inorder);
}public TreeNode helper(int preStart, int inStart, int inEnd, int[] preorder, int[] inorder) {if (preStart > preorder.length - 1 || inStart > inEnd) {return null;}//創建結點TreeNode root = new TreeNode(preorder[preStart]);int index = 0;//找到當前節點root在中序遍歷中的位置,然后再把數組分兩半for (int i = inStart; i <= inEnd; i++) {if (inorder[i] == root.val) {index = i;break;}}root.left = helper(preStart + 1, inStart, index - 1, preorder, inorder);root.right = helper(preStart + index - inStart + 1, index + 1, inEnd, preorder, inorder);return root;
}
3,使用棧解決
如果使用棧來解決首先要搞懂一個知識點,就是前序遍歷挨著的兩個值比如m和n,他們會有下面兩種情況之一的關系。
1,n是m左子樹節點的值。
2,n是m右子樹節點的值或者是m某個祖先節點的右節點的值。
對于第一個知識點我們很容易理解,如果m的左子樹不為空,那么n就是m左子樹節點的值。
對于第二個問題,如果一個結點沒有左子樹只有右子樹,那么n就是m右子樹節點的值,如果一個結點既沒有左子樹也沒有右子樹,那么n就是m某個祖先節點的右節點,我們只要找到這個祖先節點就好辦了。
搞懂了這點,代碼就很容易寫了,下面看下完整代碼
public TreeNode buildTree(int[] preorder, int[] inorder) {if (preorder.length == 0)return null;Stack<TreeNode> s = new Stack<>();//前序的第一個其實就是根節點TreeNode root = new TreeNode(preorder[0]);TreeNode cur = root;for (int i = 1, j = 0; i < preorder.length; i++) {//第一種情況if (cur.val != inorder[j]) {cur.left = new TreeNode(preorder[i]);s.push(cur);cur = cur.left;} else {//第二種情況j++;//找到合適的cur,然后確定他的右節點while (!s.empty() && s.peek().val == inorder[j]) {cur = s.pop();j++;}//給cur添加右節點cur = cur.right = new TreeNode(preorder[i]);}}return root;
}
leetcode 222. 完全二叉樹的節點個數
方法一:適合所有類型的樹的節點計算
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:int countNodes(TreeNode* root) {if(root==NULL) return 0;return countNodes(root->left)+countNodes(root->right)+1;}
};
方法二:dfs
滿二叉樹
定義:一個二叉樹,如果每一個層的結點數都達到最大值,則這個二叉樹就是滿二叉樹。
也就是說,如果一個二叉樹的層數為K(從1開始的),每一層的節點個數為2^(k-1) ,且結點總數是(2^k)-1.
代碼:k為子樹的層數,那么子樹的節點總數(1<<k)-1;加減運算優先級高于位移運算符
完全二叉樹
定義:它是一棵空樹或者它的葉子節點只出在最后兩層,若最后一層不滿則葉子節點只在最左側。
1.如果根節點的左子樹深度等于右子樹深度,則說明左子樹為滿二叉樹。
//(1<<left_level)注意括號,子樹節點數目為(1<<left_level)-1,再+1(root節點)
if(left_level == right_level){return (1<<left_level) + count(root->right);
}
-
如果根節點的左子樹深度大于右子樹深度,則說明右子樹為滿二叉樹
if(left_level != right_level) {return count(root->left) + (1<<right_level);
}
三種做法的代碼
1.暴力求解,深度遞歸dfs
class Solution {
public:int countNodes(TreeNode* root) {return root == NULL? 0:countNodes(root->left)+countNodes(root->right)+1;}
};
2.運用完全二叉樹的性質(和滿二叉樹結合)
class Solution {
public://計算子樹層數int countlevel(TreeNode* root){int level = 0;while(root!=NULL){root = root->left;level++;}return level;}int countNodes(TreeNode* root) {if(root==NULL)return 0;int leftlevel = countlevel(root->left);int rightlevel = 0;int count = 0;while(root){rightlevel = countlevel(root->right);//左邊子樹是滿二叉樹if(leftlevel == rightlevel){count += (1<<leftlevel);root = root->right;}//右邊子樹是滿二叉樹else{count += (1<<rightlevel);root = root->left;}leftlevel--; }return count;}
};
- 運用編碼和位運算
- 通過二分查找,尋找葉子節點是否存在
class Solution {
public: bool exit(int index,TreeNode* root,int level){//level=3,k子樹的層數對應 100,按位取&計算index,往左還是往右int k = 1<<(level-1);while(root && k>0){//1右移if(index & k){root = root->right;}//0右移else{root = root->left;}k>>=1;}return root!=NULL;}int countNodes(TreeNode* root) {if(root==NULL)return 0;int level = -1;TreeNode* root1 = root;while(root){root=root->left;level++;}int low = (1<<level);int high = (1<<(level+1))-1;int mid = 0;while(low < high){//mid存在,往右側找//因為high = mid-1,縮小的右邊;需要向上取整mid = (high - low + 1) / 2 + low;// mid = low+((high-low+1)>>1);if(exit(mid,root1,level)){low = mid;}//mid不存在,左側找else{high = mid-1;}}return low;}
};
3.方法三:二分查找 + 位運算
對于任意二叉樹,都可以通過廣度優先搜索或深度優先搜索計算節點個數,時間復雜度和空間復雜度都是 O(n),其中 n 是二叉樹的節點個數。這道題規定了給出的是完全二叉樹,因此可以利用完全二叉樹的特性計算節點個數。
規定根節點位于第 00 層,完全二叉樹的最大層數為 h。根據完全二叉樹的特性可知,完全二叉樹的最左邊的節點一定位于最底層,因此從根節點出發,每次訪問左子節點,直到遇到葉子節點,該葉子節點即為完全二叉樹的最左邊的節點,經過的路徑長度即為最大層數h。
當 0≤i<h 時,第 i層包含2^i ,最底層包含的節點數最少為 1,最多為 2^h 。
當最底層包含 1個節點時,完全二叉樹的節點個數是
∑i=0h?12i+1=2h\sum_{i=0}^{h-1}{2^i+1=2^h} i=0∑h?1?2i+1=2h
當最底層包含 2^h個節點時,完全二叉樹的節點個數是
∑i=0h2i=2h+1?1\sum_{i=0}^{h}{2^i}=2^{h+1}-1 i=0∑h?2i=2h+1?1
因此對于最大層數為 h的完全二叉樹,節點個數一定在 [2h,2{h+1}-1][2 h ,2 h+1 ?1] 的范圍內,可以在該范圍內通過二分查找的方式得到完全二叉樹的節點個數。
具體做法是,根據節點個數范圍的上下界得到當前需要判斷的節點個數 k,如果第 k個節點存在,則節點個數一定大于或等于 k,如果第 k個節點不存在,則節點個數一定小于 k,由此可以將查找的范圍縮小一半,直到得到節點個數。
如何判斷第 k 個節點是否存在呢?如果第 k 個節點位于第 h 層,則 k 的二進制表示包含 h+1 位,其中最高位是 1,其余各位從高到低表示從根節點到第 k個節點的路徑,0表示移動到左子節點,1表示移動到右子節點。通過位運算得到第 k 個節點對應的路徑,判斷該路徑對應的節點是否存在,即可判斷第 k 個節點是否存在。
class Solution {
public:int countNodes(TreeNode* root) {if (root == nullptr) {return 0;}int level = 0;TreeNode* node = root;while (node->left != nullptr) {level++;node = node->left;}int low = 1 << level, high = (1 << (level + 1)) - 1;while (low < high) {int mid = (high - low + 1) / 2 + low;if (exists(root, level, mid)) {low = mid;} else {high = mid - 1;}}return low;}bool exists(TreeNode* root, int level, int k) {int bits = 1 << (level - 1);TreeNode* node = root;while (node != nullptr && bits > 0) {if (!(bits & k)) {node = node->left;} else {node = node->right;}bits >>= 1;}return node != nullptr;}
};
1 h = 0/ \2 3 h = 1/ \ /
4 5 6 h = 2
現在這個樹中的值都是節點的編號,最底下的一層的編號是[2^h ,2^h - 1],現在h = 2,也就是4, 5, 6, 7。
4, 5, 6, 7對應二進制分別為 100 101 110 111 不看最左邊的1,從第二位開始,0表示向左,1表示向右,正好可以表示這個節點相對于根節點的位置。
比如4的 00 就表示從根節點 向左 再向左。6的 10 就表示從根節點 向右 再向左那么想訪問最后一層的節點就可以從節點的編號的二進制入手。從第二位開始的二進制位表示了最后一層的節點相對于根節點的位置。
那么就需要一個bits = 2^(h - 1) 這里就是2,對應二進制為010。這樣就可以從第二位開始判斷。
比如看5這個節點存不存在,先通過位運算找到編號為5的節點相對于根節點的位置。010 & 101 發現第二位是0,說明從根節點開始,第一步向左走。
之后將bit右移一位,變成001。001 & 101 發現第三位是1,那么第二步向右走。
最后bit為0,說明已經找到編號為5的這個節點相對于根節點的位置,看這個節點是不是空,不是說明存在,exist返回真
編號為5的節點存在,說明總節點數量一定大于等于5。所以二分那里low = mid再比如看7存不存在,010 & 111 第二位為1,第一部從根節點向右;001 & 111 第三位也為1,第二步繼續向右。
然后判斷當前節點是不是null,發現是null,exist返回假。
編號為7的節點不存在,說明總節點數量一定小于7。所以high = mid - 1
leetcode 劍指 Offer 54. 二叉搜索樹的第k大節點
方法一:遞歸的方式
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {
public:int getCount(TreeNode *root){if(root==NULL) return 0;return getCount(root->left)+getCount(root->right)+1;}int kthLargest(TreeNode* root, int k) {int cnt_r=getCount(root->right);if(k<=cnt_r) return kthLargest(root->right,k);if(k==cnt_r+1) return root->val;return kthLargest(root->left,k-cnt_r-1);}
};
方法二:先求前序遍歷數組,在根據數組求第k大的值
void in_order(TreeNode *root,vector<int> &ans){if(root == NULL) return;in_order(root->left,ans);ans.push_back(root->val);in_order(root->right,ans);return;}int kthLargest(TreeNode* root, int k) {vector<int> ans;in_order(root,ans);return ans[ans.size() -k];}
本文解法基于此性質:二叉搜索樹的中序遍歷為 遞增序列 。
根據以上性質,易得二叉搜索樹的 中序遍歷倒序 為 遞減序列 。
因此,求 “二叉搜索樹第 k大的節點” 可轉化為求 “此樹的中序遍歷倒序的第 k 個節點”。
中序遍歷 為 “左、根、右” 順序,遞歸法代碼如下
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-DTqA705W-1619446816627)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1619020119201.png)]
復雜度分析:
時間復雜度 O(N)O(N) : 當樹退化為鏈表時(全部為右子節點),無論 kk 的值大小,遞歸深度都為 NN ,占用 O(N)O(N) 時間。
空間復雜度 O(N)O(N) : 當樹退化為鏈表時(全部為右子節點),系統使用 O(N)O(N) 大小的棧空間。
leetcode 劍指 Offer 26. 樹的子結構
方法1:遞歸
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {
public:bool isMatch(TreeNode *A,TreeNode *B){if(B==NULL) return true;if(A==NULL) return false;if(A->val !=B->val) return false;return isMatch(A->left,B->left) && isMatch(A->right,B->right);}bool isSubStructure(TreeNode* A, TreeNode* B) {if(B==NULL) return false;if(A==NULL) return false;if(A->val == B->val && isMatch(A,B)) return true;return isSubStructure(A->left,B) || isSubStructure(A->right,B);}
};
bool isSubStructure(TreeNode* A, TreeNode* B) {if(!A || !B) return false;bool res = false;// 如果在 A 中匹配到了與 B 的根節點的值一樣的節點if(A -> val == B -> val) res = doesAHaveB(A, B);// 如果匹配不到,A 往左if(!res) res = isSubStructure(A -> left, B);// 還匹配不到,A 往右if(!res) res = isSubStructure(A -> right, B);return res;}bool doesAHaveB(TreeNode *r1, TreeNode *r2){// 如果 B 已經遍歷完了,trueif(!r2) return true;// 如果 B 還有,但 A 遍歷完了,那 B 剩下的就沒法匹配了,falseif(!r1) return false;// 不相等,falseif(r1 -> val != r2 -> val) return false;return doesAHaveB(r1 -> left, r2 -> left) && doesAHaveB(r1 -> right, r2 -> right);}
leetcode 662. 二叉樹最大寬度
方法 0:寬度優先搜索 [Accepted]
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:typedef pair<TreeNode*,int> PNI;int widthOfBinaryTree(TreeNode* root) {int ans=0;queue<PNI> q;q.push(PNI(root,0));while(!q.empty()){int cnt = q.size();int l=q.front().second,r=q.front().second;for(int i=0;i<cnt;i++){TreeNode *n=q.front().first;int ind=q.front().second;r =ind;if(n->left) q.push(PNI(n->left,(ind-l)*2));if(n->right) q.push(PNI(n->right,(ind-l)*2+1));q.pop();}ans =max(ans,r-l+1);}return ans;}
};
方法框架
解釋
由于我們需要將給定樹中的每個節點都訪問一遍,我們需要遍歷樹。我們可以用深度優先搜索或者寬度優先搜索將樹遍歷。
這個問題中的主要想法是給每個節點一個 position 值,如果我們走向左子樹,那么 position -> position * 2,如果我們走向右子樹,那么 position -> position * 2 + 1。當我們在看同一層深度的位置值 L 和 R 的時候,寬度就是 R - L + 1。
方法 1:寬度優先搜索 [Accepted]
想法和算法
寬度優先搜索順序遍歷每個節點的過程中,我們記錄節點的 position 信息,對于每一個深度,第一個遇到的節點是最左邊的節點,最后一個到達的節點是最右邊的節點。
class Solution {
public:int widthOfBinaryTree(TreeNode* root) {vector<TreeNode*> bfs={root}; //根入隊int Depth=0;int max_width=0;while(1){int sum=bfs.size();int start=0,end=0;bool flag=0;int count=0;//遍歷當前隊列中的元素,即同層的所有節點for(int i=0;i<sum;i++){if(bfs[i]!=NULL){end=i;if(flag==0) start=i;flag=1;//當前隊列的子節點(下一層)入隊bfs.push_back(bfs[i]->left);bfs.push_back(bfs[i]->right);}else{count++;//如果為空,那么兩個空入隊bfs.push_back(NULL);bfs.push_back(NULL);}}if(count==pow(2,Depth))//一層全為空break;if((end-start+1)>max_width)max_width=end-start+1;//當前節點(當前層)出隊bfs.erase(bfs.begin(),bfs.begin()+sum);Depth++;}return max_width;}
};
剛開始用BFS做,主要思想就是:把空節點也入隊,這樣BFS時,就能得到完整的一層節點(空和非空都存在),然后再遍歷該層節點,定位寬度即可,但是這樣做會因為倒數第二個的變態輸入而超時:
復雜度分析
時間復雜度: O(N)O(N),其中 NN 是輸入樹的節點數目,我們遍歷每個節點一遍。
空間復雜度: O(N)O(N),這是 queue 的大小。
方法 2:深度優先搜索 [Accepted]
想法和算法
按照深度優先的順序,我們記錄每個節點的 position 。對于每一個深度,第一個到達的位置會被記錄在 left[depth] 中。
然后對于每一個節點,它對應這一層的可能寬度是 pos - left[depth] + 1 。我們將每一層這些可能的寬度去一個最大值就是答案。
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode() : val(0), left(nullptr), right(nullptr) {}* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}* };*/
class Solution {
public:vector<int> left;int max_width=0;void dfs(TreeNode *root, unsigned long long index,int level){if(root ==NULL) return;if(level >left.size())left.push_back(index);if((index-left[level-1]+1)>max_width)max_width=index-left[level-1] +1;dfs(root->left,2*index,level+1);dfs(root->right,2*index+1,level+1);}int widthOfBinaryTree(TreeNode* root) {if(root ==NULL) return 0;dfs(root,1,1);return max_width;}
};
只需要利用樹和數組轉換關系:若假設當前節點在數組中的下標為i,那么左節點為下標為:2i,右節點為:2i+1
此外,還需要用一個容器保存每層最左節點的下標,容器的大小就是遍歷的深度,這樣遍歷每個節點時,比較當前下標-最左節點下標+1和最大寬度,取較大者,就可以得到樹的最大寬度。
那么怎么判斷當前節點是該層最左節點?根據前序遍歷的特點,最左節點一定是同層中第一個出現的節點,也就是level大于容器大小時,left容器push一個當前下標,這樣再遍歷同層其他節點時,由于left已經push了一個,該判斷也就不會為真了。
leetcode 968. 監控二叉樹
第一種解法
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Zs0HVPbY-1619446816633)(C:\Users\Administrator\AppData\Roaming\Typora\typora-user-images\1619222543902.png)]
/*** Definition for a binary tree node.* struct TreeNode {* int val;* TreeNode *left;* TreeNode *right;* TreeNode(int x) : val(x), left(NULL), right(NULL) {}* };*/
class Solution {
public:void getDP(TreeNode* root,int dp[2][2]){//dp[0][0] //父節點不放攝像頭,本節點也不放攝像頭,像覆蓋掉所有子樹,最少放置的攝像頭數量//dp數組中存放的攝像頭數量 為當前節點的數量,只能為0或者1,10000表示當前這種狀態不存在if(root ==NULL){dp[0][0] =0;dp[0][1] =10000;dp[1][0] =0;dp[1][1] =10000;return;}if(root->left ==NULL && root->right ==NULL){dp[0][0] =10000;dp[0][1] =1;dp[1][0] =0;dp[1][1] =1;}int l[2][2],r[2][2];getDP(root->left,l);getDP(root->right,r);dp[0][0] =min(min(l[0][1]+r[0][0],l[0][0]+r[0][1]),l[0][1]+r[0][1]);//表示三種狀態取最小,l[0][1]表示父節點不放,當前左節點放;r[0][1]表示父節點不放,當 //前右節點放dp[1][0] =min(dp[0][0],l[0][0]+r[0][0]);//dp[1][0]4種情況,表示當前節點的子節點放不放均可dp[0][1] =min(min(l[1][0]+r[1][0],l[1][1]+r[1][1]),min(l[1][0]+r[1][1],l[1][1]+r[1][0]))+1;//求4種情況下和的最小值,前面2種情況,要么多放,要么都不放;后面2種情況一個放,一個不放;dp[1][1] =dp[0][1];//當前節點放一個攝像頭,可以覆蓋周圍return;}int minCameraCover(TreeNode* root) {int dp[2][2];getDP(root,dp);return min(dp[0][1],dp[0][0]);}
};
解題思路
這道題目其實不是那么好理解的,題目舉的示例不是很典型,會誤以為攝像頭必須要放在中間,其實放哪里都可以只要覆蓋了就行。
這道題目難在兩點:
需要確定遍歷方式
需要狀態轉移的方程
我們之前做動態規劃的時候,只要最難的地方在于確定狀態轉移方程,至于遍歷方式無非就是在數組或者二維數組上。
本題并不是動態規劃,其本質是貪心,但我們要確定狀態轉移方式,而且要在樹上進行推導,所以難度就上來了,一些同學知道這道題目難,但其實說不上難點究竟在哪。
需要確定遍歷方式
首先先確定遍歷方式,才能確定轉移方程,那么該如何遍歷呢?
在安排選擇攝像頭的位置的時候,我們要從底向上進行推導,因為盡量讓葉子節點的父節點安裝攝像頭,這樣攝像頭的數量才是最少的 ,這也是本道貪心的原理所在!
如何從低向上推導呢?
就是后序遍歷也就是左右中的順序,這樣就可以從下到上進行推導了。
后序遍歷代碼如下:
int traversal(TreeNode* cur) {// 空節點,該節點有覆蓋if (終止條件) return ;int left = traversal(cur->left); // 左int right = traversal(cur->right); // 右邏輯處理 // 中return ;}
注意在以上代碼中我們取了左孩子的返回值,右孩子的返回值,即 left 和 right, 以后推導中間節點的狀態
需要狀態轉移的方程
確定了遍歷順序,再看看這個狀態應該如何轉移,先來看看每個節點可能有幾種狀態:
可以說有如下三種:
該節點無覆蓋
本節點有攝像頭
本節點有覆蓋
我們分別有三個數字來表示:
0:該節點無覆蓋
1:本節點有攝像頭
2:本節點有覆蓋
大家應該找不出第四個節點的狀態了。
一些同學可能會想有沒有第四種狀態:本節點無攝像頭,其實無攝像頭就是 無覆蓋 或者 有覆蓋的狀態,所以一共還是三個狀態。
那么問題來了,空節點究竟是哪一種狀態呢? 空節點表示無覆蓋? 表示有攝像頭?還是有覆蓋呢?
回歸本質,為了讓攝像頭數量最少,我們要盡量讓葉子節點的父節點安裝攝像頭,這樣才能攝像頭的數量最少。
那么空節點不能是無覆蓋的狀態,這樣葉子節點就可以放攝像頭了,空節點也不能是有攝像頭的狀態,這樣葉子節點的父節點就沒有必要放攝像頭了,而是可以把攝像頭放在葉子節點的爺爺節點上。
所以空節點的狀態只能是有覆蓋,這樣就可以在葉子節點的父節點放攝像頭了
接下來就是遞推關系。
那么遞歸的終止條件應該是遇到了空節點,此時應該返回 2(有覆蓋),原因上面已經解釋過了。
代碼如下:
// 空節點,該節點有覆蓋if (cur == NULL) return 2;
遞歸的函數,以及終止條件已經確定了,再來看單層邏輯處理。
主要有如下四類情況:
情況1:左右節點都有覆蓋
左孩子有覆蓋,右孩子有覆蓋,那么此時中間節點應該就是無覆蓋的狀態了。
如圖:
代碼如下:
// 左右節點都有覆蓋if (left == 2 && right == 2) return 0;
情況2:左右節點至少有一個無覆蓋的情況
如果是以下情況,則中間節點(父節點)應該放攝像頭:
left == 0 && right == 0 左右節點無覆蓋
left == 1 && right == 0 左節點有攝像頭,右節點無覆蓋
left == 0 && right == 1 左節點有無覆蓋,右節點攝像頭
left == 0 && right == 2 左節點無覆蓋,右節點覆蓋
left == 2 && right == 0 左節點覆蓋,右節點無覆蓋
這個不難理解,畢竟有一個孩子沒有覆蓋,父節點就應該放攝像頭。
此時攝像頭的數量要加一,并且 return 1,代表中間節點放攝像頭。
代碼如下:
if (left == 0 || right == 0) {result++;return 1;}
情況3:左右節點至少有一個有攝像頭
如果是以下情況,其實就是 左右孩子節點有一個有攝像頭了,那么其父節點就應該是2(覆蓋的狀態)
left == 1 && right == 2 左節點有攝像頭,右節點有覆蓋
left == 2 && right == 1 左節點有覆蓋,右節點有攝像頭
left == 1 && right == 1 左右節點都有攝像頭
代碼如下:
if (left == 1 || right == 1) return 2;
從這個代碼中,可以看出,如果 left == 1, right == 0 怎么辦?其實這種條件在情況 2 中已經判斷過了,如圖:
這種情況也是大多數同學容易迷惑的情況。
情況4:頭結點沒有覆蓋
以上都處理完了,遞歸結束之后,可能頭結點 還有一個無覆蓋的情況,如圖:
所以遞歸結束之后,還要判斷根節點,如果沒有覆蓋,result++,代碼如下:
int minCameraCover(TreeNode* root) {result = 0;if (traversal(root) == 0) { // root 無覆蓋result++;}return result;
}
以上四種情況我們分析完了,代碼也差不多了,整體代碼如下:
(以下我的代碼是可以精簡的,但是我是為了把情況說清楚,特別把每種情況列出來,因為精簡之后的代碼讀者不好理解。)
C++
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
// 空節點,該節點有覆蓋if (cur == NULL) return 2;int left = traversal(cur->left); // 左int right = traversal(cur->right); // 右// 情況1// 左右節點都有覆蓋if (left == 2 && right == 2) return 0;// 情況2// left == 0 && right == 0 左右節點無覆蓋// left == 1 && right == 0 左節點有攝像頭,右節點無覆蓋// left == 0 && right == 1 左節點有無覆蓋,右節點攝像頭// left == 0 && right == 2 左節點無覆蓋,右節點覆蓋// left == 2 && right == 0 左節點覆蓋,右節點無覆蓋if (left == 0 || right == 0) {result++;return 1;}// 情況3// left == 1 && right == 2 左節點有攝像頭,右節點有覆蓋// left == 2 && right == 1 左節點有覆蓋,右節點有攝像頭// left == 1 && right == 1 左右節點都有攝像頭// 其他情況前段代碼均已覆蓋if (left == 1 || right == 1) return 2;// 以上代碼我沒有使用else,主要是為了把各個分支條件展現出來,這樣代碼有助于讀者理解// 這個 return -1 邏輯不會走到這里。return -1;
}
public:int minCameraCover(TreeNode* root) {result = 0;// 情況4if (traversal(root) == 0) { // root 無覆蓋result++;}return result;}
};
t == 0 || right == 0) {
result++;
return 1;
}
情況3:左右節點至少有一個有攝像頭
如果是以下情況,其實就是 左右孩子節點有一個有攝像頭了,那么其父節點就應該是2(覆蓋的狀態)
left == 1 && right == 2 左節點有攝像頭,右節點有覆蓋
left == 2 && right == 1 左節點有覆蓋,右節點有攝像頭
left == 1 && right == 1 左右節點都有攝像頭
代碼如下:
if (left == 1 || right == 1) return 2;
從這個代碼中,可以看出,如果 left == 1, right == 0 怎么辦?其實這種條件在情況 2 中已經判斷過了,如圖:
這種情況也是大多數同學容易迷惑的情況。
情況4:頭結點沒有覆蓋
以上都處理完了,遞歸結束之后,可能頭結點 還有一個無覆蓋的情況,如圖:
所以遞歸結束之后,還要判斷根節點,如果沒有覆蓋,result++,代碼如下:
int minCameraCover(TreeNode* root) {result = 0;if (traversal(root) == 0) { // root 無覆蓋result++;}return result;
}
以上四種情況我們分析完了,代碼也差不多了,整體代碼如下:
(以下我的代碼是可以精簡的,但是我是為了把情況說清楚,特別把每種情況列出來,因為精簡之后的代碼讀者不好理解。)
C++
class Solution {
private:
int result;
int traversal(TreeNode* cur) {
// 空節點,該節點有覆蓋if (cur == NULL) return 2;int left = traversal(cur->left); // 左int right = traversal(cur->right); // 右// 情況1// 左右節點都有覆蓋if (left == 2 && right == 2) return 0;// 情況2// left == 0 && right == 0 左右節點無覆蓋// left == 1 && right == 0 左節點有攝像頭,右節點無覆蓋// left == 0 && right == 1 左節點有無覆蓋,右節點攝像頭// left == 0 && right == 2 左節點無覆蓋,右節點覆蓋// left == 2 && right == 0 左節點覆蓋,右節點無覆蓋if (left == 0 || right == 0) {result++;return 1;}// 情況3// left == 1 && right == 2 左節點有攝像頭,右節點有覆蓋// left == 2 && right == 1 左節點有覆蓋,右節點有攝像頭// left == 1 && right == 1 左右節點都有攝像頭// 其他情況前段代碼均已覆蓋if (left == 1 || right == 1) return 2;// 以上代碼我沒有使用else,主要是為了把各個分支條件展現出來,這樣代碼有助于讀者理解// 這個 return -1 邏輯不會走到這里。return -1;
}
public:int minCameraCover(TreeNode* root) {result = 0;// 情況4if (traversal(root) == 0) { // root 無覆蓋result++;}return result;}
};
總結
以上是生活随笔為你收集整理的【leetcode】二叉树与经典问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: “俗力非可营”下一句是什么
- 下一篇: 【C++】C++ 强制转换运算符