102.二叉树的层序遍历
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> qu;
vector<vector<int>> res;
if(root!= nullptr)qu.push(root);
while(!qu.empty()){
int size = qu.size();
vector<int>vec;
// 这里一定要使用固定大小size,不要使用que.size(),因为que.size是不断变化的
for(int i = 0;i<size;i++){
TreeNode* temp = qu.front();
qu.pop();
vec.push_back(temp->val);
if(temp->left)qu.push(temp->left);
if(temp->right)qu.push(temp->right);
}
res.push_back(vec);
}
return res;
}
};
复杂度分析:
记树上所有节点的个数为 n。
时间复杂度:每个点进队出队各一次,故渐进时间复杂度为 O(n)。
空间复杂度:队列中元素的个数不超过 nnn 个,故渐进空间复杂度为 O(n)。
学会二叉树的层序遍历,可以一口气打完以下十题:
- 102.二叉树的层序遍历(opens new window)
- 107.二叉树的层次遍历II(opens new window)
- 199.二叉树的右视图(opens new window)
- 637.二叉树的层平均值(opens new window)
- 429.N叉树的层序遍历(opens new window)
- 515.在每个树行中找最大值(opens new window)
- 116.填充每个节点的下一个右侧节点指针(opens new window)
- 117.填充每个节点的下一个右侧节点指针II(opens new window)
- 104.二叉树的最大深度(opens new window)
- 111.二叉树的最小深度
226.翻转二叉树 (优先掌握递归)
给你一棵二叉树的根节点 root
,翻转这棵二叉树,并返回其根节点。
可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
关键在于遍历顺序,前中后序应该选哪一种遍历顺序? (一些同学这道题都过了,但是不知道自己用的是什么顺序)
遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果
这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了
那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!
递归法
前序
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr)return root;
swap(root->left,root->right);
invertTree(root->left);
invertTree(root->right);
return root;
}
};
后序
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if(root == nullptr)return root;
invertTree(root->left);
invertTree(root->right);
swap(root->left,root->right);
return root;
}
};
迭代法
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
stack<TreeNode*> sta;
if(root ==nullptr)return root;
sta.push(root);
while(!sta.empty()){
TreeNode* temp = sta.top();
sta.pop();
swap(temp->left,temp->right);
if(temp->right)sta.push(temp->right);
if(temp->left)sta.push(temp->left);
}
return root;
}
};
广度优先遍历
广度遍历也是可以翻转这棵树的,因为层序遍历也可以把每个节点的左右孩子都翻转一遍
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
while (!que.empty()) {
int size = que.size();
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
swap(node->left, node->right); // 节点处理
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
}
return root;
}
};
101. 对称二叉树
给你一个二叉树的根节点 root
, 检查它是否轴对称。
首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
比较的是两个子树的里侧和外侧的元素是否相等。如图所示
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
递归法
递归三部曲
1、确定递归函数的参数和返回值
因为我们要比较的是根节点的两个子树是否是相互翻转的,进而判断这个树是不是对称树,所以要比较的是两个树,参数自然也是左子树节点和右子树节点。
返回值自然是bool类型。
代码如下:
bool compare(TreeNode* left, TreeNode* right)
2、确定终止条件
要比较两个节点数值相不相同,首先要把两个节点为空的情况弄清楚!否则后面比较数值的时候就会操作空指针了。
节点为空的情况有:(注意我们比较的其实不是左孩子和右孩子,所以如下我称之为左节点右节点)
- 左节点为空,右节点不为空,不对称,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
注意上面最后一种情况,我没有使用else,而是else if, 因为我们把以上情况都排除之后,剩下的就是 左右节点都不为空,且数值相同的情况。
3、确定单层递归的逻辑
此时才进入单层递归的逻辑,单层递归的逻辑就是处理 左右节点都不为空,且数值相同的情况。
- 比较二叉树外侧是否对称:传入的是左节点的左孩子,右节点的右孩子。
- 比较内侧是否对称,传入左节点的右孩子,右节点的左孩子。
- 如果左右都对称就返回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 compar(TreeNode* left,TreeNode* right){
if(left == nullptr && right!= nullptr)return false;
else if(left !=nullptr && right == nullptr)return false;
else if(left == nullptr && right == nullptr) return true;
else if(left->val != right->val) return false;
bool res1 = compar(left->left,right->right);
bool res2 = compar(left->right,right->left);
return res1 && res2;
}
bool isSymmetric(TreeNode* root) {
if(root == nullptr)return true;
return compar(root->left,root->right);
}
};
迭代
我们引入一个队列,这是把递归程序改写成迭代程序的常用方法。初始化时我们把根节点入队两次。每次提取两个结点并比较它们的值(队列中每两个连续的结点应该是相等的,而且它们的子树互为镜像),然后将两个结点的左右子结点按相反的顺序插入队列中。当队列为空时,或者我们检测到树不对称(即从队列中取出两个不相等的连续结点)时,该算法结束。
if(!temp1&& !temp2)continue;
if((!temp1||!temp2)||(temp1->val != temp2->val)){
return false;
}
如果左节点和右节点都为空,说明对称,继续下一次循环。如果左右节点有一个为空,或者它们的值不相等,说明不对称,返回fals
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if(root == nullptr)return true;
queue<TreeNode*> que;
que.push(root->left);
que.push(root->right);
while(!que.empty()){
TreeNode* temp1 = que.front();que.pop();
TreeNode* temp2 = que.front();que.pop();
if(!temp1&& !temp2)continue;
if(!temp1||!temp2||(temp1->val != temp2->val)){
return false;
}
que.push(temp1->left);
que.push(temp2->right);
que.push(temp1->right);
que.push(temp2->left);
}
return true;
}
};