系列文章目录
[数据结构——顺序表的C语言代码实现
数据结构——八种链表的C语言代码实现
数据结构——栈的C语言代码实现
文章目录
前言
使用C语言实现单链表,锻炼模块化编程能力,加深对常见数据结构的认识!
一、基础知识
1.单链表的概念(Singly Linked List)
戳此处,百科概念
单链表便是使用链式的存储结构存放线性表,将逻辑上连续的数据存储在非连续的内存空间中,通过使用指针,达到非连续空间的逻辑连续,即我们只要能掌握头指针便可以拥有整个链表!
链表是以节点的形式来表达的。在单链表中,每个节点包含两个部分:数据域(Data)、指针域(Next)。数据域用于存放数据元素,指针域用于存放指向下一个节点的指针。
2.传值调用与传址调用
传值调用中形参是实参的一份物理拷贝,在函数内部改变形参并不会影响实参;而传址调用中传递的是实参的地址,通过解引用的方式,可以在函数内部达到改变实参的功能。
在构建接口函数时,以函数功能是否需要更改外部实参来判断采用哪种调用方式。
3.相较顺序表的优缺点
(1)优点:
1.按需分配内存,使用空间更为合理(与动态顺序表中的每次插入前都要判断是否需要开辟空间相比更为便捷,且顺序表的空间开辟有时会造成大量内存的浪费!);
2.在链表的头部或者中间插入、删除数据时,操作十分便捷,不需要移动原有的数据。
(2)缺点
1.每存放一个数据,便需要创建一个节点并存放一个同类型的指针,使用了过多的空间;
2.不支持随机访问(这也恰巧是顺序表最为重要的优势)。
二、代码实现
1.SLList.h
(1)引用函数库
#define _CRT_SECURE_NO_WARNINGS 1
#include<stdio.h>
#include<stdlib.h>
(2)定义单链表
注意使用struct 和 typedef 时,需要在语句末加上分号;
建议采用typedef的方式为数据类型取具有书面义的名字,便于统一更改链表中所存处数据的类型;
建议使用typedef为struct LListNode 取别名,便于书写,减少失误。
typedef int SLLDataType;
typedef struct SLListNode
{
SLLDataType data;
struct SLListNode* next;
}SLL;
(3)接口函数声明
仍然是最常见的接口函数:增、删、查、改
以及常用的函数:打印(检测),创建等。
//创建节点
SLL* SLLCreateNode(SLLDataType n);
//尾插
void SLLPushBack(SLL** pphead, SLLDataType n);
//尾删
void SLLPopBack(SLL** pphead);
//头插
void SLLPushFront(SLL** pphead, SLLDataType n);
//头删
void SLLPopFront(SLL** pphead);
//查找
SLL* SLLFind(SLL* phead, SLLDataType n);
//在指定数字后面插入
void SLLPushInsertBack(SLL* phead, SLLDataType n,SLLDataType insert);
//在指定数字前面插入
void SLLPushInsertFront(SLL** pphead, SLLDataType n,SLLDataType insert);
//指定删除某数字
void SLLPopDelete(SLL** pphead,SLLDataType n);
//更改
void SLLChange(SLL* phead, SLLDataType n,SLLDataType change);
//打印链表
void SLLPrint(SLL* phead);
//删除链表
void FreeSLL(SLL* phead);
2.SLList.c
实现各个接口函数
(1)创建节点
SLL* SLLCreateNode(SLLDataType n)
{
SLL* p = (SLL*)malloc(sizeof(SLL));
p->data = n;
p->next = NULL;
return p;
}
(2)尾插法
注意使用传址调用,因为实参是一级指针,而尾插法在链表为空时,会通过赋值改变头指针,即改变实参,所以必须使用传址调用,而一级指针的地址的存储便需要二级指针 !
实现尾插和尾删时,可以通过设立全局变量——尾指针,使该指针永远指向尾节点,这样在实现这两个接口函数时,不需要进行遍历寻找尾节点!
//尾插法
void SLLPushBack(SLL** pphead, SLLDataType n)
{
SLL* tem = SLLCreateNode(n);
tem->data = n;
tem->next = NULL;
//链表为空
if (*pphead == NULL)
{
//将tem赋值到头指针
//这也是尾插法传二级指针的原因
//主函数中创建的是一级指针,当接口函数需要改变头指针时,
//所传的参数就必须是二级指针,即传址!
*pphead = tem;
}
else
{
//先找到尾结点
SLL* pend = *pphead;
while (pend->next != NULL)
{
pend = pend->next;
}
//找到后,把tem连接到当前尾结点之后
pend->next = tem;
}
}
(3)尾删法
该函数使用传址调用的原因:该函数在链表只有一个节点时,会在删除节点后,将头指针赋为NULL,故要采用传址调用,使用二级指针!
注意*与->同级应使用(),确保先解引用二级指针。
//尾删法
void SLLPopBack(SLL** pphead)
{
//链表为空,直接退出
if (*pphead == NULL)
{
printf("链表为空\n");
return;
}
//链表只有一个节点
//注意* 与 -> 同级,必须使用(),使解引用二级指针优先执行
else if (( * pphead)->next == NULL)
{
//释放当前头指针指向的空间
free(*pphead);
*pphead = NULL;
}
else
{
//首先从头遍历找到尾节点,与此同时保存尾节点其前的一个节点
//只释放尾节点的话并不彻底,因为其前面的一个节点的指针域仍保留着
//指向尾节点的指针,故在释放之后,还应该将该指针赋为空
//防止其变成野指针,导致程序崩溃
SLL* pend = *pphead,*pendpre=NULL;
while ( pend->next != NULL)
{
//保存尾节点前面一个节点的位置
//该写法更为简便,免去了再次遍历的麻烦
pendpre = pend;
pend = pend->next;
}
//释放尾节点
free(pend);
//将前一个节点的指针域赋为空,防止非法访问
pendpre->next = NULL;
}
}
(4)头插法
该函数必用传址调用,因为本就是对头结点进行操作,一定会改变头指针!
//头插法
void SLLPushFront(SLL** pphead, SLLDataType n)
{
//创建新节点
SLL* tem = SLLCreateNode(n);
//直接将新节点与原先的头结点相连
tem->next = *pphead;
//新节点成为头结点
*pphead = tem;
}
(5)头删法
该函数使用传址调用的原因类比上个函数。
//头删法
void SLLPopFront(SLL** pphead)
{
if (*pphead == NULL)
{
printf("链表为空\n");
return;
}
//保存原头结点的后一个节点位置
//防止释放头结点后,找不到后续链表
SLL* tem = ( * pphead )->next;
free(*pphead);
//将原第二个节点作为头结点
*pphead = tem;
}
常用的另一思路:
//头删法
void SLLPopFront(SLL** pphead)
{
if (*pphead == NULL)
{
printf("链表为空\n");
return;
}
SLL* tem = *pphead;
tem = tem->next;
free(*pphead);
*pphead = tem;
}
(6)查找
//查找
SLL* SLLFind(SLL* phead, SLLDataType n)
{
//确保链表不为空
if (phead == NULL)
{
exit(-1);
}
//从头遍历
SLL* tem = phead;
//只要tem不为空,便继续向后查询
while (tem)
{
if (tem->data == n)
{
//找到后,返回该节点位置
return tem;
}
else
{
tem = tem->next;
}
}
//找不到就返回NULL
return NULL;
}
缺陷
该查找函数有缺陷:无法返回多个相同数字的位置。
在test.c的测试函数中,可以通过以下形式,实现查找多个相同数字的位置:
void test()
{
SLL* pos = SLLFind(sl, 2);
int i = 1;
while (pos)
{
pos = SLLFind(pos, 2);
i++;
}
}
在while循环中合理使用if语句,利用i的值,来实现指定获得第n个相同数字的位置。
(7)指定数字后插入
此时使用传值调用的原因便是:该函数功能在于实现插入到指定数字后面,故绝对不会更改原有头指针!
注意在指定节点之后插入新节点时,应先连接后半部分,再连接前半部分!
//指定数字后插入
void SLLPushInsertBack(SLL* phead, SLLDataType n, SLLDataType insert)
{
//先查找指定数字的位置
SLL* tem = SLLFind(phead,n);
if (tem == NULL)
{
printf("链表中没有%d\n",n);
return;
}
else
{
//创建需要插入的节点
SLL* insertnode = SLLCreateNode(insert);
//进行插入操作
//必须先连后半部分,再连前半部分!
insertnode->next = tem->next;
tem->next = insertnode;
}
}
(8)指定数字前插入
如果所指定的节点是链表的头结点,则该函数的功能类似头插法,所以需要传址调用。
//指定数字前插入
void SLLPushInsertFront(SLL** pphead, SLLDataType n,SLLDataType insert)
{
//先查找指定节点的位置
SLL* tem = SLLFind(*pphead,n);
if (tem == NULL)
{
printf("链表中没有%d", n);
}
else
{
//指定节点是头节点,则利用头删法
if (tem == *pphead)
{
SLLPushFront(pphead, insert);
}
else
{
//创建要插入的节点,并查找指定节点(tem)前的一个节点
SLL* insertnode = SLLCreateNode(insert), * preinsert = *pphead;
while (preinsert->next != tem)
{
preinsert = preinsert->next;
}
//将要插入的节点(insert)插入到指定节点(insert)和指定节点前的节点之间(preinsert)
insertnode->next = tem;
preinsert->next = insertnode;
}
}
}
(9)指定删除某数字
通过判断所要删除数字的位置,利用已有函数头删法与尾删法,简化代码。所指定的位置如果是头结点,则函数功能类似头删法,所以使用传址调用。
//指定删除某数字
void SLLPopDelete(SLL** pphead,SLLDataType n)
{
//找到要删除的节点的位置
SLL* tem = SLLFind(*pphead,n);
if (tem == NULL)
{
printf("链表中没有%d\n", n);
return;
}
else
{
//找到尾节点的位置
SLL* pend = *pphead;
while (pend->next != NULL)
{
pend = pend->next;
}
//要删除的节点为头结点,则利用头删
if (tem == *pphead)
{
SLLPopFront(pphead);
}
//要删除的节点为尾节点,则利用尾删
else if(tem==pend)
{
SLLPopBack(pphead);
}
else
{
//找到要删除的节点前的一个节点
SLL* tempre = *pphead;
while (tempre->next != tem)
{
tempre = tempre->next;
}
//将要删除节点前的一个节点直接和要删除节点后的一个节点相连
//便可以达到删除节点的目的
tempre->next = tem->next;
//释放要删除的节点
free(tem);
}
}
}
(10)更改
只需要使用传值调用,因为该函数功能只是改变节点的数据域,而不会改变头指针。
//更改
void SLLChange(SLL* phead, SLLDataType n, SLLDataType change)
{
//先找到要更改的数字得位置
SLL* tem = SLLFind(phead, n);
if (tem == NULL)
{
printf("链表中没有%d\n", n);
return;
}
else
{
//将该节点中所存储的数据改为change
tem->data = change;
}
}
(11)打印链表
//打印链表
void SLLPrint(SLL* phead)
{
//从头遍历打印
SLL* tem = phead;
while (tem)
{
printf("%d->", tem->data);
tem = tem->next;
}
printf("NULL\n");
}
(12)删除链表
//删除链表
void FreeSLL(SLL* phead)
{
//从头遍历删除
SLL* p = phead;
while (p)
{
SLL* tem = p->next;
free(p);
p = tem;
}
}
3.test.c
(1)引用头文件
#include"SLList.h"
(2)各种测试函数
用于测试各个接口函数,无任何实际意义和参考价值。
void testSLList()
{
//想使用链表,必须有头指针,故先创建空指针
//用作空指针
SLL* sl = NULL;
SLLPushBack(&sl, 2);
SLLPushBack(&sl, 3);
SLLPushFront(&sl, 5);
SLLPushBack(&sl, 6);
SLLPushBack(&sl, 7);
SLLPushFront(&sl, 9);
SLLPrint(sl);
SLLPopBack(&sl);
SLLPopFront(&sl);
SLLPrint(sl);
SLLPushInsertBack(sl, 3, 10);
SLLPushInsertFront(&sl, 5, 11);
SLLPrint(sl);
SLLPopDelete(&sl, 2);
SLLPrint(sl);
SLLChange(sl, 11, 18);
SLLPrint(sl);
FreeSLL(sl);
SLL* pos = SLLFind(sl, 2);
int i = 1;
while (pos)
{
pos = SLLFind(pos, 2);
i++;
}
}
(3)主函数
int main()
{
testSLList();
return 0;
}
总结
坚持手敲C语言实现各数据结构!
勉之勉之!