算法简介
1.什么是链表?
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。链表有很多种不同的类型:单向链表,双向链表以及循环链表。
2.链表结构的优点
相比较普通的线性结构,链表结构的优点非常突出:
- 单个结点创建(插入)非常方便,普通的线性内存通常在创建的时候就需要设定数据的大小,而链表可以在 O ( 1 ) O(1) O(1)的时间复杂度里实现;
- 结点的删除*非常方便,不需要像线性结构那样移动剩下的数据,同样可以在 O ( 1 ) O(1) O(1)的时间复杂度里实现;
- 结点的访问方便,可以通过循环或者递归的方法访问到任意数据;
3.链表结构的缺点
.相比较普通的线性结构,链表结构的缺点也很明显:
- 链表平均的访问效率较低。
- 链表结构的调试相对复杂。
单向链表
例题 1 UVA11988 破损的键盘 Broken Keyboard (a.k.a. Beiju Text)
思路
本题采用单向链表解决。每输入一个字符就把它存起来,设输入字符串是
s
[
1
~
n
]
s[1~n]
s[1~n],则可以用
n
e
x
t
[
i
]
next[i]
next[i]表示在当前显示屏中
s
[
i
]
s[i]
s[i]右边的字符编号(即在
s
s
s中的下标)。
为了方便起见,假设字符串s的最前面还有一个虚拟的 s [ 0 ] s[0] s[0],则 n e x t [ 0 ] next[0] next[0]就可以表示显示屏中最左边的字符。再用一个变量 c u r cur cur表示光标位置:即当前光标位于 s [ c u r ] s[cur] s[cur]的右边。 c u r = 0 cur=0 cur=0说明光标位于“虚拟字符” s [ 0 ] s[0] s[0]的右边,即显示屏的最左边。
为了移动光标,还需要用一个变量 l a s t last last表示显示屏的最后一个字符是 s [ l a s t ] s[last] s[last]。
代码如下:
#include <bits/stdc++.h>
#define next pre
using namespace std;
const int MAXN=100005;
int last,cur,next[MAXN];
char str[MAXN];
int main()
{
while(scanf("%s",str+1)!=EOF)
{
int n=strlen(str+1);
cur=last=0;
next[0]=0;
for(int i=1;i<=n;++i)
{
if(str[i]=='[') cur=0;
else if(str[i]==']') cur=last;
else
{
next[i]=next[cur];
next[cur]=i;
if(cur==last) last=i;//更新“最后一个字符”的编号
cur=i;//移动光标
}
}
for(int i=next[0];i;i=next[i])
printf("%c",str[i]);
printf("\n");
}
return 0;
}
双向链表
例题 2 UVA12657 移动盒子 Boxes in a Line
思路
本题采用双向链表来解决:用
l
e
f
t
[
i
]
left[i]
left[i]和
r
i
g
h
t
[
i
]
right[i]
right[i]分别表示编号为i的盒子左边和右边的盒子编号(如果是
0
0
0,表示不存在),则下面的过程可以让两个结点相互连接:
void link(int l,int r)
{
right[l]=r; left[r]=l;
}
有了这个代码,可以先记录好操作之前 x x x和 y y y两边的结点,然后用link函数按照某种顺序把它们连起来。操作 4 4 4比较特殊,为了避免一次修改所有元素的指针,此处增加一个标记 i n v inv inv,表示有没有执行过操作 4 4 4(如果 i n v = 1 inv=1 inv=1时再执行一次操作 4 4 4,则 i n v inv inv变为 0 0 0)。这样,当 o p op op为 1 1 1和 2 2 2且 i n v = 1 inv=1 inv=1时,只需把 o p op op变成 3 − o p 3-op 3−op(显然操作 3 3 3不受 i n v inv inv影响)即可。最终输出时要根据 i n v inv inv的值进行不同处理。
代码如下:
#include <bits/stdc++.h>
#define left Left
#define right Right
using namespace std;
const int MAXN=100005;
int n,m,kase=0,inv;
int left[MAXN],right[MAXN];
inline int read()
{
int X=0; bool flag=1; char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+(ch^'0'); ch=getchar();}
if(flag) return X;
return ~(X-1);
}
void link(int l,int r)
{
right[l]=r; left[r]=l;
}
void init()
{
for(int i=1;i<=n;++i)
left[i]=i-1,right[i]=(i+1)%(n+1);
right[0]=1; left[0]=n;
inv=0;
}
void work()
{
for(int i=1;i<=m;++i)
{
int op=read();
if(op==4) inv=!inv;
else
{
int x=read(),y=read();
if(op==3&&right[y]==x) swap(x,y);
if(op!=3&&inv) op=3-op;
if(op==1&&x==left[y]) continue;
if(op==2&&x==right[y]) continue;
int lx=left[x],rx=right[x],ly=left[y],ry=right[y];
if(op==1) link(lx,rx),link(ly,x),link(x,y);
else if(op==2) link(lx,rx),link(y,x),link(x,ry);
else if(op==3)
{
if(right[x]==y) link(lx,y),link(y,x),link(x,ry);
else link(lx,y),link(y,rx),link(ly,x),link(x,ry);
}
}
}
}
void printans()
{
int b=0;
long long ans=0;
for(int i=1;i<=n;++i)
{
b=right[b];
if(i%2==1) ans+=b;
}
if(inv&&n%2==0) ans=(long long)n*(n+1)/2-ans;
printf("Case %d: %lld\n",++kase,ans);
}
int main()
{
// freopen("input.txt","r",stdin);
while(scanf("%d%d",&n,&m)!=EOF)
{
init();
work();
printans();
}
return 0;
}
链表的启发式合并
思路
一种颜色的布丁变成另一种颜色就相当于合并两种颜色的布丁,并且两种布丁合并了之后显然不可能分开,所以考虑启发式合并。
我们先求出原序列的答案,对于每一种颜色都用链表结构存储起来,并记录下尾节点。每次修改,都根据启发式合并的方法来暴力合并,然后处理一下此次合并对答案的影响。
但是如果我们把 1 1 1 染成 2 2 2并且 ∣ S 1 ∣ > ∣ S 2 ∣ |S_1|>|S_2| ∣S1∣>∣S2∣,那么我们应该把 2 2 2 接到 1 1 1 的后面。这样会有一个问题:本次修改后这个链的颜色是 1 1 1(颜色为 2 2 2 的链被删除了),如果接下来修改颜色 2 2 2(显然这是合法的),会使得找不到颜色 2 2 2 而只能找到颜色 1 1 1 了。所以我们需要使用一个 f f f 数组,表示当我们要寻找颜色 x x x 时,实际上需要寻找颜色为 f [ x ] f[x] f[x] 的链。如果 f [ x ] f[x] f[x]表示的链的大小比 f [ y ] f[y] f[y]大就要交换 f [ x ] f[x] f[x] 和 f [ y ] f[y] f[y]。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int MAXN=100005;
const int MAXM=1000005;
int n,m,ans=0;
int c[MAXN],f[MAXM],size[MAXM];
int start[MAXM],next[MAXM],head[MAXM];
inline int read()
{
int X=0; bool flag=1; char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+(ch^'0'); ch=getchar();}
if(flag) return X;
return ~(X-1);
}
void merge(int x,int y)
{
for(int i=head[x]; i;i=next[i]) ans-=(c[i-1]==y)+(c[i+1]==y);
for(int i=head[x]; i;i=next[i]) c[i]=y;
next[start[x]]=head[y]; size[y]+=size[x]; head[y]=head[x];
start[x]=head[x]=size[x]=0;
}
void readdata()
{
n=read(); m=read();
for(int i=1;i<=n;++i)
{
c[i]=read(); f[c[i]]=c[i];
ans+=(c[i]!=c[i-1]);
if(!head[c[i]]) start[c[i]]=i;
++size[c[i]]; next[i]=head[c[i]]; head[c[i]]=i;
}
//size指的是c[i]所属的链的大小(start,next,head与之相似);
//本应用f[c[i]],但此时c[i]=f[c[i]],故直接用的c[i]
}
void work()
{
for(int i=1;i<=m;++i)
{
int op=read();
if(op==2) printf("%d\n",ans);
else
{
int x=read(),y=read();
if(x==y) continue;
if(size[f[x]]>size[f[y]]) swap(f[x],f[y]);
if(size[f[x]]==0) continue;
merge(f[x],f[y]);
}
}
}
int main()
{
// freopen("input.txt","r",stdin);
readdata();
work();
return 0;
}
参考文献
刘汝佳.《算法竞赛入门经典(第2版)》 清华大学出版社