链表的概念
链表的定义
线性表的链式存储就是链表。
它是将元素存储在物理上任意的存储单元中,由于⽆法像顺序表⼀样通过下标保证数据元素之间的逻辑关系,链式存储除了要保存数据元素外,还需额外维护数据元素之间的逻辑关系,这两部分信息合称结点(node)。即结点有两个域:保存数据元素的数据域和存储逻辑关系的指针域
链表的分类
将各种类型的链表排列组合,总共有8种不同链表结构
- 单向
- 不带头节点
- 不带头节点的单链表
- 不带头节点的单向循环链表
- 带头节点
- 带头结点的单链表
- 带头节点的单向循环链表
- 不带头节点
- 双向
- 不带头节点
- 不带头节点的双向链表
- 不带头节点的双向循环链表
- 带头节点
- 带头结点的双向链表
- 带头节点的双向循环链表
- 不带头节点
单链表的模拟实现
实现⽅式
链表的实现⽅式分为动态实现和静态实现两种
- 动态实现是通过new申请结点,然后通过delete释放结点的形式构造链表。这种实现⽅式最能体现链表的特性;
- 静态实现是利⽤两个数组配合来模拟链表。第⼀次接触可能⽐较抽象,但是它的运⾏速度很快,在算法竞赛中会经常会使⽤到
定义-创建-初始化
- 两个⾜够⼤的数组,⼀个⽤来存数据,⼀个⽤来存下⼀个结点的位置
- 变量 h ,充当头指针,表⽰头结点的位置
- 变量 id ,为新来的结点分位置
const int N = 1e5 + 10;
int h; // 头指针
int id; // 下⼀个元素分配的位置
int e[N], ne[N]; // 数据域和指针域
// 下标 0 位置作为哨兵位
// 其中 ne 数组全部初始化为 0,其中 ne[i] = 0 就表⽰空指针,后续没有结点
// 当然,也可以初始化为 -1 作为空指针,看个⼈爱好
/*
e[i] 和 ne[i] 是绑定在⼀起使⽤的,也有⼀种写法是定义⼀个结构体,把这两个变量放在⼀起,⽐如:
struct node
{
int e, ne;
}list[N];
但是,定义成结构体之后,代码书写不⽅便。我们只要知道 e[i] 和 ne[i] 是绑定在⼀起使⽤的即可
*/
头插
这是链表中最常⽤也是使⽤最多的操作,后续树和图的存储中的邻接表以及链式前向星就会⽤到这个操作
// 头插
void push_front(int x)
{
// 先把 x 放在⼀个格⼦⾥⾯
id++;
e[id] = x;
// 修改指针,顺序不能颠倒!
// 1. x 的右指针指向哨兵位的后继
// 2. 哨兵位的右指针指向 x
ne[id] = ne[h];
ne[h] = id;
}
时间复杂度:
只涉及指针的修改,时间复杂度为O(1)
遍历链表
// 打印链表
void print()
{
// 定义⼀个指针从头结点开始
// 通过 ne 数组逐渐向后移动
// 直到遇到空指针
for(int i = ne[h]; i; i = ne[i])
{
cout << e[i] << " ";
}
cout << endl << endl;
}
时间复杂度:
遍历整个链表,时间复杂度为O(N)
按值查找
解法⼀:遍历整个链表即可
解法⼆:如果存储的值数据范围不⼤,可以⽤哈希表优化。
int mp[N]; // mp[i] 表⽰ i 在这个元素存放的位置
/*
push_front 和 insert 的时候,打上标记
mp[x] = id; // x 这个元素存放的位置是 id
erase 的时候,消除标记
mp[x] = 0;
*/
// 查询值为 x 的元素存储的位置
int find(int x) // 注意,这⾥传⼊的是元素的值
{
// 策略⼀:先找到 x,然后返回 x 后⾯的元素
for(int i = ne[h]; i; i = ne[i]) // 遍历链表
{
if(e[i] == x) // 如果找到 x
{
return i;
}
}
// 找不到就返回 0
return 0;
// // 策略⼆:使⽤哈希表优化
return mp[x]; // mp[x] ⾥⾯就存着 x 这个元素存储的位置下标
}
时间复杂度:
遍历整个链表查询:时间复杂度O(N) ;
使⽤标记数组优化:时间复杂度O(1)
在任意位置之后插⼊元素
// 在存储位置为 p 的元素后⾯,插⼊⼀个元素 x
void insert(int p, int x) // ⼀定要注意,这⾥的 p 是位置,不是元素
{
id++; // x 这个元素分配的位置
e[id] = x; // 将 x 放在 id 位置处
ne[id] = ne[p]; // x 指向 p 的后⾯
ne[p] = id; // p 指向 x
}
时间复杂度:
只有常数级别的操作,时间复杂度为O(1)
删除任意位置之后的元素
// 删除存储位置为 p 后⾯的元素
void erase(int p) // 注意 p 表⽰元素的位置
{
if(ne[p])
{
mp[e[ne[p]]] = 0; // 将 p 后⾯的元素从 mp 中删除
ne[p] = ne[ne[p]]; // 指向下⼀个元素的下⼀个元素
}
}
时间复杂度:
只⽤修改指针,时间复杂度为O(1)
所有测试代码
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
// 创建
int e[N], ne[N], h, id;
int mp[N]; // mp[i] 表⽰ i 这个数存储的位置
// 遍历链表
void print()
{
for(int i = ne[h]; i; i = ne[i])
{
cout << e[i] << " ";
}
cout << endl << endl;
}
// 头插
void push_front(int x)
{
id++;
e[id] = x;
mp[x] = id; // 标记 x 存储的位置
// 先让新结点指向头结点的下⼀个位置
// 然后让头结点指向新来的结点
ne[id] = ne[h];
ne[h] = id;
}
// 按值查找
int find(int x)
{
// // 解法⼀:遍历链表
// for(int i = ne[h]; i; i = ne[i])
// {
// if(e[i] == x) return i;
// }
// return 0;
// 解法⼆:⽤ mp 数组优化
return mp[x];
}
// 在任意位置 p 之后,插⼊⼀个新的元素 x
void insert(int p, int x)
{
id++;
e[id] = x;
mp[x] = id;
ne[id] = ne[p];
ne[p] = id;
}
// 删除任意位置 p 后⾯的元素
void erase(int p)
{
if(ne[p]) // 当 p 不是最后⼀个元素的时候
{
mp[e[ne[p]]] = 0; // 把标记清空
ne[p] = ne[ne[p]];
}
}
int main()
{
for(int i = 1; i <= 5; i++)
{
push_front(i);
print();
}
//cout << find(1) << endl;
//cout << find(5) << endl;
//cout << find(6) << endl;
// insert(1, 10);
// print();
// insert(2, 100);
// print();
erase(2);
print();
erase(4);
print();
return 0;
}
双向链表的模拟实现
实现⽅式
依旧采⽤静态实现的⽅式。
双向链表⽆⾮就是在单链表的基础上加上⼀个指向前驱的指针,那就再来⼀个数组,充当指向前驱的指针域即可
const int N = 1e5 + 10;
int h; // 头结点
int id; // 下⼀个元素分配的位置
int e[N]; // 数据域
int pre[N], ne[N]; // 前后指针域
// h 默认等于 0,指向的就是哨兵位
// 此时链表为空,没有任何⼏点,因此 ne[h] = 0
头插
// 头插
void push_front(int x)
{
id++;
e[id] = x;
mp[x] = id; // 存⼀下 x 这个元素的位置
// 左指向哨兵位,右指向哨兵位的下⼀个位置,也就是头结点
pre[id] = h;
ne[id] = ne[h];
// 先修改头结点的指针,再修改哨兵位,顺序不能颠倒
pre[ne[h]] = id;
ne[h] = id;
}
时间复杂度:
只涉及指针的修改,时间复杂度为O(1)
遍历链表
直接⽆视 prev 数组,与单链表的遍历⽅式⼀致
// 打印链表
void print()
{
for(int i = ne[h]; i; i = ne[i])
{
cout << e[i] << " ";
}
cout << endl << endl;
}
时间复杂度:
遍历整个链表,时间复杂度O(N)
按值查找
直接使⽤mp数组优化
// 查找元素 x 在链表中存储的位置
int find(int x)
{
// ⽤ mp 优化
return mp[x];
}
时间复杂度:
直接拿值,时间复杂度为O(1)
在任意位置之后插⼊元素
// 在存储位置为 p 的元素后⾯,插⼊⼀个元素 x
void insert_back(int p, int x)
{
id++;
e[id] = x;
mp[x] = id; // 存⼀下 x 这个元素的位置
// 先左指向 p,右指向 p 的后继
pre[id] = p;
ne[id] = ne[p];
// 先让 p 的后继的左指针指向 id
// 再让 p 的右指针指向 id
pre[ne[p]] = id;
ne[p] = id;
}
时间复杂度:
只涉及指针的修改,时间复杂度为O(1)
在任意位置之前插⼊元素
// 在存储位置为 p 的元素前⾯,插⼊⼀个元素 x
void insert_front(int p, int x)
{
id++;
e[id] = x;
mp[x] = id; // 存⼀下 x 这个元素的位置
// 先左指针指向 p 的前驱,右指针指向 p
pre[id] = pre[p];
ne[id] = p;
// 先让 p 的前驱的右指针指向 id
// 再让 p 的左指针指向 id
ne[pre[p]] = id;
pre[p] = id;
}
时间复杂度:
只涉及指针的修改,时间复杂度为O(1)
删除任意位置的元素
// 删除下标为 p 的元素
void erase(int p)
{
mp[e[p]] = 0; // 从标记中移除
ne[pre[p]] = ne[p];
pre[ne[p]] = pre[p];
}
时间复杂度:
只涉及指针的修改,时间复杂度为O(1)
所有测试代码
#include <iostream>
using namespace std;
const int N = 1e5 + 10;
// 创建双链表
int e[N], ne[N], pre[N], id, h;
int mp[N]; // mp[i] 表⽰:i 这个值存储的位置
// 遍历链表
void print()
{
for(int i = ne[h]; i; i = ne[i])
{
cout << e[i] << " ";
}
cout << endl << endl;
}
// 头插
void push_front(int x)
{
id++;
e[id] = x;
mp[x] = id;
// 先修改新来结点的左右指针
pre[id] = h;
ne[id] = ne[h];
// 修改哨兵位下⼀个结点的左指针
pre[ne[h]] = id;
ne[h] = id;
}
int find(int x)
{
return mp[x];
}
// 在任意位置 p 的后⾯插⼊新的元素 x
void insert_back(int p, int x)
{
id++;
e[id] = x;
mp[x] = id;
pre[id] = p;
ne[id] = ne[p];
pre[ne[p]] = id;
ne[p] = id;
}
// 在任意位置 p 的前⾯插⼊新的元素 x
void insert_front(int p, int x)
{
id++;
e[id] = x;
mp[x] = id;
pre[id] = pre[p];
ne[id] = p;
ne[pre[p]] = id;
pre[p] = id;
}
// 删除任意位置 p 的元素
void erase(int p)
{
mp[e[p]] = 0; // 把标记清空
ne[pre[p]] = ne[p];
pre[ne[p]] = pre[p];
}
int main()
{
for(int i = 1; i <= 5; i++)
{
push_front(i);
print();
}
//cout << find(3) << endl;
//cout << find(5) << endl;
//cout << find(0) << endl;
// insert_front(2, 22);
// print();
// insert_front(3, 33);
// print();
// insert_front(4, 44);
// print();
erase(2);
print();
erase(4);
print();
return 0;
}
循环链表的模拟实现
循环链表就是在原有的基础上,让最后⼀个元素指向表头即可
动态链表-list
new和delete是⾮常耗时的操作,
在算法⽐赛中,⼀般不会使⽤new和delete去模拟实现⼀个链表。
⽽STL⾥⾯的list的底层就是动态实现的双向循环链表,增删会涉及new和delete,效率不⾼,竞赛中⼀般不会使⽤,这⾥了解⼀下即可
创建list
#include <list>
using namespace std;
int main()
{
list<int> lt; // 创建⼀个存储 int 类型的链表
return 0;
}
push_front/push_back
- push_front :头插;
- push_back :尾插。
时间复杂度:O(1)
void testadd()
{
list<int> lt;
// 尾插
for(int i = 1; i <= 5; i++)
{
lt.push_back(i);
print(lt);
}
// 头插
for(int i = 1; i <= 5; i++)
{
lt.push_front(i);
print(lt);
}
}
pop_front/pop_back
- pop_front :头删
- pop_back :尾删
时间复杂度:O(1)
// 删
void testdelete()
{
list<int> lt;
// 尾插
for(int i = 1; i <= 5; i++)
{
lt.push_back(i);
}
// 头插
for(int i = 5; i >= 1; i--)
{
lt.push_front(i);
}
// 头删
for(int i = 1; i <= 3; i++) lt.pop_front();
// 尾删
for(int i = 1; i <= 3; i++) lt.pop_back();
print(lt);
}
所有测试代码
#include <iostream>
#include <list>
using namespace std;
void print(list<int>& l)
{
for(auto x : l)
{
cout << x << " ";
}
cout << endl;
}
int main()
{
list<int> l;
// 尾插
for(int i = 1; i <= 5; i++)
{
l.push_back(i);
print(l);
}
// 头插
for(int i = 1; i <= 5; i++)
{
l.push_front(i);
print(l);
}
// 头删
for(int i = 1; i <= 3; i++)
{
l.pop_front();
}
print(l);
// 尾删
for(int i = 1; i <= 3; i++)
{
l.pop_back();
}
print(l);
return 0;
}
练习
B3630 排队顺序 - 洛谷
本题相当于告诉了我们每⼀个点的后继,使⽤静态链表的存储⽅式能够很好的还原这个队列。
数组中[1, n]
的下标可以当做数据域,根据题意修改指针域即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
int n, h;
int ne[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) cin >> ne[i];
cin >> h;
for (int i = h; i; i = ne[i])
{
cout << i << " ";
}
return 0;
}
B3631 单向链表 - 洛谷
链表模板题,直接实现⼀个单链表即可
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10, M = 1e6 + 10;
int h, id, e[N], ne[N];
int mp[M];
int q;
int op, x, y;
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
id++;
e[id] = 1;
mp[1] = id;
cin >> q;
while (q--)
{
cin >> op >> x;
int p = mp[x]; // x存的位置
if(op == 1)
{
cin >> y;
id++;
e[id] = y;
ne[id] = ne[p];
ne[p] = id;
mp[y] = id;
}
else if (op == 2)
{
cout << e[ne[p]] << endl;
}
else
{
ne[p] = ne[ne[p]];
}
}
return 0;
}
P1160 队列安排 - 洛谷
频繁的在某⼀个位置之前和之后插⼊元素,因此可以⽤双向循环的链表来模拟
#include <bits/stdc++.h>
using namespace std;
const int N = 1e5 + 10;
int h, pre[N], ne[N];
int n, m;
bool st[N]; //表示当前元素是否已经被删除
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n;
pre[1] = h;
ne[h] = 1;
for (int i = 2; i <= n; i++)
{
int k, p; cin >> k >> p;
if (p == 0)
{
ne[i] = k;
pre[i] = pre[k];
ne[pre[k]] = i;
pre[k] = i;
}
else
{
ne[i] = ne[k];
pre[i] = k;
pre[ne[k]] = i;
ne[k] = i;
}
}
cin >> m;
while (m--)
{
int x; cin >> x;
if(st[x] == true) continue;
ne[pre[x]] = ne[x];
pre[ne[x]] = pre[x];
st[x] = true;
}
for (int i = ne[h]; i; i = ne[i])
{
cout << i << " ";
}
return 0;
}
P1996 约瑟夫问题 - 洛谷
用循环链表来模拟整个过程
- 因为要执行删除操作,所以计数的时候少算一次
- 初始化的时候可以让t = n,方便统一处理
#include <bits/stdc++.h>
using namespace std;
const int N = 110;
int n, m;
int ne[N];
int main()
{
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m;
for (int i = 1; i < n; i++) ne[i] = i + 1;
ne[n] = 1;
int t = n;
for (int i = 1; i <= n; i++)
{
for (int j = 1; j < m; j++) //让t向后移动m-1次
{
t = ne[t];
}
cout << ne[t] << " ";
ne[t] = ne[ne[t]];
}
return 0;
}