数据结构与算法之链表(超详细,适合入门)

简介: 数据结构与算法之链表(超详细,适合入门)

今天要分享的是数据结构中的链表。对于新手来说,链表确实不好学,也确实难,下面是一些链表知识点的总结,请听我一 一道来:


分享之前,和大家分享一下今天阅读的时候读到伟大诗人说的一句话:


诗人辛波斯卡说:一个人可以爬上山丘,屏住呼吸,却无法像玫瑰一样生出枝叶,长成树丛,因为“只有玫瑰才能盛开如玫瑰”。同样,每个人都只能按自己的方式绽放人生。

---共勉


链表?链表是什么?我也不晓得啊,所以我去百度搜了一下结果如下:


链表 (linked list):是一种物理存储结构上非连续 存储结构 ,数据元素的逻辑顺序是通过链表中的引用链接次序实现的. 链表由一系列 结点 ( 链表中每一个元素称为结点 )组成,结点可以在运行时动态生成。


说实话我刚开始学我也看不懂:下面给出比较容易理解的方式:


链表就是由一系列节点,用箭头把这些节点给连接起来,如下图:


cd4cb6be9bd0f9f2d60eb664c404da9d.png


先来讲一下数组的顺序存储和链表的链式存储,加以区别,更好的理解链表:


数组—顺序存储


数组作为一个顺序储存方式(数组中的每一个元素的地址都是连续的,比如1234,这一串数字就是连续的)的数据结构,可是有大作为的,它的灵活使用为我们的程序设计带来了大量的便利;


但是,数组最大的缺点就是我们的插入和删除时需要移动大量的元素,所以呢,大量的消耗时间,以及冗余度使得我们难以接受。


以C语言数组插入一个元素为例,当我们需要在一个数组{1,2,3,4}的第1个元素后的位置插入一个’A’时,我们需要做的有:


第一步:将第1个元素后的整体元素后移,形成新的数组{1,2,2,3,4}。


第二步:再将第2个元素位置的元素替换为我们所需要的元素'A'。


第三步:最终形成我们的预期,这需要很多重复操作。


07b61663b0d1118d43ee6f0fabdb7d9a.png

上图可以看出,使用数组都有这两大缺点:

1.插入删除操作所需要移动的元素很多,浪费算力。

2.必须为数组开足够的空间,否则有溢出风险


链表—链式存储


由于数组的这些缺点,自然而然的就产生链表的思想了。

链表通过不连续的储存方式,自适应内存大小,以及指针的灵活使用,巧妙的简化了上述的内容。

链表的基本思维是,利用结构体的设置,额外开辟出一份内存空间去作指针,它总是指向下一个结点,一个个结点通过NEXT指针相互串联,就形成了链表。


f5eef90a53d2a2b9fefc3f4d9f310120.png




其中DATA为自定义的数据类型,NEXT为指向下一个链表结点的指针,通过访问NEXT,可以引导我们去访问链表的下一个结点。


对于一连串的结点而言,就形成了链表如下图:

04414a559085ac1688100494acdce0f2.png


上文所说的插入删除操作只需要修改指针所指向的区域就可以了,不需要进行大量的数据移动操作。如下图:

8c7b02c694eed66305c43e9f982cf6cb.png


相比起数组,链表解决了数组不方便移动,插入,删除元素的弊端,但相应的,链表付出了更加大的内存牺牲换来的这些功能的实现。


一.链表概述:


包含单链表,双链表,循环单链表,实际应用中的功能不同,但实现方式都差不多。

单链表就像接力棒,一个一个往下传;

双链表就像传球一样,你传球给我,我传球给你。

循环链表就像是一队人,从一号一个个传到尾号然后尾号再传给一号,形成一个循环。

我们今天只介绍一下单链表,这个懂了,其他就容易理解了:


1.单链表概念和简单的设计:


单链表是一种链式存取的数据结构,链表中的数据是以结点来表示的,每个结点由元素和指针构成。


元素表示数据元素的映象,就是存储数据的存储单元;指针指示出后继元素存储位置,就是连接每个结点的地址数据。


以结点的序列表示的线性表称作线性链表,也就是单链表,单链表是链式存取的结构。


对于链表的每一个结点,我们使用结构体进行设计,其主要内容有:

f5eef90a53d2a2b9fefc3f4d9f310120.png

其中,DATA数据元素,可以为你想要储存的任何数据格式,可以是数组,可以是int,甚至可以是结构体(这就是传说中的结构体套结构体)


NEXT为一个指针,其代表了一个可以指向的区域,通常是用来指向下一个结点,链表的尾部NEXT指向NULL(空),因为尾部没有任何可以指向的空间了。


故,对于一个单链表的结点定义,可以代码描述成:

typedef struct Node
{
    int data;          //数据类型,你可以把int型的data换成任意数据类型,包括结构体struct等复合类型
    struct Node* next;          //单链表的指针域
} Node, * LinkedList;
//Node表示结点的类型,LinkedList表示指向Node结点类型的指针类型


2.链表的初始化:


初始化主要完成以下工作:


  1. 创建一个单链表的动态前置节点并向后逐步添加节点,一般指的是申请结点的空间,(注意这里动态分配就是为了区分数组和链表的存储方式,在定义数组时,操作系统会提前一次性分配给一块足够用户使用的内存,这块内存是连续的,然而链表是不需要这样做的,链表所占用的内存是不需要提前分配的,链表可根据自身情况进行分配。)
  2. 对头结点的指针域赋空值(NULL),因为不给指针赋值可能会造成野指针等不可预测的危害。


其代码可以表示为:


LinkedList listinit()
{
    Node *L;
    L=(Node*)malloc(sizeof(Node));      //开辟空间 
    if(L==NULL)
    {                     //判断是否开辟空间失败,这一步很有必要
        printf("申请空间失败");
        //exit(0);                  //开辟空间失败可以考虑直接结束程序
    }
    L->next=NULL;       //指针指向空
}


注意:一定要判断是否开辟空间失败,否则生产中由于未知的情况造成空间开辟失败,仍然在继续执行代码,后果将不堪设想啦,因此养成这样的判断是很有必要的。

下面创建链表的两种方式很重要,值得大家反复琢磨:


3.头插法创建单链表


利用指针指向下一个结点元素的方式进行逐个创建,使用头插法最终得到的结果是逆序的。

如图所示:


2ff0db74f9e85d1b0df253391e2afe0a.png

从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头,即头结点之后。

代码实现如下:

//头插法建立单链表
LinkedList LinkedListCreatH() 
{
    Node *L;
    L = (Node *)malloc(sizeof(Node));       //申请头结点空间
    L->next = NULL;                         //初始化一个空链表
    int x;                                  //x为链表数据域中的数据
    while(scanf("%d",&x) != EOF)
    {
        Node *p;
        p = (Node *)malloc(sizeof(Node));   //申请新的结点
        p->data = x;                        //结点数据域赋值
        p->next = L->next;                  //将结点插入到表头L-->|2|-->|1|-->NULL
        L->next = p;
    }
    return L;
}


4.尾插法创建单链表:


如图所示为尾插法的创建过程。



aebbad3af41bdd06a8fd92d5623349c5.png头插法生成的链表中,结点的次序和输入数据的顺序不一致。若希望两者次序一致,则需要尾插法。

该方法是将新结点逐个插入到当前链表的表尾上,为此必须增加一个尾指针tail, 使其始终指向当前链表的尾结点,代码如下:

//尾插法建立单链表
LinkedList LinkedListCreatT() 
{
    Node *L;
    L = (Node *)malloc(sizeof(Node));          //申请头结点空间
    L->next = NULL;                             //初始化一个空链表
    Node *tail;
    tail = L;                                   //tail始终指向终端结点,开始时指向头结点
    int x;                                      //x为链表数据域中的数据
    while(scanf("%d",&x) != EOF) 
    {
        Node *p;
        p = (Node *)malloc(sizeof(Node));        //申请新的结点
        p->data = x;                             //结点数据域赋值
        tail->next = p;                          //将结点插入到表头L-->|1|-->|2|-->NULL
        tail = p;
    }
    tail->next = NULL;
    return L;
}


5.遍历/打印/修改单链表:


遍历的定义:从链表的头开始,逐步向后进行每一个元素的访问,称为遍历。


对于遍历操作,我们可以衍生出很多常用的数据操作,比如增加元素,删除元素,修改元素个数,查询整个链表数据(俗称增删改查)等等操作。

进行遍历的思路不难,只需要建立一个指向链表L的结点,然后沿着链表L逐个向后搜索即可,代码如下:


  //遍历输出单链表
void printList(LinkedList L)
{
    Node *p=L->next;
    int i=0;
    while(p)
    {
        printf("第%d个元素的值为:%d\n",++i,p->data);
        p=p->next;
    }
}

对于元素修改操作,以下是代码实现:

//链表内容的修改,在链表中修改值为x的元素变为为y。
LinkedList LinkedListReplace(LinkedList L,int x,int y)
 {
    Node *p=L->next;
    int i=0;
    while(p)
  {
        if(p->data==x)
        {
            p->data=y;
        }
        p=p->next;
  }
    return L;
}


注意:简单的遍历设计的函数只需要void无参即可,而当涉及到元素操作时,可以设计一个LinkedList类型的函数,使其返回一个操作后的新链表。


6.元素插入操作:


链表的插入操作主要分为查找到第i个位置,将该位置的next指针修改为指向我们新插入的结点,而新插入的结点next指针指向我们i+1个位置的结点。


其操作方式可以设置一个前驱结点,利用循环找到第i个位置,再进行插入。


如图,在DATA1和DATA2数据结点之中插入一个NEW_DATA数据结点:


从原来的链表状态:


dfcb54fb8cfe2f0e0fbca0f71ea53f5e.png


插入后链表的状态:


7df7b00df7a1270bc7b499fd2a48eb9a.png

代码实现如下:

//单链表的插入,在链表的第i个位置插入x的元素
LinkedList LinkedListInsert(LinkedList L,int i,int x) 
{
    Node *pre;                              //pre为前驱结点
    pre = L;
    int tempi = 0;
    for (tempi = 1; tempi < i; tempi++)
    {
        pre = pre->next;                    //查找第i个位置的前驱结点
    }
    Node *p;                                //插入的结点为p
    p = (Node *)malloc(sizeof(Node));
    p->data = x;
    p->next = pre->next;
    pre->next = p;
    return L;
}


7.元素删除操作


删除元素要建立一个前驱结点和一个当前结点,当找到了我们需要删除的数据时,直接使用前驱结点跳过要删除的结点指向要删除结点的一个结点,再将有的结点通过free函数释放掉。如图所示:


image.png



代码如下:

//单链表的删除,在链表中删除值为x的元素
LinkedList LinkedListDelete(LinkedList L,int x)
 {
    Node *p,*pre;                  //pre为前驱结点,p为查找的结点。
    p = L->next;
    while(p->data != x)           //查找值为x的元素
    {                            
        pre = p;
        p = p->next;
    } 
    pre->next = p->next;           //删除操作,将其前驱next指向其后继。
    free(p);
    return L;
}


二.总结:


链表确实不好理解,准确来说数据结构这门课都很难很抽象很不好理解,但它非常重要。


建议学习链表之前先复习一下指针,结构体,数组,深入理解一下指针,结构体。


本人虽然可以基本上实现这些基本操作,但是自身还是有很多不足之处,有些东西似懂非懂,仍然需要不断学习,以上如有错误请指教,谢谢各位。


借用《钢铁是怎样炼成的》扉页中的一句话分享给大家:一个人的人生应该这样度过,当你回首往事的时候,不因碌碌无为而悔恨,不因虚度年华而羞耻。


14:00写到现在,再次抬头天已经快暗了:寒假目标励志学完数据结构,加油加油加油!!!


2023.01.07


From:努力进大厂的新青年

相关文章
|
11月前
|
存储 算法
算法入门:专题二---滑动窗口(长度最小的子数组)类型题目攻克!
给定一个正整数数组和目标值target,找出总和大于等于target的最短连续子数组长度。利用滑动窗口(双指针)优化,维护窗口内元素和,通过单调性避免重复枚举,时间复杂度O(n)。当窗口和满足条件时收缩左边界,更新最小长度,最终返回结果。
|
11月前
|
存储 算法
算法入门:专题一:双指针(有效三角形的个数)
给定一个数组,找出能组成三角形的三元组个数。利用“两边之和大于第三边”的性质,先排序,再用双指针优化。固定最大边,左右指针从区间两端向内移动,若两短边之和大于最长边,则中间所有组合均有效,时间复杂度由暴力的O(n³)降至O(n²)。
|
11月前
|
存储 算法 编译器
算法入门:剑指offer改编题目:查找总价格为目标值的两个商品
给定递增数组和目标值target,找出两数之和等于target的两个数字。利用双指针法,left从头、right从尾向中间逼近,根据和与target的大小关系调整指针,时间复杂度O(n),空间复杂度O(1)。找不到时返回{-1,-1}。
|
存储 算法 Perl
数据结构实验之链表
本实验旨在掌握线性表中元素的前驱、后续概念及链表的建立、插入、删除等算法,并分析时间复杂度,理解链表特点。实验内容包括循环链表应用(约瑟夫回环问题)、删除单链表中重复节点及双向循环链表的设计与实现。通过编程实践,加深对链表数据结构的理解和应用能力。
404 4
|
机器学习/深度学习 数据采集 算法
你天天听“数据挖掘”,可它到底在“挖”啥?——数据挖掘算法入门扫盲篇
你天天听“数据挖掘”,可它到底在“挖”啥?——数据挖掘算法入门扫盲篇
349 0
|
存储 机器学习/深度学习 算法
C 408—《数据结构》算法题基础篇—链表(下)
408考研——《数据结构》算法题基础篇之链表(下)。
698 30
|
存储 算法 C语言
C 408—《数据结构》算法题基础篇—链表(上)
408考研——《数据结构》算法题基础篇之链表(上)。
994 25
|
机器学习/深度学习 算法 机器人
强化学习:时间差分(TD)(SARSA算法和Q-Learning算法)(看不懂算我输专栏)——手把手教你入门强化学习(六)
本文介绍了时间差分法(TD)中的两种经典算法:SARSA和Q-Learning。二者均为无模型强化学习方法,通过与环境交互估算动作价值函数。SARSA是On-Policy算法,采用ε-greedy策略进行动作选择和评估;而Q-Learning为Off-Policy算法,评估时选取下一状态中估值最大的动作。相比动态规划和蒙特卡洛方法,TD算法结合了自举更新与样本更新的优势,实现边行动边学习。文章通过生动的例子解释了两者的差异,并提供了伪代码帮助理解。
1266 2
|
存储 算法 物联网
解析局域网内控制电脑机制:基于 Go 语言链表算法的隐秘通信技术探究
数字化办公与物联网蓬勃发展的时代背景下,局域网内计算机控制已成为提升工作效率、达成设备协同管理的重要途径。无论是企业远程办公时的设备统一调度,还是智能家居系统中多设备间的联动控制,高效的数据传输与管理机制均构成实现局域网内计算机控制功能的核心要素。本文将深入探究 Go 语言中的链表数据结构,剖析其在局域网内计算机控制过程中,如何达成数据的有序存储与高效传输,并通过完整的 Go 语言代码示例展示其应用流程。
326 0
|
机器学习/深度学习 存储 C++
【C++数据结构——线性表】单链表的基本运算(头歌实践教学平台习题)【合集】
本内容介绍了单链表的基本运算任务,涵盖线性表的基本概念、初始化、销毁、判定是否为空表、求长度、输出、求元素值、按元素值查找、插入和删除数据元素等操作。通过C++代码示例详细解释了顺序表和链表的实现方法,并提供了测试说明、通 - **任务描述**:实现单链表的基本运算。 - **相关知识**:包括线性表的概念、初始化、销毁、判断空表、求长度、输出、求元素值、查找、插入和删除等操作。 - **测试说明**:平台会对你编写的代码进行测试,提供测试输入和预期输出。 - **通关代码**:给出了完整的C++代码实现。 - **测试结果**:展示了测试通过后的预期输出结果。 开始你的任务吧,祝你成功!
775 5

热门文章

最新文章