线性表之链表(包括单链表、双链表和循环链表)

线性表链式存储结构的特点是: 用一组仁义的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
为了表示数据元素ai与其直接后继数据元素a i+1之间的逻辑关系,对于数据元素ai来说,除了存储其本身的信息之外,话需要存储一个治时期直接后继的信息(即直接后继的存储位置)。这连个部分信息组成数据元素ai的存储映像,称为结点。它包括两个域:其中存储数据元素信息的域称为数据域; 存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。n个结点链结成一个链表,即为线性表的链式存储结构
根据链表结点所含指针个数、指针指向和指针连接方式,可将链表分为单链表、循环链表、双向链表、二叉链表、十字链表、邻接表、邻接多重表等。 其中单链表,循环链表和双向链表用于实现线性表的链式存储结构。

单链表

下图为线性表的单链表存储结构
图1

扩展:

  • 头指针是以确定线性表中第一个元素对应的存储位置,一般用于处理数组、链表、队列等数据结构。单链表可以用头指针的名字来命名。单链表中头指针指向第一个结点。
  • 链式存储时只要不是循环链表,就一定存在头指针。

整个链表的存取必须从头指针开始进行,头指针指示链表中的第一个结点的存储位置。同时,由于最后一个数据元素没有直接后继,则单链表中最后一个结点的指针也为空(NULL).
用单链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的,则逻辑上相邻的两个数据元素其存储的物理位置不要求紧邻。由此,这种存储结构为非顺序映像或链式映像。
因为在使用链表时,关心的是它所表示的线性表中数据元素之间的逻辑顺序,并非每个元素在存储器中的实际位置。因此,通常将链表画成用箭头相链接的结点的的序列。 第一张图可以画成下面这种形式
在这里插入图片描述
由此可见,单链表可由头指针唯一确定,在C语言中可用“结构指针”来描述:

typedef struct
{
   ElemType data;        //结点的数据域
   struct LNode *next;   //结点的指针域
 }LNode,*LinKList;     //LinKList为指向结构体LNode的指针类型

这里定义的是单链表中每个结点的存储结构,包括两部分:

  1. 存储结点的数据域date:其类型用通用类型标识符ElemType表示。
  2. 存储后继结点位置的指针域next:其类型为指向结点的指针类型LNode*

通常,用LinkList定义单链表,强调定义的是某个单链表的头指针;用==LNode*==定义指向单链表中任意结点的指针变量。
区分指针变量和结点变量:若定义LinkList p或LNodep,则p为指向某结点的指针变量,表示该结点的地址;而p为对应的结点变量,表示该结点的名称。

一般,为了方便,在单链表的第一结点之前附设一个结点,称之为头节点。第二张图增加头节点后如下图
在这里插入图片描述

注:
首元结点:是链表中寻相互的第一个数据元素;
头节点:是在首元结点之前附设的的一个结点,其指针域指向首元结点。(头结点的数据域可以不存储任何信息,也可以存储与数据元素类型相同的其他附加信息)
头指针:是指向链表中的第一个结点的指针。

头结点的作用:

  1. 首元结点的地址保存在头结点的指针域中,则对链表的第一个数据元素的操作与其他数据元素相同,无需特殊处理。
  2. 便于空表和非空表的统一处理
    增加头结点后,无论链表是否为空,头指针都指向头结点的非空指针,头指针指向头结点。若为空表,则头结点的指针域为空(判定空表的条件为:L->next = = NULL)。

单链表

1.初始化

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

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

【算法步骤】

  • 生成新结点作为头结点。
  • 头结点的指针域置空。
    【算法描述】
Status InitList(LinkList)
{
   L=new LNode;    //生成新结点作为头结点,用头指针L指向头结点
   L->next = NULL; //头结点的指针域置空
   return OK;
 }

2.取值

从链表的首元结点出发,顺着链域next逐个结点找下去

【算法步骤】

  • 用指针p指向首元结点,用j做计数器初值付给l。
  • 从首元结点开始一次顺着链域next向下访问,只要指向当前结点的指针p不为空,并且没有到达序号为i的结点,则循环执行以下操作:
    p指向下一个结点;
    计数器j相应加i。
  • 退出循环时如果指针p为空,或者计数器j大于i,说明指定的序号i值不合法(i大于表长n或i小于等于0),取值失败返回ERROR;否则取值成功,此时j=i时,p所指的结点就是要找的第i个结点,用参数e保存当前结点的数据域,返回OK。
    【算法描述】
Statu GetElem(LinkList L,int i,Elemtype &e)
{
   p=L->next:j=i;         //初始化,p只指向首元结点,计数器j初赋值为1
   while(P&&j<i)         //p指向下一个结点
   {
        p=p->next;
        ++j;                 //计数器j相应加1
   }
   if(p||j>i) return ERROR; //i值不合法i>n或i<=0
   e=p->date;                //取第i个结点数据域
   return OK;
 }

[算法分析】

     ASL=(n-1)/2

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

从链表的首元结点出发,依次将结点值和给定值e进行比较,返回查找结果。

【算法步骤】

  • 用指针p指向首元结点;
  • 从首元结点开始一次顺着链域next往下找,只要当前的指针域p不为空,并且p所指结点的数据域不等于给定值e,则循环执行以下操作:p指向下一个结点。
  • 返回p。如查找成功,p此时即为结点的地址值;如查找失败,p的值即为NULL。

【算法描述】

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

【算法分析】
平均时间复杂度也为O(n)。
4.插入

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

【算法步骤】
将值为e的新结点插到第i个结点的位置上,即插入到结点ai-1与ai之间。

  • 查找结点ai-1并由指针p指向该结点。
  • 生成一个新结点*S。
  • 将新结点*s的数据域置为e。
  • 将新结点*s的指针城指向结点ai。
  • 将结点p的指针域指向新结点s。
    【算法描述】
Status ListInsert (LinkList &L,int i,ElemType e)
{
   p=L;j=0;
   while(P && (j<i-1))
     {
        p=p->next; ++j; 
     }       //查找第i-1个结点,P指向该结点
  if(!p11j>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;
}

【算法分析】
单链表的插入操作的平均时间复杂度为O(n)。

5.删除

在单链表中删除元素b时,应该首先找到其前驱点a。要想删除元素b,除需要修改a的指针域外,还要释放结点b所占的空间。因此修改指针前,应该引入另一个指针q,临时保存结点b的地址已备释放。

【算法步骤】

  • 查找结点ai-1并由指针p指向该结点。
  • 临时保存待删除结点ai的地址在q中,以备释放。
  • 将结点*p的指针域指向ai的直接后继结点。
  • 释放结点ai的空间。
    【算法描述】
Status ListDelete (LinkList &L,int 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;
}

【算法分析】
删除算法的平均时间复杂度为O(n)。

6.创建单链表

建立线性表的链式存储结构的过程就是一个动态生成链表的过程。即从空表的初始状态起,依次建立各元素结点,并逐个插入链表。

前插法

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

【算法步骤】

  • 创建一个只有头结点的空链表。
  • 根指待创建链表包括的元素个数n,循环次执行以下操作:
    生成一个新结点p;
    输入元素值赋给新结点
    p中的数据域;
    将新结点*p中插入到头结点之后。
    注:因为每次插入在链表的头部,所以应该逆序输入数据,输入顺序和线性表中的逻辑顺序是相反的。
    【算法描述】
 void CreateList H(LinkList L,int n)
 {
      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 插人到头结点之后)
     }
 }

后插法

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

【算法步骤】

  • 创建一个只有头结点的空链表。
  • 尾指针r初始化,指向头结点。
  • 根据创建链表包括的元素个数n,循环n次执行以下操作:
    生成一个新结点p;
    输人元素值赋给新结点“的数据域;
    将新结点
    p 插人到尾结点r之后:
    尾指针r指向新的尾结点
    p
    注:读人数据的顺序和线性表中的逻制顺序是相同的。
    【算法描述】
void CreateList R(LinkList &L,int n)
{
  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
    }
 }

循环链表

**特点:**是表中最后一个结点的指针域指向头结点,整个链表形成一个环。因此,从表中的任一结点出发均可找到表中其他结点。
下图为单链的循环链表
在这里插入图片描述
循环单链表和单链表的区别:当链表遍历时,判别当时指针p是否指向表尾结点的终止条件不同。 在单链表中,判别条件为p!=NULL或p->next!=NULL;循环单链表的判别条件为p!=L或p->next!=L。
在某些情况下,若在循环链表中设立尾指针而不设头指针,可使一些操作简化。

双向链表

在单链表中,查找直接后继结点的执行时间为O(l),而查找直接前驱的执行时间为O(n)。为克服单链表这种单向性的缺点,可利用双向链表
在双向链表的结点中有两个指针域,一个指向直接后继,另一个指向直接前驱。
在C语言中可描述如下;

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

下面是双向链表的循环表
在这里插入图片描述
注:图(b)是只有一个表头结点的空表。

在双向链表中,在插入结点时需要修改四个指针,在删除结点时需要修改两个指针。二者的时间复杂度均为O(n)。
在双向链表中插入和删除结点时的指针变化状况如下图
在这里插入图片描述
【算法描述】
双向链表的插入


Status ListInsert DuL(DuLinkList &L,int 1,ElemType 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中,此步对应图中1
        p->prior->next=s;  //对应图中4
        s->next=p;  //对应图中2
        p->prior=s;  //对应图中3
        return  OK;
    }

双向链表的删除

Status ListDelete_ DuL (DuLinkList &L,int i)
{
   if(! (p=GetElem_DuL(L,i))  //在L 中确定第i个元素的位置指针p
       return ERROR;         //p为NULL时,第i个元素不存在
   p->prior->next=p->next;   //修改被删结点的前驱结点的后继指针,对应图中2
   p->next->prior=p->prior;  //修改被删结点的后继结点的前驱指针,对应图中3
   delete p;                // //释放被删结点的空间
   return OK;
}

发布了3 篇原创文章 · 获赞 3 · 访问量 72

猜你喜欢

转载自blog.csdn.net/qq_46020858/article/details/104046238