C语言.数据结构.单链表

数据结构.单链表

1.链表的概念及结构

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

  • 链表的结构跟火车车厢相似,淡季时车次的车厢会相应减少,旺季时车次的车厢会额外增加几节。只需要将火车里的某节车厢去掉 / 加上,不会影响其他车厢,每节车厢都是独立存在的
  • 车厢是独立存在的,且每节车厢都有车门。想象一下这样的场景,假设每节车厢的车门都是锁上的状态,需要不同的钥匙才能解锁,每次只能携带一把钥匙的情况下如何从车头走到车尾?

最简单的做法:每节车厢里都放一把下一节车厢的钥匙

在链表里,每节"车厢"是什么样的呢?

  1. 与顺序表不同的是,链表里的每一个"车厢 "都是独立申请下来的空间,称之为"节点/结点"。
  2. 节点的组成主要有两个部分:当前节点要保存的数据和保存下一个节点的地址(指针变量)。
  3. 图中的指针 plist 保存的是第一个节点的地址,称之为"指向"第一个节点,如果希望 plist "指向"第二个节点时,只需要修改plist保存的内容0x0012FFA0

为什么还需要指针变量来保存下一个节点的位置?

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

结合前面学过的结构体知识,可以给出每个节点对应的结构体代码:

假设当前保存的节点为整型:

c 复制代码
struct SListNode
{
	int data;//节点数据
	struct SListNode* next;//指针变量用保存下一个节点的地址
};
  • 当想要保存一个整型数据时,实际是向操作系统申请一块内存,这个内存不仅要保存整型数据,也要保存下一个节点的地址(当下一个节点为空时保存的地址为空)。

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

给定的链表结构中,如何实现节点从头到尾的打印?

c 复制代码
void SListTest01()
{
	//链表是由一个个节点组成
	//创建几个节点
	SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
	node1->data = 1;

	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	node2->data = 2;

	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	node3->data = 3;

	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	node4->data = 4;

	//将四个节点连接起来:
	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;

	//调用链表的打印
	SLTNode* plist = node1;
	SLTPrint(plist);
}

思考:当想要保存的数据类型为字符串类型、浮点型或者其他自定义类型时,该如何修改?

补充说明:

  1. 链式结构在逻辑上是连续的,在物理结构上不一定连续
  2. 节点一般是从上申请的
  3. 从堆上申请的空间,是按照一定的策略分配出来的,每次申请的空间可能连续,可能不连续。

2.单链表的实现

2.1链表的打印

c 复制代码
//链表的打印
void SLTPrint(SLTNode* phead)
{
	//相当于遍历链表
	SLTNode* pcur = phead;
	while (pcur)//等价于pcur != NULL
	{
		printf("%d->", pcur->data);
		//让pcur走到下一个节点
		pcur = pcur->next;
	}
	printf("NULL\n");
}

图文理解:

2.2节点的申请

c 复制代码
//节点的申请
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;
}

2.3单链表的尾插

c 复制代码
//单链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//*pphead就是指向第一个节点的指针
	//空链表与非空链表
	//假如是空链表,就说明*pphead是头结点的指针,也是尾节点的指针
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找到尾节点
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			//遍历链表
			ptail = ptail->next;
		}
		//ptail指向的就是尾节点
		ptail->next = newnode;
	}
}

图文理解:

2.4单链表的头插

c 复制代码
//单链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	//直接把新节点插在头节点之前
	newnode->next = *pphead;
	//再让*pphead走到newnode的位置
	*pphead = newnode;
}

图文理解:


解释:

情况1就是正常的头插,没有问题。情况2头插的是空链表,也符合要求。

2.5单链表的尾删

c 复制代码
//单链表的尾删
void SLTPopBack(SLTNode** pphead)
{
	//既然要删除节点,那链表不能为空
	assert(pphead && *pphead);
	//链表只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//链表不止一个节点
	else
	{
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		while(ptail->next)
		{
			//prev为ptail前一个指针
			prev = ptail;
			ptail = ptail->next;
		}
		free(ptail);
		ptail = NULL;
		
		prev->next = NULL;
	}
}

图文理解:

2.6单链表的头删

c 复制代码
//单链表的头删
void SLTPopFront(SLTNode** pphead)
{
	//既然要删除节点,那链表不能为空
	assert(pphead && *pphead);
	//先把下一个节点的指针存储下来,防止等到删除掉头节点之后,找不到后面的节点
	SLTNode* next = (*pphead)->next;//->的优先级高于*
	free(*pphead);
	//让*pphead走到next位置,变成新的头节点
	*pphead = next;
}

图文理解:

2.7单链表节点的查找

c 复制代码
//单链表节点的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	//本质也是遍历单链表
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//到这一步,已经证明找不到x了
	return NULL;
}

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

c 复制代码
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	//若pos == *pphead;说明是头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else 
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//就是把新节点newnode插在prev与pos中间:
		//prev -> newnode -> pos
		newnode->next = pos;
		prev->next = newnode;
	}
}

图文理解:

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

c 复制代码
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	//在pos之后插入数据,证明这个链表就不为空,不用判断
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	//就是在pos与pos->next之间插入新的节点:
	//pos -> newnode -> pos->next
	newnode->next = pos->next;
	pos->next = newnode;
}

图文理解:

2.10删除pos节点

c 复制代码
//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos是头节点 / 不是头节点
	if (pos == *pphead)
	{
		//调用头删的方法
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//就是在pos与pos->next删除节点
		//prev -> pos -> pos->next
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

图文理解:

2.11删除pos之后的节点

c 复制代码
//删除pos之后的节点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos && pos->next);
	SLTNode* del = pos->next;
	//就是在pos与del->next删除节点
	//pos -> del -> del->next
	pos->next = del->next;
	free(del);
	del = NULL;
}

图文理解:

2.12单链表的销毁

c 复制代码
//单链表的销毁
void SListDestroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		//从第一个节点开始销毁,每销毁一个节点之前,先保存当前节点的下一个节点的指针,
		//否则销毁当前结点的时候,后面的节点找不到了
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = NULL;
	}
	//把头节点手动置为空
	*pphead = NULL;
}

2.13整体代码展示

SList.h

c 复制代码
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

//定义结点的结构
//数据+下一个结点的地址
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);

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

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

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

//单链表的销毁
void SListDestroy(SLTNode** pphead);

SList.c

c 复制代码
#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)//等价于pcur != NULL
	{
		printf("%d->", pcur->data);
		//让pcur走到下一个节点
		pcur = pcur->next;
	}
	printf("NULL\n");
}

//单链表的尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//*pphead就是指向第一个节点的指针
	//空链表与非空链表
	//假如是空链表,就说明*pphead是头结点的指针,也是尾节点的指针
	SLTNode* newnode = SLTBuyNode(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
		//找到尾节点
		SLTNode* ptail = *pphead;
		while (ptail->next)
		{
			//遍历链表
			ptail = ptail->next;
		}
		//ptail指向的就是尾节点
		ptail->next = newnode;
	}
}

//单链表的头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	SLTNode* newnode = SLTBuyNode(x);
	//直接把新节点插在头节点之前
	newnode->next = *pphead;
	//再让*pphead走到newnode的位置
	*pphead = newnode;
}

//单链表的尾删
void SLTPopBack(SLTNode** pphead)
{
	//既然要删除节点,那链表不能为空
	assert(pphead && *pphead);
	//链表只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	//链表不止一个节点
	else
	{
		SLTNode* prev = *pphead;
		SLTNode* ptail = *pphead;
		while(ptail->next)
		{
			//prev为ptail前一个指针
			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位置,变成新的头节点
	*pphead = next;
}

//单链表节点的查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	//本质也是遍历单链表
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//到这一步,已经证明找不到x了
	return NULL;
}



//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	//若pos == *pphead;说明是头插
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
	else 
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//就是把新节点newnode插在prev与pos中间:
		//prev -> newnode -> pos
		newnode->next = pos;
		prev->next = newnode;
	}
}

//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	//在pos之后插入数据,证明这个链表就不为空,不用判断
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	//就是在pos与pos->next之间插入新的节点:
	//pos -> newnode -> pos->next
	newnode->next = pos->next;
	pos->next = newnode;
}

//删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos);
	//pos是头节点 / 不是头节点
	if (pos == *pphead)
	{
		//调用头删的方法
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		//就是在pos与pos->next删除节点
		//prev -> pos -> pos->next
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}

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

//单链表的销毁
void SListDestroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		//从第一个节点开始销毁,每销毁一个节点之前,先保存当前节点的下一个节点的指针,
		//否则销毁当前结点的时候,后面的节点找不到了
		SLTNode* next = pcur->next;
		free(pcur);
		pcur = NULL;
	}
	//把头节点手动置为空
	*pphead = NULL;
}

test.c

c 复制代码
#include "SList.h"

void SListTest02()
{
	SLTNode* plist = NULL;
	//1.单链表的尾插
	SLTPushBack(&plist, 1);
	SLTPrint(plist);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	
	//2.单链表的头插
	SLTPushFront(&plist, 4);
	SLTPushFront(&plist, 3);
	SLTPushFront(&plist, 2);
	SLTPushFront(&plist, 1);
	SLTPrint(plist);

	//3.单链表的尾删
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPopBack(&plist);
	SLTPrint(plist);

	//4.单链表的头删
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPopFront(&plist);
	SLTPrint(plist);

	//5.单链表节点的查找
	SLTNode* find = SLTFind(plist, 1);
	if (find == NULL)
	{
		printf("找不到!\n");
	}
	else
	{
		printf("找到了!\n");
	}

	//6.在指定位置之前插入数据
	SLTNode* find = SLTFind(plist, 3);
	SLTInsert(&plist, find, 6);
	SLTPrint(plist);

	//7.在指定位置之前插入数据
	SLTNode* find = SLTFind(plist, 3);
	SLTInsertAfter(find, 6);
	SLTPrint(plist);

	//8.删除pos节点
	SLTNode* find = SLTFind(plist, 3);
	SLTErase(&plist, find);
	SLTPrint(plist);

	//9.删除pos之后的节点
	SLTNode* find = SLTFind(plist, 3);
	SLTEraseAfter(find);
	SLTPrint(plist);

	//10.单链表的销毁
	SListDestroy(&plist);
	SLTPrint(plist);
}
int main()
{
	SListTest02();
	return 0;
}

3.链表的分类

链表的结构非常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:

链表说明:

虽然有这么多的链表的结构,但是实际上中最常用还是两种结构:单链表和双向带头循环链表。

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

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

相关推荐
xieliyu.19 分钟前
Java手搓二叉树:基础遍历与核心操作全解析
java·开发语言·数据结构·学习
期待のcode27 分钟前
Redis数据类型
运维·数据结构·redis
博界IT精灵36 分钟前
图的遍历(哈喜老师)
数据结构·考研·算法·深度优先
谙弆悕博士1 小时前
Lua学习笔记
c语言·开发语言·笔记·学习·lua·创业创新·业界资讯
qq3862461961 小时前
C语言中将数字转换为字符串的方法
c语言·格式化输出·字符串转换·sprintf·snprintf
所以遗憾是什么呢?1 小时前
【题解】Codeforces Round 1097 (Div. 2, Based on Zhili Cup 2026) (致理杯) ABCDEF
数据结构·算法·acm·codeforces·icpc·ccpc·xcpc
LuminousCPP2 小时前
C 语言动态内存管理全解析:从基础函数到柔性数组与内存分区
c语言·经验分享·笔记·学习·柔性数组
Lazionr2 小时前
【栈与队列经典OJ】
c语言·数据结构
夏日听雨眠2 小时前
数据结构(哈希函数)
数据结构·算法·哈希算法
诙_2 小时前
C++数据结构--B树,B+树,B*树
数据结构·b树