链表是相对顺序表来说复杂一些些的数据结构,但其与顺序表在性质上是存在一定的互补的,我们一起来随着这篇文章来看看。
首先来了解基本的链表概念
- 链表在逻辑结构上是连续的,物理结构上则为非连续的 链表也分为静态与动态链表
(只是静态链表如今已经不常用) - 链表本质就是数据元素之间通过指针相互链接了起来(故分为了数据域和指针域)
- 其次是链表的分类,链表根据是否带“哨兵卫”的头结点,是否可循环,是双向还是单向大体分为三大类,这三大类互相排列组合共能组合出八种链表结构。
本篇就带大家来看看“最简单”和“最复杂”的链表结构是如何实现的
最简单的即是:无头单向不循环的链表(也就是常见的单链表,类似于STL中的forward_list)
最复杂的即是:带头双向循环链表(也就是STL中的list容器)
八种链表结构中最常用的即这两种,这两种懂了之后其它形式的链表结构也是一通百通的
如果单链表您已理解,不妨可以再去看看双向带头循环链表->双向带头循环链表
单链表(singleList)
单链表的每个结点形式
typedef int SLTDataType; typedef struct SLNode { SLTDataType val; struct SLNode *next; }SLNode;
该结点内仅包含了一个数据(数据域),以及一个指针用于存储下一个结点的地址(指针域)tips:该单链表也是以存储整型数据来进行实现
在单链表处,我们不进行具体的链表初始化操作
我们仅在调用时创建一个“头指针”,用于存储头结点的地址(以后操作这个单链表时都可以通过头指针来操作)。如下:
//存放头结点的地址 SLNode *plist = nullptr;
需要注意的是我们创建的是一个指针,在进行函数传参时需要传递该指针的地址(也就是二级指针)才能真正的操控我们这里创建的这个头指针。(因为我们都知道,传参时,形参只是实参的一份临时拷贝,那在函数内部操作的仅是一个拷贝指针,出了作用域即被回收)
介绍完单链表的结点以及基本的起始阶段,接下来就是对单链表的增删查改功能的实现啦
在进行增加结点时,因为链表典型特征之一即何时用何时申请,故添加数据时都需要进行下述buy_newnode()的操作来获取新结点才可对链表进行数据插入
SLNode* buy_newnode(SLTDataType x) { SLNode *newnode = (SLNode*)malloc(sizeof(SLNode)); newnode->val = x; newnode->next = NULL; return newnode; }
紧接着便是添加数据了:push_back()
void push_back(SLNode** pplist,SLTDataType x) { assert(pplist); SLNode *newnode = buy_newnode(x); if(*pplist == NULL) *pplist = newnode; else { SLNode *tail = *pplist; while(tail->next) tail = tail->next; tail->next = newnode; } }
尾插数据时,需要先找到尾部,故此每次尾插的效率都为O(N),比较低下
tips:尾插数据时即要注意了,在链表内没有数据时,头结点还并未存在(nullptr的状态),此时要将要插入的结点作为头结点进行赋值,否则则会造成对空指针进行引用(紧接着程序崩溃~)
除了尾插数据,自然还有头插数据:push_front()
void push_front(SLNode** pplist,SLTDataType x) { assert(pplist); SLNode *newnode = buy_newnode(x); newnode->next = *pplist; *pplist = newnode; }
头插数据就比较容易了,效率也较高,因为我们时刻都把握着头结点,直接进行插入操作即可,然后记得将头结点更新即可。
插入了数据之后,即是应该实现删除数据的功能了
尾删数据:pop_back()
void pop_back(SLNode** pplist) { assert(pplist); if(*pplist == NULL) { printf("当前链表无数据!\n"); return ; } if((*pplist)->next == NULL) { free(*pplist); *pplist = NULL; } else { SLNode *cur = *pplist; SLNode *prev = NULL; while(cur->next) { prev = cur; cur = cur->next; } free(cur); prev->next = NULL; } }
尾删相对考虑的地方会更多一些
- 链表没有数据(头结点为空)
- 链表只有一个结点(只剩下了头结点)
- 普通情况(链表有一个以上的结点)
根据这三种情况我们要分别处理,如代码所示:
如果链表无结点则直接提示用户并返回即可;
如果链表只有头结点,我们直接free掉头指针并置空即可;
如果链表有两个及以上的结点时,则找到尾部数据的前一个再对尾部数据进行删除即可
头删数据:pop_front()
void pop_front(SLNode** pplist) { assert(pplist); if(*pplist == NULL) { printf("当前链表无数据!\n"); return ; } SLNode *next = (*pplist)->next; free(*pplist); *pplist = next; }
头删数据就比较简单了,只用考虑链表是否有数据即可。
最后就是查找以及修改了
查找数据:find()
SLNode* Find(SLNode** const pplist,SLTDataType x) { assert(pplist); SLNode *cur = *pplist; while(cur) { if(cur->val == x) return cur; cur = cur->next; } printf("未找到该数据!\n"); retun nullptr; }
比较简单,直接遍历整个单链表即可,若没找到则直接提示即可
修改数据:modify()
修改数据比较简单,即find找到了该结点直接进行修改即可,故不做代码展示了
然后还有随机插入以及随机删除
随机插入:insert()
void insert(SLNode** pplist,SLNode *pos,SLTDataType x) { assert(pplist); SLNode *newnode = buy_newnode(x); if(*pplist == pos) push_front(pplist,x); SLNode *cur = *pplist; SLNode *prev = NULL; while(cur != pos) { prev = cur; cur = cur->next; } prev->next = newnode; newnode->next = pos; }
插入也是属于添加数据,所以也是要先“buy_newnode()”获取新结点,然后找到要插入的位置,再将其连接上即可。
随机删除:erase()
void erase(SLNode** pplist,SLNode** pos) { assert(pplist); if(*pplist == *pos) pop_front(pplist); if(*pos == NULL) { printf("该结点不存在!\n"); return ; } SLNode *cur = *pplist; SLNode *prev = NULL; while(cur != *pos) { prev = cur; cur = cur->next; } prev->next = cur->next; free(*pos); *pos = NULL; }
删除时也是注意特殊情况:当链表没有数据时要进行判断即可,然后即是遍历找到该结点及其前端结点,然后前端连接后续,再free掉删除结点即可。
销毁链表:destroy()
void destroy(SLNode **pplist) { assert(pplist); if(*pplist == NULL) { printf("当前链表无数据!\n"); return ; } SLNode *cur = *pplist; SLNode *next = (*pplist)->next; while(next) { free(cur); cur = next; next = next->next; } *pplist = NULL; printf("销毁成功!\n"); }
由于结点都是动态开辟的,所以销毁链表时需要遍历到每一个结点进行释放才行(还是相对来说比较麻烦的)
到这里,模拟实现无头单向不循环链表就告一段落了,双向带头循环链表将在下一篇中为大家展现。如果本篇文章对你有帮助的话,请顺手点个赞以及收藏吧~
链表的下篇:双向带头循环链表