单链表讲解

本文详细介绍了链表的基本概念、结构特点,重点阐述了单链表的实现,包括节点定义、插入、删除等操作,并讨论了不同类型的链表结构及其应用场景。
摘要由CSDN通过智能技术生成

一.链表的概念以及结构

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

链表的结构与火车是类似的,一节一节的,数据就像乘客一样在车厢中一样。

与顺序表不同的是,链表里的每节"车厢"都是独立申请的空间,我们将这样的空间称为节点或者结点

 既然链表是一节一节的结点构成的,那么这样的结点是怎么连接的呢?

链表中的节点一般是通过结构体来实现的,结构体中的存储着数据和下一个节点的地址,这样我们就可以访问下一个节点中的数据了。

节点:

typedef struct SListNode
{
	SLTDataType val;
	struct SListNode* next;
}SLTNode;

为了使用方便我们可以对其重命名。

二.单链表的实现

typedef int SLTDataType;
typedef struct SListNode
{
 SLTDataType data; //节点数据
 struct SListNode* next; //指针保存下⼀个节点的地址
}SLTNode;
void SLTPrint(SLTNode* phead);
//头部插⼊删除/尾部插⼊删除
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);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDesTroy(SLTNode** pphead);

首先我们来思考如何打印数据呢?

void SLTPrint(SLTNode* phead);

这个参数是指向第一个头节点的指针,为什么要使用指针呢?

通过指针我们可以访问这个结构体中的元素,如果我们不使用指针,形参是实参的一份临时拷贝,如果数据过大,这样会很浪费空间(我们这里存储的数据是整形,为了泛用性,我们还是要使用一级指针),直接使用会降低程序性能。

phead->data就可以访问到数据了,那么下一个节点的数据呢?

这时候就会用到我们在结构体中存储的指针了,这个指针指向下一个结构体,所以再通过下一个结构体我们就可以访问下一个结构体的数据,同理,下下个结构体的指针同理。

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

通常习惯上,我们是不会改变直接传过来的参数的,所以我们要使用的话,就再使用一个变量来进行接收。

这里pcur不为NULL时,代表我们的指针并没有走到结构体的末尾,我们就可以以它作为限制条件,每经过一个节点,就打印一次数据,直到遇到空指针停止。 

然后就是要完成指定位置之前插入数据了。

假设有一个链表1->>2->>3->>5->>NULL.

我们要在数据5的前面插入4.可是在插入之前,我们要找到数据五的位置才能插入。

如何查找元素呢?

SLTNode* SLTFind(SLTNode* phead, SLTDataType x);

第一个参数是指向第一个节点的指针,第二个是要寻找的数据,虽然这里可能会存在相同的数字,但是这只是我们练习的场景,假设我们要存储人的身份信息,这是不可能存在相同的人的,所以我们这里假设数据不相同。

最后返回这个节点的地址。

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

和打印数据相同。我们只需要遍历整个数组即可,如果找到就返回这个节点的地址,否则就返回空。

接着我们再来完成指定位置插入数据。

即使我们找打这个节点的地址,我们要在它之前插入数据,也就是再创建一个节点,然后把这个节点的next指针指向找的节点,这样我们就在指定节点前面插入了数据。

但是我们新建的这个节点是没有被节点指向的,所以我们还要让前一个节点指向新的节点。

这个SLTBuyNode是创建新节点函数,我们在插入数据时,会频繁用到这个代码,如果重复的写,就太浪费时间了,所以我们单独封装一个函数来完成这个功能。

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

	return newnode;
}
		SLTNode* pcur = *pphead;
		while (pcur->next != pos) {
			pcur = pcur->next;
		}
		pcur->next = SLTBuyNode(x);
		pcur->next->next = pos;

可是这样写就完了吗?

如果我们要在链表的第一个节点之前插入数据,我们会发现这个代码循环是根本没有进入的,并且会访问NULL地址,这是非法的。

所以为了防止这样的问题,我们还需要额外区分头插的情况。

void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {
	assert(pphead);
	assert(*pphead);
	if (*pphead == pos) {
		*pphead = SLTBuyNode(x);
		(*pphead)->next = pos;
	}
	else {
		SLTNode* pcur = *pphead;
		while (pcur->next != pos) {
			pcur = pcur->next;
		}
		pcur->next = SLTBuyNode(x);
		pcur->next->next = pos;
	}
};

 然后我们来思考,为什么这里要使用二级指针,我们使用指针的目的是为了在函数中改变指针所指向的变量的值,这里就是传值和传址的区别了,我们在头插的过程中指向头节点的指针,是会改变的,如果我们传一级指针,是改变不了这个指针的,所以我们要使用二级指针。

然后就是删除数据函数

void SLTErase(SLTNode** pphead, SLTNode* pos);

这里与插入函数一样,是需要考虑头删的。

void SLTErase(SLTNode** pphead, SLTNode* pos) {
	assert(pphead);
	assert(*pphead);
	SLTNode* pcur = *pphead;
	if (pos == *pphead) {
		*pphead = pos->next;
		free(pos);
	}
	else {
		while (pcur->next != pos) {
			pcur = pcur->next;
		}
		pcur->next = pos->next;
		free(pos);
		pos = NULL;
	}
};

删除指定节点,只需要我们将前一个节点指向下一个节点就行了,然后再释放指定节点即可 。

而头删就是释放头节点,然后将头指针指向头节点的下一个节点既可以。

为了方便,头插头删位插尾删,我们都使用前面两个函数来进行完成,这样更加方便。

void SLTPushBack(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	if (*pphead == NULL) {
		*pphead = SLTBuyNode(x);
	}
	else {
		SLTInsert(pphead,NULL,x);
	}
}

在尾插时,如果链表为空,我们就直接创建一个新的节点即可,否则我们就只需要在NULL前面插入一个节点即可,因为最后一个节点的next指针指向的是空。

void SLTPushFront(SLTNode** pphead, SLTDataType x) {
	assert(pphead);
	if (*pphead == NULL) {
		*pphead = SLTBuyNode(x);
	}
	else {
		SLTInsert(pphead, *pphead, x);
	}
};

头插同理,如果链表为空直接创建新的链表即可,否则就是在头节点之前插入即可,而且头节点直接作为参数给我们使用了,所以我们就不需要再查找了. 

void SLTPopBack(SLTNode** pphead) {
    assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur->next != NULL) {
		pcur = pcur->next;
	}
	SLTErase(pphead,pcur);
};

尾删就需要我们遍历链表,找到最后一个链表的指针 ,这样再删除即可。链表为空就不能删了,所以我们要断言一下。

void SLTPopFront(SLTNode** pphead) {
    assert(pphead && *pphead);
	SLTErase(pphead,*pphead);
};

头删更简单,不需要遍历,直接调用删除函数即可。

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {
    assert(pos);
	SLTNode* next = SLTBuyNode(x);
	next->next = pos->next;
	pos->next = next;
};
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos) {
    assert(pos);
	SLTNode* next = pos->next;
	pos->next = next->next;
	free(next);
	next = NULL;
};

 这里是同理,要插入就直接创建新链表然后插入即可,删除也是同理,这里是需要判断pos是否为空的,因为如果没有找到find函数就会返回空。

删除链表

void SListDesTroy(SLTNode** pphead) {
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur) {
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
};

就需要遍历整个链表然后依次释放,最后为了防止野指针,我们还需要将*pphead置为NULL。 

三.链表 

链表的种类有很多种。

任意两两组合就有2 * 2 * 2 = 8种组合

这个带头和不带头后面双链表再讲解。

我们通常所说的单链表是不带头单向不循环链表。

单向就是通过一个只能访问下一个节点不能访问上一个节点,否则就是双向。

循环就是链表围成一个圈,链表的最后一个节点next指向头节点了,而不是NULL.

虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构: 单链表 双向带头循环链表
  1.  ⽆头单向⾮循环链表:结构简单,⼀般不会单独⽤来存数据。实际中更多是作为其他数据结 构的⼦结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
  2.  带头双向循环链表:结构最复杂,⼀般⽤在单独存储数据。实际中使⽤的链表数据结构,都 是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带 来很多优势,实现反⽽简单了,后⾯我们代码实现了就知道了。

 

  • 64
    点赞
  • 41
    收藏
    觉得还不错? 一键收藏
  • 20
    评论
评论 20
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值