树是一种非常重要,并且对于我来说较难的数据结构,此博客为我参考《大话数据结构》与慕课网浙大数据结构网课所做的学习笔记,如有错误,欢迎指正。
2020.2.19草稿改动,添加树的基本概念。
2020.3.11改动,添加洛谷例题(超简洁已知中后序求前序遍历)。
树基本概念
树的定义:树是有多个结点的有限集合。结点数为0时为空树。在任何一棵非空树中;(1)有且只有一个特定的称为根的结点;(2)当节点数大于1时,其余结点可分为多个互不相交的子树。
这是比较专业的说法,在我看来哈,就是说你在一棵树中,只有一个根节点。但是吧,你可以把树拆成多个不相交的子树,而每个子树又有子树的根节点。而后面的遍历二叉树的关键,便是需要理解树的定义,递归建树。
树的存储结构:双亲表示法,孩子表示法,孩子兄弟表示法。
在之后的图中,有大量运用到三种表示法的地方。
双亲表示法:
data | parent |
---|
一个数据域,一个指向双亲结点的指针域。
孩子表示法:
data | child1 | chid2 | child3 | … | childn |
---|
第一种呢,是这样的,因为我们不知道具体有多少个子结点,所以这样保存数据域与指针域。但问题很明显,这样子会浪费大量空间。
2.
data | degree | child1 | child2 | … | childn |
---|
第二种,我们添加了一个degree域用来存储双亲结点的子结点个数,节省了空间。但是因为各个结点的结构不同,会带来时间上的损耗。
3. 以上两种都拥有很明显的缺陷,而最经常用的便是第三种。
具体结构:将每个结点的孩子结点排列起来,用链表串接,则n个结点有n个孩子链表,若为叶结点则孩子链表为空,最后n个结点用数组存储起来。
孩子链表的结构
child | next |
---|
表头数组的表头结点
data | firstchild |
---|
这种结构最为常用,在之后的图中,便有一种表现图的方式与其有关。
孩子兄弟表示法
这种表示法最大的好处便是将一棵树转化为了二叉树,下面是他的结构。
data | firstchild | rightsib |
---|
firstchild域指的是该结点的第一个孩子结点的地址,rightsib 域指的是该结点最右边孩子结点的地址。
二叉树
二叉树是树这一种数据结构中的重头戏,二叉树的定义简而言之便是:
一棵树它的所有节点至多只有两个子节点。
而二叉树可以使用顺序存储,但是我们一般用链式存储较多,接下来便是我自己写的建立一个二叉树的代码。
void creat(tree *&root)
{
char t;
scanf("%c",&t);
if(t=='#')
{
root=NULL;
}
else
{
root=(tree *)malloc(sizeof(tree));
root->data=t;
creat(root->l);
creat(root->r);
}
}
在我们建立了二叉树之后,我们便需要对他进行使用了,而使用二叉树的基本前提便是二叉树的遍历。二叉树的遍历方法有四种:前序遍历,中序遍历,后序遍历,层次遍历。
关于前中后序,我认为主要需要理解树是一个递归建立的数据结构,这样便比较好理解树的前中后序的遍历。
这里贴一个我写的层次遍历的函数,之后题目会有已知中后序求前序的代码。
**层次遍历:**层次遍历的思想为不断的将每一层的根节点存入队列中,然后输出队头再出队,最后将根节点的左右节点若不为空就入队。
如此即可。
void levels_showtree(tree *root)
{
if(root==NULL)
return;
queue <tree *> q;
q.push(root);
while(!q.empty())
{
tree *t=q.front();
cout<<t->data;
q.pop();
if(t->l!=NULL)
{
q.push(t->l);
}
if(t->r!=NULL)
{
q.push(t->r);
}
}
cout<<endl;
}
而前序、中序、后序遍历的算法十分类似。核心算法就三个语句。
void 递归(BiTree T)
{
if(T==NULL)
return;
//下面是操作
操作;
递归(T->lchild);
递归(T->rchild);
}
这是一个前序遍历的伪代码,前序遍历的顺序为:根左右;所以我们先操作,在递归左,在递归右。中序遍历的顺序为:左根右;这样的话,我们需要先递归左节点,在操作,再递归右节点。而后序遍历顺序为:左右根;也是同样的操作。
树要理解起来不难,但是理解的关键点在于需要记住它的定义是一个递归的定义,树中递归是基础。
已知中后序求前序
本来我自己写的时候,是一个十分复杂的代码,最近写洛谷,发现了一种格外简洁的代码,贴在这里分享。
题目:
#include <cstdio>
#include <iostream>
using namespace std;
void print(string t1,string t2)
{
if(t1.size()>0)
{
char ch=t2[t2.size()-1];
cout<<ch;
int k=t1.find(ch);//把根节点在中序中的位置找出
print(t1.substr(0,k),t2.substr(0,k));
print(t1.substr(k+1),t2.substr(k,t2.size()-k-1));//递归求解
}
}
int main()
{
std::ios::sync_with_stdio(false);//加快cin速度
string t1,t2;
cin>>t1;
cin>>t2;
print(t1,t2);
}
这里主要运用到了一个substr函数,这个函数第一个参数是string的数据,第二个参数是传递一个第一个参数的子字符串,如此我们可以大大改进代码长度,便于理解。