线性表的链式存储结构

一、链式存储结构定义

  链式存储结构:

         结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻,线性表的链式表示又称为非顺序映像或链式映像

  数组与链表

       数组是需要一块连续的内存空间来存储数据,对内存的要求非常高
       链表并不需要一块连续的内存空间,只要内存空间充足,即使内存空间存在碎片,只要碎片
的大小足够存储一个链表节点的数据,该碎片的空间都有可能被分配,链表通过指针将一组零散的空间串联起来使用。

把串联在链表上的每一个内存块称为链表的结点
   链表各结点由两个域组成:
      数据域:存储元素数值数据
      指针域:存储直接后继结点的存储位置

单链表、双链表、循环链表、静态链表:
   结点只有一个指针域的链表,称为单链表或线性链表
   有两个指针域的链表,称为双链表,通常头指针只设置一个,除非实际情况需要
   首尾相接的链表称为循环链表
   用数组描述的链表称为静态链表

头指针、头结点和首元结点

   头指针是指向链表中第一个结点的指针,是链表的名字
   首元结点是指链表中存储第一个数据元素a1的结点
   头结点是在链表的首元结点之前附设的一个结点

头节点和头指针的区分:

    不管带不带头节点,头指针都始终指向链表的第一个结点,而头结点是带头结点的链表中的第一个结点,结点内通常不存储信息。

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

       1.由于第一个数据结点的位置被存放在头节点的指针域中,因此在链表的第一个位置上操作和在表的其他位置上的操作一致,无须进行特殊处理。

       2.无论链表是否为空,其头指针都是指向头结点的非空指针(空表中头结点的指针域为空),因此空表和非空表的处理也就得到了统一。

如何表示空表?

      有头结点时,当头结点的指针域为空时表示空表 无头结点时,头指针为空时表示空表。

为什么在链表中设置头结点?
    1、防止空链表无法操作与表示。
    2、在第一个元素结点前插入结点(或删除第一个结点),使其操作与对其它结点一致。
    3、头结点的存在使得空链表与非空链表的处理操作一致。
    4、对单链表的多数操作应明确相应结点以及该结点的前驱。

链表(链式存储结构)的特点
(1)结点在存储器中的位置是任意的,即逻辑上相邻的数据元素在物理上不一定相邻
(2)访问时只能通过头指针进入链表,并通过每个结点的指针域向后扫描其余结点,所以寻找第一个结点和最后一个结点所花费的时间不等
    优点
           • 数据元素的个数可以自由扩充
           • 插入、删除等操作不必移动数据,只需修改链接指针,修改效率较高
    缺点
           • 存储密度小
           • 存取效率不高,必须采用顺序存取,即存取数据元素时,只能按链表的顺序进行访问

二、单链表的表示

    单链表是由表头唯一确定,因此单链表可以用头指针的名字来命名
       若头指针名是L,则把链表称为表L

存储结构定义
   typedef struct LNode{                 //定义单链表结点类型
        ElemType data;                    //数据域
        struct LNode *next;             //指针域
}LNode,*LinkList;                       // *LinkList为Lnode类型的指针

注意区分指针变量和结点变量两个不同的概念
          指针变量p:表示结点地址
          结点变量*p:表示一个结点
LinkList与LNode* ,两者本质上是等价的。
     通常用LinkList定义单链表,强调的是某个单链表的头指针;
     用LNode*定义指向单链表中任意结点的指针变量。

    若定义LinkList L,则L为单链表的头指针,即表示单链表L。
    若定义LNode *p, 则p为指向单链表中某个结点的指针,用 *p代表该结点。

三、单链表的基本操作

   初始化(构造一个空表 )
     (1)生成新结点作头结点,用头指针L指向头结点。
     (2)头结点的指针域置空。
           Status InitList_L(LinkList &L)
           {
                   L=new LNode; // L = (LNode * ) malloc (sizeof(LNode));
                   if (L == NULL) //内存不足分配失败
                         return false;
                   L->next=NULL;
                   return OK;
           }

销毁
      从头指针开始,依次释放所有结点
           Status DestroyList_L(LinkList &L)
           {
                       LinkList p;
                       while(L)
                       {
                               p=L;
                               L=L->next;
                               delete p;
                       }
                       return OK;
            }

清空与销毁的区别

      清空:链表仍存在,但链表中无元素,成为空链表(头指针和头结点仍然在)。

      销毁:依次释放所有结点,并将头结点指针域设置为空
          Status ClearList(LinkList & L)
          {
                     // 将L重置为空表
                     LinkList p,q;
                     p=L->next; //p指向第一个结点
                     while(p){
                        q=p->next;
                        delete p;
                        p=q;
                    }
                    L->next=NULL; //头结点指针域为空
                    return OK;
           }

求表长

           int ListLength_L(LinkList L){
                 //返回L中数据元素个数
                 LinkList p;
                 p=L->next;                    //p指向第一个结点  L本身为头节点指针,L->next 指向首元点。
                 i=0;
                 while(p){                      //遍历单链表,统计结点数
                 i++;
                 p=p->next; }
                 return i;
          }

判断表是否为空
            int ListEmpty(LinkList L){
               //若L为空表,则返回1,否则返回0
               if(L->next) //非空
                    return 0;
               else
                   return 1;
            }

获取元素

    根据位置i获取相应位置数据元素的内容

    链表的查找:要从链表的头指针出发,顺着链域next逐个结点往下搜索,直至搜索到第i个结点为止。因此,链表不是随机存取结构

分析:

   1、从第1个结点(L->next)扫描,用指针p指向当前扫描到的结点,p初值p = L->next。
   2、j做计数器,累计当前扫描过的结点数,j初值为1。
   3、当p指向扫描到的下一结点时,计数器j加1。
   4、当j = i时,p所指的结点就是要找的第i个结点。

//获取线性表L中的某个位置数据元素的内容
    Status GetElem_L(LinkList L,int i,ElemType &e){
             p=L->next;
             j=1;                         //初始化
            while(p&&j                p=p->next;
               ++j;
            }
           if(!p || j>i)
              return ERROR;      //第i个元素不存在
              e=p->data;            //取第i个元素
           return OK;
      }//GetElem_L

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

查找元素

   1、从第一个结点起,依次和e相比较。
   2、如果找到一个其值与e相等的数据元素,则返回其在链表中的“位置”或地址;
   3、如果查遍整个链表都没有找到其值和e相等的元素,则返回0或“NULL”


//在线性表L中查找值为e的数据元素 1、查找数据返地址 2、查找数据返序号
     LNode *LocateELem_L (LinkList L,Elemtype e) {
            //返回L中值为e的数据元素的地址,查找失败返回NULL
            p=L->next;
            while(p &&p->data!=e)
                 p=p->next;
             return p;
       }
       int LocateELem_L (LinkList L,Elemtype e) {
              //返回L中值为e的数据元素的位置序号,查找失败返回0
                    p=L->next; j=1;
                    while(p &&p->data!=e)
                    {p=p->next; j++;}
                    if(p) return j;
                    else return 0;
       }

插入

将值为x的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间
(1)找到ai-1存储位置p
(2)生成一个新结点*s
(3)将新结点*s的数据域置为x
(4)新结点*s的指针域指向结点ai
(5)令结点*p的指针域指向新结点*s

//在L中第i个元素之前插入数据元素e
         Status ListInsert_L(LinkList &L,int i,ElemType e){
                p=L;j=0;
                while(p&&jnext;++j;}                      //寻找第i−1个结点
                if(!p||j>i−1)return ERROR;                               //i大于表长 + 1或者小于1
                s=new LNode;                                                //生成新结点s
                s->data=e;                                                      //将结点s的数据域置e
                s->next=p->next;                                             //将结点s插入L中
                p->next=s;
                return OK;
          }//ListInsert_L

删除
    将表的第i个结点删去
 步骤:
(1)找到ai-1存储位置p
(2)临时保存结点ai的地址在r中,以备释放
(3)令p->next指向ai的直接后继结点
(4)将ai的值保留在e中
(5)释放ai的空间

   //将线性表L中第i个数据元素删除
  Status ListDelete_L(LinkList &L,int i,ElemType &e){
              p=L;j=0;
              while(p->next &&j                     p=p->next; ++j;
              }
              if(!(p->next)||j>i-1) return ERROR;               //删除位置不合理
              r=p->next;                                                    //临时保存被删结点的地址以备释放
              p->next=r->next;                                         //改变删除结点前驱结点的指针域
              e=r->data;                                                  //保存删除结点的数据域
              delete r;                                                     //释放删除结点的空间
              return OK;
   }//ListDelete_L

链表运算时间效率分析
    查找: 因线性链表只能顺序存取,即在查找时要从头指针找起,查找的时间复杂度为 O(n)。
    插入和删除: 因线性链表不需要移动元素,只要修改指针,一般情况下时间复杂度为 O(1)。
如果要在单链表中进行前插或删除操作,由于要从头查找前驱结点,所耗时间复杂度为 O(n)。

单链表的建立

   头插法
        从一个空表开始,重复读入数据:
           1、生成新结点
           2、将读入数据存放到新结点的数据域中
           3、将该新结点插入到链表的前端

void CreateList_F(LinkList &L,int n){
          L=new LNode;
          L->next=NULL;                                             //先建立一个带头结点的单链表
          for(i=n;i>0;--i){
                p=new LNode;                                    //生成新结点p=(LNode*)malloc(sizeof(LNode));
                cin>>p->data;                                   //输入元素值 scanf(&p-> data);
                p->next=L->next;L->next=p;           //插入到表头
         }
 }//CreateList_F

采用头插法建立单链表时,读入数据的顺序与生成的链表中的元素的顺序是相反的。每个结点插入的时间为O(1),设单链表长为n,则总时间复杂度为O(n)。

尾插法
     从一个空表L开始,将新结点逐个插入到链表的尾部,尾指针r指向链表的尾结点。
     初始时,r同L均指向头结点。每读入一个数据元素则申请一个新结点,将新结点插入到尾结点后,r指向新结点。

void CreateList_L(LinkList &L,int n){
         //正位序输入n个元素的值,建立带表头结点的单链表L
         L=new LNode;
         L->next=NULL;
         r=L;                                                              //尾指针r指向头结点
         for(i=0;i                p=new LNode;                                    //生成新结点
               cin>>p->data;                                  //输入元素值
               p->next=NULL; r->next=p;              //插入到表尾
               r=p;                                                 //r指向新的尾结点
         }
 }//CreateList_L

采用尾插法,生成的链表中结点的次序和输入数据的顺序不一致。

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

四、循环链表  

         循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。

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

  循环单链表的插入、删除算法与单链表的几乎一样,所不同的是若操作是在表尾进行,则执行的操作不同,以让单链表继续保持循环的性质。当然,正是因为循环单链表是一个“环”,因此在任何一个位置上插入和删除操作都是等价的,无须判断是否是表尾。

循环链表的合并
       LinkList Connect(LinkList &Ta,LinkList &Tb)
       {//假设Ta、Tb都是非空的单循环链表
           p=Ta->next;                                            //p存表头结点
          Ta->next=Tb->next->next;                     //Tb表头连接Ta表尾
          delete Tb->next;                                   //释放Tb表头结点
          Tb->next=p;                                        //修改指针

          return Tb;

       } 

五、双向链表

       双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。

       typedef struct DuLNode{                      //定义双链表结点类型
             ElemType data;                             //数据域
             struct DuLNode *prior;                 //前驱指针
             struct DuLNode *next;                //后继指针
       }DuLNode, *DuLinkList

 空双向循环链表:L->next=L

 双向循环链表:p->next->prior = p->prior->next = p

带头双向循环链表
   带头:存在一个哨兵位的头节点,该节点是个无效节点,不存储任何有效信息,但使用它可
以方便我们头尾插和头尾删时不用判断头节点指向NULL的情况。
   双向:每个结构体有两个指针,分别指向前一个结构体和后一个结构体。
   循环:最后一个结构体的指针不再指向NULL,而是指向第一个结构体(单向)第一个结构体的前指针指向最后一个结构体,最后一个结构体的后指针指向第一个结构体(双向)

双向链表的插入

  核心代码:

        1. s->prior=p->prior;
        2. p->prior->next=s;
        3. s->next=p;
        4. p->prior=s;

双向链表的插入:
    Status ListInsert_DuL(DuLinkList &L,int i,ElemType e){
         if(!(p=GetElemP_DuL(L,i))) return ERROR;
         s=new DuLNode;
         s->data=e;
         s->prior=p->prior;
         p->prior->next=s;
         s->next=p;
         p->prior=s;
         return OK;
  }

双向链表的删除

  核心代码:

      1. p->prior->next=p->next;
      2. p->next->prior=p->prior;

Status ListDelete_DuL(DuLinkList &L,int i,ElemType &e){
    if(!(p=GetElemP_DuL(L,i))) return ERROR;
    e=p->data;
    p->prior->next=p->next;
    p->next->prior=p->prior;
    delete p;
    return OK;
}

六、静态链表

   静态链表,也是线性存储结构的一种,它兼顾了顺序表和链表的优点于一身,既能快速访问元素,又能快速增加和删除元素。可以看做是顺序表和链表的升级版。
  使用静态链表存储数据,数据全部存储在数组中(和顺序表一样),但存储位置是随机的,数据之间“一对一”的逻辑关系通过一个整形变量维持(称为“游标”,和指针功能类似),和链表类似。

   静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,这里的指针是结点的相对地址(数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。

   通常,静态链表会将第一个数据元素放到数组下标为1的位置(a[1])中。

静态链表的定义
      数据域:用于存储数据元素的值;
      游标:其实就是数组下标,表示直接后继元素所在数组中的位置;
 因此,静态链表中节点的构成用 C 语言实现为:
      typedef struct {
           int data;              //数据域
           int cur;               //游标
     }component;

静态链表以next==-1作为其结束的标志。静态链表的插入、删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。

你可能感兴趣的:(数据结构)