【数据结构】单链表

大家好!今天我们来学习数据结构中的单链表。(链表能够按需申请释放,我们这里讲解的是不带哨兵位的单链表哦~)

目录

1. 链表的概念及结构

2. 链表的分类

2.1 单向或者双向

2.2 带头或者不带头(哨兵位)

2.3 循环或非循环​

3. 单链表的定义

4. 单链表的接口实现

4.1 动态申请一个新结点

4.2 打印单链表

4.3 尾插数据

4.4 头插数据

4.5 尾删数据

4.6 头删数据

4.7 查找指定数据

4.8 在指定位置之前插入数据

4.9 在指定位置之后插入数据

4.10 删除指定位置

4.11 删除指定位置的后一个位置

4.12 销毁单链表

5. 单链表的完整代码

5.1 SList.h

5.2 SList.c

5.3 Test.c

6. 总结


1. 链表的概念及结构

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

中的指针链接次序实现的 。

链表由一系列结点链表中每一个元素称为结点)组成,结点可以在运行时动态生成。

每个结点包括两个部分:一个是存放数据元素的数据域,另一个是存储下一个结点地址的指针域

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

假设在32位系统上,结点中值域为int类型,则一个结点的大小为8个字节,则也可能有下述链表:

2. 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:

2.1 单向或者双向

2.2 带头或者不带头(哨兵位)

2.3 循环或非循环

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1.  无头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶,图的邻接表等等。

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构带来很多优势,实现反而简单了。

3. 单链表的定义

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;  //存放下一个结点的地址
}SLTNode;

使用结构体创建一个单链表

SLTDataType替换int,方便对不同类型的数据进行修改。

SLTNode替换struct SListNode,方便简洁。

data是结点的数据域*next是结点的指针域

4. 单链表的接口实现

单链表的所有接口函数一览:

//动态申请一个新结点
SLTNode* BuySListNode(SLTDataType x);
//打印单链表
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);

//在pos位置之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos位置之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置
void SLTErase(SLTNode** phead, SLTNode* pos);
//删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos);
//销毁单链表
void SLTDestroy(SLTNode** pphead);

这些接口函数主要实现了单链表的增删改查等功能,接下来我们一一实现这些函数!

4.1 动态申请一个新结点

我们每次给链表插入数据时,都需要动态开辟空间申请结点。所以我们将这个过程封装成函数,方便后续使用。

我们使用malloc()函数动态开辟一块空间表示新结点newnode,malloc函数返回一个void*类型的指针,指向分配的内存块的起始位置。如果内存分配失败,则返回一个空指针NULL。

所以我们要判断newnode是否为空指针NULL如果newnode是空指针,则用perror()函数打印相关错误,并用exit(-1)退出程序

如果newnode不为空,我们就用newnodedata赋值。又因为这是新开辟的结点,我们暂时将newnodenext指向空

//动态申请一个新结点
SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

4.2 打印单链表

遍历单链表,依次打印单链表的元素。

我们定义一个结构体类型的指针cur,让cur一开始指向头结点。当cur不为空时,输出cur指向的结点的值(cur->data),然后让cur指向下一个结点(cur=cur->next),依次进行,直到cur为空指针时停止。

//打印单链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

4.3 尾插数据

尾插,就是要先找到单链表的尾结点,然后将数据插入到这个结点之后

我们要先判断原链表是否为空,如果为空。我们就直接将链表的头指针指向要插入的数据。

(因为我们要改变链表的头指针,也就是要改变结构体的指针,所以我们要使用二级指针)。

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL)
	{
		//改变的结构体的指针,所以要用二级指针
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//改变的结构体,用结构体指针即可
		tail->next = newnode;
	}
}

4.4 头插数据

头插,只需要申请新结点,然后让新结点的next指向链表的头再让新结点成为链表的头即可。

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

4.5 尾删数据

尾删,就是将最后一个结点的空间释放。并让倒数第二个结点指向NULL。

也就是我们需要知道链表的倒数第二个结点,这里我们使用了一个结构体指针tail用于遍历找到尾结点,还有一个结构体指针tailPrev,用于保存遍历时当前的结点。

通过while(tail->next!=NULL)让tail进行遍历找到尾结点,此时tailPrev保存的就是倒数第二个结点。

但是我们要注意两个特殊情况

1. 如果链表原本为空,那么就不能进行尾删,需要使用assert()进行断言。

2. 如果链表只有一个结点,我们直接将这个结点释放,并且将头结点置空。

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	//1、空
	assert(*pphead);

	//2、一个节点
	//3、一个以上节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tailPrev = *pphead;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tailPrev = tail;
			tail = tail->next;
		}
		free(tail);
		tailPrev->next = NULL;
	}
}

else中的代码也可以写成如下:

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

4.6 头删数据

头删就是将第一个结点释放,然后让头指针指向原先第二个头结点

我们这里引入一个指针变量newhead,用SLTNode *newhead=(*pphead)->next存原先第二个结点,等free()释放完第一个结点后,让头指针指向newhead

需要注意的是,和尾删一样。如果链表原本为空,那么就不能进行头删,需要使用assert()进行断言。

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

4.7 查找指定数据

定义一个结构体指针cur来遍历链表,先将cur指向头结点,如果链表某个结点的值和要查找的数据相等(cur->data==x)就返回cur,否则使用cur=cur->next继续遍历查找。

如果遍历完链表也没有找到返回空指针NULL

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

4.8 在指定位置之前插入数据

在指定位置pos前插入数据,就需要遍历链表找到pos的前一个位置,我们用结构体指针posPrev存pos位置的前一个位置。 同时创建一个新结点newnode,让posPrev的next指向newnode,而newnode的next指向pos。

这里也需要注意:

(1)既然是在pos位置之前插入数据,那么pos得是存在的,也就是pos不能为空。

(2)如果pos正好是头结点,相当于进行头插,我们可以复用之前头插操作的代码。

//在pos位置之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		SLTNode* newnode = BuySListNode(x);
		posPrev->next = newnode;
		newnode->next = pos;
	}
}

4.9 在指定位置之后插入数据

在指定位置pos之后插入数据比较简单,不需要遍历链表。直接创建一个新结点,让新结点的next指向pos的next再让pos的next指向newnode即可。

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

4.10 删除指定位置

删除指定的结点pos,也需要遍历链表。同时用结构体指针posPrev保存pos的前一个结点,然后将posPrev的next指向pos的next

这里也需要注意:

(1)既然是删除指定位置pos,那么pos得是存在的,也就是pos不能为空。

(2)如果pos正好是头结点,相当于进行头删,我们可以复用之前头删操作的代码。

//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
	}
}

4.11 删除指定位置的后一个位置

删除指定位置的后一个位置比较简单,不需要遍历链表。只需要定义一个结构体指针posNext用于保存指定位置pos的后一个位置,然后将pos的next指向posNext的next,再用free()释放posNext最后将posNext置空即可。

需要注意的是,这里我们也需要用assert()断言,当pos为空时断言。

如果pos为尾节点时也需要断言。

//删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//检查pos是否是尾节点
	assert(pos->next);

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

4.12 销毁单链表

销毁链表,就是将所有结点所占的内存释放最后将头指针置空

//销毁单链表
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

5. 单链表的完整代码

5.1 SList.h

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

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;  //存放下一个结点的地址
}SLTNode;

//动态申请一个新结点
SLTNode* BuySListNode(SLTDataType x);
//打印单链表
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);

//在pos位置之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在pos位置之后插入x
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos位置
void SLTErase(SLTNode** phead, SLTNode* pos);
//删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos);
//销毁单链表
void SLTDestroy(SLTNode** pphead);

5.2 SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"

//动态申请一个新结点
SLTNode* BuySListNode(SLTDataType x)
{
	SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}
	newnode->data = x;
	newnode->next = NULL;
	return newnode;
}

//打印单链表
void SLTPrint(SLTNode* phead)
{
	SLTNode* cur = phead;
	while (cur != NULL)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = BuySListNode(x);
	if (*pphead == NULL)
	{
		//改变的结构体的指针,所以要用二级指针
		*pphead = newnode;
	}
	else
	{
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		//改变的结构体,用结构体指针即可
		tail->next = newnode;
	}
}

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

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead);
	//1、空
	assert(*pphead);

	//2、一个节点
	//3、一个以上节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* tailPrev = *pphead;
		SLTNode* tail = *pphead;
		while (tail->next != NULL)
		{
			tailPrev = tail;
			tail = tail->next;
		}
		free(tail);
		tailPrev->next = NULL;
	}
}

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

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

//在pos位置之前插入x
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		SLTNode* newnode = BuySListNode(x);
		posPrev->next = newnode;
		newnode->next = pos;
	}
}

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

//删除pos位置
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* posPrev = *pphead;
		while (posPrev->next != pos)
		{
			posPrev = posPrev->next;
		}
		posPrev->next = pos->next;
		free(pos);
	}
}

//删除pos的后一个位置
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	//检查pos是否是尾节点
	assert(pos->next);

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

//销毁单链表
void SLTDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* cur = *pphead;
	while (cur)
	{
		SLTNode* next = cur->next;
		free(cur);
		cur = next;
	}
	*pphead = NULL;
}

5.3 Test.c

#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"

void TestSList()
{
	SLTNode* plist = NULL;
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPushBack(&plist, 5);
	SLTPrint(plist);

	SLTPushFront(&plist, 10);
	SLTPushFront(&plist, 20);
	SLTPushFront(&plist, 30);
	SLTPushFront(&plist, 40);
	SLTPrint(plist);

	SLTPopFront(&plist);
	SLTPrint(plist);

	SLTPopBack(&plist);
	SLTPrint(plist);

	SLTNode* pos = SLTFind(plist, 2);
	if (pos)
	{
		pos->data *= 100;
	}
	SLTPrint(plist);

	pos = SLTFind(plist,10);
	SLTInsert(&plist, pos, 39);
	SLTPrint(plist);

	SLTInsertAfter(pos, 56);
	SLTPrint(plist);
	
	pos = SLTFind(plist, 20);
	SLTErase(&plist, pos);
	SLTPrint(plist);

	pos = SLTFind(plist, 3);
	SLTEraseAfter(pos);
	SLTPrint(plist);

	SLTDestroy(&plist);
}

int  main()
{
	TestSList();
	return 0;
}

6. 总结

 到这里,我们就用C语言实现了数据结构中的单链表。有什么问题欢迎在评论区讨论。如果觉得文章有什么不足之处,可以在评论区留言。如果喜欢我的文章,可以点赞收藏哦!

评论 30
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值