【数据结构】-线性表&链表

本文深入探讨了线性表的两种主要存储形式——顺序存储和链式存储,并详细介绍了链式存储的各种变体,包括单链表、循环链表及双向链表的基本概念、特点及其关键操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

二 线性表

2.1 线性表的定义

线性表(Linear List) :是由n(n≧0)个数据元素(结点)a1,a2, …an组成的有限序列。该序列中的所有结点具有相同的数据类型。其中数据元素的个数n称为线性表的长度。

当n=0时,称为空表。

当n>0时,将非空的线性表记作: (a1,a2,…an)        

a1称为线性表的第一个(首)结点,an称为线性表的最后一个(尾)结点。

a1,a2,…ai-1都是ai(2≦i≦n)的前驱,其中ai-1是ai的直接前驱;

ai+1,ai+2,…an都是ai(1≦i ≦n-1)的后继,其中ai+1是ai的直接后继。

 

 

2.2 抽象数据类型(ADT)

ADT List{

数据对象:D = { ai| ai∈ElemSet,  i=1,2,…,n, n≧0}

数据关系:R ={<ai-1, ai> | ai-1, ai∈D,  i=2,3,…,n }

基本操作:

InitList(&L )

操作结果:构造一个空的线性表L;

ListLength( L)

初始条件:线性表L已存在;

操作结果:若L为空表,则返回TRUE,否则返回FALSE;

GetElem( L,i, &e )

初始条件:线性表L已存在,1≦i≦ListLength(L);

操作结果:用e返回L中第i个数据元素的值;

ListInsert (L, i, &e )

初始条件:线性表L已存在,1≦i≦ListLength(L) ;

操作结果:在线性表L中的第i个位置插入元素e;

       …

} ADT List

 

2.3 逻辑结构

<a1,a2,a3,……….> 序偶

 

2.4 顺序存储

2.4.1顺序存储

把线性表的结点按逻辑顺序依次存放在一组地址连续的存储单元里。用这种方法存储的线性表简称顺序表

2.4.2顺序存储的线性表的特点

 ◆线性表的逻辑顺序与物理顺序一致;

 ◆数据元素之间的关系是以元素在计算机内“物理位置相邻”来体现。

 

物里地址计算方式:

 LOC(ai+1)=LOC(ai)+l       (ai ,ai+1)的地址关系

LOC(ai)=LOC(a1)+(i-1)*l  (a1, ai)的地址关系

 

2.4.3顺序存储基本操作算法

2.4.3.1 顺序线性表初始化

思想:

 

 实现:

 Status Init_SqList( SqList *L)

{

 L->elem_array=( ElemType *)malloc(MAX_SIZE*sizeof( ElemType ) ) ;

If ( !L -> elem_array ) return ERROR ;

else {   L->length= 0;    return OK ;  } 

}

 

时间与空间

2.4.3.2 顺序线性表的插入

思想:

(1) 将线性表L中的第i个至第n个结点后移一个位置。

(2) 将结点e插入到结点ai-1之后。

(3) 线性表长度加1。

 

实现:

Status Insert_SqList(Sqlist *L,int i ,ElemType e)

 { 

int j ;

if  ( i<0||i>L->length-1)   return ERROR ;

if  (L->length>=MAX_SIZE)

{    printf(“线性表溢出!\n”);  return ERROR ;  }

for  ( j=L->length–1; j>=i-1; --j )

L->Elem_array[j+1]=L->Elem_array[j];

/*  i-1位置以后的所有结点后移  */

L->Elem_array[i-1]=e;    /*  在i-1位置插入结点 */

L->length++;

return  OK ; 

}

 

时间与空间

             设在线性表L中的第i个元素之前插入结点的概率为Pi,不失一般性,设各个位置插入是等概率,则Pi=1/(n+1),而插入时移动结点的次数为n-i+1。

总的平均移动次数: Einsert=∑pi*(n-i+1)  (1≦i≦n)

∴ Einsert=n/2 。

   即在顺序表上做插入运算,平均要移动表上一半结点。当表长n较大时,算法的效率相当低。因此算法的平均时间复杂度为O(n)。

 

2.4.3.3 顺序线性表的删除

思想

(1) 在线性表L查找值为x的第一个数据元素。

(2) 将从找到的位置至最后一个结点依次向前移动一个位置。  

(3) 线性表长度减1。

 

实现

Status Locate_Delete_SqList(Sqlist *L,ElemType x)

     /*  删除线性表L中值为x的第一个结点  */

int  i=0 , k ;

printf(“线性表L为空!\n”);return ERROR;

 

while (i<L->length)      /*查找值为x的第一个结点*/

{  

if  (L->Elem_array[i]!=x )  i++ ;

else 

  {  for ( k=i+1; k<L->length; k++)

          L->Elem_array[k-1]=L->Elem_array[k];

      L->length--;  break ;

  }

}

if (i>L->length)

{    printf(“要删除的数据元素不存在!\n”);

return ERROR;  }

return  OK;   

}

时间与空间:

时间主要耗费在数据元素的比较和移动操作上。

首先,在线性表L中查找值为x的结点是否存在;

其次,若值为x的结点存在,且在线性表L中的位置为i ,则在线性表L中删除第i个元素。

   设在线性表L删除数据元素概率为Pi,不失一般性,设各个位置是等概率,则Pi=1/n。

 ◆比较的平均次数: Ecompare=∑pi*i     (1≦i≦n)

∴ Ecompare=(n+1)/2 。

 ◆删除时平均移动次数:Edelete=∑pi*(n-i)  (1≦i≦n)

∴ Edelete=(n-1)/2 。  平均时间复杂度:Ecompare+Edelete=n ,即为O(n)

 

2.5 链式存储(单链表)

2.5.1 链式存储

用一组任意的存储单元存储线性表中的数据元素。用这种方法存储的线性表简称线性链表。

 

2.5.2 顺序链表的特点

单链表: 每一个结只包含一个指针域的链表

 

typedef struct  Lnode

{  

ElemType  data;    /*数据域,保存结点的值 */

struct   Lnode *next;      /*指针域*/

}LNode;        /*结点的类型 */

2.5.2基本操作示意图

 

 

 

2.5.4基本操作

2.5.4.1头插入法建表

 

思想

       从一个空表开始,重复读入数据,生成新结点,将读入数据存放到新结点的数据域中,然后将新结点插入到当前链表的表头上,直到读入结束标志为止。即每次插入的结点都作为链表的第一个结点

伪码

LNode *create_LinkList(void)

   /*  头插入法创建单链表,链表的头结点head作为返回值  */ 

{  

      int data ;

LNode *head, *p;

head= (LNode  *) malloc(sizeof(LNode));

head->next=NULL;       /*  创建链表的表头结点head  */

while (1)

{  

scanf(“%d”,&data) ;

if(data==32767)  break ;

p=(LNode  *)malloc(sizeof(LNode));

p–>data=data;     /*  数据域赋值  */

p–>next=head–>next;  head–>next=p ;

      /*  钩链,新创建的结点总是作为第一个结点  */

}

return (head);

}

 

时间空间

 

2.5.4.2尾插入法建表

 

思想

   头插入法建立链表虽然算法简单,但生成的链表中结点的次序和输入的顺序相反。若希望二者次序一致,可采用尾插法建表。该方法是将新结点插入到当前链表的表尾,使其成为当前链表的尾结点。

 

伪码:

LNode *create_LinkList(void)

    /*  尾插入法创建单链表,链表的头结点head作为返回值  */ 

{  int data ;

LNode *head, *p, *q;

head=p=(LNode  *)malloc(sizeof(LNode));

p->next=NULL;        /* 创建单链表的表头结点head */

while (1)

{   scanf(“%d”,& data);

if (data==32767)  break ;

q= (LNode  *)malloc(sizeof(LNode));

q–>data=data;     /*  数据域赋值  */

q–>next=p–>next;  p–>next=q; p=q ;

/*钩链,新创建的结点总是作为最后一个结点*/

}

return (head);  

}

 

时间空间


 

2.5.4.3 单链表的查找

思想

   对于单链表,不能象顺序表中那样直接按序号i访问结点,而只能从链表的头结点出发,沿链域next逐个结点往下搜索,直到搜索到第i个结点为止。因此,链表不是随机存取结构

伪码

LNode *Locate_Node(LNode *L,int key)

/* 在以L为头结点的单链表中查找值为key的第一个结点  */

{  

LNode *p=L–>next;

while ( p!=NULL&& p–>data!=key)  

    p=p–>next;

if  (p–>data==key)   return p;

else {   

printf(“所要查找的结点不存在!!\n”);

retutn(NULL); 

}

}

 

 

时间空间

平均时间复杂度为O(n)。

 

 

2.5.4.4 单链表的插入

思想

插入运算是将值为e的新结点插入到表的第i个结点的位置上,即插入到ai-1与ai之间。因此,必须首先找到ai-1所在的结点p,然后生成一个数据域为e的新结点q,q结点作为p的直接后继结点。

 

伪码

void Insert_LNode(LNode *L,int i,ElemType e)

   /*  在以L为头结点的单链表的第i个位置插入值为e的结点 */

{  

int j=0;  LNode *p,*q;

p=L–>next ;

while ( p!=NULL&& j<i-1)

p=p–>next; 

j++;  

}

if (j!=i-1)     printf(“i太大或i为0!!\n ”);

else

{

  q=(LNode *)malloc(sizeof(LNode));

q–>data=e;  

q–>next=p–>next;

p–>next=q;

}

}

  设链表的长度为n,合法的插入位置是1≦i≦n。算法的时间主要耗费移动指针p上,故时间复杂度亦为O(n)。

 

 

2.5.4.5 单链表的删除

思想

为了删除第i个结点ai,必须找到结点的存储地址。该存储地址是在其直接前趋结点ai-1的next域中,因此,必须首先找到ai-1的存储位置p,然后令p–>next指向ai的直接后继结点,即把ai从链上摘下。最后释放结点ai的空间,将其归还给“存储池”

伪码

void Delete_LinkList(LNode *L,int key)

/* 删除以L为头结点的单链表中值为key的第一个结点  */

{    LNode *p=L,  *q=L–>next;

while ( q!=NULL&& q–>data!=key)    

{ p=q;  q=q–>next;   }

if (q–>data==key)  

{ p->next=q->next; free(q);   }

else 

printf(“所要删除的结点不存在!!\n”);

}

 

 

时间空间

时间复杂度也是O(n)

 

 

 

2.5.4.6 单链表的合并

 

 

思想

算法中pa ,pb分别是待考察的两个链表的当前结点,pc是合并过程中合并的链表的最后一个结点。

伪码

LNode *Merge_LinkList(LNode *La, LNode *Lb)

     /*  合并以La,Lb为头结点的两个有序单链表  */

{   LNode *Lc,  *pa ,  *pb , *pc, *ptr ;

Lc=La ; pc=La  ;    pa=La->next ;  pb=Lb->next  ;

 while (pa!=NULL       && pb!=NULL)

 { if  (pa->data<pb->data)

   {   pc->next=pa ;  pc=pa ;  pa=pa->next  ;   }

/* 将pa所指的结点合并,pa指向下一个结点  */

if (pa->data>pb->data)

   {   pc->next=pb ;  pc=pb ;  pb=pb->next  ;   }

/* 将pa所指的结点合并,pa指向下一个结点  */

if (pa->data==pb->data)

   {   pc->next=pa ;  pc=pa ;  pa=pa->next  ;

        ptr=pb ; pb=pb->next ; free(ptr) ;  }

/* 将pa所指的结点合并,pb所指结点删除  */

}

 if (pa!=NULL) 

pc->next=pa;

else  

pc->next=pb;     /*将剩余的结点链上*/

free(Lb) ;

return(Lc) ;

}

时间空间

若La ,Lb两个链表的长度分别是m,n,则链表合并的时间复杂度为O(m+n)。

 

 

 

2.6 链式存储(循环链表)

2.6.1 循环链表

    循环链表(Circular Linked List):是一种头尾相接的链表。其特点是最后一个结点的指针域指向链表的头结点,整个链表的指针域链接成一个环。

 

 

2.6.2循环链表的操作

   对于单循环链表,除链表的合并外,其它的操作和单线性链表基本上一致,仅仅需要在单线性链表操作算法基础上作以下简单修改:

⑴ 判断是否是空链表:head->next==head ;

⑵ 判断是否是表尾结点:p->next==head ;

2.7 链式存储 (双向链表)

双向链表(Double Linked List) :指的是构成链表的每个结点中设立两个指针域:一个指向其直接前趋的指针域prior,一个指向其直接后继的指针域next。这样形成的链表中有两个方向不同的链,故称为双向链表。

typedef struct Dulnode

{    

ElemType  data ;

structDulnode  *prior , *next ;

}DulNode ;

 

双向链表结构具有对称性,设p指向双向链表中的某一结点,则其对称性可用下式描述:

(p->prior)->next=p=(p->next)->prior;结点p的存储位置存放在其直接前趋结点p->prior的直接后继指针域中,同时也存放在其直接后继结点p->next的直接前趋指针域中。

2.7.1 双向链表插入

 

① 插入时仅仅指出直接前驱结点,钩链时必须注意先后次序是: “先右后左” 。部分语句组如下:

S=(DulNode *)malloc(sizeof(DulNode));

S->data=e;

S->next=p->next;   p->next->prior=S;

p->next=S;  S->prior=p;    /*  钩链次序非常重要  */

②  插入时同时指出直接前驱结点p和直接后继结点q,钩链时无须注意先后次序。部分语句组如下:

S=(DulNode *)malloc(sizeof(DulNode));

S->data=e;

p->next=S;       S->next=q;

S->prior=p;         q->prior=S;

 

 

2.7.2 双向链表删除

设要删除的结点为p ,删除时可以不引入新的辅助指针变量,可以直接先断链,再释放结点。部分语句组如下:

p->prior->next=p->next;

p->next->prior=p->prior;

free(p);

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值