当前位置: 首页 > news >正文

【数据结构】线性表--链表

【数据结构】线性表--链表

  • 一.前情回顾
  • 二.链表的概念
  • 三.链表的实现
    • 1.链表结点的结构:
    • 2.申请新结点函数:
    • 3.尾插函数:
    • 4.头插函数:
    • 5.尾删函数:
    • 6.头删函数:
    • 7.在指定结点之前插入:
    • 8.在指定结点之后插入:
    • 8.删除指定结点:
    • 9.查找函数:
    • 10.销毁链表:
  • 四.全部源代码实现
    • 1.头文件(声明动态顺序表的结构,操作等,起到目录作用):
    • 2.源文件(具体实现各种操作):
    • 3.测试文件(测试各个函数的功能)
  • 五.单链表和顺序表的对比
    • 1.存储分配方式
    • 2.时间性能
    • 3.空间性能
    • 4.总结

一.前情回顾

上篇文章讲述了动态顺序表及其实现,我们知道了动态顺序表在物理结构上是连续的,因此我们也认识到它的缺点:
①如果空间不足需进行增容,付出一定的性能消耗,并且可能存在一定的空间浪费。
②在进行某些插入或删除操作时,需要大量移动数据。这是因为相邻数据元素在物理存储结构上也是连续存储的,中间没有空隙。
在这里插入图片描述

因此本篇文章将讲述线性表的另一种表示方法:链表。

二.链表的概念

链表,即线性表的链式实现,指用一组任意连续或者不连续的存储单元存储数据,通过指针像链条一样链结各个元素的一种存储结构。
如图所示:
在这里插入图片描述

因此,对于每个数据元素,除了要存储自身信息,也要存储后继(下一个)数据元素的信息。这两部分合起来被称为结点。
每个结点包含两个域,一个是数据域:存储数据元素的信息;另一个是指针域:存储后继元素的位置信息。n个结点链结成一个链表。如图所示:
在这里插入图片描述
对于线性表,总要有头有尾,我们把链表中第一个结点的存储位置叫做头指针,最后一个结点置为NULL。由于每个结点的指针域只包含一个指向后继位置的指针,因此该链表又称单链表(单向链表)。

三.链表的实现

1.链表结点的结构:

在C语言中用结构体指针来存储后继结点的信息。

typedef int SLDataType;//结点的结构体
typedef struct SListNode
{SLDataType data;//数据域struct SListNode* next;//指向下一个结点的指针域,所以指针类型应该为struct SListNode*
}SLTNode;//起别名,将struct SListNode简写成SLTNode

2.申请新结点函数:

因为在插入操作中需要频繁申请结点,因此可以将申请结点的操作封装成一个函数。

//申请新结点
SLTNode* BuySListNode(SLDataType x)
{SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));NewNode->data = x;NewNode->next = NULL;return NewNode;
}

3.尾插函数:

需要特别注意的是,凡是涉及修改链表,必须传二级指针,因为链表本身是用每个结点的指针链结而成的,作为参数传递时是一级指针,再将每个结点的地址作为实参传递,这是二级指针。

//尾插函数
//需要传二级指针,否则形参的改变不影响实参
void SListPushBack(SLTNode** pphead, SLDataType x)
{assert(pphead);//不能传空地址,否则解引用找链表头结点会报错//创建新结点SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));NewNode->data = x;NewNode->next = NULL;//链表为空,直接插入if (*pphead == NULL){*pphead = NewNode;}else{//需要找到最后一个结点才能尾插,因此先用一个cur结点标记当前所在位置SLTNode* cur = *pphead;while (cur->next != NULL)//循环结束走到最后一个结点{cur = cur->next;//让cur遍历到最后一个结点}if (NewNode == NULL){perror("malloc fail!");exit(1);}cur->next = NewNode;}
}

4.头插函数:

//头插函数
void SListPushFront(SLTNode** pphead, SLDataType x)
{assert(pphead);//创建新结点SLTNode* NewNode = BuySListNode(x);NewNode->next = *pphead;*pphead = NewNode;
}

5.尾删函数:

//尾删函数
void SListPopBack(SLTNode** pphead)
{assert(pphead && *pphead);//如果只有一个结点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* cur = *pphead;while (cur->next->next != NULL)//需要找到倒数第二个结点才能删除最后一个结点{cur = cur->next;}SLTNode* tmp = cur->next;free(tmp);tmp = NULL;cur->next = NULL;}
}

6.头删函数:

//头删函数
void SListPopFront(SLTNode** pphead)
{assert(pphead&&*pphead);//链表为空时不能删除SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;}

7.在指定结点之前插入:

//在指定结点之前插入函数
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{assert(pphead && *pphead);assert(pos);//需要找到指定结点的前一个结点SLTNode* prev = *pphead;//可能第一个结点就是指定结点,此时相当于头插if (prev == pos){//直接调用头插函数SListPushFront(pphead, x);}else{while (prev->next != pos){prev = prev->next;}SLTNode* NewNode = BuySListNode(x);NewNode->next = prev->next;prev->next = NewNode;}
}

8.在指定结点之后插入:

//在指定结点之后插入函数
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* NewNode = BuySListNode(x);NewNode->next = pos->next;pos->next = NewNode;
}

8.删除指定结点:

//删除指定结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{SLTNode* prev = *pphead;//如果第一个结点就是要删除的结点if (prev == pos){//直接调用头删SListPopFront(pphead);}else{while (prev->next != pos){prev = prev->next;}SLTNode* tmp = prev->next;//tmp即要删除的结点prev->next = tmp->next;free(tmp);tmp = NULL;}
}

9.查找函数:

//查找
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{SLTNode* cur = phead;while (cur != NULL){if (cur->data == x)return cur;cur = cur->next;}//没找到或链表为空时,返回空指针return NULL;
}

10.销毁链表:

//销毁链表函数
void SListDestory(SLTNode** pphead)
{assert(pphead && *pphead);while (*pphead != NULL){SLTNode* tmp = *pphead;*pphead = (*pphead)->next;free(tmp);tmp = NULL;}
}

四.全部源代码实现

1.头文件(声明动态顺序表的结构,操作等,起到目录作用):

SList.h

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int SLDataType;//结点的结构体
typedef struct SListNode
{SLDataType data;//数据域struct SListNode* next;//指向下一个结点的指针域,所以指针类型应该为struct SListNode*
}SLTNode;//起别名,将struct SListNode简写成SLTNode//打印函数(方便调试)
void SListPrint(SLTNode* phead);//申请新结点
SLTNode* BuySListNode(SLDataType x);//尾插函数
void SListPushBack(SLTNode** pphead, SLDataType x);//需要传二级指针,否则形参的改变不影响实参//头插函数
void SListPushFront(SLTNode** pphead, SLDataType x);//尾删函数
void SListPopBack(SLTNode** pphead);//头删函数
void SListPopFront(SLTNode** pphead);//在指定位置之前插入函数
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x);//在指定位置之后插入函数
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLDataType x);//删除指定结点
void SListErase(SLTNode** pphead, SLTNode* pos);//查找
SLTNode* SListFind(SLTNode* phead, SLDataType x);//销毁链表函数
void SListDestory(SLTNode** pphead);

2.源文件(具体实现各种操作):

SList.c

#include"SList.h"//打印函数(方便调试)
void SListPrint(SLTNode* phead)
{assert(phead);SLTNode* cur = phead;while (cur != NULL)//循环结束走到空结点{printf(" %d ->", cur->data);cur = cur->next;}printf("NULL\n");
}//申请新结点
SLTNode* BuySListNode(SLDataType x)
{SLTNode* NewNode = (SLTNode*)malloc(sizeof(SLTNode));NewNode->data = x;NewNode->next = NULL;return NewNode;
}//尾插函数
//需要传二级指针,否则形参的改变不影响实参
void SListPushBack(SLTNode** pphead, SLDataType x)
{assert(pphead);//不能传空地址,否则解引用找链表头结点会报错//创建新结点SLTNode* NewNode = BuySListNode(x);//链表为空,直接插入if (*pphead == NULL){*pphead = NewNode;}else{//需要找到最后一个结点才能尾插,因此先用一个cur结点标记当前所在位置SLTNode* cur = *pphead;while (cur->next != NULL)//循环结束走到最后一个结点{cur = cur->next;//让cur遍历到最后一个结点}if (NewNode == NULL){perror("malloc fail!");exit(1);}cur->next = NewNode;}
}//头插函数
void SListPushFront(SLTNode** pphead, SLDataType x)
{assert(pphead);//创建新结点SLTNode* NewNode = BuySListNode(x);NewNode->next = *pphead;*pphead = NewNode;
}//尾删函数
void SListPopBack(SLTNode** pphead)
{assert(pphead && *pphead);//如果只有一个结点if ((*pphead)->next == NULL){free(*pphead);*pphead = NULL;}else{SLTNode* cur = *pphead;while (cur->next->next != NULL)//需要找到倒数第二个结点才能删除最后一个结点{cur = cur->next;}SLTNode* tmp = cur->next;free(tmp);tmp = NULL;cur->next = NULL;}
}//头删函数
void SListPopFront(SLTNode** pphead)
{assert(pphead&&*pphead);//链表为空时不能删除SLTNode* next = (*pphead)->next;free(*pphead);*pphead = next;
}//在指定结点之前插入函数
void SListInsert(SLTNode** pphead, SLTNode* pos, SLDataType x)
{assert(pphead && *pphead);assert(pos);//需要找到指定结点的前一个结点SLTNode* prev = *pphead;//可能第一个结点就是指定结点,此时相当于头插if (prev == pos){//直接调用头插函数SListPushFront(pphead, x);}else{while (prev->next != pos){prev = prev->next;}SLTNode* NewNode = BuySListNode(x);NewNode->next = prev->next;prev->next = NewNode;}
}//在指定结点之后插入函数
void SListInsertAfter(SLTNode** pphead, SLTNode* pos, SLDataType x)
{assert(pphead && *pphead);assert(pos);SLTNode* NewNode = BuySListNode(x);NewNode->next = pos->next;pos->next = NewNode;
}//删除指定结点
void SListErase(SLTNode** pphead, SLTNode* pos)
{SLTNode* prev = *pphead;//如果第一个结点就是要删除的结点if (prev == pos){//直接调用头删SListPopFront(pphead);}else{while (prev->next != pos){prev = prev->next;}SLTNode* tmp = prev->next;//tmp即要删除的结点prev->next = tmp->next;free(tmp);tmp = NULL;}
}//查找
SLTNode* SListFind(SLTNode* phead, SLDataType x)
{SLTNode* cur = phead;while (cur != NULL){if (cur->data == x)return cur;cur = cur->next;}//没找到或链表为空时,返回空指针return NULL;
}//销毁链表函数
void SListDestory(SLTNode** pphead)
{assert(pphead && *pphead);while (*pphead != NULL){SLTNode* tmp = *pphead;*pphead = (*pphead)->next;free(tmp);tmp = NULL;}
}

3.测试文件(测试各个函数的功能)

test.c

#include"SList.h"//测试尾插函数
void test01()
{SLTNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPrint(phead);
}//测试头插函数
void test02()
{SLTNode* phead = NULL;SListPushFront(&phead, 1);SListPushFront(&phead, 2);SListPushFront(&phead, 3);SListPushFront(&phead, 4);SListPrint(phead);
}//测试尾删函数
void test03()
{SLTNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);SListPopBack(&phead);SListPrint(phead);
}//测试头删函数
void test04()
{SLTNode* phead = NULL;SListPushFront(&phead, 1);SListPushFront(&phead, 2);SListPushFront(&phead, 3);SListPushFront(&phead, 4);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);SListPopFront(&phead);SListPrint(phead);
}//测试查找函数
void test05()
{SLTNode* phead = NULL;SListPushFront(&phead, 1);SListPushFront(&phead, 2);SListPushFront(&phead, 3);SListPushFront(&phead, 4);SListPrint(phead);SLTNode* ret1 = SListFind(phead, 2);if (ret1 != NULL)printf("找到了\n");elseprintf("未找到\n");SLTNode* ret2 = SListFind(phead, 57);if (ret2 != NULL)printf("找到了\n");elseprintf("未找到\n");
}//测试在指定结点之前插入
void test06()
{SLTNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPrint(phead);//在第三个结点前插入57//先查找到第三个结点SLTNode* pos1 = SListFind(phead, 3);SListInsert(&phead, pos1, 57);SListPrint(phead);//在第一个结点前插入79//先查找到第一个结点SLTNode* pos2 = SListFind(phead, 1);SListInsert(&phead, pos2, 79);SListPrint(phead);//在最后一个结点前插入36//先查找到最后一个结点SLTNode* pos3 = SListFind(phead, 4);SListInsert(&phead, pos3, 36);SListPrint(phead);
}//测试在指定结点之后插入
void test07()
{SLTNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPrint(phead);//在第三个结点后插入57//先查找到第三个结点SLTNode* pos1 = SListFind(phead, 3);SListInsertAfter(&phead, pos1, 57);SListPrint(phead);//在第一个结点后插入79//先查找到第一个结点SLTNode* pos2 = SListFind(phead, 1);SListInsertAfter(&phead, pos2, 79);SListPrint(phead);//在最后一个结点后插入36//先查找到最后一个结点SLTNode* pos3 = SListFind(phead, 4);SListInsertAfter(&phead, pos3, 36);SListPrint(phead);
}//测试删除结点函数
void test08()
{SLTNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPrint(phead);//删除第一个结点SLTNode* pos1 = SListFind(phead, 1);SListErase(&phead, pos1);SListPrint(phead);//删除第三个结点SLTNode* pos2 = SListFind(phead, 3);SListErase(&phead, pos2);SListPrint(phead);//删除最后一个结点SLTNode* pos3 = SListFind(phead, 4);SListErase(&phead, pos3);SListPrint(phead);
}//测试销毁函数
void test09()
{SLTNode* phead = NULL;SListPushBack(&phead, 1);SListPushBack(&phead, 2);SListPushBack(&phead, 3);SListPushBack(&phead, 4);SListPrint(phead);SListDestory(&phead);SListPrint(phead);
}
int main()
{//test01();//test02();//test03();//test04();//test05();//test06();//test07();//test08();test09();return 0;
}

五.单链表和顺序表的对比

1.存储分配方式

顺序表采用一段连续的存储单元存储数据元素。
单链表采用一组任意的存储单元存储元素。

2.时间性能

查找:
顺序表按值查找O(n),按索引查找O(1)。
单链表O(n)。
插入和删除:
顺序表O(n)。
单链表O(1)。

3.空间性能

顺序表需要预分配空间,小了需再次分配,大了造成空间浪费。
单链表需要时申请结点空间。

4.总结

若线性表需要频繁查找,宜采用顺序存储结构。若频繁插入和删除,宜采用链式存储结构。比如说游戏开发中,对于用户注册的个人信息,除了注册时插入数据外,绝大多数情况都是读取,所以应该考虑用顺序存储结构 。而游戏中的玩 家的武器或者装备列表,随着玩家的游戏过程中,可能会随时增加或删除,此时再用顺序存储就不大合适了,链表结构就可以大展拳脚。当然,这只是简单的类比,现实中的软件开发,要考虑的问题会复杂得多。
总之,线性表的顺序存储和链式存储各有优缺点,不能简单说哪个好,哪个不好,需根据实际情况做出选择。

相关文章:

  • 【Linux系统】互斥量mutex
  • Spring AI 实战:第八章、Spring AI Tool Calling之与时俱进
  • Android基于绑定的控件用法
  • SpringBoot的启动流程
  • NoSQL入门实战:MongoDB与Redis核心应用全解析
  • 从 Java 开发到 AI 工程师:全面学习指南
  • 【漫话机器学习系列】238.训练误差与测试误差(Training Error And Test Error)
  • Spring AI 实战:第十一章、Spring AI Agent之知行合一
  • 56认知干货:智能化产业
  • 《政治最后的日子》章节
  • 电动调节 V 型球阀:颗粒状含碱浆液介质的完美解决方案-耀圣
  • 原码、补码、反码、有符号整数、无符号整数
  • 【漫话机器学习系列】239.训练错误率(Training Error Rate)
  • SpringBoot智能排课系统源码开发与实现
  • 正态分布习题集 · 题目篇
  • 2025牛客五一集训派对day4
  • OpenCV入门指南:从环境搭建到第一个图像处理程序
  • 【RocketMQ Broker 相关源码】- broker 启动源码(2)
  • level2.5 函数高阶
  • 气泡图、桑基图的绘制
  • 世锦赛决赛今夜打响,斯诺克运动需要赵心童创造历史
  • 中国海警局回应日本民用飞机侵闯我钓鱼岛领空:依法警告驱离
  • 商务部:外贸优品中华行活动采购意向超167亿元
  • 新华社评论员:在推进中国式现代化的宽广舞台上绽放青春光彩
  • 首日5金!中国队夺得跳水世界杯总决赛混合团体冠军
  • 美伊谈判因“后勤原因”推迟,伊朗:视美国做法再定谈判日期