2.2 链表
内存空间是所有程序的公共资源,在一个复杂的系统运行环境下,空闲的内存空间可能散落在内存各处.我们知道,存储数组的内存空间必须是连续的,而当数组非常大时,内存可能无法提供如此大的连续空间.此时链表的灵活性优势就体现出来了.
链表(linked list) 是一种线性数据结构,其中的每个元素都是一个节点对象,各个节点通过“引用”相连接.引用记录了下一个节点的内存地址,通过它可以从当前节点访问到下一个节点.
链表的设计使得各个节点可以分散存储在内存各处,它们的内存地址无须连续。
观察图2-2-1,链表的组成单位是节点(node)对象.每个节点都包含两项数据:
- data: 数据域,也是节点的值
- next: 指针域,指向下一个节点的指针
如以下代码所示,链表节点 ListNodec 除了包含值,还需额外保存一个引用(指针).因此在相同数据量下,链表比数组占用更多的内存空间 .
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | |
1 2 3 4 5 6 | |
1 2 3 4 5 | |
1 2 3 4 5 6 | |
链表中的一些概念
头节点
在单链表的开始结点之前设立一个节点称之为头结点(也称为哨兵节点或哑节点),头结点的数据域可以不存储任何信息,也可以存储链表的长度等附加信息,头结点的指针域存储指向第一个结点(首元结点)的指针。
头指针
头指针是指链表中,指向第一个结点的指针.
头指针具有标识作用,所以常常会用头指针冠以链表的名字.所以你定义一个链表,那么链表的名字一般就是这个链表的头指针.
ListNode L = new ListNode(0); 左边的是指针和结点
无论链表是否为空,头指针均不为空,头指针是链表的必要元素.
首元节点
链表中第一个元素所在的结点,它是头结点后边的第一个结点.如果是带头结点的链表,则头结点后面的为首元结点.
元素是指链表中实际存储数据的结点,像头结点就不属于元素,因为它存储的不是数据,而是一些链表的属性信息(链表长度)或者为空.
整理成一句话就是
- 头指针: 指向第一个节点
- 头节点: 在首元结点前面设立的一个结点
- 首元结点: 链表中第一个元素所在的结点
- 元素结点: 存储链表实际信息的结点
单链表(带头结点)
下面以带头节点的单链表为例展示以下6种函数的实现
按值查找节点
按位序插入节点
按位序删除节点
头插法创建链表
尾插法创建链表
1 链表初始化
建立链表分为两步,第一步是初始化各个节点对象,第二步是构建节点之间的引用关系.初始化完成后,我们就可以从链表的头节点出发,通过引用指向next依次访问所有节点.
| linked_list.c | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
| linked_list.cpp | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | |
| linked_list.py | |
|---|---|
1 | |
| linked_list.java | |
|---|---|
1 | |
可视化运行
2 链表插入节点(按位序插入)
在链表中插入节点非常容易.如图2-2-2所示,假设我们想在相邻的两个节点n0 和 n1 之间插入一个新节点 P ,则只需改变两个节点引用(指针)即可,时间复杂度为 O(1) .
相比之下,在数组中插入元素的时间复杂度为O(n),在大数据量下的效率较低.
| linked_list.c | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | |
| linked_list.cpp | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | |
| linked_list.py | |
|---|---|
1 | |
| linked_list.java | |
|---|---|
1 | |
可视化运行
3 链表删除节点
如图 2-2-3 所示,在链表中删除节点也非常方便,只需改变一个节点的引用(指针)即可。
Info
请注意,尽管在删除操作完成后节点 P 仍然指向 n1,但实际上遍历此链表已经无法访问到 P ,这意味着 P 已经不再属于该链表了。
| linked_list.c | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
| linked_list.cpp | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | |
| linked_list.py | |
|---|---|
1 | |
| linked_list.java | |
|---|---|
1 | |
4 访问节点
在链表中访问节点的效率较低.如上一节所述,我们可以在O(1)时间下访问数组中的任意元素.链表则不然,程序需要从头节点出发,逐个向后遍历,直至找到目标节点.也就是说,访问链表的第i个节点需要循环i - 1>轮,时间复杂度为O(n).
| linked_list.c | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
| linked_list.cpp | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
| linked_list.py | |
|---|---|
1 2 3 4 5 6 7 | |
| linked_list.java | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
5 查找节点
遍历链表,查找其中值为 target 的节点,输出该节点在链表中的索引.此过程也属于线性查找.代码如下所示:
| linked_list.c | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
| linked_list.cpp | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
| linked_list.py | |
|---|---|
1 2 3 4 5 6 7 8 9 | |
| linked_list.java | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 | |
6 尾插法 - 建立单链表
下面展示通过尾插法创建一个链表。
尾插法的特点是每插入一个新节点,链表的尾节点指针(pTail)会更新为新插入的节点,使其始终指向当前链表的尾结点。从而使得输入的数据在链表中按顺序存储>。 当输入数据为 999 时,循环结束,将尾节点的 next 指针置为 NULL 表示链表结束,函数返回最终的链表头节点。
| linked_list.c | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
| linked_list.cpp | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | |
7 头插法 - 建立单链表
下面展示通过尾插法创建一个链表。
请注意
头插法结果为倒叙
该代码通过头插法创建一个链表. 头插法的特点是每插入一个新节点,链表的头节点就会变成新插入的节点,从而使得输入的数据在链表中是倒序存储>的. 当输入数据为 999 时,创建链表的循环结束,函数返回最终的链表头节点.
| linked_list.c | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |
| linked_list.cpp | |
|---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 | |