线性表的链式表示和实现

线性表链式存储结构的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。

单链表的定义和表示

  • 为了表示每个数据元素ai与其直接后继数据元素a(i+1)之间的逻辑关系,对数据元素ai来说,除了存储其本身的信息之外,还需存储一个指示其直接后继的信息(即直接后继的存储位置)。这两部分信息组成数据元素ai的存储映像,称为结点(node)。它包括两个域:其中存储数据元素信息的域为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点(ai(1<=i<=n)的存储映像)链结成一个链表,即为线性表的链式存储结构。又由于此链表的每个结点中只包含一个指针域,故又称线性链表或单链表
  • 根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。其中单链表、循环链表和双向链表用于实现线性表的链式存储结构,其他形式多用于实现树和图等非线性结构。
  • 线性表的单链表存储结构,整个链表的存取必须从头指针开始进行,头指针指示链表中第一个结点(即第一个数据元素的存储映像,也称首元结点)的存储位置。同时,由于最后一个数据元素没有直接后继,则单链表中最后一个结点的指针为空(NULL)。

  • 用单链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。这种存储结构为非顺序映像或链式映像
  • 通常将链表画成用箭头相链接的结点的序列,结点之间的箭头表示链域中的指针。在使用链表时,关心的只是它所表示的线性表中数据元素之间的逻辑顺序,而不是每个数据元素在存储器中的实际位置。

单链表头可由头指针唯一确定,在C语言中可用“结构指针”来描述:

//- - - - -单链表的存储结构- - - - - 
typedef struct LNode
{
  ElemType  data;                  //结点的数据域
  struct LNode  *next;             //结点的指针域
}LNode, *LinkList;                 //LinkList为指向结构体LNode的指针类型
  1. 这里定义的是单链表中每个结点的存储结构,它包括两部分:存储结点的数据域data,其类型用通用类型标识符ElemType表示;存储后继结点位置的指针域next,其类型为指向结点的指针类型LNode*。
  2. 为了提高程序的可读性,在此对同一结构体指针类型起了两个名称,LinkList与LNode*,两者本质上是等价的。
  3. 单链表是由表头指针唯一确定,因此单链表可以用头指针的名字来命名。若头指针名是L,则简称该链表为表L。
  4. 注意区分指针变量和结点变量两个不同的概念,若定义LinkList p或LNode *p,则p为指向某结点的指针变量,表示该结点的地址;而*p为对应的结点变量,表示该结点的名称。
  • 为了处理方便,在单链表的第一个结点之前附设一个结点,称之为头结点

首元结点、头结点、头指针三个容易混淆的概念加以说明:

  1. 首元结点是指链表中存储第一个数据元素ai的结点。
  2. 头结点是在首元结点之前附设的一个结点,其指针域指向首元结点。头结点的数据域可以不存储任何信息,也可存储与数据元素类型相同的其他附加信息。
  3. 头指针是指向链表中第一个结点的指针。若链表设有头结点,则头指针所指结点为线性表的头结点;若链表不设头结点,则头指针所指结点为该线性表的首元结点。

链表增加头结点的作用如下:

1.便于首元结点的处理。

增加了头结点后,首元结点的地址保存在头结点(及其“前驱”结点)的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需进行特殊处理。

2.便于空表和非空表的统一处理。

当链表不设头结点时,假设L为单链表的头指针,它应该指向首元结点,则当单链表为长度n为0的空表时,L指针为空(判定空表的条件可记为:L==NULL)。

增加头结点后,无论链表是否为空,头指针都是指向头结点的非空指针。若为空表,则头结点的指针域为空(判定空表的条件可记为:L->next==NULL),

        在顺序表中,由于逻辑上相邻的两个元素在物理位置上相邻,则每个元素的存储位置都可从线性表的起始位置计算得到。而在单链表中,各个元素的存储位置都是随意的。然而,每个元素的存储位置都包含在其直接前驱结点的信息之中。单链表是非随机存取的存储结构,要取得第i个数据元素必须从头指针出发顺链进行寻找,也称为顺序存取的存取结构。

单链表基本操作的实现

1.初始化

单链表的初始化操作就是构造一个空表。

Status InitList(LinkList &L)
{//构造一个空的单链表L
   L=new LNode;                       //生成新结点作为头结点,用头指针L指向头结点
   L->next==NULL;                     //头结点的指针域置空
   return OK;
}

2.取值

和顺序表不同,链表中逻辑相邻的结点并没有存储在物理相邻的单元中,这样,根据给定的结点位置序号i,在链表中获取该结点的值不能像顺序表那样随机访问,而只能从链表的首元结点出发,顺着链域next逐个结点向下访问。

Status GetElem(LinkList L,int i,ElemType &e)
{//在带头结点的单链表L中根据序号i获取元素的值,用e返回L中第i个数据元素的值
   p=L->next;j=i;                 //初始化,p指向首元结点,计算器j初值赋为1
   while(p&&j<i)                  //顺链域向后扫描,直到p为空或p指向第i个元素
   {
      p=p->next;                  //p指向下一个结点
      ++j;                        //计算器j相应加1
   }
   if(!p||j>i)return ERROR;       //i值不合法i>n或i<=0
   e=p->data;                     //取第i个结点的数据域
   return OK;
}

单链表取值算法的平均时间复杂度为O(n)。

3.查找

链表中按值查找的过程和顺序表类似,从链表的首元结点出发,依次将结点值和给定值e进行比较,返回查找结果。

LNode *LocateElem(LinkList L, ElemType e)
{//在带头结点的单链表L中查找值为e的元素
   p=L->next;                      //初始化,p指向首元结点
   while(p && p->date!=e)          //顺链域向后扫描,直到p为空或p所指结点的数据域等于e
      p=p->next;                   //p指向下一个结点
   return p;                       //查找成功返回值为e的结点地址p,查找失败p为NULL
}

该算法的执行时间与待查找的值e相关,其平均时间复杂度为O(n)。

4.插入

假设要在单链表的两个数据元素a和b之间插入一个数据元素想,已知p为其单链表存储结构中指向结点a的指针。

s->next = p->next;  p->next = 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-1个结点,p指向该结点
   if(!p||j>i-1) return ERROR;   //i>n+1或者i<1
   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-1个结点。

5.删除

要删除单链表中指定位置的元素,同插入元素一样,首先应该找到该位置的前驱结点。在单链表中删除元素b时,应该首先找到前驱结点a。为了在单链表中实现元素a、b和c之间逻辑关系的变化,仅需修改结点a中的指针域即可。假设p为指向结点a的指针,则修改指针的语句为   p->next = p->next->next;  但在删除结点b时,除了修改结点a的指针域外,还要释放结点b所占的空间,所以在修改指针前,应该引入另一指针q,临时保存结点b的地址以备释放。

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-i))  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)前插法

        前插法是通过将新结点逐个插入链表的头部(头结点之后)来创建链表,每次申请一个新结点,读入相应的数据元素值,然后将新结点插入到头结点之后。

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)后插法

        后插法是通过将新结点逐个插入到链表的尾部来创建链表。同前插法一样,每次申请一个新结点,读入相应的数据元素值。不同的是,为了使新结点能够插入到表尾,需要增加一个尾指针r指向链表的尾结点。

void CreateList_R(LinkList &L,int n)
{//正位序输入n个元素的值,建立带表头结点的单链表L
   L=new LNode;
   L->next=NULL;                  //先建立一个带头结点的空链表
   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)。

  • 1
    点赞
  • 0
    收藏
  • 打赏
    打赏
  • 0
    评论

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

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页
评论

打赏作者

theday98674

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

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

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

打赏作者

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

抵扣说明:

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

余额充值