目录
链表的实现⽅式分为动态实现和静态实现两种。 (ppt演⽰两种实现⽅式的区别)
- 动态实现是通过new申请结点,然后通过delete释放结点的形式构造链表。这种实现⽅式最能体 现链表的特性;
- 静态实现是利⽤两个数组配合来模拟链表。第⼀次接触可能⽐较抽象,但是它的运⾏速度很快,在 算法竞赛中会经常会使⽤到。
一.单链表的模拟实现
1.1.定义-创建-初始化
- 两个⾜够⼤的数组,⼀个⽤来存数据,⼀个⽤来存下⼀个结点的位置
- 变量 h ,充当头指针,表⽰头结点的位置
- 变量 id ,为新来的结点分位置
注意我们这里是有一个头结点的(即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]是绑定在⼀起使⽤的即可
*/
1.2.头插
这是链表中最常⽤也是使⽤最多的操作,后续树和图的存储中的邻接表以及链式前向星就会⽤到这个 操作。 因此必须要掌握的操作。
// 头插
void push_front(int x)
{
// 先把x放在⼀个格⼦⾥⾯
id++;
e[id] = x;
// 修改指针,顺序不能颠倒!
// 1. x 的右指针指向哨兵位的后继
// 2. 哨兵位的右指针指向x
ne[id] = ne[h];
ne[h] = id;
}
时间复杂度:只涉及指针的修改,时间复杂度为O(1)
1.3.遍历链表
// 打印链表
void print()
{
// 定义⼀个指针从头结点开始
// 通过ne数组逐渐向后移动
// 直到遇到空指针
for (int i = ne[h]; i; i = ne[i])
{
cout << e[i] << " ";
}
cout << endl << endl;
}
时间复杂度:遍历整个链表,时间复杂度为O(N)
1.4.按值查找
解法⼀:遍历整个链表即可。
解法⼆:如果存储的值数据范围不⼤,可以⽤哈希表优化。(不知道哈希表是什么没有关系,只要知 道mp数组的作⽤,把它当成⼀个标记数组即可。)
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) // 如果找到
{
return i;
}
// 找不到就返回0
return 0;
}
}
// 策略⼆:使⽤哈希表优化
int find(int x) // 注意,这⾥传⼊的是元素的值
{
return mp[x]; // mp[x] ⾥⾯就存着x这个元素存储的位置下标
}
时间复杂度:
- 遍历整个链表查询:时间复杂度 O(N) ;
- 使⽤标记数组优化:时间复杂度 O(1)。
1.5.在任意位置之后插入元素
// 在存储位置为 p 的元素后面,插入一个元素 x
void insert(int p, int x) {
id++; // x 这个元素分配的位置
e[id] = x; // 将 x 放在 id 位置处
ne[id] = ne[p]; // x 指向 p 的后面
ne[p] = id; // p 指向 x
}
时间复杂度: 只有常数级别的操作,时间复杂度为 O(1) 。
1.6.删除任意位置之后的元素
// 删除存储位置为p后⾯的元素
void erase(int p) // 注意p表⽰元素的位置
{
if (ne[p])
{
mp[e[ne[p]]] = 0; // 将p后⾯的元素从mp中删除
ne[p] = ne[ne[p]]; //指向下⼀个元素的下⼀个元素
}
}
时间复杂度: 只⽤修改指针,时间复杂度为 O(1) 。
1.7.完整代码
#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;
}
二.双向链表的模拟实现
2.1.定义-创建
依旧采⽤静态实现的⽅式。
双向链表⽆⾮就是在单链表的基础上加上⼀个指向前驱的指针,那就再来⼀个数组,充当指向前驱的 指针域即可。
const int N = 1e5 + 10;
int h; // 头结点
int id; // 下⼀个元素分配的位置
int e[N]; // 数据域
int pre[N], ne[N]; // 前后指针域
// h 默认等于0,指向的就是哨兵位
// 此时链表为空,没有任何⼏点,因此ne[h]=0
2.2. 头插
// 头插
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)**。
2.3. 遍历链表
直接无视 `pre` 数组,与单链表的遍历方式一致。
// 打印链表
void print()
{
for (int i = ne[h]; i; i = ne[i])
{
cout << e[i] << " ";
}
cout << endl << endl;
}
时间复杂度:遍历整个链表,时间复杂度为 **O(N)**。
2.4. 按值查找
直接使用 `mp` 数组优化。
// 查找元素 x 在链表中存储的位置
int find(int x)
{
// 用 mp 优化
return mp[x];
}
时间复杂度:直接拿值,时间复杂度为 **O(1)**。
2.5. 在任意位置之后插入元素
// 在存储位置为 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)**。
2.6.在任意位置之前插入元素
// 在存储位置为 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)**。
2.7. 删除任意位置的元素
// 删除下标为 p 的元素
void erase(int p)
{
mp[e[p]] = 0; // 从标记中移除
ne[pre[p]] = ne[p];
pre[ne[p]] = pre[p];
}
时间复杂度:只涉及指针的修改,时间复杂度为 **O(1)**。
2.8.所有测试代码
#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;
}
```