图
图常用的存储方式有两种,一种是邻接矩阵,另一种是邻接表(前向星)
邻接矩阵
这种方法也是我最早会的一种方法,空间复杂度为 (O(n^2)),其中 (n) 为节点的个数。该方法使用简单,并且常数较小,一般用来存储稠密图。
邻接矩阵为一个 (n imes n) 的矩阵,其中 (a_{i,j}) 表示从 (i) 节点到 (j) 节点边的权值。如果这条边不存在,(a_{i,j}) 就为 (infty)。当 (i=j) 时,(a_{i,j}=0)。如果是无向图,那么此邻接矩阵就是对称的。
如果存储的是不带权的图,就可以用 (1) 表示有边直接联通,(0) 表示不直接
举个例子,看下面这张图。
那么它所对应的邻接矩阵 (a) 就是:
用代码就是:
#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) (ldotsldots) 直到该节点所有的边都被访问过,即 (a[head[i]].ne=0) 时。
此方法较节省空间,空间复杂度为 (O(m)),(m) 为边的数量。但是此方法的常数比邻接矩阵大。
二叉树
我们知道,二叉树是一种特殊的树,正是因为它的特殊性质,让它有许多神奇的存储方法。在这里我讲三种方法:线性存储、二叉链表存储、三叉链表存储。
线性存储
线性存储非常巧妙的利用的二叉树的性质,主要用于存储完全二叉树和满二叉树。
具体是怎么存储的呢?我们来结合图来了解。
上面的这颗二叉树,我们在经过观察,不难得出,按照这种编号方式,(i) 号节点的左儿子的编号为 (2 imes i),右儿子的编号是 (2 imes 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;
}
小结
树和图的存储就开始涉及高级数据结构了。面对不同种类的树和图,不同的问题,我们采取不同存储方法,灵活运用,才能真正掌握。