• 线性表


    1、定义

    线性表(List):零个或多个数据元素的有限序列。

    线性结构是最简单且最常用的数据结构。线性表是一种典型的线性结构。

    说明:

    ① 数据元素的个数n定义为表的长度(n=0时称为空表)。
    ② 将非空的线性表(n>0)记作:(a1,a2,…,an
    ③ 数据元素ai(1≤i≤n)只是个抽象符号,其具体含义在不同情况下可以不同。

    2、基本运算

    (1)、InitList(L) 
         构造一个空的线性表L,即表的初始化。

    (2)、 ListLength(L) 
         求线性表L中的结点个数,即求表长。

    (3)、GetNode(L,i) 
         取线性表L中的第i个结点,这里要求1≤i≤ListLength(L)

    (4)、LocateNode(L,x) 
         在L中查找值为x 的结点,并返回该结点在L中的位置。若L中有多个结点的值和x 相同,则返回首次找到的结点位置;若L中没有结点的值为x ,则返回一个特殊值表示查找失败。

    (5)、InsertList(L,x,i) 
         在线性表L的第i个位置上插入一个值为x 的新结点,使得原编号为i,i+1,…,n的结点变为编号为i+1,i+2,…,n+1的结点。这里1≤i≤n+1,而n是原表L的长度。插入后,表L的长度加1。

    (6)、DeleteList(L,i) 
         删除线性表L的第i个结点,使得原编号为i+1,i+2,…,n的结点变成编号为i,i+1,…,n-1的结点。这里1≤i≤n,而n是原表L的长度。删除后表L的长度减1。

    注意:
         以上所提及的运算是逻辑结构上定义的运算。只要给出这些运算的功能是"做什么",至于"如何做"等实现细节,只有待确定了存储结构之后才考虑。

    3、顺序存储结构

    (1)、顺序表

    a、顺序表的定义 

    (1) 顺序存储方法    

       即把线性表的结点按逻辑次序依次存放在一组地址连续的存储单元里的方法。
    (2) 顺序表(Sequential List)
       用顺序存储方法存储的线性表简称为顺序表(Sequential List)。

    b、结点ai的存储地址

      不失一般性,设线性表中所有结点的类型相同,则每个结点所占用存储空间大小亦相同。假设表中每个结点占用c个存储单元,其中第一个单元的存储地址则是该结点的存储地址,并设表中开始结点a1的存储地址(简称为基地址)是LOC(a1)(LOC=Location,即地址),那么结点ai的存储地址LOC(ai)可通过下式计算:
                LOC(ai)= LOC(a1)+(i-1)*c, 1≤i≤n
     注意:
        
     在顺序表中,每个结点ai的存储地址是该结点在表中的位置i的线性函数。只要知道基地址和每个结点的大小,就可在相同时间内求出任一结点的存储地址。是一种随机存取结构

    c、顺序表类型定义

      #define ListSize 100 //表空间的大小可根据实际需要而定,这里假设为100
      typedef int DataType; //DataType的类型可根据实际情况而定,这里假设为int
      typedef struct {
          DataType data[ListSize];//向量data用于存放表结点
          int length;//当前的表长度
         }SeqList;     

    注意:
         ① 用向量这种顺序存储的数组类型存储线性表的元素外,顺序表还应该用一个变量来表示线性表的长度属性,因此用结构类型来定义顺序表类型
         ② 存放线性表结点的向量空间的大小ListSize应仔细选值,使其既能满足表结点的数目动态增加的需求,又不致于预先定义过大而浪费存储空间。
         ③ 由于C语言中向量的下标从0开始,所以若L是SeqList类型的顺序表,则线性表的开始结点a1和终端结点an分别存储在L.data[0]和L.Data[L.length-1]中。
         ④ 若L是SeqList类型的指针变量,则a1和an
    分别存储在L->data[0]和L->data[L->length-1]中

    d、顺序表的特点

         顺序表是用向量实现的线性表,向量的下标可以看作结点的相对地址。因此顺序表的的特点是逻辑上相邻的结点其物理位置亦相邻

    (2)、顺序表上实现的基本运算

    a、表的初始化

       void InitList(SeqList *L)
         {\顺序表的初始化即将表的长度置为0
                L->length=0;
        }

    b、求表长

        int ListLength(SeqList *L)
           { \求表长只需返回L->length
              return L->length;
          }

    c、取表中第i个结点

      DataType GetNode(L,i)
          {//取表中第i个结点只需返回和L->data[i-1]即可
              if (i<1||i> L->length)
                      Error("position error");
              return L->data[i-1];
           }
    注意:我们需要获取i个位置的数据,在数组中对应的是i-1个数据,因此i=0或者i=length+1都是非法的位置
      bool GetNode(SqList L,int i,ElemType *e)
          {
              if (L.length==0 || i<1|| i> L.length)
                      Error("position error");
              *e=L.data[i-1];
              return OK;
           }
    注意:这里定义的是SqList对象,局部变量,属于值传递,是单向的,故需要引入指针型变量ElemType *e接收返回值,而不能使用ElemType类型变量。
       这里的函数返回值是bool值而不是ElemType,注意与前面函数的区别!!!

      也可以这样子:

      DataType GetNode(SqList *L,int i)
          {
              if ( L->length==0 || i<1|| i> L->length-1)
                      return ERROR;
              return L->data[i-1];
           }

    d、查找值为x的结点

    e、插入
    (1)、 插入运算的逻辑描述
      线性表的插入运算是指在表的第i(1≤i≤n+1)个位置上,插入一个新结点x,使长度为n的线性表: 

              (a1,…,ai-1,ai,…an)

    变成长度为n+1的线性表: 

              (a1,…,ai-1,x,ai,…an)

    注意:
         ① 由于向量空间大小在声明时确定,当L->length≥ListSize时,表空间已满,不可再做插入操作
         ② 当插入位置i的值为i>n或i<1时为非法位置,不可做正常插入操作

    (2)、顺序表插入操作过程
      在顺序表中,结点的物理顺序必须和结点的逻辑顺序保持一致,因此必须将表中位置为n ,n-1,…,i上的结点,依次后移到位置n+1,n,…,i+1上,空出第i个位置,然后在该位置上插入新结点x。仅当插入位置i=n+1时,才无须移动结点,直接将x插入表的末尾。

    (3)、具体算法描述

       void InsertList(SeqList *L,DataType x,int i)
         {//将新结点 x插入L所指的顺序表的第i个结点ai的位置上
           int j;
           if (i<1||i>L->length+1)
               Error("position error");//非法位置,退出运行
           if (L->length>=ListSize)
               Error("overflow");     //表空间溢出,退出运行
           for(j=L->length-1;j>=i-1;j--)
                L->data[j+1]=L->data[j];//结点后移
           L->data[i-1]=x;      //插入x
           L->Length++;        //表长加1
         }

    (4)、算法分析
    ① 问题的规模
       表的长度L->length(设值为n)是问题的规模。
    ② 移动结点的次数由表长n和插入位置i决定
       算法的时间主要花费在for循环中的结点后移语句上。该语句的执行次数是n-i+1。
         当i=n+1:移动结点次数为0,即算法在最好时间复杂度是0(1)
         当i=1:移动结点次数为n,即算法在最坏情况下时间复杂度是0(n)
    ③ 移动结点的平均次数Eis
    (n) 
          
           
    其中:
      在表中第i个位置插入一个结点的移动次数为n-i+1
      pi表示在表中第i个位置上插入一个结点的概率。不失一般性,假设在表中任何合法位置(1≤i≤n+1)上的插入结点的机会是均等的,则

                     p1=p2=…=pn+1=1/(n+1)

      因此,在等概率插入的情况下,

           
      即在顺序表上进行插入运算,平均要移动一半结点。

    f、 删除
    (1)、删除运算的逻辑描述
      线性表的删除运算是指将表的第i(1≤i≤n)个结点删去,使长度为n的线性表

                 (a1,…,ai-1,ai,ai+1,…,an)

      变成长度为n-1的线性表

                 (a1,…,ai-1,ai+1,…,an)

    注意:
      当要删除元素的位置i不在表长范围(即i<1或i>L->length)时,为非法位置,不能做正常的删除操作

    (2)、顺序表删除操作过程

      在顺序表上实现删除运算必须移动结点,才能反映出结点间的逻辑关系的变化。若i=n,则只要简单地删除终端结点,无须移动结点;若1≤i≤n-1,则必须将表中位置i+1,i+2,…,n的结点,依次前移到位置i,i+1,…,n-1上,以填补删除操作造成的空缺。

    (3)、具体算法描述

       void DeleteList(SeqList *L,int i)
          {//从L所指的顺序表中删除第i个结点ai
             int j;
            if(i<1||i>L->length)
               Error("position error"); //非法位置
            for(j=i;j<=L->length-1;j++)
               L->data[j-1]=L->data[j]; //结点前移
            L->length--;                //表长减小
           }


    (4)、算法分析

      ①结点的移动次数由表长n和位置i决定:
         i=n时,结点的移动次数为0,即为0(1)
         i=1时,结点的移动次数为n-1,算法时间复杂度分别是0(n)
      ②移动结点的平均次数EDE(n)

            

    其中:
      删除表中第i个位置结点的移动次数为n-i
      pi表示删除表中第i个位置上结点的概率。不失一般性,假设在表中任何合法位置(1≤i≤n)上的删除结点的机会是均等的,则

               p1=p2=…=pn=1/n


      因此,在等概率插入的情况下,

            
      
      顺序表上做删除运算,平均要移动表中约一半的结点,平均时间复杂度也是0(n)。

    3、链式存储结构

    (1)、单链表

    a、链接存储方法
         链接方式存储的线性表简称为链表(Linked List)。
         链表的具体存储表示为:
      ① 用一组任意的存储单元来存放线性表的结点(这组存储单元既可以是连续的,也可以是不连续的)
      ② 链表中结点的逻辑次序和物理次序不一定相同。为了能正确表示结点间的逻辑关系,在存储每个结点值的同时,还必须存储指示其后继结点的地址(或位置)信息(称为指针(pointer)或链(link))
    注意:
      
    链式存储是最常用的存储方式之一,它不仅可用来表示线性表,而且可用来表示各种非线性的数据结构。


    b、链表的结点结构

      ┌──┬──┐
      │data│next│
      └──┴──┘ 
           data域--存放结点值的数据域
           next域--存放结点的直接后继的地址(位置)的指针域(链域)
    注意:
         ①链表通过每个结点的链域将线性表的n个结点按其逻辑顺序链接在一起的。

         ②每个结点只有一个链域的链表称为单链表(Single Linked List)。
    【例】线性表(bat,cat,eat,fat,hat,jat,lat,mat)的单链表示如示意图   


    c、头指针head和终端结点指针域的表示
         单链表中每个结点的存储地址是存放在其前趋结点next域中,而开始结点无前趋,故应设头指针head指向开始结点。
    注意:
         
    链表由头指针唯一确定,单链表可以用头指针的名字来命名。

    【例】头指针名是head的链表可称为表head。
      终端结点无后继,故终端结点的指针域为空,即NULL。

    d、单链表的一般图示法

         由于我们常常只注重结点间的逻辑顺序,不关心每个结点的实际位置,可以用箭头来表示链域中的指针,线性表(bat,cat,fat,hat,jat,lat,mat)的单链表就可以表示为下图形式。

          
    e、单链表类型描述

      typedef char DataType; //假设结点的数据域类型为字符
      typedef struct node{   //结点类型定义
           DataType data;    //结点的数据域
           struct node *next;//结点的指针域
         }ListNode;
      typedef ListNode *LinkList;
      ListNode *p;
      LinkList head;


      注意:
         ①LinkList和ListNode *是不同名字的同一个指针类型(命名的不同是为了概念上更明确)
         ②LinkList类型的指针变量head表示它是单链表的头指针
         ③ListNode *类型的指针变量p表示它是指向某一结点的指针

    f、指针变量和结点变量

    ┌────┬────────────┬─────────────┐ 
    │    │    指针变量    │     结点变量      │
    ├────┼────────────┼─────────────┤
    │  定义  │在变量说明部分显式定义  │在程序执行时,通过标准    │
    │        │                        │函数malloc生成            │
    ├────┼────────────┼─────────────┤
    │  取值  │ 非空时,存放某类型结点 │实际存放结点各域内容      │
    │        │的地址                  │                          │
    ├────┼────────────┼─────────────┤
    │操作方式│ 通过指针变量名访问     │ 通过指针生成、访问和释放 │
    └────┴────────────┴─────────────┘
       
    ①生成结点变量的标准函数

         p=( ListNode *)malloc(sizeof(ListNode));
    //函数malloc分配一个类型为ListNode的结点变量的空间,并将其首地址放入指针变量p中

    ②释放结点变量空间的标准函数 

         free(p);//释放p所指的结点变量空间

    ③结点分量的访问 
         
    利用结点变量的名字*p访问结点分量

     方法一:(*p).data和(*p).next
     方法二:p-﹥data和p-﹥next
    ④指针变量p和结点变量*p的关系 
     
        指针变量p的值——结点地址

         结点变量*p的值——结点内容
         (*p).data的值——p指针所指结点的data域的值
         (*p).next的值——*p后继结点的地址
      *((*p).next)——*p后继结点
    注意:
       ① 若指针变量p的值为空(NULL),则它不指向任何结点。此时,若通过*p来访问结点就意味着访问一个不存在的变量,从而引起程序的错误。
         ② 有关指针类型的意义和说明方式的详细解释。

    (2)、单链表的运算

    a、建立单链表
      假设线性表中结点的数据类型是字符,我们逐个输入这些字符型的结点,并以换行符' '为输入条件结束标志符。动态地建立单链表的常用方法有如下两种:

    (1)、头插法建表
    ① 算法思路
         从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。

    注意:
         该方法生成的链表的结点次序与输入顺序相反。

    ② 具体算法实现

        LinkList CreatListF(void)
          {//返回单链表的头指针
              char ch;
              LinkList head;//头指针
              ListNode *s;  //工作指针
              head=NULL;    //链表开始为空
              ch=getchar(); //读入第1个字符
              while(ch!='
    '){
                  s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
                  s->data=ch;   //将读入的数据放入新结点的数据域中
                  s->next=head;
                  head=s;
                  ch=getchar();  //读入下一字符
                }
              return head;
           } 
    

    (2)、尾插法建表 
    ① 算法思路 
         
    从一个空表开始,重复读入数据,生成新结点,将读入数据存放在新结点的数据域中,然后将新结点插入到当前链表的表尾上,直到读入结束标志为止。

     
    注意:
        ⒈采用尾插法建表,生成的链表中结点的次序和输入顺序一致

        ⒉必须增加一个尾指针r,使其始终指向当前链表的尾结点

    ② 具体算法实现 

     LinkList CreatListR(void)
          {//返回单链表的头指针
              char ch;
              LinkList head;//头指针
              ListNode *s,*r;  //工作指针
              head=NULL;    //链表开始为空
              r=NULL;//尾指针初值为空
              ch=getchar(); //读入第1个字符
              while(ch!='
    '){
                  s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
                  s->data=ch;   //将读入的数据放入新结点的数据域中
               if (head!=NULL)
                   head=s;//新结点插入空表
               else
                   r->next=s;//将新结点插到*r之后
                  r=s;//尾指针指向新表尾
               ch=getchar();  //读入下一字符
             }//endwhile
            if (r!=NULL)
                 r->next=NULL;//对于非空表,将尾结点指针域置空head=s;
             return head;
        } 


    注意:
     
    1、开始结点插入的特殊处理

           由于开始结点的位置是存放在头指针(指针变量)中,而其余结点的位置是在其前趋结点的指针域中,插入开始结点时要将头指针指向开始结点。
     2、空表和非空表的不同处理
          若读入的第一个字符就是结束标志符,则链表head是空表,尾指针r亦为空,结点*r不存在;否则链表head非空,最后一个尾结点*r是终端结点,应将其指针域置空。

    (3)、尾插法建带头结点的单链表 
    ①头结点及作用
         
    头结点是在链表的开始结点之前附加一个结点。它具有两个优点:
        ⒈由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作就和在表的其它位置上操作一致,无须进行特殊处理;
        ⒉无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域空),因此空表和非空表的处理也就统一了。

    ②带头结点的单链表

         
       

    注意:
      
    头结点数据域的阴影表示该部分不存储信息。在有的应用中可用于存放表长等附加信息。

    ③尾插法建带头结点链表算法 

      LinkList CreatListR1(void)
          {//用尾插法建立带头结点的单链表
              char ch;
              LinkList head=(ListNode *)malloc(sizeof(ListNode));//生成头结点
              ListNode *s,*r;  //工作指针
              r=head;    // 尾指针初值也指向头结点
              while((ch=getchar())!='
    '){
                  s=(ListNode *)malloc(sizeof(ListNode));//生成新结点
                  s->data=ch;   //将读入的数据放入新结点的数据域中
                  r->next=s;
                  r=s;
                }
              r->next=NULL;//终端结点的指针域置空,或空表的头结点指针域置空
              return head;
           } 


    注意:
         
    上述算法里,动态申请新结点空间时未加错误处理,这对申请空间极少的程序而言不会出问题。但在实用程序里,尤其是对空间需求较大的程序,凡是涉及动态申请空间,一定要加入错误处理以防系统无空间可供分配。

    (4)、算法时间复杂度 
         以上三个算法的时间复杂度均为0(n)。

    b、单链表的查找运算 
    (1)、按序号查找
    ① 链表不是随机存取结构

         在链表中,即使知道被访问结点的序号i,也不能像顺序表中那样直接按序号i访问结点,而只能从链表的头指针出发,顺链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构。

    ② 查找的思想方法
         计数器j置为0后,扫描指针p指针从链表的头结点开始顺着链扫描。当p扫描下一个结点时,计数器j相应地加1。当j=i时,指针p所指的结点就是要找的第i个结点。而当p指针指为null且j≠i时,则表示找不到第i个结点。
    注意:
      头结点可看做是第0个结点。

    ③具体算法实现
     

      

      ListNode* GetNode(LinkList head,int i)
         {//在带头结点的单链表head中查找第i个结点,若找到(0≤i≤n),
          //则返回该结点的存储位置,否则返回NULL。
          int j;
          ListNode *p;
          p=head;j=0;//从头结点开始扫描
          while(p->next&&j<i){//顺指针向后扫描,直到p->next为NULL或i=j为止
              p=p->next;
              j++;
            }
          if(i==j)
             return p;//找到了第i个结点
          else return NULL;//当i<0或i>0时,找不到第i个结点
         } 


    ④算法分析

         算法中,while语句的终止条件是搜索到表尾或者满足j≥i,其频度最多为i,它和被寻找的位置有关。在等概率假设下,平均时间复杂度为:

              


    (2)、按值查找
    ①思想方法 
        
     从开始结点出发,顺着链逐个将结点的值和给定值key作比较,若有结点的值与key相等,则返回首次找到的其值为key的结点的存储位置;否则返回NULL。

    ②具体算法实现

        ListNode* LocateNode (LinkList head,DataType key)
          {//在带头结点的单链表head中查找其值为key的结点
            ListNode *p=head->next;//从开始结点比较。表非空,p初始值指向开始结点
            while(p&&p->data!=key)//直到p为NULL或p->data为key为止
                 p=p->next;//扫描下一结点
             return p;//若p=NULL,则查找失败,否则p指向值为key的结点
           }

    ③算法分析
         该算法的执行时间亦与输入实例中key的取值相关,其平均时间复杂度分析类似于按序号查找,为O(n)。

    c、插入运算
    (1)、思想方法
      插入运算是将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。
    具体步骤: 
      (1)找到ai-1存储位置p
      (2)生成一个数据域为x的新结点*s
      (3)令结点*p的指针域指向新结点
      (4)新结点的指针域指向结点ai
        
      

    (2)、具体算法实现

        void InsertList(LinkList head,DataType x,int i)
          {//将值为x的新结点插入到带头结点的单链表head的第i个结点的位置上
            ListNode *p;
            p=GetNode(head,i-1);//寻找第i-1个结点
            if (p==NULL)//i<1或i>n+1时插入位置i有错
               Error("position error");
            s=(ListNode *)malloc(sizeof(ListNode));
            s->data=x;s->next=p->next;p->next=s;
          }    

    (3)、算法分析
         算法的时间主要耗费在查找操作GetNode上,故时间复杂度亦为O(n)。

    d、删除运算
    (1)、思想方法
         删除运算是将表的第i个结点删去。
    具体步骤: 
       
     (1)找到ai-1的存储位置p(因为在单链表中结点ai的存储地址是在其直接前趋结点ai-1的指针域next中)

        (2)令p->next指向ai的直接后继结点(即把ai从链上摘下)
        (3)释放结点ai的空间,将其归还给"存储池"。
        


    (2)、具体算法实现

        void DeleteList(LinkList head,int i)
          {//删除带头结点的单链表head上的第i个结点
             ListNode *p,*r;
             p=GetNode(head,i-1);//找到第i-1个结点
             if (p==NULL||p->next==NULL)//i<1或i>n时,删除位置错
                  Error("position error");//退出程序运行
             r=p->next;//使r指向被删除的结点ai
             p->next=r->next;//将ai从链上摘下
             free(r);//释放结点ai的空间给存储池
           } 

    注意:
      设单链表的长度为n,则删去第i个结点仅当1≤i≤n时是合法的。
      当i=n+1时,虽然被删结点不存在,但其前趋结点却存在,它是终端结点。因此被删结点的直接前趋*p存在并不意味着被删结点就一定存在,仅当*p存在(即p!=NULL)且*p不是终端结点(即p->next!=NULL)时,才能确定被删结点存在。

    (3)、算法分析

      算法的时间复杂度也是O(n)。
      链表上实现的插入和删除运算,无须移动结点,仅需修改指针。

    (3)、循环链表

    循环链表是一种首尾相接的链表。

    a、循环链表

    (1)、单循环链表——在单链表中,将终端结点的指针域NULL改为指向表头结点或开始结点即可。

    (2)、多重链的循环链表
    ——将表中结点链在多个环上。

        

    b、带头结点的单循环链表
            
    注意:
      判断空链表的条件是head==head->next;


    c、仅设尾指针的单循环链表

      用尾指针rear表示的单循环链表对开始结点a1和终端结点an查找时间都是O(1)。而表的操作常常是在表的首尾位置上进行,因此,实用中多采用尾指针表示单循环链表。带尾指针的单循环链表可见下图。
              
    注意:
      判断空链表的条件为rear==rear->next;

    4、循环链表的特点

      循环链表的特点是无须增加存储量,仅对表的链接方式稍作改变,即可使得表处理更加方便灵活。
       

    【例】在链表上实现将两个线性表(a1,a2,…,an)和(b1,b2,…,bm)连接成一个线性表(a1,…,an,b1,…bm)的运算。
    分析:若在单链表或头指针表示的单循环表上做这种链接操作,都需要遍历第一个链表,找到结点an,然后将结点b1链到an的后面,其执行时间是O(n)。若在尾指针表示的单循环链表上实现,则只需修改指针,无须遍历,其执行时间是O(1)。

           
    相应的算法如下:

          LinkList Connect(LinkList A,LinkList B)
           {//假设A,B为非空循环链表的尾指针
              LinkList p=A->next;//①保存A表的头结点位置
              A->next=B->next->next;//②B表的开始结点链接到A表尾
              free(B->next);//③释放B表的头结点
              B->next=p;//④
              return B;//返回新循环链表的尾指针
        } 

    注意:
    ①  循环链表中没有NULL指针。涉及遍历操作时,其终止条件就不再是像非循环链表那样判别p或p->next是否为空,而是判别它们是否等于某一指定指针,如头指针或尾指针等。
    ②  在单链表中,从一已知结点出发,只能访问到该结点及其后续结点,无法找到该结点之前的其它结点。而在单循环链表中,从任一结点出发都可访问到表中所有结点,这一优点使某些运算在单循环链表上易于实现。

    (4)、双链表

     a、双向链表(Double Linked List)
      双(向)链表中有两条方向不同的链,即每个结点中除next域存放后继结点地址外,还增加一个指向其直接前趋的指针域prior。

    注意:  
         ①  双链表由头指针head惟一确定的。
         ②  带头结点的双链表的某些运算变得方便。
         ③  将头结点和尾结点链接起来,为双(向)循环链表。

    b、双向链表的结点结构和形式描述
    ①结点结构(见上图a)
     
         
    ②形式描述 

        typedef struct dlistnode{
             DataType data;
             struct dlistnode *prior,*next;
          }DListNode;
        typedef DListNode *DLinkList;
        DLinkList head;


    c、双向链表的前插和删除本结点操作
      由于双链表的对称性,在双链表能能方便地完成各种插入、删除操作。
    ①双链表的前插操作
         
     

        void DInsertBefore(DListNode *p,DataType x)
          {//在带头结点的双链表中,将值为x的新结点插入*p之前,设p≠NULL
            DListNode *s=malloc(sizeof(DListNode));//①
            s->data=x;//②
            s->prior=p->prior;//③
            s->next=p;//④
            p->prior->next=s;//⑤
            p->prior=s;//⑥
           }


    ②双链表上删除结点*p自身的操作
        
      

        void DDeleteNode(DListNode *p)
          {//在带头结点的双链表中,删除结点*p,设*p为非终端结点
              p->prior->next=p->next;//①
              p->next->prior=p->prior;//②
              free(p);//③
          } 


    注意:
         与单链表上的插入和删除操作不同的是,在双链表中插入和删除必须同时修改两个方向上的指针。
         上述两个算法的时间复杂度均为O(1)。

    4、顺序表与链表对比

    顺序表和链表各有短长。在实际应用中究竟选用哪一种存储结构呢?这要根据具体问题的要求和性质来决定。通常有以下几方面的考虑
    ┌───┬───────────────┬───────────────┐

    │      │         顺序表          │         链表            │
    ├─┬─┼───────────────┼───────────────┤
    │基│分│静态分配。程序执行之前必须明确动态分配只要内存空间尚有空闲,
    │于│配│规定存储规模。若线性表长度n变 就不会产生溢出。因此,当线性表
    │空│方│化较大,则存储规模难于预先确定│的长度变化较大,难以估计其存储│ 
    │间│式│估计过大将造成空间浪费,估计太规模时,以采用动态链表作为存储
    │考│  │小又将使空间溢出机会增多。    │结构为好。                    │
    │虑├─┼───────────────┼───────────────┤
    │  │存│为1。当线性表的长度变化不大, │<1                            │
    │  │储│易于事先确定其大小时,为了节约│                              │
    │  │密│存储空间,宜采用顺序表作为存储│                              │
    │  │度│结构。                        │                              │
    ├─┼─┼───────────────┼───────────────┤
    基│存│随机存取结构,对表中任一结点都│顺序存取结构,链表中的结点,需│
    │于│取│可在O(1)时间内直接取得      │从头指针起顺着链扫描才能取得。│
    │时│方│线性表的操作主要是进行查找,很│                              │

    │少做插入和删除操作时,采用顺序│                              │
    │  │表做存储结构为宜。            │                              │
    ├─┼───────────────┼───────────────┤
    │  │
    插│在顺序表中进行插入和删除,平均│在链表中的任何位置上进行插入和│
    │  │入│要移动表中近一半的结点,尤其是│删除,都只需要修改指针。对于频│
    │  │删│当每个结点的信息量较大时,移动│繁进行插入和删除的线性表,宜采│
    │  │除│结点的时间开销就相当可观。    │用链表做存储结构。若表的插入和│

    │  ││                              │删除主要发生在表的首尾两端,则│
    │  ││                              │采用尾指针表示的单循环链表为宜│
    └─┴─┴───────────────┴───────────────┘

        存储密度Storage Density)是指结点数据本身所占的存储量和整个结点结构所占的存储量之比,即

        存储密度=(结点数据本身所占的存储量)/(结点结构所占的存储总量)

     

  • 相关阅读:
    线性回归(Linear Regression)的理解及原理
    3个模型搞清楚用户留存分析
    机器学习简单介绍
    数据分析经典方法:5W2H分析法
    使用guava RateLimiter限流
    Maven之assembly自定义打包
    IDE自动编译
    神奇的$scope
    二分法查找
    深入理解CSS选择器优先级
  • 原文地址:https://www.cnblogs.com/yedushusheng/p/5526300.html
Copyright © 2020-2023  润新知