【(数据结构)— 单链表的实现】

28 篇文章 0 订阅
10 篇文章 0 订阅

一.链表的概念及结构

概念: 链表是⼀种 物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。
在这里插入图片描述

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

最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。

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

在这里插入图片描述

与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为“结点/节点”
节点的组成主要有两个部分:当前节点要保存的数据和保存下⼀个节点的地址(指针变量)。
图中指针变量 plist保存的是第⼀个节点的地址,我们称plist此时“指向”第⼀个节点,如果我们希望plist“指向”第⼆个节点时,只需要修改plist保存的内容为0x0012FFA0。
为什么还需要指针变量来保存下⼀个节点的位置?
链表中每个节点都是独⽴申请的(即需要插⼊数据时才去申请⼀块节点的空间),我们需要通过指针变量来保存下⼀个节点位置才能从当前节点找到下⼀个节点。
结合前⾯学到的结构体知识,我们可以给出每个节点对应的结构体代码:
假设当前保存的节点为整型:

struct SListNode
{
 int data; //节点数据
 struct SListNode* next; //指针变量⽤保存下⼀个节点的地址
};

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

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

在这里插入图片描述

在这里插入图片描述

思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?
补充说明:
1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续
2、节点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续

二.单链表的实现

2.1单链表头文件——功能函数的定义

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

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

//创建几个节点组成链表,并且打印链表
//打印
void SLNPrint(SLNode* phead);

//尾插
void SLNPushBack(SLNode** pphead, SLDatatype x);
//头插
void SLNPushFront(SLNode** pphead, SLDatatype x);
//尾删
void SLNPopBack(SLNode** pphead); 
//头删
void SLNPopFront(SLNode** pphead);
//在指定位置插入删除
// 
//查找指定的pos
SLNode* SLNFind(SLNode** pphead, SLDatatype x);
//在指定位置之前插入
void SLNInsrt(SLNode** pphead, SLNode* pos, SLDatatype x);
//在指定位置之后插入删除
void SLNInsrtAfter(SLNode* pos, SLDatatype x);
//删除指定位置的数据
void SLNErase(SLNode** pphead, SLNode* pos);
//删除指定位置之后的数据
void SLNEraseAfter(SLNode* pos);
//销毁链表
void SLNDestroy(SLNode** pphead);

2.2单链表源文件——功能函数的实现

SList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//打印
void SLNPrint(SLNode* phead)
{
	//循环打印
	//可以用phead直接来访问,但是注意,当代码走到
	//第16行的时候,此时phead已经变成NULL了
	//若代码没写完,我还要继续使用指向第一个节点的地址时,这时我就
	//找不到第一个节点的地址
	//所以为了避免第一个节点被更改,要定义一个新的来代替
	SLNode* pcur = phead;
	while (pcur)
	{
		printf("%d ->", pcur->data);
		pcur = pcur->next;
	}
	printf("NULL\n");
}
//创建新的节点
SLNode* SLByNode(SLDatatype x)
{
	//开辟新的空间
	SLNode* node = (SLNode*)malloc(sizeof(SLNode));
	node->data = x;
	node->next = NULL;
	return node;
}
//尾插
void SLNPushBack(SLNode** pphead, SLDatatype x)
{
	SLNode* node = SLByNode(x);
	//判断链表是否为空,如果为空,直接插入
	if (*pphead == NULL)
	{
		*pphead = node;
		return;
	}

	//说明链表不为空 ,找尾
	SLNode* pcur = *pphead;
	while (pcur->next != NULL)
	{
		pcur = pcur->next;
	}
	pcur->next = node;

}

//头插
void SLNPushFront(SLNode** pphead, SLDatatype x)
{
	//创建新的节点
	SLNode* node = SLByNode(x);
	//让新的节点跟头节点连起来
	node->next = *pphead;
	//再让新节点成为头节点
	*pphead = node;
	
}

//尾删
void SLNPopBack(SLNode** pphead)
{
	//先判断链表是否为空,如果为空就无法进行删除操作
	assert(pphead);
	//还要判断第一个节点不能为空
	assert(*pphead);
	//只有一个节点的情况
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
		return;
	}
	//有多个节点的情况
	//先找到尾节点/尾节点的前一个节点
	SLNode* ptail = *pphead;
	SLNode* prev = NULL;
	while (ptail->next)
	{
		prev = ptail;
		ptail = prev->next;
	}
	//让尾节点的前一个节点指向不再指向尾节点,而是指向尾节点的下一个节点 
	prev->next = ptail->next;
	//然后在删除尾节点
	free(ptail);
	//为什么要将ptaii置为空指针?代码后面明明没有再使用ptail,因为打印链表的代码有判断节点的地址是否为空
	ptail = NULL;

}

void SLNPopFront(SLNode** pphead)
{
	//判断链表不能为空
	assert(pphead);
	//判断头节点不能为空
	assert(*pphead);
	SLNode* del = *pphead;
	*pphead = (*pphead)->next;
	free(del);
	//出于代码规范
	del = NULL;

}


//查找指定的pos
SLNode* SLNFind(SLNode** pphead, SLDatatype x)
{
	assert(pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//出循环,代表没找到
	return NULL;
}
//在指定位置之前插入
void SLNInsrt(SLNode** pphead, SLNode* pos, SLDatatype x)
{
	SLNode* node = SLByNode(x);
	assert(pphead);
	assert(*pphead);
	//只有一个节点的情况 == pos就是头节点,直接进行头插
	if (pos == *pphead)
	{
		node->next = *pphead;
		*pphead = node;
		return;
	}
	//有多个节点的情况
	//找pos的前一个节点
	SLNode* prev = *pphead;
	while (prev->next != pos)
	{

		prev = prev->next;
	}
	//这里的插入节点的处理顺序可以调换
	prev->next = node;
	node->next = pos;

}

//在指定的位置之后插入
void SLNInsrtAfter(SLNode* pos, SLDatatype x)
{
	assert(pos);
	SLNode* node = SLByNode(x);
	// node  pos  node->next
	node->next = pos->next;
	pos->next = node;
}

//删除指定位置的节点
void SLNErase(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->next = pos->next;
	free(pos);
	pos = NULL;
	
}
//删除指定位置之后的节点
void SLNEraseAfter(SLNode* pos)
{
	assert(pos);
	assert(pos->next);
	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
	del = NULL;
}

//销毁链表
void SLNDestroy(SLNode** pphead)
{
	assert(*pphead);
	SLNode* pcur = *pphead;
	while (pcur)
	{
		SLNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	*pphead = NULL;
}

2.3 单链表源文件——功能的测试

test.c
#include"SList.h"



void sllist()
{
	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;
	SLNPrint(plist);
	
	

}
void sllist01()
{
	SLNode* plist = NULL;
	//尾插
	SLNPushBack(&plist, 1);
	SLNPushBack(&plist, 2);
	SLNPushBack(&plist, 3);
	SLNPushBack(&plist, 4);
	//头插 
	/*SLNPushFront(&plist, 1);
	SLNPushFront(&plist, 2);
	SLNPushFront(&plist, 3);
	SLNPushFront(&plist, 4);*/
	//尾删
	//SLNPopBack(&plist);
	//头删
	//SLNPopFront(&plist);

	//在指定位置前插入
	//查找指定的pos
	SLNode* find = SLNFind(&plist, 3);
	//SLNInsrt(&plist, find, 11);
	//在指定位置之后插入
	//SLNInsrtAfter(find, 5);
	//删除指定位置的节点
	//SLNErase(&plist, find);
	//删除指定位置之后的节点
	//SLNEraseAfter(find);
	//销毁链表
	SLNDestroy(&plist);
	SLNPrint(plist);//打印链表
}

2.4单链表测试结果运行展示

1.尾部插入
在这里插入图片描述

2.头部插入
在这里插入图片描述

3.尾部/头部删除
在这里插入图片描述

4.在指定位置前插入
在这里插入图片描述

5.在指定位置后插入
在这里插入图片描述

6.删除指定位置的节点
在这里插入图片描述

7.删除指定位置之后的节点
在这里插入图片描述

8.销毁链表
在这里插入图片描述

3. 链表的分类

链表的结构⾮常多样,以下情况组合起来就有8种(2 x 2 x 2)链表结构:
在这里插入图片描述

链表说明:
在这里插入图片描述
在这里插入图片描述

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:
单链表和双向带头循环链表
1. 无头单向非循环链表:结构简单,⼀般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,⼀般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

  • 33
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 22
    评论
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值