在上篇博客《线性表的概念和顺序存储》中讲解了线性表的顺序存储,它的存储结构实际上是在数组中储存,相关操作的实现在博客中有讲解,今天的博客讲讲线性表的链式存储
在讲解链式存储之前我们先讲讲顺序存储存在的缺点:
- 需要提前分配好数组存储空间,而且绝大多数情况下这段内存空间都不会被充分利用,存在资源浪费
- 如果在表中进行插入和删除的操作时需要移动大量的元素
线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素,这组存储单元可以是连续的,也可以是不连续的,所以这些数据元素可以存在内存未被占用的任意位置
物理结构如下图所示:
为了表示每个数据元素ai与其直接后继数据元素ai+1之间的逻辑关系,对于数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示指示其直接后继的信息,也就是直接后继的存储位置,我们把存储数据元素信息的部分称为数据域,存储直接后继位置的部分称为指针域,数据域和指针域组成结点
n个结点组成一个链表,也就是线性表(a1, a2,…,an),每个结点中只有一个指针域,所以这样的链表又称为单链表
这里要讲解两个概念:
- 头指针:头指针是指向链表的第一个结点的指针,如果有头结点,则头指针指向头结点
- 头结点,如果没有头结点,则头指针直接指向线性表中的第一个元素,那么插入或删除第1个结点和其他位置的结点就要分情况讨论,添加头结点可以将这两种情况进行统一
在知道了链表中每个结点的组成后,我们就可以使用C语言的结构体来定义每个节点
# define MAXSIZE 20 //给线性表的存储空间的初始分配量
# define OK 1
# define ERROR 0
# define TRUE 1
# define FALSE 0
typedef int ElemType; //ElemType就是线性表中的元素的类型,这里设置为int
typedef int Status;
typedef struct Node
{
ElemType data;
struct Node *next;
} Node;
typedef struct Node *LinkList;
在创建完结点后,我们通过代码来对链表中的元素进行读取,读取步骤为:
- 定义一个指针
p
指向链表中的第一个结点,定义一个整型变量j=1
来进行计数 - 遍历链表,让
p
不断指向下一结点,j
累计+1
- 当
j>=查找元素序号i
或者p
为NULL时跳出循环 - 跳出后进行判断,如果
p
为空则查找的结点不存在, - 否则查找位置的节点存在,打印该结点的数据域
代码实现:
Status GetElem(LinkList L, int i, ElemType *e){
int j;
LinkList p;
p = L->next;
j = 1;
if(i < 0){ //读取位置错误,直接返回ERROR
return ERROR;
}
while(j < i && p){
p = p->next;
j++;
}
if(!p || j > i){ //读取位置为0,也返回ERROR
return ERROR;
}
*e = p->data;
return OK
}
其实我是将链表中的某个元素读取出来后赋给指针变量e
指向的类型为ElemType
的变量,这样不会存在局部变量的问题,当然也可以通过设置返回值来将查找到的结点的地址返回给主函数,可以根据实际需要灵活处理
单链表的插入操作:
如果我们要将一个数据插入到链表中的第4个位置,那么我们就需要遍历链表来找到3个位置,将这个位置的元素的后继修改为要插入的新元素,将新元素的后继修改为之前的第4个位置的元素,这样就将元素顺利插入
步骤为:
- 声明指针
p
指向链表头结点,定义一个整型变量j=1
,进行计数 - 遍历链表,让
p
不断指向下一结点,j
累计+1
- 当
j>=查找元素序号i
或者p
为空时跳出循环 - 跳出后进行判断,如果
p
为空则插入位置的前驱节点不存在,插入失败 - 否则插入位置的前驱节点存在,修改该结点的后继
- 修改插入结点的后继,至此链表插操作完成
代码实现为:
Status ListInsert(LinkList L, int i, ElemType e){
int j;
LinkList p, s;
p = L;
j = 1;
while(j < i && p){
p = p->next;
j++;
}
if(!p || j > i){
return ERROR;
}
s = (LinkList) malloc(sizeof(Node));
s->data = e;
s->next = p->next;
p->next = s;
return OK;
}
在写上面这段代码时有几点注意事项:
-
在查找代码中有句
if(i < 0){ //读取位置错误,直接返回ERROR return ERROR; }
后来发现这段代码没必要存在,因为如果
i<0
则不会进入while
循环,所以j>i
,在后续if
中符合j>i
条件,直接return ERROR;
,所以上面这句代码可以直接去掉 -
s
要在s = (LinkList) malloc(sizeof(Node));
就初始化好 -
在插入代码中
p
就是头结点所在的地址,j=1
,后面p
向后移动,j
自增1,所以跳出循环后p
的指向就是插入位置的前驱结点;而在查找代码中p
就是头结点后面的结点也就是链表中的第1个结点的地址,j=1
,后面p
向后移动,j
自增1,所以跳出循环后p
的指向就是我们要查找的结点,直接打印结点的数据域即可 -
可以使用二级指针将链表传入函数,进行操作
单链表删除操作:
Status ListDelete(LinkList L, int i, ElemType *e){
int j;
LinkList p, q;
p = L;
j = 1;
while(j < i && p->next){
p = p->next;
j++;
}
if(!p || j > i){
return ERROR;
}
q = p->next;
p->next = q->next;
*e = q->next;
free(q);
return OK;
}
和在单链表中插入元素类似,在插入元素前我们需要查找插入位置的前驱节点;而在删除时我们需要查看删除的位置的元素是否存在,与此同时跳出while循环后的p要指向删除位置的前驱节点,所以我们将循环的条件设置为j < i && p->next
我们将删除的元素的前驱节点的后继结点修改为删除的元素的后继结点,之后将删除位置的那个结点的空间释放,删除完成
在讲解了上面的基本操作后,我们来讲解一下链表的整表创建:
我们创建一个链表,在链表的每个结点的数据域存放一个随机数:
void CreateList(LinkList *L, int n){
LinkList p, r;
int i;
srand(time(0));
*L = (LinkList) malloc(sizeof(Node));
r = *L;
for(i = 0; i < n; i++){
p = (LinkList) malloc(sizeof(Node));
p->data = rand() % 100 + 1;
r->next = p;
r = p;
}
r->next = NULL;
}
这里在创建方法中传入的是二级指针,也就是定义了一个指针变量L
,这个变量中存放的地址指向的变量类型为LinkList
也就是struct Node *
,在struct Node *
类型的变量中存放的值是个地址值,指向Node
结构体,所以*L
中存放的值实际上是链表头结点所在的地址,这里实际上采用的是尾插法,最新的数据永远在表尾,当然通过修改也可以修改为头插法
除了单链表的整表创建,我们还需要掌握单链表的整表删除:
步骤为:
- 声明结点
p
和q
- 将第一个结点赋值给
p
- 循环:1.将下一节点赋值给
q
2.释放p
3.将q
赋给p
代码实现
Status ClearList(LinkList *L){
LinkList p, q;
p = (*L)->next; //p指向第一个结点
while(p){ //删除p指向的结点
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL; //将头结点的指针域赋为NULL
return OK;
}
为了便于读者理解我在后续会添加图示