【数据结构初阶】--单链表(一)

发布于:2025-07-13 ⋅ 阅读:(12) ⋅ 点赞:(0)

🔥个人主页:@草莓熊Lotso

🎬作者简介:C++研发方向学习者

📖个人专栏: 《C语言》 《数据结构与算法》《C语言刷题集》《Leetcode刷题指南》

⭐️人生格言:生活是默默的坚持,毅力是永久的享受。  

前言:前面我们完成了顺序表的学习以及代码实现,大家一定要自己去实现一遍试试看并且画图理解。那么这篇博客我将会继续为大家分享单链表的定义以及其中打印,尾插,头擦,尾删,头删等接口的实现。还是一样,先分部分实现,最后展式总的代码。


目录

一.单链表的概念

二.单链表的打印

三.单链表的尾插

四.单链表的头插 

五.单链表的尾删

六.单链表的头删

七.代码展现

SList.h:

SList.c:

 test.c:


一.单链表的概念

--在之前我们学习了逻辑结构和物理结构都是线性的顺序表,但是我们会发现顺序表有以下3个比较明显的缺陷

  1. 中间/头部的插入删除,时间复杂度为O(N)
  2. 增容需要申请新空间,拷贝数据,释放旧空间,有不小的消耗
  3. 增容一般呈两倍的增长,会有一定的空间浪费

那么我们的链表刚好可以使头部插入删除的时间复杂度为O(1),不需要增容也不存在空间的浪费。但是这里需要声明一下,每个数据结构都是有它的优势和缺陷的。

链表:链表是一种是一种物理存储结构上非连续,非顺序的存储结构,但它的逻辑结构还是线性的。我们可以把链表想象成火车的一节节车厢链接在一起,具体形式如同所示这里不过多的去讲解了,这里的图是逻辑结构,正常来说链表不一定是这样连续的,而是在堆区中随意存放的,通过存储的地址找到下一个结点。

我们今天一起来实现一个单链表,链表是由一个一个节点组成的,它的节点由两个组成部分

  1. 保存的数据
  2. 指针,存放的是下一个结点的地址 

我们先在.h文件中定义一个单链表,分3个文件以及重命名的好处啥的在顺序表中都讲过了,这里就不说了。

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

typedef int SLTDataType;
typedef struct SListNode
{
	SLTDataType data;
	struct SListNode* next;
}SLTNode;

二.单链表的打印

 --为了方便后续的观察,我们先来实现一个单链表的打印

SList.c:

//打印
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;//移动pcur,保证头指针phead位置不变
	while (pcur!= NULL)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;//pcur往前走
	}
	printf("NULL\n");
}

重点提示:

 打印的实现还是很简单的,这里先用一个pcur指针存下头指针,移动它去打印,打印完一个数据后就利用pcur->next往前走,直到pcur==NULL。

--我们在test.c文件中创建几个节点,并且把它们打印出来看看吧,一定注意看注释 

test.c: 

#include"SList.h"

int test1()
{
	//创建节点
	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);
}

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

--测试完成没问题,能正确打印出想要的链表 ,退出码为0。


三.单链表的尾插

--在实现单链表的尾插之前,我们先封装一个申请新节点的函数,后续头插等接口的实现也会用上

//申请新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newcode = (SLTNode*)malloc(sizeof(SLTNode));
	newcode->data = x;
	newcode->next = NULL;
	return newcode;
}

SList.c:

//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	//申请新节点
	SLTNode* newcode =SLTBuyNode(x);
	//如果头节点为空
	if (*pphead == NULL)
	{
		*pphead = newcode;
	}
	else {
		SLTNode* ptail = *pphead;
		//找到尾节点
		while (ptail->next != NULL)
		{
			ptail = ptail->next;
		}
		//找到了之后链接新节点
		ptail->next = newcode;
	}
}

重点提示: 

  • 我们先判断当头结点为空时,是无法直接尾插的,我们直接令新节点为头结点就行了
  • 再就是我们需要找到尾结点,利用ptail从头开始往后找,如图所示,直到ptail->next==NULL时结束
  • 找到尾结点后,将新节点链接上去就行了
  • 还有就是传参数的时候,一定要传的是plist的地址,用二级指针接收。这样形参的修改才能影响实参的修改

test.c:

#include"SList.h"

int test1()
{
	//创建节点
	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 test2()
{
	//头结点
	SLTNode* plist =NULL;
	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
}
int main()
{
	//test1();
	test2();
	return 0;
}

--测试完成,打印没有问题,退出码为0。 


四.单链表的头插 

SList.c:

//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//申请新节点
	SLTNode* newcode = SLTBuyNode(x);
	newcode->next = *pphead;
	*pphead = newcode;
}

重点提示:

先断言一下pphead不能为空,再就是先用申请的新节点链接上原来的头指针,再令新节点成为头指针就可以了

test.c: 

#include"SList.h"

int test1()
{
	//创建节点
	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 test2()
{
	//头结点
	SLTNode* plist =NULL;
	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	//头插
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
}
int main()
{
	//test1();
	test2();
	return 0;
}

--测试完成,打印没有问题,退出码为0


五.单链表的尾删

SList.c:

//尾删
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	//只有一个节点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else {
		SLTNode* prev = NULL;
		SLTNode* ptail = *pphead;
		//找到尾结点以及尾节点的前一个节点
		while (ptail->next != NULL)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev ptail
		prev->next = NULL;
		free(ptail);
		ptail = NULL;
	}
}

重点提示: 

  • 首先是断言,pphead不能为空,链表也不可以为空
  • 特别判断只有一个节点的情况,这题我们还需要找到尾结点的前一个结点,如果只有一个节点的话直接释放掉就好了
  • 找尾节点的同时找到尾节点的前一个节点,每次尾节点向前走之前,先让prev指向其原来的位置
  • 最后直接让prev->next=NULL,释放掉ptail就好了

test.c:

#include"SList.h"

int test1()
{
	//创建节点
	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 test2()
{
	//头结点
	SLTNode* plist =NULL;
	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	//头插
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	//尾删
	SLTPopBack(&plist);
	SLTPrint(plist);
}
int main()
{
	//test1();
	test2();
	return 0;
}

--测试完成,删掉了4,打印没有问题,退出码为0


六.单链表的头删

SList.c:

//头删
void SLTPopFront(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

重点提示: 

断言和尾删一样,这里主要就是先定义一个next记录phead的下一个节点,再直接free掉*pphead。最后让next成为新的头节点就可以了

test.c:

#include"SList.h"

int test1()
{
	//创建节点
	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 test2()
{
	//头结点
	SLTNode* plist =NULL;
	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	//头插
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	//尾删
	SLTPopBack(&plist);
	SLTPrint(plist);
	//头删
	SLTPopFront(&plist);
	SLTPrint(plist);

}
int main()
{
	//test1();
	test2();
	return 0;
}

--测试完成,删掉了头的5打印没问题,退出码为0


七.代码展现

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;

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

SList.c:

#include"SList.h"

//打印
void SLTPrint(SLTNode* phead)
{
	SLTNode* pcur = phead;//移动pcur,保证头指针phead位置不变
	while (pcur!= NULL)
	{
		printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}
//申请新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
	SLTNode* newcode = (SLTNode*)malloc(sizeof(SLTNode));
	newcode->data = x;
	newcode->next = NULL;
	return newcode;
}
//尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
	//申请新节点
	SLTNode* newcode =SLTBuyNode(x);
	//如果头节点为空
	if (*pphead == NULL)
	{
		*pphead = newcode;
	}
	else {
		SLTNode* ptail = *pphead;
		//找到尾节点
		while (ptail->next != NULL)
		{
			ptail = ptail->next;
		}
		//找到了之后链接新节点
		ptail->next = newcode;
	}
}
//头插
void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	assert(pphead);
	//申请新节点
	SLTNode* newcode = SLTBuyNode(x);
	newcode->next = *pphead;
	*pphead = newcode;
}
//尾删
void SLTPopBack(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	//只有一个节点时
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else {
		SLTNode* prev = NULL;
		SLTNode* ptail = *pphead;
		//找到尾结点以及尾节点的前一个节点
		while (ptail->next != NULL)
		{
			prev = ptail;
			ptail = ptail->next;
		}
		//prev ptail
		prev->next = NULL;
		free(ptail);
		ptail = NULL;
	}
}
//头删
void SLTPopFront(SLTNode** pphead)
{
	//链表不能为空
	assert(pphead && *pphead);
	SLTNode* next = (*pphead)->next;
	free(*pphead);
	*pphead = next;
}

 test.c:

#include"SList.h"

int test1()
{
	//创建节点
	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 test2()
{
	//头结点
	SLTNode* plist =NULL;
	//尾插
	SLTPushBack(&plist, 1);
	SLTPushBack(&plist, 2);
	SLTPushBack(&plist, 3);
	SLTPushBack(&plist, 4);
	SLTPrint(plist);
	//头插
	SLTPushFront(&plist, 5);
	SLTPrint(plist);
	//尾删
	SLTPopBack(&plist);
	SLTPrint(plist);
	//头删
	SLTPopFront(&plist);
	SLTPrint(plist);

}
int main()
{
	//test1();
	test2();
	return 0;
}

往期回顾: 

【数据结构初阶】--算法复杂度的深度解析

【数据结构初阶】--顺序表(一)

【数据结构初阶】--顺序表(二)

【数据结构初阶】--顺序表(三)

结语:在这篇博客中,我们完成了单链表的打印,尾/头插,尾/头删这些接口的实现,但是我们单链表还有部分接口没有实现,由于篇幅原因,博主将在下一篇中继续带大家实现单链表指定位置插入,删除,查找,修改等接口。如果文章对你有帮助的话,欢迎评论,点赞,收藏加关注,感谢大家的支持。


网站公告

今日签到

点亮在社区的每一天
去签到