文章目录
一、235. 二叉搜索树的最近公共祖先

递归法-搜索边,找到立即返回
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q)
{
if(!root) return root;
if(root->val > p->val && root->val > q->val)
{
TreeNode* left = lowestCommonAncestor(root->left, p, q);
if(left) return left;
}
if(root->val < p->val && root->val <q->val)
{
TreeNode* right = lowestCommonAncestor(root->right, p, q);
if(right) return right;
}
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;
}
};
二、701.二叉搜索树中的插入操作
碰到空节点就插入新节点
递归,有返回值
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val) {
//遇到空节点就插入新节点并返回
if(root==nullptr)
{
TreeNode* node = new TreeNode(val);
return node;
}
//递归函数的返回值用root->left或者root->right接收
if(val < root->val) root->left = insertIntoBST(root->left, val);
if(val > root->val) root->right = insertIntoBST(root->right, val);
return root;
}
};
递归,没有返回值,双指针,一个遍历找空指针,另一个接收上一个节点做插入操作
class Solution {
public:
TreeNode* parent;
void traversal(TreeNode* cur, int val)
{
//遇到空节点就插入新节点,即让parent左孩子或者右孩子指向新插入的节点
if(cur==nullptr)
{
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;
}
TreeNode* insertIntoBST(TreeNode* root, int val)
{
parent = new TreeNode(0);
if(root==nullptr) root = new TreeNode(val);
traversal(root, val);
return root;
}
};
迭代,双指针,一个遍历找空指针,另一个接收上一个节点做插入操作
class Solution {
public:
TreeNode* insertIntoBST(TreeNode* root, int val)
{
//新节点
TreeNode* node = new TreeNode(val);
//空树 直接返回新节点
if(root==nullptr) return node;
TreeNode* parent = root;
TreeNode* cur = root;
//cur遍历树,找到空节点位置;pre指向cur的上一个节点,即找到插入新节点位置
while(cur)
{
parent = cur;
if(val > cur->val) cur = cur->right;
else cur = cur->left;
}
//插入操作
if(val > parent->val) parent->right = node;//右边插入,用parent节点的进行赋值
else parent->left = node;
return root;
}
};
三、450.删除二叉搜索树中的节点

五种情况:
- 没找到删除的节点,遍历到空节点直接返回了
- 找到删除的节点,左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
- 找到删除的节点,删除节点的左孩子为空,右孩子不为空,删除节点,右孩子补位,返回右孩子为根节点
- 找到删除的节点,删除节点的右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
- 找到删除的节点,左右孩子节点都不为空,先将被删除节点的左孩子(左子树头节点)放在右孩子(右子树头节点)的左孩子上,被删除节点的右孩子为新的根节点并返回。

先将被删除节点的左孩子放在右孩子的左孩子上
被删除节点的右孩子为新的根节点并返回
方法1,递归,双指针,二叉搜索树删除节点
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
//情况1,没找到删除的节点,遍历到空节点直接返回
if(root==nullptr) return root;
if(root->val == key)
{
//情况2,左右孩子都为空(叶子节点),直接删除节点, 返回NULL为根节点
if(root->left==nullptr && root->right==nullptr)
{
delete root;
return nullptr;
}
//情况3,左孩子为空,右孩子不为空,删除节点,右孩子补位 ,返回右孩子为根节点
else if(root->left==nullptr)
{
auto node = root->right;
delete root;
return node;
}
//情况4,右孩子为空,左孩子不为空,删除节点,左孩子补位,返回左孩子为根节点
else if(root->right==nullptr)
{
auto node = root->left;
delete root;
return node;
}
//情况5,左右孩子节点都不为空,则将删除节点的左子树放到删除节点的右子树的最左面节点的左孩子的位置
// 并返回删除节点右孩子为新的根节点。
else
{
//被删除节点的右孩子
TreeNode* cur = root->right;
//找到右子树的最左节点
while(cur->left)
{
cur = cur->left;
}
//被删除节点的左子树头节点放在 右子树头节点的最左节点上
cur->left = root->left;
TreeNode* temp = root;
//被删除节点的右孩子作为新的根节点
root = root->right;
delete temp;//删掉节点
return root;
}
}
if(root->val > key) root->left = deleteNode(root->left, key);
if(root->val < key) root->right = deleteNode(root->right, key);
return root;
}
};
方法2,迭代,双指针,二叉搜索树删除节点
分为两部分,一是对目标节点的删除操作,二是遍历树,找到目标节点做删除操作
deleteOneNode里边是将目标节点(删除节点)的左子树放到 目标节点的右子树的最左面节点的左孩子位置上,并返回目标节点右孩子为新的根节点;
deleteNode是找到删除节点的位置,双指针,一个cur记录删除节点,一个pre记录删除节点的父节点。同时需要让pre知道cur是pre的左孩子还是右孩子。
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
//找到被删除节点的位置 pre对应更新为cur的父节点
while(cur)
{
if(cur->val == key) break;
pre = cur;
if(cur->val > key) cur = cur->left;
else cur = cur->right;
}
//根据情况删除节点
//1.搜索树只有头结点
if(pre == nullptr) return deleteOneNode(cur);
//2.pre是cur的父节点,删除cur,要让pre要知道是cur是左孩子还是右孩子
if(pre->left && pre->left->val == key) pre->left = deleteOneNode(cur);//cur是pre的左孩子
if(pre->right && pre->right->val == key) pre->right = deleteOneNode(cur);//cur是pre的右孩子
return root;
}
};
方法2,递归 普通二叉树删除节点
普通二叉树的删除方式,用交换值的操作来删除目标节点。被删除节点被操作了两次,第一次是和目标节点的右子树最左面节点交换;第二次直接被NULL覆盖了。
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key)
{
if(root==nullptr) return nullptr;
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;
}
};
四、669. 修剪二叉搜索树



递归,通过返回值移除节点
- 参数:根节点,节点区间
- 返回值:遍历整棵树做修改,可以不需要返回值;或者通过返回值来移除节点
- 单次递归:当前节点值小于low,递归右子树,并返回右子树符合条件的头结点;当前节点值大于high,递归左子树,并返回左子树符合条件的头结点。然后root->left/root->right接收返回值
- 对于节点的删除部分,当前节点值小于low,返回右子树符合条件的头结点给上一层,下一层再用root->left来接收这个返回值,就把区间外的节点覆盖了。
过程写在注释里
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high) {
//遍历到空节点就返回
if(root==nullptr) return nullptr;
//单次递归
//当前节点值<low,递归右子树,并返回右子树符合条件的头结点
//对于示例2,这里相当于把节点0的右孩子(节点2)返回给上一层,
if(root->val < low)
{
TreeNode* right = trimBST(root->right, low, high);
return right;
}
if(root->val > high)
{
TreeNode* left = trimBST(root->left, low, high);
return left;
}
//将下一层处理完左子树的结果赋给root->left,处理完右子树的结果赋给root->right
//对于示例2,相当于用节点3的左孩子 把下一层返回的 节点0的右孩子(节点2) 接住
root->left = trimBST(root->left, low, high);
root->right = trimBST(root->right, low, high);
return root;
}
};
迭代,剪枝时分为三个部分:
- 处理头节点,将root移动到[L, R]左闭右闭区间内
- 处理头节点左孩子,将root->left移动到[L, R]左闭右闭区间内
- 处理头节点右孩子,将root->right移动到[L, R]左闭右闭区间内
class Solution {
public:
TreeNode* trimBST(TreeNode* root, int low, int high)
{
//1.空树
if(!root) return nullptr;
//2.处理头节点,让头节点位于[low, high]左闭右闭区间内
while(root && (root->val < low || root->val > high))
{
if(root->val < low) root = root->right;//小于low,往右走
else root = root->left;//大于high,往左走
}
//3.处理头节点的左孩子,让头节点的左孩子位于[low, high]左闭右闭区间内
TreeNode* cur = root;
while(cur)
{
while(cur->left && cur->left->val < low) cur->left = cur->left->right;
cur = cur->left;//更新 下一个左孩子
}
//4.处理头节点的右孩子,让头节点的右孩子位于[low, high]左闭右闭区间内 需要重新赋值一遍
cur = root;
while(cur)
{
while(cur->right && cur->right->val > high) cur->right = cur->right->left;
cur = cur->right;//更新 下一个右孩子
}
return root;
}
};
五、108.将有序数组转换为二叉搜索树

数组换位二叉树本质是找分割点,分割点作为当前节点,然后递归左区间和右区间。
构造二叉树的时候尽量不要重新定义左右区间数组,而是用下标来操作原数组。
对于二叉搜索树,分割点就是数组中间位置的节点。如果数组长度为偶数,中间节点有两个。取哪一个都可以构成平衡二叉搜索树,只是构成了不同的平衡二叉搜索树。
递归,递归函数的返回值来构造中节点的左右孩子
传入参数:数组,左下标left,右下标right,左闭右闭区间[left, right]
终止条件:当区间 left > right时,就是空节点了
单次递归:
- 取数组中间元素,注意不要越界,构造节点root
- 划分区间,root的左孩子接住下一层左区间的构造节点,右孩子接住下一层右区间构造的节点
- 返回root
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;
}
};
迭代,三个队列,一个放遍历的节点,一个队列放左区间下标,一个队列放右区间下标
中间值,作为中间节点,注意中间值的溢出
处理左区间,左节点,左区间更新
处理右区间,右节点,右区间更新
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);
rightque.push(nums.size()-1);//左闭右闭
while(!nodeque.empty())
{
//取出节点
TreeNode* cur = nodeque.front();
nodeque.pop();
//取出左右区间
int left = leftque.front();
leftque.pop();
int right = rightque.front();
rightque.pop();
//创建中间节点 注意溢界
int mid = left + ((right - left) / 2);
cur->val = nums[mid];
//处理左区间
if(left <= mid-1)
{
cur->left = new TreeNode(0);
nodeque.push(cur->left);
leftque.push(left);
rightque.push(mid-1);
}
//处理右区间
if(right >= mid+1)
{
cur->right = new TreeNode(0);
nodeque.push(cur->right);
leftque.push(mid+1);
rightque.push(right);
}
}
return root;
}
};
六、538.把二叉搜索树转换为累加树

二叉搜索树转换为累加树,按照右中左顺序遍历(反的中序遍历),从后往前累加,累加顺序如图
递归,前后指针,反中序遍历
参数:根节点
返回值:便利整棵树,不需要返回值
结束条件:遇到空节点结束,说明遍历完整个树了
单词递归:右中左顺序遍历,cur节点值累加,pre记录cur上一个节点值
要注意的是,需要一个全局变量记录当前节点的前一个节点的数值,即int pre = 0;
class Solution {
private:
int pre = 0;//记录上一个节点的值
void traversal(TreeNode* cur)
{
if(cur==nullptr) return;
//右中左顺序遍历
traversal(cur->right);//右
cur->val += pre;//节点值累加
pre = cur->val;//记录cur上一个节点的值
traversal(cur->left);
}
public:
TreeNode* convertBST(TreeNode* root) {
pre = 0;
traversal(root);
return root;
}
};
迭代,栈,反中序遍历
前中后序统一迭代写法
全局变量记录当前节点的前一个节点的数值,即int pre = 0;
class Solution {
private:
int pre = 0;//记录上一个节点的值
//迭代
void traversal(TreeNode* root)
{
stack<TreeNode*> st;
TreeNode* cur = root;
while(cur!=nullptr || !st.empty())
{
if(cur!=nullptr)
{
st.push(cur);
cur = cur->right;//右
}
else
{
cur = st.top();
st.pop();
cur->val += pre;
pre = cur->val;
cur = cur->left;//左
}
}
}
public:
TreeNode* convertBST(TreeNode* root) {
pre = 0;
traversal(root);
return root;
}
};
总结
-
二叉树理论基础:二叉树的种类、存储方式、遍历方式、定义方式
-
二叉树的遍历方式:前序遍历、中序遍历、后序遍历、深度优先搜索(DFS)、宽度优先搜索(BFS)、Morris(莫里斯)的前中后3种遍历方式
- 深度优先遍历
- 二叉树:前中后序递归法 (opens new window):递归三部曲初次亮相
- 二叉树:前中后序迭代法(一) (opens new window):通过栈模拟递归
- 二叉树:前中后序迭代法(二)统一风格(opens new window)
- 广度优先遍历
二叉树的层序遍历 (opens new window):通过队列模拟
- 求二叉树的属性
- 二叉树:是否对称
递归:后序,比较的是根节点的左子树与右子树是不是相互翻转
迭代:使用队列/栈将两个节点顺序放入容器中进行比较 - 二叉树:求最大深度
递归:后序,求根节点最大高度就是最大深度,通过递归函数的返回值做计算树的高度
迭代:层序遍历 - 二叉树:求最小深度
递归:后序,求根节点最小高度就是最小深度,注意最小深度的定义
迭代:层序遍历 - 二叉树:求有多少个节点
递归:后序,通过递归函数的返回值计算节点数量
迭代:层序遍历 - 二叉树:是否平衡
递归:后序,注意后序求高度和前序求深度,递归过程判断高度差
迭代:效率很低,不推荐 - 二叉树:找所有路径
递归:前序,方便让父节点指向子节点,涉及回溯处理根节点到叶子的所有路径
迭代:一个栈模拟递归,一个栈来存放对应的遍历路径 - 二叉树:递归中如何隐藏着回溯
详解二叉树:找所有路径中递归如何隐藏着回溯 - 二叉树:求左叶子之和
递归:后序,必须三层约束条件,才能判断是否是左叶子。
迭代:直接模拟后序遍历 - 二叉树:求左下角的值
递归:顺序无所谓,优先左孩子搜索,同时找深度最大的叶子节点。
迭代:层序遍历找最后一行最左边 - 二叉树:求路径总和
递归:顺序无所谓,递归函数返回值为bool类型是为了搜索一条边,没有返回值是搜索整棵树。
迭代:栈里元素不仅要记录节点指针,还要记录从头结点到该节点的路径数值总和
- 二叉树的修改与构造
- 翻转二叉树
递归:前序,交换左右孩子
迭代:直接模拟前序遍历 - 构造二叉树
递归:前序,重点在于找分割点,分左右区间构造
迭代:比较复杂,意义不大 - 构造最大的二叉树
递归:前序,分割点为数组最大值,分左右区间构造
迭代:比较复杂,意义不大 - 合并两个二叉树
递归:前序,同时操作两个树的节点,注意合并的规则
迭代:使用队列,类似层序遍历
- 求二叉搜索树的属性
- 二叉搜索树中的搜索
递归:二叉搜索树的递归是有方向的
迭代:因为有方向,迭代法 - 是不是二叉搜索树
递归:中序,相当于变成了判断一个序列是不是递增的
迭代:模拟中序,逻辑相同 - 求二叉搜索树的最小绝对差
递归:中序,双指针操作
迭代:模拟中序,逻辑相同 - 求二叉搜索树的众数
递归:中序,清空结果集的技巧,遍历一遍便可求众数集合 - 二叉搜索树转成累加树
递归:中序,双指针操作累加
迭代:模拟中序,逻辑相同
- 二叉树公共祖先问题
- 二叉树的公共祖先问题
递归:后序,回溯,找到左子树出现目标值,右子树节点目标值的节点。
迭代:不适合模拟回溯 - 二叉搜索树的公共祖先问题
递归:顺序无所谓,如果节点的数值在目标区间就是最近公共祖先
迭代:按序遍历
- 二叉搜索树的修改与构造
- 二叉搜索树中的插入操作
递归:顺序无所谓,通过递归函数返回值添加节点
迭代:按序遍历,需要记录插入父节点,这样才能做插入操作 - 二叉搜索树中的删除操作
递归:前序,想清楚删除非叶子节点的情况
迭代:有序遍历,较复杂 - 修剪二叉搜索树
递归:前序,通过递归函数返回值删除节点
迭代:有序遍历,较复杂 - 构造二叉搜索树
递归:前序,数组中间节点分割
迭代:较复杂,通过三个队列来模拟
- 注意点
涉及到二叉树的构造,无论普通二叉树还是二叉搜索树一定前序,都是先构造中节点
求普通二叉树的属性,一般是后序,一般要通过递归函数的返回值做计算
求二叉搜索树的属性,一定是中序,利用二叉搜索树的有序性