单链表-----重置版

本期我们对之前讲过的单链表进行重置,增加了亿些细节,不过整体变化不大

(13条消息) 链表与其多种接口实现1-CSDN博客

目录

 链表的结构

打印

尾插

创建新节点

头插

尾删

头删

查找

任意位置前插入

任意位置删除

pos后插入

pos后删除

全部代码


我们仍然使用工程化的写法

 链表的结构

typedef int SLTDataType;
typedef struct SListNode {
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

非常简单,一个存储数据的类型,一个链表节点的指针类型,用来指向下一个节点

我们用画图的方式来看看链表

我们一般画链表都会这样画,这是链表的逻辑结构,但实际当中,内存里是没有这些箭头什么的,我们来看看链表的物理结构

这是实际上链表的存储方式,大家要这样来理解,才能深刻理解链表到底是什么,这些0x112等等都是地址,链表是靠地址来找下一个节点的 

打印

void SLTPrint(SLTNode* phead)//打印
{
	SLTNode* cur = phead;
	while (cur) {
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

我们先完成打印,依次打印链表的元素,方便我们观察,我们发现,这次我们没有使用断言,为什么?phead需要断言吗?答案是不需要,如果大家看了之前的顺序表的话,会发现顺序表进行了断言,这是为什么?因为顺序表并不是直接存在结构体里的,而是我们在顺序表的结构体里设置一个指针a,指向了一个数组,数组里才存放的是数据,如果顺序表为空,是由他的容量为0来控制的,而不是指针为空,这里的链表为空,是可以的,链表为空的话就是一个null,也是可以打印的,所以我们这里没有进行断言

尾插

void SLTPushBack(SLTNode* phead, SLTDataType x)//尾插
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	//找尾
	if (phead == NULL) {
		phead = newnode;
	}
	else {
		SLTNode* tail = phead;
		while (tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

我们创建一个新节点,然后将他初始化,接着我们找到原链表的尾节点,将新节点链接在尾节点即可,如果链表本身为空,那我们把新节点就设为头节点

尾插的本质是原尾节点中存储新尾节点的地址,我们来画图演示

 ​​​​​

 看着这张图,大家就可以深刻理解尾插是怎么回事了

 我们对代码进行测试

发现测试结果和我们想的并不一样,这是为什么呢?我们慢慢往下看

我们知道,形参的改变并不影响实参 

我们传x的地址就可以改变x,这是传值和传址的区别

我们来看这样一段代码

 

画在内存里就是这样的,ptr的改变并不会影响px,而且后边还会释放Fun,存在内容泄漏的问题,我们来对比上下几个函数,我们想改变x时,x是int类型,我们传的参数为int*才可以改变x,而int不可以,这里我们想改变px,px为int*类型,那我们传int*类型正确吗?显然是错误的,我们应该传int**才可以改变int*

我们看这段代码

  

我们这次传的是px的地址,所以*pptr就是px, 此时我们申请的空间就可以用px访问了

 此时再回过头来想我们上面的尾插函数,为什么错了呢?因为我们要修改SLTNode*,我们传的却还是SLTNode*,我们需要传入SLTNode**才可以改变SLTNode*

修改后的尾插为

void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL) {
		perror("malloc fail");
		return;
	}
	newnode->data = x;
	newnode->next = NULL;
	//找尾
	if (*pphead == NULL) {
		*pphead = newnode;
	}
	else {
		SLTNode* tail = *pphead;
		while (tail->next != NULL) {
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

此时就可以正常运行了,这里就可能会有人迷惑,为什么自己在学习时的链表没有使用二级指针呢?这有多种原因,其中一种就可能是你学习的链表是有哨兵位的头节点的,关于哨兵位,循环链表,双向,这些结构之后会出一期专门讲解,这里不再探究

到这里可能会有人发现,我们在找尾时,链表为空使用的是二级指针,而不为空时使用的是一级指针,这是为什么呢? 

大家在这里要想清楚,我们要改变int,要用int*,要改变int*要用int**,我们要改变SLTNode*时就要用SLTNode**,而我们的PushBack函数,我们调用了四次

我们四次都传的是plist的地址,但是其实从第二次开始,我们就用不上plist的地址了,但因为我们调的是同一个函数,所以才会这样(我们总不可能多写一个函数来区分链表为空时的尾插和链表不为空时的尾插吧),只有当plist为空,也就是*pphead为空时,才会使用plist的地址,后边都用不上了,我们拿尾插4时举例

此时的大概就是这样一个情况,我们此时要把tail的next指向newnode,我们这里需要改变的是结构体还是结构体的指针?我们要改变的是结构体,tail是指向这个结构体的,我们有了结构体的指针,是可以改变结构体的,把结构体的next改变,我们现在要改变的是Node,改变的是这个节点,所以我们用的是Node*,大家看看链表的结构体,再画一下图就可以想明白了

此时我们回过头来再看Print函数,需要传二级吗?可以,但没必要,因为我们并没有改变任何东西

创建新节点

我们在写头插之前,发现我们的头插和尾插都需要创建新的节点,所以我们把他提取出来,写成一个函数

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;
}

将尾插的前一段代码提取即可,再修改一下malloc失败的情况,返回NULL即可

头插

void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

头插就非常简单,三句搞定,我们让新节点的next指向plist(即*pphead),然后再把新节点变为头节点即可,而且,这段代码也可以处理plist为空的情况

尾删

void SLTPopBack(SLTNode** pphead)//尾删
{
	SLTNode* prev = NULL;
	SLTNode* tail = *pphead;
	while (tail->next != NULL) {
		prev = tail;
		tail = tail->next;
	}
	free(tail);
	tail = NULL;
	prev->next = NULL;
}

尾删我们除了要找到尾节点,还需要找到前一个节点,不然我们释放尾节点的空间后,前一个节点的指向不是NULL,就是野指针了

还可以这样写

void SLTPopBack(SLTNode** pphead)//尾删
{
	SLTNode* tail = *pphead;
	while (tail->next->next != NULL) {
		tail = tail->next;
	}
	free(tail->next);
	tail->next = NULL;
}

当我们进行测试后,其实是会出现问题的,两种写法都有问题,当链表只剩一个节点时,第二种写法首先出错,tail为空时也会出问题,当只剩一个节点时,第一种写法不会进入循环,直接free掉节点,此时最后一句代码,prev->next就会出现问题,所以我们进行修改

void SLTPopBack(SLTNode** pphead)//尾删
{
	//暴力检查
	//assert(*pphead != NULL);
	//温柔的检查
	if (*pphead == NULL) {
		return;
	}
	//只有一个节点
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {//多个节点
		/*SLTNode* prev = NULL;
	SLTNode* tail = *pphead;
	while (tail->next != NULL) {
		prev = tail;
		tail = tail->next;
	}
	free(tail);
	tail = NULL;
	prev->next = NULL;*/
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL) {
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

我们分情况讨论即可,对于链表为空时,我们进行检查,大家自行选择即可

头删

void SLTPopFront(SLTNode** pphead)//头删
{
	//严厉的检查
	//assert(*pphead != NULL);
	//温柔的检查
	if (*pphead == NULL) {
		return;
	}
	SLTNode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

有了尾删的经验,头删就非常简单了

查找

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{
	SLTNode* cur = phead;
	while (cur) {
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

我们有时也需要查找数据,所以我们写出查找函数,找到的话返回该节点,否则返回NULL,有了查找我们就可以完成许多事情了

任意位置前插入

void SLTInsert(SLTNode** pphead, SLTNode* pos,SLTDataType x)//pos位置前插入
{
    assert(pos);
	assert(pphead);
	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是头节点时,我们直接头插即可,否则我们找到pos的前一个节点,接着我们创建一个新节点,将新节点插入到pos前即可,另外,我们还需要在开始时进行断言,防止传的pos是不存在的,还要判断一下pphead,我们的链表是plist,pphead是plist的地址,所以pphead绝对不会为空,所以需要进行断言,这里就可以发现,我们之前写的函数很多都是要判断pphead的,我们这里对之前的函数进行补充,尾插尾删头插头删这些需要plist的,pphead都不可能为空,所以都需要加上断言,大家进行补充即可,另外,在尾删时,我们还判断了*pphead不能为空,我们需要把判断pphead放在*pphead之前,否则判断pphead就没有意义(因为已经解引用了),头删同理,大家发现,头插和尾插没有判断*pphead,为什么呢?当链表为空时,我们是可以插入的,而链表为空时,我们是不能删除的,所以不是所有的情况都需要断言,大家要根据实际情况来进行判断

任意位置删除

void SLTErase(SLTNode** pphead, SLTNode* pos)//pos位置删除
{
	assert(pphead);
	assert(pos);
	//assert(*pphead);
	if (pos == *pphead) {
		SLTPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

接着,我们来完成pos位置的删除,我们先进行断言,这里大家发现,我们没有对*pphead进行断言,这是因为对pos进行断言的话,间接对*pphead进行了断言,如果pos没有传错的话,那么*pphead就绝对不会为空,所以我们这里没有进行断言,不过进行断言也是可以的,没有影响,删除pos位置的函数也非常简单,如果pos是头,我们进行头删即可,否则找到pos之前的位置,将pos之前的节点的next指向pos的next,接着free掉pos即可,这里释放空间后,pos没有置空,是因为形参的改变不影响实参

这里再给各位开拓一下思维,如果我们只给了pos位置,没有给pphead,我们可以在pos前插入吗?答案是可以的,我们先在pos后插入,然后交换pos和这个节点的值即可

如果只给了pos位置,没有给pphead,我们可以进行删除pos位置吗?也是可以的,我们将pos后一个节点的值给pos,然后删掉pos后一个节点即可,不过这个写法是不能删尾的

这里我们发现,单链表在之前插入和某个位置的删除是非常麻烦的,而在某个位置后删除和插入是比较舒服的

pos后插入

void SLTInsertAfter(SLTNode* pos, SLTDataType x)//pos之后插入
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

这个函数和任意位置插入的区别在于这个不需要pphead,但缺点也是不能头插

pos后删除

void SLTEraseAfter(SLTNode* pos)//pos之后删除
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

与上面的函数相对,还有pos后的删除,也很简单,我们要注意断言一下

以上即为本期的全部内容,希望大家有所收获,最后在这里附上全部代码

全部代码

#pragma once
//SList.h
#include <stdio.h>
#include<assert.h>
#include<stdlib.h>

typedef int SLTDataType;
typedef struct SListNode {
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

void SLTPrint(SLTNode* phead);//打印
SLTNode* BuySLTNode(SLTDataType x);//创建新节点
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 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);//pos之后删除
#include"SList.h"
//SList.c

void SLTPrint(SLTNode* phead)//打印
{
	SLTNode* cur = phead;
	while (cur) {
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

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 SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
	assert(pphead);
	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 = *pphead;
	*pphead = newnode;
}

void SLTPopBack(SLTNode** pphead)//尾删
{
	assert(pphead);
	//严厉的检查
	assert(*pphead);
	//温柔的检查
	/*if (*pphead == NULL) {
		return;
	}*/
	//只有一个节点
	if ((*pphead)->next == NULL) {
		free(*pphead);
		*pphead = NULL;
	}
	else {//多个节点
		/*SLTNode* prev = NULL;
	SLTNode* tail = *pphead;
	while (tail->next != NULL) {
		prev = tail;
		tail = tail->next;
	}
	free(tail);
	tail = NULL;
	prev->next = NULL;*/
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL) {
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}
void SLTPopFront(SLTNode** pphead)//头删
{
	assert(pphead);
	//严厉的检查
	assert(*pphead);
	//温柔的检查
	/*if (*pphead == NULL) {
		return;
	}*/
	SLTNode* first = *pphead;
	*pphead = first->next;
	free(first);
	first = NULL;
}

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)//查找
{
	SLTNode* cur = phead;
	while (cur) {
		if (cur->data == x) {
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

void SLTInsert(SLTNode** pphead, SLTNode* pos,SLTDataType x)//pos位置前插入
{
	assert(pos);
	assert(pphead);
	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;
	}
}

void SLTErase(SLTNode** pphead, SLTNode* pos)//pos位置删除
{
	assert(pphead);
	assert(pos);
	//assert(*pphead);
	if (pos == *pphead) {
		SLTPopFront(pphead);
	}
	else {
		SLTNode* prev = *pphead;
		while (prev->next != pos) {
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
	}
}

void SLTInsertAfter(SLTNode* pos, SLTDataType x)//pos之后插入
{
	assert(pos);
	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SLTEraseAfter(SLTNode* pos)//pos之后删除
{
	assert(pos);
	assert(pos->next);
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

如有错误,还请指正

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值