线性表的链式表示和实现(数据结构)

线性表的链式表示和实现

2.5.1 单链表的定义和表示
线性表的链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素与其直接后继数据元素,ai+1之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示直接后继的信息(即直接后继的存储位置)。这两部分信息组成的数据ai的存储映像,称为结点,它包含两个域:其中存储数据元素信息的域称为数据域,存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点链结成一个链表,即称为线性表的链式存储结构,又由于此链表的每个结点中只包含一个指针域,故又称为线性链表或单链表。
根据链表结点所含的指针数,指针指向和指针链结方式,可将链表分为单链表、循环链表、双向链表,二叉链表,十字链表,邻接表,邻接多重表。其中单链表、循环链表和双向链表用于实现线性表的链式存储结构,其它形式多用于实现树和图等非线性结构。
本节先讨论单链表,即线性表的单链表存储结构,整个链表必须从头指针开始执行,头指针指向链表的第一个结点(即第一个数据元素的存储映像,也称首元结点)的存储位置。同时,由于最后一个数据元素没有直接后继,则单链表最后一个结点的指针为空。
用单链表来表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的,换句话来说,指针为数据元素之间逻辑关系的映象,则逻辑上相邻的数据元素其物理位置不一定紧邻,由此这种存储结构为非顺序映像或链式映象。
在使用链表时,关心的只是逻辑上的相邻的关系,而不是物理上的位置相邻的关系。
单链表的指针唯一确定,在C语言中可用“结构指针来描述”:

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

存储后继结点位置的指针域next,其类型为指向结点的指针类型LNode *。
为了程序的可读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode *,两者本质上是等价的,通常用LinkList定义单链表,强调定义的是某个单链表的头指针,用LNode *定义指向单链表中任一结点的指针变量。例如,若定义LinkList L,则L为单链表的头指针,若定义LNode p,则p为指向单链表中某个结点的指针,用p代表该结点。当然也可以使用定义LinkList p,这种形式完全等价于,LNode *p。
单链表是由表头指针唯一确定的,因此单链表可以用头指针的名字来命名。若头指针为L,则称该链表为表L。
注意区分指针变量和结点变量两个不同的概念,若定义LinkList p或LNode p,则p为指向某结点的指针变量,表示该结点的地址,而p为对应的结点变量。
头结点是在首元结点之前附设的一个结点,其指针域指向首元结点,头结点的指针域可以不存储任何信息,也可以存储与数据元素类型相同的其他附加信息,例如,当数据元素为整数型时,头结点的指针域可以存放该线性表的长度。
头指针是指向链表中第一个结点的指针,若链表设有头结点,则头指针所指结点为线性表的头结点,若链表没有头结点,则头指针所指结点为该线性表的首元结点。
链表增加头结点的作用如下:
(1)、便于首元结点的处理
增加了首元结点后,首元结点的地址保存在头结点的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需特殊处理。
(2)、便于空表和非空表的统一处理
当链表不设头结点时,假设L为单链表的头指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判定空表的条件可记为:L=NULL)。增加头结点之后,无论链表是否为空,头指针都是指向头结点的非空指针,判断空表的条件可记为:L->next==NULL,在顺序表中,由于逻辑上相邻的两个元素在物理位置上紧邻,则每个元素的存储位置都可从线性表的起始位置计算得到。而在单链表中,各个元素的存储位置都是随意的。然而,每个元素的存储位置都包含在其直接前驱结点的信息之中,而且,单链表是非随机存取的存储结构,要取得第i个数据元素必须从头指针出发顺链进行查找,也称顺序存取的存取结构,因此,其基本操作的实现不同于顺序表。
2.5.2 单链表基本操作的实现
1.初始化:单链表的初始化操作就是构造一个空表。
单链表的初始化:
1.生成新结点作为头结点,用头指针L指向头结点
2.头结点的指针域置空。
算法描述:

Status InitList(LinkLIst &L)
{
     L=new LNode; //生成新结点作为头结点
     L->next=NULL; //头结点的指针域置空
     return OK;
}

2.取值
和顺序表不同,链表中相邻的点并没有存储在物理相邻的单元中,这样根据给定的结点位置序号i,在链表中获取该结点的值不能像顺序表那样随机访问,而只能从链表的首元结点出发,顺着链域next逐个结点向下访问。
算法 2. 单链表的取值
算法步骤:
1.用指针指向首元结点,用j做计数器初值赋为1.
2.从首元结点开始依次顺着链域next向下访问,只要指向当前结点的指针p不为空,并且没有到达序号为i的结点,则循环执行以下操作:
(1)、p指向下一个结点
(2)、计数器j应相应加1。
(3)、退出循环时,如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法(i大于表长n或i小于等于0)取值失败返回ERROR,否则取值成功,此时j=i时,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。
算法描述

Status GetElem(LinkList L,int i,ElemType &e)
{
      p=L->next;
      j=1
      while(p&&j<i)
      {
      p=p->next;
      ++j;
      }
      if(!p||j>i)
      return ERROR;
      e=p->data;
      return OK;
}

【算法分析】
该算法的基本操作是比较 j 和 i 并后移指针p,while循环体的语句频度与位置 i 有关。若1<=i<=n,则频度为 i -1 ,一定能取值成功。若 i > n ,则频度为 n ,取值失败。因此,算法的最坏时间复杂度为O(n)。
3 . 查找
链表中按值查找的过程和顺序表相似,从链表的首元结点出发,依次将结点值和给定值e进行比较,返回查找结果。
算法 2.8 单链表的按值查找
【算法步骤】
1、用指针p指向首元结点。
2、从首元结点开始依次顺着链域next向下查找,只要指向当前结点的指针p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作:p指向下一个结点。
3、返回p。若查找成功,p此时为结点的地址值,若查找失败,p的值即为NULL。
【算法描述】

LNode *LocateElem(LinkList L,ElemType e)
{
     p=L->next;    //初始化,p指向首元结点
     while(p && p->next!=e)  //直到p为空或p所指结点的数据域为e
     p=p->next;     //p指向下一个结点
     return p;     //查找成功返回值为e的结点地址,查找失败p为NULL
}

算法 2.9 单链表的插入
将值为e的新结点插入到表的第i个结点的位置上,即插入到结点ai-1与ai之间,具体插入过程如图2.12所示,图中对应的5个步骤说明如下:
1、查找结点ai-1并由指针p指向该结点。
2、生成一个新结点s
3、将新结点
s的数据域置为e
4、将新结点s的指针域指向结点ai
5、将结点
p的指针域指向新结点*s
【算法描述】

Status ListInsert(LinkList &L,int i,ElemType e)
{//在带头结点的单链表L中第i个位置插入值为e的新结点
    p=L;
    j=0;
    while(p&&j<i-1)
    {
    p=p->next;
    ++j;
    }//查找第i个结点,p指向该结点。
    if(!p||j>i-1)
    return ERROR;
    s=new LNode;   //生成新结点s
    s->data=e;       //将结点*s的数据域置为e
    s->next=p->next;  //将结点*s的指针域指向ai
    p->next=s;    //将结点*p的指针域指向结点 *s
    return OK;
}  

和顺序表一样,如果表中有n个结点,则插入操作中合法的插入位置有n+1个,即1<=i<=n+1。当i=n+1时,新结点则插在链表尾部。
【算法分析】
单链表的插入虽然不需要移动元素,但其平均复杂度仍为O(n)。这是因为,为了在第i个结点之前插入一个新的结点,必须首先找到第i个结点,其时间复杂度为O(n)。
算法 2.10 单链表的删除
【算法步骤】
删除单链表的第i个结点ai的具体过程:
1、查找结点ai-1并使指针p指向该结点
2、临时保存待删除结点ai的地址在q中,以备释放
3、将结点*p的指针域指向该结点
4、释放结点ai的空间
【算法描述】

Status ListDelete(LinkList &L,int i)
{  //在带头结点的单链表L中,删除第i个元素
    p=L;
    j=0;
    while((p->next)&&(j<i-1))  //查找第i-1个结点,p指向该结点
    {
    p=p->next;
    ++j;
    }
    if(!(p->next)||(j>i-1))
    return ERROR;   //当i>n或i<1时,删除位置不合理
    q=p->next;  //临时保存被删结点的地址以备释放
    p->next=q->next;  //改变删除结点前驱结点的指针域
    delete q;  //释放删除结点的空间
    return OK;
}

删除算法中的循环条件(p->next&&j<i-1)和插入算法中的循环条件(p&&(j<i-1))是有所区别的。因为插入操作的中合法的插入位置有n+1个,而删除操作中的合法的删除位置只有n个,如果使用与插入操作相同的循环条件,则会出现引用空指针的情况,使删除操作失败。
【算法分析】
类似于插入算法,删除算法的时间复杂度亦为O(n)。
6.创建单链表
如何创建一个包括若干个结点的单链表?
链表和顺序表不同,它是一种动态结构。整个可用存储空间可以为多个单链表共同使用,每个链表占用的空间不需要预先分配划定,而是由系统按需及时产生,因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。因此建立线性表的链式存储结构,的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。
根据结点插入位置的不同,链表的创建方法可分为前插法和后插法。
(1)、前插法
前插法就是通过将新结点逐个插如链表的头部(头结点之后),=来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。
算法 2.11 前插法创建单链表
【算法步骤】
(1)、创建只有一个头结点的空链表
(2)、根据待创建链表包括的元素个数n,循环n次执行以下操作。
生成一个新结点 p
输入元素值赋给新结点
p的数据域
将新结点*p插入到新结点之后
【算法描述】

void CreateList_H(LinkList &L,int n)
{  //逆位序输入n个元素的值,建立带表头结点的单链表L
   L=new LNode;
   L->next=NULL; //先建立一个带头结点的空链表
   for(i=0;i<n;++i)
   {
   p=new LNode;     //生成新结点*p
   cin>>p->data;    //输入元素值赋给新结点*p的数据域
   p->next=L->next;
   L->next=p;  //将新结点*p插入到头结点之后
}

显然,算法2.11的时间复杂度为O(n)。
(2)、后插法
后插法是通过将新结点逐个插入到链表的尾部来创建链表。同前插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针r指向链表的尾结点。
算法2.12 后插法创建单链表
【算法步骤】
1、创建一个只有头结点的单链表。
2、尾指针r初始化,指向头结点。
3、根据创建链表包括的元素个数n,循环n次执行以下操作:

  1. 生成一个新结点*p
  2. 输入元素值赋给新结点*p的数据域
  3. 将新结点p插入到尾结点r之后
  4. 尾指针r指向新的尾结点*p
    如图所示为线性表(a,b,c,d,e)后插法的创建过程,读入数据的顺序和线性表中的逻辑顺序是相同的。
    【算法描述】
void Create_R(LinkList &L,int n)
{//正位序输入n个元素的值,建立带表头结点的单链表L
   L=new LNode;
   L->next=NULL;   //先建立一个带空结点的单链表L
   r=L;    //尾指针r指向头结点
   for(i=0;i<n;++i)
   {
   p=new LNode;  //生成新结点
   cin>>p->data  //输入元素值赋给新结点*p的数据域
   p->next=NULL;
   r->next=p;  //将新结点*p插入到尾结点r之后
   r=p;  //r指向尾结点*p
   }
}  

此算法的时间复杂度亦为O(n)
2.5.3 循环链表
循环链表是另一种形式的链式存储结构。其特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环,由此,从表中任一结点出发均可找到任意其他结点,有单链表的循环链表,还有多重链的循环链表。
循环单链表的操作和单链表基本一致,差别仅在于:当遍历链表时,判断当前指针p是否指向表尾结点的终止条件不同。在单链表中,判别条件为p!=NULL或p->next!=NULL,而循环单链表的判别条件为p!=L,p->next!=L.
在某些情况下,若在循环链表中设立尾指针而不设头指针,可使一些操作简化。例如,将两个线性表合并为一个表的时候,仅需将第一个表的尾指针指向第二个表的第一个结点,第二个表的尾指针指向第一个表的头结点,然后释放第二个表的头结点,当只有一个简单的循环链表时,这个操作仅需修改两个指针值即可,主要语句段如下:

p=B->next->next;
B->next=A->next;
A->next=p;

2.5.4 双向链表
以上讨论的链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺指针向后寻找其他点。若要寻查结点的直接前驱,则必须从表头指针出发。换句话说,在单链表中,查找直接后继结点的执行时间为O(1),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表。
顾名思义,在双向链表的结点中有两个指针域,一个指向直接后继,一个指向直接前驱。
//----------双向链表的存储结构---------

typedef struct DulNode
{
      ElemType data;  //数据域
      struct DulNode *prior;  //指向直接前驱
      struct DulNode *next;   //指向直接后继
}DulNode,*DuLinkList;

和单链的循环表类似,双向链表也可以有循环链表,在双向链表中,若d为指向表中某一结点的指针(即d为DuLinkList型变量),则显然有:d->next->prior=d->prior->next=d 这个表示方式恰当地反映了这种结构的特性。
在双向链表中,有些操作(如ListLength、GetElem和LocateElem等)仅需涉及一个方向上的指针,则它们的算法描述和线性链表的操作相同,但在插入、删除时有很大的不同,在双向链表中需同时修改两个方向上的指针。在插入结点时需要修改四个方向上的指针,在删除结点时需要修改两个方向上的指针,两者的时间复杂度均为O(n)。
算法2.13 双向链表的插入
【算法描述】

Status ListInsert_Dul(DuLinkList &L,int i,ElemType e)
{//在带头结点的双向链表L中第i个位置之前插入元素e
   if(!(p=GetElem_Dul(L,i)))   //在L中确定第i个元素的指针p
   return ERROR;               //p为NULL时,第i个元素不存在
   s=new DuLNode;              //生成新结点*s
   s->data=e;                  //将结点*s的数据域置为e
   s->prior=p->prior;          //将结点*s插入到L中
   p->prior->next=s;
   s->next=p;
   p->prior=s;
   return OK;
}    

算法2.14 双向链表的删除
【算法描述】

Status ListDelete_DuL(DuLinkList &L,int i)
{  //删除带头结点的双向链表L中的第i个元素
     if(!(p=GetElem_DuL(L,i))       //在L中确定第i个元素的位置指针p
     return ERROR;
     p->prior->next=p->next;  //修改被删结点的前驱结点的后继指针
     p-->next->prior=p->prior;  //修改被删结点的后继结点的前驱指针
     delete p;     //释放被删结点空间
     return OK;
}
  • 0
    点赞
  • 11
    收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:深蓝海洋 设计师:CSDN官方博客 返回首页
评论

打赏作者

恰好路过人间

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值