一、概念与结构
概念:
链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
我们先来看一下这张图片,很明显,链表由两个部分组成,一个是数据,一个是指向下一个节点的指针。其中plist保存的是第一个结点的地址,如果我们想要plist指向第二个结点,我们只需要将plist保存的地址改为0x0012FFA0,这样我们的plist指向的就是第二个结点了。在链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。
二、链表的代码与实现
2.1 链表的代码组成
typedef int SLTDataType;//定义int类型为SLTDataType
typedef struct SListNode
{
SLTDataType data; //结点的数据
struct SListNode* next;//指向下一个结点的指针
}SLTNode;
链表主要由两个部分构成,一个是数据,一个是下一个结点的指针。
2.2 链表主要的主要功能
void SLTPrint(SLTNode* phead);/*链表的打印*/
SLTNode* SLTbuyNode(SLTDataType x);//申请新的节点
void SLTPushBack(SLTNode** pphead, SLTDataType x);//在链表的尾部插入一个节点
void SLTPushFront(SLTNode** pphead, SLTDataType x);//在链表的首部插入一个节点
void SLTPopBack(SLTNode** pphead);//删除尾部节点
void SLTPopFront(SLTNode** pphead);//删除首部节点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//查找节点
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);//在指定位置之前插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);//在指定位置之后插入数据
void SLTErase(SLTNode** pphead, SLTNode* pos);//删除pos结点
void SLTEraseAfter(SLTNode* pos);//删除pos之后的结点
void SListDestroy(SLTNode** pphead);//销毁链表
链表这个数据结构我们主要的是掌握以上几个功能,那么下面开始一一讲解。
2.2.1 申请新的结点
SLTNode* SLTbuyNode(SLTDataType x)
{
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
perror("malloc fail");//空间分配失败
exit(1);//退出程序
}
node->data = x;
node->next = NULL;
return node;
}
由于链表是由一个一个结点组成的,所以我们每次插入一个数据,都需要申请一次空间,其中格外要注意我们分配的空间大小为SLTNode的大小。申请完空间之后我们需要进行初始化,将新结点中要插入的x数据赋给data,新节点中指向下一个结点的指针置为空。到这里申请新结点的代码就写完了。
2.2.2 链表尾插
//在链表的尾部插入一个数据
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = SLTbuyNode(x);//申请一个新的结点
if (*pphead == NULL)//判断链表是否为空链表,如果为空,将newnode作为首地址。
{
*pphead = newnode;
}
else
{
SLTNode* pcur = *pphead;
while (pcur->next)//找到尾结点
{
pcur = pcur->next;
}
pcur->next = newnode;
}
}
首先当然是先申请一个新的结点,因为我们要尾插一个数据,然后进行判断,如果链表一个结点都没有,那我们就将新申请的结点作为头结点。如果有头结点我们就进行下面的一部分代码,我们先定义一个临时变量pcur,用来找尾结点,尾结点的next指针指向的是NULL,所以当pcur遍历到尾结点时退出循环,将尾结点的next指针指向新结点。
2.2.3 链表头插
//在链表的头部插入一个节点
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* newnode = SLTbuyNode(x);
//newnode *pphead
newnode->next = *pphead;
*pphead = newnode;
}
头插和尾插一样,我们都需要申请一个新的结点。但是头插比尾插简单很多,我们不需要进行遍历,直接将新节点的next指针指向原先的头结点即可,再将pphead指向新的头结点。
2.2.4 链表尾删
void SLTPopBack(SLTNode** pphead)
{
//链表为空:不可以删除
assert(pphead && *pphead);
//处理只有一个结点的情况:要删除的就是头结点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
//找 prev ptail
SLTNode* ptail = *pphead;
SLTNode* prev = NULL;
while (ptail->next)
{
prev = ptail;
ptail = ptail->next;
}
prev->next = NULL;
free(ptail);
ptail = NULL;
}
}
首先我们要判断链表本身是不是一个空链表,如果是一个空链表直接触发断言,都已经是空链表了我们还需要删什么呢?结点的删除我们分为两个,如果只有头结点了,我们直接删除就可以了,不需要再进行遍历,如果不是那么我们就需要进行遍历。首先我们创建两个变量,一个用来找尾结点,一个用来保存尾结点的前一个结点,当我们找到尾结点后,将前一个结点的next指向空,然后将尾结点释放掉,删除尾结点就完成了。
2.2.5 链表的头删
//删除链表的头节点
void SLTPopFront(SLTNode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* prev = *pphead;
prev = prev->next;
free(*pphead);
*pphead = prev;
}
}
同样分为两种情况,一种是只有一个结点,一种是还有多个结点。当还有多个结点时,这里我们只需要创建一个临时变量用来保存头结点的下一个结点,防止删除头结点之后无法找到下一个结点。保存好结点之后释放掉头结点,然后将保存好的第二个结点置为新的头结点。
2.2.6 查找结点
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
assert(phead);//这里因为不需要修改原来的参数,所以只需要用一级指针
SLTNode* pcur = phead;
while (pcur->next)
{
if (pcur->data == x)
{
return pcur;
}
pcur = pcur->next;//找到了就返回节点位置
}
return NULL;//如果没有找到就返回NULL
}
查找结点的代码比较简单,我们只需要定义一个临时变量,然后从头结点开始遍历,一个一个比较,如果相等返回当前结点,如果不相等,指向下一个结点继续遍历没有找到就返回一个NULL。
2.2.7 指定位置之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
assert(pos);
if (pos == *pphead)//如果插入位置就是头节点,则直接头插即可
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* newnode = SLTbuyNode(x);
//找prev : pos的前一个节点
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
newnode->next = pos;//将新节点放置到pos节点之前
prev->next = newnode;//新节点放置到prev之后
}
}
如果插入的位置就是头结点,直接调用头插即可。既然是插入一个结点,同样我们需要申请一个新的结点,在指定位置之前插入一个结点我们需要借用一下查找结点的函数,当找到指定位置之后会返回一个结点。
可以从这个理解一下意思,找到之后接下来的操作和其它插入数据操作是一样的。
2.2.8 在指定位置之后插入
//在pos节点之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
assert(pos);
SLTNode* newnode = SLTbuyNode(x);
newnode->next = pos->next;
pos->next = newnode;
}
这个和前一个在指定位置之前插入数据差不多,先找到结点,然后再进行插入操作。
2.2.9 删除指定位置之前和之后的结点
//删除pos之前节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(pos);
if (pos == *pphead)
{
SLTPopFront(pphead);
}
else
{
SLTNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos节点之后的数据
void SLTEraseAfter(SLTNode* pos)
{
assert(pos && pos->next);//pos以及它的下一个节点不为空
SLTNode* del = pos->next;
pos->next = pos->next->next;
free(del);
del = NULL;
}
这里主要讲一下删除pos结点之后的结点,首先我们要先保存要被删掉的结点(del),因为我们要先让指定位置的结点的next指向它的下一个再下一个结点,然后再free掉del结点。
2.2.10 链表的销毁
//销毁链表
void SListDestroy(SLTNode** pphead)
{
assert(pphead && *pphead);
SLTNode* pcur = *pphead;
while (pcur)
{
SLTNode* next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}
链表的销毁利用遍历的方式一个结点一个结点的销毁,记得最后要将头结点也置为空。