一、二叉树理论基础
1.二叉树的分类
2.二叉树的定义
class TreeNode{
TreeNode*left;//左节点
TreeNode*right;//右节点
int val;//当前节点值
TreeNode(){val=0; left=nullptr; right==nullptr;}//默认构造函数
TreeNode(int val,TreeNode*left,TreeNode*right)//构造函数
{
val=0;
this.left=left;
this.right=right;
}
};
3.二叉树的存储方式
二叉树可以链式存储,也可以顺序存储。
链式存储如图:
顺序存储的方式如图
如果父节点的数组下标是 i,那么它的左孩子就是 i * 2 + 1,右孩子就是 i * 2 + 2。
4.二叉树的遍历方式
- 深度优先遍历(这里前中后,其实指的就是中间节点的遍历顺序)
- 前序遍历(递归法,迭代法)
- 中序遍历(递归法,迭代法)
- 后序遍历(递归法,迭代法)
- 广度优先遍历
- 层次遍历(迭代法)
接下来依次对这些二叉树的遍历进行代码展示:
4.1 二叉树的前(中、后)序遍历【递归法】
//二叉树的前序遍历
void recursion(TreeNode* root)
{
if(root==nullptr)//退出当前节点的条件
return;
/*在这部分对节点进行处理*/
cout<<root->val<<endl;
/*-------------------*/
recursion(root->left);
recursion(root->right);
}
//二叉树的中序遍历
void recursion(TreeNode* root)
{
if(root==nullptr)//退出当前节点的条件
return;
recursion(root->left);
/*在这部分对节点进行处理*/
cout<<root->val<<endl;
/*-------------------*/
recursion(root->right);
}
//二叉树的后序遍历
void recursion(TreeNode* root)
{
if(root==nullptr)//退出当前节点的条件
return;
recursion(root->left);
recursion(root->right);
/*在这部分对节点进行处理*/
cout<<root->val<<endl;
/*-------------------*/
}
递归三要素:
确定递归函数的参数和返回值: 确定哪些参数是递归的过程中需要处理的,那么就在递归函数里加上这个参数, 并且还要明确每次递归的返回值是什么进而确定递归函数的返回类型。
确定终止条件: 写完了递归算法, 运行的时候,经常会遇到栈溢出的错误,就是没写终止条件或者终止条件写的不对,操作系统也是用一个栈的结构来保存每一层递归的信息,如果递归没有终止,操作系统的内存栈必然就会溢出。
确定单层递归的逻辑: 确定每一层递归需要处理的信息。在这里也就会重复调用自己来实现递归的过程。
4.2 二叉树前序遍历【栈迭代法】
void front_recursion(TreeNode* root)
{
stack<TreeNode*> sta;//用于存储树节点的栈
sta.push(root);//首先将根节点存储到栈中
vector<int> result;//保存节点值的数组
while (!sta.empty())
{
//取出栈顶元素,并将栈顶元素弹出
TreeNode* node = sta.top();
sta.pop();
/*在这部分对节点进行处理*/
result.push_back(node->val);
/*------------------------*/
//如果该节点存在左右子节点,将子节点存入栈中
if (node->left)
sta.push(node->left);
if (node->right)
sta.push(node->right);
}
return;
}
1.将树的根节点先存入栈中;
2.取出栈中的根节点,对其进行按需处理;
3.将其左右节点依次存入栈中(如果存在的话);
4.返回step2,如此循环,直至栈空。
4.3 二叉树后序遍历【栈迭代法】
//迭代法二叉树后序遍历
void back_recursion(TreeNode* root)
{
stack<TreeNode*> sta;//用于存储树节点的栈
sta.push(root);//首先将根节点存储到栈中
vector<int> result;//保存节点值的数组
while (!sta.empty())
{
//取出栈顶元素,并将栈顶元素弹出
TreeNode* node = sta.top();
sta.pop();
/*在这部分对节点进行处理*/
result.push_back(node->val);
/*------------------------*/
//如果该节点存在左右子节点,将子节点存入栈中【区别一:后序遍历与前序遍历存入节点顺序相反】
if (node->right)
sta.push(node->right);
if (node->left)
sta.push(node->left);
}
//【区别二】
reverse(result.begin(), result.end());
return;
}
后序遍历与前序遍历的迭代法类似,只不过右两处区别:
区别一:除根节点外,子节点压入栈中的顺序为先右后左;
区别二:最后获得结果数组需要reverse后才获得最终后序遍历顺序的结果数组;
4.4 二叉树中序遍历【栈迭代法】
//迭代法二叉树中序遍历
void midum_recursion(TreeNode* root)
{
stack<TreeNode*> sta;//用于存储树节点的栈
vector<int> result;//保存节点值的数组
while (root != nullptr || !sta.empty())
{
//沿着树的左节点往下走
while (root != nullptr)
{
sta.push(root);
root = root->left;
}
//当前节点为空时退出,取出栈顶元素按需处理
TreeNode* node = sta.top();
sta.pop();
/*在这部分对节点进行处理*/
result.push_back(node->val);
/*------------------------*/
//切换至右节点,继续遍历树节点
root = node->right;
}
}
二叉树的中序遍历与前序遍历(后序遍历)的迭代方式均不一样;
1.先沿着根节点的左子节点往下遍历,并将途经节点压入栈中;
2.若当前节点遍历节点为空,则取出栈顶节点,按需对该节点进行处理;
3.将当前遍历的节点切换至取出栈顶节点的右子节点,返回step1,如此循环,直至栈中元素为空且当前节点也为空;
4.4 二叉树层序遍历【队列迭代法】
void travesal(TreeNode* root)
{
queue<TreeNode*> que1;//保存当前层节点的队列
que1.push(root);//将树的根节点压入队列
vector<vector<int>> result;//保存节点值的数组
while (!que1.empty())
{
vector<int> array;//保存每一层节点值的临时数组
queue<TreeNode*> que2;//保存下一层节点的队列
while (!que1.empty())
{
//从当前层队列que1中取出队顶的节点元素
TreeNode* node = que1.front();
que1.pop();
/*在这部分对节点进行处理*/
array.push_back(node->val);
/*------------------------*/
//如果该节点存在左右子节点,将子节点存入下层队列que2中
if (node->right)
que2.push(node->left);
if (node->left)
que2.push(node->right);
}
que1 = que2;//更新当前队列节点
result.push_back(array);//将当前层的节点值存入二维数组中
}
}
采用两个队列进行层序遍历,一个队列que1用于保存当前层的节点,另一个队列que2用于保存下一层的节点。当前层节点遍历完后,将下一层节点更新至当前层。
1.将根节点压入当前层队列中;
2.遍历当前层队列,取出节点,按需对节点进行处理。并将取出节点的左右子节点依次压入下一层队列que2中。
3.当前层队列为取空后,用一层节点队列que2更新当前层队列que1。更新后的que1不为空时,循环返回step2,更新后的que1为空时,层序遍历结束。
二、二叉树题目分类
1.二叉树遍历方式
1. 填充每个节点的下一个右侧节点指针
主要注意:1.节点在压入队列时的顺序; 2.直接对当前层进行节点连起来;
//采用队列进行层序遍历
//每一层进行next连接
class Solution {
public:
Node* connect(Node* root) {
Node* result=root;
//排除头节点为空时的情况
if(root==nullptr)
return result;
queue<Node*> que1;
que1.push(root);
while(!que1.empty())
{
queue<Node*> que2;
while(!que1.empty())
{
//取出靠左边的节点
Node* temp=que1.front();
que1.pop();
//判断该节点是否存在右边的节点,不存在则next==nullptr,存在则next连接上下一节点
if(que1.empty())
{
temp->next=nullptr;
}else
{
temp->next=que1.front();
}
//用队列que2存储下一层的节点
if(temp->left!=nullptr)
que2.push(temp->left);
if(temp->right!=nullptr)
que2.push(temp->right);
}
que1=que2;
}
return result;
}
};
2.二叉树的属性
1.从根节点到叶节点数字之和
class Solution {
public:
int sum=0;
void backTree(TreeNode* root,int val)
{
if(root==nullptr)
return;
//在每一个节点判断该节点是否为叶子节点,如果是,则累加从根节点到该节点的组合数值
if(root->left==nullptr&&root->right==nullptr)
{
val=10*val+root->val;
sum+=val;
return;
}
//如果当前节点不是叶子节点,则继续累加当前经过节点的路径的组合数值
val=10*val+root->val;
backTree(root->left,val);
backTree(root->right,val);
}
int sumNumbers(TreeNode* root) {
int val=0;
backTree(root,val);
return sum;
}
};
2.相同的树
class Solution {
public:
bool backTree(TreeNode* p, TreeNode* q)
{
//如果两个节点都为空,则不用再该层往下遍历,直接返回true
if(p==nullptr&&q==nullptr)
return true;
//只有一个节点为空,或者两个节点值不相等,直接返回false
if(p==nullptr||q==nullptr)
return false;
else if(p->val!=q->val)
return false;
//分别对这两个节点的左节点,右节点进行比较
if(backTree(p->left, q->left)==false) return false;
if(backTree(p->right, q->right)==false) return false;
//左节点右节点遍历到最后还是没有返回false,最终返回true
return true;
}
bool isSameTree(TreeNode* p, TreeNode* q) {
return backTree(p, q);
}
};
113.路径总和||
class Solution {
public:
vector<vector<int>> result;
vector<int> array;
int cur=0;
void backTree(TreeNode* root, int target)
{
if(root==nullptr)
return;
array.push_back(root->val);
cur+=root->val;
//在这里判断是否满足路径和==target,满足则将其加入结果数组
//但是不要return,因为后面还要减root->val,为其他路径遍历回退。
if(root->left==nullptr&&root->right==nullptr)
{
if(cur==target)
{
result.push_back(array);
}
}
backTree(root->left, target);
backTree(root->right, target);
array.pop_back();
cur-=root->val;
}
vector<vector<int>> pathSum(TreeNode* root, int target) {
backTree(root, target);
return result;
}
};
222.完全二叉树的节点个数
在完全二叉树中,如果递归向左遍历的深度等于递归向右遍历的深度,那说明就是满二叉树。
class Solution {
public:
int countNodes(TreeNode* root) {
if (root == nullptr) return 0;
TreeNode* left = root->left;
TreeNode* right = root->right;
int leftDepth = 0, rightDepth = 0; // 这里初始为0是有目的的,为了下面求指数方便
while (left) { // 求左子树深度
left = left->left;
leftDepth++;
}
while (right) { // 求右子树深度
right = right->right;
rightDepth++;
}
if (leftDepth == rightDepth) {
return (2 << leftDepth) - 1; // 注意(2<<1) 相当于2^2,所以leftDepth初始为0
}
return countNodes(root->left) + countNodes(root->right) + 1;
}
};
3.二叉树公共祖先问题
1、普通二叉树最近公共祖先
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;
}
}
};
2、二叉搜索树的最近公共祖先
解题关键:当我们从上向下去递归遍历,第一次遇到 cur节点是数值在[p, q]区间中,那么cur就是 p和q的最近公共祖先。
4.二叉搜索树的修改与构造
1.将二叉搜索树变平衡
解法一:在将根节点作为函数的参数,因为需要对形参的root->left root->right进行赋值,使其在退出函数后改变仍在,所以形参为参数引用的方式 void Tree(TreeNode* &root,vector<int> array,int start,int end)
注意:中间节点的位置不是(end-start)/2,而是start+(end-start)/2
class Solution {
private:
vector<int> array;
public:
// 有序树转成有序数组
void traversal(TreeNode* cur) {
if (cur == nullptr) {
return;
}
traversal(cur->left);
array.push_back(cur->val);
traversal(cur->right);
}
//注意!!!!:此处的root指针用的是引用变量,只有引用变量在函数内部对该形参进行修改后,该形参将发生变化
void Tree(TreeNode* &root,vector<int> array,int start,int end)
{
if(end-start<0)
return ;
//计算父节点的值
int mid=start+(end-start)/2;
cout<<mid<<endl;
//对父节点进行赋值
TreeNode* node=new TreeNode(array[mid]);
root=node;
//分别对左右子节点进行赋值
Tree(root->left,array,start,mid-1);
Tree(root->right,array,mid+1,end);
}
TreeNode* balanceBST(TreeNode* root) {
traversal(root);
TreeNode* r=new TreeNode(0);
Tree(r,array,0,array.size()-1);
return r;
}
};
解法二:直接在函数内部定义一个节点,待二叉树向下蔓延以后,最后返回该节点
class Solution {
private:
vector<int> array;
public:
// 有序树转成有序数组
void traversal(TreeNode* cur) {
if (cur == nullptr) {
return;
}
traversal(cur->left);
array.push_back(cur->val);
traversal(cur->right);
}
TreeNode* Tree(vector<int> array,int start,int end)
{
if(end-start<0)
return nullptr;
int mid=start+(end-start)/2;
//直接在函数内部定义一个节点,待二叉树向下蔓延以后,最后返回该节点
TreeNode* root=new TreeNode(array[mid]);
root->left=Tree(array,start,mid-1);
root->right=Tree(array,mid+1,end);
return root;
}
TreeNode* balanceBST(TreeNode* root) {
traversal(root);
return Tree(array,0,array.size()-1);
}
};
2、删除二叉树中的节点
本题解题需要注意以下两点:
1、在寻找需要删除的目标节点过程中,需要保存其父节点。
为了避免要删除的节点为根节点,根节点没有父节点这种情况,在父节点初始化时,初始化为根节点的父节点。即TreeNode* parent=new TreeNode(全局最大值,root,nullptr) 或TreeNode* parent=new TreeNode(全局最小值,nullptr,root);
2、在删除节点时分三种情况
被删除节点无左子节点;
被删除节点无右子节点;
被删除节点左右子节点都存在;
class Solution {
public:
TreeNode* deleteNode(TreeNode* root, int key) {
if(root==nullptr)
return nullptr;
//避免要删除的节点为根节点时,没有父节点
TreeNode* parent=new TreeNode(100001,root,nullptr);
TreeNode* result=parent;
while(root!=nullptr)
{
if(root->val>key)
{
parent=root;
root=root->left;
}
else if(root->val<key)
{
parent=root;
root=root->right;
}
else
break;
}
if(root==nullptr)
return result->left;
//被删除节点,左子节点为空
if(root->left==nullptr)
{
if(root->val<parent->val)
parent->left=root->right;
else
parent->right=root->right;
}
//被删除节点,右子节点为空
else if(root->right==nullptr)
{
if(root->val<parent->val)
parent->left=root->left;
else
parent->right=root->left;
}
//被删除节点左右子节点都不为空
else
{
//将被删除节点左节点作为其右子节点最左叶子节点中
//被删除右子节点顶替被删除节点位置
TreeNode* nodeR=root->right;
TreeNode* temp=nodeR;
while(nodeR->left!=nullptr)
{
nodeR=nodeR->left;
}
nodeR->left=root->left;
if(temp->val<parent->val)
parent->left=temp;
else
parent->right=temp;
}
return result->left;
}
};
3、修剪二叉搜索树
5.二叉树的修改与构造
6.求二叉搜索树的属性
关于二叉树必须知道的证明:对于任一二叉树,若度为2的结点有n2个,则叶子结点数必为n2+1。
假设该二叉树总共有n个结点(n=n0+n1+n2),则该二叉树总共会有n-1条边,度为2的结点会延伸出两条边,同理,度为1的结点会延伸出一条边,则可列公式:n-1 = 2*n2 + 1*n1 ,合并两个式子可得:2*n2 + 1*n1 +1 =n0 + n1 + n2 ,则计算可知 n0=n2+1。