二、树
写在最前面,当我们面对树结构时,不应该把树结构看成一种特殊的算法,树结构只是一种“树”形式的数据存储方式,可以将树看成一种特殊的数组。树结构与数组之间只是数据遍历方式的不同:对于数组,在遍历过程中只存在两个方向,但对与树结构可分为前序遍历,中序遍历和后序遍历三种方式,对于不同的树,其通过不同的遍历方式存在着不同性质 (如,BST树在中序遍历时就是排序后的数组,当搜索路径始终向下就是前序遍历)。
面对树的数据结构问题,重点是要理解不同种类的树的性质,以及其运算过程。在树的数据结构中,常用递归来求解,必须熟悉掌握递归过程中的逻辑关系。
针对树数据结构的递归问题,通常通过以下一个三个步骤去理解和建立递归:
① 整个递归的终止条件及终止后的返回值。
② 找到递归的返回值-注意,这里的递归返回值与终止后返回值不同,这里是指应该向上一级返回什么信息。
③ 本级递归 (一个结点与其左右结点) 应该做怎样的运算(递归是一个反复调用自身的过程,这就说明它每一级的功能都是一样的,只要确定了其中一级的递归操作就确定了整个递归过程) 不要去纠结每一级调用和返回的细节。
1. 普通二叉树
★为必须掌握的基础题。
🐟1. 104. 二叉树的最大深度 ★
给定一个二叉树,找出其最大深度。二叉树的深度为根节点到最远叶子节点的最长路径上的节点数。
输入 | 输出 | 解释 |
---|---|---|
[3,9,20,null,null,15,7] | 最大深度 3 |
思路:根据上述的三步思路进行分析:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val; TreeNode *left; TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
int maxDepth(TreeNode* root) {
if(root==NULL) //终止条件
return 0; //终止条件的返回值
int left_num=maxDepth(root->left);
int right_num=maxDepth(root->right);
return max(left_num,right_num)+1; //递归的返回值,其中max(left_num,right_num)+1为本级递归进行的运算
}
};
🐟 2. 112. 路径总和 ★
给定一个二叉树和一个目标和,判断该树中是否存在根节点到叶子节点的路径,这条路径上所有节点值相加等于目标和。说明: 叶子节点是指没有子节点的节点。
输入 | 输出 | 解释 |
---|---|---|
目标和 sum = 22 | true | 因为存在目标和为 22 的根节点到叶子节点的路径 5->4->11->2 |
思路:根据三步思路进行分析:
class Solution {
public:
int flag=0;
bool hasPathSum(TreeNode* root, int sum) {
if(root==NULL) //终止条件
return false;
if(root->left==NULL && root->right==NULL) //找到递归的返回值
return sum-root->val==0;
return hasPathSum(root->left,sum-root->val) || hasPathSum(root->right,sum-root->val); //本级递归应该做怎样的运算
//hasPathSum(root->left,sum-root->val)左子树满足
//hasPathSum(root->right,sum-root->val)右子树满足
}
};
🐟 3. 437. 路径总和 III ★
给定一个二叉树,它的每个结点都存放着一个整数值。找出路径和等于给定数值的路径总数。路径不需要从根节点开始,也不需要在叶子节点结束,但是路径方向必须是向下的(只能从父节点到子节点)。
输入 | 输出 | 解释 |
---|---|---|
sum = 8 | 3 | 和等于 8 的路径有: 1. 5 -> 3 2. 5 -> 2 -> 1 3. -3 -> 11 |
思路: 此题涉及了二叉树的二重循环遍历
① 首先路径的开头可以不是根节点,结束也可以不是叶子节点。
② 其次搜索的路径方向必须是向下的,这个方式符合二叉树的前序遍历,前序遍历会先访问根节点
③ 然后依次访问左,右节点。然后在解决了以根节点开始的所有路径后,就要找以根节点的左孩子和右孩子开始的所有路径,三个节点构成了一个递归结构;
class Solution {
public:
int res=0;
int pathSum(TreeNode* root, int sum) {
if(root==NULL)
return 0;
//在解决了根节点开始的所有路径后,以根节点的左孩子和右孩子开始的所有路径(树结构的二重循环遍历)
return path(root,sum)+pathSum(root->left,sum)+pathSum(root->right,sum);
}
int path(TreeNode* root,int sum){ //根据路径方向必须是向下的,利用前序遍历来计算
if(root==NULL)
return 0;
int res=0;
if(root->val==sum)
res++;
res+=path(root->left,sum-root->val); //前序遍历
res+=path(root->right,sum-root->val);
return res;
}
};
🐟 4. 543. 二叉树的直径
给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过根结点。
输入 | 输出 | 解释 |
---|---|---|
返回 3 | 它的长度是路径 [4,2,1,3] 或者 [5,2,1,3] |
思路:拿到问题不要上来就开始写程序,要仔细分析问题,
找到问题的关联和细节。如本题,此题求二叉树直径,且路径可能穿过根节点。那么根据分治策略,可以将此问题分为根节点左边直径和根节点右边直径两部分。这时可以发现,左,右子树直径分别就是其深度。因此此问题转化为求解二叉树的左,右子树的最大深度。
class Solution {
public:
int diameterOfBinaryTree(TreeNode* root) {
if(root==NULL || (root->left==NULL && root->right==NULL))
return 0;
int weight=0;
getwight(root,weight);
return weight;
}
int getwight(TreeNode* root,int &cnt){
if(root==NULL)
return 0;
int left=getwight(root->left,cnt); //求左子树深度
int right=getwight(root->right,cnt); //求右子树深度
cnt=max(left+right,cnt); //记录左右子树的最大深度和
return max(left,right)+1;
}
};
🐟 5. 563. 二叉树的坡度 ★
给定一个二叉树,计算整个树的坡度。一个树的节点的坡度定义即为,该节点左子树的结点之和和右子树结点之和的差的绝对值。空结点的的坡度是0。整个树的坡度就是其所有节点的坡度之和。
输入 | 输出 | 解释 |
---|---|---|
1 | 结点的坡度 2 : 0 结点的坡度 3 : 0 结点的坡度 1 : |2-3| = 1 树的坡度 : 0 + 0 + 1 = 1 |
思路: 此题涉及了二叉树的二重循环遍历
因为坡度为该节点左子树的结点之和和右子树结点之和的差的绝对值,所以问题关键的求左子树节点和和右子树节点和。
class Solution {
public:
int sum=0;
int findTilt(TreeNode* root) {
if(root==NULL)
return 0;
return findTilt(root->left)+findTilt(root->right)+abs(getSum(root->left)-getSum(root->right));
//findTilt(root->left) 转向左节点 findTilt(root->right) 转向右节点
//abs(getSum(root->left)-getSum(root->right)) 计算当前节点左子树和右子树之和的差
}
int getSum(TreeNode *root){ //获得某节点的节点和
if(root==NULL)
return 0;
return getSum(root->left)+getSum(root->right)+root->val;
}
};
🐟 6. 100. 相同的树 ★
给定两个二叉树,编写一个函数来检验它们是否相同。如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。
输入 | 输出 | 解释 |
---|---|---|
true |
思路:这个题是一道基础题,就是一一对比每个元素。
class Solution {
public:
bool isSameTree(TreeNode* p, TreeNode* q) {
if(p==NULL && q==NULL)
return true;
if(p==NULL || q==NULL)
return false;
if(p->val!=q->val)
return false;
return isSameTree(p->left,q->left) && isSameTree(p->right,q->right);
}
};
🐟 7. 637. 二叉树的层平均值 ★
给定一个非空二叉树, 返回一个由每层节点平均值组成的数组.
输入 | 输出 | 解释 |
---|---|---|
[3, 14.5, 11] | 第0层的平均值是 3, 第1层是 14.5, 第2层是 11. 因此返回 [3, 14.5, 11]. |
思路:二叉树的层平均值就是要按照二叉树广度优先排序(BFS),然后按层求平均值。
class Solution {
public:
vector<double> averageOfLevels(TreeNode* root) {
vector<double> aver;
if(root==NULL)
return aver;
queue<TreeNode*> Q;
Q.push(root);
while(!Q.empty()){
int n=Q.size();
int count=0;
double num=0;
while(count++<n){ //计算本层的平均值
TreeNode *temp=Q.front(); //BFS的关键步骤1
Q.pop(); //BFS的关键步骤2
num+=temp->val;
if(temp->left) Q.push(temp->left); //BFS的关键步骤3
if(temp->right) Q.push(temp->right); //BFS的关键步骤4
}
aver.push_back(num/n);
}
return aver;
}
};
🐟 8. 653. 两数之和 IV - 输入 BST
给定一个二叉搜索树和一个目标结果,如果 BST 中存在两个元素且它们的和等于给定的目标结果,则返回 true。
输入 | 输出 | 解释 |
---|---|---|
Target = 9 | True |
思路: 此题与【1. 两数之和】相似。利用unordered_set
来存储计算的差值,再次计算差值后在unordered_set
中进行查找,如果查找到则返回为true,否则将差值放入到unordered_set
。然后递归到下一元素。
class Solution {
public:
unordered_set<int> record;
bool findTarget(TreeNode* root, int k) {
if(root==NULL)
return false;
return findsum(root,k);
}
bool findsum(TreeNode* root, int k){
if(root==NULL)
return false;
int diff=k-root->val;
if(record.find(diff)!=record.end())
return true;
record.insert(root->val);
return findsum(root->left,k) || findsum(root->right,k);
}
};
2. 二叉搜索树(BST)
在求解二叉搜索树的问题之前,一定要深刻的了解二叉搜索树的性质。在BST中最重要的性质就是当其按照中序遍历时,其本质是一个排序后的数组。
🐟1. 108. 将有序数组转换为二叉搜索树
将一个按照升序排列的有序数组,转换为一棵高度平衡二叉搜索树。本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
输入 | 输出 |
---|---|
给定有序数组: [-10,-3,0,5,9], | [0,-3,9,-10,null,5],它可以表示下面这个高度平衡二叉搜索树: 0 / \ -3 9 / / -10 5 |
思路:
构造一棵树的过程可以拆分成无数个这样的子问题:构造树的每个节点以及节点之间的关系。对于每个节点来说,需要以下三个步骤:
① 选取节点
② 构造该节点的左子树
③ 构造该节点的右子树
这道题实际上是平衡BST中序遍历的逆过程。由于要将有序数组转为平衡二叉搜索树,由于二叉搜索树可以看做是二分查找算法的另一个存储方式,将有序数组转为平衡二叉搜索树可以利用二分查找法,在选取节点时选择数组的中点作为根节点,以此来保证平衡性。
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val; TreeNode *left; TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
class Solution {
public:
TreeNode* sortedArrayToBST(vector<int>& nums) {
if(nums.size()==0)
return NULL;
return getTree(nums,0,nums.size()-1);
}
TreeNode *getTree(const vector<int> &nums,int l,int r){
if(l<=r){ //终止条件
int mid=(r-l)/2+l; //获取 数组的中点 作为根节点
TreeNode *Node=new TreeNode(nums[mid]); //创建新节点 ,每一级需要做的(运算)
Node->left=getTree(nums,l,mid-1); //创建左子树
Node->right=getTree(nums,mid+1,r); //创建右子树
return Node; //每一级递归的返回值
}else
return NULL; //终止条件的返回值
}
};
🐟 2. 235. 二叉搜索树的最近公共祖先
给定一个二叉搜索树, 找到该树中两个指定节点的最近公共祖先。
百度百科中最近公共祖先的定义为:“对于有根树 T 的两个结点 p、q,最近公共祖先表示为一个结点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”
输入 | 输出 | 解释 |
---|---|---|
root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 8 root = [6,2,8,0,4,7,9,null,null,3,5], p = 2, q = 4 | 6 2 | 节点 2 和节点 8 的最近公共祖先是 6。 节点 2 和节点 4 的最近公共祖先是 2, 因为根据定义最近公共祖先节点可以为节点本身 |
思路:
根据二叉搜索树的性质,若p
,q
结点的值都大于root
结点的值,则p,q
结点都在root->right
中,若p
,q
结点的值都小于root
结点的值,则p,q
结点都在root->left
中。若p,q
结点的值一个大于root->val
,一个小于root->val
,则root
为p,q
结点的公共祖先。
class Solution {
public:
TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
if(root==NULL)
return NULL;
if(p->val<root->val && q->val<root->val)
return lowestCommonAncestor(root->left,p,q); //p,q结点的值都小于root结点的值,则p,q结点都在root->left中
if(p->val>root->val && q->val>root->val)
return lowestCommonAncestor(root->right,p,q); //p,q结点的值都大于root结点的值,则p,q结点都在root->right中
return root;
}
};
🐟 3. 501. 二叉搜索树中的众数
给定一个有相同值的二叉搜索树(BST),找出 BST 中的所有众数(出现频率最高的元素)。假定 BST 有如下定义:
(1).结点左子树中所含结点的值小于等于当前结点的值
(2).结点右子树中所含结点的值大于等于当前结点的值
(3).左子树和右子树都是二叉搜索树
输入 | 输出 | 解释 |
---|---|---|
给定 BST [1,null,2,2] | 返回[2] |
思路:根据二叉搜索树的性质,其中序遍历是排序数组,所以相当于在排序数组中找众数。可以参考数组问题中的双指针方法,求取排序数组中的众数。
class Solution {
public:
vector<int> findMode(TreeNode* root) {
vector<int> res;
if(root==NULL)
return res;
int curTime=1; //当前元素的出现次数
int maxTime=0; //出现次数最多的元素的出现次数
TreeNode *pre=NULL;
helper(root,pre,curTime,maxTime,res);
return res;
}
/*设置双指针算法,pre为前一个索引,root为当前元素索引*/
void helper(TreeNode *root,TreeNode *&pre,int &curTime,int &maxTime,vector<int>&res){
if(root==NULL)
return;
helper(root->left,pre,curTime,maxTime,res); //BST中序遍历
if(pre) //如果前一个索引不为空,则比较当前元素与前一索引的值,若相同curTime+1
curTime=(root->val==pre->val)?curTime+1:1;
if(curTime==maxTime) //若curTime与maxTime相同,则将该数值放入res.
res.push_back(root->val);
if(curTime>maxTime){ //若curTime大于maxTime,则众数元素为此元素,因此清空res,并将当前元素放入res
res.clear();
res.push_back(root->val);
maxTime=curTime;
}
pre=root; //更改前一索引值
helper(root->right,pre,curTime,maxTime,res); //BST中序遍历
}
};
🐟 4. 530. 二叉搜索树的最小绝对差
给定一个所有节点为非负值的二叉搜索树,求树中任意两节点的差的绝对值的最小值。
输入 | 输出 | 解释 |
---|---|---|
1 | 最小绝对差为1,其中 2 和 1 的差的绝对值为 1(或者 2 和 3) |
思路:由于所有节点为非负节点,且求树中任意两节点的差的绝对值的最小值,由二叉搜索树性质,中序遍历时为排序数组,且任意两节点的差的绝对值的最小值一定位于排序数组的相邻元素。
class Solution {
public:
int getMinimumDifference(TreeNode* root) {
int pre=-1; //用于存储排序数组中的上一元素
int res=INT_MAX;
getmin(root,res,pre);
return res;
}
void getmin(TreeNode *root,int &res,int &pre){
if(root==NULL)
return;
getmin(root->left,res,pre); //中序遍历
if(pre>=0)
res=min(root->val-pre,res); //计算排序数组相邻元素的差值,与上一差值求取最小值
pre=root->val; //存储这一元素
getmin(root->right,res,pre); //中序遍历
}
};
3. 平衡二叉树(AVL)
因为AVL的本质是BST,AVL是在BST的基础上增加了分支的旋转操作。
4. N叉树
N叉树可以以二叉树为对象进行类比,将二叉树的两个分支方式改为通过循环访问分支的方式,就可以将二叉树改为N叉树。
🐟 1. 559. N叉树的最大深度
给定一个 N 叉树,找到其最大深度。最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。
输入 | 输出 | 解释 |
---|---|---|
给定一个 3叉树 : | 其最大深度为3。 |
思路:根据二叉树求深度的原理:
class Solution {
public:
int maxDepth(Node* root) {
if(root==NULL)
return 0;
int depth=0;
for(int i=0;i<root->children.size();i++){ //通过遍历的方式,访问根节点的子节点
depth=max(maxDepth(root->children[i]),depth);
}
return depth+1;
}
};