【学习笔记 3】图和二叉树的存储

时间:2020-05-06
本文章向大家介绍【学习笔记 3】图和二叉树的存储,主要包括【学习笔记 3】图和二叉树的存储使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

图常用的存储方式有两种,一种是邻接矩阵,另一种是邻接表(前向星)

邻接矩阵

这种方法也是我最早会的一种方法,空间复杂度为 \(O(n^2)\),其中 \(n\) 为节点的个数。该方法使用简单,并且常数较小,一般用来存储稠密图。

邻接矩阵为一个 \(n\times n\) 的矩阵,其中 \(a_{i,j}\) 表示从 \(i\) 节点到 \(j\) 节点边的权值。如果这条边不存在,\(a_{i,j}\) 就为 \(\infty\)。当 \(i=j\) 时,\(a_{i,j}=0\)。如果是无向图,那么此邻接矩阵就是对称的。

如果存储的是不带权的图,就可以用 \(1\) 表示有边直接联通,\(0\) 表示不直接

举个例子,看下面这张图。

那么它所对应的邻接矩阵 \(a\) 就是:

\[a= \begin{bmatrix} 0 & 1 & 0 & 0 & 1 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 \\ 1 & 0 & 0 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 & 0 & 1 \\ 0 & 0 & 1 & 0 & 0 & 1 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ \end{bmatrix} \]

用代码就是:

#include<bits/stdc++.h>
using namespace std;
int n,m,u,v,w;
bool a[105][105];
int main()
{
    cin>>n>>m;  //有 n 个节点,m 条边。 
    for(register int i=0;i<m;++i)
    {
    	cin>>u>>v>>w;  //有一条从 u 到 v,权值为 w 的边。  
    	a[u][v]=w;
    	//a[v][u]=w;     如果是无向图还要反向存一次。 
    }
    return 0;
}                             

邻接表(链式前向星)

邻接矩阵是相当于存节点,而邻接表就是相当于存边。如果一个图是稀疏图,那么邻接矩阵所含的信息就很少,就会浪费空间。这时,邻接表就是一个更好的选择。

一般来说,邻接表是由一个结构体链表和一个 \(head\) 数组来实现。链表中的每个元素表示 \(1\) 条边,存储该边的权值、到达的节点、和出发的节点引出的上一条边。\(head [ i ]\) 表示从 \(i\) 节点引出的最后一条边。

代码(vector 实现链表):

#include<bits/stdc++.h>
using namespace std;
struct node
{
	int ne,to,val;
}now;
vector<node> a;
int n,m,u,v,w,cnt;
int head[1005];
inline void add(int u,int v,int w)  //建造一条边。
{
	now.val=w; //权值。
	now.to=v;  //到达的节点。
	now.ne=head[u];  //出发的节点引出的最后一条边。
	head[u]=++cnt;  //更新 head。
	a.push_back(now);
}
int main()
{
    cin>>n>>m;  //有 n 个节点,m 条边。 
    a.push_back(now); //为了方便,a[0]不要,从a[1]开始存。
    for(register int i=0;i<m;++i)
    {
    	cin>>u>>v>>w;  //有一条从 u 到 v,权值为 w 的边。 
    	add(u,v,w);
    	//add(v,u,w);     如果是无向图还要反向存一次。 
    }
    return 0;
}

至于为什么要有一个 \(head\) 数组,它是用来遍历图时要用的。从节点 \(i\) 遍历图时,从 \(a[head[i]]\) 开始遍历,每次遍历完后,再遍历 \(a[head[i]].ne\) \(\ldots\ldots\) 直到该节点所有的边都被访问过,即 \(a[head[i]].ne=0\) 时。

此方法较节省空间,空间复杂度为 \(O(m)\)\(m\) 为边的数量。但是此方法的常数比邻接矩阵大。

二叉树

我们知道,二叉树是一种特殊的树,正是因为它的特殊性质,让它有许多神奇的存储方法。在这里我讲三种方法:线性存储、二叉链表存储、三叉链表存储。

线性存储

线性存储非常巧妙的利用的二叉树的性质,主要用于存储完全二叉树和满二叉树。

具体是怎么存储的呢?我们来结合图来了解。

上面的这颗二叉树,我们在经过观察,不难得出,按照这种编号方式,\(i\) 号节点的左儿子的编号为 \(2\times i\),右儿子的编号是 \(2\times i+1\)

这样一来,我们就可以将二叉树巧妙地存储在数组里了。上面这颗二叉树的线性存储就是:

通过这种方式,可以快速的找到节点和对应的左右儿子,十分方便快捷。

代码:

#include<bits/stdc++.h>
using namespace std;
int n,mp[256];  //mp 用来记录节点对应的下标。 
char a,b,c,tree[10000];
int main()
{
    cin>>n;  //有 n 个节点。
    cin>>a>>b>>c;  //根节点要单独处理一下。 
    tree[1]=a;
    tree[1*2]=b;
    tree[1*2+1]=c;
    mp[b]=1*2;
    mp[c]=1*2+1;
    for(register int i=1;i<n;++i)
    {
    	cin>>a>>b>>c;
    	tree[mp[a]*2]=b;  //找到下标,分别存储。 
    	tree[mp[a]*2+1]=c;
    	mp[b]=1*2;  //记录下标。 
    	mp[c]=1*2+1;
    }
    return 0;
}

但其的缺点就是,在存储不完全二叉树时就会显得浪费空间。

二叉链表

二叉链表又叫儿子表示法,顾名思义,链表中的元素分别记录节点本身、左儿子和右儿子。此方法节省空间,也是竞赛中最常用的方法。

它的优点很多,我就不一一列举,但它唯一也是最大的缺点是:无法从儿子节点直接找到父亲节点。

代码实现(vector 实现链表):

#include<bits/stdc++.h>
using namespace std;
struct node
{
	char data,cl,cr;
}now;
vector<node> tree;
int n,mp[256];  //mp 用来记录节点对应的下标。 
char a,b,c;
int main()
{
    cin>>n;
    for(register int i=0;i<n;++i)
    {
    	cin>>now.data>>now.cl>>now.cr;  //输入。 
    	mp[now.data]=i;  //记录下标。 
    	tree.push_back(now);  //存进链表。 
    }
    return 0;
}

三叉链表

三叉链表又叫父亲儿子表示法,它既存储节点的儿子,也存储节点的父亲。经常用于解决较复杂的问题。它可以通过一个节点找到父亲,也可以找到儿子。如果该节点是根节点,它的父亲就指向 NULL(vector 链表中用 0 代替)。该方法使用灵活,但是缺点就是,较浪费空间,操作麻烦。

代码实现(vector 实现链表):

#include<bits/stdc++.h>
using namespace std;
struct node
{
	char data,cl,cr,fa;
}now;
vector<node> tree;
int n,mp[256];  //mp 用来记录节点对应的下标。 
char a,b,c,f[256];
int main()
{
    cin>>n;
    for(register int i=0;i<n;++i)
    {
    	cin>>now.data>>now.cl>>now.cr;  //输入。 
    	now.fa=f[now.data];
    	mp[now.data]=i;  //记录下标。
		f[now.cr]=f[now.cl]=now.data; 
    	tree.push_back(now);  //存进链表。 
    }
    return 0;
}

小结

树和图的存储就开始涉及高级数据结构了。面对不同种类的树和图,不同的问题,我们采取不同存储方法,灵活运用,才能真正掌握。

原文地址:https://www.cnblogs.com/win10crz/p/12835730.html