数据结构_第三关:线性表——单链表的实现

在上一关,我们通过了顺序表的实现,但是,顺序表还有一些缺陷:

1. 空间不够,需要扩容,扩容(尤其是异地扩容)是有一定代价的,其次还可能存在一定的空间浪费。

2.头部和中部插入删除,需要挪动数据,效率低下

优化方案:

1.按需申请空间

2.不要挪动数据

1.链表的概念及结构

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

结构:

注意:
1从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
2.现实中的结点一般都是从堆上申请出来的
3.从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

 2.链表的分类

1.单项或者双向

2.带头或者不带头

3.循环或者非循环

可以看出这三种分类多可以结合处8个不同类型的单链表

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

下面我们主要对无头单向非循环链表进行实现:

3.链表的实现

3.1结点的定义

typedef int SLTDataType;

typedef struct SlistNode
{
	SLTDataType data;
	struct SlistNode* next;
}SLTNode;

3.2函数的声明

// 动态申请一个结点
SLTNode* BuySLTNode(SLTDataType x);//返回类型是:SLTnode* 结构体地址

//单链表打印
void SListPrint(SLTNode* phead);

//尾插,尾删
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);

//头插,头删
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopFront(SLTNode** pphead);

//查找一个数,返回其地址
SLTNode* SListFind(SLTNode* phead, SLTDataType x);

//在单链表pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//在pos之前插入,因为在前面插入会改变phead的指针,所以需要传双指针
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);

//删除pos后的节点
void SLTEraseAfter(SLTNode* pos);

//单链表的释放:
void SLTDestroy(SLTNode** pphead);

3.3函数的实现和原理解释:

1)动态申请一个结点

SLTNode* BuySLTNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

malloc申请一个动态结点,如果malloc调用失败,返回NULL则直接结束掉程序

成功,赋值,并返回

2)打印单链表

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

传函数头结点的地址,并用cur进行接收,以防止phead的值被改变,从而导致该链表找不到头

 3)尾插

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

这里传递双指针的原因是:如果要改变头结点的数据,需要2次传参,就需要用双指针
改变之前地址就要用地址的地址去进行
(你要改变int,传int的地址int*,你要改变int*,就要传递int*的地址int**)
(下面的函数传参中,需要改变头结点的数据时,都需要用双指针) 

用newnode接受一个新的结点,然后判断结点是否为空,空则直接赋值,不为空则找处尾结点的地址再进行链接

4)尾删

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

如果链表为空,则不能再进行删除,用assert断言判断一下
要删掉尾节点,就要找到尾结点的前一个结点,将前一个结点的->next=NULL
所以再找的时候找的时tail->next->next,然后将tail->next=NULL
这样写的话,如果该单链表只有一个数,tail->next->next不存在,则代码就会出现错位
所以要加上一个判断,用(*pphead)->next == NULL来判断该单链表时否只有一个结点。

错误写法:

 正确写法如实现中那样,也可以这样写:

 先定义一个prev,tail再最后一个,prev再tail的前一个,如果tail为空,直接让prev的next为空即可

 5)头插和头删

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

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

头插和头删是单链表主要的优势,又快又简单

6)查找一个数,返回其地址

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

用cur等于phead防止phead改变,找到返回cur,未找到,返回NULL

7)在单链表pos之后插入x

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

先用newnode的next指向pos的next
再让pos的next指向newnode

8)在pos之前插入,因为在前面插入会改变phead的指针,所以需要传双指针

void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (pos == *pphead)//如果在头节点前插入就相当于头插
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		newnode->next = prev->next;
		prev->next = newnode;
	}
}

在之前插入,我们的插入可能在1之前,也就是要改变头节点,相当于头插,就需要二级指针
是头插,直接调用头插函数就行

先找到pos的前一个,我们定义一个prev,prev->next=pos时,即可在中间插入newnode

9)删除pos后的节点

void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);

	if (pos->next == NULL)//只有一个头节点
	{
		return;
	}
	else
	{
		SLTNode* nextNode = pos->next;
		pos->next = nextNode->next;
		free(nextNode);
	}
}

和插入一样,只有一个头节点要单独处理

10)删除pos位置的节点

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	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);
	}
}

先用prev来表示前一个,当prev->next=pos时,进行删除

11)单链表的释放:

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

单链表不像顺序表,需要一次一次进行释放
 

如果先释放了1,就找不到后面的结点了
所以先要定义一个next或者nextnode来接受cur的下一个结点
然后再free,最后将nextnode赋值给cur

最后将*pphead置为NULL

4.单链表的OJ题

1.删除链表中等于给定值 val 的所有结点。OJ题链接

2.反转一个单链表。OJ题链接

3.给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。OJ题链接

4.. 输入一个链表,输出该链表中倒数第k个结点。OJ题链接

5.将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。OJ题链接

6.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。OJ题链接

7.链表的回文结构。OJ题链接

8.输入两个链表,找出它们的第一个公共结点。OJ题链接

9.给定一个链表,判断链表中是否有环。OJ题链接

10.给定一个链表,返回链表开始入环的第一个结点。 如果链表无环,则返回 NULL。OJ题链接

11.给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点或空结点。OJ题链接

OJ题的代码和讲解在下一篇文章中发布

5.源代码如下(VS2022下编译):

#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

//无头+单向+非循环链表增删查改实现

//声明

typedef int SLTDataType;

typedef struct SlistNode
{
	SLTDataType data;
	struct SlistNode* next;
}SLTNode;

// 动态申请一个结点
SLTNode* BuySLTNode(SLTDataType x);//返回类型是:SLTnode* 结构体地址
//单链表打印
void SListPrint(SLTNode* phead);

//尾插,尾删
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
//头插,头删
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopFront(SLTNode** pphead);

//查找一个数,返回其地址
SLTNode* SListFind(SLTNode* phead, SLTDataType x);

//在单链表pos之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//在pos之前插入,因为在前面插入会改变phead的指针,所以需要传双指针
void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x);

//删除pos位置的节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//删除pos后的节点
void SLTEraseAfter(SLTNode* pos);

//单链表的释放:
void SLTDestroy(SLTNode** pphead);


//实现
SLTNode* BuySLTNode(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 SListPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	SLTNode* newnode = BuySLTNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

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

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

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

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

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

void SLTInsertBefore(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pos);
	if (pos == *pphead)//如果在头节点前插入就相当于头插
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		SLTNode* newnode = BuySLTNode(x);
		newnode->next = prev->next;
		prev->next = newnode;
	}
}

void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	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 SLTEraseAfter(SLTNode* pos)
{
	assert(pos);

	if (pos->next == NULL)//只有一个头节点
	{
		return;
	}
	else
	{
		SLTNode* nextNode = pos->next;
		pos->next = nextNode->next;
		free(nextNode);
	}
}

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

SLTNode* CreateSlist(int n)
{
	SLTNode* phead = NULL, * ptail = NULL;
	for (int i = 0;i < n;i++)
	{
		SLTNode* newnode = BuySLTNode(i);
		if (phead == NULL)
		{
			ptail = phead = newnode;
		}
		else
		{
			ptail->next = newnode;
			ptail = newnode;
		}
	}
	return phead;
}

//测试
void TestSList1()
{
	
	SLTNode* plist = NULL;
	printf("尾插:\n");
	SLTPushBack(&plist, 100);
	SLTPushBack(&plist, 200);
	SLTPushBack(&plist, 300);
	SListPrint(plist);
	printf("\n");

	printf("尾删:\n");
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	//SLTPopBack(&plist);删到了空,再继续删会报错
	SListPrint(plist);
	printf("\n");

	printf("头插:\n");
	SLTPushFront(&plist, 100);
	SLTPushFront(&plist, 300);
	SLTPushFront(&plist, 600);
	SLTPushFront(&plist, 10);
	SLTPushFront(&plist, 50);
	SLTPushFront(&plist, 360);
	SLTPushFront(&plist, 369);
	SListPrint(plist);
	printf("\n");

	printf("头删:\n");
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SListPrint(plist);
	printf("\n");

	printf("查找600,返回其位置用pos接受,并在其后面插入88:\n");
	SLTNode* pos = SListFind(plist, 600);
	SLTInsertAfter(pos, 88);
	SListPrint(plist);
	printf("\n");

	printf("查找600,并在其前面插入666:\n");
	SLTInsertBefore(&plist, pos, 666);
	SListPrint(plist);
	printf("\n");

	printf("如果  pos=phead,查看是否可以插入33\n");
	SLTInsertBefore(&plist, plist, 33);
	SListPrint(plist);
	printf("\n");

	printf("删除600后的节点88\n");
	SLTEraseAfter(pos);
	SListPrint(plist);
	printf("\n");

	printf("删除600位置的节点\n");
	SLTErase(&plist, pos);
	SListPrint(plist);
	printf("\n");

	printf("点链表释放\n");
	SLTDestroy(&plist);
	SListPrint(plist);
	printf("\n");
}
int main()
{
	TestSList1();//测试函数

	return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_60630270/article/details/127676790