文章目录
单向不带头不循环链表
今天这篇文章将介绍一个新的数据结构类型------链表。
链表有八种结构,以单向/双向,带头/不带头,循环/不循环进行分类。
其中有两种最常用的:
1.单向不带头不循环链表
2.双向不带头循环链表
这篇文章对第一种形式进行实现
链表与顺序表的区别
在此之前,已经了解过了顺序表的实现了。顺序表的底层是使用数组进行实现的。数组的优点就是可以随机访问,但是由于空间容量的倍增,很容易造成空间的浪费。
在这里先介绍链表的优点,具体会在后面来说:
链表是由一系列节点组成的,每个节点存储着下一个节点的地址。这样子就可以通过地址的索引找到下一个节点。链表每开辟一个新的节点,就向内存申请一块空间。这样子不会造成空间的浪费。
多文件管理
链表的文件管理仍是采取和顺序表一样的方式:
SingleLinkedList.h
负责头文件的包含、变量的定义、函数的定义
SingleLinkedList.c
对头文件中定义的函数进行实现
test.c
测试与调试对应功能的是否可行正确
链表的定义结构
首先我们来看看什么是链表:
来举一个生动形象的例子:比如火车。火车是由一节一节的车厢组成的,每一节车厢里面坐着乘客,车厢与车厢之间是连接的。这就很像链接起来的。
而链表的组成就是一系列的节点通过链接而来。每个节点内存放着想要的数据。而节点与节点之间的连接是通过指针实现的。每个节点都存放着下一个节点的地址,若没有下一个节点,那么这个地址置空即可。
所以我们知道了定义链表,也就是定义链表的节点:
c
typedef int SListDataType;
typedef struct SListNode {//每个节点的定义
SListDataType x;
struct SListNode* next;
}SListNode;
还是一样,需要把数据类型进行重命名,方便修改想要存储的数据。
这里有两种方式对链表进行管理。
1.再创建一个结构体
就像顺序表一样,顺序表是通过SL结构体进行管理顺序表的。这个结构体内部含有一个指向顺序表首元素地址的指针。所以在测试流程中需要专门写一个函数对这个变量进行初始化。
同样的道理:我们可以专门创建一个结构体来管理链表的后续空间:
c
typedef struct SingleLinkedList {
SListNode* head;//指向第一个节点的指针
int num;//节点个数
}SList;
这样子操作,就需要专门写一个函数对链表进行初始化了。
2.不需要创建结构体
当然,也可以直接在main函数中创建一个指向的节点的指针:SListNode* head=NULL;
后续插入元素的时候就把这个头指针作为参数传入函数中插入即可。因为已经完成了对头指针的初始化,所以并不需要再专门写一个函数。
在本篇文章中,使用第二种方法进行管理。
获得链表节点个数
c
int GetSListDataQuantity(SListDataType** pphead) {
SListNode* pmove = *pphead;
int count = 0;
while (pmove) {
pmove = pmove->next;
count++;
}
return count;
}
这个操作十分的简单。我们知道链表是由一个一个节点组成的。只要某节点的下一个节点为空,就说明该节点就是最后一个节点。所以只需要从头开始遍历即可,直到pmove
为空指针时退出循环。
链表增加元素
链表的尾插及创建节点函数
链表的尾插,顾名思义,就是在链表的结尾插入一个新的节点。
假设有这么个链表,我们应该如何进行尾插呢?
1.得开辟一个新的节点,并且将新的节点中的元素赋值完毕
这一步操作就需要使用malloc
函数,开辟一个大小为sizeof(SListNode)
的空间,并把这个新空间的内容赋值完毕。
2.需要找到链表的尾部,然后将尾部节点的next指针指向这个新的节点
但是这里要注意的是,如果链表有节点存在,尾巴就是最后一个节点。但是如果此时链表为空呢?
此时head==NULL
,我们直接在head的后面插入新节点就可以了。所以找尾插入是需要进行分类讨论的。
还需要注意一点的是:插入节点的方法有很多种,比如尾插,头插,任意节点后面插入。都是需要使用malloc
函数开辟新的节点。
正常插入操作如果在每一个方法内都写,非常冗杂,且重复片段。这个时候我们就想,能不能创建一个函数,专门生成节点的并且把地址返回呢?这样不就很方便插入了吗?
当然是可以的:
c
SListNode* SListBuyNode(SListDataType x) {
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (!newnode) {
perror("malloc failed");
exit(-1);
}
newnode->x = x;
newnode->next = NULL;
return newnode;
}
定义一个SListBuyNode
函数,返回值为SListNode*
,这样子,开辟新节点的事情就是由这个函数进行完成的。这个函数会返回这个节点的地址。再插入的时候,只需要将链表的尾部的next指针指向这个新节点的地址就ok了。然后要注意的是如果节点开辟不成功程序不能再继续进行,需要马上退出。
尾插函数的实现:
c
void SListPushBack(SListNode** pphead, SListDataType x) {
if (!*pphead) *pphead = SListBuyNode(x);
else {
SListNode* ptail = *pphead;
while (ptail->next) {
ptail = ptail->next;
}
ptail->next = SListBuyNode(x);
}
}
再来解释一下为什么要使用二级指针作为形参
我们知道,要想通过函数修改一个变量的内容,是需要传址调用的。因为形参是实参的一份临时拷贝。只有接收到地址时候,才能根据这个地址修改到实参的内容。
而刚刚讲到,在main函数中声名了一个结构体指针SListNode* head = NULL;
虽然head本身就是一个地址,但是传址调用是相对的。也就是说,形参是指向形参的地址。此时head是一个指针变量,存放地址。但head终归是一个变量,也会有地址。所以需要传入head变量的地址也就是&head作为实参。一级指针变量的地址是二级指针类型。所以形参需要用二级指针进行接收。
当不用修改链表的内容的时候,只需要传入一级指针就可以了。但是为了方便,我在定义全部函数的时候都选择使用二级指针接收 (因为可以直接cv参数部分)。
链表的头插
前面是在链表的尾部进行插入,那能不能在链表的头部插入呢?答案是可以的。有了前面尾插的介绍,头插就十分简单了。
先来看代码:
c
void SListPushFront(SListNode** pphead, SListDataType x) {
if (!*pphead) *pphead = SListBuyNode(x);
else {
SListNode* NewNode = SListBuyNode(x);
SListNode* OriginHead = *pphead;
(*pphead) = NewNode;
NewNode->next = OriginHead;
}
}
还是需要进行分析:
如果链表为空,那么头插其实就是在链表的头指针后插入。
如果链表不为空,就开辟一个新节点,然后使用标记一下头插前链表的第一个节点的地址OriginHead
*pphead永远指向的是新链表的头,所以让 *pphead指向newnode,然后让newnode的next指针指向OriginHead,这样,头插的连接操作就完成了。
任意位置节点后插入
讲完头插和尾插后,我们再来实现一个在给定位置节点后插入:
c
void SListInsertPos(SListNode** pphead, SListDataType x,int pos) {
if (pos<0 || pos>GetSListDataQuantity(pphead)) {
printf("The Pos %d Is Not Legal! Cannot Delete!\n",pos);
return;
}
else {
if (!pos) {
SListPushFront(pphead, x);
}
else {
pos--;
SListNode* p = *pphead;
while (pos--) {
p = p->next;
}
//p指向的是要第pos个节点
SListNode* pnext = p->next;
SListNode* new = SListBuyNode(x);
new->next = p->next;
p->next = new;
}
}
}
虽然是任意位置插入,但不是真的任意。
当传入的pos<0或者pos>此时链表节点个数时,不能插入,并且需要提示。
当pos=0时,规定为头插。
其余的情况都是找到第pos个位置的节点,在这个节点的"尾插"即可。
判断链表是否为空
在讲到删除操作前,首先的i先定义一个函数SListEmpty,判断链表是否为空。
为什么呢?
因为如果是一个空链表,那么就没有执行删除操作的必要了,直接提示即可。
c
bool SListEmpty(SListDataType** pphead) {
if (!*pphead) return true;
else return false;
}
使用bool类型作为返回值。只需要判断一下*pphead是否为NULL即可。
链表删除元素
链表的尾删
对应链表尾插,我们可以定义函数尾删。
我们先来分析一下:
如果链表为空,给予提示后直接返回,不执行删除操作。
如果不为空,需要找到最后一个节点,并且将该节点的前一个节点的next指针指向该节点的下一个节点的地址。
就像这样,需要把plast->next=p->next;
但是,当链表只有一个节点的时候,是没有所谓的前面一个节点的。所以当链表节点个数为1时候,直接删除唯一的一个节点就可以,然后让*pphead=NULL
。其余情况按照上述操作。
c
void SListDeleteBack(SListNode** pphead) {
if (SListEmpty(pphead)) {
printf("Already No Element! Cannot Delete\n");
return;
}
else {
if (GetSListDataQuantity(pphead) == 1) {
free(*pphead);
*pphead = NULL;
}
else {
SListNode* pdelete = *pphead;
while (pdelete->next->next) {
pdelete = pdelete->next;
}
SListNode* ppdelete = pdelete->next;
pdelete->next = NULL;
free(ppdelete);
ppdelete = NULL;
}
return;
}
}
当然,在这里我进行了一些操作,走到的是倒数第二个节点,然后再找到最后一个节点。这样子十分方便。因为单链表是没办法往前走的,所以当先走到尾节点的时候,需要再进行一次遍历找到尾节点的前一个,非常麻烦。
删除节点,其实也就是释放节点内存,使用函数free
即可,删除完后及时置空,避免野指针出现。
链表的头删
对应头插 也可以进行头删
c
void SLitsDeleteFront(SListNode** pphead) {
if (SListEmpty(pphead)) {
printf("Already No Element! Cannot Delete\n");
return;
}
else{
SListNode* OriginHead = *pphead;
*pphead = OriginHead->next;
free(OriginHead);
OriginHead = NULL;
}
}
还是一样,若链表为空,不能删。
如果不为空:因为头删就是删除*pphead指向的节点。所以标记一下,让OriginHead
指向原来的头,然后真正的头往后移动一个节点。删除OriginHead
指向的节点即可。
任意位置删除
当想要任意位置删除时:
c
void SListDeletePos(SListNode** pphead,int pos) {
if (SListEmpty(pphead)) {
printf("Already No Element! Cannot Delete\n");
return;
}
else {
if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
printf("The Pos %d Is Not Legal!\n", pos);
return;
}
else {
if (pos == 1) SLitsDeleteFront(pphead);
else {
pos = pos - 2;
SListNode* p = *pphead;
while (pos--) p = p->next;
//此时p指向的是第pos个节点的上一个
SListNode* pnext = p->next, * ppnext = pnext->next;
p->next = ppnext;
free(pnext);
pnext = NULL;
}
}
}
}
链表为空:无法删除,提示并返回
链表不为空:还是需要分类。
当pos<0或者pos>节点个数,这是无法删除的,给予提示并返回
反之:因为删除某节点是需要将被删除节点的前一个节点的next指针指向被删除节点的后一个节点。当pos=1,即删除第一个节点的时候,没有前面节点,但是其实就是头删方式。
其余情况则先找到第pos个节点的前一个,然后找到pos节点和pos节点的下一个,然后执行删除pos节点操作即可。
如图:就是让p指向pos前一个节点,pnext指向pos节点,ppnext指向pos下一个节点。
然后连接p与ppnext指向的节点,断开pnext和ppnext指向节点的连接,删除pnext指向节点即可。
链表查找元素
可以通过坐标来找到坐标节点指向元素,也可以输入数据来寻找该数据是否存在。这些都十分简单,只重点讲述一下通过输入数据寻找。
代码实现
c
void SListDataFind(SListNode** pphead, SListDataType x) {
if (SListEmpty(pphead)) printf("No Elements!\n");
else {
int flag = 0;
SListNode* pfind = *pphead;
int pos = 1;
while (pfind) {
if (pfind->x == x) {
printf("The Data %d Is In No.%d Node!\n", x, pos);
flag = 1;
}
pfind = pfind->next;
pos++;
}
if (!flag) printf("The Data %d Is Not Exist!\n", x);
}
}
链表为空 提示链表无元素后返回。
反之:让指针pfind从头到尾找,并提示坐标。不使用返回值形式是因为链表中可能存在一样的数据。
定义的flag是一个标志。因为无论如何链表一定会被pfind遍历完,只要找到过一次元素,就让标志flag=1,出循环后,如果没有找到想要的元素,flag一定为0。
链表修改元素
在这里简单介绍一下通过坐标进行修改对应元素:
c
void SListChangeDataPos(SListNode** pphead, SListDataType x, int pos) {
if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
printf("The Pos %d Is Not Legal!\n",pos);
return;
}
SListNode* p = *pphead;
pos--;
while (pos--) {
p = p->next;
}
p->x = x;
}
还是一样,需要判断坐标是否合理。
然后找到第pos个节点修改即可。
单向链表的遍历
c
void SListTraverse(SListNode** pphead) {
SListNode* pmove = *pphead;
while (pmove) {
printf("%d -> ", pmove->x);
pmove = pmove->next;
}
printf("NULL\n");
}
遍历十分简单,让pmove从头开始走,直到为空。依次将pmove指向节点的数据进行打印。
如果链表为空,不会进入循环,直接打印NULL。
链表销毁
因为在对上开辟了空间,向内存申请空间,当不需要的时候,就需要马上释放。
但是这里释放过程和顺序表那不一样。因为顺序表是的指针a是指向一块连续的空间,所以使用函数free的时候,会将这一块连续的空间一起释放。
但是链表的每个节点的地址大概率不是连续的。因为每开辟一个节点,就申请一个空间。这个空间在堆上申请的,malloc函数会随机选取一个未被使用的空间。所以虽然链表也是连续的,只不过它只是在逻辑上连续了,而不是物理上连续。
所以销毁链表的时候,需要从头到尾,依次删除节点:
c
void SListDestroy(SListNode** pphead) {
if (SListEmpty(pphead)) {
printf("The Single LinkedList has been Destroy!\n");
return;
}
while (*pphead) {
SListNode* pfree = *pphead;
*pphead = (*pphead)->next;
free(pfree);
pfree = NULL;
}
}
链表为空时,不需要再执行销毁操作。
链表不为空时,只要*pphead不为空,就让pfree指向此时 *pphead指向的节点,再让 *pphead往后移动一次。删除pfree指向的节点。
直到*pphead指向为NULL时候,就会退出循环。销毁完毕。
相关代码
c
SingleLinkedList.h
c
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<errno.h>
#include<stdbool.h>
typedef int SListDataType;
typedef struct SListNode {//每个节点的定义
SListDataType x;
struct SListNode* next;
}SListNode;
typedef struct SingleLinkedList {
SListNode* head;//指向第一个节点的指针
int num;//节点个数
}SList;
SListNode* SListBuyNode(SListDataType x);//创建新的节点
void SListPushBack(SListNode** pphead, SListDataType x);//链表尾插
void SListPushFront(SListNode** pphead, SListDataType x);//链表头插
void SListInsertPos(SListNode** pphead, SListDataType x,int pos);//在第几个节点后面插入
//第0个节点后面也可以插入 头插即可
int GetSListDataQuantity(SListDataType** pphead);//链表中元素个数/节点个数
bool SListEmpty(SListDataType** pphead);//判断链表是否为空 为空返回真 反之为假
void SListDeleteBack(SListNode** pphead);//链表元素尾删
void SLitsDeleteFront(SListNode** pphead);//链表元素头删
void SListDeletePos(SListNode** pphead,int pos);//删除第pos个位置的节点
void SListTraverse(SListNode** pphead);//链表遍历
void SListDataFind(SListNode** pphead, SListDataType x);//寻找元素是否存在 并且告知在第几个节点
void SListChangeDataPos(SListNode** pphead, SListDataType x, int pos);//把第pos个节点的元素修改为x
void SListDestroy(SListNode** pphead);//销毁链表
c
SingleLinkedList.c
c
#include"SingleLinkedList.h"
SListNode* SListBuyNode(SListDataType x) {
SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));
if (!newnode) {
perror("malloc failed");
exit(-1);
}
newnode->x = x;
newnode->next = NULL;
return newnode;
}
void SListPushBack(SListNode** pphead, SListDataType x) {
if (!*pphead) *pphead = SListBuyNode(x);
else {
SListNode* ptail = *pphead;
while (ptail->next) {
ptail = ptail->next;
}
ptail->next = SListBuyNode(x);
}
}
void SListPushFront(SListNode** pphead, SListDataType x) {
if (!*pphead) *pphead = SListBuyNode(x);
else {
SListNode* NewNode = SListBuyNode(x);
SListNode* OriginHead = *pphead;
(*pphead) = NewNode;
NewNode->next = OriginHead;
}
}
void SListInsertPos(SListNode** pphead, SListDataType x,int pos) {
if (pos<0 || pos>GetSListDataQuantity(pphead)) {
printf("The Pos %d Is Not Legal! Cannot Delete!\n",pos);
return;
}
else {
if (!pos) {
SListPushFront(pphead, x);
}
else {
pos--;
SListNode* p = *pphead;
while (pos--) {
p = p->next;
}
//p指向的是要第pos个节点
SListNode* pnext = p->next;
SListNode* new = SListBuyNode(x);
new->next = p->next;
p->next = new;
}
}
}
int GetSListDataQuantity(SListDataType** pphead) {
SListNode* pmove = *pphead;
int count = 0;
while (pmove) {
pmove = pmove->next;
count++;
}
return count;
}
bool SListEmpty(SListDataType** pphead) {
if (!*pphead) return true;
else return false;
}
void SListDeleteBack(SListNode** pphead) {
if (SListEmpty(pphead)) {
printf("Already No Element! Cannot Delete\n");
return;
}
else {
if (GetSListDataQuantity(pphead) == 1) {
free(*pphead);
*pphead = NULL;
}
else {
SListNode* pdelete = *pphead;
while (pdelete->next->next) {
pdelete = pdelete->next;
}
SListNode* ppdelete = pdelete->next;
pdelete->next = NULL;
free(ppdelete);
ppdelete = NULL;
}
return;
}
}
void SLitsDeleteFront(SListNode** pphead) {
if (SListEmpty(pphead)) {
printf("Already No Element! Cannot Delete\n");
return;
}
else{
SListNode* OriginHead = *pphead;
*pphead = OriginHead->next;
free(OriginHead);
OriginHead = NULL;
}
}
void SListDeletePos(SListNode** pphead,int pos) {
if (SListEmpty(pphead)) {
printf("Already No Element! Cannot Delete\n");
return;
}
else {
if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
printf("The Pos %d Is Not Legal!\n", pos);
return;
}
else {
if (pos == 1) SLitsDeleteFront(pphead);
else {
pos = pos - 2;
SListNode* p = *pphead;
while (pos--) p = p->next;
//此时p指向的是第pos个节点的上一个
SListNode* pnext = p->next, * ppnext = pnext->next;
p->next = ppnext;
free(pnext);
pnext = NULL;
}
}
}
}
void SListTraverse(SListNode** pphead) {
SListNode* pmove = *pphead;
while (pmove) {
printf("%d -> ", pmove->x);
pmove = pmove->next;
}
printf("NULL\n");
}
void SListDataFind(SListNode** pphead, SListDataType x) {
if (SListEmpty(pphead)) printf("No Elements!\n");
else {
int flag = 0;
SListNode* pfind = *pphead;
int pos = 1;
while (pfind) {
if (pfind->x == x) {
printf("The Data %d Is In No.%d Node!\n", x, pos);
flag = 1;
}
pfind = pfind->next;
pos++;
}
if (!flag) printf("The Data %d Is Not Exist!\n", x);
}
}
void SListChangeDataPos(SListNode** pphead, SListDataType x, int pos) {
if (pos <= 0 || pos > GetSListDataQuantity(pphead)) {
printf("The Pos %d Is Not Legal!\n",pos);
return;
}
SListNode* p = *pphead;
pos--;
while (pos--) {
p = p->next;
}
p->x = x;
}
void SListDestroy(SListNode** pphead) {
if (SListEmpty(pphead)) {
printf("The Single LinkedList has been Destroy!\n");
return;
}
while (*pphead) {
SListNode* pfree = *pphead;
*pphead = (*pphead)->next;
free(pfree);
pfree = NULL;
}
}
test.c内的实现看个人,在这不进行展示。
想要代码的也可以去作者的gitee账户中获取:gitee账户