萌新: 数组真不错啊,使用连续的存储空间,访问和使用都很简单。
大佬: 是的,不过数组不够灵活,它有以下缺点:
- 需要占用连续空间。若某个数组很大,可能没有这么大的连续空间给它用。
- 不方便删除和增加。若删除了数组中间一段数据,就需要把后面的往前挪,产生大量的拷贝开销。若需要增加空间,也不方便操作。
萌新: 那遇到了这类问题该怎么办呀?
大佬: 那就要引入我们这节课的主角——链表了。链表不需要把数据存储在连续的空间上,而且删除和增加空间都很方便。链表可以看成是用指针串起来的数组,它是用一组位于任意位置的存储单元存线性表的数据元素,这组存储单元可以是连续的,也可以不连续。 链表又可分为单向链表和双向链表:
-
单向链表如下图。单向链表一般首尾相接,最后的nextnext指向第一个data。
双向链表见下图。双向链表一般首尾相接,最后的next指针指向第一个data,第一个pre指针指向最后的data。
双向链表比单向链表的访问稍微方便一点点,也快一点点。在需要频繁访问前后几个结点的场合,可以使用双向链表。
链表这种数据结构用起来比较简单,它的操作有初始化、添加、遍历、插入、删除、查找、排序、释放等。
萌新: 哇,链表这么NB
,那还要数组作甚?
大佬: 链表也是有缺点的,它的查找元素比较慢,复杂度为O(n)。比如需要查找data等于某个值的结点时,可能要遍历整个链表,才能找到它。查找慢是线性表的通病。
萌新: 原来是这样,那么链表该如何实现呢?
大佬: 链表的实现有动态链表、静态链表、STL链表等多种方法。在算法竞赛中为加快编码速度,一般用静态链表或STL list。一会我会重点讲讲静态链表和 STL list 的实现。
- 静态链表
静态链表使用预先分配的一段连续空间来存储链表。下面给出用结构体数组实现的单向静态链表。
const int N = 10000; //按需要定义静态链表的空间大小
struct node{ //单向链表
int id; //这个结点的id
int data; //数据
int nextid; //指向下一个结点的id
}nodes[N]; //静态分配需要定义在全局
//为链表的next指针赋初值,例如:
nodes[0].nextid = 1;
for(int i = 1; i <= n; i++){
nodes[i].id = i; //把第i个结点的id就赋值为i
nodes[i].nextid = i + 1; //next指针指向下一个结点
}
//定义为循环链表:尾指向头
nodes[n].nextid = 1;
//遍历链表,沿着nextid访问结点即可
//删除结点。设当前位于位置now,删除这个结点
nodes[prev].nextid = nodes[now].nextid; //跳过结点now,即删除now
now = nodes[prev].nextid; //更新now
- 双向静态链表
下面是用结构体数组实现的双向静态链表
const int N = 10000;
struct node{ //双向链表
int id; //结点编号
int data; //数据
int preid; //前一个结点
int nextid; //后一个结点
}nodes[N];
//为结点的指针赋初值,例如
nodes[0].nextid = 1;
nodes[1].preid = 0;
for(int i = 1; i <= n; i++){ //建立链表
nodes[i].id = i;
nodes[i].preid = i-1; //前结点
nodes[i].nextid = i+1; //后结点
}
//定义为循环链表
nodes[n].nextid = 1; //循环链表:尾指向头
nodes[1].preid = n; //循环链表:头指向尾
//遍历链表,沿着preid和nextid访问结点即可
//删除结点。设当前位于位置now,删除这个结点
prev = nodes[now].preid;
next = nodes[now].nextid;
nodes[prev].nextid = nodes[now].nextid; //删除now
nodes[next].preid = nodes[now].preid;
now = next; //更新now
//插入结点,见后面的习题“自行车停放”
- STL-list
上述链表的手写代码已经比较简单了,如果还嫌麻烦,可以使用C++的STL list。 list 是双向链表,它的内存空间可以是不连续的,通过指针访问结点数据,它能高效率地删除和插入。
//定义一个list
list<int>node;
//为链表赋值,例如定义一个包括n个结点的链表
for(int i=1;i<=n;i++)
node.push_back(i);
//遍历链表,用it遍历链表,例如从头遍历到尾:
list<int>::iterator it = node.begin();
while(node.size()>1){ //list的大小由STL自己管理
it++;
if(it == node.end()) //循环链表,end()是list末端下一位置
it = node.begin();
}
//删除一个结点
list<int>::iterator next = ++it;
if(next==node.end()) next=node.begin(); //循环链表
node.erase(--it); //删除这个结点,node.size()自动减1
it = next; //更新it
//插入结点,见后面的习题“自行车停放”
链表练习:自行车停放
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
struct node{ //双向链表
//int id; //结点编号,没用到
int data; //数据
int preid; //前一个结点
int nextid; //后一个结点
}nodes[N];
int now;
int locate[N]; //locate(x) = now; 值为x的结点位置在nodes[now]
void init() {
nodes[0].nextid = 1;
nodes[1].preid = 0;
now = 2;
}
void insert(int k, int x) { //插入一个nodes[now],插到nodes[k]的右面
nodes[now].data = x;
locate[x] = now; //记录值为x的结点的位置
nodes[now].nextid = nodes[k].nextid;
nodes[now].preid = k;
nodes[nodes[k].nextid].preid = now;
nodes[k].nextid = now;
now++;
}
int main() {
int n; cin >> n;
init();
int a; cin >> a; //第一辆车编号
insert(0, a);
n--;
while (n--) {
int x, y, z; cin >> x >> y >> z;
if (z == 0) //把x插到y的左边
insert(nodes[locate[y]].preid, x); //用locate[]快速定位
else //把x插到y的右边
insert(locate[y], x);
}
for (int i = nodes[0].nextid; i != 1; i = nodes[i].nextid)
cout << nodes[i].data << " ";
return 0;
}
- STL list
大佬:当然你也使用 STL list 求解,它的代码量也少了不少;注意我代码中 loc的作用,它和上面代码中 locate[]的作用一样。
萌新: C++的STL真香。
#include <bits/stdc++.h>
using namespace std;
long long n,x,a,b,c;
list<int>::iterator loc[100003]; //小技巧
int main(){
long long i;
list<int> L; //链表
scanf("%ld %ld",&n,&x);
L.push_back(x); //插入刚开始编号
loc[x] = L.begin(); //迭代器地址存入数组
list<int>::iterator temp; //临时迭代器
for(i=1;i<=n-1;i++){
cin>>a>>b>>c; //a为待插元素编号 b为表中元素编号 c表示左右
temp = loc[b];
if(c==0){
L.insert(temp,a);
//L.insert函数表示在链表L的temp位置前插入元素a
loc[a] = --temp; //将新插入的元素地址记录到数组中
}
else{
L.insert(++temp,a);
loc[a] = --temp;
}
}
for(list<int>::iterator it=L.begin();it!=L.end();it++)
cout<<*it<<" ";
return 0;
}
- Java
本题的Java代码,请参考 蓝桥杯 算法训练 自行车停放_sunnnnh的博客-CSDN博客