-
数据元素可以存在内存未被占用的任意位置。
-
除了存数据元素信息外,还要存储它的直接后继元素的存储地址。
-
数据元素 a i a_i ai的存储映像,称为结点(Node):数据域、指针域(指针or链)
-
结点由存放数据元素的数据域和存放后继元素结点地址的指针域组成
-
假设P为指向线性表第i个元素的指针
-
结点 a i a_i ai的数据域: a i = P . d a t a a_i=P.data ai=P.data
-
结点 a i a_i ai的指针域:P.next
-
a i + 1 = P . n e x t . d a t a a_{i+1}=P.next.data ai+1=P.next.data
-
n个结点链接成一个链表,即线性表 ( a 1 , a 2 , . . . , a n ) (a_1,a_2,...,a_n) (a1,a2,...,an)的链式存储结构,因为每个结点中只包含一个指针域,所以叫做单链表
-
链表中第一个结点的存储位置叫做头指针,整个链表的存取必须从头指针开始进行。
-
链表的最后一个结点指针为空(NULL或^)。
-
头结点:
- 头结点的数据域可以不存储任何信息,也可以存储线性表的长度等附加信息。
- 头结点的指针域存储指向第一个结点的指针。
-
头指针与头结点的异同:
- 头指针:
- 头指针是指链表指向第一个结点的指针
- 若链表有头结点,则是指向头结点的指针
- 头指针具有标识作用,所有常用头指针冠以链表的名字
- 无论链表是否为空,头指针均不为空。
- 头指针是链表的必要元素。
- 头结点:
- 头结点是为了操作的统一和方便而设立的,放在第一元素的结点之前,其数据域一般无意义(也可存放链表的长度)
- 有了头结点,对在第一元素结点前插入结点和删除第一结点,其操作与其它结点的操作就统一了。
- 头结点不一定是链表的必须要素。
- 头指针:
3.2.1单链表的读取
获得链表第i个数据的算法思路:
-
核心思想:工作指针后移
-
声明一个指针 p p p指向链表第一个结点,初始化 j j j从1开始。
-
当 j < i j<i j<i时,就遍历链表,让 p p p的指针向后移动,不断指向下一结点, j j j累加1.
-
若到链表末尾p为空,这说明第i个结点不存在。
-
否则查找成功,返回结点p的数据。
GetElem(L,i):
p = L.next # 让p指向链表L的第一个结点
j = 1
while p and j<i: # p不为空且计数器j还没有等于i时,循环继续
p = p.next # 让p指向下一个结点
j++
if !p or j>i:
return ERROR # 第i个结点不存在
e = p.data # 取第i个结点的数据
这个算法的时间复杂度取决于i的位置,当i=1时,不需遍历,第一个就取出数据了,而当i=n时则遍历n-1次才可以。因此最坏时间复杂度为O(n)
3.2.2单链表的插入与删除
- 单链表的插入
算法思路:
-
声明一指针p指向链表头结点,初始化j从1开始;
-
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
-
若到链表末尾p为空,则说明第i个结点不存在;
-
否则查找成功,在系统中生成一个空结点s;
-
将数据元素e赋值给s.data;
-
单链表的插入标准语句
s.next = p.next p.next = s
-
返回成功
ListInsert(L,i,e): p = L.next # 让p指向链表L的第一个结点 j = 1 while p and j < i: # 寻找第i-1个结点 p = p.next j++ if !p or j > i: return ERROR # 第i个结点不存在 s.data = e # 生成新结点 s.next = p.next # 将p的后继结点赋值给s的后继 p.next = s # 将s赋值给p的后继 return OK
- 单链表的删除
将它的前继结点的指针绕过,指向它的后继结点即可。(把p的后继结点改成p的后继的后继结点)
算法思路:
-
声明一指针p指向链表头结点,初始化j从1开始;
-
当j<i时,就遍历链表,让p的指针向后移动,不断指向下一结点,j累加1;
-
若到链表末尾p为空,则说明第i个结点不存在;
-
否则查找成功,将欲删除的结点p.next赋值给q;
-
单链表的删除标准语句
q = p.next p.next = q.next
-
将q结点中的数据赋值给e,作为返回;
-
释放q结点
-
返回成功
ListDelete(L,i): p = L.next # 让p指向链表L的第一个结点 j = 1 while p and j < i: # 寻找第i-1个结点 p = p.next j++ if !p or j > i: return ERROR # 第i个结点不存在 q = p.next p.next = q.next # 将q的后继赋值给p的后继 e = q.data # 将q结点中的数据给e free(e) # 让系统回收此结点,释放内存 return OK
单链表的插入与删除算法由两部分组成:
- 遍历查找第i个结点
- 插入和删除结点
时间复杂度都是O(n)
如果我们不知道第i个结点的指针位置,单链表在插入和删除操作上与顺序存储结构没有太大的优势。
但是,如果我们希望从第i个位置,插入10个结点,对于顺序存储结构,每一次插入都需要移动n-i个结点,每次都是O(n)。
而对于单链表,我们只需要在第一次时,找到第i个位置的指针,此时为O(n),接下来只是简单的通过赋值移动指针而已,时间复杂度为O(1)
对于插入和删除数据越频繁的操作,单链表的效率优势就越是明显
3.2.3单链表的整表创建
- 头插法(始终让新结点在第一的位置)
算法思路:
-
声明一指针p和计数器变量i;
-
初始化一空链表L;
-
让L的头结点的指针指向NULL,即建立一个带头结点的单链表
-
循环:
- 生成一新结点赋值给p
- 随机生成一数字赋值给p的数据域p.data
- 将p插入到头结点与前一新结点之间
CreateListHead(L,n): i = 0 L.next = NULL while i<n: i ++ p.data = random(100) # 随机生成100以内的数字 p.next = L.next L.next = p # 插入到表头
- 尾插法(把新结点放在最后)
CreateListTail(L,n):
i = 0
r = L # r为指向尾部的结点
while i<n:
i++
p.data = random(100) # 随机生成100以内的数字
r.next = p # 将表尾终端结点的指针指向新结点
r = p # 将当前的新结点定义为表尾终端结点
r.next = NULL # 表示当前链表结束(循环结束后,让这个结点的指针域置空,以便以后遍历时可以确认其是尾部)
L是指整个单链表,r是指向尾结点的变量。
3.2.3单链表的整表删除
算法思路:
- 声明一结点p和q
- 将第一个结点赋值给p
- 循环:
- 将下一结点赋值给q
- 释放p
- 将q赋值给p
考虑一个问题:q变量有没有存在的必要?
p结点除了有数据域还有指针域,在释放p时,是对它整个结点进行删除和内存释放的工作。变量q的作用,它使得下一个结点是谁得到了记录,以便于等当前结点释放后,把下一结点拿回来补充。
ClearList(L):
p = L.next # p指向第一个结点
while p: # 没到表尾
q = p.next
free(p)
p = q
L.next = NULL # 头结点指针域为空
return OK
3.3 单链表结构与顺序存储结构的优缺点
-
存储分配方式:
- 顺序存储结构用一段连续的存储单元依次存储线性表的数据元素
- 单链表采用链式存储结构,用一组任意的存储单元存放线性表的元素
-
时间性能:
- 查找:
- 顺序存储结构O(1)
- 单链表O(n)
- 插入和删除:
- 顺序存储结构需要平均移动表长一半的元素,时间为O(n)
- 单链表在得出某位置的指针后,插入和删除时间仅为O(1)
- 查找:
-
空间性能:
- 顺序存储结构需要预分配存储空间,分大了,浪费,分小了容易发生上溢
- 单链表不需要分配存储空间,只要有就可以分配,元素个数也不受限制
-
经验性结论:
- 若线性表需要频繁查找,很少进行插入和删除操作,采用顺序存储结构
- 若需要频繁插入和删除操作,采用单链表结构
- 当线性表中的元素个数变化较大或者根本不知道有多大时,最好用单链表结构。这样不用考虑存储空间的大小问题
- 如果事先知道线性表大致长度,用顺序存储结构效率会高一些。