【数据结构】无头+单向+非循环链表的增删查改(结尾附源码)

目录

1 链表的概念和结构

2 无头+单向+非循环链表的接口实现

2.1 前期结构准备

2.2 动态申请结点

2.3 头插和尾插

2.4 头删和尾删

2.5 查找与修改

2.6 任意位置的插入

2.6.1 在任意位置之前插入

2.6.2 在任意位置之后插入 

2.7 任意位置的删除

2.7.1 在任意位置之前删除

2.7.2 在任意位置之后删除 

2.8 单链表的销毁

3 文件源码 

3.1 SList.h

3.2 SList.c 


2.7 任意位置的删除


1 链表的概念和结构

2 无头+单向+非循环链表的接口实现

2.1 前期结构准备

首先我们需要先创建一个新项目,在这里我采用模块化开发,将头文件的声明、函数的声明等包含在“SLT.h”中,函数的功能在“SLT.c”中具体实现,在“test.c”中进行测试。

首先需要定义好链表中每一个元素的类型。链表中的元素都应该是结构体类型,现在我们定义一种最简单的结构体类型。

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

在实际应用中,如果我们直接将x的类型设置为int,那么如果日后需要修改x的类型就会变得非常麻烦。于是我们采用重命名的方式,这样以后在修改x的类型的时候就只需要修改一次。

同时,我们对结构体采取重命名,可以增加代码的可读性,同时更加方便代码的编辑与维护。

2.2 动态申请结点

我们知道,链表在内存中的存储是不连续的。因此想要在链表中增加数据,就需要采用malloc函数来完成新结点的动态开辟。

SLTNode* BuyLTNode(SLTDatatype x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

首先定义一个newnode指针来存放新开辟的空间地址,其次判断一下是否开辟成功,如果开辟失败就用perror打印错误信息,并返回空指针。(内附perror函数的使用介绍

如果开辟成功,将结点的数据部分置为x,同时将结点的指针部分置为空,返回新结点的地址。

2.3 头插和尾插

头插,就是在链表的头部进行数据插入

void SLPushFront(SLTNode** pphead, SLTDatatype x)
{
	assert(pphead);  //即使链表为空,pphead也不为空,因为它是头指针的地址
	SLTNode* newnode = BuyLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

可以注意到,这里我们用了链表的二级指针而不是一级指针,这是为什么呢?

其实仔细琢磨一下便不难发现,因为在执行插入操作的时候,需要对链表指针进行修改。如果这里我们采取一级指针作为参数,那么函数会对指针做一份临时拷贝,改变的其实是函数在栈区临时创建的新变量,出函数作用域后就会被销毁。

因此,想要修改链表指针,就需要传链表指针的地址给函数,进行传址调用。

可以注意到这里我们采取了一个断言操作,这是为了避免实际操作中传错参数而导致不必要的错误,增加断言就可以避免类似情况的发生。

尾插,就是在链表的尾部进行数据插入

void SLPushBack(SLTNode** pphead, SLTDatatype x)
{
	assert(pphead);
	SLTNode* newnode = BuyLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

尾插的大致思路就是:

先判断原链表是否是空链表,如果是空链表,那么就将新创建的结点赋给头指针;如果非空,先定义一个尾指针tail,并对链表找尾。找到尾之后将原尾结点的next置为新结点的地址,就可以完成尾插操作。

2.4 头删和尾删

无头单链表的头删还是比较容易实现的。就将头结点的下一个结点作为新的头结点,在释放掉原头结点的空间即可。当然也要在函数头部进行合适的判空。

void SLPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	SLTNode* del = *pphead;
	*pphead = del->next;
	free(del);
}

至于尾删就要稍微麻烦一点,因为需要判断一下当前删除的结点是否是头结点。如果是头结点,那么就可以直接释放。

如果不是头结点呢?在这里会有一个误区,很多人可能会这样写代码

void SLPopBack(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	SLTNode* tail = *pphead;
	while (tail->next)
	{
		tail = tail->next;
	}
	free(tail);
}

这样写其实是错误的。表面看上去我们确实删除掉了尾结点,让原尾结点的上一个结点成为新的尾结点。但是我们会发现,链表新的尾结点并不指向NULL,还是指向原结点未删除前的地址。这是因为free函数只会将空间释放,并不会将指针置空。所以这里就出现了内存泄漏的问题。

想要解决这个问题也很简单,就把新尾结点的next置空就行了。但这里就不能采用将链表遍历的方法,因为已经形成了野指针问题,采用遍历会发生死循环。

因此我们改变一下思路,既然我们要改原结点的上一个结点,不妨在第一次遍历时就在这个节点处停下,即找到倒数第二个节点。

void SLPopBack(SLTNode** pphead)
{
	assert(pphead);//链表为空,pphead也不为空,因为他是头指针的地址
	assert(*pphead);//链表为空就不能尾删

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	SLTNode* tail = *pphead;
	while (tail->next->next)
	{
		tail = tail->next;
	}
	free(tail->next);
	tail->next = NULL;
}

这个就是正确的尾删 

2.5 查找与修改

查找的思路很简单,最简单易懂的方法就是给一个目标值,遍历整个链表,返回该值所对应的结点的地址。

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

而修改一般是与查找配合使用的。因为链表的性质,并不能像顺序表一样通过下标随机访问。所以链表元素的修改也不如顺序表那般方便。基础思路是通过查找找到目标值所在的结点,再直接改变该节点的data。 

2.6 任意位置的插入

我们先来分析一下单链表的两种任意位置插入情形。

2.6.1 在任意位置之前插入

首先进行常规的判空,之后如果pos是头结点,就相当于头插,可以用头插函数完成。

如果pos不是头结点,那么我们就需要找到pos的前一个结点,在其于pos之间插入一个新的结点。

void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
	assert(pphead);
	assert(pos);

	if (*pphead == pos)
	{
		SLPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuyLTNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}

这种方法时间复杂福是O(n) 

2.6.2 在任意位置之后插入 

在任意位置之后插入操作就会简单很多。

大致思路就是直接在pos之后链接一个新结点,新结点再同原来pos之后的结点链接。

void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{
	assert(pos);

	SLTNode* newnode = BuyLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

这种方法有两个好处:

1.常规判空之后无需再判断pos是否等于头结点,同时也不用在找pos之前的结点,代码量减少的同时更易于理解;

2.时间复杂度是O(1),效率提升更多。

所以,对于单链表的任意位置的插入操作,一般都是默认在pos之后进行,因为更加方便快捷。 

2.7 任意位置的删除

2.7.1 在任意位置之前删除

思路大差不差,如果pos是头结点相当于头删,否则还是要找到pos之前的结点,将其直接链接到pos之后的结点。时间复杂度为O(n)

void SLErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);

	if (pos == *pphead)
	{
		SLPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
	}
}

2.7.2 在任意位置之后删除 

同样是在常规判空后,直接找到pos的下一个节点(待删结点),然后将pos直接与待删节点的下一个结点进行链接,再将待删节点释放。

代码同样变得更加简洁易懂,时间复杂度为O(1)。

void SLEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}

如同任意位置的插入操作一样,为了操作方便,在单链表中,任意位置的删除也是默认在pos之后进行的。 

2.8 单链表的销毁

对于销毁操作,就是将单链表的所有结点所申请的空间都释放掉,最后再将头结点指针置空。

首先我们不能一上来就将头指针置空,因为这样做的话就无法找到下面的结点。

所以在这里采用的操作是:从头结点往后依次遍历释放,直至释放完毕以后,再来回头处理头指针。

void SLDestory(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}

	*pphead = NULL;
}

3 文件源码 

3.1 SList.h

#pragma once
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <assert.h>

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

//动态申请结点
SLTNode* BuyLTNode(SLTDatatype x);

//头插
void SLPushFront(SLTNode** pphead, SLTDatatype x);

//尾插
void SLPushBack(SLTNode** pphead, SLTDatatype x);

//头删
void SLPopFront(SLTNode** pphead);

//尾删
void SLPopBack(SLTNode** pphead);

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDatatype x);

//在pos之前插入
void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x);

//在pos之后插入
void SLInsertAfter(SLTNode* pos, SLTDatatype x);

//删除pos处数据
void SLErase(SLTNode** pphead, SLTNode* pos);

//删除pos之后数据
void SLEraseAfter(SLTNode* pos);

//销毁
void SLDestory(SLTNode* phead);

3.2 SList.c 

#define _CRT_SECURE_NO_WARNINGS 1
#include "SLT.h"

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

SLTNode* BuyLTNode(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 SLPushFront(SLTNode** pphead, SLTDatatype x)
{
	assert(pphead);

	SLTNode* newnode = BuyLTNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

void SLPushBack(SLTNode** pphead, SLTDatatype x)
{
	assert(pphead);

	SLTNode* newnode = BuyLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

void SLPopFront(SLTNode** pphead)
{
	assert(pphead);
	assert(*pphead);

	SLTNode* del = *pphead;
	*pphead = del->next;
	free(del);
}

void SLPopBack(SLTNode** pphead)
{
	assert(pphead);//链表为空,pphead也不为空,因为他是头指针的地址
	assert(*pphead);//链表为空就不能尾删

	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	SLTNode* tail = *pphead;
	while (tail->next->next)
	{
		tail = tail->next;
	}
	free(tail->next);
	tail->next = NULL;
}

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

void SLInsert(SLTNode** pphead, SLTNode* pos, SLTDatatype x)
{
	assert(pphead);
	assert(pos);

	if (*pphead == pos)
	{
		SLPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuyLTNode(x);
		newnode->next = pos;
		prev->next = newnode;
	}
}

void SLInsertAfter(SLTNode* pos, SLTDatatype x)
{
	assert(pos);

	SLTNode* newnode = BuyLTNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

void SLErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);

	if (pos == *pphead)
	{
		SLPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}

		prev->next = pos->next;
		free(pos);
	}
}

void SLEraseAfter(SLTNode* pos)
{
	assert(pos);
	assert(pos->next);

	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
}

void SLDestory(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}

	*pphead = NULL;
}

(本篇完,如内容有误可在评论区指正,感谢!)

猜你喜欢

转载自blog.csdn.net/fbzhl/article/details/130547477