学习来源:
代码随香炉:https://www.programmercarl.com/
labuladong算法:https://labuladong.github.io/algo/
二叉树
基本理论
二叉树的种类、存储方式、遍历方式、定义方式
二叉树的种类
-
满二叉树
如果一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,则这棵二叉树为满二叉树。
深度为k,有2^k-1个节点的二叉树。 -
完全二叉树
完全二叉树的定义如下:在完全二叉树中,除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层,则该层包含 1~ 2^(h-1) 个节点。 -
二叉搜索树
前面介绍的树,都没有数值的,而二叉搜索树是有数值的了,二叉搜索树是一个有序树。
若它的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
它的左、右子树也分别为二叉排序树
- 平衡二叉搜索树
平衡二叉搜索树:又被称为AVL(Adelson-Velsky and Landis)树,且具有以下性质:它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。
C++中map、set、multimap,multiset的底层实现都是平衡二叉搜索树,所以map、set的增删操作时间时间复杂度是logn
unordered_map、unordered_set,unordered_map、unordered_map底层实现是哈希表。
二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
二叉树的遍历方式
深度优先遍历
前序遍历(递归法,迭代法)
中序遍历(递归法,迭代法)
后序遍历(递归法,迭代法)
广度优先遍历
层次遍历(迭代法)
二叉树的定义
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
遍历方式
前中后遍历
前序遍历
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;
}
};
中序遍历
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); // 中
}
二叉树的层序遍历
1.二叉树的层序遍历
2. 二叉树的层次遍历 II (其节点值自底向上的层次遍历)
相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。
3. 二叉树的右视图
想象自己站在它的右侧,按照从顶部到底部的顺序,返回从右侧所能看到的节点值。
4.二叉树的层平均值
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组。
5.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;
}
};
*/
6. 在每个树行中找最大值
7. 填充每个节点的下一个右侧节点指针
给定一个 完美二叉树 ,其所有叶子节点都在同一层,每个父节点都有两个子节点.
本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了
struct Node {
int val;
Node *left;
Node *right;
Node *next;
}
8. 二叉树的最大深度
9. 二叉树的最小深度
需要注意的是,只有当左右孩子都为空的时候,才说明遍历的最低点了。如果其中一个孩子为空则不是最低点
二叉树的属性
对称二叉树
给你一个二叉树的根节点 root , 检查它是否轴对称。
递归法:
- 层序遍历,然后对每一层进行对称判断
- 进行左右节点的判断 注意节点为空的情况,如果相等且不为空再进入下一层,
迭代
使用队列
相同的树
判断一棵树是不是另外一颗树的子树
定义两个递归函数:
第一个,判断是不是相同的树
第二个,判断是不是子树,s和t都有值返回真,s和t有一个为空的另外一个有值返回false ; 最后对 两棵树的根节点 和 子节点进行比较递归
二叉树最大深度
- 后序遍历,后序max(…)+1
- 层序遍历 每层遍历 +1
后序遍历
层序遍历
二叉树的最小深度
- 后序遍历 需要判断左子树 或者 右子树 为空的情况 特殊考虑 1+左右,其他情况和最大深度一样
- 层序遍历 中间加上最小的判断 如果为空直接return res
层序遍历
完全二叉树的节点个数
给出一个完全二叉树,求出该树的节点个数。
普通二叉树的求法
完全二叉树性质的求法
- 普通二叉树的求法
递归
迭代法
时间复杂度:O(n)
空间复杂度:O(n)
- 满二叉树的公式求解
情况一:就是满二叉树
情况二:最后一层叶子节点没有满。
对于情况一,可以直接用 2^树深度 - 1 来计算,注意这里根节点深度为1。
对于情况二,分别递归左孩子,和右孩子,递归到某一深度一定会有左孩子或者右孩子为满二叉树,然后依然可以按照情况1来计算。
- 普通二叉树遍历方式 bfs dfs
- 根据特性的遍历方式 左子树的高度 右子树的高度 如果两者相等就直接利用公式 如果不等则递归
平衡二叉树
给定一个二叉树,判断它是否是高度平衡的二叉树。
本题中,一棵高度平衡二叉树定义为:一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过1。
-
明确递归函数的参数和返回值
// -1 表示已经不是平衡二叉树了,否则返回值是以该节点为根节点树的高度
int getHeight(TreeNode* node) -
if (node == NULL) {
return 0;
} -
明确单层递归的逻辑
分别求出其左右子树的高度,然后如果差值小于等于1,则返回当前二叉树的高度,否则则返回-1,表示已经不是二叉平衡树了。
二叉树所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
二叉树的直径
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。
class Solution {
public:
int ans;
int depth(TreeNode* rt){
if (rt == NULL) {
return 0; // 访问到空节点了,返回0
}
int L = depth(rt->left); // 左儿子为根的子树的深度
int R = depth(rt->right); // 右儿子为根的子树的深度
ans = max(ans, L + R + 1); // 计算d_node即L+R+1 并更新ans
return max(L, R) + 1; // 返回该节点为根的子树的深度
}
int diameterOfBinaryTree(TreeNode* root) {
ans = 0;
depth(root);
return ans - 1; // 求路径,不是求节点个数,要减去1
}
};
左叶子之和
给定二叉树的根节点 root ,返回所有左叶子之和。
如果左节点不为空,且左节点没有左右孩子,那么这个节点的左节点就是左叶子
判断当前节点是不是左叶子是无法判断的,必须要通过节点的父节点来判断其左孩子是不是左叶子
当遇到左叶子节点的时候,记录数值,然后通过递归求取左子树左叶子之和,和 右子树左叶子之和,相加便是整个树的左叶子之和。
树左下角的值
给定一个二叉树,在树的最后一行找到最左边的值。
层序遍历 找树的最底层左边的值即可
路径总和(是否存在路径)
给你二叉树的根节点 root 和一个表示目标和的整数 targetSum 。判断该树中是否存在 根节点到叶子节点 的路径,这条路径上所有节点值相加等于目标和 targetSum 。如果存在,返回 true ;否则,返回 false
- 参数:需要二叉树的根节点,还需要一个计数器,这个计数器用来计算二叉树的一条边之和是否正好是目标和,计数器为int型。
遍历的路线,并不要遍历整棵树,所以递归函数需要返回值,可以用bool类型表示。
不要去累加然后判断是否等于目标和,那么代码比较麻烦,可以用递减,让计数器count初始为目标和,然后每次减去遍历路径节点上的数值。
如果最后count == 0,同时到了叶子节点的话,说明找到了目标和。
如果遍历到了叶子节点,count不为0,就是没找到。
因为终止条件是判断叶子节点,所以递归的过程中就不要让空节点进入递归了。
二叉树中的最大路径和
路径 被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中 至多出现一次 。该路径 至少包含一个 节点,且不一定经过根节点。
路径和 是路径中各节点值的总和。
给你一个二叉树的根节点 root ,返回其 最大路径和 。
class Solution {
public:
int res = INT_MIN;
int maxPathSum(TreeNode* root) {
if(root==nullptr){
return 0;
}
oneSideMax(root);
return res;
}
// 定义:计算从根节点 root 为起点的最大单边路径和
int oneSideMax(TreeNode* root){
if(root==nullptr){
return 0;
}
int lmax = max(0,oneSideMax(root->left));
int rmax = max(0,oneSideMax(root->right));
// 后序遍历位置,顺便更新最大路径和
int sums = lmax+rmax+root->val;
res = max(res,sums);
// 实现函数定义,左右子树的最大单边路径和加上根节点的值
// 就是从根节点 root 为起点的最大单边路径和
return max(lmax,rmax)+root->val;
}
};
二叉树的修改与改造
二叉树的序列化与反序列化
/*
struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
TreeNode(int x) :
val(x), left(NULL), right(NULL) {
}
};
*/
class Solution {
private:
string SerializeCore(TreeNode* root) {
if(root == nullptr) {
return "#!";
}
string str;
str +=to_string(root->val) + '!';
str +=SerializeCore(root->left);
str +=SerializeCore(root->right);
return str;
}
TreeNode* DeserializeCore(char*& str) {
if(*str == '#'){
str++;
return nullptr;
}
int num = 0;
while( *str != '!'){
num = num*10 + (*str)-'0';
str++;
}
TreeNode *node = new TreeNode(num);
node->left = DeserializeCore(++str);
node->right = DeserializeCore(++str);
return node;
}
public:
char* Serialize(TreeNode* root) {
string str = SerializeCore(root);
char *chs = new char[str.size()];
for(int i = 0;i<str.size();++i){
chs[i] = str[i];
}
return chs;
}
TreeNode* Deserialize(char* str) {
return DeserializeCore(str);
}
};
翻转二叉树
给你一棵二叉树的根节点 root ,翻转这棵二叉树,并返回其根节点。
前序遍历
广度优先遍历
从中序与后序遍历序列构造二叉树
第一步:如果数组大小为零的话,说明是空节点了。
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
第五步:切割后序数组,切成后序左数组和后序右数组
第六步:递归处理左区间和右区间
从前序与中序遍历序列构造二叉树
最大二叉树
给定一个不含重复元素的整数数组。一个以此数组构建的最大二叉树定义如下:
二叉树的根是数组中的最大元素。
左子树是通过数组中最大值左边部分构造出的最大二叉树。
右子树是通过数组中最大值右边部分构造出的最大二叉树。
合并二叉树
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
你需要将他们合并为一个新的二叉树。合并的规则是如果两个节点重叠,那么将他们的值相加作为节点合并后的新值,否则不为 NULL 的节点将直接作为新二叉树的节点。
思路
不要陷进去思考,从递归函数的含以上思考问题,然后把每个节点的操作 思考清楚
给定两个二叉树,想象当你将它们中的一个覆盖到另一个上时,两个二叉树的一些节点便会重叠。
二叉搜索树的属性
二叉搜索树中的搜索
给定二叉搜索树(BST)的根节点和一个值。 你需要在BST中找到节点值等于给定值的节点。 返回以该节点为根的子树。 如果节点不存在,则返回 NULL。
递归
验证二叉搜索树
给定一个二叉树,判断其是否是一个有效的二叉搜索树。
假设一个二叉搜索树具有如下特征:
节点的左子树只包含小于当前节点的数。
节点的右子树只包含大于当前节点的数。
所有左子树和右子树自身必须也是二叉搜索树。
中序遍历,从前往后走
二叉搜索树的最小绝对值之差
给你一棵所有节点为非负值的二叉搜索树,请你计算树中任意两节点的差的绝对值的最小值。
提示:树中至少有 2 个节点。
思路:
方法一 中序遍历 数组 求差 然后求解
方法二 使用一个临时节点保存前一个值,然后中序遍历过程中求解
二叉搜索树中的众数
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。
假定 BST 有如下定义:
结点左子树中所含结点的值小于等于当前结点的值
结点右子树中所含结点的值大于等于当前结点的值
左子树和右子树都是二叉搜索树
-
如果不是二叉搜索树:
-
是二叉搜索树
既然是搜索树,它中序遍历就是有序的。
把二叉搜索树转换为累加树
给出二叉 搜索 树的根节点,该树的节点值各不相同,请你将其转换为累加树(Greater Sum Tree),
使每个节点 node 的新值等于原树中大于或等于 node.val 的值之和。
提醒一下,二叉搜索树满足下列约束条件:
节点的左子树仅包含键 小于 节点键的节点。
节点的右子树仅包含键 大于 节点键的节点。
左右子树也必须是二叉搜索树。
类似于二叉搜索树的第k个大的树的做法
将中序遍历的步骤,倒转一下
中序遍历 从小到大
中序遍历先访问右边 从大到小
一边统计序列,一边统计前缀和
逆二叉树遍历
二叉树的公共祖先问题
二叉树的最近公共祖先
给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。
先给出递归函数的定义:给该函数输入三个参数 root,p,q,它会返回一个节点:
情况 1,如果 p 和 q 都在以 root 为根的树中,函数返回的即使 p 和 q 的最近公共祖先节点。
情况 2,那如果 p 和 q 都不在以 root 为根的树中怎么办呢?函数理所当然地返回 null 呗。
情况 3,那如果 p 和 q 只有一个存在于 root 为根的树中呢?函数就会返回那个节点。
- 根据这个定义,分情况讨论:
情况 1,如果 p 和 q 都在以 root 为根的树中,那么 left 和 right 一定分别是 p 和 q(从 base case 看出来的)。
情况 2,如果 p 和 q 都不在以 root 为根的树中,直接返回 null。
情况 3,如果 p 和 q 只有一个存在于 root 为根的树中,函数返回该节点。
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 { // (left == NULL && right == NULL)
return NULL;
}
}
};
二叉搜索树的修改与改造
二叉搜索树的插入
只要按照二叉搜索树的规则去遍历,遇到空节点就插入节点就可以了。
到这里,大家应该能感受到,如何通过递归函数返回值完成了新加入节点的父子关系赋值操作了,下一层将加入节点返回,本层用root->left或者root->right将其接住。
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
// 找到空位置插入新节点
if (root == nullptr) return new TreeNode(val);
if (root->val < val)
root->right = insertIntoBST(root->right, val);
if (root->val > val)
root->left = insertIntoBST(root->left, val);
return root;
}
};
二叉搜索树的删除
搜索树的节点删除要比节点增加复杂的多,有很多情况需要考虑,做好心里准备。
方案一:普通二叉树的比删除方式
第一次是和目标节点的右子树最左面节点交换。
第二次直接被NULL覆盖了。
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if (root == nullptr) return root;
if (root->val == key) {
if (root->right == nullptr) {
// 这里第二次操作目标值:最终删除的作用
return root->left;
}
TreeNode *cur = root->right;
while (cur->left) {
cur = cur->left;
}
swap(root->val, cur->val);
// 这里第一次操作目标值:交换目标值其右子树最左面节点。
}
root->left = deleteNode(root->left, key);
root->right = deleteNode(root->right, key);
return root;
}
};
方案二:搜索树的删除方式
-
确定递归函数参数以及返回值
遇到空返回,其实这也说明没找到删除的节点,遍历到空节点直接返回了
if (root == nullptr) return root;
- 确定单层递归的逻辑
这里就把二叉搜索树中删除节点遇到的情况都搞清楚。