二叉树在学习过程中非常重要,很多底层的实现均是基于二叉树,学习完之后相信自己会有很大的提升!!
在我们解题过程中二叉树有两种主要的形式:满二叉树和完全二叉树。
满二叉树:如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
如图所示:
这棵二叉树为满二叉树,也可以说深度为k,有 2 k − 1 2^k-1 2k−1个节点的二叉树(必须是 2 k − 1 2^k-1 2k−1个节点)。
什么是完全二叉树?
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含1~ 2 h − 1 2^h -1 2h−1 个节点。
之前我们刚刚讲过优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系。
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
左<中<右
下面这两棵树都是搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
如图:
最后一棵 不是平衡二叉树,因为它的左右两个子树的高度差的绝对值超过了1。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn,注意我这里没有说unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。
所以大家使用自己熟悉的编程语言写算法,一定要知道常用的容器底层都是如何实现的,最基本的就是map、set等等,否则自己写的代码,自己对其性能分析都分析不清楚!
二叉树可以链式存储,也可以顺序存储。
那么链式存储方式就用指针, 顺序存储的方式就是用数组。
顾名思义就是顺序存储的元素在内存是连续分布的,而链式存储则是通过指针把分布在散落在各个地址的节点串联一起。
链式存储如图:
链式存储是大家很熟悉的一种方式,那么我们来看看如何顺序存储呢?
其实就是用数组来存储二叉树,顺序存储的方式如图:
如果父节点的数组下表是i,那么它的左孩子就是i * 2 + 1,右孩子就是 i * 2 + 2。
但是用链式表示的二叉树,更有利于我们理解,所以一般我们都是用链式存储二叉树。
所以大家要了解,用数组依然可以表示二叉树。
关于二叉树的遍历方式,要知道二叉树遍历的基本方式都有哪些。
一些同学用做了很多二叉树的题目了,可能知道前中后序遍历,可能知道层序遍历,但是却没有框架。
我这里把二叉树的几种遍历方式列出来,大家就可以一一串起来了。
二叉树主要有两种遍历方式:
深度优先遍历:先往深走,遇到叶子节点再往回走。
广度优先遍历:一层一层的去遍历。
这两种遍历是图论中最基本的两种遍历方式,后面在介绍图论的时候 还会介绍到。
那么从深度优先遍历和广度优先遍历进一步拓展,才有如下遍历方式:
深度优先遍历
广度优先遍历
在深度优先遍历中:有三个顺序,前中后序遍历, 有同学总分不清这三个顺序,经常搞混,我这里教大家一个技巧。
这里前中后,其实指的就是中间节点的遍历顺序,只要大家记住 前中后序指的就是中间节点的位置就可以了。
看如下中间节点的顺序,就可以发现,中间节点的顺序就是所谓的遍历方式
前序遍历:中左右
中序遍历:左中右
后序遍历:左右中
大家可以对着如下图,看看自己理解的前后中序有没有问题。
最后再说一说二叉树中深度优先和广度优先遍历实现方式,我们做二叉树相关题目,经常会使用递归的方式来实现深度优先遍历,也就是实现前中后序遍历,使用递归是比较方便的。
之前我们讲栈与队列的时候,就说过栈其实就是递归的一种是实现结构,也就说前中后序遍历的逻辑其实都是可以借助栈使用非递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
刚刚我们说过了二叉树有两种存储方式顺序存储,和链式存储,顺序存储就是用数组来存,这个定义没啥可说的,我们来看看链式存储的二叉树节点的定义方式。
C++代码如下:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode() : val(-1), 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);
};
大家会发现二叉树的定义 和链表是差不多的,相对于链表 ,二叉树的节点里多了一个指针, 有两个指针,指向左右孩子.
这里要提醒大家要注意二叉树节点定义的书写方式。
在现场面试的时候 面试官可能要求手写代码,所以数据结构的定义以及简单逻辑的代码一定要锻炼白纸写出来。
二叉树是一种基础数据结构,在算法面试中都是常客,也是众多数据结构的基石。
本篇我们介绍了二叉树的种类、存储方式、遍历方式以及定义,比较全面的介绍了二叉树各个方面的重点,帮助大家扫一遍基础。
说道二叉树,就不得不说递归,很多同学对递归都是又熟悉又陌生,递归的代码一般很简短,但每次都是一看就会,一写就废。
一看就会,一写就废!!!
这里帮助大家确定下来递归算法的三个要素。每次写递归,都按照这三要素来写,可以保证大家写出正确的递归算法!
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
以下以前序遍历为例:
void traversal(TreeNode* cur, vector<int>& vec)
if (cur == NULL) return;
vec.push_back(cur->val); // 中
traversal(cur->left, vec); // 左
traversal(cur->right, vec); // 右
单层递归的逻辑就是按照中左右的顺序来处理的,这样二叉树的前序遍历,基本就写完了,在看一下完整代码:
前序遍历:
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;
}
};
中序遍历:
class Solution{
public:
void traversal(TreeNode* cur, vector<int>& vec){
if (cur!=nullptr){
traversal(cur->left, vec);
vec.push_back(cur->val);
traversal(cur->right, vec);
}
}
vector<int> inorderTraversal(TreeNode* root){
vector<int> result;
traversal(root, result);
return result;
}
};
后序遍历:
class Solution{
public:
void traversal(TreeNode* cur, vector<int>& vec){
if (cur!=nullptr){
traversal(cur->left, vec);
traversal(cur->right, vec);
vec.push_back(cur->val);
}
}
};
此时大家可以做一做leetcode上三道题目,分别是:
144.二叉树的前序遍历
145.二叉树的后序遍历
94.二叉树的中序遍历
递归的实现就是:每一次递归调用都会把函数的局部变量、参数值和返回地址等压入调用栈中,然后递归返回的时候,从栈顶弹出上一次递归的各项参数,所以这就是递归为什么可以返回上一层位置的原因。
我们先看一下前序遍历。
前序遍历是中左右,每次先处理的是中间节点,那么先将根节点放入栈中,然后将右孩子加入栈,再加入左孩子。
为什么要先加入 右孩子,再加入左孩子呢? 因为这样出栈的时候才是中左右的顺序。
动画如下:
C++代码
class Solution {
public:
vector<int> preorderTraversal(TreeNode* root) {
stack<TreeNode*> stk;
vector<int> result;
if (root != nullptr) stk.push(root);
while (!stk.empty()){
TreeNode* cur = stk.top();
stk.pop();
result.push_back(cur->val);
if (cur->right) stk.push(cur->right);
if (cur->left) stk.push(cur->left);
}
return result;
}
};
此时会发现貌似使用迭代法写出前序遍历并不难,确实不难。
此时是不是想改一点前序遍历代码顺序就把中序遍历搞出来了?
其实还真不行!
但接下来,再用迭代法写中序遍历的时候,会发现套路又不一样了,目前的前序遍历的逻辑无法直接应用到中序遍历上。
为了解释清楚,我说明一下 刚刚在迭代的过程中,其实我们有两个操作:
1. 处理:将元素放进result数组中
2. 访问:遍历节点
分析一下为什么刚刚写的前序遍历的代码,不能和中序遍历通用呢,因为前序遍历的顺序是中左右,先访问的元素是中间节点,要处理的元素也是中间节点,所以刚刚才能写出相对简洁的代码,因为要访问的元素和要处理的元素顺序是一致的,都是中间节点。
那么再看看中序遍历,中序遍历是左中右,先访问的是二叉树顶部的节点,然后一层一层向下访问,直到到达树左面的最底部,再开始处理节点(也就是在把节点的数值放进result数组中),这就造成了处理顺序和访问顺序是不一致的。
那么在使用迭代法写中序遍历,就需要借用指针的遍历来帮助访问节点,栈则用来处理节点上的元素。
动画如下:
C++代码
class Solution {
public:
vector<int> inorderTraversal(TreeNode* root) {
stack<TreeNode*> stk;
vector<int> result;
TreeNode* cur = root;
// 由于cur表示每次结点位置,存在为null可能,因此循环条件需要设置两种,并且stk在第一次循环时为0,也需要cur判断条件
while (!stk.empty() || cur != nullptr){
if (cur != nullptr){
stk.push(cur);
cur = cur->left;
}
else{
cur = stk.top();
stk.pop();
result.push_back(cur->val);
cur = cur->right;
}
}
return result;
}
};
再来看后序遍历,先序遍历是中左右,后续遍历是左右中,那么我们只需要调整一下先序遍历的代码顺序,就变成中右左的遍历顺序,然后在反转result数组,输出的结果顺序就是左右中了,如下图:
所以后序遍历只需要前序遍历的代码稍作修改就可以了,代码如下:
class Solution {
public:
vector<int> postorderTraversal(TreeNode* root) {
stack<TreeNode*> stk;
vector<int> result;
if (root != nullptr) stk.push(root);
while (!stk.empty()){
TreeNode* cur = stk.top();
stk.pop();
result.push_back(cur->val);
if (cur->left) stk.push(cur->left);
if (cur->right) stk.push(cur->right);
}
reverse(result.begin(), result.end());
return result;
}
};
此时我们用迭代法写出了二叉树的前后中序遍历,大家可以看出前序和中序是完全两种代码风格,并不像递归写法那样代码稍做调整,就可以实现前后中序。
这是因为前序遍历中访问节点(遍历节点)和处理节点(将元素放进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;
}
};
看代码有点抽象我们来看一下动画(中序遍历):
动画中,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;
}
};
此时我们写出了统一风格的迭代法,不用在纠结于前序写出来了,中序写不出来的情况了。
但是统一风格的迭代法并不好理解,而且想在面试直接写出来还有难度的。
所以大家根据自己的个人喜好,对于二叉树的前中后序遍历,选择一种自己容易理解的递归和迭代法。
学会二叉树的层序遍历,可以一口气打完以下十题:
102.二叉树的层序遍历
107.二叉树的层次遍历II
199.二叉树的右视图
637.二叉树的层平均值
429.N叉树的前序遍历
515.在每个树行中找最大值
116.填充每个节点的下一个右侧节点指针
117.填充每个节点的下一个右侧节点指针II
104.二叉树的最大深度
111.二叉树的最小深度
题目链接
给你一个二叉树,请你返回其按 层序遍历 得到的节点值。 (即逐层地,从左到右访问所有节点)。
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。这种遍历的方式和我们之前讲过的都不太一样。
需要借用一个辅助数据结构即队列来实现,队列先进先出,符合一层一层遍历的逻辑,而是用栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
而这种层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
使用队列实现二叉树广度优先遍历,动画如下:
这样就实现了层序从左到右遍历二叉树。
代码
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()){
int size = que.size();
vector<int> vec;
for (int i = 0; i < size; i++){
TreeNode* cur = que.front();
que.pop();
vec.push_back(cur->val);
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
result.push_back(vec);
}
return result;
}
};
题目链接
给定一个二叉树,返回其节点值自底向上的层次遍历。 (即按从叶子节点所在层到根节点所在的层,逐层从左向右遍历)
相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。
C++代码:
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* cur = que.front();
que.pop();
vec.push_back(cur->val);
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
result.push_back(vec);
}
reverse(result.begin(), result.end());
return result;
}
};
题目链接
给定一棵二叉树,想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。
C++代码:
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* cur = que.front();
que.pop();
if (i == size-1) result.push_back(cur->val);
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
}
return result;
}
};
题目链接
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
思路:
本题就是层序遍历的时候把一层求个总和在取一个均值。
C++代码:
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* cur = que.front();
que.pop();
sum += cur->val;
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
result.push_back(sum/size);
}
return result;
}
};
题目链接
给定一个 N 叉树,返回其节点值的层序遍历。 (即从左到右,逐层遍历)。
例如,给定一个 3叉树 :
[ [1], [3,2,4], [5,6] ]
思路:
这道题依旧是模板题,只不过一个节点有多个孩子了
C++代码:
/*
// Definition for a Node.
class Node {
public:
int val;
vector children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector _children) {
val = _val;
children = _children;
}
};
*/
//注意Node定义的不同
class Solution {
public:
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* cur = que.front();
que.pop();
vec.push_back(cur->val);
for (int j = 0; j < cur->children.size(); j++){
que.push(cur->children[j]);
}
}
result.push_back(vec);
}
return result;
}
};
题目链接
您需要在二叉树的每一行中找到最大的值。
层序遍历,取每一层的最大值
C++代码:
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_num = INT_MIN;
for (int i = 0; i < size; i++){
TreeNode* cur = que.front();
que.pop();
max_num = max_num > cur->val? max_num: cur->val;
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
result.push_back(max_num);
}
return result;
}
};
题目链接
给定一个完美二叉树,其所有叶子节点都在同一层,每个父节点都有两个子节点。二叉树定义如下:
struct Node{
int val;
Node* left;
Node* right;
Node* next;
};
填充它的每个 next 指针,让这个指针指向其下一个右侧节点。如果找不到下一个右侧节点,则将 next 指针设置为 NULL。
初始状态下,所有 next 指针都被设置为 NULL。
本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了
/*
// 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 != NULL) que.push(root);
while (!que.empty()){
int size = que.size();
Node* nodepre;
Node* node;
for (int i = 0; i < size; i++){
if (i == 0){
node = que.front();
que.pop();
nodepre = node;
}
else {
node = que.front();
que.pop();
nodepre->next = node;
nodepre = nodepre->next;
}
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
nodepre->next = NULL;
}
return root;
}
};
题目链接
思路:
这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道
C++代码:
/*
// 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 que;
if (root != NULL) que.push(root);
while (!que.empty()){
int size = que.size();
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;
nodePre = nodePre->next;
}
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
nodePre->next = NULL;
}
return root;
}
};
后面讲解
后面讲解
题目链接
我们之前介绍的都是各种方式遍历二叉树,这次要翻转了,感觉还是有点懵逼。
这得怎么翻转呢?
如果要从整个树来看,翻转还真的挺复杂,整个树以中间分割线进行翻转,如图:
可以发现想要翻转它,其实就把每一个节点的左右孩子交换一下就可以了。
关键在于遍历顺序,前中后序应该选哪一种遍历顺序? (一些同学这道题都过了,但是不知道自己用的是什么顺序)
遍历的过程中去翻转每一个节点的左右孩子就可以达到整体翻转的效果。
注意只要把每一个节点的左右孩子翻转一下,就可以达到整体翻转的效果
这道题目使用前序遍历和后序遍历都可以,唯独中序遍历不方便,因为中序遍历会把某些节点的左右孩子翻转了两次!建议拿纸画一画,就理解了
那么层序遍历可以不可以呢?依然可以的!只要把每一个节点的左右孩子翻转一下的遍历方式都是可以的!
先序遍历动画:
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) return nullptr;
//先序遍历--中左右
swap(root->left, root->right); // 中
invertTree(root->left); //左
invertTree(root->right); //右
return root;
}
};
前序遍历
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
stack<TreeNode*> stk;
if (root != nullptr) stk.push(root);
while (!stk.empty()){
TreeNode* cur = stk.top();
stk.pop();
swap(cur->left, cur->right);
if (cur->right) stk.push(cur->right);
if (cur->left) stk.push(cur->left);
}
return root;
}
};
层序遍历
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
queue<TreeNode*> que;
if (root != nullptr) que.push(root);
while (!que.empty()){
int size = que.size();
for (int i = 0; i < size; i++){
TreeNode* cur = que.front();
que.pop();
swap(cur->left, cur->right);
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
}
return root;
}
};
题目链接
给定一个二叉树,检查它是否是镜像对称的。
首先想清楚,判断对称二叉树要比较的是哪两个节点,要比较的可不是左右节点!
对于二叉树是否对称,要比较的是根节点的左子树与右子树是不是相互翻转的,理解这一点就知道了其实我们要比较的是两个树(这两个树是根节点的左右子树),所以在递归遍历的过程中,也是要同时遍历两棵树。
那么如果比较呢?
比较的是两个子树的里侧和外侧的元素是否相等。如图所示:
本题遍历只能是“后序遍历”,因为我们要通过递归函数的返回值来判断两个子树的内侧节点和外侧节点是否相等。
正是因为要遍历两棵树而且要比较内侧和外侧节点,所以准确的来说是一个树的遍历顺序是左右中,一个树的遍历顺序是右左中。
但都可以理解算是后序遍历,尽管已经不是严格上在一个树上进行遍历的后序遍历了。
其实后序也可以理解为是一种回溯,当然这是题外话,讲回溯的时候会重点讲的。
说到这大家可能感觉我有点啰嗦,哪有这么多道理,上来就干就完事了。别急,我说的这些在下面的代码讲解中都有身影。
那么我们先来看看递归法的代码应该怎么写。
class Solution {
public:
bool compare(TreeNode* left, TreeNode* right){
//如果两侧树都为空
if (left == nullptr && right == nullptr) return true;
// 一侧树为空
else if (left == nullptr || right == nullptr) return false;
// 两侧都不为空,但是数值不相同
else if (left->val != right->val) return false;
//比较外侧和内存
bool outside = compare(left->left, right->right);
bool inside = compare(left->right, right->left);
return outside&&inside;
}
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
return compare(root->left, root->right);
}
};
这道题目我们也可以使用迭代法,但要注意,这里的迭代法可不是前中后序的迭代写法,因为本题的本质是判断两个树是否是相互翻转的,其实已经不是所谓二叉树遍历的前中后序的关系了。
这里我们可以使用队列来比较两个树(根节点的左右子树)是否相互翻转,(注意这不是层序遍历)
使用队列
通过队列来判断根节点的左子树和右子树的内侧和外侧是否相等,如动画所示:
代码结果如下:
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* leftNode = que.front(); que.pop();
TreeNode* rightNode = que.front(); que.pop();
if (leftNode == nullptr && rightNode == nullptr) continue;
else if (leftNode == nullptr || rightNode == nullptr) return false;
else if (leftNode->val != rightNode->val) return false;
que.push(leftNode->left);
que.push(rightNode->right);
que.push(leftNode->right);
que.push(rightNode->left);
}
return true;
}
};
使用栈
class Solution {
public:
bool isSymmetric(TreeNode* root) {
if (root == nullptr) return true;
stack<TreeNode*> stk;
stk.push(root->left);
stk.push(root->right);
while (!stk.empty()){
TreeNode* leftNode = stk.top(); stk.pop();
TreeNode* rightNode = stk.top(); stk.pop();
if (leftNode == nullptr && rightNode == nullptr) continue;
else if (leftNode == nullptr || rightNode == nullptr) return false;
else if (leftNode->val != rightNode->val) return false;
stk.push(leftNode->left);
stk.push(rightNode->right);
stk.push(leftNode->right);
stk.push(rightNode->left);
}
return true;
}
};
这两道题目基本和本题是一样的,只要稍加修改就可以AC。
100.相同的树
//递归直接拿下,也可以使用深度优先和广度优先
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if (p == nullptr && q == nullptr) return true;
else if (p == nullptr || q == nullptr) return false;
else if (p->val != q->val) return false;
return (isSameTree(p->left, q->left) && isSameTree(p->right, q->right));
}
};
572.另一个树的子树
class Solution {
public:
bool compare(TreeNode* left, TreeNode* right){
if (left == nullptr && right == nullptr) return true;
else if (left == nullptr || right == nullptr) return false;
else if (left->val != right->val) return false;
return (compare(left->left, right->left) && compare(left->right, right->right));
}
bool isSubtree(TreeNode* root, TreeNode* subRoot) {
if (root == nullptr) return false;
return (compare(root, subRoot) || isSubtree(root->left, subRoot) || isSubtree(root->right, subRoot));
}
};
题目链接
给定一个二叉树,找出其最大深度。
二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
说明: 叶子节点是指没有子节点的节点。
示例: 给定二叉树 [3,9,20,null,null,15,7],
可以看出使用了前序(中左右)的遍历顺序,这才是真正求深度的逻辑!
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == nullptr) return 0;
return max(maxDepth(root->left), maxDepth(root->right))+1;
}
};
队列
层序遍历
class Solution {
public:
int maxDepth(TreeNode* root) {
queue<TreeNode*> que;
if (root != nullptr) que.push(root);
int depth = 0;
while (!que.empty()){
int size = que.size();
depth++;
for (int i = 0; i < size; i++){
TreeNode* cur = que.front();
que.pop();
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
}
return depth;
}
};
n叉树的最大深度
给定一个 n 叉树,找到其最大深度。
最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。
例如,给定一个 3叉树 :
依然可以提供递归法和迭代法,来解决这个问题,思路是和二叉树思路一样的,直接给出代码如下:
迭代
/*
// Definition for a Node.
class Node {
public:
int val;
vector children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
int maxDepth(Node* root) {
if (root == nullptr) return 0;
int depth = 1;
for (int i = 0; i < root->children.size(); i++){
depth = max(depth, maxDepth(root->children[i])+1);
}
return depth;
}
};
递归
/*
// Definition for a Node.
class Node {
public:
int val;
vector children;
Node() {}
Node(int _val) {
val = _val;
}
Node(int _val, vector _children) {
val = _val;
children = _children;
}
};
*/
class Solution {
public:
int maxDepth(Node* root) {
queue<Node*> que;
int depth = 0;
if (root != nullptr) que.push(root);
while (!que.empty()){
int size = que.size();
depth++;
for (int i = 0; i < size; i++){
Node* cur = que.front();
que.pop();
for (int j = 0; j < cur->children.size(); j++){
que.push(cur->children[j]);
}
}
}
return depth;
}
};
题目链接
给定一个二叉树,找出其最小深度。
最小深度是从根节点到最近叶子节点的最短路径上的节点数量。
说明: 叶子节点是指没有子节点的节点。
示例:
给定二叉树 [3,9,20,null,null,15,7],
看完了这篇104.二叉树的最大深度 ,再来看看如何求最小深度。
直觉上好像和求最大深度差不多,其实还是差不少的。
遍历顺序上依然是后序遍历(因为要比较递归返回之后的结果),但在处理中间节点的逻辑上,最大深度很容易理解,最小深度可有一个误区,如图:
这就重新审题了,题目中说的是:最小深度是从根节点到最近叶子节点的最短路径上的节点数量。,注意是叶子节点。
什么是叶子节点,左右孩子都为空的节点才是叶子节点!
class Solution {
public:
int minDepth(TreeNode* root) {
if (root == nullptr) return 0;
else if (root->left == nullptr) return minDepth(root->right)+1;
else if (root->right == nullptr) return minDepth(root->left)+1;
else return min(minDepth(root->left), minDepth(root->right))+1;
}
};
层序遍历
class Solution {
public:
int minDepth(TreeNode* root) {
queue<TreeNode*> que;
int depth = 0;
if (root != nullptr) que.push(root);
while (!que.empty()){
int size = que.size();
depth++;
for (int i = 0; i < size; i++){
TreeNode* cur = que.front();
que.pop();
if (cur->left == nullptr && cur->right == nullptr) return depth;
if (cur->left != nullptr) que.push(cur->left);
if (cur->right != nullptr) que.push(cur->right);
}
}
return depth;
}
};
这里应该进行详细的分析,BFS(Broad First Search,广度优先遍历)和DFS(Depth First Search,深度优先遍历)区别,BFS优先遍历主要在时间效益上高于DFS,但DFS在空间上效益高于BFS。
题目链接
给出一个完全二叉树,求出该树的节点个数。
示例 1:
输入:root = [1,2,3,4,5,6]
输出:6
示例 2:
输入:root = []
输出:0
示例 3:
输入:root = [1]
输出:1
提示:
树中节点的数目范围是[0, 5 * 10^4]
0 <= Node.val <= 5 * 10^4
题目数据保证输入的树是 完全二叉树
本篇给出按照普通二叉树的求法以及利用完全二叉树性质的求法。
普通二叉树
首先按照普通二叉树的逻辑来求。
这道题目的递归法和求二叉树的深度写法类似, 而迭代法,二叉树:层序遍历登场!遍历模板稍稍修改一下,记录遍历的节点数量就可以了。
递归遍历的顺序依然是后序(左右中)。
// 版本一
class Solution {
private:
int getNodesNum(TreeNode* cur) {
if (cur == 0) return 0;
int leftNum = getNodesNum(cur->left); // 左
int rightNum = getNodesNum(cur->right); // 右
int treeNum = leftNum + rightNum + 1; // 中
return treeNum;
}
public:
int countNodes(TreeNode* root) {
return getNodesNum(root);
}
};
精简之后:
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
return countNodes(root->left)+countNodes(root->right)+1;
}
};
时间复杂度:O(n)
空间复杂度:O(logn),算上了递归系统栈占用的空间
层序遍历
class Solution {
public:
int countNodes(TreeNode* root) {
queue<TreeNode*> que;
if (root != nullptr) que.push(root);
int count = 0;
while (!que.empty()){
int size = que.size();
for (int i = 0; i < size; i++){
count++;
TreeNode* cur = que.front();
que.pop();
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
}
return count;
}
};
完全二叉树
完全二叉树只有两种情况,情况一:就是满二叉树,情况二:最后一层叶子节点没有满。
对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。
对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。
完全二叉树(一)如图:
可以看出如果整个树不是满二叉树,就递归其左右孩子,直到遇到满二叉树为止,用公式计算这个子树(满二叉树)的节点数量。
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
TreeNode* left = root->left;
TreeNode* right = root->right;
int leftHeight = 0, rightHeight = 0; // 这里初始为0是有目的的,为了下面求指数方便
while (left) { // 求左子树深度
left = left->left;
leftHeight++;
}
while (right) { // 求右子树深度
right = right->right;
rightHeight++;
}
if (leftHeight == rightHeight) {
return (2 << leftHeight) - 1; // 注意(2<<1) 相当于2^2,所以leftHeight初始为0
}
return countNodes(root->left) + countNodes(root->right) + 1;
}
};
时间复杂度:O(logn * logn)
空间复杂度:O(logn)
题目链接
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
示例 1:
给定二叉树 [3,9,20,null,null,15,7]
返回 true 。
示例 2:
给定二叉树 [1,2,2,3,3,null,null,4,4]
返回 false
咋眼一看这道题目和104.二叉树的最大深度很像,其实有很大区别。
强调一下:
但leetcode中强调的深度和高度很明显是按照节点来计算的,如图:
关于根节点的深度究竟是1 还是 0,不同的地方有不一样的标准,leetcode的题目中都是以节点为一度,即根节点深度是1。但维基百科上定义用边为一度,即根节点的深度是0,我们暂时以leetcode为准(毕竟要在这上面刷题)。
因为求深度可以从上到下去查 所以需要前序遍历(中左右),而高度只能从下到上去查,所以只能后序遍历(左右中)。
有的同学一定疑惑,为什么104.二叉树的最大深度中求的是二叉树的最大深度,也用的是后序遍历。
那是因为代码的逻辑其实是求的根节点的高度,而根节点的高度就是这颗树的最大深度,所以才可以使用后序遍历。
递归,有两种思路,一种是考虑采用自顶向下的比较,根据左右树的最大深度,从上到下逐一比较。另一种是从下到上进行比较,这样的好处是计算梁会小,实现从下到上的迭代,计算不重复,而从上到下,是计算梁存在大量的重复。
自顶向下:
class Solution{
public:
int maxDepth(TreeNode* node){
if (node == nullptr) return 0;
return max(maxDepth(node->left), maxDepth(node->left))+1; //后序遍历
}
bool isBalanced(TreeNode* root){
if (root == nullptr) return true;
return abs(maxDepth(root->left)-maxDepth(root->right))<=1 && isBalanced(root->left) && isBalanced(root->right);
}
};
自底向上
class Solution{
public:
int getDepth(TreeNode* node){
if (node==nullptr) return 0;
int leftDepth = getDepth(node->left);
if (leftDepth==-1) return -1;
int rightDepth = getDepth(node->right);
if (rightDepth==-1) return -1;
return abs(leftDepth-rightDepth)>1? -1: max(leftDepth, rightDepth)+1;
}
bool isBalanced(TreeNode* root){
return getDepth(root)==-1? false:true;
}
};
在104.二叉树的最大深度 中我们可以使用层序遍历来求深度,但是就不能直接用层序遍历来求高度了,这就体现出求高度和求深度的不同。
本题的迭代方式可以先定义一个函数,专门用来求高度。
这个函数通过栈模拟的后序遍历找每一个节点的高度(其实是通过求传入节点为根节点的最大深度来求的高度)
代码如下:
class Solution {
private:
int getDepth(TreeNode* cur) {
stack<TreeNode*> st;
if (cur != NULL) st.push(cur);
int depth = 0; // 记录深度
int result = 0;
while (!st.empty()) {
TreeNode* node = st.top();
if (node != NULL) {
st.pop();
st.push(node); // 中
st.push(NULL);
depth++;
if (node->right) st.push(node->right); // 右
if (node->left) st.push(node->left); // 左
} else {
st.pop();
node = st.top();
st.pop();
depth--;
}
result = result > depth ? result : depth;
}
return result;
}
public:
bool isBalanced(TreeNode* root) {
stack<TreeNode*> st;
if (root == NULL) return true;
st.push(root);
while (!st.empty()) {
TreeNode* node = st.top(); // 中
st.pop();
if (abs(getDepth(node->left) - getDepth(node->right)) > 1) {
return false;
}
if (node->right) st.push(node->right); // 右(空节点不入栈)
if (node->left) st.push(node->left); // 左(空节点不入栈)
}
return true;
}
};
当然此题用迭代法,其实效率很低,因为没有很好的模拟回溯的过程,所以迭代法有很多重复的计算。
虽然理论上所有的递归都可以用迭代来实现,但是有的场景难度可能比较大。
例如:都知道回溯法其实就是递归,但是很少人用迭代的方式去实现回溯算法!
因为对于回溯算法已经是非常复杂的递归了,如果在用迭代的话,就是自己给自己找麻烦,效率也并不一定高。
题目链接
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
这道题目要求从根节点到叶子的路径,所以需要前序遍历,这样才方便让父节点指向孩子节点,找到对应的路径。
在这道题目中将第一次涉及到回溯,因为我们要把路径记录下来,需要回溯来回退一一个路径在进入另一个路径。
前序遍历以及回溯的过程如图:
我们先使用递归的方式,来做前序遍历。要知道递归和回溯就是一家的,本题也需要回溯。
前序遍历
class Solution{
public:
void traversal(TreeNode* node, string path, vector<string>& paths){
if (node != nullptr){
path += to_string(node->val); // 中
//判断是否为叶子结点,如果是一条路就完成。
if (node->left == nullptr && node->right == nullptr){
paths.push_back(path);
}
else{
path += "->";
traversal(node->left, path, paths); // 左
traversal(node->right, path, paths); // 右
}
}
}
vector<string> BinaryTreePaths(TreeNode* root){
if (root == nullptr) return {};
vector<string> paths;
traversal(root, "", paths);
return paths;
}
};
栈
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
stack<TreeNode*> treeSt;// 保存树的遍历节点
stack<string> pathSt; // 保存遍历路径的节点
vector<string> result; // 保存最终路径集合
if (root == NULL) return result;
treeSt.push(root);
pathSt.push(to_string(root->val));
while (!treeSt.empty()) {
TreeNode* node = treeSt.top(); treeSt.pop(); // 取出节点 中
string path = pathSt.top();pathSt.pop(); // 取出该节点对应的路径
if (node->left == NULL && node->right == NULL) { // 遇到叶子节点
result.push_back(path);
}
if (node->right) { // 右
treeSt.push(node->right);
pathSt.push(path + "->" + to_string(node->right->val));
}
if (node->left) { // 左
treeSt.push(node->left);
pathSt.push(path + "->" + to_string(node->left->val));
}
}
return result;
}
};
队列也可以实现,大家可以自己考虑一下
class Solution {
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> paths;
if (root == nullptr) {
return paths;
}
queue<TreeNode*> node_queue;
queue<string> path_queue;
node_queue.push(root);
path_queue.push(to_string(root->val));
while (!node_queue.empty()) {
TreeNode* node = node_queue.front();
string path = path_queue.front();
node_queue.pop();
path_queue.pop();
if (node->left == nullptr && node->right == nullptr) {
paths.push_back(path);
} else {
if (node->left != nullptr) {
node_queue.push(node->left);
path_queue.push(path + "->" + to_string(node->left->val));
}
if (node->right != nullptr) {
node_queue.push(node->right);
path_queue.push(path + "->" + to_string(node->right->val));
}
}
}
return paths;
}
};
题目链接
计算给定二叉树的所有左叶子之和。
示例:
首先要注意是判断左叶子,不是二叉树左侧节点,所以不要上来想着层序遍历。
因为题目中其实没有说清楚左叶子究竟是什么节点,那么我来给出左叶子的明确定义:如果左节点不为空,且左节点没有左右孩子,那么这个节点就是左叶子
大家思考一下如下图中二叉树,左叶子之和究竟是多少?
那么判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子。
如果该节点的左节点不为空,该节点的左节点的左节点为空,该节点的左节点的右节点为空,则找到了一个左叶子。
首先,看一下自己写的代码,有点提升空间
class Solution {
public:
void sum_left(TreeNode* node, int& sum){
if (node == nullptr) return;
if (node->left != nullptr && node->left->left == nullptr && node->left->right == nullptr){
sum += node->left->val;
}
if (node->left) sum_left(node->left, sum);
if (node->right) sum_left(node->right, sum);
}
int sumOfLeftLeaves(TreeNode* root) {
if (root == nullptr) return 0;
int sum = 0;
sum_left(root, sum);
return sum;
}
};
学习别人的代码
class Solution{
public:
int sumOfLeftLeaves(TreeNode* root){
if (root == nullptr) return 0;
int midval = 0;
if (root->left != nullptr && root->left->left == nullptr && root->left->right == nullptr){
midval = root->left->val;
}
return midval + sumOfLeftLeaves(root->left) + sumOfLeftLeaves(root->right);
}
};
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
stack<TreeNode*> stk;
int result = 0;
if (root == nullptr) return 0;
stk.push(root);
while (!stk.empty()){
TreeNode* cur = stk.top();
stk.pop();
if (cur->left != nullptr && cur->left->left == nullptr && cur->left->right == nullptr) result += cur->left->val;
if (cur->right) stk.push(cur->right);
if (cur->left) stk.push(cur->left);
}
return result;
}
};
题目链接
给定一个二叉树,在树的最后一行找到最左边的值。
示例 1:
本地要找出树的最后一行找到最左边的值。此时大家应该想起用层序遍历是非常简单的了,反而用递归的话会比较难一点。
我们依然还是先介绍递归法。
class Solution {
public:
int maxlen = INT_MIN;
int maxleftval;
void traversal(TreeNode* node, int leftLen){
if (node == nullptr) return;
if (!node->left && !node->right){
if (leftLen > maxlen){
maxlen = leftLen;
maxleftval = node->val;
}
}
else{
leftLen++;
traversal(node->left, leftLen);
traversal(node->right, leftLen);
}
}
int findBottomLeftValue(TreeNode* root) {
traversal(root, 0);
return maxleftval;
}
};
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode*> que;
que.push(root);
int result;
while (!que.empty()){
int size = que.size();
for (int i = 0; i<size; i++){
TreeNode* cur = que.front();
que.pop();
if (i == 0) result = cur->val;
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
}
return result;
}
};
class Solution {
public:
int findBottomLeftValue(TreeNode* root) {
queue<TreeNode*> que;
que.push(root);
while (!que.empty()){
int size = que.size();
for (int i = 0; i<size; i++){
TreeNode* cur = que.front();
que.pop();
if (cur->right) que.push(cur->right);
if (cur->left) que.push(cur->left);
}
}
return cur->val;
}
};
题目链接
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。
说明: 叶子节点是指没有子节点的节点。
示例: 给定如下二叉树,以及目标和 sum = 22,
返回 true, 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2。
这道题我们要遍历从根节点到叶子节点的的路径看看总和是不是目标和。
自己写了一个代码,是按照所有路径的字符串修改的,但是没有这么简洁
class Solution {
public:
void pathsum(TreeNode* node, int sumNum, int targetSum, bool& bl){
if (node != nullptr){
sumNum += node->val;
if (node->left == nullptr && node->right == nullptr && sumNum == targetSum){
bl = true;
return;
}
pathsum(node->left, sumNum, targetSum, bl);
pathsum(node->right, sumNum, targetSum, bl);
}
}
bool hasPathSum(TreeNode* root, int targetSum) {
bool bl = false;
pathsum(root, 0, targetSum, bl);
return bl;
}
};
学习《代码随想录》的,确实很不催哦
class Solution {
public:
bool hasPathSum(TreeNode* root, int targetSum) {
if (root == nullptr) return false;
if (root->left == nullptr && root->right == nullptr && targetSum == root->val) return true;
if (root->left == nullptr && root->right == nullptr) return false;
return hasPathSum(root->left, targetSum-root->val) || hasPathSum(root->right, targetSum-root->val);
}
};
题目链接
根据一棵树的中序遍历与后序遍历构造二叉树。
注意: 你可以假设树中没有重复的元素。
例如,给出
中序遍历 inorder = [9,3,15,20,7] 后序遍历 postorder = [9,15,7,20,3] 返回如下的二叉树:
首先回忆一下如何根据两个顺序构造一个唯一的二叉树,相信理论知识大家应该都清楚,就是以 后序数组的最后一个元素为切割点,先切中序数组,根据中序数组,反过来在切后序数组。一层一层切下去,每次后序数组最后一个元素就是节点元素。
如果让我们肉眼看两个序列,画一颗二叉树的话,应该分分钟都可以画出来。
流程如图:
说到一层一层切割,就应该想到了递归。
来看一下一共分几步:
第一步:如果数组大小为零的话,说明是空节点了。
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
第五步:切割后序数组,切成后序左数组和后序右数组
第六步:递归处理左区间和右区间
class Solution {
public:
TreeNode* traversal(vector<int>& inorder, vector<int>& postorder){
//1. 空
if (postorder.size() == 0) return nullptr;
//2.
int rootVal = postorder[postorder.size()-1];
TreeNode* root = new TreeNode(rootVal);
if (postorder.size() == 1) return root;
//3.
int index;
for (index = 0; index < inorder.size(); ++index){
if (rootVal == inorder[index]) break;
}
//4.
vector<int> leftInorder(inorder.begin(), inorder.begin()+index);
vector<int> rightInorder(inorder.begin()+index+1, inorder.end());
vector<int> leftPostorder(postorder.begin(), postorder.begin()+index);
vector<int> rightPostorder(postorder.begin()+index, postorder.end()-1);
//5.
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
return traversal(inorder, postorder);
}
};
考虑分配区间,有两种形式
class Solution {
private:
// 中序区间:[inorderBegin, inorderEnd),后序区间[postorderBegin, postorderEnd)
TreeNode* traversal (vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& postorder, int postorderBegin, int postorderEnd) {
if (postorderBegin == postorderEnd) return NULL;
int rootValue = postorder[postorderEnd - 1];
TreeNode* root = new TreeNode(rootValue);
if (postorderEnd - postorderBegin == 1) return root;
int delimiterIndex;
for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
// 切割中序数组
// 左中序区间,左闭右开[leftInorderBegin, leftInorderEnd)
int leftInorderBegin = inorderBegin;
int leftInorderEnd = delimiterIndex;
// 右中序区间,左闭右开[rightInorderBegin, rightInorderEnd)
int rightInorderBegin = delimiterIndex + 1;
int rightInorderEnd = inorderEnd;
// 切割后序数组
// 左后序区间,左闭右开[leftPostorderBegin, leftPostorderEnd)
int leftPostorderBegin = postorderBegin;
int leftPostorderEnd = postorderBegin + delimiterIndex - inorderBegin; // 终止位置是 需要加上 中序区间的大小size
// 右后序区间,左闭右开[rightPostorderBegin, rightPostorderEnd)
int rightPostorderBegin = postorderBegin + (delimiterIndex - inorderBegin);
int rightPostorderEnd = postorderEnd - 1; // 排除最后一个元素,已经作为节点了
root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, postorder, leftPostorderBegin, leftPostorderEnd);
root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, postorder, rightPostorderBegin, rightPostorderEnd);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return NULL;
// 左闭右开的原则
return traversal(inorder, 0, inorder.size(), postorder, 0, postorder.size());
}
};
class Solution {
public:
TreeNode* traversal(vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& postorder, int postorderBegin, int postorderEnd){
//中序区间[inorderBegin, inorderEnd), 后序区间[postorderBegin, postorderEnd)
//中序区间[inorderBegin, inorderEnd], 后序区间[postorderBegin, postorderEnd]
//没有数字
if (postorderEnd < postorderBegin) return nullptr;
//根据后序遍历取根节点最后一位数字
int rootvalue = postorder[postorderEnd];
TreeNode* root = new TreeNode(rootvalue);
//判断循环是否继续进行
if (postorderEnd == postorderBegin) return root;
//寻找前序遍历中的根节点位置
int midindex;
for (midindex = inorderBegin; midindex < inorderEnd+1; midindex++){
if (inorder[midindex] == rootvalue) break;
}
//调整前序遍历 [inorderBegin, inorderEnd]
//从mid中间切开
int leftinorderBegin = inorderBegin;
int leftinorderEnd = midindex-1;
int rightinorderBegin = midindex+1;
int rightinorderEnd = inorderEnd;
//调整后序遍历 [postorderBegin, postorderEnd]
//根据前序遍历的左边数字数量相同
int leftpostorderBegin = postorderBegin;
int leftpostorderEnd = postorderBegin + (midindex - inorderBegin - 1);
int rightpostorderBegin = leftpostorderEnd + 1;
int rightpostorderEnd = postorderEnd-1;
root->left = traversal(inorder, leftinorderBegin, leftinorderEnd, postorder, leftpostorderBegin, leftpostorderEnd);
root->right = traversal(inorder, rightinorderBegin, rightinorderEnd, postorder, rightpostorderBegin, rightpostorderEnd);
return root;
}
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return nullptr;
return traversal(inorder, 0, inorder.size()-1, postorder, 0, postorder.size()-1);
}
};
题目链接
思路与上题相同
class Solution {
public:
TreeNode* traversal(vector<int>& preorder, vector<int>& inorder){
//前序:左中右
//中序:左右中
// 1. 判断?
if (preorder.size() == 0) return nullptr;
// 2. rootVal
int rootVal = preorder[0];
TreeNode* root = new TreeNode(rootVal);
if (preorder.size() == 1) return root;
// 3. index
int index;
for (index = 0; index < inorder.size(); ++index){
if (rootVal == inorder[index]) break;
}
// 4.划分
vector<int> leftInorder(inorder.begin(), inorder.begin() + index);
vector<int> rightInorder(inorder.begin() + index + 1, inorder.end());
vector<int> leftPreorder(preorder.begin()+1, preorder.begin() + 1 + index);
vector<int> rightPreorder(preorder.begin()+index+1, preorder.end());
// 5. 传递
root->left = traversal(leftPreorder, leftInorder);
root->right = traversal(rightPreorder, rightInorder);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.size() == 0 || inorder.size() == 0) return nullptr;
return traversal(preorder, inorder);
}
};
class Solution {
private:
TreeNode* traversal (vector<int>& inorder, int inorderBegin, int inorderEnd, vector<int>& preorder, int preorderBegin, int preorderEnd) {
if (preorderBegin == preorderEnd) return NULL;
int rootValue = preorder[preorderBegin]; // 注意用preorderBegin 不要用0
TreeNode* root = new TreeNode(rootValue);
if (preorderEnd - preorderBegin == 1) return root;
int delimiterIndex;
for (delimiterIndex = inorderBegin; delimiterIndex < inorderEnd; delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
// 切割中序数组
// 中序左区间,左闭右开[leftInorderBegin, leftInorderEnd)
int leftInorderBegin = inorderBegin;
int leftInorderEnd = delimiterIndex;
// 中序右区间,左闭右开[rightInorderBegin, rightInorderEnd)
int rightInorderBegin = delimiterIndex + 1;
int rightInorderEnd = inorderEnd;
// 切割前序数组
// 前序左区间,左闭右开[leftPreorderBegin, leftPreorderEnd)
int leftPreorderBegin = preorderBegin + 1;
int leftPreorderEnd = preorderBegin + 1 + delimiterIndex - inorderBegin; // 终止位置是起始位置加上中序左区间的大小size
// 前序右区间, 左闭右开[rightPreorderBegin, rightPreorderEnd)
int rightPreorderBegin = preorderBegin + 1 + (delimiterIndex - inorderBegin);
int rightPreorderEnd = preorderEnd;
root->left = traversal(inorder, leftInorderBegin, leftInorderEnd, preorder, leftPreorderBegin, leftPreorderEnd);
root->right = traversal(inorder, rightInorderBegin, rightInorderEnd, preorder, rightPreorderBegin, rightPreorderEnd);
return root;
}
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (inorder.size() == 0 || preorder.size() == 0) return NULL;
// 参数坚持左闭右开的原则
return traversal(inorder, 0, inorder.size(), preorder, 0, preorder.size());
}
};
class Solution {
public:
TreeNode* traveral(vector<int>& preorder, int preorderBegin, int preorderEnd, vector<int>& inorder, int inorderBegin, int inorderEnd){
// [preorderBegin, preorderEnd], [inorderBegin, inorderEnd]
//先序遍历:中左右,中序遍历:左中右
//第一步:判断是否执行
if (preorderEnd < preorderBegin) return nullptr;
//第二步:找到root
int rootvalue = preorder[preorderBegin];
TreeNode* root = new TreeNode(rootvalue);
//判断是否继续执行
if (preorderBegin == preorderEnd) return root;
//第三步:找到分隔值
int midval;
for (midval = inorderBegin; midval < inorderEnd+1; midval++){
if (inorder[midval] == rootvalue) break;
}
//第四步:调整[]
int leftinorderBegin = inorderBegin;
int leftinorderEnd = midval - 1;
int rightinorderBegin = midval + 1;
int rightinorderEnd = inorderEnd;
int leftpreorderBegin = preorderBegin + 1;
int leftpreorderEnd = leftpreorderBegin + (midval - leftinorderBegin -1);
int rightpreorderBegin = leftpreorderEnd + 1;
int rightpreorderEnd = preorderEnd;
root->left = traveral(preorder, leftpreorderBegin, leftpreorderEnd, inorder, leftinorderBegin, leftinorderEnd);
root->right = traveral(preorder, rightpreorderBegin, rightpreorderEnd, inorder, rightinorderBegin, rightinorderEnd);
return root;
}
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
if (preorder.size() == 0 || inorder.size() == 0) return nullptr;
return traveral(preorder, 0, preorder.size()-1, inorder, 0, inorder.size()-1);
}
};
思考题
前序和中序可以唯一确定一颗二叉树。
后序和中序可以唯一确定一颗二叉树。
那么前序和后序可不可以唯一确定一颗二叉树呢?
**前序和后序不能唯一确定一颗二叉树!**因为没有中序遍历无法确定左右部分,也就是无法分割。
举一个例子:
tree1 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。
tree2 的前序遍历是[1 2 3], 后序遍历是[3 2 1]。
那么tree1 和 tree2 的前序和后序完全相同,这是一棵树么,很明显是两棵树!
所以前序和后序不能唯一确定一颗二叉树!
无法避免树对称情况的区分!!
注:
一般情况下:如果需要搜索整颗二叉树,那么递归函数就不要返回值,如果要搜索其中一条符合条件的路径,递归函数就需要返回值,因为遇到符合条件的路径了就要及时返回。
特别是有些时候 递归函数的返回值是bool类型,一些同学会疑惑为啥要加这个,其实就是为了找到一条边立刻返回。
题目链接
给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:
二叉树的根是数组中的最大元素。
左子树是通过数组中最大值左边部分构造出的最大二叉树。
右子树是通过数组中最大值右边部分构造出的最大二叉树。
通过给定的数组构建最大二叉树,并且输出这个树的根节点。
给定的数组的大小在 [1, 1000] 之间。
最大二叉树的构建过程如下:
class Solution {
public:
int maxId(vector<int>& nums, int left, int right){
int maxIdx = left;
for (int i = left; i < right; i++){
if (nums[maxIdx] < nums[i]){
maxIdx = i;
}
}
return maxIdx;
}
TreeNode* maxTree(vector<int>& nums, int left, int right){
if (left >= right) return nullptr;
int maxindex = maxId(nums, left, right);
TreeNode* root = new TreeNode(nums[maxindex]);
root->left = maxTree(nums, left, maxindex);
root->right = maxTree(nums, maxindex+1, right);
return root;
}
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
return maxTree(nums, 0, nums.size());
}
};
题目链接
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
示例 1:
递归
class Solution{
public:
TreeNode* mergeTree(TreeNode* root1, TreeNode* root2){
if (root1 == nullptr) return root2;
if (root2 == nullptr) return root1;
root1->val += root2->val;
root1->left = mergeTree(root1->left, root2->left);
root2->right = mergeTree(root1->right, root2->right);
return root1;
}
};
迭代
class Solution {
public:
TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
if (t1 == NULL) return t2;
if (t2 == NULL) return t1;
queue<TreeNode*> que;
que.push(t1);
que.push(t2);
while(!que.empty()) {
TreeNode* node1 = que.front(); que.pop();
TreeNode* node2 = que.front(); que.pop();
// 此时两个节点一定不为空,val相加
node1->val += node2->val;
// 如果两棵树左节点都不为空,加入队列
if (node1->left != NULL && node2->left != NULL) {
que.push(node1->left);
que.push(node2->left);
}
// 如果两棵树右节点都不为空,加入队列
if (node1->right != NULL && node2->right != NULL) {
que.push(node1->right);
que.push(node2->right);
}
// 当t1的左节点 为空 t2左节点不为空,就赋值过去
if (node1->left == NULL && node2->left != NULL) {
node1->left = node2->left;
}
// 当t1的右节点 为空 t2右节点不为空,就赋值过去
if (node1->right == NULL && node2->right != NULL) {
node1->right = node2->right;
}
}
return t1;
}
};
题目链接
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
递归
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if (root == nullptr) return nullptr;
if (val == root->val) return root;
else if (val > root->val) return searchBST(root->right, val);
else return searchBST(root->left, val);
}
};
迭代
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
queue<TreeNode*> que;
if (root != nullptr) que.push(root);
while (!que.empty()){
int size = que.size();
for (int i = 0; i < size; i++){
TreeNode* cur = que.front();
que.pop();
if (cur->val == val) return cur;
if (cur->left) que.push(cur->left);
if (cur->right) que.push(cur->right);
}
}
return NULL;
}
};
题目链接
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
要知道中序遍历下,输出的二叉搜索树节点的数值是有序序列。
有了这个特性,验证二叉搜索树,就相当于变成了判断一个序列是不是递增的了。
class Solution {
public:
void traversal(TreeNode* node, vector<int>& vec){
if (node != nullptr){
traversal(node->left, vec);
vec.push_back(node->val);
traversal(node->right, vec);
}
}
bool isValidBST(TreeNode* root) {
if (root == nullptr) return true;
vector<int> result;
traversal(root, result);
for (int i = 0; i < result.size()-1; i++){
if (result[i] >= result[i+1]) return false;
}
return true;
}
};
递归
首先,要明确整个题的思路,并不是说每棵树满足各自范围的条件即可,而是要求满足整体的要求,看完这幅图就能够和很好的明白其中的奥秘
相比于使用vector,更高效的方案如下:
class Solution{
public:
TreeNode* pre = nullptr;
bool isValidBST(TreeNode* root){
if (root == nullptr) return true;
//中序遍历
bool left = isValidBST(root->left);
if (pre != nullptr && pre->val <= root->val) return false;
else pre = root;
bool right = isValidBST(root->right);
return left && right;
}
};
迭代
class Solution {
public:
bool isValidBST(TreeNode* root) {
stack<TreeNode*> stk;
TreeNode* cur = root;
TreeNode* pre = nullptr;
while (!stk.empty() || cur != nullptr){
if (cur != nullptr){
stk.push(cur);
cur = cur->left; // 左
}
else{
cur = stk.top(); // 中
stk.pop();
if (pre != nullptr && cur->val <= pre->val) return false; //
else pre = cur;
cur = cur->right; // 右
}
}
return true;
}
};
题目链接
给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。
题目中要求在二叉搜索树上任意两节点的差的绝对值的最小值。
注意是二叉搜索树,二叉搜索树可是有序的。
遇到在二叉搜索树上求什么最值啊,差值之类的,就把它想成在一个有序数组上求最值,求差值,这样就简单多了。
class Solution {
public:
void traversal(TreeNode* node, vector<int>& vec){
//中序遍历 左中右
if (node != nullptr){
traversal(node->left, vec);
vec.push_back(node->val);
traversal(node->right, vec);
}
}
int getMinimumDifference(TreeNode* root) {
vector<int> result;
traversal(root, result);
int minNum = INT_MAX;
for (int i = 1; i < result.size(); i++){
if (minNum > result[i] - result[i-1]) minNum = result[i] - result[i-1];
}
return minNum;
}
};
public:
TreeNode* pre = nullptr; // 前一个结点
int minNum = INT_MAX;
void traversal(TreeNode* root){
if (root != nullptr){
traversal(root->left);
if (pre != nullptr) minNum = min(minNum, root->val - pre->val) ;
pre = root;
traversal(root->right);
}
}
int getMinimumDifference(TreeNode* root) {
traversal(root);
return minNum;
}
};
迭代
class Solution {
public:
int getMinimumDifference(TreeNode* root) {
stack<TreeNode*> stk;
TreeNode* pre = nullptr;
TreeNode* cur = root;
int minNum = INT_MAX;
while (!stk.empty() || cur != nullptr){
if (cur != nullptr){
stk.push(cur);
cur = cur->left;
}
else{
cur = stk.top();
stk.pop();
if (pre != nullptr) minNum = min(minNum, cur->val - pre->val);
pre = cur;
cur = cur->right;
}
}
return minNum;
}
};
题目链接
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。
假定 BST 有如下定义:
例如:
提示:如果众数超过1个,不需考虑输出顺序
进阶:你可以不使用额外的空间吗?(假设由递归产生的隐式调用栈的开销不被计算在内)
这道题目呢,递归法我从两个维度来讲。
首先如果不是二叉搜索树的话,应该怎么解题,是二叉搜索树,又应该如何解题,两种方式做一个比较,可以加深大家对二叉树的理解。
递归法
如果不是二叉搜索树
如果不是二叉搜索树,最直观的方法一定是把这个树都遍历了,用map统计频率,把频率排个序,最后取前面高频的元素的集合。
class Solution {
public:
void findTree(TreeNode* node, unordered_map<int, int>& mp){
if (node != nullptr){
mp[node->val]++;
findTree(node->left, mp);
findTree(node->right, mp);
}
return;
}
bool static compare(const pair<int, int>& a, const pair<int, int>& b){
return a.second > b.second;
}
vector<int> findMode(TreeNode* root) {
if (root == nullptr) return {};
unordered_map<int, int> mp;
findTree(root, mp);
vector<int> result;
vector<pair<int, int>> vec(mp.begin(), mp.end());
sort(vec.begin(), vec.end(), compare);
result.push_back(vec[0].first);
for (int i = 1; i < vec.size(); i++){
if (vec[i].second == vec[0].second) result.push_back(vec[i].first);
else break;
}
return result;
}
};
class Solution {
public:
int count = 0;
int maxcount = 0;
TreeNode* pre = nullptr;
vector<int> result;
void searchBST(TreeNode* cur){
if (cur == nullptr) return;
searchBST(cur->left); // 左
if (pre == nullptr){
count = 1;
}
else if (pre->val == cur->val){
count++;
}
else{
count = 1;
}
pre = cur;
if (count == maxcount){
result.push_back(cur->val);
}
else if (count > maxcount){
maxcount = count;
result.clear();
result.push_back(cur->val);
}
searchBST(cur->right); // 右
return;
}
vector<int> findMode(TreeNode* root) {
searchBST(root);
return result;
}
};
迭代
class Solution {
public:
vector<int> findMode(TreeNode* root) {
stack<TreeNode*> st;
TreeNode* cur = root;
TreeNode* pre = NULL;
int maxCount = 0; // 最大频率
int count = 0; // 统计频率
vector<int> result;
while (cur != NULL || !st.empty()) {
if (cur != NULL) { // 指针来访问节点,访问到最底层
st.push(cur); // 将访问的节点放进栈
cur = cur->left; // 左
} else {
cur = st.top();
st.pop(); // 中
if (pre == NULL) { // 第一个节点
count = 1;
} else if (pre->val == cur->val) { // 与前一个节点数值相同
count++;
} else { // 与前一个节点数值不同
count = 1;
}
if (count == maxCount) { // 如果和最大值相同,放进result中
result.push_back(cur->val);
}
if (count > maxCount) { // 如果计数大于最大值频率
maxCount = count; // 更新最大频率
result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
result.push_back(cur->val);
}
pre = cur;
cur = cur->right; // 右
}
}
return result;
}
};
题目链接
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉树: root = [3,5,1,6,2,0,8,null,null,7,4]
示例 1: 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 1 输出: 3 解释: 节点 5 和节点 1 的最近公共祖先是节点 3。
示例 2: 输入: root = [3,5,1,6,2,0,8,null,null,7,4], p = 5, q = 4 输出: 5 解释: 节点 5 和节点 4 的最近公共祖先是节点 5。因为根据定义最近公共祖先节点可以为节点本身。
说明:
遇到这个题目首先想的是要是能自底向上查找就好了,这样就可以找到公共祖先了。
那么二叉树如何可以自底向上查找呢?
回溯啊,二叉树回溯的过程就是从低到上。
后序遍历就是天然的回溯过程,最先处理的一定是叶子节点。
接下来就看如何判断一个节点是节点q和节点p的公共公共祖先呢。
如果找到一个节点,发现左子树出现结点p,右子树出现节点q,或者 左子树出现结点q,右子树出现节点p,那么该节点就是节点p和q的最近公共祖先。
使用后序遍历,回溯的过程,就是从低向上遍历节点,一旦发现如何这个条件的节点,就是最近公共节点了。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root == q || root == p || root == NULL) return root;
TreeNode* left = lowestCommonAncestor(root->left, p, q);
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if (left != NULL && right != NULL) return root;
if (left == NULL && right != NULL) return right;
else if (left != NULL && right == NULL) return left;
else return NULL;
}
};
题目链接
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
例如,给定如下二叉搜索树: root = [6,2,8,0,4,7,9,null,null,3,5]
示例 1:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8
输出: 6
解释: 节点 2 和节点 8 的最近公共祖先是 6。
示例 2:
输入: root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4
输出: 2
解释: 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身。
说明:
所有节点的值都是唯一的。
p、q 为不同节点且均存在于给定的二叉搜索树中。
做过二叉树:公共祖先问题 (opens new window)题目的同学应该知道,利用回溯从底向上搜索,遇到一个节点的左子树里有p,右子树里有q,那么当前节点就是最近公共祖先。
那么本题是二叉搜索树,二叉搜索树是有序的,那得好好利用一下这个特点。
在有序树里,如果判断一个节点的左子树里有p,右子树里有q呢?
其实只要从上到下遍历的时候,cur节点是数值在[p, q]区间中则说明该节点cur就是最近公共祖先了。
理解这一点,本题就很好解了。
和二叉树:公共祖先问题 (opens new window)不同,普通二叉树求最近公共祖先需要使用回溯,从底向上来查找,二叉搜索树就不用了,因为搜索树有序(相当于自带方向),那么只要从上向下遍历就可以了。
那么我们可以采用前序遍历(其实这里没有中节点的处理逻辑,遍历顺序无所谓了)。
如图所示:p为节点3,q为节点5
可以看出直接按照指定的方向,就可以找到节点4,为最近公共祖先,而且不需要遍历整棵树,找到结果直接返回!
递归法
前序遍历
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (p->val < root->val && q->val < root->val) return lowestCommonAncestor(root->left, p, q);
else if (p->val > root->val && q->val > root->val) return lowestCommonAncestor(root->right, p, q);
return root;
}
};
迭代法
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
while(root) {
if (root->val > p->val && root->val > q->val) {
root = root->left;
} else if (root->val < p->val && root->val < q->val) {
root = root->right;
} else return root;
}
return NULL;
}
};
题目链接
给定二叉搜索树(BST)的根节点和要插入树中的值,将值插入二叉搜索树。 返回插入后二叉搜索树的根节点。 输入数据保证,新值和原始二叉搜索树中的任意节点值都不同。
注意,可能存在多种有效的插入方式,只要树在插入后仍保持为二叉搜索树即可。 你可以返回任意有效的结果。
给定的树上的节点数介于 0 和 10^4 之间
每个节点都有一个唯一整数值,取值范围从 0 到 10^8
-10^8 <= val <= 10^8
新值和原始二叉搜索树中的任意节点值都不同
其实这道题目其实是一道简单题目,但是题目中的提示:有多种有效的插入方式,还可以重构二叉搜索树,一下子吓退了不少人,瞬间感觉题目复杂了很多。
其实可以不考虑题目中提示所说的改变树的结构的插入方式。
如下演示视频中可以看出:只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。
例如插入元素10 ,需要找到末尾节点插入便可,一样的道理来插入元素15,插入元素0,插入元素6,需要调整二叉树的结构么? 并不需要。。
只要遍历二叉搜索树,找到空节点 插入元素就可以了,那么这道题其实就简单了。
接下来就是遍历二叉搜索树的过程了。
递归
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == nullptr){
return new TreeNode(val);
};
if (root->val > val) root->left = insertIntoBST(root->left, val);
if (root->val < val) root->right = insertIntoBST(root->right, val);
return root;
}
};
迭代
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
if (root == NULL) {
TreeNode* node = new TreeNode(val);
return node;
}
TreeNode* cur = root;
TreeNode* parent = root; // 这个很重要,需要记录上一个节点,否则无法赋值新节点
while (cur != NULL) {
parent = cur;
if (cur->val > val) cur = cur->left;
else cur = cur->right;
}
TreeNode* node = new TreeNode(val);
if (val < parent->val) parent->left = node;// 此时是用parent节点的进行赋值
else parent->right = node;
return root;
}
};
题目链接
给定一个二叉搜索树的根节点 root 和一个值 key,删除二叉搜索树中的 key 对应的节点,并保证二叉搜索树的性质不变。返回二叉搜索树(有可能被更新)的根节点的引用。
一般来说,删除节点可分为两个步骤:
首先找到需要删除的节点; 如果找到了,删除它。 说明: 要求算法时间复杂度为 O(h),h 为树的高度。
搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑,做好心里准备
有以下五种情况:
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root; // 第一种情况:没找到删除的节点,遍历到空节点直接返回了
if (root->val == key) {
// 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
if (root->left == nullptr && root->right == nullptr) {
///! 内存释放
delete root;
return nullptr;
}
// 第三种情况:其左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
else if (root->left == nullptr) {
auto retNode = root->right;
///! 内存释放
delete root;
return retNode;
}
// 第四种情况:其右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
else if (root->right == nullptr) {
auto retNode = root->left;
///! 内存释放
delete root;
return retNode;
}
// 第五种情况:左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
// 并返回删除节点右孩子为新的根节点。
else {
TreeNode* cur = root->right; // 找右子树最左面的节点
while(cur->left != nullptr) {
cur = cur->left;
}
cur->left = root->left; // 把要删除的节点(root)左子树放在cur的左孩子的位置
TreeNode* tmp = root; // 把root节点保存一下,下面来删除
root = root->right; // 返回旧root的右孩子作为新root
delete tmp; // 释放节点内存(这里不写也可以,但C++最好手动释放一下吧)
return root;
}
}
if (root->val > key) root->left = deleteNode(root->left, key);
if (root->val < key) root->right = deleteNode(root->right, key);
return root;
}
};
迭代
class Solution {
private:
// 将目标节点(删除节点)的左子树放到 目标节点的右子树的最左面节点的左孩子位置上
// 并返回目标节点右孩子为新的根节点
// 是动画里模拟的过程
TreeNode* deleteOneNode(TreeNode* target) {
if (target == nullptr) return target;
if (target->right == nullptr) return target->left;
TreeNode* cur = target->right;
while (cur->left) {
cur = cur->left;
}
cur->left = target->left;
return target->right;
}
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
TreeNode* cur = root;
TreeNode* pre = nullptr; // 记录cur的父节点,用来删除cur
while (cur) {
if (cur->val == key) break;
pre = cur;
if (cur->val > key) cur = cur->left;
else cur = cur->right;
}
if (pre == nullptr) { // 如果搜索树只有头结点
return deleteOneNode(cur);
}
// pre 要知道是删左孩子还是右孩子
if (pre->left && pre->left->val == key) {
pre->left = deleteOneNode(cur);
}
if (pre->right && pre->right->val == key) {
pre->right = deleteOneNode(cur);
}
return root;
}
};
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
if (root->val == key){
if (root->left == nullptr){
TreeNode* node = root->right;
delete root;
return node;
}
else if (root->right == nullptr){
TreeNode* node = root->left;
delete root;
return node;
}
else{
TreeNode* node = root->right;
while (node->left){
node = node->left;
}
root->val = node->val;
root->right = deleteNode(root->right, node->val);
}
}
else if (root->val > key) root->left = deleteNode(root->left, key);
else if (root->val < key) root->right = deleteNode(root->right, key);
return root;
}
};
普通二叉树
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
if (root->val == key){
if (root->left == nullptr){
TreeNode* node = root->right;
delete root;
return node;
}
else if (root->right == nullptr){
TreeNode* node = root->left;
delete root;
return node;
}
else{
TreeNode* node = root->right;
while (node->left){
node = node->left;
}
swap(node->val, root->val);
}
}
root->left = deleteNode(root->left, key);
root->right = deleteNode(root->right, key);
return root;
}
};
题目链接
给定一个二叉搜索树,同时给定最小边界L 和最大边界 R。通过修剪二叉搜索树,使得所有节点的值在[L, R]中 (R>=L) 。你可能需要改变树的根节点,所以结果应当返回修剪好的二叉搜索树的新的根节点。
递归法
直接想法就是:递归处理,然后遇到 root->val < low || root->val > high 的时候直接return NULL,一波修改,赶紧利落。
不难写出如下代码:
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (root == nullptr || root->val < low || root->val > high) return nullptr;
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};
然而[1, 3]区间在二叉搜索树的中可不是单纯的节点3和左孩子节点0就决定的,还要考虑节点0的右子树。
我们在重新关注一下第二个示例,如图:
所以以上的代码是不可行的!
从图中可以看出需要重构二叉树,想想是不是本题就有点复杂了。
其实不用重构那么复杂。
在上图中我们发现节点0并不符合区间要求,那么将节点0的右孩子 节点2 直接赋给 节点3的左孩子就可以了(就是把节点0从二叉树中移除),如图:
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (root == nullptr) return nullptr;
if (root->val < low) return trimBST(root->right, low, high);
if (root->val > high) return trimBST(root->left, low, high);
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};
迭代法
因为二叉搜索树的有序性,不需要使用栈模拟递归的过程。
在剪枝的时候,可以分为三步:
代码如下:
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int L, int R) {
if (!root) return nullptr;
// 处理头结点,让root移动到[L, R] 范围内,注意是左闭右闭
while (root != nullptr && (root->val < L || root->val > R)) {
if (root->val < L) root = root->right; // 小于L往右走
else root = root->left; // 大于R往左走
}
TreeNode *cur = root;
// 此时root已经在[L, R] 范围内,处理左孩子元素小于L的情况
while (cur != nullptr) {
while (cur->left && cur->left->val < L) {
cur->left = cur->left->right;
}
cur = cur->left;
}
cur = root;
// 此时root已经在[L, R] 范围内,处理右孩子大于R的情况
while (cur != nullptr) {
while (cur->right && cur->right->val > R) {
cur->right = cur->right->left;
}
cur = cur->right;
}
return root;
}
};
题目链接
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
示例:
题目中说要转换为一棵高度平衡二叉搜索树。这和转换为一棵普通二叉搜索树有什么差别呢?
其实这里不用强调平衡二叉搜索树,数组构造二叉树,构成平衡树是自然而然的事情,因为大家默认都是从数组中间位置取值作为节点元素,一般不会随机取,所以想构成不平衡的二叉树是自找麻烦。
本质就是寻找分割点,分割点作为当前节点,然后递归左区间和右区间。
分割点就是数组中间位置的节点。
那么为问题来了,如果数组长度为偶数,中间节点有两个,取哪一个?
取哪一个都可以,只不过构成了不同的平衡二叉搜索树。
例如:输入:[-10,-3,0,5,9]
如下两棵树,都是这个数组的平衡二叉搜索树:
如果要分割的数组长度为偶数的时候,中间元素为两个,是取左边元素 就是树1,取右边元素就是树2。
这也是题目中强调答案不是唯一的原因。 理解这一点,这道题目算是理解到位了。
递归
class Solution {
public:
TreeNode* traversal(vector<int>& nums, int left, int right){
if (left > right) return nullptr;
int mid = left + ((right - left) / 2);
TreeNode* root = new TreeNode(nums[mid]);
root->left = traversal(nums, left, mid-1);
root->right = traversal(nums, mid+1, right);
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
TreeNode* root = traversal(nums, 0, nums.size()-1);
return root;
}
};
class Solution {
public:
TreeNode* bulidTree(vector<int>& nums, int left, int right){
if (left == right) return nullptr;
int mid = left + (right - left) / 2;
TreeNode* root = new TreeNode(nums[mid]);
root->left = bulidTree(nums, left, mid);
root->right = bulidTree(nums, mid+1, right);
return root;
}
TreeNode* sortedArrayToBST(vector<int>& nums) {
if (nums.size() == 0) return nullptr;
return bulidTree(nums, 0, nums.size());
}
};
迭代
迭代法可以通过三个队列来模拟,一个队列放遍历的节点,一个队列放左区间下表,一个队列放右区间下表。
模拟的就是不断分割的过程,C++代码如下:
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
if (nums.size() == 0) return nullptr;
TreeNode* root = new TreeNode(0); // 初始根节点
queue<TreeNode*> nodeQue; // 放遍历的节点
queue<int> leftQue; // 保存左区间下表
queue<int> rightQue; // 保存右区间下表
nodeQue.push(root); // 根节点入队列
leftQue.push(0); // 0为左区间下表初始位置
rightQue.push(nums.size() - 1); // nums.size() - 1为右区间下表初始位置
while (!nodeQue.empty()) {
TreeNode* curNode = nodeQue.front();
nodeQue.pop();
int left = leftQue.front(); leftQue.pop();
int right = rightQue.front(); rightQue.pop();
int mid = left + ((right - left) / 2);
curNode->val = nums[mid]; // 将mid对应的元素给中间节点
if (left <= mid - 1) { // 处理左区间
curNode->left = new TreeNode(0);
nodeQue.push(curNode->left);
leftQue.push(left);
rightQue.push(mid - 1);
}
if (right >= mid + 1) { // 处理右区间
curNode->right = new TreeNode(0);
nodeQue.push(curNode->right);
leftQue.push(mid + 1);
rightQue.push(right);
}
}
return root;
}
};
题目链接
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
节点的左子树仅包含键 小于 节点键的节点。 节点的右子树仅包含键 大于 节点键的节点。 左右子树也必须是二叉搜索树。
输入:[4,1,6,0,2,5,7,null,null,null,3,null,null,null,8]
输出:[30,36,21,36,35,26,15,null,null,null,33,null,null,null,8]
示例 2:
输入:root = [0,null,1]
输出:[1,null,1]
示例 3:
输入:root = [1,0,2]
输出:[3,3,2]
示例 4:
输入:root = [3,2,4,1]
输出:[7,9,4,10]
提示:
树中的节点数介于 0 和 104 之间。
每个节点的值介于 -104 和 104 之间。
树中的所有值 互不相同 。
给定的树为二叉搜索树。
其实这就是一棵树,大家可能看起来有点别扭,换一个角度来看,这就是一个有序数组[2, 5, 13],求从后到前的累加数组,也就是[20, 18, 13],是不是感觉这就简单了。
为什么变成数组就是感觉简单了呢?
因为数组大家都知道怎么遍历啊,从后向前,挨个累加就完事了,这换成了二叉搜索树,看起来就别扭了一些是不是。
那么知道如何遍历这个二叉树,也就迎刃而解了,从树中可以看出累加的顺序是右中左,所以我们需要反中序遍历这个二叉树,然后顺序累加就可以了。
递归
右中左
class Solution {
public:
TreeNode* pre = nullptr;
void traversal(TreeNode* node){
if (node != nullptr){
traversal(node->right);
if (pre != nullptr)
node->val += pre->val;
pre = node;
traversal(node->left);
}
}
TreeNode* convertBST(TreeNode* root) {
traversal(root);
return root;
}
};