三十分钟学会数据结构--树

数据结构——树

一边看数据结构,一边刷Leetcode,顺便写篇博客串讲一下。

树的常用类型

一般而言,常用的树都是二叉树。也就是一个结点最多有两个子节点。
而对于不同的应用场景,有多种定义的树:

  • 搜索二叉树:左子节点数值小于节点数值小于右子节点数值;
  • 满二叉树:一个二叉树上面,所有的分支节点都存在左子树和右子树,且所有的叶子都在同一层上。

二叉树的性质

  • 在二叉树的第 i i i层,最多有 2 i − 1 2^{i-1} 2i1个结点。注意根结点是第1层。
  • 深度为k的二叉树最多有 2 k − 1 2^{k}-1 2k1个结点。假设是满二叉树,最多不过是: 1 + 2 + 4 + 8 + ⋯ = 1 ( 1 − 2 k ) 1 − 2 1+2+4+8 +\dots =\frac{1(1-2^k)}{1-2} 1+2+4+8+=121(12k)
  • 对任何一个二叉树,如果其终端结点数(叶子结点数)为 n 0 n_0 n0,度为2的结点数为 n 2 n_2 n2,则 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1假设度为1的结点数为 n 1 n_1 n1则总结点数为 n = n 0 + n 1 + n 2 n=n_0+n_1+n_2 n=n0+n1+n2而每一个结点对应着一根输入连接线(除了根结点没有),则连接线总数 s u m = n − 1 = n 0 + n 0 + n 2 − 1 sum=n-1=n_0+n_0+n_2-1 sum=n1=n0+n0+n21同时连接线也可以通过输出来计算 s u m = 2 ∗ n 2 + 1 ∗ n 1 sum=2*n_2+1*n_1 sum=2n2+1n1两者联立可得 n 0 + n 0 + n 2 − 1 = 2 ∗ n 2 + 1 ∗ n 1 n_0+n_0+n_2-1=2*n_2+1*n_1 n0+n0+n21=2n2+1n1 n 0 = n 2 + 1 n_0=n_2+1 n0=n2+1

二叉树遍历方法

  1. 前序遍历
    前序遍历
    主要特点是,根结点 | 左子树 | 右子树:{A} {B(DGH)} {C(EI)F},第一位是根结点。
  2. 中序遍历
    中序遍历
    主要特点是,左子树 | 根结点 | 右子树: (GDH)B A (EI)C(F)
  3. 后序遍历
    后序遍历
    主要特点是,左子树 | 右子树 | 根结点,GHDB IEFC A,最后一位是根结点。
  4. 层序遍历
    层序遍历
    最为直观的遍历方式,从上到下,从左到右,依次遍历一遍。

遍历算法的实现:

/**
 * Definition for a binary tree node.
 * struct TreeNode {
 *     int val;
 *     TreeNode *left;
 *     TreeNode *right;
 *     TreeNode(int x) : val(x), left(NULL), right(NULL) {}
 * };
 */
int Preorder(TreeNode * root, vector<int> & key)
{
	if(root == NULL)
		return 0;
	key.push_back(root->val);	
	Preorder(root->left,key);
	Preorder(root->right,key);
	return 1;
}
/*************实现功能
key将记录树的前序排列
*********************/
int Inorder(TreeNode * root, vector<int> & key)
{
	if(root == NULL)
		return 0;	
	Preorder(root->left,key);
	key.push_back(root->val);
	Preorder(root->right,key);
	return 1;
}
/*************实现功能
key将记录树的中序排列
*********************/
int Postorder(TreeNode * root, vector<int> & key)
{
	if(root == NULL)
		return 0;	
	Preorder(root->left,key);
	Preorder(root->right,key);
	key.push_back(root->val);
	return 1;
}
/*************实现功能
key将记录树的后序排列
*********************/

层序遍历稍微有点复杂,不过结合队列来处理,也是比较简单的。

int Sequence(TreeNode * root, vector<int> & key)
{
	if(root == NULL)
		return 0;
	queue<TreeNode* > q;
	q.push(root);
	while(q.empty() != 1)
	{
		TreeNode temp = q.front();
		q.pop();
		key.push_back(temp->val);
		if(temp->left != NULL)
			q.push(temp->left);
		if(temp->right != NULL)
			q.push(temp->right);
	}	
}

如果还有不清楚的地方,可以浏览Leetcode上的题目:从上到下打印二叉树

二叉树的构建方法

给出中序遍历和前序/后续遍历,重建出二叉树,同样是结合题目来看:重建二叉树。需要注意的是,仅知道前序遍历和后序遍历无法唯一的确定二叉树

class Solution {
public:
    TreeNode* buildTree(vector<int>& preorder, vector<int>& inorder) {
        this->preorder = preorder;
        for(int i = 0; i < inorder.size(); i++)
            dic[inorder[i]] = i;								//构建一个hash表,用于快速查找中序遍历中root的索引
        return recur(0, 0, inorder.size() - 1);
    }
private:
    vector<int> preorder;
    unordered_map<int, int> dic;
    TreeNode* recur(int root, int left, int right) { 
    //root是在前序遍历的索引,left,right是在中序遍历的索引(左边界和右边界)
        if(left > right) return nullptr;                        // 递归终止
        TreeNode* node = new TreeNode(preorder[root]);          // 建立根节点
        int i = dic[preorder[root]];                            // 划分根节点、左子树、右子树
        node->left = recur(root + 1, left, i - 1);              // 开启左子树递归
        node->right = recur(root + i - left + 1, i + 1, right); // 开启右子树递归
        return node;                                            // 回溯返回根节点
    }
};

这段代码容易造成混淆,不过重点是讲清楚这么做的思路
前序遍历Preorder特点:

rootleftright
0[1, index_root][index_root+1, Preorder.size()-1]

中序遍历Inorder特点:

leftrootright
[0, index_root-1]index_root[index_root+1, Preorder.size()-1]

所以说,如果已知中序遍历和前序遍历,那么具体的思路是:

  1. 前序遍历第一个元素就是根结点
  2. 找到根结点在中序遍历的索引,分割左子树和右子树,中序遍历中,根结点左侧是左子树,右侧是右子树
  3. 找到左子树和右子树后,再回到前序遍历,分割开左子树与右子树;
  4. 前序遍历中,左子树第一个元素就是左子结点,右子树的第一个元素就是右子结点
  5. 整理出子问题的PreorderInorder,进入到下一层。

思路清楚后,主要的问题就是该如何编程和提高效率:

  • 采用hash表,提高查找效率
  • 使用递归方法,从上到下解决问题,递进的构建子树
  • 将子问题的前序遍历和中序遍历用两端的索引进行存储,提高了空间利用率,避免每一个递归都需要重新构建遍历序列。

二叉树路径的记录

有时,需要记录找到一个结点的路径,实际上是一种搜索的思想。
考虑这么一个问题:给定一个二叉树,要求打出所有符合求和要求的路径,
参考题目:二叉树中和为某一数值的路径

class Solution {
public:
    vector<vector<int>> pathSum(TreeNode* root, int target) {
        vector<vector<int>>  key;
        vector<int> path;
        subFunc(root,target,path,key);
        return key;
    }
    int  subFunc(TreeNode * root,int target,vector<int> & path,vector<vector<int>> & key)
    {
        if(root == nullptr)
            return 0;
        if(root->val==target && root->left == nullptr && root->right ==nullptr)
        {
            path.push_back(root->val);    
            key.push_back(path);       
            path.pop_back();
        }
        if(root->left != nullptr)
        {
            path.push_back(root->val);
            subFunc(root->left,target-root->val,path,key);
            path.pop_back();
        }
        if(root->right != nullptr)
        {
            path.push_back(root->val);
            subFunc(root->right,target-root->val,path,key);
            path.pop_back();
        }
        return 1;
    }   
};

那么朴素的想法就是采用回溯法。因为每一个结点都有最多两种可能性(左结点,右结点),那么就要不断的尝试,如果做错了回退到上一步换另一个方向,这样就能遍历所有的路径。
回溯法的关键在于能够完好如初的回到上一状态,也就是代码中的path在回溯之后,需要弹出上一次的输入项。而且这个题目中,无论是否找到符合要求的路径,都要进行回溯。因为我们的目的是遍历所有的路径,找到所有符合要求的路径,不是只找到其中一条。
脱离这道题,回溯也是一种对二叉树的遍历方法,或者从某种层次讲,它就是深度优先搜索

如何找到相同的父结点?

这一类问题可以归结于如何找到父节点? 根据节点的性质,我们可以很容易找到其的子节点(用指针不断的向下寻找即可),但对于其父节点却不是很方便。因为树的结点并没有指向上层结点的指针。参考的题目有两道:

  1. 二叉搜索树的最深公共祖先
  2. 二叉树的最深公共祖先

其实这两者的解决思路都是一样的,只不过二叉搜索树具有一定的大小性质,所以可以更快一些。而二叉树需要较多的运算。
最深的公共祖先应该具有如下的性质,其左子树内有一个节点,右子树有另一个节点,或者自身是一个节点。
对于二叉树,相应的解法如下:

class Solution {
public:
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root==NULL)
            return root;
        TreeNode *temp=root;
        while(1)
        {
            if(inNode(temp->left,p) && inNode(temp->left,q))		//都是其左子树的节点,则进入左子树
            {
                temp=temp->left;                
            }
            else if(inNode(temp->right,p) && inNode(temp->right,q))	//都是其右子树的节点,则进入右子树
            {	
                temp=temp->right;
            }
            else
            {
                return temp;
            }
        }
        return temp;
    }
    bool inNode(TreeNode * root, TreeNode *p)	//判断p是不是属于root的子节点,或者是root自身
    {
        if(root == NULL)
            return false;
        if(root == p)
            return true;
        bool left_flag = inNode(root->left,p);
        bool right_flag = inNode(root->right,p);
        if(left_flag || right_flag)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
};

而对于二叉搜索树,

bool inNode(TreeNode * root, TreeNode *p)

可以基本省略,用二叉搜索树的性质直接进行判断。此处不再详说。
在此提供一个较快的思路,找到pq的路径,直接对路径进行比对,就可以以很低的运算复杂度来解决该问题:

/**
 * 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:        
    vector<TreeNode *> p_path;
    vector<TreeNode *> q_path;
    vector<TreeNode *> path;
    TreeNode* lowestCommonAncestor(TreeNode* root, TreeNode* p, TreeNode* q) {
        if(root==NULL)
            return root;
        TreeNode *temp=root;
        bool left_flag;
        bool right_flag;
        inNode(root,p);
        p_path=path;
        path.clear();
        inNode(root,q);
        q_path=path;
        int p_num=p_path.size();
        int q_num=q_path.size();
        for(int i=1;1;i++)					//对节点路径进行比对
        {
            if(p_path[p_num-i]!=q_path[q_num-i])
            {

                temp = p_path[p_num-i+1];
                return temp;
            }
            if(p_num-i==0)
            {
                temp = p_path[0];
                break;
            }
            else if(q_num-i==0)
            {
                temp =q_path[0];
                break;
            }
        }
        return temp;
    }
    bool inNode(TreeNode * root, TreeNode *p)		//获取节点的路径
    {
        if(root == NULL)
            return false;
        if(root == p)
        {
            path.push_back(root);
            return true;
        }
        bool left_flag = inNode(root->left,p);
        bool right_flag = inNode(root->right,p);
        if(left_flag || right_flag)
        {
            path.push_back(root);
        }
        else
        {
            return false;
        }
        return true;
    }
};

不过需要注意不同情况的处理,
比如pq分居两侧,情况容易得到考虑;而pq是另一个节点的祖先节点,就需要小心谨慎的判断。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值