🍗个人主页: 起名字真南 🍟个人专栏:【数据结构初阶】 【C语言】
@TOC
链表的概念
链表是一种物理存储结构上非连续的,无序的结构,数据元素的逻辑顺序是通过链表中的指针链接来实现的
1 链表的结构
链表就类似于小火车的结构由一节一节单独的车厢通过挂钩互相链接,而每一节车厢就相当于链表中的节点,链接的挂钩就是每个节点的指针。
如上图所示:
- 每个节点包含两个部分,分别是数据和指针
- 每个节点中存储的指针是下一个节点的地址
- 最后一个节点中存储的指针为NULL
- 包含一个plist用来指向头指针
- 链式结构在逻辑上是连续的,但是在物理上不一定是
- 每个节点都是在堆上申请出来的
注:该图假设在32位的系统下,节点中存储值(数据域,另外一个是指针域)的类型位int类型,则一个节点的大小为8个字节,其中0x是十六进制的表示方式,四个二进制位表示一个十六进制位,一位(bite)可以表示一个二进制数0/1.在32位系统下一个int类型是4个字节,而当你存储的是指针的话那么指针的大小通常等于系统的大小如果是32位系统则是4个字节.加起来就是一个节点的大小为8个字节
2 链表的分类
2.1 带头链表和不带头链表
2.2 双向链表和单向链表
2.3循环链表和非循环链表
三种两两组合链表的类型一共是8种,不过最常用的还是无头单向非循环链表,带头双向循环链表
1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势。
3 链表的实现
3.1链表的创建
由于链表是由一个一个单独的节点组成的所以我们只需要创建一个节点其中包括其中的值(数据域),下一个节点的指针(指针域)。
typedef int StructDataType;
//定义节点
//包含数据和指针
typedef struct SListNode
{
StructDataType data;
struct SListNode* next;
}SLTnode;
由于链表中储存的数据类型可能会发生变化,所以我们为了后期的修改将类型typedef 为StructDataType。其中指针域内的值是下一个节点的地址所以类型是 struct SListNode* 命名为 next。
3.2创建节点开辟空间
//创建新节点
SLTnode* CreatNode(StructDataType x)
{
SLTnode* newnode = (SLTnode*)malloc(sizeof(SLTnode));
if (newnode == NULL)
{
perror("malloc");
exit(1);
}
newnode->data = x;
newnode->next = NULL;
return newnode;
}
不管是尾插还是头插都会使用malloc用来开辟一块空间,为了增加代码的可读性以及分类管理,避免输入大量的重复代码,所以在.c文件中构造一个函数用来创建新的节点。
3.3打印链表中所有数据
//打印链表中的所有的数据
void SLT_print(SLTnode* phead)
{
assert(phead);
while (phead)
{
printf("%d->", phead->data);
phead = phead->next;
}
printf("NULL\n");
}
想要打印链表中的所有数据只需要将链表遍历在打印,所以我们只需要一个遍历链表的方法,其中使用了while循环如果phead不为空的话则会执行循环,每循环一次 phead = phead->next,就会执行一次。其中**->**是引用结构体中的元素。
3.4尾插数据
//尾插
//传参数需要传二级指针才可以修改
void PushBack(SLTnode** pphead, StructDataType data)
{
assert(pphead);
SLTnode* newnode = CreatNode(data);
//一个数据都没有的情况下进行尾插
if (*pphead == NULL)
{
*pphead = newnode;
}
//链表中有不止一个数据
else
{
//先找到尾节点
SLTnode* ptail = *pphead;
while (ptail->next)
{
ptail = ptail->next;
}
ptail->next = newnode;
}
}
在尾插的时候我们需要传的参数是二级指针,因为如果链表中元素为空的话pphead作为头节点需要被修改。当我们进行尾插的时候有两种情况,第一点就是链表为空的情况,第二种就是链表不为空,所以我们需要找到尾节点。
3.5头插数据
//头插
void PushFront(SLTnode** pphead, StructDataType data)
{
SLTnode* newnode = CreatNode(data);
if (*pphead == NULL)
{
*pphead = newnode;
}
else
{
newnode->next = *pphead;
*pphead = newnode;
}
}
和尾插类似唯一不同的是不需要找尾节点,也是需要传二级指针用来修改pphead。
3.6尾删
//尾删
void PopBack(SLTnode** pphead)
{
assert(pphead && *pphead);
//直接找尾节点还有尾节点的前一个节点
//如果只有一个节点
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
printf("NULL\n");
}
else
{
SLTnode* pcur = *pphead;
SLTnode* ptail = *pphead;
while (ptail->next)
{
pcur = ptail;
ptail = ptail->next;
}
free(ptail);
ptail = NULL;
pcur->next = NULL;
}
}
我们在进行链表尾删的时候首先进行断言不能为空,在进行删除的时候也有两种情况第一种是链表中只有一个节点时需要用if进行判断,第二种情况是正常尾删所以我们需要找到尾节点,和尾节点的前一个节点,原因是当我们直接删除尾节点的时候尾节点的上一个节点还存储着尾节点的指针,如果我们不置为空会出现野指针报错,所以定义两个指针:ptail(尾节点)pcur(尾节点的牵着一个节点)。
3.7头删
//头删
void PopFront(SLTnode** pphead)
{
assert(pphead && *pphead);
if ((*pphead)->next == NULL)
{
free(*pphead);
*pphead = NULL;
printf("NULL\n");
}
else
{
SLTnode* tmp = (*pphead)->next;
free(*pphead);
*pphead = NULL;
*pphead = tmp;
}
}
在我们进行头删的时候也有两种情况,只有一个节点的时候和正常头删,只有一个节点的时候直接删除free掉然后置空,当我们进行头删的时候需要创建一个临时的指针用来储存头节点的下一个节点,如果不进行这个操作在删除完之后后面的节点会丢失。
3.8在链表中查询数据
//查询数据
SLTnode* FindData(SLTnode* phead, StructDataType data)
{
assert(phead);
while (phead)
{
if (phead->data == data)
{
//printf("存在数据");
return phead;
}
else
{
phead = phead->next;
}
}
printf("不存在该数据");
return NULL;
}
在查询数据的时候我们只需要遍历链表不需要对链表进行修改所以只需要传一级指针,查找数据只需要遍历,在每次遍历的时候数据进行比较如果找到了就直接返回当前指针没有找到就退出循环输出没有找到。
3.9指定位置后面插入数据
//在指定位置后插入数据
void PushPosBack(SLTnode* pos, StructDataType data)
{
assert(pos);
SLTnode* newnode = CreatNode(data);
newnode->next = pos->next;
pos->next = newnode;
}
由于不会出现空链表的情况所以不需要传二级指针,只需要传一级指针,注意后面的顺序不能调换,要先将创建的节点的next链接pos后面的指针,顺序颠倒的话则会找不到后面的节点。
3.10指定位置后面删除数据
//在指定位置后删除数据
void PopPosBack(SLTnode* pos)
{
SLTnode* tmp = NULL;
tmp = pos->next->next;
free(pos->next);
pos->next = NULL;
pos->next = tmp;
}
删除后为了不丢失数据所以要找到被删除节点的下一个节点定义一个临时指针tmp = pos->next->next ,删除后进行链接。
3.11在指定位置前插入数据
//在指定位置前插入数据
void PushPosFront(SLTnode** pphead, SLTnode* pos, StructDataType data)
{
assert(pphead && *pphead);
SLTnode* newnode = CreatNode(data);
if ((*pphead) == pos)
{
//头插
PushFront(pphead, data);
}
else
{
//需要找到pos前的一个数据
SLTnode* pcur = *pphead;
while (pcur->next != pos)
{
pcur = pcur->next;
}
newnode->next = pos;
pcur->next = newnode;
}
}
同样是有两种情况,第一种情况当被插入的节点是头节点是只需要进行一个头插即可,第二行总之那个情况是在数据总进行插入,我们需要找的pos指针的前一个节点我们定义为pcur,然后在pcur后面进行尾插。
3.12删除指定位置节点
//删除指定位置节点
void PopPos(SLTnode** pphead,SLTnode* pos)
{
//需要找到指定位置前一个节点
//尾删
if (pos->next == NULL)
{
PopBack(pphead);
}
else
{
SLTnode* prev = *pphead;
while (prev->next != pos)
{
//找到pos前一个节点
prev = prev->next;
}
prev->next = pos->next;
free(pos);
pos = NULL;
}
}
删除pos节点我们需要找到pos的前一个节点和后一个节点然后进行链接,由于单链表只能向后查找所以我们只需要找前一个节点,后面的节点通过pos->next。注意先后顺序。
3.13销毁链表
//销毁链表
void Destory(SLTnode** pphead)
{
SLTnode* tmp = *pphead;
while (*pphead)
{
tmp = (*pphead)->next;
free(*pphead);
*pphead = NULL;
}
free(tmp);
tmp = NULL;
*pphead = NULL;
}
销毁链表时要先找到该节点的下一个节点,然后再删除前一个节点这样不会出现删除之后找不到数据的情况,最后记得将创建的指针free掉。
头文件.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int StructDataType;
//定义节点
//包含数据和指针
typedef struct SListNode
{
StructDataType data;
struct SListNode* next;
}SLTnode;
//打印链表中的所有的数据
void SLT_print(SLTnode* phead);
//尾插
void PushBack(SLTnode** pphead, StructDataType data);
//头插
void PushFront(SLTnode** pphead, StructDataType data);
//尾删
void PopBack(SLTnode** pphead);
//头删
void PopFront(SLTnode** pphead);
//查询数据
SLTnode* FindData(SLTnode* phead, StructDataType data);
//在指定位置后插入数据
void PushPosBack(SLTnode* pos, StructDataType data);
//在指定位置后删除数据
void PopPosBack(SLTnode* pos);
//在指定位置前插入数据
void PushPosFront(SLTnode** pphead, SLTnode* pos, StructDataType data);
//删除指定位置节点
void PopPos(SLTnode** pphead,SLTnode* pos);
//销毁链表
void Destory(SLTnode** pphead);