【数据结构】C语言实现单链表万字详解(附完整运行代码)

🦄个人主页:修修修也

🎏所属专栏:数据结构

⚙️操作环境:Visual Studio 2022


一.了解项目功能

在本次项目中我们的目标是实现一个单链表:

单链表使用动态内存分配空间,可以用来存储任意数量的同类型数据.

单链表结点(Node)需要包含两个要素:数据域data,指针域next.

结点(Node)逻辑结构图示如下:

单链表提供的功能有:

  1. 单链表的初始化.
  2. 单链表的新节点创建.
  3. 单链表元素的尾插.
  4. 单链表元素的头插.
  5. 单链表的元素位置查找.
  6. 单链表的任意指定元素前插入.
  7. 单链表的任意指定位置后插入.
  8. 单链表的尾删.
  9. 单链表的头删.
  10. 单链表的任意指定元素删除.
  11. 单链表的指定元素后删除.
  12. 单链表打印.
  13. 单链表的元素位序查找.
  14. 单链表的销毁.

二.项目功能演示

要编写一个单链表项目,首先要明确我们想要达到的效果是什么样,下面我将用vs2022编译器来为大家演示一下单链表程序运行时的样子:

单链表的C语言实现


三.逐步实现项目功能模块及其逻辑详解

通过第二部分对项目功能的介绍,我们已经对单链表的功能有了大致的了解,虽然看似需要实现的功能很多,貌似一时间不知该如何下手,但我们可以分步分模块来分析这个项目的流程,最后再将各部分进行整合,所以大家不用担心,跟着我一步一步分析吧!


!!!注意,该部分的代码只是为了详细介绍某一部分的项目实现逻辑,故可能会删减一些与该部分不相关的代码以便大家理解,需要查看或拷贝完整详细代码的朋友可以移步本文第四部分。


1.实现单链表程序菜单

菜单部分的逻辑比较简单,就是利用C语言printf函数打印出这个菜单界面即可。但要注意菜单的标序要和后续switch...case语句的分支相应,以免导致后续执行语句错乱的问题.基础问题就不过多赘述了,代码如下:

该部分功能实现代码如下: 

//菜单
void SLTMenu()
{printf("************************************\n");printf("******请选择要进行的操作      ******\n");printf("******1.单链表尾插            ******\n");printf("******2.单链表头插            ******\n");printf("******3.单链表指定元素前插入  ******\n");printf("******4.单链表指定元素后插入  ******\n");printf("******5.单链表尾删            ******\n");printf("******6.单链表头删            ******\n");printf("******7.单链表指定元素删除    ******\n");printf("******8.单链表指定元素后删除  ******\n");printf("******9.单链表打印            ******\n");printf("******10.单链表元素查找       ******\n");printf("******11.单链表销毁           ******\n");printf("******0.退出单链表程序        ******\n");printf("************************************\n");printf("请选择:>");
}

2.实现单链表程序功能可循环使用

由于我们要实现单链表的功能可以反复使用的逻辑,因此选择do...while的循环语句来实现这一部分的逻辑.

该部分功能实现代码如下: 

// 导入SLTNode结构体的定义
int main()
{SLTNode* plist=NULL; // 定义一个单链表的头指针,并初始化为NULLint swi = 0; // 创建变量swi作为do...while循环的终止条件,以及switch语句的运行条件do // 使用do...while实现单链表可循环使用{SLTMenu(); // 调用SLTMenu函数,显示菜单scanf("%d", &swi); // 从标准输入读取用户输入的选项switch (swi) // 根据用户选择的选项执行相应的操作{case 0: // 退出程序SLTDestroy(&plist); // 销毁单链表printf("您已退出程序:>\n");// 释放链表内存break;case 1: // 尾插数据printf("请输入要尾插的数据:>");SLTDataType pushback_data = 0;scanf("%d", &pushback_data);SLTPushBack(&plist, pushback_data); // 调用尾插函数printf("已成功插入:>\n");break;case 2: // 头插数据printf("请输入要头插的数据:>");SLTDataType pushfront_data = 0;scanf("%d", &pushfront_data);SLTPushFront(&plist, pushfront_data); // 调用头插函数printf("已成功插入:>\n");break;case 3: // 在指定位置前插入数据printf("请输入要插入的数据:>");SLTDataType insert_data = 0;scanf("%d", &insert_data);printf("请输入要插入的位置上的数据:>");SLTDataType insert_posdata = 0;scanf("%d", &insert_posdata);SLTNode* insert_pos = SLTFind(plist, insert_posdata); // 查找插入位置SLTInsert(&plist, insert_pos, insert_data); // 调用插入函数printf("已成功在'%d'数据前插入'%d':>\n", insert_posdata, insert_data);break;case 4: //在指定位置后插入数据printf("请输入要插入的数据:>");SLTDataType insertafter_data = 0;scanf("%d", &insertafter_data);printf("请输入要插入的位置上的数据:>");SLTDataType insertafter_posdata = 0;scanf("%d", &insertafter_posdata);SLTNode* insertafter_pos = SLTFind(plist, insertafter_posdata);if (insertafter_pos == NULL){printf("该元素不存在,无法插入:<\n");}else{SLTInsertAfter(insertafter_pos, insertafter_data);printf("已成功在'%d'数据后插入'%d':>\n", insertafter_posdata, insertafter_data);}break;case 5://单链表尾删SLTPopBack(&plist);break;case 6://单链表头删SLTPopFront(&plist);break;case 7:    //单链表删除指定元素printf("请输入要删除的数据:>");SLTDataType erase_data = 0;scanf("%d", &erase_data);SLTNode* erase_pos = SLTFind(plist, erase_data);if (erase_pos == NULL){printf("该元素不存在:<\n");}else{SLTErase(&plist, erase_pos);printf("已成功删除:>\n");}break;case 8:      //单链表删除指定元素后元素printf("请输入要删除数据的前一个数据:>");SLTDataType eraseafter_data = 0;scanf("%d", &eraseafter_data);SLTNode* eraseafter_pos = SLTFind(plist, eraseafter_data);if (eraseafter_pos == NULL){printf("该元素不存在:<\n");}else{SLTEraseAfter(eraseafter_pos);printf("已成功删除:>\n");}break;case 9:   //单链表打印printf("打印单链表:>\n");SLTPrint(plist);break;case 10:   //单链表元素位序查找printf("请输入要查找的单链表元素:>");SLTDataType find_data = 0;scanf("%d", &find_data);int find_pos = SLTFind_NO(plist, find_data);if (find_pos != -1){printf("元素%d在单链表的第%d个\n", find_data, find_pos);}else{printf("没找到该元素:<\n");}break;case 11:    //单链表销毁SLTDestroy(&plist);printf("单链表销毁成功:>\n");break;default: // 输入错误printf("输入错误,请重新输入\n");break;}} while (swi); // 循环条件return 0;
}

3.创建单链表

创建单链表成员的结构体应该包括:存储数据的数据域data,以及存储下一个结点地址的指针域next.

图示如下:

因此我们创建SListNode结构体类型时应由一个数据成员类型及一个指向该结构体的结构体指针组成.

这里的第一行使用的typedef类定义的作用方便我们后续在使用单链表时对存储的数据类型做更改,比如后续我们的链表不想存储int类型数据了,就可以很方便的在这里对单链表数据域的存储数据类型做更改.比如改成char类型,或者double类型,甚至改成任意自己构造的结构类型.

在之前的实战项目通讯录中,我们就创建过类似的自定义结构体,如下图:

综上,该部分代码如下:

typedef int SLTDataType;typedef struct SListNode    //对SlistNode类型进行重命名,方便后续操作
{SLTDataType data;struct SListNode* next;  //typedef在7行之后才起作用.
}SLTNode;

4.初始化单链表

初始化单链表部分的逻辑与之前顺序表以及通讯录不同.

在初始化顺序表部分,我们是实打实申请了一块空间以备后续使用,因此需要对这块空间进行初始化.

而在单链表部分,我们是需要插入数据时才会创建结点,因此结点空间在开辟时就会被使用,这样也就不需要初始化空间这个动作了.

因此,在初始化单链表部分,我们只需要创建一个头指针,将它初始化为NULL或让它指向头结点即可:

空链表:头指针无头结点示意图:

空链表:头指针有头结点示意图:

在本次项目中,我们采用的是无头结点指针链表,因此在初始化的时候只需要开辟一个头指针并将它置为NULL即可.

该部分功能实现代码如下: 

SLTNode* plist=NULL;

5.单链表的新节点创建

因为后续我们单链表尾插,头插等插入操作时都需要先创建一个新结点,为了使代码的利用效率变高,我们不如将创建新节点这一操作封装成一个函数,后续当需要创建新节点时,直接调用该函数即可.

函数的参数需要接收新结点的数据域,至于新结点的指针域,在我们不清楚新结点的用途时,直接将其置为NULL即可.

该部分功能实现代码如下: 

//开辟新结点
SLTNode* BuySLTNode(SLTDataType x)   //定义函数BuySLTNode,参数为SLTDataType类型的变量x
{//使用malloc开辟新结点//声明一个SLTNode类型的指针newnode,并使用malloc动态分配内存空间,大小为SLTNode结构体的大小SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail::"); //如果newnode为空,输出"malloc fail::",提示malloc开辟空间出错return NULL;//并返回一个空指针}//将newnode指向的结点的data成员赋值为xnewnode->data = x;//将newnode指向的结点的next成员赋值为NULLnewnode->next = NULL;//返回newnode结点指针return newnode;
}

6.单链表元素的尾插

在单链表尾插部分,我们要特别注意理解:函数形参的改变不影响实参.

因此在函数内想改变SLTNode类型的实参需要传SLTNode*的指针,

而在函数内想改变的是尾结点的指针域SLTNode*类型的实参需要传SLTNode**的二级指针.

尾插的逻辑为:

判断单链表是不是空链表,

如果,直接让头指针指向newnode即可.

如果不是,则需要先找尾,再将newnode的地址链接到尾结点的指针域.

 尾插逻辑示意图:

该部分功能实现代码如下: 

//链表尾插
//形参的改变,不影响实参
void SLTPushBack(SLTNode** pphead, SLTDataType x)//不能断言,链表为空也可插入数据
{assert(pphead);//因为pphead是plist的地址,所以绝对不是空的,但是,防止有传参时传错的现象,所以断言一下//创建新节点SLTNode* newnode = BuySLTNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}//链接tail->next = newnode;}
}

7.单链表元素的头插

头插我们同样分为两种情况来看:

一种是当单链表为空时,另一种是当单链表不为空时.

通过观察分析我们可以发现,这两种情况下,单链表的插入逻辑都是相同的:

即,先让newnode的指针域指向原来头指针pphead指向的内容(结点或NULL),然后让头指针pphead指向newnode即可.

头插逻辑示意图:

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//创建新结点SLTNode* newnode = BuySLTNode(x);//先将newnode的next指向首结点newnode->next = *pphead;//再将pphead指向newnode*pphead = newnode;}

8.单链表的元素位置查找

因为后续我们要使用的单链表按位插入和按位删除需要知道用户传入的链表元素在链表中的位置在哪,因此我们把查找链表元素位置的操作封装成一个单独的函数,后续需要查找某一链表元素的位置直接调用这个函数就行.

注意,查找只需要遍历链表,而不需要改变链表内容,因此我们传给函数链表的一级头指针即可,函数的参数还应该接收待查找的结点的数据域,以便我们在遍历链表的过程中能够找到它.

函数的返回值是链表结点指针型(SLTNode*),这样可以方便我们在找到要查找的指针后直接对齐进行相关操作,而不需要再在函数外再遍历一遍链表了.

该部分功能实现代码如下: 

//单链表的元素位置查找
// 定义函数SLTFind,参数为SLTNode类型的指针phead和SLTDataType类型的变量x
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{// 声明一个SLTNode类型的指针cur,初始化为pheadSLTNode* cur = phead;// 当cur不为空时进入循环while (cur){// 如果cur指向的结点的data成员等于xif (cur->data == x){// 返回cur指针return cur;}// 将cur指向下一个结点cur = cur->next;}// 循环结束后,如果未找到匹配的结点,返回空指针return NULL;
}

9.单链表的任意指定元素前插入

在指定元素前插入函数中我们需要三个参数,一个是头指针的地址,一个是指定元素的地址,一个是新结点的数据域的数据值.

在插入时我们会遇到两种情况:

一种是pos指针指向首结点,这种情况下函数的插入逻辑是和头插相同的,因此我们可以直接调用头插函数来实现该操作.

示意图:

 还有一种情况是当pos不指向首结点时,我们的链接逻辑是:

  1. 先让newnode的指针域链接到pos指针指向的位置.
  2. 再找到pos前一个结点的指针域,将其指向newnode.

注意,当传入的pos指针为NULL,不能认为此时相当于单链表的尾插执行尾插逻辑.

因为这里pos为空的原因还可能是因为SLTFind函数根本没有在单链表中找到指定插入元素的位置.因此传回了一个代表没有找到该元素的NULL指针.

所以如果遇到pos为NULL的情况我们直接断言报错即可,不需要加入判断的操作为别的函数传入的错误指针而买单了.

该部分功能实现代码如下: 

//pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);//pos为NULL不一定是尾插!并且我们不要在函数内去判断pos为NULL是不是尾插//每个函数只要完成自己分内的工作即可,不需要为别人可能出现的错误买单!if (pos == *pphead)//当pos位置与头指针相等时,相当于头插{SLTPushFront(pphead, x);}else{//找到pos的前一个位置SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//创建新节点SLTNode* newnode = BuySLTNode(x);//链接prev->next = newnode;newnode->next = pos;}
}

10.单链表的任意指定位置后插入.

在pos位置后插入的逻辑比在pos位置前插入简单,我们甚至不需要链表头指针遍历链表,只需要pos位置的指针就可以访问pos结点的指针域然后修改其相关值了.

该部分操作示意图:

在这里我们的链接逻辑同样是让newnode连接上pos结点的指针域指向的内容(下一结点/NULL),将pos结点的指针域指向newnode即可.

该部分功能实现代码如下: 

//pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);//pos为NULL,无法进行插入操作,因此直接让assert报错SLTNode* newnode = BuySLTNode(x);//先让newnode连上后面,再让前面连上newnode.不然就断了newnode->next = pos->next;pos->next = newnode;}

11.单链表的尾删

尾删分为三种情况,对这三种情况我们要分别处理:

 

如果不判断单链表只有一个结点时的情况二,按照有多个节点的逻辑操作,会导致在只有一个结点的情况下出现空指针访问的问题

在这种情况下,如果直接执行 (*pphead)->next会出现空指针访问导致程序崩溃

因此,在进行尾删操作之前需要先判断单链表是否只有一个结点,如果只有一个结点,则需要特殊处理而不是按照有多个节点的逻辑操作

其中情况三是我们需要特别注意的,在找到尾后,我们要使用一个指针记录下尾结点的前一个结点的地址,因为在释放尾结点后,我们还需要将它的前一个结点的指针域置为空.

该部分功能实现代码如下: 

void SLTPopBack(SLTNode** pphead)
{assert(pphead);//如果pphead为空,头指针不存在,则链表是不存在的//如果*pphead为空,代表头指针存在,但首结点不存在,链表没法继续删除if (*pphead == NULL){printf("链表为空,无法进行尾删:<\n");return;}//只有一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else//多个结点{//找尾SLTNode* tail = *pphead;SLTNode* prev = NULL;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;}printf("已成功尾删数据:>\n");
}

12.单链表的头删

头删也有三种情况,我们分别画图看一下:

通过对三种情况的分析,我们发现情况二和情况三可以归为一种情况处理,并且在头删部分不会出现和尾删一样的对空指针的解引用操作,所以我们只需要对情况一作单独处理就行.

该部分功能实现代码如下: 

// 从链表头部删除结点
// 定义函数SLTPopFront,参数为指向指针的指针pphead
void SLTPopFront(SLTNode** pphead)
{assert(pphead);   // 断言pphead不为空if (*pphead == NULL)   // 如果链表为空{printf("链表为空,无法进行头删:<\n");// 打印提示信息return;        // 返回}SLTNode* first = *pphead; // 定义指针first指向头结点*pphead = first->next; // 将头结点指向下一个结点free(first); // 释放first指向的内存first= NULL; // 将first置为空printf("已成功头删:>\n"); // 打印成功提示信息}

13.单链表的任意指定元素删除

既然要删除单链表中的某一元素,那么前提是这个元素要存在于单链表中,因此pos不能为NULL.

pos指针指向的位置和头指针指向的位置相同时,则相当于头删,执行头删逻辑即可.

函数逻辑示意图:

该部分功能实现代码如下:

// 从链表中删除指定位置的结点 
// 定义函数SLTErase,参数为指向指针的指针pphead和要删除的结点位置pos void SLTErase(SLTNode** pphead, SLTNode* pos) 
{ assert(pphead); // 断言pphead不为空 assert(pos); // 断言pos不为空 if (pphead == pos) // 如果要删除的位置是头结点 { SLTPopFront(pphead); // 调用SLTPopFront删除头结点 }else {    // 找到pos的前一个位置 SLTNode prev = *pphead; // 定义指针prev指向头结点 while (prev->next != pos) // 当prev的下一个结点不等于pos时 { prev = prev->next; // prev指向下一个结点 }prev->next = pos->next; // 将prev的下一个结点指向pos的下一个结点free(pos); // 释放pos指向的内存}
}

14.单链表的指定元素后删除

我们要删除pos结点后一个结点,只需要一个参数,即pos的位置.

因为删除pos结点后的结点,需要更改的指针域就是pos的指针域,而pos的指针域我们拿pos指针就可以访问,所以也就不需要链表的头指针来遍历链表了.

该部分功能实现代码如下: 

// 定义函数SLTEraseAfter,参数为SLTNode类型的指针pos,用于删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{// 断言pos不为空,确保pos指向的节点存在assert(pos);// 断言pos的下一个节点不为空,确保pos之后还有节点assert(pos->next);// 声明一个SLTNode类型的指针del,指向pos的下一个节点SLTNode* del = pos->next;// 将pos的next指针指向del的下一个节点pos->next = del->next;// 释放del指向的节点的内存free(del);// 将del指针置为NULL,防止出现悬空指针del = NULL;
}

15.单链表打印

单链表的打印逻辑很简单,顺着头指针向后循环遍历打印整个链表结点的数据域即可,当结点的指针域为空时,则代表已经遍历打印完链表的所有元素,这时跳出循环即可.

该部分功能实现代码如下: 

// 打印链表 
void SLTPrint(SLTNode* phead) // 定义函数SLTPrint,参数为指向链表头结点的指针phead
{//不用断言phead,因为phead为空不代表链表为空SLTNode* cur = phead; // 定义指针cur指向phead while (cur != NULL) // 当cur不为空时 { printf("%d->", cur->data); // 打印当前结点的数据 cur = cur->next; //使cur指向下一个结点 } printf("NULL\n"); // 打印NULL表示链表结束 
}

16.单链表的元素位序查找

元素位序的查找和元素位置的查找的区别是:

  1. 位序查找需要返回元素在链表中的第几个,因此返回值是int,而位置查找需要返回元素的地址,因此返回值是结构体指针(SLTNode*).
  2. 位序查找在遍历链表查找时需要一个计数器来记录链表当前遍历到第几个元素,而位置查找则不需要计数器来记录,只需要在找到时返回该元素的地址即可.

该部分功能实现代码如下: 

// 根据值查找结点位序
int SLTFind_NO(SLTNode* phead, SLTDataType x) // 定义函数SLTFind_NO,参数为指向链表头结点的指针phead和要查找的值x
{SLTNode* cur = phead; // 定义指针cur指向pheadint count = 1; // 初始化计数器count为1while (cur) // 当cur不为空时{if (cur->data == x) // 如果当前结点的数据等于x{return count; // 返回当前位置}count++; // 计数器加1cur = cur->next; // 移动到下一个结点}return -1; // 如果未找到,返回-1
}

17.单链表的销毁

当我们使用完单链表想要退出程序时,就应该将之前动态开辟的内存释放掉,还给操作系统.即销毁单链表操作.

我们使用free()函数将前面开辟的结点的内存逐一释放,释放完将头指针置为空即可.

 该部分功能实现代码如下:

// 单链表的销毁 
void SLTDestroy(SLTNode** pphead) // 定义函数SLTDestroy,参数为指向指针的指针pphead 
{ assert(pphead); // 断言pphead不为空 SLTNode* cur = *pphead; // 定义指针cur指向pphead所指向的地址while (cur != NULL) // 当cur不为空时{SLTNode* prev = cur->next; // 定义指针prev指向cur的下一个结点free(cur); // 释放cur指向的内存cur = prev; // cur指向下一个结点}*pphead = NULL; // 将pphead指向的地址置为空
}

四.项目完整代码

我们将程序运行的代码分别在三个工程文件中编辑,完整代码如下:

test.c文件

#include"SList.h"int main()
{SLTNode* plist=NULL;int swi = 0;//创建变量swi作为do...while循环的终止条件,以及switch语句的运行条件do          //使用do...while实现{SLTMenu();scanf("%d", &swi);switch (swi){case 0:SLTDestroy(&plist);printf("您已退出程序:>\n");// 释放链表内存break;case 1:printf("请输入要尾插的数据:>");SLTDataType pushback_data = 0;scanf("%d", &pushback_data);SLTPushBack(&plist, pushback_data);printf("已成功插入:>\n");break;case 2:printf("请输入要头插的数据:>");SLTDataType pushfront_data = 0;scanf("%d", &pushfront_data);SLTPushFront(&plist, pushfront_data);printf("已成功插入:>\n");break;case 3:printf("请输入要插入的数据:>");SLTDataType insert_data = 0;scanf("%d", &insert_data);printf("请输入要插入的位置上的数据:>");SLTDataType insert_posdata = 0;scanf("%d", &insert_posdata);SLTNode* insert_pos = SLTFind(plist, insert_posdata);SLTInsert(&plist, insert_pos, insert_data);printf("已成功在'%d'数据前插入'%d':>\n", insert_posdata, insert_data);break;case 4:printf("请输入要插入的数据:>");SLTDataType insertafter_data = 0;scanf("%d", &insertafter_data);printf("请输入要插入的位置上的数据:>");SLTDataType insertafter_posdata = 0;scanf("%d", &insertafter_posdata);SLTNode* insertafter_pos = SLTFind(plist, insertafter_posdata);if (insertafter_pos == NULL){printf("该元素不存在,无法插入:<\n");}else{SLTInsertAfter(insertafter_pos, insertafter_data);printf("已成功在'%d'数据后插入'%d':>\n", insertafter_posdata, insertafter_data);}break;case 5:SLTPopBack(&plist);break;case 6:SLTPopFront(&plist);break;case 7:printf("请输入要删除的数据:>");SLTDataType erase_data = 0;scanf("%d", &erase_data);SLTNode* erase_pos = SLTFind(plist, erase_data);if (erase_pos == NULL){printf("该元素不存在:<\n");}else{SLTErase(&plist, erase_pos);printf("已成功删除:>\n");}break;case 8:printf("请输入要删除数据的前一个数据:>");SLTDataType eraseafter_data = 0;scanf("%d", &eraseafter_data);SLTNode* eraseafter_pos = SLTFind(plist, eraseafter_data);if (eraseafter_pos == NULL){printf("该元素不存在:<\n");}else{SLTEraseAfter(eraseafter_pos);printf("已成功删除:>\n");}break;case 9:printf("打印单链表:>\n");SLTPrint(plist);break;case 10:printf("请输入要查找的单链表元素:>");SLTDataType find_data = 0;scanf("%d", &find_data);int find_pos = SLTFind_NO(plist, find_data);if (find_pos != -1){printf("元素%d在单链表的第%d个\n", find_data, find_pos);}else{printf("没找到该元素:<\n");}break;case 11:SLTDestroy(&plist);printf("单链表销毁成功:>\n");break;default:printf("输入错误,请重新输入\n");break;}} while (swi);return 0;
}

SList.h文件

#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>#include<stdio.h>typedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;//typedef在15行之后才起作用.
}SLTNode;void SLTMenu();void SLTPrint(SLTNode* phead);//不改变指针,就不传二级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);
//pos之前插入
void SLTInsert(SLTNode** pphead,SLTNode* pos, SLTDataType x);
//pos位置删除
void SLTErase(SLTNode** pphead,SLTNode* pos);//pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//pos位置后面删除
void SLTEraseAfter(SLTNode* pos);//单链表查找位序
int SLTFind_NO(SLTNode* phead, SLTDataType x);//单链表的销毁
void SLTDestroy(SLTNode** pphead);

SList.c文件

#include"SList.h"//菜单
void SLTMenu()
{printf("************************************\n");printf("******请选择要进行的操作      ******\n");printf("******1.单链表尾插            ******\n");printf("******2.单链表头插            ******\n");printf("******3.单链表指定元素前插入  ******\n");printf("******4.单链表指定元素后插入  ******\n");printf("******5.单链表尾删            ******\n");printf("******6.单链表头删            ******\n");printf("******7.单链表指定元素删除    ******\n");printf("******8.单链表指定元素后删除  ******\n");printf("******9.单链表打印            ******\n");printf("******10.单链表元素查找       ******\n");printf("******11.单链表销毁           ******\n");printf("******0.退出单链表程序        ******\n");printf("************************************\n");printf("请选择:>");
}SLTNode* BuySLTNode(SLTDataType x)
{//开辟新结点SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));if (newnode == NULL){perror("malloc fail::");return NULL;}//赋值newnode->data = x;newnode->next = NULL;return newnode;
}//打印链表
void SLTPrint(SLTNode* phead)//不用断言phead,因为phead为空不代表链表为空
{SLTNode* cur = phead;while (cur != NULL){printf("%d->", cur->data);cur = cur->next;    //使cur指向下一个结点}printf("NULL\n");}//链表尾插
//形参的改变,不影响实参
void SLTPushBack(SLTNode** pphead, SLTDataType x)//不能断言,链表为空可插
{assert(pphead);//因为pphead是plist的地址,所以绝对不是空的,但是,防止有传参时传错的现象,所以断言一下//创建新节点SLTNode* newnode = BuySLTNode(x);if (*pphead == NULL){*pphead = newnode;}else{//找尾SLTNode* tail = *pphead;while (tail->next != NULL){tail = tail->next;}//链接tail->next = newnode;}
}//单链表头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{assert(pphead);//创建新结点SLTNode* newnode = BuySLTNode(x);//先将newnode的next指向首结点newnode->next = *pphead;//再将phead指向newnode*pphead = newnode;}//单链表尾删
void SLTPopBack(SLTNode** pphead)
{assert(pphead);//如果链表为空,温柔检查一下,暴力检查assertif (*pphead == NULL){printf("链表为空,无法进行尾删:<\n");return;}//只有一个节点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else//多个结点{//找尾SLTNode* tail = *pphead;SLTNode* prev = NULL;while (tail->next != NULL){prev = tail;tail = tail->next;}free(tail);tail = NULL;prev->next = NULL;}printf("已成功尾删数据:>\n");
}//单链表头删
void SLTPopFront(SLTNode** pphead)
{assert(pphead);if (*pphead == NULL){printf("链表为空,无法进行头删:<\n");return;}SLTNode* first = *pphead;*pphead = first->next;free(first);first= NULL;printf("已成功头删:>\n");}//单链表查找元素地址
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{SLTNode* cur = phead;while (cur){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}//单链表查找位序
int SLTFind_NO(SLTNode* phead, SLTDataType x)
{SLTNode* cur = phead;int count = 1;while (cur){if (cur->data == x){return count;}count++;cur = cur->next;}return -1;
}//pos之前插入
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{assert(pphead);assert(pos);//pos为NULL不一定是尾插!别为别人的错误买单!if (pos == *pphead)//相当于头插{SLTPushFront(pphead, x);}else{//找到pos的前一个位置SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}//创建新节点SLTNode* newnode = BuySLTNode(x);//链接prev->next = newnode;newnode->next = pos;}
}//pos位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos)
{assert(pphead);assert(pos);if (*pphead == pos){SLTPopFront(pphead);}else{//找到pos的前一个位置SLTNode* prev = *pphead;while (prev->next != pos){prev = prev->next;}prev->next = pos->next;free(pos);}}//pos后面插入
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{assert(pos);SLTNode* newnode = BuySLTNode(x);//先让newnode连上后面,再让前面连上newnode.不然就断了newnode->next = pos->next;pos->next = newnode;}//pos位置后面删除
void SLTEraseAfter(SLTNode* pos)
{assert(pos);assert(pos->next);SLTNode* del = pos->next;pos->next = del->next;free(del);del = NULL;
}//单链表的销毁
void SLTDestroy(SLTNode** pphead)
{assert(pphead);SLTNode* cur = *pphead;while (cur != NULL){SLTNode* prev = cur->next;free(cur);cur = prev;}*pphead = NULL;
}

结语

希望这篇顺序表的实现详解能对大家有所帮助,欢迎大佬们留言或私信与我交流.

学海漫浩浩,我亦苦作舟!关注我,大家一起学习,一起进步!

相关文章推荐

【数据结构】什么是线性表?

【数据结构】线性表的链式存储结构

【C语言】memcpy()函数

【C语言】malloc()函数详解(动态内存开辟函数)

【C语言】free()函数详解(动态内存释放函数)

【C语言实战项目】通讯录(动态增容版)

【C语言】memset()函数

【实用编程技巧】不想改bug?初学者必须学会使用的报错函数assert!(断言函数详解)



数据结构线性表篇思维导图:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/188224.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode2834. 找出美丽数组的最小和

Every day a Leetcode 题目来源&#xff1a;2834. 找出美丽数组的最小和 解法1&#xff1a;贪心 从最小正整数 1 开始枚举&#xff0c;设当前数为 num&#xff0c;如果 nums 里没有 target - num&#xff0c;就说明可以添加 num&#xff0c;依次填满直到有 n 个数即可。 用…

【云栖2023】张治国:MaxCompute架构升级及开放性解读

简介&#xff1a; 本文根据2023云栖大会演讲实录整理而成&#xff0c;演讲信息如下 演讲人&#xff1a;张治国|阿里云智能计算平台研究员、阿里云MaxCompute负责人 演讲主题&#xff1a;MaxCompute架构升级及开放性解读 活动&#xff1a;2023云栖大会 MaxCompute发展经历了…

“目标值排列匹配“和“背包组合问题“的区别和leetcode例题详解

1 目标值排列匹配 1.1 从目标字符串的角度来看&#xff0c;LC139是一个排列问题&#xff0c;因为最终目标子串的各个字符的顺序是固定的&#xff1f; 当我们从目标字符串 s 的角度来看 LC139 “单词拆分” 问题&#xff0c;确实可以认为它涉及到排列的概念&#xff0c;但这种…

凯美瑞 vs 太空船:Web3 游戏生长的两条路径

撰文&#xff1a;Teng Yan&#xff08;0xPrismatic&#xff09;&#xff0c;Delphi Digital 研究员 编译&#xff1a;TinTinLand 来源&#xff1a;https://0xprismatic.substack.com/p/my-short-web3-gaming-thesis 经常有人问我关于 Web3 游戏的看法&#xff0c;所以我想以这…

思维模型 超限效应

本系列文章 主要是 分享 思维模型&#xff0c;涉及各个领域&#xff0c;重在提升认知。物极必反。 1 超限效应的应用 1.1 教育中的超限效应 一位老师在课堂上批评了一位学生&#xff0c;这位学生可能会因为老师的批评而感到沮丧和失落。如果老师在接下来的课程中继续批评这位…

Android拖放startDragAndDrop拖拽Glide加载堆叠圆角图,Kotlin(5)

Android拖放startDragAndDrop拖拽Glide加载堆叠圆角图&#xff0c;Kotlin&#xff08;5&#xff09; import android.content.ClipData import android.graphics.Canvas import android.graphics.Point import android.os.Bundle import android.util.Log import android.view.…

到底是什么是Python?语言的核心是什么?

文章目录 前言一、为什么提出python编程的核心是什么&#xff1f;二、Python需要REPL&#xff1f;三、Python的哪些部分需要被视为“Python”&#xff1f;四、需要多少兼容性才能有用&#xff1f;Python技术资源分享1、Python所有方向的学习路线2、学习软件3、精品书籍4、入门学…

SQL表、字段、查询参数获取

SQL工具类表、字段、查询参数提取 1. 执行效果2. 使用2.1 引入依赖2.2 相关实体2.3 工具类 1. 执行效果 2. 使用 2.1 引入依赖 <!-- sql 解析处理--><dependency><groupId>com.github.jsqlparser</groupId><artifactId>jsqlparser</artifact…

【教3妹学编程-算法题】2923. 找到冠军 I

3妹&#xff1a;2哥2哥&#xff0c;你看到新闻了吗&#xff1f;襄阳健桥医院院长 公然“贩卖出生证明”&#xff0c; 真是太胆大包天了吧。 2哥 : 我也看到新闻了&#xff0c;7人被采取刑事强制措施。 就应该好好查查他们&#xff0c; 一查到底&#xff01; 3妹&#xff1a;真的…

springboot定时服务

上一篇文章【修改定时时间&#xff0c;定时任务及时生效】 是定时任务与功能项目共用一个&#xff1b; 我目前所在公司的定时服务是专门有一个项目处理&#xff0c;然后定时查询库里面的定时信息配置。 话不多说&#xff0c;上程序 数据库设置 create table SCHEDULER_JOB…

语音识别与自然语言处理(NLP):技术前沿与未来趋势

语音识别与自然语言处理&#xff08;NLP&#xff09;&#xff1a;技术前沿与未来趋势 随着科技的快速发展&#xff0c;语音识别与自然语言处理&#xff08;NLP&#xff09;技术逐渐成为人工智能领域的研究热点。这两项技术的结合&#xff0c;使得机器能够更好地理解和处理人类语…

自主开发刷题应用网站H5源码(无需后端无需数据库)

该应用使用JSON作为题库的存储方式&#xff0c;层次清晰、结构简单易懂。 配套的word模板和模板到JSON转换工具可供使用&#xff0c;方便将题库从word格式转换为JSON格式。 四种刷题模式包括顺序刷题、乱序刷题、错题模式和背题模式&#xff0c;可以根据自己的需求选择适合的模…

arm2 day4

汇编编写流水灯 代码&#xff1a; LED流水灯现象&#xff1a;

浅谈高并发以及三大利器:缓存、限流和降级

引言 高并发背景 互联网行业迅速发展&#xff0c;用户量剧增&#xff0c;系统面临巨大的并发请求压力。 软件系统有三个追求&#xff1a;高性能、高并发、高可用&#xff0c;俗称三高。三者既有区别也有联系&#xff0c;门门道道很多&#xff0c;全面讨论需要三天三夜&#…

掌动智能性能压力测试优势有哪些

企业通过自动化的测试工具模拟多种正常、峰值以及异常负载条件来对系统的各项性能指标进行测试。本文将介绍性能压力测试的价值及主要优势! 一、性能压力测试的价值 1、评估系统能力&#xff1a;有助于参数的基准测试&#xff0c;可以度量系统的响应时间;还有助于检查系统是否可…

[工业自动化-12]:西门子S7-15xxx编程 - PLC从站 - ET200 SP系列详解

目录 一、概述 1.1 概述 二、系统组成 2.1 概述 2.2 与主站的通信接口模块 2.3 总线适配器 2.4 基座单元 2.5 电子模块 2.6 服务器模块 一、概述 1.1 概述 PLC ET200 SP 是西门子&#xff08;Siemens&#xff09;公司生产的一款模块化可编程逻辑控制器&#xff08;PL…

Linux输入与输出设备的管理

计算机系统中CPU 并不直接和设备打交道&#xff0c;它们中间有一个叫作设备控制器&#xff08;Device Control Unit&#xff09;的组件&#xff0c;例如硬盘有磁盘控制器、USB 有 USB 控制器、显示器有视频控制器等。这些控制器就像代理商一样&#xff0c;它们知道如何应对硬盘…

【MATLAB源码-第73期】基于matlab的OFDM-IM索引调制系统不同子载波数目误码率对比,对比OFDM系统。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 OFDM-IM索引调制技术是一种新型的无线通信技术&#xff0c;它将正交频分复用&#xff08;OFDM&#xff09;和索引调制&#xff08;IM&#xff09;相结合&#xff0c;以提高频谱效率和系统容量。OFDM-IM索引调制技术的基本思想…

嵌入式养成计划-52----ARM--开发板介绍--相关硬件基础内容介绍--GPIO讲解

一百三十一、开发板介绍 131.1 核心板介绍 131.2 拓展板 一百三十二、相关硬件基础内容介绍 132.1 PCB PCB&#xff08; Printed Circuit Board&#xff09;&#xff0c;中文名称为印制电路板&#xff0c;又称印刷线路板&#xff0c; 是重要的电子部件&#xff0c;是电子元器…

Direct3D粒子系统

粒子和点精灵 粒子(是种微小的物体,在数学上通常用点来表示其模型。所以显示粒子时,使用点图元(由 D3 DPRIMITIVETYPE类型的D3 DPT POINTLIST枚举常量表示)是一个很好的选择。但是光栅化时,点图元将被映射为一个单个像素。这样就无法为我们提供很大的灵活性,因为实际应用…