目录
9、单链表的指定位置后面插入---SListInsertAfter
推荐先看我写的链表预备性知识那篇,因为应该先了解什么是头结点,以及各种链表为空的条件是什么判断清楚了,再来看这篇就更容易理解了。
总共分为三个文件
编译器:vs
SList.h:函数声明和函数定义
SList.c:函数实现
test.c:链表逻辑的测试等
整体函数名的代码风格是跟以后所学c++一致
一、顺序表的优缺点和链表的优缺点
顺序表的优点:
1、支持随机访问:有些算法,需要结构支持随机访问。比如:二分查找,比如优化的快排等等
顺序表缺点:
1、空间不够了需要增容,而增容是有消耗的
2、避免频繁扩容,而且可能导致一定的空间浪费
3、顺序表要求数据从开始位置连续存储,那么我们在头部或者中间位置插入或者删除数据就需要挪动数据,效率不高
针对顺序表的缺陷,就设计出了链表
链表的优点:
1、按需申请空间,不用了就释放空间(更合理的使用了空间),不存在空间浪费
2、头部或者中间删除数据,不需要挪动数据,效率高,
链表的缺点:
1、每隔一个数据,都要存一个指针去链接后面的数据节点
2、不支持随机访问(用下标直接访问第i个)
3、他多了指针的空间开销
初始状态我们用的是没有头结点的单链表,那么刚开始初始化就初始化NULL即可
SLTNode* plist = NULL;
二、单链表的功能实现
首先先引入创造节点的函数,因为链表的每一部分都是一个节点,包含有数据域和指针域。至于为什么每一个节点都是动态开辟的,是因为其需要根据实际存储的数据类型和节点前后指针的大小进行调整。
1、CreatNode函数
单链表:Single linked list(故简称为SLT)
创建节点:需要动态开辟一个节点的空间,然后会根据输入的值x,把这个新开辟的节点的值域设置为x
//结点的创建
SLTNode* CreateNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (NULL == newnode)
{
perror("SListPushBack");
exit(-1);
//这里值得一说的是malloc动态开辟的内存很少失败
//因为堆区内存很大,不差你这一块
//如果失败了就说明内存没多少空间了!直接结束程序就可以
}
newnode->Next = NULL;
newnode->data = x;
return newnode;
}
2、单链表的打印---SListPrint
链表打印的结束标志是NULL,所以打印到这个NULL,其可以作为循环结束的标志
因为链表不是连续的,所以不能像顺序表一样++访问下一个元素,所以要通过指针域访问下一个元素,如cur=cur->Next,因为Next存的是下一个节点的地址(结构体SLTNode的地址),所以可以放到cur中,所以就相当于cur指向了下一个节点
//单链表的打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->Next;
}
printf("NULL\n");
}
3、单链表的尾插数据---SListPushBack
想尾插数据,就要先找到尾节点。让原来尾节点的next,指向我的新的节点。下面写的phead和plist都是指向链表第一个节点的,因为一个phead是plist的拷贝。
void SlistPushBack(SLTNode* phead, SLTDataType x)
{
//首先要先找到尾节点
SLTNode* tail = phead;//遍历链表最好不要用头指针来便利
while (tail->next)
{//只有该节点指针域为NULL,NULL相当于0,才算找到尾节点
tail = tail->next;
}
SLTNode* newnode = CreateNode(x);//创建节点,并插入数据x
tail->next = newnode;//让尾节点指向新开辟的节点
}
问题一、
phead只是实参头指针plist的一个值的拷贝,如果在函数体内改变phead,不会影响函数外部的plist,而我们真正想改变的是plist,所以要传plist的地址,那么这就变成一个二级指针。则形参可改为SLTNode** pphead
问题二、
我们发现这么写程序会崩溃,错在哪呢? 假如我传入的是空链表,也就是说phead为NULL,(假如plist为空,实参传给形参的过程。)而此时tail也被赋为NULL,而tail->next属于解引用行为,对NULL解引用,这是非法访问内存的行为!所以要先判断如果为空链表,那就直接使形参*pphead=newnode就可以了,如果不是空链表我们就要先找到尾节点。是尾节点的next(指针域)指向新开辟的节点newnode。
//单链表的尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = CreateNode(x);
//newnode的数据域和指针域的填充会在CreateNode函数中实现
SLTNode* tail = *pphead;
//通过*phead找到了plist
//设置tail因为其为了找尾节点,他需要不断指向
//下一个节点的,如果没有找到尾节点的话
if (*pphead == NULL)
{//如果这个plist为空链表
//就没必要找尾节点了
*pphead = newnode;
}
else
{
while (tail->Next)
{
//如果tail->next等于NULL,则判断为0,那就找到尾节点了
//也可以写为tail->Next != NULL
tail = tail->Next;
}
//让前一个节点指向这个新开辟的节点
tail->Next = newnode;
}
}
4、单链表的尾删---SListPopBack
既然是尾删,那首先要找到这个单链表的尾。那就创建一个变量tail来找,然后再free这个tail指向的结点(因为每一个结点都是动态内存开辟的)就可以了吗?不会这么简单
所以不仅仅是把尾节点直接删除那么简单,还要找到尾节点之前的那个结点,把它的指针域置为NULL,这个步骤很重要,所以这个问题又转为了找到尾节点之前的一个结点,单链表却不好找前一个结点,这也是单链表的缺点,以后将双链表找前驱结点就很容易了!那么单链表找前驱节点有两种方法
单链表找前驱结点的两种方法:
一、
void SListPopBack(SLTNode** pphead)
{
SLTNode* pretail = NULL;
SLTNode* tail = *pphead;
while (tail->Next)
{
pretail = tail;
tail = tail->Next;
}
//while循环操作下来,pretail就指向了尾节点的前一个结点
//因为每一个结点都是动态开辟的,所以这个操作是支持的
free(tail);
tail = NULL;
pretail->Next = NULL;
}
二、
SLTNode* tail = *pphead;
while (tail->Next->Next)
{
tail = tail->Next;
}
free(tail->Next);
tail->Next = NULL;
对于链表的操作往往要考虑边界情况,如果这个链表为空链表,无需删除。如果这个链表只有一个有效节点,就无需找前驱节点,直接删除即可。
//单链表的尾删
void SListPopBack(SLTNode** pphead)
{
//温柔一点的方式
/*if (*pphead == NULL)
{
return;
}*/
//粗暴一点的方式
//我们以后学的c++使用的是粗暴的方式
//如果为NULL--assert直接会让程序报错
assert(*pphead != NULL);//传入为空链表则无需删除
if ((*pphead)->Next == NULL)
{//如果只有一个节点,直接删除即可,无需找前驱节点
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* pretail = NULL;
SLTNode* tail = *pphead;
while (tail->Next)
{
pretail = tail;
tail = tail->Next;
}
//while循环操作下来,pretail就指向了尾节点的前一个结点
free(tail);
tail = NULL;
pretail->Next = NULL;
}
}
5、单链表的头插---SListPushFront
//单链表的头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = CreateNode(x);
newnode->Next = *pphead;
*pphead = newnode;
}
6、单链表的头删---SListPopFront
头删会改变头指针的指向,故这里还用二级指针。对于其他操作,我们只需要保存头结点的下一节点的地址,然后free掉头结点,再使头指针*pphead指向原头结点的下一节点就可以了。然后我们还需要考虑的一个问题是边界问题--空链表的问题,先考虑*pphead->Next为NULL的情况,只有一个有效节点的时候是适用的,但是如果*pphead为NULL呢(空链表的情况)?此时实参plist为NULL,那么写的操作*pphead->Next显然不行,因为用->操作符,是访问NULL了,属于非法访问内存错误。所以要加一个判断。
//单链表的头删
void SListPopFront(SLTNode** pphead)
{
//温柔的处理方式
//if (*pphead == NULL)
//{
// return;
// //本来就是空的,就没有删的必要了!
//}
//粗暴的处理方式
assert(*pphead != NULL);
/*下面展示的是错误的一种写法,为何错?
因为tmp不是动态开辟的,不能free!
SLTNode* tmp = *pphead;
*pphead = (*pphead)->Next;
free(tmp);
tmp = NULL;
*/
//那么正确的写法如何?
SLTNode* tmp = (*pphead)->Next;
free(*pphead);
*pphead = tmp;
//也就是说先保存要删除结点的下一节点位置
//然后释放,再令*pphead指向被删除节点的下一节点
}
7、查找单链表的某个节点---SListFind
想查找单链表中的某个节点,我可以传入我想要查找的元素是多少,然后遍历一下单链表,找到就返回这个单链表中的某个节点对应的指针,否则返回NULL,但是如果我这个链表中有重复的元素呢。我如何能查找到第一个元素,并且能查找第二个或者以上的元素呢?
//查找单链表的某个数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;//遍历链表最好不要用头指针
while (cur)
{//如果传入的本来就为空链表,那直接返回NULL
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->Next;
}
}
return NULL;
}
对于查找链表中重复的数据:
写在test.c文件中,如果不懂,可以看看最后的源码
有了这个查找的操作,那么我们修改某一个节点就十分容易了,代码如下:
//修改单链表的数据3,改为30
pos = SListFind(plist, 3);
if (pos)
{
pos->data = 30;
printf("修改成功!\n");
}
SListPrint(plist);
8、单链表的指定位置前面插入---SListInsert
插入传入位置pos节点的前面,插入的时候,肯定还要找到插入位置的前一个节点,因为前一个节点的指针域要指向插入位置。怎么找到插入位置的前一个节点呢?只要在没插入之前,找到pos前面的一个节点就可以了。同时我们还要考虑边界条件,如果是头插,你要找他的前一个节点prevpos,你会发现你找不到,因为没有一个节点的指针域指向头,所以如果插在头,要么调用头插函数,要么自己写下操作。
pos节点位置可以通过SListFind函数来查找到。 值得注意的是既然是在pos节点之前插入一个节点,那么就说明这个单链表至少有一个有效节点。也就是不存在空链表的情况(这里讲解的都是在没有头结点的基础上讲解的)。所以只要讨论有一个有效节点和多个有效节点的情况即可
//单链表的指定位置插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//newnode指向我要插入数据为x的节点-- - 创建这个节点
SLTNode * newnode = CreateNode(x);
if (pos == *pphead)
{
//符合头插条件,那他就没有上一个节点,直接头插就行
newnode->Next = pos;
*pphead = newnode;
//实现头插操作,调用SListPushBack也可以
}
else
{
//用prevpos来找插入位置的前一个节点其实
//就是找没插入之前pos之前的节点
SLTNode* prevpos = *pphead;
while (prevpos->Next != pos)
{
prevpos = prevpos->Next;
}
//这个while循环下来,prevpos就指向了pos的前一个节点
prevpos->Next = newnode;
newnode->Next = pos;
}
}
9、单链表的指定位置后面插入---SListInsertAfter
// 单链表的指定位置后面插入
void SListInsertAfter(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{
SLTNode* newnode = CreateNode(x);
newnode->Next = pos->Next;
pos->Next = newnode;
}
10、单链表的指定位置删除---SListErase
删除某一个节点,要找到要被删除节点的上一个节点,因为要让上一个节点的指针域指向被删除节点的下一个节点,所以又涉及单链表找前节点问题。然后就是考虑边界问题,如果是尾删可以吗?这个细想,可以!那么头删呢?头删就没有说找被删除节点的前一个节点的概念了,他已经是头了,直接删就好,而且如果链表为空,根本不用删除!
//单链表的指定位置删除
//意思是删除pos位置的那个节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
//链表为空什么都不需要删除
assert(*pphead != NULL);
//if是头删
if (*pphead == pos)
{
*pphead = pos->Next;
free(pos);
pos = NULL;
//不置为NULL也行,没有意义的
//调用头删函数也是可以的
}
else
{
SLTNode* prev = *pphead;
while (prev->Next != pos)
{
prev = prev->Next;
}
//while循环后prev就是pos的前一个节点了
prev->Next = pos->Next;
free(pos);
pos = NULL;
//这里pos置不置为NULL都可以,但是还是建议置为NULL,不置为NULL也没影响
//因为pos是局部变量,pos出了函数即销毁,把他置为NULL,并不影响外面的实参
//我们最终要的是外面的实参,实参没改变,你改形参没用,除非传地址修改
}
}
11、单链表的销毁---PListDestroy
单链表的全部节点都要销毁,值得注意的是,每次销毁一个节点前,都要保存这个节点的指针域,因为free后,这个节点就为随机值了,就找不到他的原来的Next域了,最后*pphead置为空,防止再次调用发生错误
//单链表的销毁
void SListDestroy(SLTNode** pphead)
{
//为空啥也不用销毁
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
//这里一定要先保存指针域,因为free后
//cur就变成随机值了,他的Next域肯定也是随机值了
SLTNode* next = cur->Next;
free(cur);
//这里用cur来遍历链表,因为cur一开始指向第一个有效节点
//所以free(cur)相当于释放掉了一个有效节点
//cur再指向后续节点,那么就可以一个个释放有效节点了
cur = next;
}
*pphead = NULL;
}
最后源码如下:
test.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
void TestSList1()
{
//刚开始的初始状态链表为空
SLTNode* plist = NULL;
SListPushBack(&plist, 1);
SListPushFront(&plist, 2);
SListPushFront(&plist, 2);
SLTNode* pos = SListFind(plist, 1);
SListInsert(&plist, pos, 3);
SListPrint(plist);
pos = SListFind(plist, 2);
int i = 1;
while (pos)
{
//如果pos为空,说明这个元素本来就不存在,就不需要重复查找了
//如果不为空,那么就再找一次,找到了就再打印,没找到则不重复
printf("第%d个pos节点:%p->%d\n", i++, pos, pos->data);
pos = SListFind(pos->Next, 2);
//因为是重复的了,那么这个重复的肯定在pos后面,所以从
//pos->Next往后找,没找到则pos=NULL,循环结束
//找到了,则返回这个pos的地址,然后接着下一轮循环
}
//修改单链表的数据3,改为30
pos = SListFind(plist, 3);
if (pos)
{
pos->data = 30;
printf("修改成功!\n");
}
SListPrint(plist);
SListDestroy(&plist);
}
int main()
{
TestSList1();
return 0;
}
SList.c:
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//单链表的打印
void SListPrint(SLTNode* phead)
{
SLTNode* cur = phead;
while (cur != NULL)
{
printf("%d->", cur->data);
cur = cur->Next;
}
printf("NULL\n");
}
//结点的创建
SLTNode* CreateNode(SLTDataType x)
{
SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
if (NULL == newnode)
{
perror("SListPushBack");
exit(-1);
//这里值得一说的是malloc动态开辟的内存很少失败
//因为堆区内存很大,不差你这一块
//如果失败了就说明内存没多少空间了!直接结束程序就可以
}
newnode->Next = NULL;
newnode->data = x;
return newnode;
}
//单链表的尾插
void SListPushBack(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = CreateNode(x);
SLTNode* tail = *pphead;
//通过*phead找到了plist
//设置tail因为其为了找尾节点,他需要不断指向
//下一个节点的,如果没有找到尾节点的话
if (*pphead == NULL)
{//如果这个plist为空链表
//就没必要找尾节点了
*pphead = newnode;
}
else
{
while (tail->Next)
{
//如果tail->next等于NULL,则判断为0,那就找到尾节点了
//也可以写为tail->Next != NULL
tail = tail->Next;
}
//让前一个节点指向这个新开辟的节点
tail->Next = newnode;
}
}
//单链表的头插
void SListPushFront(SLTNode** pphead, SLTDataType x)
{
SLTNode* newnode = CreateNode(x);
newnode->Next = *pphead;
*pphead = newnode;
}
//单链表的尾删
void SListPopBack(SLTNode** pphead)
{
//温柔一点的方式
/*if (*pphead == NULL)
{
return;
}*/
//粗暴一点的方式
//我们以后学的c++使用的是粗暴的方式
//如果为NULL--assert直接会让程序报错
assert(*pphead != NULL);
//空链表的另一种情况
if ((*pphead)->Next == NULL)
{
free(*pphead);
*pphead = NULL;
}
else
{
SLTNode* pretail = NULL;
SLTNode* tail = *pphead;
while (tail->Next)
{
pretail = tail;
tail = tail->Next;
}
//while循环操作下来,pretail就指向了尾节点的前一个结点
//因为每一个结点都是动态开辟的,所以这个操作是支持的
free(tail);
tail = NULL;
pretail->Next = NULL;
}
}
//单链表的头删
void SListPopFront(SLTNode** pphead)
{
//温柔的处理方式
if (*pphead == NULL)
{
return;
//本来就是空的,就没有删的必要了!
}
//粗暴的处理方式
assert(*pphead != NULL);
/*下面展示的是错误的一种写法,为何错?
因为tmp不是动态开辟的,不能free!
SLTNode* tmp = *pphead;
*pphead = (*pphead)->Next;
free(tmp);
tmp = NULL;
*/
//那么正确的写法如何?
SLTNode* tmp = (*pphead)->Next;
free(*pphead);
*pphead = tmp;
//也就是说先保存要删除结点的下一节点位置
//然后释放,再令*pphead指向被删除节点的下一节点
}
//查找单链表的某个数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
SLTNode* cur = phead;
while (cur)
{//如果传入的本来就为空链表,那直接返回NULL
if (cur->data == x)
{
return cur;
}
else
{
cur = cur->Next;
}
}
return NULL;
}
//单链表的指定位置插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
//newnode指向我要插入数据为x的节点-- - 创建这个节点
SLTNode * newnode = CreateNode(x);
if (pos == *pphead)
{
//符合头插条件,那他就没有上一个节点,直接头插就行
newnode->Next = pos;
*pphead = newnode;
//实现头插操作,调用SListPushBack也可以
}
else
{
//用prevpos来找插入位置的前一个节点其实
//就是找没插入之前pos之前的节点
SLTNode* prevpos = *pphead;
while (prevpos->Next != pos)
{
prevpos = prevpos->Next;
}
//这个while循环下来,prevpos就指向了pos的前一个节点
prevpos->Next = newnode;
newnode->Next = pos;
}
}
//单链表的指定位置删除
//意思是删除pos位置的那个节点
void SListErase(SLTNode** pphead, SLTNode* pos)
{
//链表为空什么都不需要删除
assert(*pphead != NULL);
//if是头删
if (*pphead == pos)
{
*pphead = pos->Next;
free(pos);
pos = NULL;
//不置为NULL也行,没有意义的
//调用头删函数也是可以的
}
else
{
SLTNode* prev = *pphead;
while (prev->Next != pos)
{
prev = prev->Next;
}
//while循环后prev就是pos的前一个节点了
prev->Next = pos->Next;
free(pos);
pos = NULL;
//这里pos置不置为NULL都可以,但是还是建议置为NULL,不置为NULL也没影响
//因为pos是局部变量,pos出了函数即销毁,把他置为NULL,并不影响外面的实参
//我们最终要的是外面的实参,实参没改变,你改形参没用,除非传地址修改
}
}
// 单链表的指定位置后面插入
void SListInsertAfter(SLTNode* pphead, SLTNode* pos, SLTDataType x)
{
SLTNode* newnode = CreateNode(x);
newnode->Next = pos->Next;
pos->Next = newnode;
}
//单链表的销毁
void SListDestroy(SLTNode** pphead)
{
//为空啥也不用销毁
assert(pphead);
SLTNode* cur = *pphead;
while (cur)
{
//这里一定要先保存指针域,因为free后
//cur就变成随机值了,他的Next域肯定也是随机值了
SLTNode* next = cur->Next;
free(cur);
cur = next;
}
*pphead = NULL;
}
SList.h:
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
//单链表的创建
typedef struct SListNode
{
SLTDataType data;
struct SListNode* Next;
}SLTNode;
//单链表的打印
void SListPrint(SLTNode* phead);
//单链表的尾插
void SListPushBack(SLTNode** pphead, SLTDataType x);
//单链表的头插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//单链表的尾删
void SListPopBack(SLTNode** pphead);
//单链表的头删
void SListPopFront(SLTNode** pphead);
//查找单链表的某个数据
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//单链表的指定位置前面插入
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//函数参数也可以写成int pos,也就是通过找坐标,但是用SLTNode* pos是
//更建议的,因为这种写法是以后c++用的,这种写法的意思是,在pos位置之前
//去插入一个节点
// 单链表的指定位置后面插入
void SListInsertAfter(SLTNode* pphead, SLTNode* pos, SLTDataType x);
//单链表的指定位置删除
void SListErase(SLTNode** pphead, SLTNode* pos);
//单链表的销毁
void SListDestroy(SLTNode** pphead);