二叉树递归遍历
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); // 右
}
// 中序遍历
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
vec.push_back(cur->val); // 中
traversal(cur->right, vec); // 右
}
后序遍历
void traversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL) return;
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
vec.push_back(cur->val); // 中
}
vector<int> preorderTraversal(TreeNode* root) {
vector<int> result;
traversal(root, result);
return result;
}
};
二叉树迭代遍历
使用栈来实现迭代遍历,栈的优势是可以记录之前访问到的元素,满足条件才弹出
前序遍历
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
使用栈来记录之前访问到的节点:由于每次只能弹出一个节点,当父节点同时有左右节点时,只有左节点会被弹出,这样右节点就被保存了下来。达到最大树深之后,回退的路径上,右节点才被弹出。
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st; // 每次弹出栈顶元素时,需要继续访问栈顶元素的左右孩子,因此这里要保存完整节点
vector<int> result;
if (root == nullptr) 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;
}
};
后序遍历
先序遍历是中左右,后序遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root == nullptr) 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;
}
};
中序遍历
为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
- 处理:将元素放进result数组中
- 访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
动画如下:
/**
* 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> inorderTraversal(TreeNode* root) {
stack<TreeNode*> st; // 栈用来存储之前访问过的节点
vector<int> result; // 记录中序遍历的节点值
TreeNode* cur = root; // 用来控制节点的入栈和出栈
while(cur != nullptr || !st.empty()) { // cur != nullptr对应cur=root, 分析可知,cur=root出现两次,最开始一次,遍历中一次,此时st都是空的,但循环仍要继续
if (cur != nullptr) {
st.push(cur); // 访问到的节点入栈
cur = cur->left; // 如果当前节点有左节点就一直访问到最底层
} else {
cur = st.top(); // 指针为空,说明访问到了叶子节点的下一层,栈弹出的是左叶子节点
st.pop();
result.push_back(cur->val); // 记录弹出的节点值
cur = cur->right; // 左叶子节点没有右节点,下一轮循环将父节点弹出,
// 再下一轮指针就指向了右叶子节点,将右叶子节点弹出
// 可以看到最左下角的遍历顺序就是左中右
// 左下角遍历完了之后,指针为空,再弹出的就是父父节点了,然后访问右孩子
// 右孩子看作一个小二叉树,访问完之后,再弹出就是父父父节点了
}
}
return result;
}
};
二叉树统一迭代法
迭代法实现的先中后序,其实风格也不是那么统一,除了先序和后序,有关联,中序完全就是另一个风格了,一会用栈遍历,一会又用指针来遍历。
其实针对三种遍历方式,使用迭代法是可以写出统一风格的代码!
以中序遍历为例,上一种迭代法中说使用栈的话,无法同时解决访问节点(遍历节点)和处理节点(将元素放进结果集)不一致的情况。
那我们就将访问的节点放入栈中,把要处理的节点也放入栈中但是要做标记。
如何标记呢,就是要处理的节点放入栈之后,紧接着放入一个空指针作为标记。 这种方法也可以叫做标记法。
/**
* 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> inorderTraversal(TreeNode* root) {
stack<TreeNode*>st; // 使用栈记录访问过的所有节点,在中间节点后加入nullptr作为标记
vector<int> result;
if (root != nullptr) st.push(root);
while(!st.empty()) {
// 每次迭代取出栈顶元素
TreeNode* node = st.top();
// 栈顶不空,先将栈顶元素弹出,再将栈顶元素的右、自身、null、右节点入栈
// 当然栈顶元素也可能没有右节点或左节点,有哪个添加哪个
// 由于左节点后入栈,每次先弹出的就是左节点
// 下一轮对左节点继续上述操作,而右节点只有等到左节点,中节点都处理完了才能重复上述操作
// 试试看,如果栈顶元素是左叶子节点,它没有左右节点,下边的操作就是节点重新入栈加nullptr
// 下一轮循环时,进入else块,空节点和这个节点就被弹出了
// 再下一轮,栈顶元素nullptr, 次一个是中间节点,下一轮进入else两个同时被弹出
// 再下一轮,栈顶元素是右叶子节点,没有左右节点,重新入栈和nullptr入栈,下一轮进入else两个同时被弹出
// 再下一轮,栈顶元素就是父父节点后的nullptr了,下一轮进入else两个同时被弹出
// 再下一轮,栈顶元素是父父节点的右节点
// 将该右节点开始的树当作一个子树,对这个子树的遍历就是上面过程的重复
if (node != nullptr) {
st.pop(); // 先将栈顶元素弹出,再根据右中左顺序将右孩子,父节点,左孩子入栈
if (node->right) st.push(node->right); // 添加右节点(空节点不入栈)
st.push(node); // 中间节点入栈
st.push(nullptr); // 在中间节点后添加nullptr标记
if (node->left) st.push(node->left); // 添加左节点(空节点不入栈)
} else { // 栈顶元素为nullptr, 说明当前节点的左节点已处理,接下来就处理自身
st.pop(); // // 空节点弹出
node = st.top(); // 取出栈顶元素
st.pop(); // 这个元素要被处理了,所以这里也要弹出
result.push_back(node->val);
}
}
return result;
}
};
对于前序遍历,入栈的顺序变成了右,左,中,nullptr(nullptr的位置为什么总是在“中”后面,因为“中”是我们要处理的对象,遍历的分类也是根据中间节点被遍历到的顺序)
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root != nullptr) st.push(root); // 当前节点先入栈,在下面的代码中要再次弹出,并根据右,左,中,nullptr的顺序再次入栈
while (!st.empty()) {
TreeNode* node = st.top();
if (node != nullptr) {
st.pop();
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
st.push(node); // 中
st.push(nullptr);
} else {
st.pop();
node = st.top();
st.pop();
result.push_back(node->val);
}
}
return result;
}
};
对于后序遍历,入栈的顺序变成了中、nullptr、右、左
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> st;
vector<int> result;
if (root != nullptr) st.push(root);
while(!st.empty()) {
TreeNode* node = st.top();
if (node != nullptr) {
// st.pop();
// st.push(node);
st.push(nullptr); // 中
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;
}
};
二叉树的层序遍历
102.二叉树的层序遍历
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:
层序遍历的思想很简单,就是每一层的元素,从左往右依次出队,同时呢,一个元素出队时,将他下层的左右节点入队,这样下层的节点也都是按照从左往右的顺序入队的
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
vector<vector<int>> result;
if (root != nullptr) que.push(root);
while (!que.empty()) {
// 每次循环,开始时que保存的总是当前层的元素
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;
}
};
也可以用递归法实现,采用类似前序递归遍历的思路,只是需要有个depth变量记录每次递归到的层数,然后按照层数,将对应的结果记录到相应层数的数组里。但这种方法耗时且耗空间,并且递归法只能从上到小从左往右层序遍历,试试中序或后序递归遍历,顺序就乱了,不推荐
class Solution {
public:
vector<vector<int>> order(TreeNode* cur, vector<vector<int>>& result, int depth) {
if (cur == nullptr) return result;
if (depth == result.size()) result.push_back(vector<int>()); // 如果递归到了result中没记录到的层,先开辟记录这层结果的空间
result[depth].push_back(cur->val); // 中
order(cur->left, result, depth + 1); // 左
order(cur->right, result, depth + 1); // 右
return result;
}
vector<vector<int>> levelOrder(TreeNode* root) {
vector<vector<int>> result;
int depth = 0; // depth从0计数,便于和result.size()比较
order(root, result, depth);
return result;
}
};
107.二叉树的层序遍历II
相对于102.只需要将最后的result数组翻转一下就行
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
queue<TreeNode*> que;
vector<vector<int>> result;
if (root != nullptr) que.push(root);
while(!que.empty()) {
int size = que.size();
vector<int> vec;
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);
}
reverse(result.begin(), result.end()); // 只需将最后的result数组翻转一下
return result;
}
};
199.二叉树的右视图
层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
queue<TreeNode*> que;
vector<int> result;
if (root != nullptr) que.push(root);
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);
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;
vector<double> result;
if (root != nullptr) que.push(root);
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;
}
};
429.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:
// 2叉数还是N叉数,使用队列来进行层序遍历时,原理都是一样的
// 遍历当前层时,按从左往右的顺序将下一层元素加入队尾
vector<vector<int>> levelOrder(Node* root) {
queue<Node*> que;
vector<vector<int>> result;
if (root != nullptr) que.push(root);
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 (Node* ch: node->children) {
if (ch) que.push(ch);
}
}
result.push_back(vec);
}
return result;
}
};
515.在每个树行中找最大值
层序遍历,取每一层的最大值
class Solution {
public:
vector<int> largestValues(TreeNode* root) {
queue<TreeNode*> que;
vector<int> result;
if (root != nullptr) que.push(root);
while(!que.empty()) {
int size = que.size();
int max_val = INT_MIN; // 记录每层的最大值
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
max_val = node->val > max_val? node->val: max_val; // 当前元素大于之前记录的最大值就更新最大值
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(max_val);
}
return result;
}
};
116.填充每个节点的下一个右侧节点指针
这里的完美二叉树,应该指的是满二叉树。本题依然是层序遍历,只不过在单层遍历的时候使用两个指针,一个记录当前节点,一个记录当前节点的前一个节点,然后在遍历的时候让前一个节点的next指向本节点就可以了,注意第一个节点没有前一个节点
/*
// Definition for a Node.
class Node {
public:
int val;
Node* left;
Node* right;
Node* next;
Node() : val(0), left(NULL), right(NULL), next(NULL) {}
Node(int _val) : val(_val), left(NULL), right(NULL), next(NULL) {}
Node(int _val, Node* _left, Node* _right, Node* _next)
: val(_val), left(_left), right(_right), next(_next) {}
};
*/
class Solution {
public:
Node* connect(Node* root) {
queue<Node*> que;
if (root != nullptr) que.push(root);
while (!que.empty()) {
int size = que.size();
Node* node; // 记录当前节点
Node* nodePre = nullptr; // 记录当前节点的前一个节点,初始化为nullptr, 因为第一个节点不存在前一个节点
for (int i = 0; i < size; i++) {
node = que.front(); // 取出当前节点
que.pop();
if (nodePre != nullptr) nodePre->next = node; // 除第一个节点外,将当前节点的前一个节点指向当前节点
nodePre = node; // 更新
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
nodePre->next = nullptr; // 本层最后一个节点指向NULL
}
return root;
}
};
117.填充每个节点的下一个右侧节点指针II
层序遍历的解法,与二叉树是否是满二叉树无关,因此和116的代码是一样的
104.二叉树的最大深度
使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。
在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
int depth = 0;
queue<TreeNode*> que;
que.push(root);
while(!que.empty()) {
int size = que.size();
depth++; // 记录深度
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
}
return depth;
}
};
111.二叉树的最小深度
class Solution {
public:
int minDepth(TreeNode* root) {
if (root == nullptr) return 0;
int depth = 0;
queue<TreeNode*> que;
que.push(root);
while(!que.empty()) {
int size = que.size();
depth++; // 记录深度
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
// 如果左右子节点都为空,说明遍历到了叶子节点,直接返回
if (!node->left && !node->right) return depth;
}
}
return depth;
}
};