单链表详解(不带头单向不循环链表)

一.链表

链表也是线性表的一种,其结构特征是在逻辑结构上是连续的,但在物理结构上不是连续的。链表的组成是由一个一个的节点组成的。单个节点内有要储存的数据以及指向下一个结点的指针,即数据域与指针域。链表中每个节点都是独立申请的(即需要插入数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。如图所示

结合前面我们所学习的结构体的知识,给出每个节点所对应的结构体代码。假设当前保存的数据类型是SLDataType型

//首先我们先定义一个节点
typedef int SLDataType;
typedef struct SLNode {
	SLDataType data;//数据域
	struct SLNode* next;//指针域
}SLNode;

当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数 据,也需要保存下⼀个节点的地址(当下⼀个节点为空时保存的地址为空)。 当我们想要从第⼀个节点走到最后⼀个节点时,只需要在前⼀个节点拿上下⼀个节点的地址(下一个 节点的钥匙)就可以了。

二.详细实现链表的增删擦改等等功能

2.0 首先在测试文件中创建一个节点

//SLTest.c

SLNode* plist = NULL;

2.1 头文件

先给出头文件,列出我们即将实现的功能

//SList.h

typedef int SLDataType;
typedef struct SLNode {
	SLDataType data;//数据域
	struct SLNode* next;//指针域
}SLNode;

//链表的打印
void SLPrint(SLNode* phead);

//链表的头插和尾插
void SLPushFront(SLNode** phead,SLDataType x);
void SLPushBack(SLNode** phead,SLDataType x);

//链表的头删尾部删除
void SLPopFront(SLNode** pphead);
void SLPopBack(SLNode** pphead);

//查找
void SLFind(SLNode** phead, SLDataType x);

//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x);

//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos);

//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x);

//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos);

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

2.2 实现

//SList.c

2.2.1 实现链表的打印

要想实现链表的打印,就要遍历整个链表,在传参是应该将节点的地址传过来,用一个循环即可

//链表的打印
void SLPrint(SLNode* phead)
{
	SLNode* pcur = phead;
	while (pcur)
	{
		printf("%d->",pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}

我们一般不直接使用头节点地址,而是设置一个临时变量,让这个临时变量遍历链表

while(pcur)-->遍历整个链表    while(pcur->next)-->找到末尾节点

2.2.2 链表的头插和尾插

1.链表的头插

有一点需要我们特别是注意,我们把newnode赋值给了phead,相当于改变了头指针的指向,那我们这时只传结构体的地址是不够的,我们需要把指向结构体指针的地址也传过来(二级指针pphead),来改变结构体指针的指向

还有一点--》无论是头插还是尾插,我们都需要申请一个新节点,我们简洁性,我们给一些一个函数来申请一个新节点SLBuyNode

SLBuyNewNode函数

//创建新节点
SLNode* SLBuyNewNode(SLDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	newnode->data = x;
	newnode->next = NULL;//不管链表为不为空,newnode的next一定时空指针
	return newnode;
}

现在头插分为两种请况,链表为空或者链表不为空,两种结果是一样的

void SLPushFront(SLNode** pphead,SLDataType x)
{
	assert(pphead);
	SLNode* newnode = SLBuyNewNode(x);
	//链表为空或不为空都是一样的,新节点的next为原本的头指针,再把next赋为新的头节点
	newnode->next = *pphead;
	*pphead = newnode;
}

若链表为空,也就是*pphead=NULL;这时把newnode->next = *pphead;也是正确的;

2.链表的尾插

void SLPushBack(SLNode** pphead,SLDataType x)
{
	assert(pphead);
	SLNode* newnode = SLBuyNewNode(x);
	//当链表为空时,newnode成为头节点
	if (*pphead == NULL)
	{
		*pphead = newnode;//改编身命运,让newnode指向的空间成为头节点
		return;
	}
	//链表不为空时,找出尾部节点
	//最好不要动头节点,防止待会要用而找不到位置
	SLNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;
}

2.2.3 链表的头部删除与尾部删除

由于删除后要改变指向同时要释放空间,还是传二级指针,并且链表不能为空

1.头部删除

a.链表不管有几个节点直接让下一个节点成为头节点就好

!!注意->的优先级高于*,注意括号!!

//链表的头删尾部删除
void SLPopFront(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	//注意 ->的优先级高于*
	//不管有几个节点,只需让第二个节点成为新的头节点即可
	SLNode* next = (*pphead)->next;//记录下第二个节点的位置,否则释放后将找不到第二个节点了
	free(*pphead);
	*pphead = next;//让第二个节点成为空指针
}

2.尾部删除

首先,肯定不能是空链表,分为只有一个节点或者不止一个节点

a.只有一个节点--即(*phead)->next == NULL,可直接释放头节点,在把头节点置为空即可

b.不止一个节点--先找到末尾节点和末尾节点的前一个结点(用快慢双指针)

​
void SLPopBack(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLNode* ptail = *pphead;
	//找到末尾节点前的一个节点,释放malloc所得节点
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//while (pcur->next->next != NULL)//必须有两个以上节点
	//{
	//	pcur = pcur->next;
	//}
	//另一种好阅读的写法\双指针(快慢指针)
	SLNode* prev = NULL;
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = NULL;
	//销毁尾部节点
	free(ptail);
	ptail = NULL;
}

​

2.2.3 链表的查找

链表的查找是通过data来确定其位置,所以我们可以遍历链表找到指向data数据的指针,在返回该指针即可

//查找(可以不穿二级指针,但是为了一致性,还是传了)
SLNode* SLFind(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	//遍历整个链表查找x,
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if ((pcur)->data == x)
		{
			printf("链表中存在该数据\n");
			return pcur;
		}
		pcur = pcur->next;
	}
	printf("查找的数据不存在\n");
}


2.2.4.在指定位置之前插入数据

//首先,不能是空指针,但我们只用判断pos位置是否为空即可,pos不为空-->链表不可能为空

由于是在指定位置之前插入数据,我们需要得到指定位置的前一个位置的指针(prev)

//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	 SLNode* newnode = SLBuyNewNode(x);
	 //pos刚好是头节点
	 if (pos == *pphead)
	 {
		 newnode->next = pos;
		 pos->next = NULL;
		 return;
	 }
	//先遍历链表找到前面一个数据的指针
	 SLNode* prev = *pphead;
	 while (prev->next != pos)
	 {
		 prev = prev->next;
	 }
	 prev->next = newnode;
	 newnode->next = pos;
}

2.2.5 在指定位置之后插入节点

//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x)
{
	//pos存在说明链表不会为空并且链表一定存在,因为pos处有数据
	assert(pos);
	SLNode* newnode = SLBuyNewNode(x);
	//由于设置的newnode->NULL 无论pos在哪里,下面代码都通用
    newnode->next = pos->next;
    pos->next = newnode;
}

2.2.6 删除pos节点

还是只用检查pos节点是否为空

//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	//若是头节点,直接头删
	if (pos == *pphead)
	{
		SLPopFront(pphead);
		return;
	}
	//不是头节点,取得pos之前位置的节点
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

2.2.7 删除pos位置之后的节点

先把pos->next = pe=os->next->next;

再把pos->next 释放即可,注意要将指针置为空,避免出现野指针(结构体中定义过的next)

//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos)
{
	//还是只要pos存在,链表就存在
	assert(pos&&pos->next!=NULL);
	pos->next = pos->next->next;
	free(pos->next);
	pos->next = NULL;//指针是存在的
}

2.2.8 销毁链表

即遍历链表,将链表中的所有空间全部释放

//销毁链表
void SLDestory(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	//需要遍历链表,一个一个释放,但又不能释放完上一个就找不到下一个了
	//需要一个辅助指针
	SLNode* pcur = *pphead;
	while (*pphead )
	{
		pcur = *pphead;
		(*pphead) = (*pphead)->next;
		free(pcur);
	}
	pcur = NULL;

}

以上就是全部的实现啦!

三.全部代码

以下是全部的代码

//头文件SList.h

#pragma once
#include <stdio.h>
#include<stdlib.h>
#include<assert.h>
//思考,都已经有顺序表来储存数据了,为什么还要用单链表呢?
//1.顺序表扩大倍数时可能会浪费内存,有没有另一种数据结构能够避免呢?
//2.顺序表的删除和插入需要挪动后方/前方的所有数据,太麻烦
//所以就请出我们的单链表
//单链表也是线性表的一种,在逻辑上是连续的,在物理结构上是非连续的
//链表右节点构成,每个节点分为数据域和指针域,数据域储存数据,指针域储存指向下一个节点的地址
//这样做不需要扩大内存,按需申请节点即可
//下面我将用单链表实现数据的增删查改

//首先我们先定义一个节点
typedef int SLDataType;
typedef struct SLNode {
	SLDataType data;//数据域
	struct SLNode* next;//指针域
}SLNode;

//链表的打印
void SLPrint(SLNode* phead);

//链表的头插和尾插
void SLPushFront(SLNode** phead,SLDataType x);
void SLPushBack(SLNode** phead,SLDataType x);

//链表的头删尾部删除
void SLPopFront(SLNode** pphead);
void SLPopBack(SLNode** pphead);

//查找
void SLFind(SLNode** phead, SLDataType x);

//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x);

//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos);

//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x);

//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos);

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

//实现文件SList.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SLlist.h"
//单链表这里指的是单项不循环无头链表,不需初始化
// node -- 我啥也改变不了,我只是一个结构体
// &node也就是phead(一级指针) -- 可以改变node中的next和data
// &phead(二级指针) --可以改变自身指向的空间,决定自己的命运
//链表的打印
void SLPrint(SLNode* phead)
{
	SLNode* pcur = phead;
	while (pcur)
	{
		printf("%d->",pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}
//创建新节点
SLNode* SLBuyNewNode(SLDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	newnode->data = x;
	newnode->next = NULL;//不管链表为不为空,newnode的next一定时空指针
	return newnode;
}
//链表的头插和尾插
void SLPushFront(SLNode** pphead,SLDataType x)
{
	assert(pphead);
	SLNode* newnode = SLBuyNewNode(x);
	//链表为空或不为空都是一样的,新节点的next为原本的头指针,再把next赋为新的头节点
	newnode->next = *pphead;
	*pphead = newnode;
}
void SLPushBack(SLNode** pphead,SLDataType x)
{
	assert(pphead);
	SLNode* newnode = SLBuyNewNode(x);
	//当链表为空时,newnode成为头节点
	if (*pphead == NULL)
	{
		*pphead = newnode;//改编身命运,让newnode指向的空间成为头节点
		return;
	}
	//链表不为空时,找出尾部节点
	//最好不要动头节点,防止待会要用而找不到位置
	SLNode* ptail = *pphead;
	while (ptail->next)
	{
		ptail = ptail->next;
	}
	ptail->next = newnode;
}

//链表的头删尾部删除
void SLPopFront(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	//注意 ->的优先级高于*
	//不管有几个节点,只需让第二个节点成为新的头节点即可
	SLNode* next = (*pphead)->next;//记录下第二个节点的位置,否则释放后将找不到第二个节点了
	free(*pphead);
	*pphead = next;//让第二个节点成为空指针
}
void SLPopBack(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SLNode* ptail = *pphead;
	//找到末尾节点前的一个节点,释放malloc所得节点
	//只有一个节点
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//while (pcur->next->next != NULL)//必须有两个以上节点
	//{
	//	pcur = pcur->next;
	//}
	//另一种好阅读的写法\双指针(快慢指针)
	SLNode* prev = NULL;
	while (ptail->next)
	{
		prev = ptail;
		ptail = ptail->next;
	}
	prev->next = NULL;
	//销毁尾部节点
	free(ptail);
	ptail = NULL;
}

//查找(可以不穿二级指针,但是为了一致性,还是传了)
void SLFind(SLNode** pphead, SLDataType x)
{
	assert(pphead);
	//遍历整个链表查找x,
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if ((pcur)->data == x)
		{
			printf("链表中存在该数据\n");
			return;
		}
		pcur = pcur->next;
	}
	printf("查找的数据不存在\n");
}

//在指定位置之前插入数据
void SLInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
	assert(pphead);
	assert(pos);
	 SLNode* newnode = SLBuyNewNode(x);
	 //pos刚好是头节点
	 if (pos == *pphead)
	 {
		 newnode->next = pos;
		 pos->next = NULL;
		 return;
	 }
	//先遍历链表找到前面一个数据的指针
	 SLNode* prev = *pphead;
	 while (prev->next != pos)
	 {
		 prev = prev->next;
	 }
	 prev->next = newnode;
	 newnode->next = pos;
}


//在指定位置之后插入数据
void SLInsertAfter( SLNode* pos, SLDataType x)
{
	//pos存在说明链表不会为空并且链表一定存在,因为pos处有数据
	assert(pos);
	SLNode* newnode = SLBuyNewNode(x);
	//由于设置的newnode->NULL 无论pos在哪里,下面代码都通用
    newnode->next = pos->next;
    pos->next = newnode;
}

//删除pos节点
void SLErase(SLNode** pphead, SLNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	//若是头节点,直接头删
	if (pos == *pphead)
	{
		SLPopFront(pphead);
		return;
	}
	//不是头节点,取得pos之前位置的节点
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{
		prev = prev->next;
	}
	prev->next = pos->next;
	free(pos);
	pos = NULL;
}

//删除pos位置之后的节点
void SLEraseAfter(SLNode** pphead, SLNode* pos)
{
	//还是只要pos存在,链表就存在
	assert(pos&&pos->next!=NULL);
	pos->next = pos->next->next;
	free(pos->next);
	pos->next = NULL;//指针是存在的
}

//销毁链表
void SLDestory(SLNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	//需要遍历链表,一个一个释放,但又不能释放完上一个就找不到下一个了
	//需要一个辅助指针
	SLNode* pcur = *pphead;
	while (*pphead )
	{
		pcur = *pphead;
		(*pphead) = (*pphead)->next;
		free(pcur);
	}
	pcur = NULL;

}

//测试文件SLTest.c

#define _CRT_SECURE_NO_WARNINGS 1
#include "SLlist.h"

void SLTest()
{

	/*SLNode* plist1 = (SLNode*)malloc(sizeof(SLNode));
	SLNode* plist2 = (SLNode*)malloc(sizeof(SLNode));
	SLNode* plist3 = (SLNode*)malloc(sizeof(SLNode));
	SLNode* plist4 = (SLNode*)malloc(sizeof(SLNode));
	plist1->next = plist2;
	plist2->next = plist3;
	plist3->next = plist4;
	plist4->next = NULL;
	plist1->data = 1;
	plist2->data = 2;
	plist3->data = 3;
	plist4->data = 4;*/
	SLNode* plist = NULL;
	SLPushBack(&plist, 1);
	SLPushBack(&plist, 2);
	SLPushBack(&plist, 3);
	SLPushBack(&plist, 4);
	/*SLPushBack(NULL, 4);*/
	SLPushFront(&plist, 5);
	SLPrint(plist);
	/*SLPopBack(&plist);
	SLPopBack(&plist);
	SLPopBack(&plist);*/
	SLPopFront(&plist);
	SLPopFront(&plist);
	SLPrint(plist);
	SLFind(&plist, 5);
	SLFind(&plist, 3);
	SLDestory(&plist);
	SLPrint(plist);
}
int main()
{
	SLTest();
	return 0;
}

四.结束啦!一起进步,越来越好!

  • 24
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
不带头结点单向循环链表可以通过以下步骤进行建立: 1. 定义链表节点结构体,包含数据域和指向下一个节点的指针; 2. 定义一个指向链表首节点的指针,初始值为NULL; 3. 读入第一个节点的数据,创建一个节点,将其作为链表首节点,指向自身,将其指针赋值给链表首指针; 4. 读入下一个节点的数据,创建一个节点,将其插入到链表的尾部,即将链表尾节点的指针指向新节点,新节点的指针指向首节点,完成插入操作; 5. 重复执行步骤4,直到读取完所有节点的数据。 具体实现可以参考以下代码: ```c typedef struct Node { int data; // 数据域 struct Node *next; // 指向下一个节点的指针 } Node; int main() { Node *head = NULL; // 链表首指针初始化为NULL Node *tail = NULL; // 链表尾指针初始化为NULL int n; // 节点个数 int data; // 数据 scanf("%d", &n); // 读入节点个数 for (int i = 0; i < n; i++) { scanf("%d", &data); // 读入节点数据 Node *node = (Node*)malloc(sizeof(Node)); // 创建新节点 node->data = data; // 赋值数据域 node->next = NULL; // 新节点的next指针初始化为NULL if (head == NULL) { // 如果链表为空,将新节点作为首节点 head = node; node->next = head; // 新节点的next指针指向自身 tail = head; // 将尾指针指向首节点 } else { // 如果链表不为空,将新节点插入到尾部 node->next = head; // 新节点的next指针指向首节点 tail->next = node; // 将尾节点的next指针指向新节点 tail = node; // 将尾指针指向新节点 } } return 0; } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值