2.3线性表的链式表示和实现

来源:互联网 发布:手机主板测试软件 编辑:程序博客网 时间:2024/06/17 01:22

从上一节的讨论中我们线性表的顺序存储结构特点:逻辑关系上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中的任意一个元素,它的存储位置可用一个简单、直观的公式表达。

然而,从另一面,它的特点也造成了这种存储结构的弱点:在做插入和删除时候需要大量的移动元素。本节我们讨论线性表的另一种表示方法——链式存储结构,由于链式存储结构不要求逻辑上相邻的元素在物理位置上相邻,因此,它没有顺序存储结构的弱点,但是也没有顺序存储结构的优点:随机存取。

2.3.1 线性链表

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。因此,为了表示每个数据元素ai与其直接后继元素a(i+1)之间的逻辑关系,对数据元素ai来说除了要存储本身的信息外,还需要存储一个指向后继元素的信息(即直接后继的存储位置)。这两部分信息组成数据元素ai的存储映像,称为结点(node)。它包括两个域:其中存储本身信息的为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或链。

链表的每个结点中只包含一个指针域则称为线性链表或单链表。单链表可由头指针唯一确定,在C语言中可用“结构指针”来描述。

//---------------------------线性表的单链表存储结构----------------------------------------------------------------

typedef struct LNode{

          ElemType data;

          structLNode *next;

}Lnode,*LinkList;//定义结点的结构体



定义LinkList L;
则L为链表的头指针。
 L=(LinkList) malloc (sizeof(LNode)); //创建一个结点
此处返回给L的是一个指针,并且赋给了头指针
L->next=null; //这里说明我创建了一个头结点,即同时运用了头指针和头结点,不过感觉头结点用处不大,所以一般还是不用最好。

  假设L是LinkList型变量,则L为单链表的头指针,它指向表中的第一个结点(node[0])。若L为空(L=NULL),则所表示的链表为“空”表,其长度n为0。有时我们在单链表的第一个结点之前设一个结点,称为头结点。头结点的数据域可以不存储任何信息,也可以存储如线性表的长度等类的附加信息,头结点的指针域存储指向第一结点的指针(即第一个元素结点的存储位置)。

如下图所示,单链表的头指针指向头结点。若线性表为空表,则头结点的指针域为空。




由于有个童鞋问了个问题,所以在这里插入一些话: (头指针和头结点的疑问)

链表里有“头指针”变量,它存放一个地址,该地址指向一个元素。链表里的每个元素称为“节点”。
头节点是链表中第一个有效的节点,而不是用来存储第一个节点地址的头节点。
头指针在链表中必须有,而头结点不一定要有。
头指针作用:
就是存放数组地址,也即是链表地址。
头结点好处:
首先它是链表中的元素,是个有效的结点;
好处(1):对带头结点的链表,在链表中的任何位置插入或删除结点,要做的就是修改前一结点的指针域,因为任何结点都有前驱结点;如果链表中没有头结点,则首元素结点没有前驱,那么在其前插入或删除首元素结点时候,操作比较麻烦。
好处(2):对带头结点的链表,表头指针是指向头结点的非空指针,因为空表和非空表的处理是一样的。


现在,我们假设p是指向线性表中第i个元素的(结点ai,也是a(i-1)的指针域的内容)指针,则p->next是指向第i+1个元素(结点a(i+1))的指针。换句话说,若p->data = ai;那么p->next->data = a(i+1);。由此,在单链表中,取得第i个元素必须从头指针(记住是头指针,而不是头结点)出发寻找,因此单链表是非随机存取的存储结构。

下面看GetElem在单链表中的实现:

算法:

Status GetElem_L(LinkList L;int i;ElemType &e){  //L为带头结点的单链表的头指针;  p = L->next; j = 1;//初始化,p指向第一个结点,j为计数器;也即是技术i次,刚好找到第i个元素;  while(p&&j < i)//顺指针向后查找,直到p指向第i个元素(也即是j = i - 1的时候)或p为空;  {    p = p->next; ++j;  }  if(!p || j > i) return ERROR;  e = p->data;  return OK;}

算法2.8的基本操作是比较j和i并后移指针p,while循环体中的语句频度与被查元素在表中位置有关,若1<=i<=n,则频度为n,因此时间复杂度为O(n)。

在单链表中如何实现“插入”和“删除”操作呢?

插入:

假设我们要在线性表的两个数据元素a和b之间插入一个数据元素x,已知p是单链表存储结构中指向结点a的指针。如下图


为插入数据元素x,首先要生成一个数据域为x的结点,然后插入在单链表中。根据插入操作的逻辑单元还需要修改a中的指针域,令其指向结点x,而结点x的指针域应该指向结点b,从而实现3元素a、b和x之间逻辑关系的变化。

假设s为指向结点x的指针,则上述逻辑关系变化的程序描述为:s->next = p->next;p->next = s;

删除:

根据图删除b结点,仅需要修改a结点的指针域即可,假设如图p为指向结点a的指针,则修改逻辑关系的程序描述为:p->next = p->next->next;

算法2.8:在单链表中实现插入

Status ListInsert_L(LinkList &L;int i;ElemType e);{  p = L; j = 0;  while(p&&j < i-1)//寻找第(i-1)个结点;  {    p= p->next;    ++j;  }  if(!p || j > i)return ERROR;  s = (LinkList)malloc(sizeof(LNode));//生成新的结点;  s->data = e;
  s->next = p->next;  p->next = s;  return OK;}

算法2.9:实现单链表中的删除

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

算法2.10:是一个从表尾到表头逆向建立单链表的算法,其实际复杂度为O(n);

void CreatList_L(LinkList &L,int n){  L = (LinkList )malloc(LNode);//在创建链表时候,是不是先创建一个结点,再实现链表?  L->next = NULL;//先建立一个带头结点的单链表;  for(i = n; i >=1;- -i)  {    p = (LinkList)malloc(LNode);    scanf(&p->data);    p->next = L->next;    L->next = p;  }}

算法2.11:归并两个链表的算法

void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){  //已知单链线性表La和Lb的元素按非递减排列。  pa = La->next;  pb = Lb->next;  Lc = pc = La;//用La的头结点作为Lc的头结点;没有再新建结点。  while(pa&&pb)  {    if(pa->data <= pb->data)    {      pc->next = pa;
      pc = pa;      pa = pa->next;    }    else    {      pc->next = pb;      pc = pb;      pb = pb->next;    }  }  pc->next = pa?pa:pb;  free(Lb);//释放Lb结点;}

与算法2.7的比较?算法2.7里面讲的是线性表的顺序存储结构的合并;而算法2.11是将的线性表的链式存储结构的合并;存储结构的不同导致合并的算法不一样。

算法2.7:

void MergeList_Sq(Sqlist La,Sqlist Lb,Sqlist &Lc){  pa = La.elem;  pb = Lb.elem;  Lc.listsize = Lc.length = La.length + Lb.length;  pc = Lc.elem = (ElemType *)malloc(Lc.listsize*sizeof(ElemType));  if(!Lc.elem)exit(OVERFLOW);  pa_last = La.elem + La.length -1;  pb_last = Lb.elem + Lb.length -1;  while(pa <= pa_last && pb <= pb_last)  {   if(*pa <= *pb) * pc++ = * pa++;
   else *pc++ = * pb++;  }  while(pa <= pa_last)*pc++ = *pa++;  while(pb <= pb_last)*pc++ = *pb++;}

有时,也可以用一位数组来描述线性表 ,其类型说明如下:

#define MAXSIZE 1000 //链表的最大长度;typedef struct{    ElemType data;    intcur; }component,SLinkList[MAXSIZE];//SLinkList在这里是数组;

这种描述方法便于在不设“指针”的高级程序设计语言中使用链表结构。为了和指针型描述的线性表相区别,我们把这种数组描述的链表起名为静态链表

假设S是SLinkList型变量,则S[0].cur指示第一个结点在数组中的位置,若设i = S[0].cur,则S[i].data存储线性表的第一个数据元素,且S[i].cur指示第二个结点在数组中的位置。一般情况,若第 i 分量表示链表的第k个结点,则S[i].cur指示第 k+1 个结点位置。

因此,在静态链表中实现线性表的操作和动态链表相似,以整型游标 i 代替动态指针 p ,i = S[i].cur的操作实为指针后移(类似于p = p->next)。

算法2.13:在静态链表中实现的定位函数LocateElem

int  LocateElemType(SLinkList S;ElemType e){    //在静态线性链表S中查找第一个值为e的元素。    i = S[0].cur;//i指示表中第一个结点。    while(i && S[i].data != e)i = S[i].cur;//在表中顺序查找,当S[i].data = e的时候,跳出while,返回此时游标 i 的值。    return i;//用数组代替了链表,实际是数组但是操作代表的意思是链表的操作。}

类似地可写出在静态链表中实现插入和删除的操作的算法。为了辨明数组中那些分量未被使用,解决的办法是将所有未被使用过或被删除的分量用游标链成一个备用的链表,每当进行插入时便可从备用链表上取得一个结点作为带插入的新结点;反之,删除时,把删除下来的结点链接到备用链表上。


现以结合运算(A-B)U(B-A)为例来讨论静态链表的算法。


例2.3 假设由终端输入集合元素,先建立不是集合A的镜头链表S,然后在终端输入集合B的元素的同时查找S表,看是否存在和B相同的元素,则从S表删除之,否则插入S表。

为使算法清晰可见我们先给出三个步骤:

1. 将整个数组空间初始化为一个链表;——2.14

2. 从备用空间取得一个结点;——2.15

3. 将空闲结点链接到备用链表上,——2.16

分别如算法2.14、2.15和2.16;

算法2.14:

void  InitSpace_SL(SLinkList &space){    //将一维数组space中各分量链成一个备用的链表space[0].cur为头指针;0表示空指针。
    for(i = 0; i <= MAXSIZE - 1; ++i)  space[i].cur = i + 1;    space[MAXSIZE - 1].cur = 0;//0表示空指针,那么意思是它指向一个的是空地址;}

算法2.15:

int Malloc_SL(SLinklist &space){    //若备用空间链表非空,那么返回分配的结点下标,否则返回0;
    i = sapce[0].cur;//i是space的第一个结点;    if(space[0].cur) space[0].cur = space[i].cur;//也就是若i 不为0,则返回分配的结点i的下标;    return i;} 

算法2.16:

void Free_SL(SLinkList &space,int k){    //将下标为k的空闲结点回收到备用链表;    space[k].cur = space[0].cur;//space[k].cur指向第一个结点    space[0].cur = k;//space[0].cur表示是第一个结点;意思就是k是space的第一个结点了。}

算法2.17:实现例2.3的功能

void difference(SLinkList &space,int &S){  //依次输入集合A和B的元素,在数组space中建立表示集合(A-B)U(B-A)的静态链表,S为其头指针。假设备用空间足够大,space[0].cur为其头指针。  InitSpace_SL(space);//初始化备用空间;  S = Malloc_SL(space);//生成S的头结点;  r = S;//r指向S的最后结点;  scanf(m,n);//输入集合A\B的个数计数;  for(j = 1;j <= m;++j)//建立集合A的链表;  {    i = Malloc_SL(space);//分配结点;    scanf(space[i].data);//输入A的元素值;    space[r].cur = i; r = i;//插入到表尾;  }  space[r].cur = 0;//尾结点的指针为空;  for(j = 1;j <= n;++j)//依次输入集合B的元素,在A中不存在插入,存在,删除;  {     scanf(b); p = S; k = space[S].cur;//输入集合B的元素b,k指向集合B的第一个结点     while(k != space[r].cur && space[r].data != b)//在表中查找,也即是看输入的集合B的元素在备用表(集合A的元素)中是否存在;     {       p = k;//       k = space[k].cu;     }     if(k == space[r].cur)//查找一遍不存在,则插入到链表中;      {       i = Malloc_SL(space);       space[i].data = b;       space[i].cur = space[r].cur;       space[r].cur = i;     }
       else//在表中,则删除
     {
       space[p].cur = space[k].cur;
       Free_SL(space, k);//删除;       if(r == k)r = p;//若删除的是r所指结点,则修改尾指针,因为r指向链表的尾部;     }  }} 


注意:用数组描述线性表的时候与用指针的操作上的一些异同,比如,插入操作了,删除操作这些;像其中的分配结点函数,在数组里面是要自己实现的,而在指针里面直接用malloc函数就可以分配一个结点;








原创粉丝点击