一.链表(数组模拟)
1.引入
链表相比于数组:
优势:
插入删除操作非常快,并且因为动态分配内存的缘故,即便事先不知道数据范围的大小也没关系,对内存的使用效率较高;
劣势:
查询速度慢,且可能内存泄漏。而且,在ACM比赛中用链表写的程序不容易调试
所以,可以使用数组来模拟链表,因为ACM的题目都会事先给出数据的范围。
2.模板
用数组模拟链表,就是给每一个结点编一个号,用这个号码来代替指针指向结点
结点的建立:
const int maxn=10000000;
typedef struct{
int data;
int pre,next;
}Linklist;
Linklist node[maxn];
初始化:
设立头尾结点,以减少程序编写时对链表边界的判断
int tot,head,rear;
void init(){
head=1,rear=2;
tot=2;
node[head].next=rear;
node[rear].pre=head;
}
插入:
基本思想和链表一样
void L_insert(int p,int value){
int q=tot++;
node[node[p].next].pre=q;
node[q].next=node[p].next;
node[q].data=value;
node[q].pre=p;
node[p].next=q;
}
删除:
可以发现删除结点后,被删除结点的编号并没有类似“释放内存”的操作,这块空间永远还是存在,只是从链表中被“删除”了
void L_remove(int p){
node[node[p].pre].next=node[p].next;
node[node[p].next].pre=node[p].pre;
}
二.链式前向星
1.引入
邻接表是图的一种存储方式。它的思想是用N条链表储存一个有N个顶点的有向图,其中第i条链表储存以第i个顶点为始点的所有边,注意邻接表储存的是边的信息所以适用于点稀疏图。
若要储存无向图,则将一条边看成两条有向边储存即可。
但是用链表来写很麻烦,所以我们同样可以用数组来进行模拟,实现静态的邻接表,也就是所谓的链式前向星
邻接矩阵是用一个N*N的矩阵来存放任意两点间的权值(没有通路则为inf),空间复杂度为 O ( N 2 ) O(N^2) O(N2);邻接表的空间复杂度为 O ( M + N ) O(M+N) O(M+N),其中M为边的条数,N为点的个数。
即便是N个点的完全图,其边数也只有N(N-1)/2,所以邻接表在简单图中显然是更好的选择。
2.模板
初始化
首先,我们用cnt来分配储存空间。规定0相当于NULL指针。
head数组,即表头数组,储存N个链表的表头,表头不需要存放数值,只要记录链表首元结点的指针即可。其值储存的其实相当于链表中表头的next指针。head数组因为是全局变量,所以自动初始化为0。代表每个表头的next初始化为NULL;
结点是用来储存边的信息的,我们需要两个值to和next。to代表的是边的终点,因为边的始点是由表头数组确定的,所以不需要再记录。next则指向链表中的下一个结点,如果为0则代表下一个结点为NULL。
vis数组标记是否访问,是在遍历时使用的(为了保证每个点只被访问一次)
value存放结点的权值
const int maxn=2e5+10;
typedef struct{
int to;
int next;
}edge;
edge e[maxn];
int cnt=1;
int head[maxn];
int vis[maxn];
long long value[maxn];
加边
加边操作用add函数来实现
原理与链表建立的头插法相同,具体细节见注释
void add(int u,int v){
e[cnt].to=v;//cnt指向该边
e[cnt].next=head[u];//该边的next是表头的next,相当于头插法
head[u]=cnt++;//表头的next等于该边
}
dfs遍历
访问u为始点,与之有边相连的所有结点
即访问第u条链表,对于这个for循环for(int i=head[u];i;i=e[i].next
的个人理解:
head[u]即首元结点的指针,为循环的起点
中间的i其实是i!=0
的意思,因为链表的结尾是NULL指针,即0
i=e[i].next即访问下一个结点的指针
i其实是指针,e[i]存放的才是内容
for循环的内部有一句dfs(v),即递归调用该函数
void dfs(int u){
vis[u]=1;
for(int i=head[u];i;i=e[i].next){ //i=0时链表遍历完毕
int v=e[i].to;
if(vis[v]) continue;
//访问结点v
dfs(v);
}
}
bfs遍历
bfs遍历借助队列,是按层遍历,详情可见代码
void bfs(int u){
queue<int>q;
q.push(u);
vis[u]=1;
while(!q.empty()){
int temp=q.front();
q.pop();
for(int i=head[temp];i;i=e[i].next){
int v=e[i].to;
if(vis[v]) continue;
vis[v]=1;
//访问结点v
q.push(v);
}
}
}
三.vector 实现邻接表
vector实现邻接表写法应该是最简单的了
其实之所以用数组模拟而不是二维数组,就是因为在边稀疏图中将对空间储存造成较大的浪费,但是用vector来模拟不仅写法非常的简单,而且vector是不定长的数组,可以直接在尾端加入元素而不用事先声明数组的长度。(我不太懂vector的内存分配机制,但是香就完事了!)
stl永远滴神
const int maxn=1e5+10;
int N;
vector <int> ver[maxn];
long long value[maxn];
void add(int u,int v){
ver[u].push_back(v);
}
void dfs(int u){//起点u
vis[u]=1;
for(int i=0;i<ver[u].size();i++){
int v=ver[u][i];
if(vis[v]) continue;
vis[v]=1;
//访问结点v
dfs(v);
}
}