一、链表的结构
线性表的链式存储又称单链表,它是指通过一组任意的存储单元来存储线性表中的数据元素。为了建立数据元素之间的线性关系,对每个链表节点,除存放元素自身的信息外,还需要存放一个指向其后继节点的指针。单链表的节点类型定义如下:
typedef struct LINKLIST
{
int data;//存放数据
struct LINKLIST* next;//存放下一节点的指针
}link;
对于单链表的结构,主要分为物理结构和逻辑结构。
1.1物理结构
在物理上,每一个单链表节点在内存中都是随机的,并不像顺序表那样是连续存储的,两两链表之间是通过指针域存放的指针来寻找下一节点的位置。
链表的的本质就是结构体,结构体内分别有data数据域和next下一节点的指针域,其中data中存放的是链表中所存放的数据,next指针所指向的是下一个节点的指针。
所以在物理上,链表不是连续的,彼此之间的联系是通过指针维系的,本质上是一个结构体,结构体内存放着数据和下一节点的指针。
1.2逻辑结构
在逻辑上,我们为了更好的去理解链表,所以会有像下方图片中所描绘的逻辑结构图:
如图所示,在数据域中,data中存储着数据,指针域则是指向下一个节点,如果最后一个链表后方没有了链表,则它的指针置为空,如此一来,我们就能直观的理解链表的结构了。
二、单链表的相关操作
2.1初始化单链表
链表的初始化,需要我们将链表的指针即数据全部置为空,也可以将数据域置为你要的数据,将指针域置为空,如下图所示:
代码实现如下:
//初始化链表
void LinkListInit(link* pf)
{
assert(pf);
pf->data = 4;
pf->next = NULL;
}
int main()
{
link pf;
LinkListInit(&pf);
return 0;
}
2.2单链表的插入
单链表的插入分为多种,有头插、尾插、还有给定位置插入,这三种插入所要对指针的改动均不相同,需要在不影响原来单链表的相对位置下将新的节点插入其中。
2.2.1头插法
头插法顾名思义就是在单链表的头部插入数据,从而使插入的数据成为单链表的表头,位居第一的位置。
头插法的思想是直接将新的节点的next指针指向原来单链表的头部,然后再将单链表的表头更改为新的节点上,如此一来就可以将节点顺利插入到表头位置了,但是需要注意的是,如果是在无头节点的情况下,想要更改内存中头节点的位置,必须使用二级指针进行传参,方可达到改变头节点位置的效果。但是如果在有头节点的情况下,因为不论是单链表的增、删、查、改,都只需调动一下指针即可,无需调用二级指针。
对于无头节点要调用二级指针的问题,不明白的话可以参考一下两个数据的交换函数,对于形参来说,它只是实参的一份临时拷贝,并不能在本质上解决问题。
如上图片,要使用头插法插入节点,其步骤图如下:
代码如下:
//建立节点
link* CreatNode(int n)
{
link* pf = (link*)malloc(sizeof(link));
assert(pf);
pf->data = n;
pf->next = NULL;
return pf;
}
//头插
void LinkListPushHead(link** head, int n)
{
assert(*head);
link* phead = CreatNode(n);//建立新节点
phead->next = *head;//将新节点的next指针指向原先的头节点
*head = phead;//更新头节点指针
}
int main()
{
link pf;
link* pt = &pf;
link** ph = &pt;
LinkListInit(pt);//初始化单链表
LinkListPushHead(ph, 3);//头插
return 0;
}
2.2.2尾插
相比于头插,尾插不需要移动头节点的指针,只需在单链表尾部直接插入数据即可,将原先单链表尾部的next指针指向新建的节点即可,不需要传二级指针。
但在插入之前要找到链表的表尾位置,所以要进行一趟从前往后的遍历。
尾插逻辑图如下:
代码如下:
//尾插
void LinkListPushTail(link* pf, int n)
{
assert(pf);
link* tail = CreatNode(n);
while (pf->next)
pf = pf->next;
pf->next = tail;
}
int main()
{
link pf;
link* pt = &pf;
link** ph = &pt;
LinkListInit(pt);//初始化
LinkListPushHead(ph, 3);//头插
LinkListPushTail(pt, 5);//尾插
return 0;
}
2.2.3指定节点前后插入
如果说要求在节点数据为4之前插入一个值为3的节点,我们首先要找到值为4节点的前一个节点,然后再改变前一个节点和新节点的指针,这样就能完成插入操作。(以第一个出现的4为例)
具体逻辑操作图如下所示:
代码如下:
//指定节点之前插入
void AppointPush(link** pt, int n, int m)//在n之前插入值为m的新节点
{
assert(*pt);
//如果头节点的数据为n则是进行头插
if ((*pt)->data == n)
{
link* newnode = CreatNode(m);
newnode->next = *pt;
*pt = newnode;
return;
}
link* phead = *pt;
while (phead->next->data != n)
{
phead = phead->next;
}
link* newnode = CreatNode(m);
newnode->next = phead->next;
phead->next = newnode;
}
int main()
{
link pf;
link* pt = &pf;
link** ph = &pt;
LinkListInit(pt);//初始化
LinkListPushHead(ph, 3);//头插
LinkListPushTail(pt, 5);//尾插
AppointPush(ph,4, 3);//在4之前插入值为3的新节点
return 0;
}
目前经过初始化、头插、尾插、指定位置插入,链表中的节点为3-> 3-> 4-> 5-> NULL。
2.3单链表的删除
单链表的删除同插入一样,也大致分为头删、尾删、指定位置删除。
2.3.1头删
头删就是对表头节点进行删除,但是删除时必须要注意不能使后面的节点断开或丢失指针,必须在删除前先将头指针向后移动,再进行删除操作。
与头插操作一样,头删也需要传入二级指针进行头节点指针的改变。
代码如下:
//头删
void LinkListDeleteHead(link** pt)
{
assert(*pt);
if ((*pt)->next == NULL)//如果单链表中只有一个节点,则直接释放,头指针置空
{
free(*pt);
*pt = NULL;
return;
}
link* phead = *pt;//如果不只有一个节点,头节点先行赋给新节点,再向后移动一位
*pt = (*pt)->next;
free(phead);
phead = NULL;
}
int main()
{
link pf;
link* pt = &pf;
link** ph = &pt;
LinkListInit(pt);//初始化
LinkListPushHead(ph, 3);//头插
LinkListPushTail(pt, 5);//尾插
AppointPush(ph,4, 3);//指定位置插入
LinkListDeleteHead(ph);//头删
PrintNode(pt);
return 0;
}
2.3.2尾删
尾删的操作十分简单,如果单链表中只有一个节点,直接删除释放即可,如果不止一个,则要从前往后遍历,找到倒数第二个节点,然后删除尾节点。
逻辑图如下所示:
代码如下:
//尾删
void LinkListDeleteTail(link* pf)
{
assert(pf);
if (pf->next == NULL)
{
free(pf);
pf = NULL;
return;
}
while (pf->next->next)
{
pf = pf->next;
}
free(pf->next);
pf->next = NULL;
}
int main()
{
link pf;
link* pt = &pf;
link** ph = &pt;
LinkListInit(pt);//初始化
LinkListPushHead(ph, 3);//头插
LinkListPushTail(pt, 5);//尾插
AppointPush(ph,4, 3);//指定位置插入
//LinkListDeleteHead(ph);//头删
LinkListDeleteTail(pt);//尾删
PrintNode(pt);
return 0;
}
2.4单链表的查找、改正
对于查改,只需遍历链表即可,如若找到该节点则操作,找不到就显示false。
三、链表的特点
链表在内存中是分散存储的,因此他适合较为分散且密度小的数据。
优点:
(1)对数据的插入和删除操作较为方便,无需移动大量的数据,只需改变指针即可。
(2)当遇到难以估计线性表长度或存储规模时,链表较为符合。
缺点:
(1)在内存中占用空间大,且相比于顺序表来说,它的读取速度要小于顺序表。
(2)不能随机访问数据,要访问某个数据,只能从头节点依次向后遍历。
无论是对于顺序表还是链表,我们在实际中应当以具体的环境和情况为主,两种结构均有优劣,只有在深刻了解顺序表和链表的操作后,才能理解它们各自的优缺点。