• 【玩转408数据结构】线性表——单链表的定义以及增删改查(线性表的链式表示 上)


    知识回顾

            到这里我们已经了解到线性表是具有相同数据类型有限个数据元素序列,而线性表的顺序存储也就是顺序表,顺序表的存储形式十分直观,我们在实现时使用数组进行实现,但顺序表在插入或者删除元素时需要移动大量元素,那么怎么样才能在插入删除元素时不需要大费周章的移动如此之多的元素呢?为了解决这个问题,今天我们就来继续了解一下线性表的链式存储——链表。

    单链表定义

            线性表的链式存储又叫单链表,既然是属于线性表的一种存储方式,那么其应该满足线性表的特征(具有相同数据类型有限个数据元素序列)。

            那么什么是链式存储呢?我们不难想象,就像链条一样,我们存在很多个相同的结点,这些结点之间进行相连最终称为一个链条;那么链式存储也跟其类似,其也是有很多个相同的结点所组成,之后我们在将其结点按次序逐个串联,这样就可以形成一个链表。

            不过这样的定义显然是不准确的,我们线性表的链式存储结构用一组任意的存储单元来存储线性表中的数据元素(这组存储单元可以是连续的,也可以是不连续的) ;既然我们不对其存储单元做连续的要求的话,我们应该去设法建立其数据间的存储关系;那么为了保持链表中的元素a_{i}和其下一个元素 a_{i+1} 之间的逻辑关系,那么对于数据元素 a_{i} 来说,其不仅需要存储其本身的信息,还需要开辟一块空间去存放其直接后续的信息(也就是直接后续的存储位置);那么我们在设计时,就可以使用指针去指向该元素的直接后续元素存储位置,这样在查找时就可以通过指针去直接查找出当前元素的直接后续元素;所以我们的数据元素并不单单指的是其本身的信息,而是由这两个部分同时构成其数据元素  a_{i} (我们称其为 结点 )。

    定义单链表结点类型

            我们已经了解到其结点的构成,那么我们在实现时就需要使用代码去构造出其结点类型。

    1. //结点
    2. typedef struct LNode{
    3. int data;
    4. struct LNode *next;
    5. }LNode, *LinkList;

             这里我们同样使用struct关键字定义一个新类型,通过前面的分析,我们不难观察出其中需要一个data存放数据本身,因此其类型也就和我们需要存放数据本身类型一致即可;之后我们需要存放一个指针去存放下一个结点的地址,其类型自然也就是我们struct新定义的类型了。

    如何增加新结点

            前面我们已经可以将链表中单个结点的数据类型定义出来了,可是这里我们只是存在这个类型,但我们在实际的使用中是需要不断的开辟新的空间去存放这一个个的结点;那么我们就需要了解如何去添加新结点(开辟出一个新空间去存放新节点的数据)。

            这里我们需要使用到malloc函数去申请一片新的内存空间,下面的代码也就是使用malloc函数去申请一个新的内存空间存放结点;下面的代码中malloc内部写明的是需要开辟空间的大小,这里也就是使用sizeof求出我们前面定义新类型的大小(在这里也就是int + LNode*),至于malloc前面的括号则是C++中的强制类型转换,将其转换为LNode * 类型。

    LNode * p = (LNode *) malloc(sizeof(LNode));

            由于单链表的元素都是一个个单独的结点,且其元素离散的分布在存储空间之中,我们只能通过指针去按顺序一个个寻找其元素,因此单链表是非随机存取的存储结构 (也就是不可以直接找到表中的某个特定的结点)。

    分析LNode和*LinkList

            我们在观察结点类型的构造时会发现,其存在两个命名(LNode,*LinkList) ,对于这两个实际上存在这样的关系:

    LNode * = LinkList

            既然这样为什么这里我们要去分别起两个名字呢?其实只有一个也是可以完成该链表的代码实现的,只不过为了提升我们代码的可读性,也就是方便我们以后在阅读代码时可以更好的理解,我们在这里使用这两个命名。

            在这里我们如果在代码中想要,强调我们传入或者返回的是一个单链表,需要使用LinkList;如果我们想要强调传入或者返回的是一个结点,需要使用LNode *;通过这样分辨,我们可以很好的将代码中的结点和链表区分开,提高我们代码的阅读性。

    单链表头结点的分析

            在链表中,我们通常需要一个头指针去标识一个链表,其指向我们链表的头部,如果单链表的头指针为NULL,也就意味着此时为一个空链表;此时为了操作的方便我们可以选择去在头指针后加上一个空的结点,这个结点被称为头结点。头结点数据域不需要设置任何信息,当然也可以去记录表长。

    不带头结点的单链表

            如果我们不设置头节点的话,我们在单链表初始化时其头指针将直接指向NULL,此时也就说明其单链表为空,当我们后续插入元素时,再更改其头指针的指向即可。 

     

    带头结点的单链表

            对于含有头结点的指针,我们的头指针就需要指向头结点,而头结点指向NULL才表明我们的链表为空,当然使用这种方法我们再添加结点时就只需要更改头结点的指向即可,不需要更改头指针的指向(其主要原因也是其头部已经固定,就是我们的头结点)。

    两种方式的分析 

            不论链表是否带头结点,我们的头指针始终指向链表的第一个结点,只不过头结点是带头结点链表的第一个结点,只不过该结点不存储信息。

            如果单链表中不带头结点,我们在写代码时相对麻烦,对第一个数据结点和后续数据结点的 处理需要用不同的代码逻辑 对空表和非空表的处理需要用不同的代码逻辑。

    而使用头结点会使我们在使用时更加方便:

    • 由于第一个数据结点位置被存放在头结点的指针域,因此在链表上第一个位置上的操作就与在其他位置上的操作一致,不需要特殊处理。
    • 不论链表是否为空,其头结点都是指向头结点的非空指针(空表中的头结点指针域为空),因此空表和非空表的处理也就得到了统一。   

            因此我们可以发现 使用头结点可以避免掉我们不少的麻烦。

    单链表的基本操作实现

            既然我们了解了单链表,也知道了其类型的定义,那么接下来我们就需要尝试着去实现它的功能。

    单链表的插入

             既然要学习一个链表,那么首先我们就要知道如何在链表中添加元素,也就是下面我们所学习的单链表的插入。

            通过上文可知,我们的单链表分为含头结点与不含头结点的两种单链表,不同的单链表在执行有些插入操作的方法也存在一些差异。

            对于插入操作我们在这里主要介绍:按位序插入、指定结点的后插、指定结点的前插。由于指定节点的前后插操作是已经指定过节点,所以对此情况我们单链表是否带头结点对其没什么影响;只有按位序插入时,单链表是否带头结点会影响其实现,接下来我们在分析按位序插入时也将是否带头结点来分开讨论。 

    按位序插入

            按位序插入,顾名思义也就是在我们需要插入的地方,插入一个值即可,在这里我们创建该函数表示:

    ListInsert(&L,i,e):插入操作。在表L中的第i个位置上插入指定元素e。 

            由于链表的特殊性,我们无法跟顺序表一样,可以直接的找到其第i个位置;我们知道,单链表就是由很多结点相连,所以我们要想找到第i个位置的结点,就需要从头部向后,顺着我们结点中指向下一个结点的指针,逐步向后查找,知道找到第i个即可。由于需要从头部开始查找,所以带头结点的单链表插入与不带头结点的单链表插入,在实现上就存在一定的不同。 

    带头结点

             由上文我们已经了解到了带头结点的单链表,那么在这里对于带头结点的链表就不作描述。既然我们需要在第i个位置上插入一个元素,那么首先我们要做的就是检查i是否合法(如果i的值为0,我们的链表怎么会有第0位呢?或者是i的值超出我们链表的长度呢?这样就是不合法)。

     

            我们核验完i的合法性后,就需要思考如何在第i个位置上插入一个结点;以图为例:如果我们要在第二个位置插入一个结点e,那么首先我们要做的是使用malloc函数开辟出一片新的空间(前文有malloc描述);开辟出一段空间之后,我们就需要去查找出链表中第二个结点前的位置,这是因为我们需要在第二个位置插入元素的话,就需要将元素插入到第一个元素和第二个元素之间(这样我们插入的元素就成为了第二个元素);又由于在我们的单链表中 ,前一个结点中存在一个指针指向下一个结点的地址,当我们找到第一个元素时,也相当于找到了第二个元素的位置,这时,我们让新插入的结点中的指针指向原链表中的第二个结点位置,而原链表中第一个元素的指针则指向新插入结点的位置;这样我们是不是就成功的将其连接起来了。

            又因为我们这里在第一个结点前是存在一个头结点的,所以当我们想要在第一个位置插入一个元素时,这时待插入的位置是头结点和原链表中第一个结点之间,由于同样是在两个结点之间,所以其插入方法也同上。

             由于我们在进行按位插入时需要从头部循环向后遍历到第i-1个结点,所以需要遍历i-1次;那么其平均复杂度也就是O(n);同理,当我们需要插入的位置在第一个时,我们就不需要遍历,那个我们这是的时间复杂度就是最优时间复杂度:O(1)。

            需要注意的是,我们在进行插入操作时,其顺序可不可以更改?也就是如上图所示:正常来说,我们需要首先让e的指针指向a_{1}的地址,之后让头结点指针指向e的地址;那么我们可以不可以颠倒顺序呢?

            答案当然是不可以的。其主要原因就是,由于我们头结点的指针初始是存放的 a_{1} 的地址(我们只能通过头结点的指针寻找到其地址),那么按照我们最初的方法,是需要将存放的这个a_{1} 的地址赋值给e结点中指针,之后我们再更换掉头结点指针的指向(由于e为malloc开辟的空间,所以我们在开辟时会创建出一个指针直接存放其地址,所以我们可以直接通过这个指针对其赋值);而当我们首先将头结点指针的指向更改为e的存储地址后,在后面我们e中指针指向 a_{1} 地址时,我们会将头结点的指针指向位置值赋给e中指针,但此时头结点指针的指向已经不是a_{1} 地址了,这样就会造成如上图所示的错误。

             当然由于是从前向后遍历,那么最差的时间复杂度当然就是在最末位插入结点了,其时间复杂度为:O(n)。

    1. //插入 : 在第i个位置插入(含头节点)
    2. bool ListInsert(LinkList &L, int i, int e) {
    3. if(i<1) return false ;
    4. LNode *p ; //扫描指针
    5. int j = 0; //带头节点 从0开始
    6. p = L;
    7. while(p != NULL && j < i-1) { //找到 i-1 的位置
    8. p = p->next ;
    9. j++;
    10. }
    11. if(p == NULL) return false;
    12. LNode *s = (LNode *)malloc(sizeof(LNode)) ;
    13. s->data = e;
    14. s->next = p->next;
    15. p->next = s;
    16. return true;
    17. }

    需要注意的是:由于我们这里的链表是没有记录长度的,所以我们在while循环中 p!=NULL 是为了判断i是否会超出链表长度。 当然也可以写出一个计算长度的函数,其原理大致就是从头结点开始向后遍历,直到循环到该结点指针指向NULL即可,在循环中,每执行一次就让记录长度的变量自增即可。

    不带头结点

             由于这里单链表不存在头结点,所以这里在i=1时,我们需要特殊处理以下;当i>1且合法时,我们此时插入前后都是结点,这时方法就同带头结点单链表的插入方法。

             这里我们只需分析插入第一个位置的情况,由于我们不存在头结点,所以当我们想要插入第一个元素时,就需要利用头指针;在插入前,我们的头指针指向的是当时的第一个结点,也是我们插入后的第二个结点;那么我们在插入时,首先需要使插入结点e的指针指向a_{1}(也就是L指向的位置),之后我们更改L头指针的指向,使其指向e结点的位置即可。(当然这里我们的存储指向顺序也是不可以颠倒的,如果颠倒会同上面一样,形成一个闭环)

            除了头部的插入以外,其余的插入方法就同带头结点单链表插入的方法相同,我们在这里就不做多描述,其时间复杂度也同带头结点的插入相同。

    综上来看:

            带头结点与不带头结点的单链表在按位序插入中,其基本实现方法是相同的,仅仅只在头结点插入时略有不同;其时间复杂度也都是相同的:最优为O(1),最差为O(n)。 

    指定结点后插

            对于指定结点后插的操作,其实现方法其实是跟前面的按位序插入是类似的。

            我们回想以下,我们在实现按位序插入时,是要找的插入位置的前一个结点的位置进行操作的,而这里是直接给出需要插入位置的前一个结点位置,那么我们只需按照之前按位序插入的方法执行一遍即可(首先让e结点的指针指向y,也就是x结点指针的指向;之后让x指针的指向e的存储位置即可) 。

    1. bool InsertNextNode (LNode *p, ElemType e) {
    2. if (p == NULL) return false ;
    3. LNode *s = (LNode *) malloc(sizeof(LNode));
    4. if(s==NULL) return false //内存分配失败
    5. s->data = e;
    6. s->next = p->next;
    7. p->next = s;
    8. return true;
    9. }

             在实现时需要判断以下其内存是否分配成功,也就是 s 是否为 NULL。

    指定结点前插

    InsertPriorNode (LNode *p, ElemType e) :在p结点之前插入元素e 

             在这种情况下,看似跟之前很像,但不太一样,由于我们的 p 指向的为插入结点后的元素,而我们单链表又无法找到结点x前驱结点,那么我们就很难通过之前的办法让x的前驱结点指向我们需要插入结点的存储位置,那么我们就需要更改方法。

            首先,提供一种简单的方法,若函数可以传入单链表的头指针,此时我们只需从头指针向后遍历,遍历到某结点的指针指向为p即可,这时我们就成功找到其插入后的前驱结点,就可以按我们之前的方法去进行插入了。

            不过这种方法是复杂的,其需要遍历,所以其时间复杂度为O(n);那么我们用什么办法可以使得其不从头遍历也可以前插呢?

            

            在这里,我们按照之前的办法,在结点e的后面插入一个结点x,这就等同于我们之前的方法,并不难实现。

             紧接着呢,我们定义一个与结点中数据同类型的变量,利用该变量颠倒e与x的存储数位置,这样我们就等同于在p的前面插入一个结点了,只不过此时p指向的是我们所插入的结点。

    1. bool InsertPriorNode (LNode *p, ElemType e) {
    2. if (p == NULL) return false ;
    3. LNode *s = (LNode *) malloc(sizeof(LNode));
    4. if(s==NULL) return false //内存分配失败
    5. s->next = p->next;
    6. p->next = s; //新结点相连
    7. s->data = p->data; //p中元素给s
    8. p->data = e; // e的值给p
    9. return true;
    10. }

            由于我们只在单个的位置中插入,该算法的时间复杂度为:O(1)。 

    单链表的删除

    按位序删除 

     ListDelete(&L, i, &e) :删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。

             由于我们的链表是有单个的结点不断相连的,所以我们在删除某一节点时,只需要更改其相连的方向即可,不需要向顺序表一样,将删除结点后的结点均向前移动,这样就提高了效率。

    带头结点

            由于我们需要删除第i个元素,那么我们在删除操作中,首先需要找到第i-1个结点的位置,然后我们直接更改其i-1结点的指针指向,使其指向i+1结点的存储位置;这样在后面我们遍历时,就可以直接跳过第i个元素了。

     

            我们的删除操作只是需要改一个删除结点前的结点指针指向即可,其时间复杂度为O(1);所以其整体的删除操作时间主要耗费在寻找第i-1个结点的位置上;根据我们前面的分析,寻找需要从头循环遍历,所以其平均时间复杂度则是O(n);那么最好的时间复杂度则是在第一个位置插入,为O(1);最差的时间复杂度则是在最末尾插入,时间复杂度为O(n) 。

            对于含有头结点的单链表而言,其在第一个删除时前面存在头结点,所以插入的方式就跟在后面结点插入的方式相同。

    1. //删除 : 删除第i个位置的元素 并返回 (含头节点)
    2. bool ListDelete(LinkList &L, int i, int &e) {
    3. if(i<1) return false;
    4. LNode *p;
    5. int j=0;
    6. p = L;
    7. while(p != NULL && j < i-1) {
    8. p = p->next;
    9. j++;
    10. }
    11. if(p == NULL) return false;
    12. e = p->next->data;
    13. p->next = p->next->next;
    14. /*
    15. LNode *q = p->next;
    16. e = q->data;
    17. p->next = q->next;
    18. free(q);
    19. */
    20. return true;
    21. }

     

    不带头结点 

            对于不含头结点的单链表,其除在头部的删除外,其余的方法均与带头结点的单链表相同,所以这里我们只讨论起头部的删除。

            由于不带头结点的单链表删除头部时 前面无头结点,只有一个头指针指向其存储位置,所以我们在删除时,只需要将头指针指向第二个结点的存储位置即可。

    1. //删除 : 删除第i个位置的元素 并返回 (不含头节点)
    2. bool ListDelete(LinkList &L, int i, int &e) {
    3. if(i<1) return false;
    4. LNode *p;
    5. int j=1;
    6. p = L;
    7. if(i==1) {
    8. e = p->data ;
    9. L = p->next;
    10. }
    11. while(p != NULL && j < i-1) {
    12. p = p->next;
    13. j++;
    14. }
    15. if(p == NULL) return false;
    16. e = p->next->data;
    17. p->next = p->next->next;
    18. /*
    19. LNode *q = p->next;
    20. e = q->data;
    21. p->next = q->next;
    22. free(q);
    23. */
    24. return true;
    25. }

     指定结点删除

    DeleteNode(LNode *p)        删除指定结点 p

            这里,我们若要删除p指向的结点,就需要更改其前驱结点的指向,但是我们并不能通过p去找到其前驱结点,那么如果这里我们引入一个头指针L,从头部向后遍历寻找其前驱结点,这样我们就可以根据上面的删除方法对其进行删除了。

            该种方法由于要从前遍历寻找,所以其时间复杂度为O(n)。 

            还有一种办法,我们只需要将p后续结点的数据值存入p指向的结点中,之后让p指向的结点的指针直接跳过其后继结点,指向指向其后面的那个即可。也就是我们将y的值存入x的位置,然后让p指向的结点直接跳过q指向下一个即可。

    1. //删除指定节点 (删除 p 节点)
    2. bool DeleteNode(LNode *p, LinkList &L) {
    3. if(p == NULL) return false;
    4. if(p == L) {
    5. L = p->next;
    6. }
    7. p->data = p->next->data;
    8. p->next = p->next->next;
    9. /*
    10. LNode *q;
    11. q = p->next;
    12. p->data = q->data;
    13. p->next = q->next;
    14. free(q);
    15. */
    16. return true;
    17. }

            这种办法只需要更改其指向即可,所以其时间复杂度为O(1)。 

    单链表的查找

            对于单链表的查找,其实我们实现的底层逻辑都是相同的,就是从头指针开始向后遍历,知道寻找到我们需要查找的结点为止;由于需要从前向后遍历查找,所以其时间复杂度自然为:O(n)。

    按位查找

     GetElem(L,i):按位查找操作。获取表L中第i个位置的元素的值。

             对于按位查找,我们只需要利用一个for循环,使其循环i次,其循环到的结果自然就是我们元素的值了;不过需要注意的是,在循环过程中,需要判断一个i的合法性,避免长度超出我们链表长度。

    1. //按位查找
    2. LNode *GetElem(LinkList L, int i){
    3. if(i<0) return NULL;
    4. LNode *p;
    5. int j=0;
    6. p = L;
    7. while(p!=NULL && j
    8. p = p->next;
    9. j++;
    10. }
    11. //if(p==NULL) return NULL;
    12. return p;
    13. }

    按值查找

     LocateElem(L,e):按值查找操作。在表L中查找具有给定关键字值的元素。

             按值查找的方法与按位查找略有不同,按值查找需要遍历整个链表,之后在每次遍历时都要判断一下其是否为我们需要寻找的值,如果遍历到NULL还未找到,则说明该值不在链表之中。

    1. //按值查找
    2. LNode *LocateElem(LinkList L, int e) {
    3. LNode *p = L; //初始化
    4. while(p != NULL && p->data != e) {
    5. p = p->next;
    6. }
    7. return p;
    8. }

    单链表建立

            前面我们学习了那么多的单链表操作,那么我们对单链表一定有了一个基本的了解;那么我们在初始化链表时,一定要将很多个结点相连到一起,那么我们该如何实现该问题呢? 在这里我们提供头插法和尾插法两种方法。

    头插法

            如下图所示,头插法顾名思义也就是从头部开始插入,我们每次插入一个新的元素,都需要从其头部插入(如果是不含头结点的链表,我们需要更改其头指针);这样逐个的插入。 

     

            不过这样每次插入头部方法,会使我们先插入链表的结点在后面,而后插入的结点在链表的前面。这个可以用于链表的逆置。

            由于是需要一个一个插入,其总的时间复杂度为O(n)。

    1. //头插法建立
    2. LinkList List_HeadInsert(LinkList &L) {
    3. LNode *s; int x;
    4. L = (LinkList)malloc(sizeof(LNode)) ;
    5. L->next = NULL; //创建头结点
    6. cin >> x ;
    7. while(x != 9999){
    8. s = (LinkList)malloc(sizeof(LNode));
    9. s->data = x;
    10. s->next = L->next ;
    11. L->next = s ;
    12. cin >> x ;
    13. }
    14. return L ;
    15. }

    尾插法

             至于尾插法,顾名思义就是在链表的尾部插入一个结点;这种插入方法我们需要时刻更改尾部结点的指针指向,所以我们需要一个指针一直指向尾部结点,方便我们去更改;尾部插入的方法,其插入顺序和在链表中的排列顺序是相同的。

            由于是需要一个一个插入,其总的时间复杂度为O(n)。

    1. //尾插法建立
    2. LinkList List_TailInsert(LinkList &L) {
    3. int x;
    4. cin >> x;
    5. L = (LinkList)malloc(sizeof(LNode));
    6. LNode *s, *r = L;
    7. while(x!=9999) {
    8. s = (LNode *)malloc(sizeof(LNode));
    9. s->data = x;
    10. r->next = s;
    11. r = s;
    12. cin >> x;
    13. }
    14. r->next = NULL;
    15. return L;
    16. }

     单链表代码

    含头结点单链表代码

     

    1. //2.3 链表 单链表(带头节点)
    2. #include
    3. using namespace std ;
    4. //结点
    5. typedef struct LNode{
    6. int data;
    7. struct LNode *next;
    8. }LNode, *LinkList;
    9. //头插法建立
    10. LinkList List_HeadInsert(LinkList &L) {
    11. LNode *s; int x;
    12. L = (LinkList)malloc(sizeof(LNode)) ;
    13. L->next = NULL; //创建头结点
    14. cin >> x ;
    15. while(x != 9999){
    16. s = (LinkList)malloc(sizeof(LNode));
    17. s->data = x;
    18. s->next = L->next ;
    19. L->next = s ;
    20. cin >> x ;
    21. }
    22. return L ;
    23. }
    24. //尾插法建立
    25. LinkList List_TailInsert(LinkList &L) {
    26. int x;
    27. cin >> x;
    28. L = (LinkList)malloc(sizeof(LNode));
    29. LNode *s, *r = L;
    30. while(x!=9999) {
    31. s = (LNode *)malloc(sizeof(LNode));
    32. s->data = x;
    33. r->next = s;
    34. r = s;
    35. cin >> x;
    36. }
    37. r->next = NULL;
    38. return L;
    39. }
    40. //插入 : 在第i个位置插入(含头节点)
    41. bool ListInsert(LinkList &L, int i, int e) {
    42. if(i<1) return false ;
    43. LNode *p ; //扫描指针
    44. int j = 0; //带头节点 从0开始
    45. p = L;
    46. while(p != NULL && j < i-1) { //找到 i-1 的位置
    47. p = p->next ;
    48. j++;
    49. }
    50. if(p == NULL) return false;
    51. LNode *s = (LNode *)malloc(sizeof(LNode)) ;
    52. s->data = e;
    53. s->next = p->next;
    54. p->next = s;
    55. return true;
    56. }
    57. //删除 : 删除第i个位置的元素 并返回 (含头节点)
    58. bool ListDelete(LinkList &L, int i, int &e) {
    59. if(i<1) return false;
    60. LNode *p;
    61. int j=0;
    62. p = L;
    63. while(p != NULL && j < i-1) {
    64. p = p->next;
    65. j++;
    66. }
    67. if(p == NULL) return false;
    68. e = p->next->data;
    69. p->next = p->next->next;
    70. /*
    71. LNode *q = p->next;
    72. e = q->data;
    73. p->next = q->next;
    74. free(q);
    75. */
    76. return true;
    77. }
    78. //删除指定节点 (删除 p 节点)
    79. bool DeleteNode(LNode *p) {
    80. if(p == NULL) return false;
    81. p->data = p->next->data;
    82. p->next = p->next->next;
    83. /*
    84. LNode *q;
    85. q = p->next;
    86. p->data = q->data;
    87. p->next = q->next;
    88. free(q);
    89. */
    90. return true;
    91. }
    92. //按位查找
    93. LNode *GetElem(LinkList L, int i){
    94. if(i<0) return NULL;
    95. LNode *p;
    96. int j=0;
    97. p = L;
    98. while(p!=NULL && j
    99. p = p->next;
    100. j++;
    101. }
    102. //if(p==NULL) return NULL;
    103. return p;
    104. }
    105. //按值查找
    106. LNode *LocateElem(LinkList L, int e) {
    107. LNode *p = L; //初始化
    108. while(p != NULL && p->data != e) {
    109. p = p->next;
    110. }
    111. return p;
    112. }
    113. //展示当前链表
    114. void ShowList(LinkList L) {
    115. LNode *p = L->next;
    116. while(p!=NULL) {
    117. cout << p->data << " ";
    118. p = p->next;
    119. }
    120. cout << endl;
    121. }
    122. //初始化
    123. bool InitList(LinkList &L) {
    124. L = NULL;
    125. return true;
    126. }
    127. int main(){
    128. LinkList L;
    129. InitList(L); //初始化
    130. int x ;
    131. cout << "请选择头插(1、尾插(2:";
    132. cin >> x ;
    133. if(x == 1) {
    134. cout << "头插法:" ;
    135. List_HeadInsert(L); //头插法创建
    136. ShowList(L);
    137. }
    138. else {
    139. cout << "尾插法: " ;
    140. List_TailInsert(L); //尾插法创建
    141. ShowList(L);
    142. }
    143. ListInsert(L, 2, 3); //插入
    144. ShowList(L);
    145. int e;
    146. ListDelete(L, 2, e); //删除
    147. ShowList(L);
    148. // GetElem(L, 3); //查找按位
    149. ShowList(L);
    150. cout << GetElem(L, 3)->data << endl;
    151. // LocateElem(L, 3); //按值查
    152. ShowList(L);
    153. cout << LocateElem(L, 3)->data << endl;
    154. DeleteNode(LocateElem(L, 3));
    155. ShowList(L);
    156. return 0;
    157. }

    不含头结点单链表代码

    1. //2.3 链表 单链表(不带头节点)
    2. #include
    3. using namespace std ;
    4. //结点
    5. typedef struct LNode{
    6. int data;
    7. struct LNode *next;
    8. }LNode, *LinkList;
    9. //头插法建立
    10. LinkList List_HeadInsert(LinkList &L) {
    11. LNode *s; int x;
    12. cin >> x;
    13. L = (LinkList)malloc(sizeof(LNode)) ;
    14. L->data = x;
    15. L->next = NULL;
    16. cin >> x;
    17. while(x != 9999){
    18. s = (LinkList)malloc(sizeof(LNode));
    19. s->data = x;
    20. s->next = L ;
    21. L = s ;
    22. cin >> x ;
    23. }
    24. return L ;
    25. }
    26. //尾插法建立
    27. LinkList List_TailInsert(LinkList &L) {
    28. int x ;
    29. cin >> x;
    30. L = (LinkList)malloc(sizeof(LNode));
    31. LNode *s, *r = L;
    32. L->data = x;
    33. cin >> x;
    34. while(x!=9999) {
    35. s = (LNode *)malloc(sizeof(LNode));
    36. s->data = x;
    37. r->next = s;
    38. r = s;
    39. cin >> x;
    40. }
    41. r->next = NULL;
    42. return L;
    43. }
    44. //插入 : 在第i个位置插入(不含头节点)
    45. bool ListInsert(LinkList &L, int i, int e) {
    46. if(i<1) return false ;
    47. LNode *p ; //扫描指针
    48. int j = 1; //不带头节点 从1开始
    49. p = L;
    50. if(i==1) {
    51. LNode *s = (LNode *)malloc(sizeof(LNode)) ;
    52. s->data = e;
    53. s->next = p;
    54. p = s;
    55. return true;
    56. }
    57. while(p != NULL && j < i-1) { //找到 i-1 的位置
    58. p = p->next ;
    59. j++;
    60. }
    61. if(p == NULL) return false;
    62. LNode *s = (LNode *)malloc(sizeof(LNode)) ;
    63. s->data = e;
    64. s->next = p->next;
    65. p->next = s;
    66. return true;
    67. }
    68. //删除 : 删除第i个位置的元素 并返回 (不含头节点)
    69. bool ListDelete(LinkList &L, int i, int &e) {
    70. if(i<1) return false;
    71. LNode *p;
    72. int j=1;
    73. p = L;
    74. if(i==1) {
    75. e = p->data ;
    76. L = p->next;
    77. }
    78. while(p != NULL && j < i-1) {
    79. p = p->next;
    80. j++;
    81. }
    82. if(p == NULL) return false;
    83. e = p->next->data;
    84. p->next = p->next->next;
    85. /*
    86. LNode *q = p->next;
    87. e = q->data;
    88. p->next = q->next;
    89. free(q);
    90. */
    91. return true;
    92. }
    93. //删除指定节点 (删除 p 节点)
    94. bool DeleteNode(LNode *p, LinkList &L) {
    95. if(p == NULL) return false;
    96. if(p == L) {
    97. L = p->next;
    98. }
    99. p->data = p->next->data;
    100. p->next = p->next->next;
    101. /*
    102. LNode *q;
    103. q = p->next;
    104. p->data = q->data;
    105. p->next = q->next;
    106. free(q);
    107. */
    108. return true;
    109. }
    110. //按位查找
    111. LNode *GetElem(LinkList L, int i){
    112. if(i<=0) return NULL;
    113. LNode *p;
    114. int j=1;
    115. p = L;
    116. while(p!=NULL && j
    117. p = p->next;
    118. j++;
    119. }
    120. //if(p==NULL) return NULL;
    121. return p;
    122. }
    123. //按值查找
    124. LNode *LocateElem(LinkList L, int e) {
    125. LNode *p = L; //初始化
    126. while(p != NULL && p->data != e) {
    127. p = p->next;
    128. }
    129. return p;
    130. }
    131. //展示当前链表
    132. void ShowList(LinkList L) {
    133. LNode *p = L;
    134. while(p!=NULL) {
    135. cout << p->data << " ";
    136. p = p->next;
    137. }
    138. cout << endl;
    139. }
    140. //初始化
    141. bool InitList(LinkList &L) {
    142. L = NULL;
    143. return true;
    144. }
    145. int main(){
    146. LinkList L;
    147. InitList(L); //初始化
    148. int x ;
    149. cout << "请选择头插(1、尾插(2:";
    150. cin >> x ;
    151. if(x == 1) {
    152. cout << "头插法:" ;
    153. List_HeadInsert(L); //头插法创建
    154. ShowList(L);
    155. }
    156. else {
    157. cout << "尾插法: " ;
    158. List_TailInsert(L); //尾插法创建
    159. ShowList(L);
    160. }
    161. ListInsert(L, 2, 3); //插入
    162. ShowList(L);
    163. int e;
    164. ListDelete(L, 2, e); //删除
    165. ShowList(L);
    166. // GetElem(L, 3); //查找按位
    167. ShowList(L);
    168. cout << GetElem(L, 3)->data << endl;
    169. // LocateElem(L, 3); //按值查
    170. ShowList(L);
    171. cout << LocateElem(L, 3)->data << endl;
    172. DeleteNode(LocateElem(L, 3), L);
    173. ShowList(L);
    174. return 0;
    175. }

    运行结果如下: 

             第一行是我们头插法后单链表的展示,我们可以看出,其头插法的存储会与我们输入链表的顺序相反;第二行就是我们的插入操作结果,第三行就是我们的删除操作结果,第4-7行就是查找第三个位置的元素和3在什么位置并返回;最后一行则是删除第三个结点。

             第一行是我们尾插法后单链表的展示,我们可以看出,其尾插法的存储会与我们输入链表的顺序相同(为1-7);第二行就是我们的插入操作结果(在第二个位置插入3),第三行就是我们的删除操作结果(删除第二个位置的结点),第4-7行就是查找第三个位置的元素和3在什么位置并返回(这里我们查找3所在的位置和第三个元素与头插法查找的结果就是不同的);最后一行则是删除第三个结点。

             到这里我们对于单链表的基本学习也就结束了,至于一些其他的操作例如:求长度,这些简单的循环我们就不在这里过多叙述;相信各位大佬通过上面的学习,可以轻松的实现这些简单的功能。

  • 相关阅读:
    搭建源码调试环境—RocketMQ源码分析(一)
    RabbitMQ事务机制和确认机制
    Spring面试题16:Spring框架中的单例bean是线程安全的吗?Spring框架中bean的生命周期?哪些是重要的bean生命周期方法?
    笔记-fabric.js的使用-参考官网
    【C#】项目中添加png文件时,设置其“生成操作”(Build Action)属性
    【DAY04 软考中级备考笔记】数据结构基本结构和算法
    小微企业低成本获客攻略,媒介盒子无偿分享
    【广州华锐互动】VR景区云游:打造沉浸式网上虚拟旅游体验
    2023年上半年系统集成项目管理工程师什么时候报考?(附具体报名步骤)
    LeetCode数据流的第 K 大数值
  • 原文地址:https://blog.csdn.net/qq_62464995/article/details/136108445