【C】单链表的实现(详解增删查改等功能和OJ练习题)

有了之前的学习,我们认识到顺序表的不足:

1. 中间 / 头部的插入删除,时间复杂度为 O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100 ,满了以后增容到 200 ,我们再继续插入了5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。
那么如何优化数据结构以达到优化程序的目的呢?
本文章我们就将介绍单链表的实现,能有效优化上述部分问题。

1、链表的定义

链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链次序实现的 。

根据上图,链表·的·基本结构就是一个结构体中包含两个变量,一个变量是存储该结点存储的值,而另一个变量是是存储下一个结点的地址,这样我们就可以把一个个结点连接起来,从而从头结点开始可以历遍整个链表。
链表的基本结构:
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

结构体中第一个元素是该结点存储的值,第二个就是下一个结点的地址(下一个结点地址的类型也是该结构体的类型)

下面我们来详解链表的增删查改等一系列操作:
大致包括 增删查改,我们先预设一下实现这些操作的函数名
void SListPrint(SLTNode* phead);
//打印链表
void SListPushBcak(SLTNode** pphead, SLTDataType x);
//尾插
void SListPushFront(SLTNode** pphead, SLTDataType x);
//头插
void SListPopBack(SLTNode** pphead);
//尾删
void SListPopFront(SLTNode** pphead);
//头删
SLTNode* SListFind(SLTNode* phead, SLTDataType x);
//找到存储x结点的位置并返回
void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos前插入一个值
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//在pos后插入一个值
void SListErase(SLTNode** pphead, SLTNode* pos);
//删除pos位置的结点
void SListEraseAfter(SLTNode* pos);
//删除pos后的结点
void SListDestroy(SLTNode** pphead);
//清空链表

这里我们发现一些传参传的是二级指针,而一些传的是一级指针。

在这里说明一下:

可以发现,只要涉及到修改链表内容的我们用了二级指针,但是如果我们不对链表进行修改,而只进行类似打印的一些操作我们传的只是一级指针。我们类比一下最基本的交换a,b两个值的函数和参考前面的函数栈帧的知识,我们想修改a,b的值必须要传a,b的地址才可以。这里我们想修改链表结点中next指针指向的地址,这就涉及到了修改链表中的内容,所以我们传他地址的地址,也就是二级指针。

下面我们我们来详解链表中的上述操作:

1、链表结点的初始化

创建一个结点首先我们要开辟一块内存空间,然后再进行对结点的初始化

SLTNode* BuySListNode(int x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		printf("malloc fail\n");
		exit(-1);
	}
	else
	{
		newnode->data = x;
		newnode->next = NULL;
	}
	return newnode;
}

2、链表的打印

链表的打印只需要我们历遍链表。我们要打印每一个结点的data的值,然后通过.next找到下一个结点的地址,然后继续打印这个结点data的值,再找到下一个结点,如此反复,直到找到一个结点next的值是NULL后结束打印。

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

	}
	printf("NULL");
	printf("\n");
}

3、链表的前插和尾插

头插相对简单一点,我们只需要初始化要插入的结点后,再存储头结点pphead的地址,然后再把插入的结点置为头结点。

头插代码:

void SListPushFront(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

尾插相对来说复杂一点,初始化后,我们要历遍链表找到尾结点,然后把尾结点next指向的地址置为插入的结点的地址,再把插入结点的next指向的地址置空。

尾插与头插不同的是,我们要考虑链表为空的情况,因为为空的话我们的历遍代码就会出问题。当链表为空时,头结点就是我们要插入的结点,初始化后直接把创建的结点的地址赋给头结点即可。

尾插代码:


void SListPushBcak(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySListNode(x);//创建插入的结点
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

4、链表的头删和尾删

删除之前我们首先要考虑的是链表是否为空,空链表肯定无法进行删除类的操作,我们利用断言或者if语句排除这一特殊情况。

头删也很简单,把第二个结点置为头结点,再把原头结点free掉再置空,这就达到了删除的操作。

到这,不免引起我们一些思考,如果没有第二个结点呢?或者说,该链表只有一个结点呢?所以我们也要单独讨论这一特殊情况。

头删代码:

void SListPopFront(SLTNode** pphead)
{
	assert(*pphead);//断言
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;//链表只有一个结点的特殊情况
	}
	else
	{
		SLTNode** cur = *pphead;
		*pphead = (*pphead)->next;
		free(cur);
		cur = NULL;
	}

}

尾删的思路首先也是要历遍链表找到表尾,然后删除表尾。但是,到这我们发现需要将尾结点前一个的next值置为NULL,所以不仅要找到表尾,也要保存倒数第二个结点。所以我们有两种思路,第一种是用快慢指针,第二种是用链表的结构性质,用判断语句判断一个结点的next地址结点的next存储是否为空。

前面我们也单独介绍过快慢指针,我们这里给出第二种思路:

尾删代码:

void SListPopBack(SLTNode** pphead)
{
	SLTNode* cur = *pphead;
	assert(*pphead);//暴力
	if (*pphead == NULL)
	{
		return;
	}//温柔的
	else if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}


}

当然,这种方法只在两个结点及以上的链表中适用,所以特殊情况我们要单独说明。

5、链表中指定位置结点的插入

上面我们只介绍了头插尾插两种特殊情况,但是更多的时候我们需要在指定位置进行插入操作。

下面我们也会介绍指定位置结点的删除,这两项操作前我们都需要做的一件事就是查找到这一元素所在的结点位置,所以我们首先来封装一下查找函数以避免代码的重复输入。

查找我们的思路就是历遍链表,看结点的data值和待查找x的值是否一致,一致就返回。(这里我们只给出查找首次出现这一元素位置的函数,查找重复出现的可以在test.c的测试文件中用循环实现。)

查找代码:


SLTNode* SListFind(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	SLTNode* cur = phead;
	while (cur->next != NULL)
	{
		if (cur->data != x)
			cur = cur->next;
		else if (cur->data == x)
			return cur;
	}
	return NULL;
}

有了查找浙这一基础后,我们可以对返回的pos地址处的结点进行操作了,我们可以在此结点前插入,也可以在此结点后插入。

在pos位置前面插入:我们知道结点的地址了,但前插需要改变他前一个结点next指向的位置,通过pos无法找到他前一个结点位置,所以我们还是需要历遍链表来查找。当然,特殊情况解释该结点是头结点的时候,我们需要另外说明。

实现代码:

void SListInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	if(pos==*pphead)
	{
		SListPushFront(pos, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while(prev->next!=pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySListNode(x);
		prev->next = newnode;
		newnode->next = pos;
		
		
	}
}

在pos位置后面插入:

这种情况就相对简单多了,我们知道了指定位置pos,通过pos->next就可以知道他下一个结点的地址,这里我们的修改就方便多了。把pos后一个结点地址赋值给待插入的next指向的地址,再把待插入的结点的地址赋值给pos->next,这就实现了重新的连接。

实现代码:
 

void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next=newnode;
}

6、链表指定位置结点的删除

这里我们也介绍删除pos位置的结点和删除pos后一个结点的两种情况。

删除pos位置的结点,我们和上面一样,要找到pos结点的前一个结点,这样才可以删除pos位置的结点然后重新将链表链接起来。思路和上面一样:
删除pos位置的结点:

void SListErase(SLTNode** pphead, SLTNode* pos)
{
	assert(*pphead);
	assert(pos);
	if (pos == *pphead)
	{
		*pphead = NULL;
		free(pos);
		pos = NULL;
	}

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

删除pos结点的下一个结点,这就很简单了,我们只需要让pos结点和删除的结点的下一个结点连接起来就好!

删除pos结点的下一个结点:

id SListEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* pnext = pos->next;
	if(pnext)
	{ 
	pos->next = pnext->next;
	free(pnext);
	pnext = NULL;
	}
}

7、链表的清空释放

我们执行完一个程序后,为了防止造成内存泄漏,在关闭程序前通常要清空链表:

历遍链表,然后释放置空即可!

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

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

	*pphead = NULL;
}

猜你喜欢

转载自blog.csdn.net/m0_67821824/article/details/127542552