一、二叉树的重要性
很多经典算法如 回溯、广度优先遍历、分治、动态规划等通常需要转化为树的问题,而树的题目难免涉及到递归的问题,因此掌握树的三种遍历框架是必须的。
先序遍历:根,左,右
中序遍历:左,根,右
后序遍历:左,右,根
可以看到三种遍历的命名主要看根遍历的顺序,左右的先后位置不变。
下面直接贴上遍历的框架代码:
/* 二叉树遍历框架 */
void traverse(TreeNode root) {
// 前序遍历
traverse(root.left)
// 中序遍历
traverse(root.right)
// 后序遍历
}
从上面的代码不难看出树的三种遍历方式其实只是改变了当前节点记录函数的位置,
刚开始学习递归算法的时候很难想明白里面的过程,其实理解遍历方式的话不需要对整棵树进行模拟,对他的整体逻辑理解或者找一个最简单的二叉树特例理解就好了,中间的过程就是因为很难用详细文字表征才用递归的。我们可以取后续遍历来分析,假设遍历的是一个最简单的两层二叉树。每次在函数要结束退出时才开始记录当前结点值。可以想象,因为左右结点的遍历函数在当前结点返回函数代码之前,他们退出函数时一定记录了了root->left.val 和 root->right.val,然后才论文记录 root->val,这样就实现了中序遍历。
进一步地,当层数变多时,由于左子树遍历一直在挖深度,所以会实现复杂的递归过程~~意会即可。
二、写递归算法的秘诀
写递归算法的关键是要明确函数的「定义」是什么,然后相信这个定义,利用这个定义推导最终结果,绝不要试图跳入递归。 怎么理解上面的话呢?可以看下下面的程序,定义:要求我们返回树的结点个数。
```cpp
// 定义:count(root) 返回以 root 为根的树有多少节点
int count(TreeNode root) {
// base case
if (root == null) return 0;
// 自己加上子树的节点数就是整棵树的节点数
return 1 + count(root.left) + count(root.right);
}
这个问题非常简单,大家应该都会写这段代码,root本身就是一个节点,加上左右子树的节点数就是以root为根的树的节点总数。
写树相关的算法,简单说就是,先搞清楚当前root节点该做什么,然后根据函数定义递归调用子节点,递归调用会让孩子节点做相同的事情。
上面的代码我们首先就是在最大或者说最开始框架下,需要返回的是节点总数(当前root该做什么),所以我需要计算当前结点个数+左子树节点个数+右子树节点个数即可。问题来了,左右子树节点个数怎么算?只要你在最大框架下待定的计算项和当前的计算过程相同,递归就完事了,因为你整棵树的节点计算框架已经出来了,左右子树也是树,调用本身即可。
三、算法实践
- Leetcode226. 翻转二叉树
- Leetcode116. 填充二叉树节点的右侧指针
- Leecode 117. 填充每个节点的下一个右侧节点指针 II
- Leecode 654. 最大的二叉树
- Leecode 105. 从前序与中序遍历序列构造二叉树
- Leecode 106. 从中序与后序遍历序列构造二叉树
第一题、翻转二叉树
我们先从简单的题开始,看看力扣第 226 题「翻转二叉树」,输入一个二叉树根节点root,让你把整棵树镜像翻转,比如输入的二叉树如下:
通过观察,我们发现只要把二叉树上的每一个节点的左右子节点进行交换,最后的结果就是完全翻转之后的二叉树。前序和 后序遍历的代码分别如下:
class Solution {
public:
TreeNode* invertTree(TreeNode* root) {
if (root == nullptr) {
return nullptr; //为什么不能直接return呢
} //在我们程序中返回的是TreeNode格式,没返回值会出错
TreeNode *tmp=root->left;
root->left=root->right; //前序遍历,先对左右交换,左右如果一个空一个非空,交换嘛
root->right=tmp; //这里前序遍历的意思就是操作时所处的上帝节点位置
invertTree(root->left);
invertTree(root->right);
return root;
}
};
————————————————————————————————————————
第二题:Leetcode116. 填充二叉树节点的右侧指针
该题是Leecode116题,要求将一个完美二叉树的next指针变成如下样子连接。这道题目最好的是使用层序遍历,层次遍历基于广度优先搜索,它与广度优先搜索的不同之处在于,广度优先搜索每次只会取出一个节点来拓展,而层次遍历会每次将队列中的所有元素都拿出来拓展,这样能保证每次从队列中拿出来遍历的元素都是属于同一层的,因此我们可以在遍历的过程中修改每个节点的 \text{next}next 指针,同时拓展下一层的新队列。
class Solution {
public:
Node* connect(Node* root) {
if (root == nullptr) {
return root;
}
// 初始化队列同时将第一层节点加入队列中,即根节点
queue<Node*> Q;
Q.push(root);
// 外层的 while 循环迭代的是层数
while (!Q.empty()) {
// 记录当前队列大小
int size = Q.size();
// 遍历这一层的所有节点
for(int i = 0; i < size; i++) {
// 从队首取出元素
Node* node = Q.front();
Q.pop();
// 连接
if (i < size - 1) {
node->next = Q.front();
}
// 拓展下一层节点
if (node->left != nullptr) {
Q.push(node->left);
}
if (node->right != nullptr) {
Q.push(node->right);
}
}
}
// 返回根节点
return root;
}
};
上面的题目再分享三种种递归的思路:
方法1: 每个节点需要做的事:把左子树的所有靠右边的节点的next指向右子树的所有靠左边的节点。
方法2: 加函数参数,一个节点做不到,我们就给他安排两个节点,「将每一层二叉树节点连接起来可以细化成将每两个相邻节点都连接起来
方法3: 通过判断当前数的next是否为null,非null进行紧邻操作~
class Solution {
public:
Node* connect(Node* root) {
if(root==NULL)//避免dfs时root为空报错,因为null没有left,right
{
return root;
}
if(root->left==NULL&&root->right==NULL)
{
return root;
}
Node* childLeft = root->left;
Node* childRight = root->right;
while(childLeft!=NULL)
{
childLeft->next = childRight;
childLeft = childLeft->right;
childRight = childRight->left;
}
connect(root->left);
connect(root->right);
return root;
}
};
class Solution {
public:
Node* connect(Node* root) {
if(root==NULL)
return root;
dfs(root->left,root->right); //说白了 当前层是rootq
return root;
}
void dfs(Node* root1, Node* root2){
if(!root1 || !root2)
return;
root1->next=root2; //当层得操作
dfs(root1->left,root1->right);
dfs(root2->left,root2->right); //这三个函数每个进去后都要经历3个递归调用 最终无死角。
dfs(root1->right,root2->left);
}
};
class Solution {
public:
Node* connect(Node* root) {
if(root==NULL)//避免dfs时root为空报错,因为null没有left,right
return root;
connection(root);
return root;
}
void connection(Node* root){
if(root->left==NULL)
return;
root->left->next=root->right;
if(root->next!=NULL)
root->right->next=root->next->left;
connection(root->left);
connection(root->right);
}
};
————————————————————————————————————————
第三题:Leecode 117. 填充每个节点的下一个右侧节点指针 II
这道题目是上面Leetcode116的升级,现在树的结构不再是完美二叉树了,用层序遍历仍然不影响算法的执行,但是递归就会相对麻烦一点,需要增加一个判断,先看层序遍历的算法:
class Solution {
public:
Node* connect(Node* root) {
if(!root)
return root;
queue<Node*>Queue;
Queue.push(root);
while(!Queue.empty()){
int size=Queue.size(); //注意一定要在循环之前记录size,不然每次节点扩张都增加了size
for(int i=0; i<size;i++){
Node* temp=Queue.front();
Queue.pop();
if(i<size-1)
temp->next=Queue.front();
//节点扩张
if(temp->left!=NULL)
Queue.push(temp->left);
if(temp->right!=NULL)
Queue.push(temp->right);
}
}
return root;
}
};
除了层序遍历的算法,Leetcode官方题解还给出了一个很有意思的算法,
具体来说:
从根节点开始。因为第 00层只有一个节点,不需要处理(不需要建立next指针操作),但我们可以在上一层为下一层建立next 指针。
该方法最重要的一点是:位于第 x层时为第 x+1 层建立 next 指针。一旦完成这些连接操作,移至第 x+1 层为第 x+2 层建立next 指针。通过这样的操作每一层都形成了单向链表的结构,我们可以用start来标记链表的初始位置,然后在每个链表元素对他的左右节点进行链表连接和新start的记录即可~~。退出循环的时候即start等于0,即下一层没有空结点
class Solution {
public:
void handle(Node* &last, Node* &p, Node* &nextStart) { //这是位置 不是连接 所以要改~~
if (last) {
last->next = p;
}
if (!nextStart) {
nextStart = p;
}
last = p;
}
Node* connect(Node* root) {
if (!root) {
return nullptr;
}
Node *start = root;
while (start) {
Node *last = nullptr, *nextStart = nullptr;
for (Node *p = start; p != nullptr; p = p->next) {
if (p->left) {
handle(last, p->left, nextStart);
}
if (p->right) {
handle(last, p->right, nextStart);
}
}
start = nextStart;
}
return root;
}
};
————————————————————————————————————————
第四题: Leecode 654 :最大的二叉树
class Solution {
public:
TreeNode* constructMaximumBinaryTree(vector<int>& nums) {
int l=0,r=nums.size()-1;
return newtree(nums,l,r);
}
TreeNode* newtree(vector<int>& nums,int l,int r){
if(l>r)
return nullptr;
int max=INT_MIN;int index=0;
for(int i=l; i<=r; i++){
if(nums[i]>max){
max=nums[i];
index=i;
}
}
TreeNode* root=new TreeNode(nums[index]);
root->left=newtree(nums,l,index-1);
root->right=newtree(nums,index+1,r);
return root;
}
};
————————————————————————————————————————
第五题:Leecode 105. 从前序与中序遍历序列构造二叉树
示例:
前序遍历:preorder=[3,9,20,15,7]
中序遍历:inorder= [9,3,15,20,7]
返回3,9,20,null,null,15,7]
思路: 这道题目挺有意思的,已知一个二叉树的前序和中序遍历数组,让我们构造对应的一个二叉树。我们直到前序遍历中二叉树的根节点出现在第一个位置。接下来不断对左子树和右子树进行递归实现前序遍历。本身前序和中序遍历的数组就是递归所得。注意到在前序遍历数组中 根 左 右,对应的数字是连续的,那么如果我们能根据中序遍历查到每一次左子树和右子树的区分临界数组下标就能对这些元素不断做递归最终构建二叉树。那么问题的突破口还是根节点!通过前序遍历的根节点是数组首位,然后在中序遍历中查找根节点的index,index左侧元素是左子树的个数。然后根据个数在前序遍历中划分出左右子树的范围和中序遍历左右子树的范围,分别传入当前根节点的root->left和root->right,最终不断递归构造二叉树,返回root~~~确定下标范围是为了之后的查找都在这些下标内赋值查找以及递归。递归算法的终止条件要从最后分析,当搜索为到叶节点了,此时某个子树的长度肯定是1,然后左边界等于右边界。 再下一次搜索时候我们程序默认是index+1的,右边界无法变化长度到顶了,导致 是左边界>右边界此时要return nullptr 给他的左右子树赋0了~ 注意到虽然每次递归都生成新的树空间,但是都归并到了root的左节点或者右结点~ 下面附上代码:
class Solution {
public:
TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
//很明显此题需要递归
return newtree(preorder,0,preorder.size()-1,inorder,0,inorder.size()-1);
}
//构造调用函数
TreeNode* newtree(vector<int>& preorder,int l1,int r1, vector<int>& inorder,int l2,int r2){
if(l1>r1)
return nullptr;
TreeNode* root=new TreeNode();
root->val=preorder[l1];
//要找到根节点在中序遍历中的位置
int index=0;int length=0;
for(int i=l2;i<=r2;i++){
if(inorder[i]==root->val){
index=i;
length=index-l2;
break;
}
}
//构建左右子树
root->left=newtree(preorder,l1+1,l1+length,inorder,l2,index-1);
root->right=newtree(preorder,l1+length+1,r1,inorder,index+1,r2);
return root;
}
};
第六题:Leecode 106. 从中序与后序遍历序列构造二叉树
思路:这一题其实和上面的第五题是一个思路,知识将前序换成了后序,同样我们需要找一个突破口。之前的突破口是根结点是前序数组中第一个元素。在这里我们根节点变成了后序数组中的最后一个元素~,基本思路和上题一致,可写出:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode() : val(0), left(nullptr), right(nullptr) {}
* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
* TreeNode(int x, TreeNode *left, TreeNode *right) : val(x), left(left), right(right) {}
* };
*/
class Solution {
public:
TreeNode* buildTree(vector<int>& inorder, vector<int>& postorder) {
//很明显此题需要递归
return newtree(inorder,0,inorder.size()-1,postorder,0,postorder.size()-1);
}
//构造递归函数
TreeNode* newtree(vector<int>& inorder,int l1,int r1, vector<int>& postorder,int l2,int r2){
if(l1>r1)
return nullptr;
TreeNode* root=new TreeNode();
root->val=postorder[r2];
//要找到根节点在中序遍历中的位置
int index=0;int length=0;
for(int i=l1;i<=r1;i++){
if(inorder[i]==root->val){
index=i;
length=index-l1;
break;
}
}
//构建左右子树
root->left=newtree(inorder,l1,index-1,postorder,l2,l2+length-1);
root->right=newtree(inorder,index+1,r1,postorder,l2+length,r2-1);
return root;
}
};