文章目录
单链表
观看这里的uu建议先看顺序表
线性表之顺序表点击这里
一、前言
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列等。
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
链表是典型的线性表之一,以链式结构的形式存储。
链表又被分为单链表和双链表。
本篇仅对单链表详细说明。
二、单链表
1·单链表的概念及其结构
概念:单链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针单向链接次序实现的。
逻辑结构图如下:
根据单链表的逻辑结构可以想到单链表这种结构体中包含存入的数据和指向下一个结构体的结构体指针。
物理结构图如下:
单链表需要增加空间存储新数据时只需要动态申请一个结构体大小的空间即可。对空间的使用率非常高。完美解决了顺序表频繁扩容浪费空间的缺点。
2·单链表的分类
实际中要实现的单链表的结构非常多样,以下情况组合起来就有很多种单链表结构:
-
不带头、带头非循环单链表
-
单链表、循环单链表
-
无头单向非循环链表
虽然有这么多的单链表结构,但是在实际中最常用的是无头单向非循环链表
无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。
单链表是否带头的区别:当不带头要改变头指针的指向时,那么就需要传二级指针形参来改变头指针的指向;当带头时只需要传一级指针,操作头节点来处理后续数据的操作即可。
单链表是否循环的区别:当非循环时头节点与尾节点没有逻辑上的联系,某些操作会不方便;当循环时头节点与尾节点有逻辑上的联系,某些操作会方便许多。
说明:
单链表无论带头或不带头、循环或非循环的实现都大同小异,这里只详细说明不带头+单向+非循环链表的实现。
是否带头的唯一区别:
3·单链表接口函数
单链表头文件Slist.h的声明如下:
#pragma once
#include <stdio.h>
#include <stdlib.h>
// 无头+单向+非循环链表增删查改实现
typedef int SLTDateType;
//无头+单向+非循环链表结构体
typedef struct SListNode
{
//存储的数据
SLTDateType data;
//指向下一个结构体数据的结构体指针
struct SListNode* next;
}SLNode;
// 动态申请一个节点
SLNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SLNode* phead);
// 单链表尾插
void SListPushBack(SLNode** pphead, SLTDateType x);
// 单链表的头插
void SListPushFront(SLNode** pphead, SLTDateType x);
// 单链表的尾删
void SListPopBack(SLNode** pphead);
// 单链表头删
void SListPopFront(SLNode** pphead);
// 单链表查找
SLNode* SListFind(SLNode* phead, SLTDateType x);
// 单链表在pos位置之后插入x
void SListInsertAfter(SLNode* pos, SLTDateType x);
// 单链表在pos位置之后删除x
void SListEraseAfter(SLNode* pos);
// 单链表在pos位置插入x
void SListInsert(SLNode** pphead, SLNode* pos, SLTDateType x);
// 单链表删除pos位置的值
void SListErase(SLNode** pphead, SLNode* pos);
//释放链表
void SListDestroy(SLNode** pphead);
三、单链表的实现
说明:以下函数的实现思路在代码注释中已详细解释。
1·申请新节点、打印链表和释放链表
申请新节点函数在Slist.c中如下:
// 动态申请一个节点,返回一个节点首地址
SLNode* BuySListNode(SLTDateType x)
{
//开辟一个sizeof(SLNode)的新节点
SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
//开辟失败则报错且终止程序
if (newnode == NULL)
{
printf("malloc fail");
exit(-1);
}
//开辟成功则进行赋值
newnode->data = x;
newnode->next = NULL;
return newnode;
}
打印链表函数在Slist.c中如下:
// 单链表打印
/*只需要输出当前指针解引用data后的值,一级指针传参*/
void SListPrint(SLNode* plist)
{
SLNode* cur = plist;
/*当当前指针cur不为空时说明还未到尾部,
此时打印当前数据并将当前的指针cur移动至下一个结构体以此循环*/
while (cur != NULL)
{
printf("%d->", cur->data);
//cur->next与plist的区别就是plist是首字节的指针
cur=cur->next;
}
printf("NULL");
}
说明:申请节点只要插入节点都会用到,因此申请节点在尾部插入节点、头部插入节点和任意位置插入节点据函数中嵌套使用。
释放链表函数在Slist.c中如下:
//释放链表
void SListDestroy(SLNode** pphead)
{
SLNode* cur = *pphead;
while (cur != NULL)
{
//保存当前要释放的下一节点的位置
SLNode* next = cur->next;
//释放当前节点
free(cur);
//转移当前节点
cur = next;
}
*pphead = NULL;
}
以上函数的具体实现情况尾部、头部和任意位置操作时大同小异且比较简单,不做具体说明。
2·尾部插入节点&尾部删除节点
尾部插入节点函数在Slist.c中如下:
// 单链表尾插
/*先申请一个新节点后分两种情况*/
/*1·链表中有数据,此时不需要二级指针的参与,定义一个结构体指针tail,用遍历移动的方式指向尾节点,
用tail指针修改尾节点next的值为新节点的地址,即可将尾节点与新节点链接/
/*2·链表中无数据,此时需要用二级指针传参是因为当链表中无数据时,
单纯的一级指针传参无法改变头指针plist指针的指向,
因此需要用二级指针解引用来修改头指针的指向为新节点的地址*/
//实现单链表尾部插入不是只有这一种方式的函数,这里采取的只是最典型的一种方式
void