102.二叉树的层次遍历
leetcode链接:力扣题目链接
视频链接:讲透二叉树的层序遍历 | 广度优先搜索 | LeetCode:102.二叉树的层序遍历 (opens new window)
给你二叉树的根节点 root ,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:[[3],[9,20],[15,7]]
之前使用迭代法进行前中后序遍历时,借助的数据结构是栈,也就是DFS的思想。而层序遍历就是BFS的思想,需要借助队列。
代码如下:
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> q;
vector<vector<int>> res;
if(root == nullptr){
return res;
}
q.push(root);
while(!q.empty()){
vector<int> res1;
unsigned int sz = q.size();
for(int i = 0; i < sz; i++){
TreeNode* cur = q.front();
q.pop();
res1.push_back(cur->val);
if(cur->left != nullptr){
q.push(cur->left);
}
if(cur->right != nullptr) {
q.push(cur->right);
}
}
res.push_back(res1);
}
return res;
}
};
这里有以下几个需要注意的点:
- C++中要先用q.front()取队首元素,再用q.pop()出队;
- 在最开始的时候要用变量sz记录队列长度,因为运行过程中队列长度会变;
- while的每次循环控制的是每一层,for循环则控制的该层的每个节点。
这就是层序遍历的框架,里面的res.push_back就是我们根据需要修改的内容。
用这个方法还可以解决111. 二叉树的最小深度问题。
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明:叶子节点是指没有子节点的节点。
示例 1:
输入:root = [3,9,20,null,null,15,7]
输出:2
示例 2:
输入:root = [2,null,3,null,4,null,5,null,6]
输出:5
class Solution {
public:
int minDepth(TreeNode* root) {
queue<TreeNode*> q;
int depth = 1;
vector<vector<int>> res;
if(root == nullptr){
return 0;
}
q.push(root);
while(!q.empty()){
unsigned int sz = q.size();
for(int i = 0; i < sz; i++){
TreeNode* cur = q.front();
q.pop();
if(cur->left == nullptr && cur->right == nullptr){
return depth;
}
if(cur->left != nullptr){
q.push(cur->left);
}
if(cur->right != nullptr) {
q.push(cur->right);
}
}
depth++;
}
return depth;
}
};
相对于上面的res.push_back(),定义depth,初始化为1(因为记录的是节点数),然后判断叶子结点,就是左右孩子都是空就是叶子结点,遍历一次depth加1,第一次走到叶子结点就是目标depth并返回。
这道题直接使用BFS效率更高,因为可以更早找到目标并返回。BFS适合求最短路径的题目。比如求二叉树的最大深度,则用DFS更好些。
比如说下面的求最大深度的题目:
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) {
return 0;
}
// 利用定义,计算左右子树的最大深度
int leftMax = maxDepth(root->left);
int rightMax = maxDepth(root->right);
// 整棵树的最大深度等于左右子树的最大深度取最大值,
// 然后再加上根节点自己
int res = max(leftMax, rightMax) + 1;
return res;
}
};
使用递归的算法,代码简介易懂,BFS追根溯源往往能找到最深的结果,而BFS找到的是最短的。
实际上用BFS也是可以完成的,层序遍历的次数就是最大深度。
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == NULL) 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;
}
};
1161. 最大层元素和、637. 二叉树的层平均值、515. 每行的最大值
这三题都是类似的,在for循环里面改,while最后收集。以515为例:
class Solution {
public:
vector<int> largestValues(TreeNode* root) {
queue<TreeNode*> q;
vector<int> res;
if(root == nullptr){
return res;
}
q.push(root);
while(!q.empty()){
int sz = q.size();
int max = INT32_MIN;
cout<< max;
for(int i = 0; i < sz; i++){
TreeNode* cur = q.front();
if(cur->val > max){
max = cur->val;
}
q.pop();
if(cur->left != nullptr){
q.push(cur->left);
}
if(cur->right != nullptr){
q.push(cur->right);
}
}
// cout<< max;
res.push_back(max);
}
return res;
}
};
这里需要注意的是int max = INT32_MIN;
,不需要使用-INT_MAX
。
429. N叉树的层序遍历
N叉树的数据结构如下:
class Node {
public:
int val;
vector<Node*> children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector<Node*> _children) {
val = _val;
children = _children;
}
};
可以看到其孩子并不是只有左右两个了,而是一个Node*的数组。因此修改的就是原本左右孩子的入队部分:
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
queue<Node*> q;
vector<vector<int>> res;
if(root == nullptr){
return res;
}
q.push(root);
while(!q.empty()){
vector<int> res1;
unsigned int sz = q.size();
for(int i = 0; i < sz; i++){
Node* cur = q.front();
q.pop();
res1.push_back(cur->val);
// if(cur->left != nullptr){
// q.push(cur->left);
// }
// if(cur->right != nullptr) {
// q.push(cur->right);
// }
for(auto child : cur->children){
if(child != nullptr){
q.push(child);
}
}
}
res.push_back(res1);
}
return res;
}
};
这也就是BFS算法的雏形。
199. 二叉树的右视图
给定一个二叉树的 根节点 root,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
示例 1:
输入: [1,2,3,null,5,null,4]
输出: [1,3,4]
这题是求每一层的最后一个元素,故在for循环里面改,只要i到最后一个,就收集进res:
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
queue<TreeNode*> q;
vector<int> res;
if(root == nullptr){
return res;
}
q.push(root);
while(!q.empty()){
unsigned int sz = q.size();
for(int i = 0; i < sz; i++){
TreeNode* cur = q.front();
cout << cur->val << ' ';
if(i == sz - 1){
res.push_back(cur->val);
}
q.pop();
if(cur->left != nullptr){
q.push(cur->left);
}
if(cur->right != nullptr) {
q.push(cur->right);
}
}
}
return res;
}
};
116. 填充每个节点的下一个右侧节点指针
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
输入:root = [1,2,3,4,5,6,7]
输出:[1,#,2,3,#,4,5,6,7,#]
解释:给定二叉树如图 A 所示,你的函数应该填充它的每个 next 指针,以指向其下一个右侧节点,如图 B 所示。序列化的输出按层序遍历排列,同一层节点由 next 指针连接,'#' 标志着每一层的结束。
示例 2:
输入:root = []
输出:[]
这题的思路跟右视图一样,找到每一层的最后一个节点将其next赋值为null,其他的为下一个也就是当前节点出队后的队首。
class Solution {
public:
Node* connect(Node* root) {
queue<Node*> q;
if (root == nullptr){
return nullptr;
}
q.push(root);
while(!q.empty()){
int sz = q.size();
for(int i = 0; i < sz; i++){
Node* cur = q.front();
q.pop();
if(i == sz - 1){//每一层到头了
cur->next = nullptr;
}else{
cur->next = q.front();
}
if(cur->left != nullptr){
q.push(cur->left);
}
if(cur->right != nullptr){
q.push(cur->right);
}
}
}
return root;
}
};
实际上,通过117.填充每个节点的下一个右侧节点指针II我们可以得到,即使不是完美二叉树也可使用上面的代码,因为在层序遍历中,以我们的上帝视角看,其实结构还是一样的,都处于同一层。
226. 翻转二叉树
leetcode链接:力扣题目链接(opens new window)
视频链接:听说一位巨佬面Google被拒了,因为没写出翻转二叉树 | LeetCode:226.翻转二叉树 (opens new window)
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
示例 1:
输入:root = [4,2,7,1,3,6,9]
输出:[4,7,2,9,6,3,1]
(这里是指针做交换)
有两种方法,遍历法和分解法。
二叉树解题的思维模式分两类:
1、是否可以通过遍历一遍二叉树得到答案?如果可以,用一个
traverse
函数配合外部变量来实现,这叫「遍历」的思维模式。
2、是否可以定义一个递归函数,通过子问题(子树)的答案推导出原问题的答案?如果可以,写出这个递归函数的定义,并充分利用这个函数的返回值,这叫「分解问题」的思维模式。
无论使用哪种思维模式,你都需要思考:
如果单独抽出一个二叉树节点,它需要做什么事情?需要在什么时候(前/中/后序位置)做?其他的节点不用你操心,递归函数会帮你在所有节点上执行相同的操作。
遍历法(代码随想录)
这道题目使用前序和后序比较方便。
回顾一下递归三部曲:
- 确定递归函数的参数和返回值
参数就是要传入节点的指针,不需要其他参数了,通常此时定下来主要参数,如果在写递归的逻辑中发现还需要其他参数的时候,随时补充。
返回值的话其实也不需要,但是题目中给出的要返回root节点的指针,可以直接使用题目定义好的函数,所以就函数的返回类型为TreeNode*
。
TreeNode* invertTree(TreeNode* root)
- 确定终止条件
当前节点为空的时候,就返回
if (root == NULL) return root;
- 确定单层递归的逻辑
前序:中左右,中就是我们要处理的节点。
因为是先前序遍历,所以先进行交换左右孩子节点,然后反转左子树,反转右子树。
swap(root->left, root->right);//中
invertTree(root->left);//左
invertTree(root->right);//右
后序:左右中,就把上面的顺序改一下就可以了:
swap(root->left, root->right);
invertTree(root->left);
invertTree(root->right);
中序:左中右,不太方便,因为有的节点会被交换两次!!!正确的代码应该这么写:
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == NULL) return root;
invertTree(root->left); // 左
swap(root->left, root->right); // 中
invertTree(root->left); // 注意 这里依然要遍历左孩子,因为中间节点已经翻转了
return root;
}
};
这里遍历两边root->left,因为原来的右子树已经被换到左子树去了。
附上我认为比较好的递归的统一框架,加一个traverse()
操作使目标函数与遍历函数分离:
class Solution {
public:
void traverse(TreeNode* node){
if(node == nullptr){
return;
}
//交换每个node的左右节点。
TreeNode* tmp = node->left;
node->left = node->right;
node->right = tmp;
// delete tmp;
traverse(node->left);
traverse(node->right);
}
TreeNode* invertTree(TreeNode* root) {
traverse(root);
return root;
}
};
分解法(labuladong)
// 定义:将以 root 为根的这棵二叉树翻转,返回翻转后的二叉树的根节点
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;
// 和定义逻辑自恰:以 root 为根的这棵二叉树已经被翻转,返回 root
return root;
}
然后思考,对于某一个二叉树节点
x
执行invertTree(x)
,你能利用这个递归函数的定义做点啥?
我可以用
invertTree(x.left)
先把x
的左子树翻转,再用invertTree(x.right)
把x
的右子树翻转,最后把x
的左右子树交换,这恰好完成了以x
为根的整棵二叉树的翻转,即完成了invertTree(x)
的定义。
这个方法还是挺难想的。先掌握遍历法!
101. 对称二叉树
leetcodel链接:力扣题目链接(opens new window)
视频链接:同时操作两个二叉树 | LeetCode:101. 对称二叉树 (opens new window)
遍历法
- 确定递归函数的参数和返回值
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
返回值自然是bool类型。
代码如下:
bool compare(TreeNode* left, TreeNode* right)
我总是纠结递归要不要另外写函数?这边直接定了,不管怎么样都额外写一个函数类似于traverse进行实现。
- 确定终止条件
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点)
- 左节点为空,右节点不为空,不对称,return false
- 左不为空,右为空,不对称 return false
- 左右都为空,对称,返回true
此时已经排除掉了节点为空的情况,那么剩下的就是左右节点不为空:
- 左右都不为空,比较节点数值,不相同就return false
此时左右节点不为空,且数值也不相同的情况我们也处理了。
代码如下:
if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
else if (left->val != right->val) return false; // 注意这里我没有使用else
这边如果if都不进,就是左右节点都不为空且值相等的情况,因此这种情况是继续向下遍历,看左右孩子。就会进入单层递归。
- 确定单层递归的逻辑
这里选用后序遍历(左右中),因为需要不断收集左右孩子的信息给上一个节点,故我们需要处理的“中”应该是最后处理的。实际上后序遍历就是回溯算法的一种表现。
单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。
- 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
- 比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。
- 如果左右都对称就返回true ,有一侧不对称就返回false 。
代码如下:
bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右
bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左
bool isSame = outside && inside; // 左子树:中、 右子树:中(逻辑处理)
return isSame;
可以看出左子树左右中,右子树右左中,把这个遍历顺序也称之为“后序遍历”(尽管不是严格的后序遍历)。
最后的代码如下:
class Solution {
public:
bool compare(TreeNode* left, TreeNode* right) {
// 首先排除空节点的情况
if (left == NULL && right != NULL) return false;
else if (left != NULL && right == NULL) return false;
else if (left == NULL && right == NULL) return true;
// 排除了空节点,再排除数值不相同的情况
else if (left->val != right->val) return false;
// 此时就是:左右节点都不为空,且数值相同的情况
// 此时才做递归,做下一层的判断
bool outside = compare(left->left, right->right); // 左子树:左、 右子树:右
bool inside = compare(left->right, right->left); // 左子树:右、 右子树:左
bool isSame = outside && inside; // 左子树:中、 右子树:中 (逻辑处理)
return isSame;
}
bool isSymmetric(TreeNode* root) {
if (root == NULL) return true;
return compare(root->left, root->right);
}
};
总结
二叉树章节题目的框架意识很强。本质上解决方法两个:递归和迭代,递归注意遍历方式。