本文已收录至《数据结构》专栏,欢迎大家 点赞 + 收藏 + 关注 !
目录
前言
正文
链表的分类
单链表
带头(带哨兵位)单链表
双向链表
循环链表
常用链表
单链表的接口函数
预处理和数据结构
预处理
数据结构
单链表的实现
单链表动态申请节点函数
单链表遍历输出函数
单链表头部插入数据函数(头插)
单链表尾部插入函数(尾插)
单链表的表头节点删除函数(头删)
单链表的表尾节点删除函数(尾删)
单链表的查找函数
单链表在指定位置插入节点(数据)函数
单链表指定节点删除(数据)函数
单链表销毁函数
单链表的优缺点
单链表的优点
单链表的缺点
总结
上一篇线性表的文章我向大家介绍了线性表的顺序结构并实现了动态顺序表和其接口函数,但是线性表的结构还有另一种,那就是链式结构。
可以发现链表确实像一条链子,链子上数据相互连接;但这是我们想象出来的逻辑结构,实际上链表中计算机中从物理结构并非如此,本期我们来详细介绍——链表。
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
链表在逻辑结构上是连续的,但物理结构上不一定连续,实际中的数据节点是从堆上申请出来的,而从堆上申请空间有一定的策略,两次申请的空间可能连续也可能不连续,这取决于编译器和内存情况。
链表的分类
链表分为很多个种类,每一个种类都有不同的优缺点!
单链表
链表中一个data域存储数据,一个next指针域指向下一个节点(指向后继)。
带头(带哨兵位)单链表
带头的单链表,在单链表的基础上多了一个哨兵节点指向单链表的第一个节点,哨兵节点的数据类型与链表中节点基本上一样,但是哨兵节点本身的data域不存储任何数据。
双向链表
双向链表有数据域data存储数据和指针域next指向下一个节点,唯一与单链表不同的是,双向链表还有一个节点prev指向上一个节点(指向前驱)。
循环链表
循环列表在结构上与单链表非常相似,但是区别在于循环列表的尾节点不指向空而是指向头节点。
常用链表
以上这四种类型的链表任意组合可以组合出八种链表,但最常用的只有两种!
第一种是单链表,第二种是集合了上面四种链表特性的带头双向循环链表。
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结 构的子结构,如哈希桶、图的邻接表等等,而且这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带 来很多优势,实现反而简单了,后面我们代码实现了就知道了。
本次我们先介绍最基本的单链表。
单链表的接口函数
单链表的操作与顺序表大致相同,仍然是对数据的增删查改,但是在实现上与顺序表有一定的区别。
//单链表增删查改实现 // 动态申请一个结点 SListNode* BuySListNode(SLTDateType x); // 单链表打印 void SListPrint(SListNode* plist); // 单链表尾插 void SListPushBack(SListNode** pplist, SLTDateType x); // 单链表的头插 void SListPushFront(SListNode** pplist, SLTDateType x); // 单链表的尾删 void SListPopBack(SListNode** pplist); // 单链表头删 void SListPopFront(SListNode** pplist); // 单链表查找 SListNode* SListFind(SListNode* plist, SLTDateType x); // 单链表在pos位置插入x void SListInsertAfter(SListNode* pos, SLTDateType x); // 单链表删除pos位置的值 void SListEraseAfter(SListNode* pos); // 单链表的销毁 void SListDestroy(SListNode** plist);
接下来我们逐一实现这些接口函数!
预处理和数据结构
预处理
与顺序表中相同,我们仍然需要typedef重定义我们所使用的数据类型,方便随时的切换和更改。
以及我们需要定义的两个头文件stdio.h和stdlib.h。
数据结构
我们的每个节点都需要一个data存储数据,每个data的数据类型为我们自己typedef重定义的数据类型,且需要一个单链表结构体类型的指针,指向下一个节点(结构体数据)。
typedef int SLTDateType; //单链表结构体类型数据 typedef struct SListNode { SLTDateType data;//数据域 struct SListNode* next;//指针域 }SListNode;
单链表的实现
单链表动态申请节点函数
单链表与顺序表不同,扩容不需要一片连续的空间,而是申请一块新的节点空间(这块空间的数据类型是结构体SListNode),将这些节点逐一连起来就是单链表了。实现上,我们需要一个SLTDateType重定义类型x接收该节点data域的数据,利用malloc申请,malloc申请一块一块的空间非常合适,申请的字节大小为一个单链表节点的大小,即SListNode,申请后还需要if判断是否申请成功,虽然是一个小小的举动,但是可以帮我们避免很多不必要的麻烦;在返回该节点的地址前还需要将其next指针域置空,防止野指针。
// 动态申请一个节点 SListNode* BuySListNode(SLTDateType x) { SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));//malloc申请一块空间 if (newnode == NULL)//如果为空则申请失败 { perror("malloc fail!\n"); exit(-1);//强制退出 } newnode->data = x;//将x赋值给新节点的data域 newnode->next = NULL;置空指针域 return newnode;//返回这个地址 }
单链表遍历输出函数
对于遍历输出全部数据,我们采用迭代的方式,利用while循环。可能有人会问那我们如何从一个节点跳跃到下一个节点?我们通过next指针一直向后迭代,一直走到空就停止,因为最后一个节点的next域为空!这里我们需要借助临时变量cur进行遍历,而不使用plist形参,因为如果改变了函数形参,那么下次再次需要使用时就无法找到原形参了;在进行遍历之前我们需要对指针Plist进行检查防止对空指针和野指针的引用!如果我们还想知道节点个数,还可以增加一个变量进行计数,最后随节点数据一起输出即可!
// 单链表的打印 void SListPrint(SListNode* plist) { SListNode* cur = plist;//定义临时变量cur进行遍历 int SLTSize=0;//计节点数 while (cur!=NULL) { printf("%d->", cur->data);//打印当前节点的data域 cur = cur->next;//走向下一个节点 SLTSize++; } printf("NULL|表长%d\n",SLTSize);输出NULL(代表表尾)和节点数 }
单链表头部插入数据函数(头插)
单链表的头插相当于其他插入来说比较简单,只需要将头指针改为新插入的节点然后让新节点指向原头节点的地址即可。但是这里会有一个形参与实参的问题,我们传入的是一个指针参数,如果我们要修改指针变量(修改传入参数的变量),那么传入一级指针是没用的,需要将这个变量的地址也传入这个函数,然后在函数中解引用才能修改这个指针变量中的地址!
我们看下面fun1和fun2两个例子!
void fun1(SListNode* plist) { plist = NULL; }//出函数就失效--不会造成任何影响 void fun2(SListNode** plist) { //解引用二级指针 *plist=NULL; }//对指针变量(实参)造成了修改,出函数后头指针被置空
在fun1函数中,plist只是形参,fun1函数执行结束后就销毁了,无法对头指针plist造成任何影响!
在fun2函数中,plist接收的是指针变量(实参)的地址,解引用后就是头指针的地址,此时修改*plist会对原指针变量造成影响!
这里在插入前还需要先判断plist是否为空指针,如果为空指针则是首次头插,直接插入即可。
//单链表的头插 void SListPushFront(SListNode** pplist, SLTDateType x) { if (!*pplist)//判断是否为空指针 { *pplist = BuySListNode(x);//直接插入 } else//如果不为空则链入 { SListNode* node = (*pplist);//保存原头节点 *pplist= BuySListNode(x);//头指针指向新头节点 (*pplist)->next = node;//新头节点指向旧头节点 } }
单链表尾部插入函数(尾插)
单链表的尾插是在表尾链入一个新节点,在链入之前,我们先需要找到尾,我们通过while迭代找到尾节点(next为空的节点),这里我们仍然需要一个临时变量cur来接收头指针代替头指针去遍历,所以这里while继续的条件是cur->next!=NULL(或cur->next),而且如果当表为空时(plist为NULL),则是直接插入,就不需要去找尾了!这里要注意的是,虽然是对尾节点进行操作,但是由于有空表的情况,我们需要对头指针变量进行修改,所以需要传入头指针变量的地址对变量进行修改,也就是二级指针!
// 单链表尾插 void SListPushBack(SListNode** pplist, SLTDateType x) { SListNode* cur = *pplist;//临时变量cur指向头节点 if (!*pplist)//如果链表为空则直接插入 { *pplist =BuySListNode(x); } else//如果不为空开始找尾并插入 { while (cur->next)//迭代找尾 { cur = cur->next; } cur->next= BuySListNode(x);//插入新节点 } }
单链表的表头节点删除函数(头删)
单链表的头删同头插一样,仍然是对头指针变量进行操作,所以也需要二级指针,我们在顺序表中提到过,动态内存管理中内存申请和销毁是同时出现的,我们申请了系统的内存就必须还给系统,否则会造成内存泄漏;这里我们不能直接free头指针变量,如果直接销毁头指针变量就会丢失后面的所有节点造成内存泄漏,我们需要先定义一个临时变量存放要删除的头节点地址,然后改变头指针变量的指向,让头指针指向当前头节点的下一个节点即可!如果链表中只有一个节点,那么就可以直接free头指针变量,然后置空头指针即可,如果表中没有节点(头指针为空)则不需要删除!
// 单链表头删 void SListPopFront(SListNode** pplist) { if (*pplist)//判断头指针是否为空(判断表空) { if (!((*pplist)->next))//判断是否只有一个节点 { free(*pplist);//直接释放 *pplist = NULL;//置空头指针(防止野指针) } else { SListNode* freenode = *pplist;//临时变量存储头节点地址 *pplist = (*pplist)->next;//头指针指向表中第二个节点 free(freenode);//释放头节点 freenode = NULL; } } }
单链表的表尾节点删除函数(尾删)
对于尾删,如果表中只有一个节点直接删除置空头指针即可,如果没有节点则不需要删除,所有要判断表空的情况。这里我们需要是利用while迭代找尾节点的前驱,定义一个临时变量cur去遍历链表,直到找到尾节点的前驱,这里找尾与尾插不同,我们需要找到尾节点的前驱(上一个节点)然后定义一个临时变量保存尾节点地址销毁并置空尾节点前驱(上一个节点)的next指针,防止野指针的访问。这里我们仍然是使用二级指针接收参数,因为链表只有一个节点时需要置空头指针,所以这里需要注意不能使用头指针变量迭代找尾,会丢失除尾以外的所有节点!
// 单链表的尾删 void SListPopBack(SListNode** pplist) { if (*pplist)//判断是否为空指针 { if (!((*pplist)->next))//判断是否只有一个节点 { free(*pplist); *pplist = NULL; } else { SListNode* cur = *pplist;//临时变量用作遍历 while (cur->next->next)//遍历找尾节点前驱 { cur = cur->next; } SListNode* freenode = cur->next;//获取尾节点 cur->next = NULL;//置空尾节点前驱的next指针 free(freenode);//释放尾节点 freenode = NULL; } } }
单链表的查找函数
对于单链表的查找,这个函数是为了服务接下来的指定插入和删除的,具体的实现思路是从头开始迭代遍历节点, 当节点值等于查找值x时返回该节点的地址,若没找到则返回NULL。这里我们需要传入一级指针(因为不需要对头指针进行修改,只是遍历)。而且如果链表为空则直接返回NULL。
// 单链表查找 SListNode* SListFind(SListNode* plist, SLTDateType x) { if (plist)//判断链表是否为空 { while (plist)//迭代查找 { if (((plist)->data)==x)//判断节点值是否等于x { return plist;//等于就返回节点地址 } plist = plist->next;//不等于指针向后走 } } return NULL;//如果没找到或者链表为空则返回NULL }
单链表在指定位置插入节点(数据)函数
单链表在指定位置插入函数是指定待插入节点位置,然后本函数会调用查找函数查找该函数的地址,查找失败则输出地址错误,查找成功则将新节点链入此节点之前,但是单链表没有前驱指针指向上一个节点,我们要将节点插入到另一个节点之前必须知道这个节点的前驱,所以我们还需要用迭代找指定位置节点的前驱。如果链表为空则直接执行头插函数(或尾插函数)。这里需要注意的是,头指针仍然需要传递二级指针,将指针变量的地址传递入函数,因为可能涉及头插,需要对头指针进行修改。
// 单链表在pos位置之后插入x void SListInsertAfter(SListNode** pplist,SListNode* pos, SLTDateType x) { if (*pplist)//判断是否为空表 { if (pos)//判断位置是否合法 { SListNode* newnode = BuySListNode(x);//申请新节点,data值为x SListNode* cur=*plist;//借助变量迭代找pos的前一个 while(cur != NULL && cur->next != pos)//如果cur为空指针或者找到了pos的前一个节点就停止 { cur = cur->next;//指针向后走 } newnode->next = cur->next;//新节点的next保存指定位置前驱的next cur->next = newnode;//指定位置前驱的next指向新节点 } else//位置不合法反馈 { printf("位置错误,插入失败!\n"); } } else//如果链表为空则指向头插(尾插) { SListPushFront(&(*pplist), x);//头插 //SListPushBack(&(*pplist), x);//尾插 } }
单链表指定节点删除(数据)函数
单链表的指定删除函数,与指定插入函数类似,也是需要调用查找函数并判断位置是否合法且链表不为空且找指定节点的前驱,而且需要对特殊情况进行特殊处理,这里我对两种特殊情况进行了特殊处理,如果删除的节点是头节点则调用头插函数,如果链表中只有一个节点则调用尾删函数(或者节点为尾节点),这样可以增强代码的复用性和减少代码量,然后使用临时变量保存将被删除的节点,让当前删除位节点的next域指向被删除节点的next域,完成链接和节点的剥离,最后free保存的节点即可!
单链表指定节点删除函数动图演示// 单链表删除pos位置的值 void SListEraseAfter(SListNode** pplist, SListNode* pos) { if (*pplist)//判断是否为空表 { if (pos)//判断位置是否合法 { if (*pplist == pos)//判断特殊位置(头插) { SListPopFront(&(*pplist));//SListPopFront(pplist); } else if ((pos->next) == NULL)//判断特殊位置(尾插) { SListPopBack(&(*pplist));//SListPopBack(pplist); } else { SListNode* cur = *pplist; while (cur != NULL && (cur->next)!= pos )//开始找被删节点的前驱 { cur = cur->next; } cur->next = pos->next; free(pos); } } else { printf("该值不存在!\n"); } } else { printf("空表!\n"); } }
单链表销毁函数
单链表销毁函数的实现是对链表进行迭代遍历,每次遍历free释放上一个节点的地址内存空间,直到指针走到NULL为止,然后置空头指针变量即可!这里需要注意的是要传入二级指针,因为需要对头指针变量进行修改!
// 单链表的销毁 void SListDestroy(SListNode** plist) { while (*plist)//迭代销毁 { SListNode* freenode = *plist;//保存当前节点 *plist = (*plist)->next;//*plist向后走 free(freenode);//释放前一个节点 } *plist = NULL;//最后置空头指针 }
单链表的优缺点
单链表的优点
1.插入和删除的链接操作效率非常高(不需要频繁的挪动数据)。
2.没有额外的空间浪费,“使用就申请,不用就释放”,空间利用率高。
单链表的缺点
1.对于查找来说效率非常低(例如尾插尾删找尾)。
2.不支持随机访问(访问一个节点必须遍历全链表查找)。
本次我们介绍了数据结构中的线性表的链式结构的一种,介绍了关于单链表增删查改等各种操作。相信到这里大家对单链表已经有了一定的认识,单链表在平时用到的还是非常多的,甚至还会配合顺序表一起使用。了解完单链表,那么大家对于另一种常用的链式结构“双向带头循环链表”一定充满了期待,下一篇我将为大家介绍双向带头循环链表,敬请期待!
本次线性表链式结构的知识分享就暂时先到这里啦,喜欢的读者可以多多点赞收藏和关注。
如果文章中有瑕疵,还请各位大佬细心点评和留言,我将立即修补错误,谢谢!
其他文章阅读推荐
数据结构初级<时间和空间复杂度>_ARMCSKGT的博客-CSDN博客
数据结构初级<线性表之顺序表>_ARMCSKGT的博客-CSDN博客
C语言初级<数组>_ARMCSKGT的博客-CSDN博客
欢迎读者多多浏览多多支持!