图片出处:The world's biggest drone photo and video sharing platform | SkyPixel.com
在上两篇博文中,我写了顺序表及以顺序表为底层结构实现通讯录项目的相关内容,这都是线性表的一种,本文将详细介绍另一种线性表数据结构——链表。
目录
前言
链表的概念及结构:
概念:
结构:
链表的实现:
(1)头文件
1))链表单个节点结构创建
2))声明链表的各个接口
(2)源文件
1))打印链表
2))尾插
1/创建一个新的节点
2/尾插
3))头插
4))尾删
5))头删
6))在指定节点之前插入
1/查找某值在链表中第一次出现的节点
2/在指定节点之前插入
7))在指定节点之后插入
8)) 删除pos位置节点
9)) 删除pos位置之后节点
10))销毁链表
单链表源码:
(1)头文件源码
(2)源文件源码
链表是线性表的一种,其逻辑结构是线性,物理存储结构是非连续、非顺序的。
链表分为八种,本文讲解的是无头单向不循环链表,简称单链表。
链表的结构跟火车车厢相似,淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只 需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢。
⻋厢是独⽴存在的,且每节⻋厢都有⻋⻔。想象⼀下这样的场景,假设每节⻋厢的⻋⻔都是锁上的状 态,需要不同的钥匙才能解锁,每次只能携带⼀把钥匙的情况下如何从⻋头⾛到⻋尾? 最简单的做法:每节⻋厢⾥都放⼀把下⼀节⻋厢的钥匙。
那么在链表中,每节车厢是什么样的呢?
如上便是链表的结构示意图。与顺序表不同的是,链表的每节“车厢”都是独立申请下来的空间,我们称之为“节点”/“结点”,而每个节点中保存下一节点地址的指针变量便是钥匙。
节点的组成主要有两部分:当前节点的要保存的数据和保存下一节点的地址(指针变量)。
那么为什么需要指针变量保存下一个节点的位置呢?
答:在链表结构中,每个节点都是独立申请的空间,而节点与节点之间唯一的联系便是节点中存储的下一节点地址,我们只有通过该地址才能找到下一个节点。这样的设计可以使我们无需连续的空间也可以实现存储并管理数据,相较于顺序表,不仅省去了调整空间与拷贝数据的过程,更是大大提高了空间的利用率。
当大家对链表有了初步的认识后,便可跟着我的思路来尝试一步一步构思并实现链表这一数据结构。按照惯例,本次依旧是按照声明与实现过程分离的方式进行,这样更有益于搭建代码的整体框架。
在头文件中,我们需要做三件事:
- 包含所要用到的库函数头文件
- 创建链表单个节点的结构
- 声明链表的各个接口
包含头文件我省略不讲,接下来直接开始创建链表单个节点的结构。
到此,我们需要构思一下,在该链表中我们需要对其实现一些申明接口呢?
在本文我需要实现的接口如下:
- 打印链表
- 尾插
- 头插
- 尾删
- 头删
- 指定节点之前插入
- 指定节点之后插入
- 删除pos位置节点
- 删除pos位置之后节点
- 销毁链表
在头文件中声明如下:(形参为初步设想,最终头文件以文末源码为准)
此处我在略微解释一下上图中谈到的程序接口传参统一的必要性,举个例子:若是我们写了一个程序 交给别人使用,但是在该程序中对同一个参数在不同接口传参的形式各种各样,这就会让使用者很苦恼,所以我们要尽量避免此类情况。
在源文件板块,我会对上述已声明的接口一一实现,同时若是有必要,也会增加其他接口或是改动原接口等等。需要注意的是,我会对在本文中第一次出现的重难点进行讲解,但在讲解之后再遇到同类逻辑则不再提及。
在实现之前,需要在源文件中包含我们所写的头文件,如下:
在实际使用场景下,链表是不需要该接口的,但是在此处实现该接口可以让我们更好地观察链表的增删变化,故将其放在第一个实现。
打印链表接口实现如下:
循环方式图示:
此逻辑在后续接口中将被大量用到,后续将不再做过多解释。
在链表中,不论是尾插、头插、任意插,都需要创建一个新的节点,故我将创建新节点这一重复代码包装为函数,让其它接口可以直接调用。
在实现之前,我需要请大家思考一个问题:在链表中如何指定节点?貌似这确实值得我们深思,链表不同于顺序表,其没有固定的下标。那么我们需要通过什么来指定节点呢?地址?可操作性不强。那就只剩下节点中的值了,我们可以通过查找节点中的值来找到指定节点!
但是使用该方式来指定节点貌似不能称之为指定,因为链表中每个节点的值并不是唯一的,故准确来说,应当是查找该值在链表中第一次出现的节点
因为我们后续会有很多接口需要指定节点,故我将其封装成接口。
需要注意的是:若是需要调用其它指定节点插入/删除...等接口时,需要先调用该接口找到指定节点,再将指定节点传给其他接口。
至此,一个单链表数据结构接口便全部完成,由于链表与之前数组的存储形式不同,故本文中很多地方我都画图解释,希望能对大家有所帮助。
ChainList.h
#pragma once
#include
#include
#include
//定义该顺序表储存的数据类型
typedef int SLDataType;
//定义链表单个节点结构
typedef struct SLNode
{
SLDataType x;//数据
struct SLNode* next;//保存下一个节点地址的指针变量
}SLNode;
//尾插
void SLNPushBack(SLNode** pphead, SLDataType x);
//头插
void SLNPushFront(SLNode** pphead, SLDataType x);
//尾删
void SLNPopBack(SLNode** pphead);
//头删
void SLNPopFront(SLNode** pphead);
//查找某值在链表中第一次出现的节点
SLNode* SLNFind(SLNode** pphead, SLDataType x);
//在指定节点之前插入
void SLNInsert(SLNode** pphead, SLNode* pos, SLDataType x);
//在指定节点之后插入
void SLNInsertAfter(SLNode* pos, SLDataType x);
//删除pos位置节点
void SLNErase(SLNode** pphead, SLNode* pos);
//删除pos位置之后节点
void SLNEraseAfter(SLNode* pos);
//打印链表
void SLNPrint(SLNode** pphead);
//销毁链表
void SLNDstroy(SLNode** pphead);
ChainList.c
#define _CRT_SECURE_NO_WARNINGS 1
#include "ChainList.h"
//打印链表
void SLNPrint(SLNode** pphead)
{
assert(pphead);
//创建临时变量,用以遍历
SLNode* pcur = *pphead;
while (pcur)
{
printf("%d-> ", pcur->x);
pcur = pcur->next;
}
printf("NULL\n");
}
//创建新节点
SLNode* SLNByNode(SLDataType x)
{
SLNode* node = (SLNode*)malloc(sizeof(SLNode));
if (node == NULL)
{
perror("malloc");
return NULL;
}
node->x = x;
node->next = NULL;
return node;
}
//尾插
void SLNPushBack(SLNode** pphead, SLDataType x)
{
SLNode* node = SLNByNode(x);
//不论是头插、尾插、任意插,都需要创建一个新的节点
//故将创建新节点包装成函数,以方便调用
assert(pphead);
//尾插分两种情况
//1,当链表中无节点时
if (*pphead == NULL)
{
*pphead = node;
}
//2,链表中至少有一个节点
else
{
//找尾
SLNode* pcur = *pphead;
while (pcur->next)
{
pcur = pcur->next;
}
pcur->next = node;
}
}
//头插
void SLNPushFront(SLNode** pphead, SLDataType x)
{
assert(pphead);
SLNode* node = SLNByNode(x);
node->next = *pphead;
*pphead = node;
}
//尾删
void SLNPopBack(SLNode** pphead)
{
assert(pphead);
//尾删需要保证其链表至少存在一个节点
assert(*pphead);
//尾删分两种情况:
//1:当需要删除的节点就是首节点时
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
}
//:当链表中至少有两个节点时,需要找尾
else
{
SLNode* pcur = *pphead;
SLNode* prev = NULL;
while (pcur->next)
{
prev = pcur;
pcur = pcur->next;
}
prev->next = pcur->next;
free(pcur);
pcur = NULL;
}
}
//头删
void SLNPopFront(SLNode** pphead)
{
assert(pphead);
assert(*pphead);
SLNode* pcur = *pphead;
*pphead = (*pphead)->next;
free(pcur);
pcur = NULL;
}
//查找某值在链表中第一次出现的节点
SLNode* SLNFind(SLNode** pphead, SLDataType x)
{
assert(pphead);
assert(*pphead);
SLNode* pcur = *pphead;
while (pcur)
{
if (pcur->x == x)
{
return pcur;
}
pcur = pcur->next;
}
return NULL;
}
//在指定节点之前插入
void SLNInsert(SLNode** pphead, SLNode* pos, SLDataType x)
{
assert(pphead);
assert(*pphead);
assert(pos);
SLNode* node = SLNByNode(x);
//在指定节点之前插入分两种情况
//1,指定节点就是头节点
if (pos == *pphead)
{
node->next = *pphead;
*pphead = node;
}
//2,指定节点不是头节点
else
{
//需要找到pos前一个节点
SLNode* pcur = *pphead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = node;
node->next = pos;
}
}
//在指定节点之后插入
void SLNInsertAfter(SLNode* pos, SLDataType x)
{
assert(pos);
SLNode* node = SLNByNode(x);
node->next = pos->next;
pos->next = node;
}
//删除pos位置节点
void SLNErase(SLNode** pphead, SLNode* pos)
{
assert(pphead);
assert(*pphead);
assert(pos);
//删除pos位置的节点分两种情况
//1,当pos为首节点
if (pos == *pphead)
{
*pphead = (*pphead)->next;
free(pos);
pos = NULL;
}
//2,当pos不是首节点
else
{
//找pos前一个节点
SLNode* pcur = *pphead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
pcur->next = pos->next;
free(pos);
pos = NULL;
}
}
//删除pos位置之后节点
void SLNEraseAfter(SLNode* pos)
{
assert(pos && pos->next);
SLNode* del = pos->next;
pos->next = del->next;
free(del);
del = NULL;
}
//销毁链表
void SLNDstroy(SLNode** pphead)
{
assert(pphead);
SLNode* pcur = *pphead;
SLNode* next = NULL;
while (pcur)
{
next = pcur->next;
free(pcur);
pcur = next;
}
*pphead = NULL;
}