目录
1. 链表的概念和结构
2.单链表的实现
1.创建结构体变量
2. 实现头插(在链表开始的位置插入数据)
2.1 动态申请节点
3.实现尾插(在链表的末尾插入数据)
4.实现头删(删除链表第一个元素)
5. 实现尾删
6.在链表中查找
7.在pos 位置后插入一个 新节点。
8. 在pos位置前,插入一个节点
9. 打印链表
10. 销毁链表
3. 总结 ,及单链表相关笔试题详解
4. 单链表功能实现源码分享
链表是一种物理存储结构上非连续,非顺序的储存结构。数据元素的逻辑顺序是通过链表中的指针链接次序来实现的。
在数据结构中,链表的结构非常多样,
1.单向,双向
2.带头,不带头
3.循环,非循环
以上的情况组合起来就有8种之多。
本篇博客主要介绍无头单向链表的分析及其功能的模拟实现。这种结构在笔试面试中考察的次数很多。
实现单链表,需要掌握的知识主要有,结构体的使用,指针的使用,动态内存开辟,这些在我前面的博客都有分析,需要复习一下的小伙伴可以自行观看。
单链表的的实现需要使用的接口有点多,所以我们为了代码的书写和调试更方便的角度考虑,创造3个部分,包括头文件的包含和函数的声明,函数的具体实现,主测试函数。三个部分。
在头文件SList.h 中创建结构变量,由于链表的数据元素逻辑顺序是通过指针链接顺序来实现的,因此结构体中的指针变量较为重要。
#include
#include // 断言,判断指针合法性
#include // 动态内存开辟
typedef int SLTDateType; // 重定义,后续需要更改链表数据类型较为方便
typedef struct SListNode
{
SLTDateType data;
struct SListNode* next; //储存下一个元素的地址
} SListNode;
先在主函数中,先创建一个空链表。
#include"SList.h"
int main()
{
SListNode* plist = NULL;
return 0;
}
在插入数据前,我们需要先动态申请一个节点。
// 增加数据
SListNode* BuySListNode(SLTDateType x)
{
SListNode* node = (SListNode*)malloc(sizeof(SListNode));
if (node != NULL) //判断是否申请成功
{
node->data = x;
node->next = NULL;
return node;
}
else
{
perror("buySListNode");
}
}
在申请节点后,我们只知道它数据的大小,并不清楚它后续的节点位置,所以我们选择将每一个申请的节点数据中的指针置空,具体使用时,再赋值。
我们可以通过这张图,了解单链表在内存中的存储,因为美观,上图看着好像单链表在内存中连续存储,但实际可能是
理解了这些以后,我们可以尝试实现头插功能。
将原链表的第一个数据的地址赋值给 需要插入的数据存储的地址,再将需要插入的数据的地址赋值给plist。
代码实现
SList.h (函数申明)
// 头插
void SListPushFront(SListNode** pp, SLTDateType x);
test.c (测试)
#include"SList.h"
int main()
{
SListNode* plist = NULL;
SListPushFront(&plist, 1);
return 0;
}
SList.c (函数实现)
void SListPushFront(SListNode** pp, SLTDateType x)
{
SListNode* node = BuySListNode(x);
if (*pp == NULL)
{
*pp = node;
}
else
{
node->next = *pp;
*pp = node;
}
}
由于,我们需要对 plist 更改,所以我们选择传址调用,由于plist 为一级指针,所以在函数中,我们使用二级指针来接收。(如果我们传值调用,形参只是实参的一份临时拷贝,对形参的修改不会影响实参)。
如果是一个空链表,我们只需要将创建的数据的地址直接赋给plist。
我们需要遍历整个链表,找到链表结束的位置,并且将最后一个元素储存的地址(结尾,此时地址为NULL)改为创建的新元素的地址。
void SListPushBack(SListNode** pp, SLTDateType x)
{
if (*pp == NULL)
{
*pp = BuySListNode(x);
}
else
{
SListNode* tail = *pp;
while (tail->next != NULL)
{
tail = tail->next;
}
tail->next = BuySListNode(x);
}
}
由于我们需要对地址进行解引用,因此,如果是个空链表,对空指针解引用会出现错误,我们需要先判空。
当tail->next 为NULL 时,代表了,tail这是链表的最后一个元素,我们将新元素的地址赋给tail->next ,即可完成尾插。
//头删
void SListPopFront(SListNode** pp)
{
if (*pp == NULL) // 空链表
return;
else
{
SListNode* cur = *pp;
(*pp) = (*pp)->next;
free(cur);
cur = NULL;
}
}
如果是一个空链表,我们对一个空指针解引用操作就会报错,我们先要判断链表是否为空,为空则直接返回。
创建变量tail ,进入循环,当tail->next 为NULL 时,tail 就是我们需要找到的最后一个节点。
同时,我们需要找到tail 前一个节点(prev),将其存储的地址置成空。
但是如果链表为空,或者链表只有一个节点,我们就找不到尾删最后一个节点的前一个节点。这种就需要分开讨论。
我们在创建变量名时,最好使用英文缩写,这样更有意义,不至于后期查看代码时,忘记变量的意义
//尾删
void SListPopBack(SListNode** pp)
{
if (*pp == NULL) // 空链表
{
return;
}
else if ((*pp)->next == NULL) // 一个节点
{
free(*pp);
*pp = NULL;
}
else
{ // 多个节点
SListNode* prev = NULL;
SListNode* tail = *pp;
while (tail->next != NULL)
{
prev = tail;
tail = tail->next;
}
free(tail);
tail = NULL;
prev->next = NULL;
}
}
这个函数较简单,遍历一遍链表,判断值是否相等,返回地址,注意其返回值的类型
//单链表查找
SListNode* SListFind(SListNode* p, SLTDateType x)
{
while (p != NULL)
{
if (p->data == x)
{
printf("找到了\n");
return p;
break;
}
else
{
p = p->next;
}
}
printf("没找到\n");
return;
}
查找函数对参数没有改变,我们可以直接传值。
基本思路:将pos->next 的值 赋给 newnode(新创建的节点)->next , 再将pos->next 赋值为newnode地址。
将pos指向的旧节点位置赋给新创建的节点,然后再将新节点的位置赋给pos ,这样就实现了 链式访问。
但是这只是最普通的情况,我们在设计程序时,还应该考虑到多种情况。
1. pos 地址不合法 (assert断言)
2. pos位置处于链表的尾端(相当于尾插)
//在pos后,增加数据
void SListInsertAfter(SListNode*pos, SLTDateType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
SListNode* tmp = pos->next;
pos->next = newnode;
newnode->next = tmp;
}
基本思路,遍历链表,找到pos 位置 前的一个节点,再插入新节点。想对于pos位置后插入新节点,pos位置前更加复杂,需要遍历链表,需要做出的判断也更多。
需要注意的点
1. pos的合法性
2. pos如果是第一个节点,那么就不存在前一个节点 prev 。
// 在pos 位置前,加入一个数据
void SListInsertBefore(SListNode** pp,SListNode *pos, SLTDateType x)
{
assert(pos);
SListNode* newnode = BuySListNode(x);
if (pos == *pp) // pos 是第一个节点
{
newnode->next = pos;
*pp = newnode;
}
else
{
SListNode* cur = *pp;
SListNode* prev = NULL;
while (cur != pos) // cur 为pos 位置时,循环结束
{
prev = cur; // prev 为 pos的前一个节点
cur = cur->next;
}
prev->next = newnode;
newnode->next = pos;
}
}
遍历链表,打印数值
void SlistPrintf(SListNode* p)
{
SListNode* cur = p;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL");
printf("\n");
}
由于链表的空间都是动态开辟的,我们在使用结束时,应该主动释放
//单链表的销毁
void SListDestroy(SListNode** pp)
{
SListNode* tmp = NULL;
while (*pp)
{
tmp = *pp;
*pp = (*pp)->next;
free(tmp);
}
}
单链表的销毁同样,会对参数进行改变,我们选择传址调用,遍历链表,一步一步的释放,不同顺序表,只需要释放一次,链表的每一个节点都是单独的内存空间,释放起来相对复杂。
在处理链表相关问题时,可以先构造一个简单的思路来适应大多数普通场景,然后根据这个思路来调整在特殊情境下的使用,要考虑好链表的头尾问题。很多的笔试题面试题,本质上其实都是在考察链表的基本功能,比如增删查改,只是他们的使用场景更加复杂,我们需要考虑的问题更多。
笔试题分析会在下一篇博客进行,敬请期待,先留个坑。
Single linked list/Single linked list · 斯文/Date Stucture - 码云 - 开源中国 (gitee.com)
本篇博客到此结束,谢谢观看。