单链表C语言

一、链表的引入

        前面的文章当中我们讲过顺序表,顺序表存在以下几个不足的地方:1.中间插入数据或头部插入数据需要将插入位置之后的数据往后移动,再插入,效率低下。2.增容用到realloc函数,根据realloc函数的工作原理,若原有空间后面有足够大的空间,就在原有空间的后面开辟新空间,若原有空间后面没有足够大的空间,则需要另外开辟空间,把旧数据拷贝到新空间当中,再释放旧空间,如果每次增容都涉及到这几个操作,程序运行效率就会降低。3.realloc函数增容会造成空间浪费。例如:realloc函数开辟了100个字节的空间,但是只使用了1个字节,就会造成浪费。针对顺序表存在的以上三个问题,我们可以用链表这种顺序结构来解决。

二、什么是链表

(一)、链表的概念

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。通俗来讲,链表在物理结构上不是线性的,在逻辑结构上是线性的。那么,如何理解在物理结构上不是线性呢?我们对比一下顺序表,顺序表在物理结构上就是线性的,顺序表在内存中是一块连续的内存空间,顺序表每个数据的地址是连续的,而链表则相反,链表的每个数据在内存中的地址不是连续的,是分散的。

链表在物理结构上是非线性的,又如何实现在逻辑结构上是线性的呢?很简单,可以通过指针来实现,通过指针将每个结点连接起来,就可以从当前节点找到下一个节点。把链表的每个结点用指针连接起来后,将首尾结点一拉,就可以变成一条线。

(二)、链表的结构

在了解了链表的概念之后,我们来看一下链表的结构。链表的结构可以类比于火车,火车是由一节一节的车厢组成,链表则是由一个一个的节点组成。结点组成主要有两个部分,一个是当前节点要保存的数据,一个是下一个结点的地址。

(三)、链表的分类

链表根据带头/不带头、循环/不循环,单向/双向,可分为8种,本文要实现的是不带头不循环单向链表,简称单链表。

二、单链表的实现

(一)、定义结点的结构

链表是由一个一个的结点构成,实现单链表的结构,就是实现节点的结构,我们先定义单链表的结构。

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;//当前节点要存储的数据
	struct SListNode* next;//指向下一个节点的指针
}SLTNode;

(二)、单链表的打印

在单链表的打印方法中,先定义一个指针pcur,指向传递过来的第一个结点,然后通过pcur指针遍历链表,依次打印链表的值。如何通过pcur遍历链表呢?很简单,我们只需要在打印完当前节点的数据之后,将pcur赋值为下一个结点的地址,就实现了pcur指向下一个节点。而下一个结点的地址就存储在当前节点的next指针当中,所以pcur = pcur->next这行代码就实现了指向下一个结点这个操作。当pcur指向为NULL时,链表就遍历完成。

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

(三)、申请节点

SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);//申请失败就退出
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;//申请成功则返回该节点的地址
}

(四)、单链表的尾插

关于函数的参数这里为什么是二级指针:1.代码涉及到对指针解引用,如果参数是一级指针,此时链表又是一个空链表,就会对空指针进行解引用,而我们不能对空指针解引用。2.传值调用(把参数设置成一级指针)的话形参的改变不能影响实参,传址调用(传地址,用二级指针来接收)形参的改变才能影响实参的值。

尾插分为两种情况,一种是链表为空,一种是链表不为空。如果链表为空,新插入的节点就是第一个结点;如果链表不为空,遍历链表,找到链表的尾结点,把尾结点的next指针指向新申请的结点即可。

要注意的是,pphead是指向第一个结点的指针的地址,*pphead是指向第一个节点的指针。

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
    //由于后面要对pphead解引用,所以这里要对pphead判空(不能对空指针解引用)
	SLTNode* newnode = SLTBuyNode(x);//申请新节点
	//处理空链表的情况
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//非空链表
		SLTNode* pcur = *pphead;
		while (pcur->next != NULL)
		{
			pcur = pcur->next;
		}
		pcur->next = newnode;
	}
}

(四)、单链表的头插

对于头插情况,只需要让新申请的结点的next指针指向第一个结点,再改变*pphead的指向,让*pphead指向新的第一个结点即可。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;
	*pphead = newnode;
}

(五)、单链表的尾删

关于形参是二级指针:形参的改变要影响实参(考虑把链表删到为空的情况 *pphead = NULL)。

单链表的尾删有两种情况,当链表只有一个结点时,直接释放该节点即可;当链表有多个节点时,需要找到尾结点以及尾结点的前一个结点,找到这两个结点之后,要释放尾结点,并把尾结点的前一个结点的next指针置为空。

void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next == NULL)
	{
        //链表只有一个节点的情况
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
        //链表存在多个结点的情况
		//找尾结点
		SLTNode* ptail = *pphead;
		SLTNode* prev = *pphead;
        //通过循环遍历找到尾结点以及尾结点的前一个结点
		while (ptail->next != NULL)
		{
			prev = ptail;
			ptail = ptail->next;
		}
        //出了循环之后,尾结点和尾结点的前一个结点都已经找到
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

(六)、单链表的头删

头删要把第一个结点释放掉,然后让第二个结点成为新的第一个结点。由于把第一个结点释放掉之后,不能找到第二个结点,所以先定义一个next指针把第二个结点保存下来。

void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

(七)、单链表的查找

遍历链表,找到节点就返回该节点,找不到就返回NULL。

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

(八)、在指定位置之前插入数据

在指定位置之前插入数据分两种情况,一种是pos == *pphead,此时表示在第一个结点之前插入数据,即头插,直接调用头插函数即可;一种是在其他节点之前插入数据,这种情况下,要找到指定位置的结点以及指定位置结点的前一个,找到之后,让指定位置结点的前一个结点的next指针指向新申请的节点,再让新申请的结点的next指针指向指定位置的结点。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	//给定了pos结点,说明链表中存在pos结点,存在pos结点就说明链表不为空
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
    //头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
        //其他位置结点之前插入
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//出循环之后找到了pos节点的前一个结点
		newnode->next = pos;
		prev->next = newnode;
	}
}

(九)、在指定位置之后插入数据

在指定位置之后插入数据不需要遍历,因为需要修改指针指向的结点可以通过pos找到。在指定位置之后插入数据,让newnode的next指针指向pos的next指针指向的结点,再修改pos的next指针的指向,使其指向newnode即可。

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

(十)、删除pos节点

删除pos节点有两种情况,如果pos == *pphead,说明是头删,直接调用头删方法即可;对于删除其他结点,遍历链表找到pos的前一个结点,将pos的前一个结点和pos的后一个结点连接起来,再释放pos结点即可。

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos是头结点
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		//删除pos节点要先找到pos的前一个结点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//prev pos pos->next
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

(十一)、删除pos之后的结点

删除pos之后的结点,需要将pos和pos之后的第二个结点连接起来,再删除pos之后的节点。

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	//如果pos之后的结点为空就不能删除pos之后的结点了
	SLTNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

(十二)、单链表的销毁

链表的每个结点是我们动态申请的,动态申请的空间如果不销毁的话就会造成内存泄漏,所以我们要对链表进行销毁。

void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur != NULL)
	{
		//释放当前节点之前,先把下一个结点存储起来
		//释放之后,让pcur走到下一个节点的位置
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//*pphead指向的是第一个结点,出了循环之后第一个结点也被释放掉了,如果不把*pphead置为空,*pphead就会成为一个野指针
	*pphead = NULL;
}

三、总代码

#include"SList.h"

SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

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


//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	//处理空链表的情况
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//非空链表
		SLTNode* pcur = *pphead;
		while (pcur->next != NULL)
		{
			pcur = pcur->next;
		}
		pcur->next = newnode;
	}
}

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

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead && *pphead);
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		//找尾结点
		SLTNode* ptail = *pphead;
		SLTNode* prev = *pphead;
		while (ptail->next != NULL)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		prev->next = NULL;
	}
}

//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

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

//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);

	SLTNode* newnode = SLTBuyNode(x);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//出循环之后找到了pos节点的前一个结点
		//prev newnode pos

		newnode->next = pos;
		prev->next = newnode;
	}
}

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos是头结点
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		//删除pos节点要先找到pos的前一个结点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//prev pos pos->next
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

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

//销毁链表
void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur != NULL)
	{
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

  • 18
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值