文章目录
一:SList.h
- 首先我们把整个工程的实现分到三个文件中,分别是SList.c,SList.h,Test.c
- 在SList.h中完成结构体的定义和头文件的包含和函数的声明三个任务
- 其中我们有两点需要注意:
1.我们链表的每一个节点都是一个结构体,包含数据域和指针域
2.数据域中的类型不要直接写死,比如直接写成int,double,不方便修改
为了便于修改,可以使用typedef 将所需的类型重命名为SLTDataType,这样后续只要修改typedef后的类型就可实现数据域存储数据的类型修改 - 代码如下:
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//链表打印
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);
//链表任意位置删除
void SLTErase(SLTNode** pphead, SLTNode* pos);
//链表pos位置前插入
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//链表pos位置后插入
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//链表销毁
void SLTDestory(SLTNode** pphead);
二:SList.c(完成函数的实现)
1.SLTPrint(链表打印)
- 这个函数实现非常简单,只需要链表的头节点的地址,用一个指针cur从头开始遍历链表,打印数据域的数值之后,将cur指针指向下一个节点
- 代码如下:
void SLTPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur)
{
printf("%d->", cur->data);
cur = cur->next;
}
printf("NULL");
}
2.CreateListNode(创建新节点)
- 这个函数是一个辅助函数,功能是创建一个新节点并返回这个节点的地址
- 在后面进行头插,尾插,任意位置的插入时都会使用
- 为了避免代码重复,所以写成一个函数
- 代码如下:
SLTNode* CreateListNode(SLTDataType x)
{
SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));
if (NewNode == NULL)
{
perror("malloc:");
exit(-1);
//申请失败结束程序,如果使用return,其它函数仍会继续执行,会得到一个空指针,导致程序出错
}
NewNode->data = x;
NewNode->next = NULL;
return NewNode;
}
3.SLTPushBack(链表尾插)
- 这个函数有几点需要注意:
- 函数传参是需要用二级指针接收,因为传参会传头指针的地址
- 传头指针地址的原因是头指针有可能被修改,比如空链表进行尾插时
- 为了避免使用者在传参时误传成头指针而不是头指针的地址,可以对pphead加上断言。因为pphead存的是头指针的地址,不可能是空指针,但要是链表为空时进行尾插,参数误传成头指针而不是头指针的地址,pphead的值就会为NULL,加上assert断言可以帮助使用者快速找出错误。其它需要传头指针地址时同样需要加上断言。
- 需要对空链表的情况特殊考虑
-
具体实现:
-
先使用CreateListNode创建一个新节点,之后对链表是否为空进行判断
-
如果为空直接将新节点赋给头指针
-
如果不为空创建指针tail,遍历链表找到链表的尾部
-
终止条件是tail->next为空指针,此时tail指向最后一个节点
-
将tail的next即指针域存储新节点的地址,链接成功
-
代码如下
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* NewNode = CreateListNode(x);
if (*pphead == NULL)
{
*pphead = NewNode;
}
else
{
SLTNode* tail = *pphead;
while (tail->next)
{
tail = tail->next;
}
tail->next = NewNode;
}
}
4.SLTPushFront(链表头插)
-
相比于尾插,头插就十分的简单
-
不需要考虑链表是否为空,所有情况都一样
-
效果展示:
-
代码如下:
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
assert(pphead);
SLTNode* NewNode = CreateListNode(x);
NewNode->next = *pphead;
*pphead = NewNode;
}
5.SLTPopBack(链表尾删)
- 这个函数有几点需要注意:
- 函数传参是需要用二级指针接收,因为传参会传头指针的地址
- 传头指针地址的原因是头指针有可能被修改,比如链表只有一个节点进行尾删时
- 为了避免使用者在传参时误传成头指针而不是头指针的地址,可以对pphead加上断言。(具体原因在尾插中解释过,不赘述)
- 需要防止空链表进行尾删
- 只有一个节点进行删除时需要进行另外考虑
-
具体实现:
-
先对pphead和phead进行断言防止空指针
-
判断是否链表只有一个节点
-
如果只有一个节点,直接进行free,然后将头指针置为NULL
-
如果不止一个节点,创建变量tail找到倒数第二个节点和尾节点
-
终止条件是tail->next->next为空指针,此时tail指向倒数第二个节点
-
释放tail->next,即释放尾节点将tail节点的指针域next置为空指针
-
代码如下:
void SLTPopBack(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* tail = *pphead;
while (tail->next->next)
{
tail = tail->next;
}
free(tail->next);
tail->next = NULL;
}
}
6.SLTPopFront(链表头删)
- 相比于尾删,头删又是十分简单
- 不过我们还是要注意
- 函数传参是需要用二级指针接收,因为传参会传头指针的地址
- 传头指针地址的原因是头指针有可能被修改,比如链表只有一个节点进行头删时
- 为了避免使用者在传参时误传成头指针而不是头指针的地址,可以对pphead加上断言。(具体原因在尾插中解释过,不赘述)
- 需要防止空链表进行头删
-
具体实现:
-
先对pphead和phead进行断言防止空指针
-
创建变量Newhead存储下一个节点的地址
-
释放头节点,将头指针的值改为Newhead
-
如果先释放头指针就找不到新的头了,所以需要先保存再释放
-
效果展示:
-
代码如下:
void SLTPopFront(SLTNode** pphead)
{
assert(pphead);
assert(*pphead);
SLTNode* Newhead = (*pphead)->next;
free(*pphead);
*pphead = Newhead;
}
7.SLTFind(链表查找)
- 这个函数实现也比较简单
- 因为不会改变头指针我们可以不使用二级指针
- 具体实现:
- 创建变量cur先指向链表头,开始遍历链表
- 判断cur此时数据域是否是想要的值
- 如果是返回此时的cur
- 如果不是cur指向下一个节点
- 如果没找到返回NULL
多个相同值的查找
- 也许你会想有多个相同的值如何查找,下面的代码或许能帮助你少许
修改链表节点的值
-
能找到某个节点修改它就是轻而易举了
-
代码如下:
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->next;
}
}
return NULL;
}
8.SLTErase(链表任意位置删除)
- 这个函数有几点需要注意:
- 函数传参是需要用二级指针接收,因为传参会传头指针的地址
- 传头指针地址的原因是头指针有可能被修改,比如链表只有一个节点进行尾删时
- 为了避免使用者在传参时误传成头指针而不是头指针的地址,可以对pphead加上断言。(具体原因在尾插中解释过,不赘述)
- 需要防止空链表进行删除
- 其中参数之一的pos是通过SLTFind函数查找而得,可以说SLTFind也是个辅助函数
- 头删可以另外考虑,也可以复用代码
-
具体实现:
-
先对pphead和phead进行断言防止空指针
-
判断是否是头删,是头删可以复用前面头删函数
-
不是头删则创建指针PosPrev,目的是找到Pos前一个节点
-
遍历链表找到Pos的前一个节点,注意终止条件为PosPrev->next等于Pos
-
之后将PosPrev的指针域的值改为pos的next即pos指针域的值
-
这一步相当于PosPrev的指针域指向Pos之后的一个节点
-
释放Pos,完成节点删除
-
将Pos置为NULL
-
代码如下
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
assert(pphead);
assert(*pphead);
//删头时(复用头删代码)
if (*pphead == pos)
{
//*pphead = pos->next;
//free(pos);
SLTPopFront(pphead);
}
else
{
SLTNode* PosPrev = *pphead;
while (PosPrev->next != pos)
{
PosPrev = PosPrev->next;
}
PosPrev->next = pos->next;
free(pos);
pos = NULL;
}
}
9.SLTInsertBefore(链表任意节点前插入)
- 这个函数有几点需要注意:
- 函数传参是需要用二级指针接收,因为传参会传头指针的地址
- 传头指针地址的原因是头指针有可能被修改,比如链表只有一个节点进行尾删时
- 为了避免使用者在传参时误传成头指针而不是头指针的地址,可以对pphead加上断言。(具体原因在尾插中解释过,不赘述)
- 空链表插入需要单独考虑
- 其中参数之一的pos是通过SLTFind函数查找而得,可以说SLTFind也是个辅助函数
- 头插可以另外考虑,也可以复用代码
-
具体实现:
-
先对pphead和断言防止空指针
-
如果链表为NULL,pos也为NULL,是空链表插入,可以复用头插代码
-
如果单纯pos为NULL,链表不为空,则是参数传错,进行断言规避这种情况
-
判断是否是头插,是头插可以复用前面头插函数
-
不是头插则 申请新节点准备插入,再创建指针PosPrev,目的是找到Pos前一个节点
-
遍历链表找到Pos的前一个节点,注意终止条件为PosPrev->next等于Pos
-
之后将PosPrev的指针域的值改为新节点NewNode的next即NewNode指针域的值
-
这一步相当于PosPrev的指针域指向NewNode这个新节点
-
将NewNode的next改为pos,就是让NewNode的指针域指向pos,到此完成在任意位置的前面插入新节点
-
效果展示:
-
代码如下:
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
//空链表插入
if (*pphead == NULL && pos==NULL)
{
SLTPushFront(pphead, x);
return;
}
assert(pos);
//头部的插入(复用头插函数)
if (pos==*pphead)
{
SLTPushFront(pphead, x);
}
else
{
SLTNode* NewNode = CreateListNode(x);
SLTNode* PosPrev = *pphead;
while (PosPrev->next != pos)
{
PosPrev = PosPrev->next;
}
PosPrev->next = NewNode;
NewNode->next = pos;
}
}
10.SLTInsertAfter(链表任意节点后插入)
- 这个函数实现起来比在链表任意节点前插入要简单,因为不用遍历链表找pos的上一个节点了,实现也十分的相似
- 注意点如下:
- 函数传参是需要用二级指针接收,因为传参会传头指针的地址
- 传头指针地址的原因是头指针有可能被修改,比如链表只有一个节点进行尾删时
- 为了避免使用者在传参时误传成头指针而不是头指针的地址,可以对pphead加上断言。(具体原因在尾插中解释过,不赘述)
- 空链表插入需要单独考虑
- 其中参数之一的pos是通过SLTFind函数查找而得,可以说SLTFind也是个辅助函数
- 头插可以另外考虑,也可以复用代码
-
具体实现:
-
先对pphead和断言防止空指针
-
如果链表为NULL,pos也为NULL,是空链表插入,可以复用头插代码
-
如果单纯pos为NULL,链表不为空,则是参数传错,进行断言规避这种情况
-
判断是否是头插,是头插可以复用前面头插函数
-
申请新节点准备插入
-
将NewNode的next改为pos的next,就是让NewNode的指针域指向pos的next
-
再讲pos的next改为NewNode,就是让pos的指针域指向NewNode
-
效果展示:
-
代码如下:
void SLTInsertAfter(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
assert(pphead);
//空链表插入
if (*pphead == NULL && pos == NULL)
{
SLTPushBack(pphead, x);
return;
}
assert(pos);
SLTNode* NewNode = CreateListNode(x);
NewNode->next = pos->next;
pos->next = NewNode;
}
11.SLTDestory(链表的销毁)
- 或许有的人会想直接free(*pphead)就算结束了,但这会有问题
- 这只释放了头节点,后面的节点不释放会造成内容泄露
- 所以我们需要遍历链表对每个节点进行释放
- 为此我们需要先创建变量存储下一个节点的地址
- 因为如果直接先释放我们就找不到下一个节点了
- 代码如下:
void SLTDestory(SLTNode** pphead)
{
assert(pphead);
SLTNode* cur = *pphead;
SLTNode* next = *pphead;
while (cur)
{
next = cur->next;
free(cur);
cur = next;
}
*pphead = NULL;
}
三:菜单的实现
-
菜单的编写还是十分简单的,对玩家输入的值进行判断进入不同的接口
-
不再赘述,直接上代码了
-
效果展示:
-
代码如下:
#include"SList.h"
void menu()
{
printf("************************\n");
printf("*** 1.头插 2.尾插******\n");
printf("*** 3.头删 4.尾删******\n");
printf("***5.在任意位置前插入***\n");
printf("***6.在任意位置后插入***\n");
printf("***7.删除任意位置节点***\n");
printf("***8.修改任意节点的值***\n");
printf("******9.打印链表********\n");
printf("***0.销毁链表并退出*****\n");
printf("************************\n");
}
int main()
{
menu();
int input = 0;
int x = 0;
SLTNode* plist = NULL;
do
{
printf("请选择> ");
scanf("%d", &input);
switch (input)
{
case 1:
printf("请输入你想要头插的值> ");
scanf("%d", &x);
SLTPushFront(&plist, x);
SLTPrint(plist);
printf("\n");
break;
case 2:
printf("请输入你想要尾插的值> ");
scanf("%d", &x);
SLTPushBack(&plist, x);
SLTPrint(plist);
printf("\n");
break;
case 3:
SLTPopFront(&plist);
SLTPrint(plist);
printf("\n");
break;
case 4:
SLTPopBack(&plist);
SLTPrint(plist);
printf("\n");
break;
case 5:
{
SLTNode* pos = NULL;
while (1)
{
printf("请输入节点的值> ");
scanf("%d", &x);
pos = SLTFind(plist, x);
if (pos == NULL)
{
printf("不存在这个节点,请重新输入\n");
}
else
{
break;
}
}
printf("请输入想要插入的值> ");
int y = 0;
scanf("%d", &y);
SLTInsertBefore(&plist, pos, y);
SLTPrint(plist);
printf("\n");
break;
}
case 6:
{
SLTNode* pos = NULL;
while (1)
{
printf("请输入节点的值> ");
scanf("%d", &x);
pos = SLTFind(plist, x);
if (pos == NULL)
{
printf("不存在这个节点,请重新输入\n");
}
else
{
break;
}
}
printf("请输入想要插入的值> ");
int y = 0;
scanf("%d", &y);
SLTInsertAfter(&plist, pos, y);
SLTPrint(plist);
printf("\n");
break;
}
case 7:
{
SLTNode* pos = NULL;
while (1)
{
printf("请输入节点的值> ");
scanf("%d", &x);
pos = SLTFind(plist, x);
if (pos == NULL)
{
printf("不存在这个节点,请重新输入\n");
}
else
{
break;
}
}
SLTErase(&plist, pos);
SLTPrint(plist);
printf("\n");
break;
}
case 8:
{
SLTNode* pos = NULL;
while (1)
{
printf("请输入节点的值> ");
scanf("%d", &x);
pos = SLTFind(plist, x);
if (pos == NULL)
{
printf("不存在这个节点,请重新输入\n");
}
else
{
break;
}
}
printf("请输入想要修改的值> ");
int y = 0;
scanf("%d", &y);
pos->data = y;
SLTPrint(plist);
printf("\n");
break;
}
case 9:
SLTPrint(plist);
printf("\n");
break;
case 0:
SLTDestory(&plist);
break;
default:
printf("无效数字,请重新输入");
break;
}
} while (input);
return 0;
}