线性表的链式表示

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lierming__/article/details/81182713

   由于顺序表的插入、删除操作需要移动大量的元素,影响了运行效率,由此引入了线性表的

链式存储。链式存储线性表时,不需要使用地址连续的存储单元,即它不要求逻辑上相邻的两个

元素在物理位置上也相邻,它是通过 “ 链  ” 建立起数据元素之间的逻辑关系,因此,对线性表

 的插入、删除不需要移动元素,而只需要修改指针

-------------单链表的定义

      线性表的链式存储又称为单链表,它是指通过一组任意的存储单元来存储线性表中的数据

元素。为了建立起数据元素之间的线性关系,对每个链表结点,除了存放元素自身的信息之外,

还需要存放一个指向其后继的指针。单链表结点结构如下图,其中,data 为数据域,存放数据

元素next 为指针域,存放其后继结点的地址

     

      --------------------------------------------------------------------------------------------------

扫描二维码关注公众号,回复: 2951113 查看本文章

       单链表中结点类型的描述如下:

             typedef  struct  LNode{              // 定义单链表结点类型

                   ElemType data ;                  // 数据域

                   struct  LNode  *next ;          // 指针域

             } LNode , * LinkList ;

       ---------------------------------------------------------------------------------------------------

         利用单链表可以解决顺序表需要大量的连续存储空间的缺点,但是单链表附加指针域,也存在

浪费存储空间的缺点。由于单链表的元素是离散地分布在存储空间中的,所以单链表是非随机存取

存储结构,即不能直接找到表中某个特定的结点。查找某个特定的结点时,需要从头开始遍历,依次

查找。

         通过 “ 头指针 ” 来标识一个单链表,如单链表 L ,头指针为 “  NULL ” 时则表示一个空表。此外,

为了操作上的方便,在单链表第一个结点之前附加一个结点,称为头结点。头结点的数据域不设任何信

息,也可以记录表长等相关信息。头结点的指针域指向线性表的第一个元素结点,如下图:

       

     头结点头指针的区分:不管带不带头结点,头指针始终指向链表的一个结点,而头结点是带头结点

链表中的第一个结点,结点内通常不存储信息。

     引入头结点后,可以带来两个优点:

     》》由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他

          位置上的操作一致,无须进行特殊处理。

     》》无论链表是否为空,其头指针是指向头结点的非空指针(空表中头结点的指针域为空),因此空表

          和非空表的处理也就统一了。

-------------单链表上基本操作的实现

     1. 采用头插法建立单链表

          该方法从一个空表开始,生成新结点,并将读取到的数据存放到新结点的数据域中,然后将新结点

       插入到当前链表的表头,即头结点之后。如下图:

         

         --------------------------------------------------------------------------------------------------------------------------

         头插法建立单链表的算法如下:

         LinkList  CreateList1(LinkList &L){

                  //从表尾到表头逆向建立单链表 L ,每次均在头结点之后插入元素

                  LNode  *s ;

                  int  x ;

                  L = ( LinkList ) malloc ( sizeof( LNode ) ) ;                  // 创建头结点

                  L->next = NULL ;                                                     // 初始为空链表

                  scanf ( " %d "  , &x) ;                                               // 输入结点的值

                  while( x!= 9999){                                                      // 输入9999 表示结束

                         s = ( LNode * ) malloc ( sizeof( LNode ) ) ;          // 创建新结点

                         s->data =  x ;

                         s->next = L->next;                                           // 将新结点插入表中,L为头指针

                         L->next = s ;

                         scanf ( " %d "  , &x) ;                                       

                  }                                                                            // while 结束

                 return  L;

         }

         --------------------------------------------------------------------------------------------------------------------------------------

                采用头插法建立单链表,读入数据的顺序与生成的链表中元素的顺序是相反的。每个结点插入

         的时间为 O(1) , 设单链表长为 n , 则总的时间复杂度为 O( n ) 。

     2. 采用尾插法建立单链表

                头插法建立单链表的算法虽然简单,但生成的链表中结点的次序和输入数据的顺序不一致。若

         希望两者次序一致,可采用尾插法。该方法是将新结点插入到当前链表的表尾上,为此必须增加一个

         尾指针 r , 使其始终指向当前链表的尾结点。如下图所示:

               

                ---------------------------------------------------------------------------------------------------------------

                尾插法建立单链表的算法如下:

                LinkList  CreateList2( LinkList &L){

                        // 从表头到表尾正向建立单链表 L ,每次均在表尾插入元素

                        int  x ;                                      // 设置元素类型为整型

                        L = ( LinkList ) malloc( sizeof( LNode ) ) ;

                        LNode  *s , *r = L ;                    // r 为表尾指针

                        scanf( " %d " , &x ) ;                 // 输入结点的值

                        while( x!= 9999){                       // 输入 9999 表示结束

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

                              s->data = x;

                              r-next = s ;

                              r = s ;                              // r 指向新的表尾结点

                              scanf( " %d " , &x ) ;

                        }

                        r->next = NULL;                     // 尾结点指针置空

                        return  L ;

                }

         ------------------------------------------------------------------------------------------------------------------------------

                因为附设了一个指向表尾结点的指针,故时间复杂度和头插法的相同。

     3.  按序号查找结点值

              在单链表中从第一个结点出发,顺指针 next 域逐个往下搜索,直到找到第 i 个结点为止,

         否则返回最后一个结点指针域 NULL 。

         ------------------------------------------------------------------------------------------------------------------------------

                按序号查找结点值的算法如下:

                LNode * GetElem( LinkList L , int i ){

                    // 本算法取出单链表 L (带头结点)中第 i 个位置的结点指针

                    int j = 1 ;                      // 计数,初始为1,即指向第一个结点

                    LNode * p = L->next ;    // 头结点指针赋给 p

                    if ( i == 0 ){

                           return L;               // 如果 i 等于 0 , 则返回头结点

                    }

                    if ( i < 1 ){

                          return NULL;         // 若 i 无效,则返回 NULL

                    }

                    while( p && j < i ){

                           p = p->next ;

                           j++;

                    }

                    return p ;                    // 返回第 i 个结点的指针,如果 i大于表长,

                                                       p = NULL , 直接返回 p 即可

                }

       ------------------------------------------------------------------------------------------------------------------

         按序号查找操作的时间复杂度为 O( n ) 。

     4.  按值查找结点

          从单链表的第一个结点开始,由前往后依次比较表中各结点数据域的值,若某结点数据域的

       值等于给定值 e , 则返回该结点的指针;若整个单链表中没有这样的结点,则返回 NULL。

          -----------------------------------------------------------------------------------------------------------------

          按值查找结点的算法如下:

          LNode * LocateElem ( LinkList L , ElemType  e ){

                //本算法直接查找单链表 L (带头结点)中数据域值等于 e 的结点指针,否则返回 NULL

               LNode * p = L->next ;

               while( p != NULL && P->data != e){   // 从 第 1 个结点开始查找 data 域为 e 的结点

                         p = p->next ;

               }

              return  p ;                                       // 找到返回该结点指针,否则返回 NULL

         }

         ----------------------------------------------------------------------------------------------------------------------

            按值查找操作的时间复杂度为 O( n)

     5. 插入结点操作

        插入操作是将值为 x 的新结点插入到单链表的第 i 个位置上。先检查插入位置的合法性,然后

    然后找到待插入位置的前驱结点,即第 i -1 个结点,再在其后插入新结点。

        算法首先调用 “ 按序号查找结点值的算法 ” GetElem(  L ,  i - 1 ) ,查找第 i -1 个结点。假设返回

    的第 i -1 个结点为 *p ,然后令新结点  *s 的指针域指向 *p 的后继结点,再令结点 *p 的指针域指向

    新插入结点 * s 。其操作过程如下图:

        

         -------------------------------------------------------------------------------------------------------------------------------

          实现插入结点的代码片段如下:

             第一步: p = GetElem(  L ,  i - 1 )       // 查找插入位置的前驱结点

             第二步: s->next = p->next ;             // 上图中的操作步骤 1

             第三步: p->next = s ;                      // 上图中的操作步骤  2

        ---------------------------------------------------------------------------------------------------------------------------------

            上面的代码片段中,第二步和第三步顺序不能颠倒。

            本算法主要的时间开销在于查找第 i -1 个元素,时间复杂度为 O( n ) 。若是在给定的结点

       后面插入新结点,则时间复杂度仅为 O( 1 ) 。

       扩展:对某结点进行前插操作。

              前插操作是指在某结点的前面插入一个新结点,后插操作的定义刚好与之相反,在单链表

         插入算法中,通常都是采用后插算法的。

               -----------------------------------------------------------------------------------------------------------

                 // 将 *s 结点插入到 *p 之前的主要代码片段

                s->next = p->next ;                           // 修改指针域,不能颠倒

                p->next = s ;

                temp = p->data ;                             //  交换数据域部分

                p->data = s->data ;

                s->data = temp ;

              小总结:先把申请的空间插入进去,然后交换一下数据域部分,即后插法

              变为前插法。

           ------------------------------------------------------------------------------------------------------------------

     6. 删除结点操作

             删除结点是将单链表的第 i 个结点删除。先检查删除位置的合法性,然后查找

       表中第 i -1 个结点,即被删除结点的前驱结点,再将其删除。其操作过程如下图:

            

             假设结点 *p 为找到的被删除结点的前驱结点,为了实现这一操作后的逻辑关系

       的变换,仅需要修改 *p 的指针域,即将 *p 的指针域 next 指向 *q 的下一个结点。

            ------------------------------------------------------------------------------------------------------------

              实现删除结点的代码片段如下:

              p = GetElem( L , i - 1 ) ;                   // 查找删除位置的前驱结点 

              q = p->next ;                                  // 令 q 指向被删除结点

              p->next = q -> next ;                       // 将 *q 结点从链中 “ 断开 ”

              free( q ) ;                                        // 释放结点的存储空间

          -------------------------------------------------------------------------------------------------------------

              和插入的算法一样,该算法的主要时间也是耗费在查找操作上,时间复杂度为 O( n ) 。

           扩展:删除结点 *p

            要实现删除某一给定结点 *p ,通常的做法是先从链表的头结点开始顺序找到其前驱

         结点,然后再执行删除操作即可,算法的时间复杂度为 O( n ) 。

             其实,删除结点 *p 的操作可以用删除 *p 的后继结点操作来实现,实质就是将其后继

         结点的值赋予其自身,然后删除后继结点,也能使得时间复杂度为 O( 1 ) 。

          ---------------------------------------------------------------------------------------------------------------

            实现上述操作的代码片段如下:

            q = p->next ;                                     //    令 q 指向 *p 的后继结点

            p->data = p->next->data;                   //  用后继结点中的数据覆盖要删除结点的数据

            p->next = q->next ;                          //  将 *q 结点从链中 “ 断开 ”

            free( q ) ;                                         // 释放后继结点的存储空间

        --------------------------------------------------------------------------------------------------------------------

   7. 求表长操作

        求表长操作就是计算机单链表中数据结点(不含头结点)的个数,需要从第一个结点开始

     顺序依次访问表中的每一个结点,为此需要设置一个计数器变量,每访问一个结点,计数器

     加 1 ,直到访问到空结点为止。算法的时间复杂度为 O( n ) 。

         需要注意的是,因为单链表的长度时不包括头结点的,因此,不带头结点和带头结点的

    单链表在求表长操作上会略有不同。对不带头结点的单链表,当表为空时,但单独处理。

         单链表是整个链表的基础,读者一定要熟练掌握单链表的基本操作算法,在设计算法时,

   建议先通过图示的方法理清算法的思路,然后再进行算法的编写。

-------------双链表

         单链表结点中只有一个指向其后继的指针,这使得单链表只能从头结点依次顺序地向后遍历。

   若要访问某个结点的前驱结点(插入、删除操作时),只能从头开始遍历,访问后继结点的时间

   复杂度为 O(1) , 访问前驱结点的时间复杂度为 O( n ) 。

          为了克服单链表的删除缺点,引入了双链表,双链表结点中有两个指针 prior 和 next  ,分别

    指向其前驱结点和后继结点。如下图所示:

         

     -----------------------------------------------------------------------------------------------------------------------------

             双链表中结点类型的描述如下:

              typedef struct DNode {                               // 定义双链表结点类型

                       ElemType  data ;                             // 数据域

                       struct  DNode  *prior , * next ;           // 前驱和后继指针

             }DNode , * DLinkList ;

       ---------------------------------------------------------------------------------------------------------------------------

           双链表仅仅是在单链表结点中增加了一个指向其前驱的 prior 指针,因此,在双链表中

      执行按值查找按位查找的操作和单链表相同。但双链表在插入和删除操作的实现上,和

       单链表有着较大的不同。这是因为 “ 链 ” 变化时也需要对 prior 指针做出修改,其关键在于

      保证在修改的过程中不断链。此外,双链表可以很方面地找到其前驱结点,因此,插入、

      删除结点算法的时间复杂度为 O(1) 。

      1. 双链表的插入操作

              在双链表中 p  所指的结点之后插入结点 *s ,其指针的变化过程如下图:

             

              -----------------------------------------------------------------------------------------------------------

              插入操作的代码片段如下:

                    第一步: s->next = p->next ;                           // 将结点 *s 插入到结点 *p 之后

                    第二步: p->next->prior  = s ; 

                    第三步: s->prior = p ;

                    第四步: p->next = s ;

             ------------------------------------------------------------------------------------------------------------

                 上面的代码的语句顺序不是唯一的,但也不是任意的,第一步和第二步必须在第四步

              之前,否则 *p 的后继结点的指针就丢掉了,导致插入失败。

     2. 双链表的删除操作

              删除双链表中结点 *p 的后继结点 *q ,其指针的变化过程如下图:

             

            --------------------------------------------------------------------------------------------------------------

               删除操作的代码片段如下:

               p->next = q->next ;             // 上图中的第一步

               q->next->prior = p ;             // 上图中的第二步

               free( q ) ;                           // 释放结点空间

              建立双链表的操作中,也可以采用如同单链表的头插法和尾插法,但是在操作上需要

       注意指针的变化和单链表有所不同。

-------------循环链表

           1. 循环单链表

               循环单链表和单链表的区别在于,表中最后一个结点指针不是 NULL ,而改为指向

             头结点,从而整个链表形成了一个环,如下图所示:

                

               在循环单链表中,表尾结点 *r 的 next 域指向 L ,故表中没有指针域为 NULL  的结点,因此,

           循环单链表的判空条件不是头结点的指针是否为空,而是它是否等于头指针。

                循环单链表的插入、删除算法与单链表几乎一样,所以不同的是如果操作是在表尾进行,则

            执行的操作不同,以让单链表继续保持循环的性质。当然,正是因为循环单链表是一个 “ 环 ” ,

            因此,在任何一个位置的插入和删除操作都是等价的,无须判断是否是表尾。

                 在单链表中只能从表头结点开始往后顺序遍历整个链表,而循环单链表可以从表中的任一结

            点开始遍历整个链表。有时对单链表常做的操作是在表头和表尾进行的,此时可以对循环单链表

             不设头指针而仅设尾指针,从而使得操作效率更高。其原因是若设的是头指针,对表尾进行操作

            需要 O( n ) 的时间复杂度,而如果设的是尾指针 r , r->next 即为头指针,对于表头与表尾进行

             操作都只需要 O( 1 ) 的时间复杂度。

          2. 循环双链表

               由循环单链表的定义不难推出循环双链表,不同的是在循环双链表中,头结点的 prior 指针还要

             指向表尾结点,如下图所示:

              

                  在循环双链表 L 中,某结点 *p 为尾结点时, p->next = = L ; 当循环双链表为空表时,其

             头结点的 prior 和 next 域都等于 L 。

-------------静态链表

            静态链表是借助数组来描述线性表的链式存储结构,结点也有数据域 data 和 指针域 next ,

       与前面所讲的链表中的指针不同的是,这里的指针结点的相对地址(数组下标),又称为游标

       和顺序表一样,静态链表也要预先分配一块连续的内存空间。

            静态链表和单链表的对应关系如下图:

            

      ---------------------------------------------------------------------------------------------------------------------

          静态链表结构类型的描述如下:

          # define MaxSize 50                            // 静态链表的最大长度

          typedef  struct {                                   // 静态链表结构类型的定义

                ElemType  data ;                           // 存储数据元素

                int  next ;                                      // 下一个元素的数组下标

          } SLinkList[ MaxSize ] ;

       -------------------------------------------------------------------------------------------------------------------

         静态链表以 next == -1 作为其结束的标志。静态链表的插入、删除操作与动态链表相同,

      只需要修改指针,而不需要移动元素。总体来说,静态链表没有单链表使用起来方便,但是在

      一些不支持指针的高级语言(如 Basic)中 ,这又是一种非常巧妙的设计方法。

-------------顺序表和链表的比较

      1. 存取方式

           顺序表可以顺序存取,也可以随机存取,链表只能从表头顺序存取元素。

      2. 逻辑结构和物理结构

           采用顺序存储时,逻辑上相邻的元素,其对应的物理存储位置也相邻。而采用链式存储时,

         逻辑上相邻的元素,其物理存储位置则不一定相邻,其对应的逻辑关系是通过指针链接来表示的。

          这里请读者注意区别存取方式存储方式

      3. 查找、插入和删除操作

           对于按值查找,当顺序表在无序的情况下,两者的时间复杂度均为 O( n ) ; 而当顺序表有序时,

        可采用折半查找,此时时间复杂度为 O()  。

           对于按序号查找,顺序表支持随机访问,时间复杂度为 O( 1 ) ,而链表的平均时间复杂度为

       O( n ) 。顺序表的插入、删除操作,平均需要移动半个表长的元素。链表的插入、删除操作,只需

       要修改相关结点的指针域即可。由于链表每个结点带有指针域,因而在存储空间上比顺序存储要

      付出较大的代价,存储密度不够大。

      4. 空间分配

            顺序存储在静态存储分配情形下,一旦存储空间装满就不能扩充,如果再加入新元素将出现内存

         溢出,需要预先分配足够大的存储空间。预先分配过大,可能会导致顺序表后部大量闲置;

         预先分配过小,又会造成溢出。

             动态存储分配虽然存储空间可以扩充,但需要移动大量元素,导致操作效率降低,而且若内存中

         没有更大块的连续存储空间将导致分配失败。

            链式存储的结点空间只在需要的时候申请分配,只要内存有空间就可以分配,操作灵活、高效。

---------------在实际中应该怎样选取存储结构呢?

      1. 基于存储的考虑

          对线性表的长度或存储规模难以估计时,不宜采用顺序表;链表不用事先估计存储规模,但是链表

       的存储密度较低,显然链表存储结构的存储密度是小于 1 的。

      2. 基于运算的考虑

          在顺序表中按序号访问 a (i) 的时间复杂度为 O( 1 ) ,而链表中按序号访问的时间复杂度为 O( n ) ,

      所以如果经常做的运算是按序号访问数据元素,显然顺序表优于链表。

         在顺序表中做插入、删除操作时,平均移动表中一半的元素,当数据元素的信息量较大且表较长时,

       这一点是不应忽视的;在链表中做插入、删除操作时,虽然也要找插入位置,但是操作是比较简单的,

      从这个角度考虑显然后者优于前者。

      3. 基于环境的考虑

          顺序表容易实现,任何高级语言中都有数组类型;链表的操作是基于指针的,相对来讲,前者实现

       较为简单,这也是用户考虑的一个因素。

          总之,两种存储结构各有长短,选择哪一种由实际问题的主要因素决定。通常较稳定的线性表选择

       顺序存储,而频繁做插入、删除操作的线性表(即动态性较强)宜选择链式存储。

猜你喜欢

转载自blog.csdn.net/lierming__/article/details/81182713