二叉树相关题目预览:
理论基础速览
关于树,以下概念必须明确清楚:结点、结点的度(一个结点拥有的子节点数)、叶子、树的度、孩子、双亲、兄弟、祖先、结点的层次、树的深度、有序树和无序树、森林
AVL 树
平衡二叉搜索树(BBT):任何结点的 两个子树的 高度差别 最大为1
平衡因子(BF):结点左子树的深度减去右子树的深度,计算每个结点的高度和平衡因子
好处是:使用AVL树,树的高度始终是O(lgN),高度越小,对树的一些基本操作的时间复杂度就会越小,因此,平衡二叉树最大的作用是查找和插入、删除,在平均和最坏的情况下都是O(logn),AVL树的效率就是高在这里
右旋:(以E为旋转中心)
(旋转动图来自:数据结构 —— 图解AVL树(平衡二叉树)-CSDN博客)
左旋:
红黑树
(参考博客【数据结构】史上最好理解的红黑树讲解,让你彻底搞懂红黑树-CSDN博客)
自平衡的二叉查找树,高效的查找树。可在 O(logN) 时间内完成查找、增加、删除等操作
它是具备了某些特性的二叉搜索树,能解决非平衡树问题,红黑树是一种接近平衡的二叉树(说它是接近平衡因为它并没有像AVL树的平衡因子的概念,它只是靠着满足红黑节点的5条性质来维持一种接近平衡的结构,进而提升整体的性能,并没有严格的卡定某个平衡因子来维持绝对平衡)。
特性(<结点只有黑红>,根黑叶黑<注意叶是指虚拟的NULL结点>,黑夹红且红不连续,**每一条黑色数量相同**)这五条规则可以保证红黑树没有一条路径会大于其他路径的两倍,最大高度是2*log2(n+1)
红黑树的优点是对有序数据的查询操作不会慢到 O(logN) 的时间复杂度
红黑树和AVL树:
- AVL树的时间复杂度虽然优于红黑树,但是对于现在的计算机,cpu太快,可以忽略性能差异
- 红黑树的插入删除比AVL树更便于控制操作
- 红黑树整体性能略优于AVL树(红黑树旋转情况少于AVL树)
set/map容器底层实现
这两个容器底层实现都是红黑树,set使用的是key模型,map使用的是<K,V>模型
都用insert进行插入,但map还多了一个pair参数,pair本质上是一个类,存储着两个类型的值,返回一个pair的时候相当于返回了两个值。
满二叉树:一棵二叉树只有度为0的结点和度为2的结点,并且度为0的结点在同一层上,其中深度为k,有2^k-1个节点
完全二叉树:除了最底层节点可能没填满外,其余每层节点数都达到最大值,并且最下面一层的节点都集中在该层最左边的若干位置。若最底层为第 h 层(h从1开始),则该层包含 1~ 2^(h-1) 个节点。
(回顾:优先级队列其实是一个堆,堆就是一棵完全二叉树,同时保证父子节点的顺序关系)
二叉搜索树:有数值,是一个有序树,左小于根,右大于根
二叉树可以链式存储也可以顺序存储,如下图:
用数组仍然可以表示二叉树
遍历方式
- 深度优先遍历:先往深走,遇到叶子节点再往回走。
- 广度优先遍历:一层一层的去遍历。
- 深度优先遍历
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
前中后指的是中间节点的遍历顺序!!前序遍历中间节点在前面:前左右 中序遍历中间节点在中间:左中右 后序遍历中间节点在最后:左右中
前中后序遍历的逻辑其实都是可以借助栈使用递归的方式来实现的。
而广度优先遍历的实现一般使用队列来实现,这也是队列先进先出的特点所决定的,因为需要先进先出的结构,才能一层一层的来遍历二叉树。
这里其实我们又了解了栈与队列的一个应用场景了。
二叉树定义:
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(NULL), right(NULL) {}
};
最好能手写这段代码
二叉树的递归遍历
写递归前,你要想清楚:1.确定递归函数的参数和返回值 2.确定终止条件 3.确定单层递归的逻辑
这三点考虑清楚很重要!!!然后直接写即可~
class Solution {
public:
void trversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL)return;
vec.push_back(cur->val);
trversal(cur->left, vec);
trversal(cur->right, vec);
}
void trversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL)return;
trversal(cur->left, vec);
vec.push_back(cur->val);
trversal(cur->right, vec);
}
void trversal(TreeNode* cur, vector<int>& vec) {
if (cur == NULL)return;
trversal(cur->left, vec);
trversal(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 == NULL) 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;
}
};
二叉树的层序遍历
层序遍历有一个模板和十道变式,先理解模板,后面的变式都是举一反三的题目
先定义一个队列,判断一下把根放进去,前三行是涉及层序遍历必有的,
然后是设置一个和结果相关的容器,这就多变起来了
最后通过一个while(!que.empty())的判断,这也是迭代终止条件,在里面通过遍历que里的数,进行访问每个数左右节点进行层序的遍历。
class Solution {
public:
vector<vector<int>> levelOrder(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL)que.push(root);
vector<vector<int>> result;
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);
}
return result;
}
};
变式开始:
层次遍历倒序版
区别就是在结尾反转一下数组
class Solution {
public:
vector<vector<int>> levelOrderBottom(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
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());
return result;
}
};
二叉树的右视图
class Solution {
public:
vector<int> rightSideView(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<int> result;
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);
// 将每一层的最后元素放入result数组中
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
}
return result;
}
};
二叉树的层平均值
class Solution {
public:
vector<double> averageOfLevels(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<double> result;
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;
}
};
N叉数的层序遍历
class Solution {
public:
vector<vector<int>> levelOrder(Node* root) {
queue<Node*> que;
if (root != NULL) que.push(root);
vector<vector<int>> result;
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 (int i = 0; i < node->children.size(); i++) {
// 将节点孩子加入队列
if (node->children[i]) que.push(node->children[i]);
}
}
result.push_back(vec);
}
return result;
}
};
在每个树行中找最大值
class Solution {
public:
vector<int> largestValues(TreeNode* root) {
queue<TreeNode*> que;
if (root != NULL) que.push(root);
vector<int> result;
while (!que.empty()) {
int size = que.size();
int maxValue = INT_MIN;
// 取每一层的最大值
for (int i = 0; i < size; i++) {
TreeNode* node = que.front();
que.pop();
maxValue = node->val > maxValue ? node->val : maxValue;
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
result.push_back(maxValue);
// 把最大值放进数组
}
return result;
}
};
填充每个节点的下一个右侧节点指针
class Solution {
public:
Node* connect(Node* root) {
queue<Node*> que;
if (root != NULL) que.push(root);
while (!que.empty()) {
int size = que.size();
// vector<int> vec;
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; // 本层前一个节点next指向本节点
nodePre = nodePre->next;
}
if (node->left) que.push(node->left);
if (node->right) que.push(node->right);
}
nodePre->next = NULL; // 本层最后一个节点指向NULL
}
return root;
}
};
二叉树的最大深度
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;
}
};
二叉树的最小深度
注意只有当左右孩子都为空的时候,才说明是最低点的一层了,退出
class Solution {
public:
int minDepth(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);
if (!node->left && !node->right) {
return depth;
}
}
}
return depth;
}
};
翻转二叉树
采用递归的代码也非常简单,关键弄清楚,递归参数和返回值,什么条件终止,递归逻辑,
(1.确定递归函数的参数和返回值 2.确定终止条件 3.确定单层递归的逻辑)
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == NULL)return root;
swap(root->left, root->right);
invertTree(root->left);
invertTree(root->right);
return root;
}
};
对称二叉树
主要是两个部分,一个做判断找出数值相同子节点,然后进行递归外外相同,内内相同做判断。
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);
}
};
类似题目:判断两棵树是否完全相等
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;
}else{
return isSameTree(p->left, q->left)&&isSameTree(p->right,q->right);
}
}
};
二叉树的最大深度
用后序遍历(左右中)来计算树的高度。
确定递归函数的参数和返回值:参数就是传入树的根节点,返回就返回这棵树的深度,所以返回值为int类型。
int getdepth(TreeNode* node)
确定终止条件:如果为空节点的话,就返回0,表示高度为0。
if (node == NULL) return 0;
确定单层递归的逻辑:先求它的左子树的深度,再求右子树的深度,最后取左右深度最大的数值 再+1 (加1是因为算上当前中间节点)就是目前节点为根节点的树的深度。
int leftdepth = getdepth(node->left); // 左
int rightdepth = getdepth(node->right); // 右
int depth = 1 + max(leftdepth, rightdepth); // 中
return depth;
class Solution {
public:
int getdepth(TreeNode* node) {
if (node == NULL)return 0;
int leftdepth = getdepth(node->left);
int rightdepth = getdepth(node->right);
int depth = max(leftdepth, rightdepth) +1 ;
return depth;
}
int maxDepth(TreeNode* root) {
return getdepth(root);
}
};
class Solution {
public:
int maxDepth(TreeNode* root) {
if (root == null)return 0;
return 1 + max(maxDepth(root->left), maxDepth(root->right));
}
};
二叉树的最小深度
还是跟上一个判断一样,唯一要注意的是最小深度,如果从根节点开始有一边子树是空的,要从那个子树开始,通到叶子节点才有最小深度
1.确定递归函数的参数和返回值 2.确定终止条件 3.确定单层递归逻辑
class Solution {
public:
int minDepth(TreeNode* root) {
if(root == NULL)return 0;
int leftDepth = minDepth(root->left);
int rightDepth = minDepth(root->right);
if(root->left == NULL && root->right != NULL){
return 1+rightDepth;
}
if(root->right == NULL && root->left != NULL){
return 1+leftDepth;
}
int result = 1+min(leftDepth,rightDepth);
return result;
}
};
完全二叉树的节点个数
仍然一样三部曲 确定递归函数参数和返回值类型,确定函数终止条件,执行递归逻辑
class Solution {
public:
int countNodes(TreeNode* root) {
if(root == NULL)return 0;
int leftNum = countNodes(root->left);
int rightNum = countNodes(root->right);
int TreeNode = leftNum + rightNum +1;
return TreeNode;
}
};
平衡二叉树
平衡二叉树可以想到和最开始的二叉平衡树还有自平衡算法的红黑树都有关联
二叉树节点的深度:指从根节点到该节点的最长简单路径边的条数(节点数)。
二叉树节点的高度:指从该节点到叶子节点的最长简单路径边的条数(节点数)。
leetcode使用节点数,
深度是从根开始,高度是从叶子开始
深度从上到下,采用前序遍历,高度从下到上,采用后序遍历
先学习求最大深度的代码
class Solution {
public:
void getDepth(TreeNode* node, int depth) {
int result;
result = depth > result ? depth : result;
if (node->left == NULL && node->right == NULL)return;
if (node->left) {
depth++;
getDepth(node->left, depth);
depth--;//回溯??
}
if (node->right) {
depth++;
getDepth(node->right, depth);
depth--;
}
return;
}
int maxDepth(TreeNode* root) {
result = 0;
if (root == NULL)return result;
getDepth(root, 1);
return result;
}
};
回溯解释:在递归遍历二叉树的过程中,每次进入一个节点的子节点时,深度(depth)会增加1,表示进入了下一层。当遍历完当前节点的所有子节点后,需要回溯到上一层,因此深度需要减1。这样可以确保在遍历过程中,深度值始终表示当前节点所在的层数。
二叉树的所有路径
给定一个二叉树,返回所有从根节点到叶子节点的路径。
说明: 叶子节点是指没有子节点的节点。
示例:
这道题递归和回溯是要写在一起的,一个花括号内。一一对应的。
还是三个分析方向1.递归函数参数和返回值。 2.终止条件。 3.确定循环逻辑
class Solution {
public:
void traversal(TreeNode* cur, vector<int>& path, vector<string>& result) {
path.push_back(cur->val);
if (cur->left == NULL && cur->right == NULL) {
string sPath;
for (int i = 0; i < path.size() - 1; i++) {
sPath += to_string(path[i]);
sPath += "->";
}
sPath += to_string(path[path.size() - 1]);
result.push_back(sPath);
return;
}
if (cur->left) {
traversal(cur->left, path, result);
path.pop_back();
}
if (cur->right) {
traversal(cur->right, path, result);
path.pop_back();
}
}
public:
vector<string> binaryTreePaths(TreeNode* root) {
vector<string> result;
vector<int> path;
if (root == NULL) return result;
traversal(root, path, result);
return result;
}
};
左叶子之和
左叶子:左子树不为空,左子树的孩子都为空,则找到叶子
把左叶子的值放到左叶子和的函数里。然后再通过右子树找右子树的左叶子
class Solution {
public:
int sumOfLeftLeaves(TreeNode* root) {
if (root == NULL) return 0;
if (root->left == NULL && root->right == NULL) return 0;
int leftValue = sumOfLeftLeaves(root->left);
if (root->left && !root->left->left && !root->left->right) {
leftValue = root->left->val;
}
int rightValue = sumOfLeftLeaves(root->right);
int sum = leftValue + rightValue;
return sum;
}
};
找树左下角的值
本质是找深度最大的叶子节点,然后保证优先左边搜索,然后记录深度最大的叶子节点,此时就是树的最后一行最左边的值,注意寻找左右深度的时候及时回溯检测。
class Solution {
public:
int maxDepth = INT_MIN;
int result;
void traversal(TreeNode* root, int depth) {
if (root->left == NULL && root->right == NULL) {
if (depth > maxDepth) {
maxDepth = depth;
result = root->val;
}
return;
}
if (root->left) {
depth++;
traversal(root->left, depth);
depth--;
}
if (root->right) {
depth++;
traversal(root->right, depth);
depth--;
}
return;
}
int findBottomLeftValue(TreeNode* root) {
traversal(root, 0);
return result;
}
};
路径总和
确定递归函数和返回类型, 使用递减的计数器比较方便,这样只用判0就可以了,然后确定终止条件,遇到叶子节点,是否返回,最后是单层递归逻辑,递归函数是有返回值的,如果返回true找到合适路径就立刻返回,不用费时间遍历了。
class Solution {
private:
bool traversal(TreeNode* cur, int count) {
if (!cur->left && !cur->right && count == 0) return true; // 遇到叶子节点,并且计数为0
if (!cur->left && !cur->right) return false; // 遇到叶子节点直接返回
if (cur->left) { // 左
count -= cur->left->val; // 递归,处理节点;
if (traversal(cur->left, count)) return true;
count += cur->left->val; // 回溯,撤销处理结果
}
if (cur->right) { // 右
count -= cur->right->val; // 递归,处理节点;
if (traversal(cur->right, count)) return true;
count += cur->right->val; // 回溯,撤销处理结果
}
return false;
}
public:
bool hasPathSum(TreeNode* root, int sum) {
if (root == NULL) return false;
return traversal(root, sum - root->val);
}
};
理解上面之后的精简版:
class Solution {
public:
bool hasPathSum(TreeNode* root, int sum) {
if (!root) return false;
if (!root->left && !root->right && sum == root->val) {
return true;
}
return hasPathSum(root->left, sum - root->val) || hasPathSum(root->right, sum - root->val);
}
};
从中序与后序遍历构造二叉树
复习:中序和后序的意思是“中”在中还是后,如何通过中序和后序构造唯一的一颗二叉树:
以后序数组的最后一个元素为切割点,因为后序数组的最后一个元素一定是根节点,所以,先确定这个数值,然后在中序数组中进行切割,分为左右两边,然后再根据后序数组找到子树的根节点,再在中序子树上进行切割,如此层层循环。
这道题同样,递归解决
第一步:如果数组大小为零的话,说明是空节点了。
第二步:如果不为空,那么取后序数组最后一个元素作为节点元素。
第三步:找到后序数组最后一个元素在中序数组的位置,作为切割点
第四步:切割中序数组,切成中序左数组和中序右数组 (顺序别搞反了,一定是先切中序数组)
第五步:切割后序数组,切成后序左数组和后序右数组
第六步:递归处理左区间和右区间
class Solution {
private:
TreeNode* traversal (vector<int>& inorder, vector<int>& postorder) {
if (postorder.size() == 0) return NULL;
// 后序遍历数组最后一个元素,就是当前的中间节点
int rootValue = postorder[postorder.size() - 1];
TreeNode* root = new TreeNode(rootValue);
// 叶子节点
if (postorder.size() == 1) return root;
// 找到中序遍历的切割点
int delimiterIndex;
for (delimiterIndex = 0; delimiterIndex < inorder.size();
delimiterIndex++) {
if (inorder[delimiterIndex] == rootValue) break;
}
// 切割中序数组
// 左闭右开区间:[0, delimiterIndex)
vector<int> leftInorder(inorder.begin(), inorder.begin() + delimiterIndex);
// [delimiterIndex + 1, end)
vector<int> rightInorder(inorder.begin() + delimiterIndex + 1, inorder.end());
// postorder 舍弃末尾元素
postorder.resize(postorder.size() - 1);
// 切割后序数组
// 依然左闭右开,注意这里使用了左中序数组大小作为切割点
// [0, leftInorder.size)
vector<int> leftPostorder
(postorder.begin(), postorder.begin() + leftInorder.size());
// [leftInorder.size(), end)
vector<int> rightPostorder
(postorder.begin() + leftInorder.size(), postorder.end());
root->left = traversal(leftInorder, leftPostorder);
root->right = traversal(rightInorder, rightPostorder);
return root;
}
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
if (inorder.size() == 0 || postorder.size() == 0) return NULL;
return traversal(inorder, postorder);
}
};
最大二叉树
构造树一般采用前序遍历,因为先构造中间节点,然后是左右子树
确定递归函数的参数和返回值,确定终止条件,确定单层递归逻辑
用最大值和对应的下标,最大值的构造根节点,下标用来下一步分割数组。
数组构造二叉树,每次分割尽量不要构造新的数组,在原数组上操作,节省时间空间开销。
class Solution {
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
TreeNode* node = new TreeNode(0);
if (nums.size() == 1) {
node->val = nums[0];
return node;
}
// 找到数组中最大的值和对应的下标
int maxValue = 0;
int maxValueIndex = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] > maxValue) {
maxValue = nums[i];
maxValueIndex = i;
}
}
node->val = maxValue;
// 最大值所在的下标左区间 构造左子树
if (maxValueIndex > 0) {
vector<int> newVec(nums.begin(), nums.begin() + maxValueIndex);
node->left = constructMaximumBinaryTree(newVec);
}
// 最大值所在的下标右区间 构造右子树
if (maxValueIndex < (nums.size() - 1)) {
vector<int> newVec(nums.begin() + maxValueIndex + 1, nums.end());
node->right = constructMaximumBinaryTree(newVec);
}
return node;
}
};
合并二叉树
同时遍历两个二叉树!确定递归函数参数和返回值,确定终止条件:如果一方为空,返回另一方的数值,确定单层递归逻辑:把两棵树的元素相加,放到一起。
class Solution {
public:
TreeNode* mergeTrees(TreeNode* t1, TreeNode* t2) {
if (t1 == NULL) return t2; // 如果t1为空,合并之后就应该是t2
if (t2 == NULL) return t1; // 如果t2为空,合并之后就应该是t1
// 修改了t1的数值和结构
t1->val += t2->val; // 中
t1->left = mergeTrees(t1->left, t2->left); // 左
t1->right = mergeTrees(t1->right, t2->right); // 右
return t1;
}
};
二叉搜索树中的搜索
二叉搜索树本质是一个有序数,左子树小于根,右子树大于根。二叉搜索树是有序的,所以在确定单层递归逻辑的时候可以有方向的去搜索,root->val>val 搜索左子树,root->val<val 搜索右子树。
class Solution {
public:
TreeNode* searchBST(TreeNode* root, int val) {
if (root == NULL || root->val == val) return root;
if (root->val > val) return searchBST(root->left, val);
if (root->val < val) return searchBST(root->right, val);
return NULL;
}
};
验证二叉搜索树
核心:使用中序遍历可以把其变成有序数组,然后直接判断是不是递增即可
class Solution {
private:
vector<int> vec;
void traversal(TreeNode* root) {
if (root == NULL) return;
traversal(root->left);
vec.push_back(root->val); // 将二叉搜索树转换为有序数组
traversal(root->right);
}
public:
bool isValidBST(TreeNode* root) {
vec.clear(); // 不加这句在leetcode上也可以过,但最好加上
traversal(root);
for (int i = 1; i < vec.size(); i++) {
// 注意要小于等于,搜索树里不能有相同元素
if (vec[i] <= vec[i - 1]) return false;
}
return true;
}
};
二叉搜索树的最小绝对差
计算差值和最小值可以转换成,是有序数组,然后遍历找到最小差值
二叉搜索树因为他的有序可以很多转换成有序数组再继续进行。
class Solution {
private:
int result = INT_MAX;
TreeNode* pre = NULL;
void traversal(TreeNode* cur) {
if (cur == NULL) return;
traversal(cur->left); // 左
if (pre != NULL){ // 中
result = min(result, cur->val - pre->val);
}
pre = cur; // 记录前一个
traversal(cur->right); // 右
}
public:
int getMinimumDifference(TreeNode* root) {
traversal(root);
return result;
}
};
二叉搜索树的众树
简单,一看就懂
class Solution {
private:
int maxCount = 0; // 最大频率
int count = 0; // 统计频率
TreeNode* pre = NULL;
vector<int> result;
void searchBST(TreeNode* cur) {
if (cur == NULL) return ;
searchBST(cur->left); // 左
// 中
if (pre == NULL) { // 第一个节点
count = 1;
} else if (pre->val == cur->val) { // 与前一个节点数值相同
count++;
} else { // 与前一个节点数值不同
count = 1;
}
pre = cur; // 更新上一个节点
if (count == maxCount) { // 如果和最大值相同,放进result中
result.push_back(cur->val);
}
if (count > maxCount) { // 如果计数大于最大值频率
maxCount = count; // 更新最大频率
result.clear(); // 很关键的一步,不要忘记清空result,之前result里的元素都失效了
result.push_back(cur->val);
}
searchBST(cur->right); // 右
return ;
}
public:
vector<int> findMode(TreeNode* root) {
count = 0;
maxCount = 0;
pre = NULL; // 记录前一个节点
result.clear();
searchBST(root);
return result;
}
};
二叉树的最近公共祖先
利用回溯从底向上搜索,遇到一个节点的左子树里有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 { // (left == NULL && right == NULL)
return NULL;
}
}
};
二叉搜索树的最近公共祖先
二叉搜索树因为是有序的,所以要找一个节点的左右子树有不同的数,就可以通过比大小,中节点的数值在[p,q]区间中。
回顾一下三部曲:确定递归函数参数和返回值,
参数就是当前节点,以及两个结点 p、q。
返回值是要返回最近公共祖先,所以是TreeNode * 。
确定终止条件:遇到空可以返回
确定单层递归逻辑:如果递归函数有返回值,如何区分要搜索一条边,还是搜索整棵树。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if (root->val > p->val && root->val > q->val) {
return lowestCommonAncestor(root->left, p, q);
} else if (root->val < p->val && root->val < q->val) {
return lowestCommonAncestor(root->right, p, q);
} else return root;
}
};
二叉搜索树中的插入操作
题目感觉好难,但其实对于二叉搜索树,只用找到空节点,插入元素就可以了
还是三部曲思考
class Solution {
private:
TreeNode* parent;
void traversal(TreeNode* cur, int val) {
if (cur == NULL) {
TreeNode* node = new TreeNode(val);
if (val > parent->val) parent->right = node;
else parent->left = node;
return;
}
parent = cur;
if (cur->val > val) traversal(cur->left, val);
if (cur->val < val) traversal(cur->right, val);
return;
}
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
parent = new TreeNode(0);
if (root == NULL) {
root = new TreeNode(val);
}
traversal(root, val);
return root;
}
};
删除二叉搜索树中的节点
通过递归返回值来删除节点
确定单层递归的逻辑
这里就把二叉搜索树中删除节点遇到的情况都搞清楚。
有以下五种情况:
- 第一种情况:没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点
- 第二种情况:左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 第三种情况:删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 第四种情况:删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 第五种情况:左右孩子节点都不为空,则将删除节点的左子树头结点(左孩子)放到删除节点的右子树的最左面节点的左孩子上,返回删除节点右孩子为新的根节点。
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 {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
if (root == nullptr ) return nullptr;
if (root->val < low) {
TreeNode* right = trimBST(root->right, low, high); // 寻找符合区间[low, high]的节点
return right;
}
if (root->val > high) {
TreeNode* left = trimBST(root->left, low, high); // 寻找符合区间[low, high]的节点
return left;
}
root->left = trimBST(root->left, low, high); // root->left接入符合条件的左孩子
root->right = trimBST(root->right, low, high); // root->right接入符合条件的右孩子
return root;
}
};
将有序数组转换为二叉搜索树
class Solution {
private:
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;
}
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
TreeNode* root = traversal(nums, 0, nums.size() - 1);
return root;
}
};
把二叉搜索树转换为累加树
反中序遍历二叉树,然后顺序累加。
class Solution {
private:
int pre = 0; // 记录前一个节点的数值
void traversal(TreeNode* cur) { // 右中左遍历
if (cur == NULL) return;
traversal(cur->right);
cur->val += pre;
pre = cur->val;
traversal(cur->left);
}
public:
TreeNode* convertBST(TreeNode* root) {
pre = 0;
traversal(root);
return root;
}
};
二叉树总结
在每一道二叉树的题目中,我都使用了递归三部曲来分析题目,相信大家以后看到二叉树,看到递归,都会想:返回值、参数是什么?终止条件是什么?单层逻辑是什么?
-
涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点。
-
求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算。
-
求二叉搜索树的属性,一定是中序了,要不白瞎了有序性了。
-
注意在普通二叉树的属性中,我用的是一般为后序,例如单纯求深度就用前序,这是为了方便让父节点指向子节点。
所以求普通二叉树的属性还是要具体问题具体分析。
-