前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单链表介绍

单链表介绍

作者头像
zxctscl
发布2024-01-22 21:58:41
1120
发布2024-01-22 21:58:41
举报
文章被收录于专栏:zxctscl个人专栏zxctscl个人专栏

1. 链表的概念

1.1 链表

链表也是线性表的一种。

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

链表火车
链表火车

链表的结构跟火车车厢相似,每节车厢都是独立存在的,需要将火车的某节车厢去掉/加上,不会影响其他车厢。

1.2 链表的结构

链表
链表

与顺序表不同的是,链表里的每节"车厢"都是独立申请下来的空间,我们称之为“结点/节点”。 节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)。

结构
结构

图中指针变量 plist保存的是第一个节点的地址,我们称plist此时“指向”第一个节点,如果我们希望plist“指向”第二个节点时,只需要修改plist保存的内容为0x0012FFA0。

为什么还需要指针变量来保存下一个节点的位置? 链表中每个节点都是独立申请的(即需要插入数据时才去申请一块节点的空间),我们需要通过指针变量来保存下一个节点位置才能从当前节点找到下一个节点。

链表节点的结构

代码语言:javascript
复制
struct SListNode
{
        int data;
        struct SListNode* next;
}

当我们想要保存一个整型数据时,实际是向操作系统申请了一块内存,这个内存不仅要保存整型数据,也需要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。

当我们想要从第一个节点走到最后一个节点时,只需要在前一个节点拿上下一个节点的地址(下一个节点的钥匙)就可以了。

2. 链表的实现

2.1 链表打印

2.1.1 构造一个链表

test.c文件中 先要用malloc向操作系统申请一块空间,然后让第一个节点的数据等于1.

代码语言:javascript
复制
	SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
	node1->data = 1;

然后用同样的方法再申请三个节点

代码语言:javascript
复制
SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
	node1->data = 1;
	SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
	node2->data = 2;
	SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
	node3->data = 3;
	SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));
	node4->data = 4;

用图展示就是这样

实现
实现

然后让node1保存node2的地址,再让node2保存node3的地址,再让node3保存node4的地址,最后让node4的next置为NULL

用代码实现

代码语言:javascript
复制
    node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;
2.1.2 打印链表

先定义一个变量让它指向第一个节点。

代码语言:javascript
复制
    SLNode* plist = node1;
    SLPrint(plist);

将链表的地址传过去。 然后在构造打印函数,因为之后可能还涉及到链表的打印,所以定义了一个SList.c用来存储链表所用到的函数。

2.1.2.1 打印链表函数

这里用循环来实现打印,用可以用phead直接来访问。但是注意,当代码走到第14行的时候,此时phead已经变成NULL了。若代码没写完,我还要继续使用指向第一个节点的地址时,这时我就找不到第一个节点的地址。 所以用一个新的指针变量phead来存储。

代码语言:javascript
复制
void SLPrint(SLNode* phead) 
{
	SLNode* pcur = phead;
	while (pcur)
	{
		printf("%d ->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

最终可以得到这样

结果
结果

2.2 链表的尾插

2.2.1 分析

在实现链表尾插时我们需要考虑两点: 1.链表为空时,申请新节点,新节点作为头节点。

空

2.当链表不为空时,申请新节点,原链表中的为节点的next指针指向新节点的地址。

尾插
尾插
2.2.2 尾插函数代码

通过指针pcur = pcur->next,跳往下一个节点。

代码语言:javascript
复制
void SLPushBack(SLNode** pphead, SLDataType x) {
	SLNode* node = SLBuyNode(x);
	if (*pphead == NULL) 
	{
		*pphead = node;
		return;
	}
	//说明链表不为空,找尾
	SLNode* pcur = *pphead;
	while (pcur->next)
	{
		pcur = pcur->next;
	}
	pcur->next = node;
}

2.3 链表的头插

2.3.1 分析
头插
头插

在链表第一个节点之前插入x=4。 先让nodenext保存phead,再让4成为新的头节点。

插入
插入

而当链表为空时也同样适用。

2.3.2 头插函数代码
代码语言:javascript
复制
void SLPushFront(SLNode** pphead, SLDataType x)
 {
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	//新节点跟头结点连接起来
	node->next = *pphead;//plist
	//让新的节点成为头结点
	*pphead = node;
}

实现的结果

结果实现
结果实现

2.4 链表的尾删

2.4.1 分析

要实现链表的尾删除,首先要找到尾节点。 用cur指针找,还需要记录尾节点的前面一个节点用prev记录。然后将prevnext置为NULL

图
2.4.2 尾删函数代码
代码语言:javascript
复制
void SLPopBack(SLNode** pphead) 
{
	assert(pphead);//第一个节点不能为空
	assert(*pphead);//只有一个节点的情况
	if ((*pphead)->next == NULL)
	 {
		//直接把头结点删除
		free(*pphead);
		*pphead = NULL;
	}
	else 
	{
		//有多个节点的情况
		//找尾结点和尾结点的前一个节点
		SLNode* prev = NULL;
		SLNode* ptail = *pphead;
		while (ptail->next != NULL)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev的next指针不再指向ptail,而是指向ptail的下一个节点
		prev->next = ptail->next;
		free(ptail);
		//为什么必须要置为空?代码后面明明没有再使用ptail?打印链表方法里有判断节点的地址是否为空
		ptail = NULL;
	}
}

2.5 链表的头删

2.5.1 分析
头

首先使用临时节点的指针指向头节点,然后将头节点指向新的头,最后把临时指针指向的节点释放掉。

一个
一个

当只有一个节点时也可以处理

2.5.2 头删函数代码
代码语言:javascript
复制
void SLPopFront(SLNode** pphead)
 {
	assert(pphead);
	assert(*pphead);
	SLNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;//出于代码规范
}

2.6 链表的节点前面插入

2.6.1 分析

在pos前插入一个x=11,先找到pos前的一个节点,然后处理2和3。将prev->next=node;再将node->next=pos注意在这里prev->next=nodenode->next=pos,位置可以互换,因为3的位置已经用pos来记录了,不会造成丢失。

查找
查找

不考虑链表为空时的情况。 当链表里面只有一个节点时,就相当于pos刚好是头结点,实行头插。

图
2.6.2 节点前面插入函数代码
代码语言:javascript
复制
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
 {
	assert(pphead);
	//约定链表不能为空,pos也不能为空
	assert(pos);
	assert(*pphead);
	SLNode* node = SLBuyNode(x);
	//pos刚好是头结点,头插
	if (pos == *pphead) {
		node->next = *pphead;
		*pphead = node;
		return;
	}
	//找pos的前一个节点
	SLNode* prev = *pphead;
	while (prev->next != pos)//第一次循环的时候(缺少pos刚好是第一个节点的判断)
	{
		prev = prev->next;
	}
	//prev node  pos
	node->next = pos;
	prev->next = node;
}

2.7 链表的节点后面插入

2.7.1 分析
图

与链表的节点前面插入不同的是,prev->next=nodenode->next=pos,位置不可以互换,因为没有记录3节点的位置。 所以必须先执行node->next=pos再执行prev->next=node

2.7.2 节点后面插入函数代码
代码语言:javascript
复制
void SLInsertAfter(SLNode* pos, SLDataType x) {
	assert(pos);
	SLNode* node = SLBuyNode(x);
	//pos  node  pos->next
	node->next = pos->next;
	pos->next = node;
}

2.7 链表的删除

2.7.1 删除pos位置节点
2.7.1.1 分析

首先我们要先找到pos节点和找pos的前一个节点prev。执行prev->next = pos->next,再实现free(pos)

pos
pos
2.7.1.2 删除pos位置节点函数
代码语言:javascript
复制
void SLErase(SLNode** pphead, SLNode* pos) 
{
	assert(pphead);
	assert(*pphead);
	assert(pos);

	if (pos == *pphead) {
		*pphead = (*pphead)->next;
		free(pos);
		return;
	}
	//找pos的前一个节点
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	//prev pos pos->next
	prev->next = pos->next;
	free(pos);
	pos = NULL;//只是规范
}
2.7.2 删除pos位置之后节点
2.7.2.1 分析
图

先判断pospos的下一个节点是否为空。 用del存储pos的下一个节点,然后执行pos->next = del->next;最后free(del)

2.7.1.2 删除pos位置之后节点函数
代码语言:javascript
复制
void SLEraseAfter(SLNode* pos)
 {
	assert(pos && pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
}

2.8 链表的销毁

2.8.1 分析
销毁
销毁

通过循环从前往后依次进行,而在销毁节点之前先把下一个个节点保存下来。

2.8.2 销毁函数
代码语言:javascript
复制
void SLDesTroy(SLNode** pphead) {
	assert(pphead);
	SLNode* pcur = *pphead;
	//循环删除
	while (pcur)
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

3. 附代码

SList.h

代码语言:javascript
复制
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//定义链表节点的结构
typedef int SLDataType;
typedef struct SListNode {
	SLDataType data;//要保存的数据
	struct SListNode* next;
}SLNode;

//我来创建几个结点组成一个链表,并打印链表
void SLPrint(SLNode* phead);

//尾插
void SLPushBack(SLNode** pphead, SLDataType x);
void SLPushFront(SLNode** pphead, SLDataType x);
//删除
void SLPopBack(SLNode** pphead);
void SLPopFront(SLNode** pphead);

//找节点,这里的第一个参数写一级指针还是二级指针
//这里传一级实际上就可以了,因为不改变头结点
//但是这里要写二级指针,因为要保持接口一致性
SLNode* SLFind(SLNode** pphead, SLDataType x);

//指定位置的插入删除
//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x);
//在指定位置之后插入数据
void SLInsertAfter(SLNode* pos, SLDataType x);
//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos);
//删除pos之后节点
void SLEraseAfter(SLNode* pos);

//销毁链表
void SLDesTroy(SLNode** pphead);

SList.c

代码语言:javascript
复制
#include"SList.h"

void SLPrint(SLNode* phead) {
	//循环打印
	//可以用phead直接来访问,但是注意,当代码走到
	//第14行的时候,此时phead已经变成NULL了
	//若代码没写完,我还要继续使用指向第一个节点的地址时,这时我就
	//找不到第一个节点的地址
	SLNode* pcur = phead;
	while (pcur != NULL)
	{
		printf("%d ->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}
SLNode* SLBuyNode(SLDataType x) {
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	node->data = x;
	node->next = NULL;
	return node;
}
//尾插
void SLPushBack(SLNode** pphead, SLDataType x) {
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	if (*pphead == NULL) {
		*pphead = node;
		return;
	}
	//说明链表不为空,找尾
	SLNode* pcur = *pphead;
	while (pcur->next)
	{
		pcur = pcur->next;
	}
	pcur->next = node;
}
void SLPushFront(SLNode** pphead, SLDataType x) {
	assert(pphead);
	SLNode* node = SLBuyNode(x);
	//新节点跟头结点连接起来
	node->next = *pphead;//plist
	//让新的节点成为头结点
	*pphead = node;
}
void SLPopBack(SLNode** pphead) {
	assert(pphead);
	//第一个节点不能为空
	assert(*pphead);
	//只有一个节点的情况
	if ((*pphead)->next == NULL) {
		//直接把头结点删除
		free(*pphead);
		*pphead = NULL;
	}
	else {
		//有多个节点的情况
		//找尾结点和尾结点的前一个节点
		SLNode* prev = NULL;
		SLNode* ptail = *pphead;
		while (ptail->next != NULL)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev的next指针不再指向ptail,而是指向ptail的下一个节点
		prev->next = ptail->next;
		free(ptail);
		//为什么必须要置为空?代码后面明明没有再使用ptail?打印链表方法里有判断节点的地址是否为空
		ptail = NULL;
	}
}
void SLPopFront(SLNode** pphead) {
	assert(pphead);
	assert(*pphead);
	SLNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
	del = NULL;//出于代码规范
}

//查找第一个为X的节点
SLNode* SLFind(SLNode** pphead, SLDataType x) {
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == x) {
			return pcur;
		}
		pcur = pcur->next;
	}
	return NULL;
}


//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x) {
	assert(pphead);
	//约定链表不能为空,pos也不能为空
	assert(pos);
	assert(*pphead);
	SLNode* node = SLBuyNode(x);
	//pos刚好是头结点,头插
	if (pos == *pphead) {
		node->next = *pphead;
		*pphead = node;
		return;
	}
	//找pos的前一个节点
	SLNode* prev = *pphead;
	while (prev->next != pos)//第一次循环的时候(缺少pos刚好是第一个节点的判断)
	{
		prev = prev->next;
	}
	//prev node  pos
	node->next = pos;
	prev->next = node;
}
//在指定位置之后插入数据
void SLInsertAfter(SLNode* pos, SLDataType x) {
	assert(pos);
	SLNode* node = SLBuyNode(x);
	//pos  node  pos->next
	node->next = pos->next;
	pos->next = node;
}
//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos) {
	assert(pphead);
	assert(*pphead);
	assert(pos);

	if (pos == *pphead) {
		*pphead = (*pphead)->next;
		free(pos);
		return;
	}
	//找pos的前一个节点
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	//prev pos pos->next
	prev->next = pos->next;
	free(pos);
	pos = NULL;//只是规范
}
//删除pos之后节点
void SLEraseAfter(SLNode* pos) {
	assert(pos && pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
}
//销毁链表
void SLDesTroy(SLNode** pphead) {
	assert(pphead);
	SLNode* pcur = *pphead;
	//循环删除
	while (pcur)
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

test.c

代码语言:javascript
复制
#include"SList.h"

void slttest() {
	SLNode* node1 = (SLNode*)malloc(sizeof(SLNode));
	node1->data = 1;
	SLNode* node2 = (SLNode*)malloc(sizeof(SLNode));
	node2->data = 2;
	SLNode* node3 = (SLNode*)malloc(sizeof(SLNode));
	node3->data = 3;
	SLNode* node4 = (SLNode*)malloc(sizeof(SLNode));
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	//打印链表
	SLNode* plist = node1;
	SLPrint(plist);
}

void slttest01() {
	SLNode* plist = NULL;
	//尾插
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);//1->2->3->4->NULL
	SLPrint(plist);
	//SLPushFront(&plist, 1);
	//SLPushFront(&plist, 2);
	//SLPushFront(&plist, 3);
	//SLPushFront(&plist, 4);//4->3->2->1->NULL
	//尾删
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//SLPopBack(&plist);
	//头删
	//SLPopFront(&plist);
	//SLPopFront(&plist);
	//SLPopFront(&plist);
	//SLPopFront(&plist);
	//SLPopFront(&plist);
	//SLNode* find = SLFind(&plist, 4);
	//SLInsert(&plist, find,11);//1->11->2->3->4->NULL
	//在指定位置之后插入数据
	//SLInsertAfter(find, 100);
	//删除pos位置的节点
	//SLErase(&plist, find);//1->2->3->NULL
	//删除pos之后的节点
	//SLEraseAfter(find);
	//销毁链表
	SLDesTroy(&plist);
	SLPrint(plist);
}
int main() {
	//slttest();
	slttest01();
	return 0;
}
本文参与?腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-10-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客?前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与?腾讯云自媒体分享计划? ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. 链表的概念
    • 1.1 链表
      • 1.2 链表的结构
      • 2. 链表的实现
        • 2.1 链表打印
          • 2.1.1 构造一个链表
          • 2.1.2 打印链表
        • 2.2 链表的尾插
          • 2.2.1 分析
          • 2.2.2 尾插函数代码
        • 2.3 链表的头插
          • 2.3.1 分析
          • 2.3.2 头插函数代码
        • 2.4 链表的尾删
          • 2.4.1 分析
          • 2.4.2 尾删函数代码
        • 2.5 链表的头删
          • 2.5.1 分析
          • 2.5.2 头删函数代码
        • 2.6 链表的节点前面插入
          • 2.6.1 分析
          • 2.6.2 节点前面插入函数代码
        • 2.7 链表的节点后面插入
          • 2.7.1 分析
          • 2.7.2 节点后面插入函数代码
        • 2.7 链表的删除
          • 2.7.1 删除pos位置节点
          • 2.7.2 删除pos位置之后节点
        • 2.8 链表的销毁
          • 2.8.1 分析
          • 2.8.2 销毁函数
      • 3. 附代码
        • SList.h
          • SList.c
            • test.c
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
            http://www.vxiaotou.com