LeetCode刷题 树专题

树专题

关于树的几个基本概念

1 树的节点定义

对于我们来说,我们遇到的树一般都是二叉树,一般有根节点以及各个中间节点和叶子节点构成。二叉树的父节点最多只有两个儿子节点,以下是二叉树节点的定义:

/**
 * 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) {}
 * };
 */

2 关于二叉树的遍历方法

2.1 前序遍历

前序遍历的思想是先访问父节点,再访问左儿子,最后访问右儿子
对于这种遍历我们一般是通过递归的方式进行,但采用来代替递归也是一个不错的方法
我们这里直接给出递归的算法,对于栈的应用,我们只简单介绍,在题目中有深入探讨

递归前序遍历

void preorder(TreeNode* root)
{
	if(!root) return;
	cout<<root->val<<endl;
	preorder(root->left);
	preorder(root->right);
	return;
}

栈 前序遍历

void preorder(TreeNode* root)
{
	if(!root) return;
	stack<TreeNode*> stk;
	stk.push(root);
	while(!stk.empty())
	{
		TreeNode t=stk.top();
		stk.pop();
		cout<<t->val<<endl;
		//根据栈的先进后出,我们要先访问left孩子就要让left孩子后入栈
		if(t->right) stk.push(t->right);
		if(t->left) stk.push(t->left);
	}
	return;
}

2.2 中序遍历

与前序遍历只是略有不同,我们的访问顺序改变了,先访问左儿子,再访问父节点,最后访问右儿子
同样可以使用栈 和 递归两种方式进行遍历操作

递归中序遍历

void inorder(TreeNode* root)
{
	if(!root) return;
	inorder(root->left);
	cout<<root->val<<endl;
	inorder(root->right);
	return;
}

栈 中序遍历

void inorder(TreeNode* root)
{
	if(!root) return;
	stack<TreeNode*> stk;
	stk.push(root);
	auto p=root;
	while(p || !stk.empty())
	{
		//由于中序遍历是要先访问左孩子,所以我们先找到最左的节点
		while(p && stk.top()->left) stk.push(stk.top()->left);
		TreeNode* t=stk.top();
		stk.pop();
		cout<<t->val<<endl;
		p=t->right;
		if(t->right) stk.push(t->right);
	}
	return;
}

2.3 后序遍历

同样后序遍历也只是和前两个有细微差别,访问顺序为先访问右孩子,再访问父节点,最后访问左孩子
同样有栈 和 递归两种方式

递归后序遍历

void postorder(TreeNode* root)
{
	if(!root) return;
	postorder(root->left);
	postorder(root->right);
	cout<<root->val<<endl;
	return;
}

栈 后序遍历

void postorder(TreeNode* root)
{
	if(!root) return;
	stack<TreeNode*> stk;
	TreeNode *p=root,*r=nullptr;
	while(p||!s.empty())
	{
		if(p)
		{
			s.push(p);
			p=p->left;
    	}
		else
		{
            p=s.top();
            if(p->right&&p->right!=r) p=p->right;
			else
			{
                s.pop();
                cout<<p->val<<endl;
                r=p;
                p=nullptr;
            }
		}
	}
	return;
}

2.4 层序遍历

层序遍历也是广度优先遍历,我们之前介绍的三种遍历查询方式是深度优先遍历
层序遍历一般通过队列实现

队列 层序遍历

void level(TreeNode* root)
{
	if(!root) return;
	queue<TreeNode*> que;
	que.push(root);
	while(!que.empty());
	{
		TreeNode* t=que.front();
		que.pop();
		cout<<t->val<<endl;
		if(t->left) que.push(t->left);
		if(t->right) que.push(t->right);
	}
}

3 几种常见的树介绍

3.1 完全二叉树

完全二叉树 对一颗具有n个结点的二叉树按层编号,如果编号为i(1<=i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则这棵二叉树称为完全二叉树。

特点:

  • 叶子节点只能出现最下层或者次下层;
  • 同样节点数目的二叉树,完全二叉树的深度最小;
  • 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树;

3.2 二叉搜索树

二叉搜索树 又叫二叉查找树、二叉排序树,其本质上也是一颗二叉树,只不过二叉搜索树要满足一定的规则;

二叉搜索树的规则:

  • 空树是一个二叉搜索树
  • 若左子树不空,则左子树上所有节点的值都小于根节点的值;
  • 若右子树不为空,则右子树上所有节点的值都大于根节点的值;
  • 左子树与右子树也是一颗二叉搜索树;

满足上述四条规则的树就是一颗二叉搜索树

3.3 线索二叉树

线索二叉树 提出的原因就在于想避免空间浪费,我们如果想不通过递归就能得到一个二叉树的中序序列应该怎么做?我们应该找到每一个节点的前驱后继,这样就不用通过递归来寻找;

线索化规则:

  • 若结点的左子树为空,则该结点的左孩子指针指向其前驱结点。
  • 若结点的右子树为空,则该结点的右孩子指针指向其后继结点。

有了"前驱后继"之后,带来的新的问题是如何判定一个节点的孩子节点是"前驱后继"还是真的孩子节点?

为了解决这个问题,我们必须增加两个标志位 来说明;

标志位(ltag、rtag)说明:

  • ltag为0时,root->left指向左孩子;ltag为1时,root->left指向前驱
  • rtag为0时,root->right指向右孩子;rtag为1时,root->right指向后继

3.4 平衡二叉树

平衡二叉树 又称为AVL树 ,它具有以下性质:

  • 空树是平衡二叉树
  • 左右子树的高度差不超过1
  • 左右子树也是一棵平衡二叉树

对于平衡二叉树的插入以及删除,建议参考博客好好复习,这里不再重复

LeedCode实战

关于使用递归和栈以及队列

对于一道题目,往往可以有不同的做法,但主要是算法思想。采用递归和栈又或者是队列,有的只是遍历元素的方法不同,我们要看清本质。

LC98. 验证二叉搜索树

给定一个二叉树,判断其是否是一个有效的二叉搜索树。

假设一个二叉搜索树具有如下特征:

  • 节点的左子树只包含小于当前节点的数。
  • 节点的右子树只包含大于当前节点的数。
  • 所有左子树和右子树自身必须也是二叉搜索树。

解法思路

在解决树一类的题目时,经常采用递归遍历或者是用栈来遍历的做法
对于本题来说,我们只需要验证所给出的二叉树,满足二叉搜索树的三个条件,就是题目给出的三个条件;

  1. 空树一定是二叉搜索树,如果是空树可直接return true
  2. 左子树所有节点都小于当前节点,如果不满足 则return false
  3. 右子树所有节点都大于当前节点,如果不满足 则return false
  4. 如果2、3都满足,就验证左子树与右子树是否是二叉搜索树,即 return dfs(root->left) && dfs(root->right)

代码如下

class Solution {
public:
    bool isValidBST(TreeNode* root) {
        return dfs(root,INT_MIN-1ll,INT_MAX+1ll);
    }
    bool dfs(TreeNode* root,long long vmin,long long vmax)
    {
        if(!root) return true;
        if(root->val<vmax && root->val>vmin)
            return dfs(root->left,vmin,root->val) && dfs(root->right,root->val,vmax);
        return false;
    }
};

注意 1ll表示的是long long 型的数字1

LC101. 对称二叉树

给定一个二叉树,检查它是否是镜像对称的。

解法思路

本题的解题思路要去观察,找出镜像对称的二叉树有什么关系,只要找到这个关系,那我们直接dfs就可以了;

  1. 空树是一个镜像对称二叉树
  2. 镜像对称二叉树的镜像与本身完全相同
  3. 镜像对称性质:左孩子的右孩子=右孩子的左孩子,左孩子的左孩子=右孩子的右孩子

根据上面三条我们可以直接进行构造dfs函数

代码如下

class Solution {
public:
    bool isSymmetric(TreeNode* root) {
        return dfs(root,root);//root与自身互成镜像,第二条性质
    }
    bool dfs(TreeNode* pl,TreeNode* pr)
    {
        if(!pl && !pr) return true;//空树是镜像对称二叉树,第一条性质
		//第三条性质
        if(pl && pr && pl->val==pr->val)
            return dfs(pl->left,pr->right) && dfs(pl->right,pr->left);
        return false;
    }
};

注意 这里给出的只是递归方式的代码,我们也可以采用queue队列来做这道题。

LC94. 二叉树的中序遍历

给定一个二叉树的根节点 root ,返回它的中序遍历。

解法思路一 递归

我们直接给出代码
代码如下

class Solution {
public:
    vector<int> ans;
    vector<int> inorderTraversal(TreeNode* root) {
        dfs(root);
        return ans;
    }
    void dfs(TreeNode* root)
    {
        if(!root) return;
        dfs(root->left);
        ans.push_back(root->val);
        dfs(root->right);
        return;
    }
};

解法思路二 栈

我们直接给出代码
代码如下

class Solution {
public:
    vector<int> inorderTraversal(TreeNode* root) {
        vector<int> ans;
        if(!root) return ans;
        stack<TreeNode*> stk;
        stk.push(root);
        auto p=root;
        while(p || !stk.empty())
        {
            while(p && stk.top()->left) stk.push(stk.top()->left);//先找到最左的节点
            TreeNode* t=stk.top();
            stk.pop();
            ans.push_back(t->val);//取出中序遍历的值
            p=t->right;
            if(t->right) stk.push(t->right);//如果有右孩子,push入栈
        }
        return ans;
    }
};

LC105. 从前序与中序遍历序列构造二叉树

根据一棵树的前序遍历与中序遍历构造二叉树。

注意:
你可以假设树中没有重复的元素

解法思路

  • 前序遍历的第一个值是根节点root的值
  • 中序遍历的的构成总是前面是左子树、中间是根节点、后面是右子树
  • 根据上面的分析,我们可以从前序遍历中得到根节点的值,由于没有重复的值,所以我们可以根据根节点的值在中序遍历序列中找到根节点的位置,并把中序遍历分成左子树与右子树
  • 根据左子树与右子树的节点个数,我们可以确定根节点的左孩子与右孩子
  • 到此,我们已经发现了本题具有递归的性质,所以直接采用递归解决

解题步骤

  1. 构造头节点root,root的值为前序遍历序列的第一个元素,并在中序遍历序列中找到根节点的下标index
  2. 根据index将root的左右子树区间划分出来。其中左子树,前序遍历序列为preorder[pl+1:pl+1+index-il],中序遍历序列为inorder[il:index];右子树,前序遍历序列为preorder[pr-ir+index+1:pr],中序遍历序列为inorder[index+1:ir];
  3. 构造左右子树的根节点,并赋值给root->left、root->right

按照上面步骤写出代码即可,不过这里我们提供了一个查找根节点坐标比较快速的方法,用一个hash表将inorder的值与下标对应

代码如下

class Solution {
public:
    unordered_map<int,int> pos;
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        for(int i=0;i<inorder.size();i++) pos[inorder[i]]=i;
        return dfs(preorder,inorder,0,preorder.size(),0,inorder.size());
    }
    TreeNode* dfs(vector<int>& preorder,vector<int>& inorder,int pl,int pr,int il,int ir)
    {
        if(pl>=pr) return NULL;
        auto index=pos[preorder[pl]];
        auto root=new TreeNode(inorder[index]);
        root->left=dfs(preorder,inorder,pl+1,pl+1+index-il,il,index);
        root->right=dfs(preorder,inorder,pr-ir+index+1,pr,index+1,ir);
        return root;
    }
};

LC102. 二叉树的层序遍历

典型的层序遍历,不同的是我们需要记录每一层的节点数
通过两层嵌套循环执行来实现记录每一层的节点数

代码如下

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        if(!root) return vector<vector<int>>();
        queue<TreeNode*> que;
        que.push(root);
        vector<vector<int>> res;
        while(!que.empty())
        {
            vector<int> tmp;
            int len=que.size();//记录当前层的节点数
			//for循环实现对本层节点的遍历
            for(int i=0;i<len;++i)
            {
                auto pn=que.front();
                que.pop();
                tmp.push_back(pn->val);
                if(pn->left) que.push(pn->left);
                if(pn->right) que.push(pn->right);
            }
            res.push_back(tmp);
        }
        return res;
    }
};

LC236. 二叉树的最近公共祖先

给定一个二叉树, 找到该树中两个指定节点的最近公共祖先。

百度百科中最近公共祖先的定义为:“对于有根树 T 的两个节点 p、q,最近公共祖先表示为一个节点 x,满足 x 是 p、q 的祖先且 x 的深度尽可能大(一个节点也可以是它自己的祖先)。”

解题思路

本题要体现递归的一个设计思想

  • 我们在设计有返回参数的递归函数时一定要明确返回的值是什么
  • 一旦确定了返回值,我们再用这个递归时,可默认他返回了我们想要的值
  • 之后便是确定终止条件

解题步骤:

  1. root不包含p、q,则return NULL
  2. root只含有p/q,则return p/q
  3. root包含p、q,则return root
  4. root=p或者root=q,则return root
    我们下面就按照这四条给定的返回值,进行编写程序

代码如下

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        //root不包含p、q,则return NULL
        //root只包含p/q,则return p/q
        //root包含p、q,则return root
        if(!root || root==p || root==q) return root;
        auto l=lowestCommonAncestor(root->left,p,q);
        auto r=lowestCommonAncestor(root->right,p,q);
        if(!r) return l;
        if(!l) return r;
        return root;
    }
};

LC297. 二叉树的序列化与反序列化

序列化是将一个数据结构或者对象转换为连续的比特位的操作,进而可以将转换后的数据存储在一个文件或者内存中,同时也可以通过网络传输到另一个计算机环境,采取相反方式重构得到原数据。

请设计一个算法来实现二叉树的序列化与反序列化。这里不限定你的序列 / 反序列化算法执行逻辑,你只需要保证一个二叉树可以被序列化为一个字符串并且将这个字符串反序列化为原始的树结构。

提示: 输入输出格式与 LeetCode 目前使用的方式一致,详情请参阅 LeetCode 序列化二叉树的格式。你并非必须采取这种方式,你也可以采用其他的方法解决这个问题。

解法思路

对于序列化

  • 对于序列化很简单,我们只需要进行先序遍历然后取值就行,但注意一点,由于我们还需要进行反序列化,而且我们知道仅从前序遍历序列并不能重建二叉树,所以我们在进行序列化时,对于空节点,我们也要进行记录。这样我们才能仅从前序遍历得到的序列进行重建二叉树
  • 为了方便,我们以','进行节点的分割,将null节点赋值为'#',其余的用to_string(node->val)

对于反序列化

  1. 还原二叉树我们首先要进行节点值的还原,即将数字字符串转换为整型数
  2. 构建节点,我们的序列时先序遍历序列,所以我们优先向left构建,当遇到'#'则构建空节点,同时向right构建
  3. 将序列循环一遍即可

代码如下

class Codec {
public:

    // Encodes a tree to a single string.
    string serialize(TreeNode* root) {
        string res;
        dfs1(root,res);
        return res;
    }
    void dfs1(TreeNode* root, string& res)
    {
        if(!root)
        {
            if(!res.empty()) res+=",#";
            else res+="#";
            return;
        }
        if(!res.empty()) res+=(','+to_string(root->val));
        else res+=(to_string(root->val));
        dfs1(root->left,res);
        dfs1(root->right,res);
        return;
    }

    // Decodes your encoded data to tree.
    TreeNode* deserialize(string data) {
        int u=0;
        return dfs2(data,u);
    }
    
    TreeNode* dfs2(string& data,int &u)
    {
        if(u>=data.size()) return NULL;
        if(data[u]=='#')
        {
            u+=2;
            return NULL;
        }
        bool isminer=false;
        if(data[u]=='-')
        {
            isminer=true;
            u++;
        }
        int v=0;
        while(data[u]!=',')
        {
            v=10*v+data[u]-'0';
            u++;
        }
        u++;
        if(isminer) v=-1*v;
        auto root=new TreeNode(v);
        root->left=dfs2(data,u);
        root->right=dfs2(data,u);
        return root;
    }
};

// Your Codec object will be instantiated and called as such:
// Codec ser, deser;
// TreeNode* ans = deser.deserialize(ser.serialize(root));

LC543. 二叉树的直径

给定一棵二叉树,你需要计算它的直径长度。一棵二叉树的直径长度是任意两个结点路径长度中的最大值。这条路径可能穿过也可能不穿过根结点。

解法思路

  • 我们可以计算出root的左子树最大深度、右子树的最大深度
  • 我们枚举每一个节点的左右子树的深度之和,然后更新到最大的那一个,那个便是我们的答案

代码如下

class Solution {
public:
    int ans=0;//用来更新保存最长的直径
    int diameterOfBinaryTree(TreeNode* root) {
        dfs(root);
        return ans;
    }
    int dfs(TreeNode* root)
    {
        //空节点返回0
        //返回树的最大深度
        if(!root) return 0;
        int l=dfs(root->left);//左子树的最大深度
        int r=dfs(root->right);//右子树的最大深度
        ans=max(ans,l+r);//更新最大值,l+r为当前节点的深度和
        return max(l,r)+1;
    }
};

LC124. 二叉树中的最大路径和

路径被定义为一条从树中任意节点出发,沿父节点-子节点连接,达到任意节点的序列。同一个节点在一条路径序列中至多出现一次。该路径至少包含一个节点,且不一定经过根节点。

路径和 是路径中各节点值的总和。

给你一个二叉树的根节点 root ,返回其 最大路径和

解法思路

此题与上一题LC543类型相似,同样采用枚举的方式来进行

  1. 如果root为空,则return INT_MIN
  2. 求出root的左子树最大路径和l,求出root的右子树最大路径和r
  3. 由于本题的路径值可能是负数,所以要进行判断,分四种情况进行分别判断,分别是1、l<0 && r<0;2、l>=0 && r<0;3、l<0 && r>=0;4、l>=0 && r>=0
  4. 更新ans

代码如下

class Solution {
public:
    int ans=INT_MIN;
    int maxPathSum(TreeNode* root) {
        dfs(root);
        return ans;
    }
    int dfs(TreeNode* root)
    {
        if(!root) return INT_MIN;
        int l=dfs(root->left);
        int r=dfs(root->right);
        if(l<0 && r<0)
        {
            ans=max(ans,root->val);
            return root->val;
        }
        else if(l>=0 && r<0)
        {
            ans=max(ans,l+root->val);
            return l+root->val;
        }
        else if(l<0 && r>=0)
        {
            ans=max(ans,r+root->val);
            return r+root->val;
        }
        ans=max(ans,l+r+root->val);
        return max(l,r)+root->val;
    }
};

LC173. 二叉搜索树迭代器

实现一个二叉搜索树迭代器类BSTIterator ,表示一个按中序遍历二叉搜索树(BST)的迭代器:

  • BSTIterator(TreeNode root) 初始化 BSTIterator 类的一个对象。BST 的根节点 root 会作为构造函数的一部分给出。指针应初始化为一个不存在于 BST 中的数字,且该数字小于 BST 中的任何元素。
  • boolean hasNext() 如果向指针右侧遍历存在数字,则返回 true ;否则返回 false 。
  • int next()将指针向右移动,然后返回指针处的数字。

注意,指针初始化为一个不存在于 BST 中的数字,所以对 next() 的首次调用将返回 BST 中的最小元素。

你可以假设 next() 调用总是有效的,也就是说,当调用 next() 时,BST 的中序遍历中至少存在一个下一个数字。

解法思路

参照二叉搜索树 以及 中序遍历

代码如下

class BSTIterator {
public:
    stack<TreeNode*> stk;
    BSTIterator(TreeNode* root) {
        while(root)
        {
            stk.push(root);
            root=root->left;
        }
    }
    
    int next() {
        auto p=stk.top();
        stk.pop();
        int res=p->val;
        p=p->right;
        while(p)
        {
            stk.push(p);
            p=p->left;
        }
        return res;
    }
    
    bool hasNext() {
        return !stk.empty();
    }
};

/**
 * Your BSTIterator object will be instantiated and called as such:
 * BSTIterator* obj = new BSTIterator(root);
 * int param_1 = obj->next();
 * bool param_2 = obj->hasNext();
 */
posted @ 2021-04-22 00:07  haoyuhuang  阅读(69)  评论(0)    收藏  举报