第三章 链表和list 单链表部分
本章涉及链表和list的基本操作,因为个人感觉理解较为复杂,在此分为多部分进行总结,本篇涉及其中的单链表的创建、插入、删除、查找等。
链表的概念
链表的定义
线性表的链式存储就是链表。
它是将元素存储在物理上的任意存储单元中,因此无法像顺序表一样通过下标来保证数据元素之间的逻辑关系,链式存储除了要保存数据元素外,还需要额外维护数据元素之间的逻辑关系,这两部分信息合称结点。每个结点存在两个域:保存数据元素的数据域和保存下一个结点地址的指针域。
我们注意到,链表是通过指针来维护数据元素之间的逻辑关系的,因此在本节利用数组模拟单链表时,数组中的下标仅代表其物理地址,而不代表其逻辑地址,务必进行区分。
链表的分类
链表根据指针域的连接方式可以主要分为单链表、双向链表。
而其中,根据其是否带有头结点,又可以分为带头结点的单/双向链表和不带头结点的单/双向链表。还可以根据是否循环,分为循环单/双向链表和非循环单/双向链表。
根据以上分类,我们可以得到8种链表的组合:
链表 |
---|
不带头结点的单链表 |
不带头结点的单向循环链表 |
带头结点的单链表 |
带头结点的单向循环链表 |
不带头结点的双向链表 |
不带头结点的双向循环链表 |
带头结点的双向链表 |
带头结点的双向循环链表 |
链表的种类繁多,我们只需掌握单向链表、双向链表、循环链表即可。
单链表的模拟实现
实现方式
链表的实现方式分为动态实现和静态实现。
- 动态实现时通过new申请结点,delete释放结点的形式构造链表。这种实现方式最能够体现链表的特征。
- 静态实现是利用两个数组配合来模拟链表。运行速度很快,在算法竞赛中经常会使用到。
定义-创建-初始化
- 两个足够大的数组,
e[]
表示结点的值,ne[]
表示结点的next指针,两者下标(即物理地址)是相同且一一对应的,一定注意:下标代表的是结点的物理地址,而不是逻辑地址。 - 变量
h
充当头指针,表示头结点的位置。 - 变量
id
用于为新结点分配地址。
示例:
const int N = 100010;
int h; /* 头指针 */
int e[N]; /* 数据域 */
int ne[N]; /* 指针域 */
int id; /* 分配地址给新结点 */
头插
代码实现:
void insert_head(int x) {
id++;
e[id] = x;
ne[id] = ne[h]; /* 先将新结点的next指针指向原头结点 */
ne[h] = id; /* 再将头指针指向新结点 */
}
时间复杂度:O(1)
遍历
代码实现:
void print() {
/* 从头结点开始遍历直到遇到空指针 */
for (int i = ne[h]; i ; i = ne[i]) {
cout << e[i] << " ";
}
cout << endl;
}
时间复杂度:O(n)
按值查找
方法一:遍历整个链表。
代码实现:
int find(int x) {
for (int i = ne[h]; i ; i = ne[i]) {
if (e[i] == x) {
return i;
}
}
return -1;
}
时间复杂度:O(n)
方法二:当存储范围较小且不含重复元素时,利用哈希表,可以理解为一个标记数组,将值映射到下标。
代码实现:
int mp[100010];
/*在插入操作时,将值映射到下标
mp[x] = id;
在删除操作时,消除标记
mp[x] = 0;*/
void find(int x) {
return mp[x];
}
时间复杂度:O(1)
在任意位置 后 插入元素
代码实现:
/*
在p结点后插入元素x
即p结点的next指针指向新结点,新结点的next指针指向p结点的原next指针*/
void insert(int p, int x) {
id++;
e[id] = x;
ne[id] = ne[p];
ne[p] = id;
}
时间复杂度:O(1)
- 我们注意到这里的插入操作是针对于指定节点后面插入的,而不是指定位置插入,因为我们这里模拟的单向列表仅能找到指定节点后面的元素,而无法找到前序元素进行插入,后面的删除操作原理相同。
删除任意位置之后的元素
代码实现:
/*
删除p结点的后一个结点
即p结点的next指针指向p结点的后一位(原next指针指向的元素)的next指针*/
void del(int p) {
if(ne[p]){
ne[p] = ne[ne[p]];
}
}
时间复杂度:O(1)
- 当删除最后一给元素时,倒数第二个元素的next指针指向空指针,即
ne[ne[p]] = 0
。所以这里务必进行判断。
其他问题
上文提到,我们不会去实现尾插、尾删、删除任意位置的元素等操作。在实际应用中,单向链表不会像在模拟数组中一样十分方便的找到最后一个元素,即无法通过id–和id++直接实现尾删或尾插。
我们需要通过遍历才能找到最后一个元素,所需时间过长。
同时,因为无法定位到前序元素,无法将后续元素接上,所以无法实现删除任意位置的元素。
一更:请注意:e[]
和ne[]
括号中的元素都是地址而不是存储元素,虽然在示例中其与存储数据相同,但请注意区分。