1.什么是链表
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表
中的指针链接次序实现的。
链表的逻辑图:
链表的物理图:
也就是说,在定义链表节点的时候,会有一个变量来存放它下一个节点的指针,让它们连接起来。可以去想象一些火车,火车的车厢就是一个一个连接起来的,在逻辑上我们可以认为它是连接的,物理上可能不是连接。
2.链表的实现
2.1链表节点的定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;//存放下一个节点的指针
}SLTNode;
2.2新增节点的函数
SLTNode* BuySListNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if(newnode == NULL)
{
perror("malloc fail");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
在堆上开辟好空间后,除了检查和赋值外重点就是将存放下一个节点的指针给设置空,方便后面的尾插时的操作。
2.3链表的遍历(链表的打印)
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while(cur)
{
printf("%d -> ",cur->data);
cur = cur->next;
}
printf("NULL\n");
}
假设phead指向的是下图中的存放数据1的节点,上图代码中我们定义了一个指针变量cur,并且将phead的值赋值给了cur,所以cur和phead的指向都是下图中的存放数据1的节点。
cur = cur->next这句代码的意思将cur的指向给改成了存放数据1节点中下一个节点的指针的指向(cur原本的指向是0x0012FF90,改变后cur的指向就变成了0x0012FFA0),所以cur就指向了数据2这个节点。
循环下去,cur的指向最终就会变成空,如下图所示:
当cur变为空的时候循环终止,链表的遍历就这样结束了。
2.4链表的尾插
链表的尾插需要考虑两个点:1.是链表为空,2.链表不为空
1.链表为空:
当链表为空时,如果我们想要完成尾插时单靠一级指针是不行的,如果想要改变外部函数中的一级指针变量的指向时,需要使用二级指针才行,二级指针解引用找到外部一级指针变量然后修改就能改变外部函数的一级指针变量。(要实现一个链表,考验的是大家指针和结构体的水平)
2.链表不为空:
下图链表的逻辑图中,如果我们想要尾插就需要先找到存放数据3这个节点,之后再去改变数据3这个节点next的指向,这样就能尾插新数据了。
假设我们要尾插的是数据4这个节点,需要将数据3节点的next指针的指向改成数据4这个节点。
这样尾插就完成了。
需要注意的是在找尾的时候我们要判断的是节点next指针是否为空,如果像遍历一样去找空的话会造成新节点的丢失导致内存泄漏。
正确找尾节点的代码:
SLTNode* tail = *pphead;
while(tail->next)//找尾节点
{
tail = tail->next;
}
错误找尾节点的代码:
SLTNode* tail = *pphead;
while(tail)//找尾节点
{
tail = tail->next;
}
3.尾插代码
void SLTPushBack(SLTNode** pphead,SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySListNode(x);
if(*pphead == NULL)
{
*pphead = newnode;
}
else
{
SLTNode* tail = *pphead;
while(tail->next)//找尾节点
{
tail = tail->next;
}
tail->next = newnode;
}
}
2.5链表的尾删
尾删需要注意的3个点:1.链表为空,2.链表只有一个节点,3.链表有多个节点
1.链表为空
如果链表为空还删什么呀!直接给它报错,让他反思一下。
2.链表只有一个节点
只有一个节点的情况下,我们把节点的空间给释放掉后就需要将外部的一级指针给置空,所以是需要使用二级指针。
3.链表有多个节点
如下图,如果我们想要删除数据为4的节点,那么我们要找的是数据4节点的上一个节点,也就是数据3节点。
找到数据3节点后,将数据4节点的空间给释放掉
最后将数据3节点的next指针给置空,完成尾删
找尾节点的上一个节点的方法:假设我们用tail这个变量去找,tail是本节点,我们就可以用tail->next去判断它next指针是否为空,tail->next->next就可以判断它next指向的节点中的next是否为空(我自己也晕,看下图的标记)。
当tail->next->next为空时,tail就是尾节点的上一个节点,下面是找尾节点的上一个节点的代码:
SLTNode* tail = *pphead;
while(tail->next->next)//找尾节点的上一个节点
{
tail = tail->next;
}
4.尾删的代码
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);//检查链表是否为空
if((*pphead)->next == NULL)//只有一个节点
{
free(*pphead);
*pphead = NULL;
}
else//多个节点
{
SLTNode* tail = *pphead;
while(tail->next->next)//找尾节点的上一个节点
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
2.6链表的头插
下图中如果要在头节点的前面插入新的数据4节点。
将数据4节点的next指针指向头节点。
最后将头节点指向变成数据4节点,这样头插就完成了
即使链表为空,也不影响头插。
void SLTPushFront(SLTNode** pphead,SLTDataType x)
{
assert(pphead);
SLTNode* newnode = BuySListNode(x);
newnode->next = *pphead;
*pphead = newnode;
}
2.7链表的头删
下图中如果我们要删除头节点也就是数据1节点,需要将数据2节点保存起来
当保存好数据2节点的时候,就可以释放掉头节点
最后将头节点的指针设置为数据2节点,头删完成
需要注意的点是,检查链表是否为空。
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* pheadnext = (*pphead)->next;
free(*pphead);
(*pphead) = pheadnext;
}
2.8查找
链表的查找和遍历其实是一样,只不过是将打印换成了比较而言,如果找到了返回节点的指针,找不到返回空。
SLTNode* SLTFind(SLTNode* phead,SLTDataType x)
{
SLTNode* cur = phead;
while(cur)
{
if(cur->data == x)
{
return cur;
}
cur = cur->next;
}
return NULL;
}
2.9在指定位置之后插入和删除
1.在指定位置之后插入:
如果我们想在pos位置的后面插入新的节点,那么我们需要保存数据3节点的指针。
最后将pos节点的next指针指向新节点,新节点的next指向数据3节点,在指定位置之后插入完成
void SLIInsertAfter(SLTNode* pos,SLTDataType x)
{
assert(pos);
SLTNode* newnode = BuySListNode(x);
SLTNode* posnext = pos->next;
newnode->next = posnext;
pos->next = newnode;
}
2.在指定位置之后删除:
如果我们想删除pos后的数据3节点,需要将数据4节点保存下来
将数据3节点释放掉后,将pos节点和数据4节点连接起来,这样删除就完成了
void SLTEraseAfter(SLTNode* pos)
{
assert(pos);
assert(pos->next);//检查是否为尾节点
SLTNode* del = pos->next;
SLTNode* delnext = del->next;
pos->next = delnext;
free(del);
}
2.10在指定位置之前插入删除
1.在指定位置之前插入:
如果想在下图pos位置之前插入节点,需要先找到pos的前一个节点。
重点:找的时候比较的不是节点的数据,而是节点的指针,原因是因为链表中可能存在相同数据的节点,但节点的指针只有一个,如果pos和头节点相等就是头插。
void SLTInsert(SLTNode** pphead,SLTNode* pos,SLTDataType x)
{
assert(pphead);
assert(*pphead);
assert(pos);
SLTNode* cur = *pphead;
if(cur == pos)//头插
{
SLTPushFront(pphead,x);
}
else
{
while(cur->next != pos)//找pos前一个节点
{
cur = cur->next;
}
SLTNode* newnode = BuySListNode(x);
cur->next = newnode;
newnode->next = pos;
}
}
2.删除指定位置:
先去找pos节点,将pos前一个节点和后一个节点连接起来,释放掉pos,删除就完成了
重点:如果pos和头节点相等就是头删。
void SLTErase(SLTNode** pphead,SLTNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
SLTNode* cur = *pphead;
if(cur == pos)//头删
{
SLTPopFront(pphead);
}
else
{
while(cur->next != pos)//找pos前一个节点
{
cur = cur->next;
}
cur->next = pos->next;
free(pos);
}
}
3.链表的优点
链表的优点:
1.按需申请和释放
2.中间插入和删除效率比顺序表高