目录
一、单链表概念
单链表的特点
二、单链表的实现
1、打印函数的实现
2、尾插函数的实现
3、全部函数的实现
总结:
一、单链表概念
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
单链表是一种链式数据结构,它由一系列节点(Node)组成,每个节点包含两部分:数据域(data)和指针域(next)。数据域存储数据,指针域存储下一个节点的地址。单链表中的节点按顺序链接,形成一条链。
单链表的特点
- 单向性:链表中的每个节点只包含指向下一个节点的指针,因此,链表只能从头节点开始,逐个访问后续节点。
- 动态性:链表的节点可以动态地进行插入和删除操作,不需要像数组那样事先分配固定大小的空间。
- 不连续性:链表中的节点在内存中不必是连续存储的,这增加了数据管理的灵活性。
每个节点包括一个数据域data和 一个指针域next(存储下一个节点的地址)
typedef int SLTDataType;
typedef struct SListNode {SLTDataType data;struct SListNode* next;
}SLTNode;
我们现在就可以定义一个节点了,首先我们定义一个节点node1,然后定义个指针指向该节点,然后让node1的next域指向node2
SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* node2 = (SLTNode*)malloc(sizeof(SLTNode));
SLTNode* plist = node1; //定义头指针,头指针指向 node1
node1->next = node2;
node2->next = NULL;
这里,单链表与顺序表不同的是,链表的每节“车厢”都是单独申请下来的空间,我们这里称为结点。图中,指针变量plist保存的是第一个节点的地址,我们称为plist此时“指向”第一个结点,如果我们希望plist“指向”第二个结点时,只需要修改plist保存的地址内容为0x0000003.
链表的性质:
1、链式结构在逻辑上时连续的,在物理结构上不一定连续
2、结点一般是从堆上申请的
3、从堆上申请来的空间,是按照一定策略分配出来的,每次申请的空间可能连续,也可能不连续
二、单链表的实现
这里,我们定义了一个头文件,SList.h来实现对单链表操作的一些列函数,有打印、尾插、头插、尾删、头删、查找、插入等操作。
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode {SLTDataType data;struct SListNode* next;
}SLTNode;//1.打印函数
void SLTPrint(SLTNode* phead);
//2.尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//3.头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//4.尾删
void SLTPopBack(SLTNode** pphead);
//5.头删
void SLTPopFront(SLTNode** pphead);
//6.查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//7.在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//8.删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//9.在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//10.删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//11.删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//12.销毁链表
void SListDestroy(SLTNode** pphead);
为了申请节点方便,我们实现了一个对结点初始化的函数,在这里,我们通过调用函数,申请节点,初始化后,我们将该节点的地址返回,因为是在函数中申请的节点,node变量会被栈空间收回,但是节点空间是在堆上申请的,申请的空间还会存在,我们在函数外用一个节点指针接收,这时该节点申请成功,且可以通过接收的节点指针操作该节点。
//申请节点,并初始化
SLTNode* SLTBuyNode(SLTDataType x) {SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//申请失败if (node == NULL) {perror("malloc fail!\n");exit(1);}//初始化node->data = x;node->next = NULL;return node;
}
1、打印函数的实现
SLTPrint(plist);
将plist存储的第一个节点的地址值给phead,这时,变量phead也可以访问单链表,遍历每个节点,就可以实现对单链表的打印。
//1.打印链表
void SLTPrint(SLTNode* phead) {SLTNode* pcur = phead; //定义一个新的头指针指向 node1while (pcur != NULL) {printf("%d -> ", pcur->data);pcur = pcur->next; //指针指向下一个节点}printf("NULL\n");
}
2、尾插函数的实现
这里,我们可以看到,我们是用二级指针接收的plist的地址,通过接收plist的地址,我们可以直接操作原单链表中的plist,实现各种操作。
注意:这里和上面打印函数中用一级指针的区别是,上面用一级指针接收地址操作单链表的方式不能操作plist的指向关系,因为在上面的函数中,plist是一直存在的,ppphead是函数中的临时变量,可以改变每个节点中的值(data域,和next域),但是一旦函数执行完毕,对phead的操作就会消失。
//2.尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x) {assert(pphead);//申请一个新的节点SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL) { //头指针为空(链表为空)*pphead = newnode; //将头指针指向新的节点}else { //链表不为空,找尾节点SLTNode* ptail = *pphead; //把头节点的地址赋值给 ptailwhile (ptail->next != NULL) {ptail = ptail->next; //ptail指向下一个节点}ptail->next = newnode;}
}
这里,就得举个特殊情况得例子,当plist的值为NULL,就是没有申请节点的时候,我们要尾插一个节点,这时候我们要插入的地方时plist后面,即使当函数完成后,pphead变量被回收,也不会影响plist的指向关系,如同:
如果我们这个时候不是用的二级指针接收的plist的地址,而是直接使用一级指针,这个时候传递的是plist的值, 我们操作的是当前的指针,也就是在pphead后面插入数据,当数据插入完成后,函数完成,pphead被释放,申请的节点就找不到了,plist后面还是没有连接节点。
3、全部函数的实现
SList.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLTDataType;
typedef struct SListNode {SLTDataType data;struct SListNode* next;
}SLTNode;//1.打印函数
void SLTPrint(SLTNode* phead);
//2.尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x);
//3.头插
void SLTPushFront(SLTNode** pphead, SLTDataType x);
//4.尾删
void SLTPopBack(SLTNode** pphead);
//5.头删
void SLTPopFront(SLTNode** pphead);
//6.查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//7.在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//8.删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//9.在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//10.删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//11.删除pos之后的节点
void SLTEraseAfter(SLTNode* pos);
//12.销毁链表
void SListDestroy(SLTNode** pphead);
Slist.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"SList.h"
//测试函数ff
void ff(SLTNode* pphead ,int x) {SLTNode* newnode = (SLTNode*)malloc(sizeof(SLTNode));newnode->data = x;newnode->next = NULL;if (pphead == NULL) { //头指针为空(链表为空)pphead = newnode; //将头指针指向新的节点}else { //链表不为空,找尾节点SLTNode* ptail = pphead; //把头节点的地址赋值给 ptailwhile (ptail->next != NULL) {ptail = ptail->next; //ptail指向下一个节点}ptail->next = newnode;}SLTPrint(pphead);
}
//1.打印链表
void SLTPrint(SLTNode* phead) {SLTNode* pcur = phead; //定义一个新的头指针指向 node1while (pcur != NULL) {printf("%d -> ", pcur->data);pcur = pcur->next; //指针指向下一个节点}printf("NULL\n");
}
//申请节点,并初始化
SLTNode* SLTBuyNode(SLTDataType x) {SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//申请失败if (node == NULL) {perror("malloc fail!\n");exit(1);}//初始化node->data = x;node->next = NULL;return node;
}
//2.尾插
void SLTPushBack(SLTNode** pphead, SLTDataType x) {assert(pphead);//申请一个新的节点SLTNode* newnode = SLTBuyNode(x);if (*pphead == NULL) { //头指针为空(链表为空)*pphead = newnode; //将头指针指向新的节点}else { //链表不为空,找尾节点SLTNode* ptail = *pphead; //把头节点的地址赋值给 ptailwhile (ptail->next != NULL) {ptail = ptail->next; //ptail指向下一个节点}ptail->next = newnode;}
}
//3.头插
void SLTPushFront(SLTNode** pphead, SLTDataType x) {assert(pphead);SLTNode* newnode = SLTBuyNode(x); //创建新的节点newnode->next = *pphead; //新的节点指向当前的头节点*pphead = newnode; //头指针指向新的节点
}
//4.尾删
void SLTPopBack(SLTNode** pphead) {assert(pphead && *pphead); //传的参数不能为空,链表也不能为空//如果只有一个节点if ((*pphead)->next == NULL) {free(*pphead);*pphead = NULL;}else {SLTNode* prev = NULL; //空指针SLTNode* ptail = *pphead; //将头节点赋值给ptailwhile (ptail->next != NULL) {prev = ptail; //prev指向当前的位置ptail = ptail->next; //ptail指向下一个节点}prev->next = NULL; //前一个节点置为NULLfree(ptail); //释放掉最后一个节点ptail = NULL;}
}
//5.头删
void SLTPopFront(SLTNode** pphead) {assert(pphead && *pphead);SLTNode* next = (*pphead)->next; //保存头节点的下一个地址free(*pphead); //释放头节点*pphead = next; //头节点下移}
//6.查找,找到了,返回该指向的指针(原单链表的节点地址)
SLTNode* SLTFind(SLTNode* phead, SLTDataType x) {SLTNode* pcur = phead;while (pcur) //pcur != NULL{if (pcur->data == x){return pcur;}pcur = pcur->next; //pcur往后走}//没找到return NULL;
}
//7.在指定位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x) {assert(pphead && pos);//当pos就是头节点,相当于头插if (pos == *pphead){SLTPushFront(pphead, x);}else {SLTNode* newnode = SLTBuyNode(x); //创建新的节点并赋初值 SLTNode* prev = *pphead; //prev 指向头节点while (prev->next != pos) { //prev指向pos前面一个节点prev = prev->next;}//改变指向关系newnode->next = pos;prev->next = newnode;}
}
//9.在指定位置之后插入数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x) {SLTNode* next = pos->next; //将pos的下个节点地址存储SLTNode* newnode = SLTBuyNode(x); //创建一个新节点 newnode->next = next; pos->next = newnode;
}
//10.删除pos节点
void SLTErase(SLTNode** pphead, SLTNode* pos) {assert(pphead && pos);//删除的节点就是头节点if (pos == *pphead) {SLTPopFront(pphead);}else {SLTNode* prev = *pphead;//找pos前面的节点while (prev->next != pos) { prev = prev->next;}prev->next = pos->next; //pos前一个节点指向pos后一个节点free(pos);pos = NULL;}
}
//11.删除pos之后的节点
void SLTEraseAfter(SLTNode* pos) {assert(pos && pos->next); //pos之后的节点不能为NULLSLTNode* del = pos->next; //找到pos的下一个的节点pos->next = del->next; //pos的下一个节点为del的下一个节点free(del);del = NULL;
}
//12.销毁链表
void SListDestroy(SLTNode** pphead) {SLTNode* pcur = *pphead;while (pcur) {SLTNode* next = pcur->next; //指向pcur下一个节点free(pcur); //释放当前的pcurpcur = next; //pcur后移到next位置}*pphead = NULL;
}
test.c
void test03()
{SLTNode* plist = NULL; //申请一个空指针SLTNode* node1 = (SLTNode*)malloc(sizeof(SLTNode));node1->data = 1;node1->next = NULL;SLTPrint(plist);
}
int main()
{test03();return 0;
}
总结:
这里给出完整实现单链表的函数方法,其他的函数实现比较简单,这里重要的是解决单链表中的传递二级指针和一级指针的区别,方便个位读者明了的知道函数的实现原理,更好的学习其他的数据结构,希望各位读者可以多多对文章提出建议,方便我后期改进文章以供其他的读者阅读。