【数据结构】单链表及单链表的实现

文章目录

  • 1.单链表
    • [1.1 概念与结构](#1.1 概念与结构)
      • [1.1.1 结点](#1.1.1 结点)
      • [1.1.2 链表的性质](#1.1.2 链表的性质)
      • [1.1.3 链表的打印](#1.1.3 链表的打印)
    • [1.2 实现单链表](#1.2 实现单链表)

1.单链表

1.1 概念与结构

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

淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/ 加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。

在链表⾥,每节"⻋厢"是什么样的呢?

1.1.1 结点

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为"结点/结点"

火车有一节一节车厢组成

链表由一个一个结点组成

结点有两个组成部分:

​ 1)保存的数据

​ 2)指针:始终保存下一个结点的地址

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

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

1.1.2 链表的性质

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续

2、结点⼀般是从堆上申请的

3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:

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

c 复制代码
struct SListNode 
{ 
    int data; //结点数据 
    struct SListNode* next; //指针变量⽤保存下⼀个结点的地址 
}; 

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

1.1.3 链表的打印

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

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

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

复制代码
typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

1.2 实现单链表

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

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

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;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//pphead是一级指针的地址,是二级指针
	//如果pphead为空,我们无法通过pphead来访问头结点指针,也无法修改头结点指针
	//*pphead是指向第一个结点的地址
	
	//申请新空间
	SLTNode* newnode = SLTBuyNode(x);
	//特殊情况,链表为空
	if(*pphead == NULL)//*pphead获取pphead头指针内容
	{
		*pphead = newnode;
	}
	else
	{
		//找链表的尾结点
		SLTNode* ptail = *pphead;
		while (ptail->next != NULL)
		{
			ptail = ptail->next;//这是在往后走
		}
		//找到了尾结点
		ptail->next = newnode;
	}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	//形参为SLTNode** pphead,是因为要对plist(存放的地址)本身进行修改
	assert(pphead);
	//给新结点申请空间
	SLTNode* newnode = SLTBuyNode(x);
	newnode->next = *pphead;//赋值*pphead本身
	*pphead = newnode;//此时newnode是第一个结点
}
//尾删
void SLTPopBack(SLTNode** pphead)//第一个结点可能会被删,会发生改变,所以是二级指针
{
	//链表为空不能删除,也就是第一个结点的地址不能为空
	//也就是说pphead不能为空,因为不能传空指针,第一个结点的地址也不能为空
	assert(pphead&&*pphead);
	//链表只有一个结点的情况
	if ((*pphead)->next == NULL)//先解引用
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		SLTNode* prev = NULL;//最后指向尾结点的前一个结点,为了将前一个结点的next置为空
		SLTNode* ptail = *pphead;//最后指向尾结点
		while (ptail->next)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		prev->next = NULL;
		free(ptail);
		ptail = NULL;
	}	
}
//头删
void SLTPopFront(SLTNode** pphead)
{
	assert(pphead && *pphead);
	//要先让*pphead指向第二个结点,再删除第一个结点
	SLTNode* next = (*pphead)->next;//先将第二个结点的地址存下来,释放掉第一个结点后,给*pphead赋值
	free(*pphead);
	*pphead = next;
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	//不会改变第一个结点的内容,用*phead接收就行
	SLTNode* pcur = phead;
	while (pcur)
	{
		if (pcur->data == x)
			return pcur;
		pcur = pcur->next;
	}
	return NULL;
}
//在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) 
{
	assert(pphead && pos);
	SLTNode* newnode = SLTBuyNode(x);//先创建一个新结点
	if (pos == *pphead)
	{
		//头插
		SLTPushFront(pphead, x);
	}
	else
	{
		//pos的前一个结点
		SLTNode* prev = *pphead;
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}
//在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
	assert(pos);
	SLTNode* newnode = SLTBuyNode(x);
	//先连后断
	newnode->next = pos->next;//先让newnode指向pos的下一个结点,再让pos->next=newnode
	pos->next = newnode;
}

//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead && pos);
	SLTNode* prev = *pphead;
	if (pos == *pphead)//pos是头结点
		SLTPopFront(pphead);//头删
	else 
	{
		while (prev->next != pos)
		{
			prev = prev->next;
		}
		prev->next = pos->next;
		free(pos);
		pos = NULL;
	}
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos&&pos->next);
	SLTNode* del = pos->next;//pos的下一个结点
	//一旦调用 free(pos->next),pos->next 指向的内存就被系统回收了
	//后续再访问 pos->next->next 就是访问已释放内存,会导致未定义行为,所以用del保存
	pos->next = del->next;
	free(del);
	del = NULL;
}

//销毁链表
void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;//先保存下一个结点,再释放
		free(pcur);
		pcur = next;
	}
	//*pphead始终保存第一个结点的地址,但是这个地址已经还给操作系统了
	//*pphead此时是个野指针了,要置为空
	*pphead = NULL;
}
c 复制代码
#pragma once

#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);
//申请一个节点的空间
SLTNode* SLTBuyNode(SLTDataType x);

//尾插一个
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);
c 复制代码
#include"SList.h"

void test01()
{
	//创建一个链表
	SLTNode* node1 =(SLTNode*) malloc(sizeof(SLTNode));
	SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node3 = (SLTNode*)malloc(sizeof(SLTNode));
	SLTNode* node4 = (SLTNode*)malloc(sizeof(SLTNode));
	//存数据
	node1->data = 1;
	node2->data = 2;
	node3->data = 3;
	node4->data = 4;

	node1->next = node2;
	node2->next = node3;
	node3->next = node4;
	node4->next = NULL;
	
	//打印链表
	SLTNode* plist = node1;
	SLTPrint(plist);
}
void test02()
{
	SLTNode* plist = NULL;
	//尾插
	//SLTPushBack(plist, 1);传的是plist存的数据(也就是NULL的地址),而不是plist本身
	//要让phead(形参)的改变影响实参,就要传plist的地址
	//此时形参phead改为pphead,表示phead的地址,数据类型由SLTNode*变为SLTNode**
	//除了特殊情况(数组等)都用&取地址
	
	//SLTPushBack(&plist, 1);
	//SLTPushBack(&plist, 2);
	//SLTPushBack(&plist, 3);
	//SLTPushBack(&plist, 4);
	//SLTPrint(plist);
	
	//尾删
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);
	//SLTPopBack(&plist);
	//SLTPrint(plist);

	//头插
	SLTPushFront(&plist, 1);
	SLTPrint(plist);
	SLTPushFront(&plist, 2);
	SLTPrint(plist);
	SLTPushFront(&plist, 3);
	SLTPrint(plist);
	SLTPushFront(&plist, 4);
	SLTPrint(plist);
	//头删
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	//SLTPopFront(&plist);
	//SLTPrint(plist);
	
	//查找
	SLTNode* pos = SLTFind(plist, 2);
	if (pos)
		printf("找到了\n");
	else
		printf("没找到\n");
	//在pos之前的位置删除一个数据
	//SLTInsert(&plist, pos, 100);
	//SLTPrint(plist);
	
	//删除pos结点
	//SLTErase(&plist, pos);
	//SLTPrint(plist);
	//删除pos之后的结点
	SLTEraseAfter(pos);
	SLTPrint(plist);
	//销毁
	SListDestroy(&plist);
}
int main()
{
	//test01();
	test02();
	return 0;
}
c 复制代码
//当只剩下一个结点时,调用这个函数
void SLTPopBack(SLTNode** pphead)
{
	assert(pphead&&*pphead);
	SLTNode* prev = NULL;
	SLTNode* ptail = *pphead;
	while (ptail->next)//不执行
	{
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = NULL;//当只剩下一个结点时,prev已经是NULL,不能对next进行赋值
	free(ptail);
	ptail = NULL;
}
c 复制代码
void SLTEraseAfter(SLTNode* pos)
{
	assert(pos);
	SLTNode* del = pos->next;//pos的下一个结点
//一旦调用 free(pos->next),pos->next 指向的内存就被系统回收了
//后续再访问 pos->next->next 就是访问已释放内存,会导致未定义行为,所以先用del保存
	pos->next = del->next;
	free(del);
	del = NULL;
}
c 复制代码
//销毁链表
void SListDestroy(SLTNode** pphead)
{
	assert(pphead);
	SLTNode* pcur = *pphead;
	while (pcur)
	{
		SLTNode* next = pcur->next;//先保存下一个结点,再释放
		free(pcur);
		pcur = next;
	}
	//*pphead始终保存第一个结点的地址,但是这个地址已经还给操作系统了
	//*pphead此时是个野指针了,要置为空
	*pphead = NULL;
}

销毁完之后的调试

尾插和尾删的时间复杂度O(N)

头插和头删的时间复杂度O(1)

总结:头插和头删用的多使用链表

尾插和尾删用的多使用顺序表
在指定位置之前插入数据时间复杂度O(n)

在指定位置之前插入数据时间复杂度O(1)

相关推荐
z187461030032 小时前
list(带头双向循环链表)
数据结构·c++·链表
T.Ree.3 小时前
cpp_list
开发语言·数据结构·c++·list
童话ing4 小时前
【Golang】常见数据结构原理剖析
数据结构·golang
是苏浙4 小时前
零基础入门C语言之C语言实现数据结构之顺序表应用
c语言·数据结构·算法
lkbhua莱克瓦245 小时前
Java基础——常用算法3
java·数据结构·笔记·算法·github·排序算法·学习方法
小白程序员成长日记5 小时前
2025.11.07 力扣每日一题
数据结构·算法·leetcode
ohnoooo95 小时前
251106 算法
数据结构·c++·算法
雾岛听蓝6 小时前
算法复杂度解析:时间与空间的衡量
c语言·数据结构·经验分享·笔记
惊讶的猫6 小时前
字符串- 字符串转换整数 (atoi)
数据结构·算法