链表是一种物理存储单元上非连续、非顺序的存储结构。数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
线性表的顺序存储结构缺点是每一次插入和删除元素,大量元素的移动会导致时间效率低下。为了改进顺序存储结构的缺点,引入链式存储结构,即为链表。
链式存储结构的特点是用一组任意的存储单元来存储线性表中的数据元素。这样在插入和删除元素时,可以通过直接修改指针完成操作,时间效率大大提高。但因为链式存储结构的存储单元不连续,所以需要通过指针来访问它的后续元素。
为了表示每个数据元素与其直接后继数据元素之间的逻辑关系,我们需要存出一个其直接后继的存储位置。我们把存储数据元素信息的域成为数据域,把存储后继位置的域称为指针域,这两部分构成一个节点。
n个节点链接成一个链表,即为线性表的链式存储结构。因为每个节点只有一个指针域,所以又将这样的链表称为单链表。
下面介绍单链表的几种基本操作:
单链表的基本操作
链表的创建
链表的一个节点由指针域和数据域构成。
链表的整体思想其实并不难懂,但一旦让他和指针结合在一起时,就容易让人摸不得头脑。
可以这样理解:在链表的创建中,添加一个指向下一个节点的指针,这个指针保存的是下一个节点的地址,我们说这个指针指向下一个节点。
那么指针的类型是什么呢?当然是Node了,因为指针不仅仅指向数据域,同时也指向指针域。
下面引入一段话来帮助理解:
将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针,或者反过来说,指针中存储了这个变量的内存地址,指向了这个变量,通过指针就能找到这个变量。
代码实现:
struct Node{
ElemType data;
struct Node *next;
};
链表的初始化
- 参数的传入:涉及改变链表的操作统统用指针传链表,不然函数调用完成之后,为传入的链表分配的的内存会自动释放,链表不会有任何改变。
- 创建头结点,为头结点分配内存。
- 令头节点的指针为空指针(指针不初始化容易出现很多问题)
PS:这里为什么要动态分配内存呢?
因为这就是数组和链表的区别呀:线性表的顺序存储结构用数组实现,而数组占用的是一整块内存,数组元素分布很集中,需要提前预定数组的长度。
而链表是一种动态结构,它所占用的大小和位置是不需要提前分配的,可以根据自身的需求即时生成。
代码实现:
void InitList(LinkList *L){
*L = (LinkList)malloc(sizeof(Node));
(*L)->next = NULL;
}
判断链表是否为空
如果链表头结点指针域不为空,证明链表不为空。反之链表为空。(因为头结点指针域存储的就是链表的第一个元素的地址)
bool EmptyList(LinkList L){
if(L->next)
return false;
else
return true;
}
返回链表元素个数
因为链表中没有定义表长,所以要用到“工作指针后移”的思想,就是从第一个节点指针开始依次后移,直到节点为空为止,这时循环执行的的次数就是表长。
- 声明一个节点指针 p 指向链表的第一个节点。
- 当 p 不为空时,使指针 p 不断后移。
- 用 i 计数,循环结束后返回。
代码实现:
int LengthList(LinkList L){
int i = 0;
LinkList p = L->next;
//L是头结点,L->next 代表链表的第一个节点。
//LinkList其实是 Node * ,所以p指向链表的链表的第一个节点。
while(p){
i++;
p = p->next;
}
//指针一直后移,直到节点不存在为止。
return i;
}
清空链表
这里仍然用到了“工作指针后移”的思想,从第一个节点开始,每一个节点依次释放内存,直到最后一个节点停止。
- 声明节点q,p。
- 将第一个节点赋值给p。
- 将下一个节点赋值给q,释放q,将q赋值给q。
- 往复循环,直到全部节点的内存释放完成。
代码实现:
void ClearList(LinkList *L){
LinkList p,q;
p = (*L)->next;
while(p){
q = p->next;
free(p);
p = q;
}
(*L)->next = NULL;
}
思考:while循环里的值能不能为:
free(p);
p = p->next;
答:肯定会出错。
因为这里的free释放的是 p 整个节点,指针域也会消失。指针域消失后运行第二行代码时会报错。
返回给定位置的元素值
节点指针从第一个节点开始后移,直到指针移动到到指定位置时,若节点不为空,直接返回其值。
- 创建一个节点指针p指向链表的第一个节点,初始化cnt从1开始
(为什么从1开始,因为位置最小是1) - 当 cnt < i 时,遍历链表,使p的指针不断后移
- 当 cnt = i 时,返回该节点数据域的值。
- 如果链表末尾p为空,则说明第i个元素不存在
代码实现:
Status(LinkList L,int i,Elemtype *e){
int cnt = 1;
LinkList p = L->next;
while(p && cnt < i){
p = p->next;
cnt++;
}
if(!p)
return ERROR;
*e = p->data;
return OK;
}
查找数据
结合“工作指针后移”的思想,以下的代码应该不难理解。
- 声明一个节点 p 指向链表的第一个节点,初始化 i 从0开始。
- 依次对每一个 p 节点的指针域与 e 进行对比,相等则返回对应值,不相等则返回0。
- 当 p 不为空时,使 p 指针不断后移。
代码实现:
int LocateList(LinkList L,Elemtype *e){
int i = 0;
LinkList p = L->next;
while(p){
i++;
if(p->data == e)
return i;
p = p->next;
}
return 0;
}
删除元素
节点指针依次后移,到指定位置后,如果节点不为空,返回其数据域。释放该节点前,要将前一个节点的指针指向该删除节点的后继元素。
- 声明一节点 p 指向链表的头结点,初始化 cnt 为 1。
- 当 cnt 小于 i 时,遍历链表,然p的指针不断后移,不断指向下一个节点,cnt 逐次加1。
- 如果链表末尾 p 不存在,说明要查找的元素不存在。
- 将要删除的节点赋值给 q。
- 将 q 的后继赋值给 p 的后继。
- 返回 q 的数据域给 e。
- 系统回收 q 节点,释放q的内存。
代码实现:
Status ListDelete(LinkList *L,int i,ElemType *e){
int cnt = 1;
LinkList q,p;
p = (*L);
//此时 p 为头节点,p->next为第一个节点,对应cnt的值为1。
while( p->next && cnt < i){
p