数据结构:线性表(c语言)

一、线性表的类型定义

线性表是最常用且最简单的一种数据结构。一个线性表是n个数据元素的有限序列。线性表的顺序存储结构的特点是逻辑上相邻的两个元素在物理位置上也相邻,因此可以随机存取表中的任意一个元素。然而,这个特点也造成了这种存储结构的弱点:在进行插入和删除操作时。需要移动大量的元素。而另一种表示方法(链式存储结构),由于它不要求逻辑上相邻的元素在物理位置上也相邻,因此它没有顺序结构所具有的弱点,但同时也失去了顺序表可随机存取的优点。

在稍微复杂的线性表中,一个数据元素可以由若干个数据项组成,在这种情况下,常把数据元素称为记录,含有大量记录的线性表又称为文件。线性表中的数据元素可以是各种各样的,但同一个线性表中的元素必定具有相同的特性,即属于同一个数据对象,相邻的数据元素之间存在着序偶关系。

线性表中元素的个数n(n>=0)定义为线性表的长度,n=0时称为空表。

抽象数据类型的线性表的定义如下:

ADT List{

数据对象:D={ai|ai属于ElemSet,i=1,2,...,n,n>=0}

数据关系:R1={<ai-1,ai>|ai-1,ai属于D,i=2,...n}

数据操作:

InitList(&L)

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

Destroy(&L)

初始化条件:线性表L已经存在。

操作结果:销毁线性表L。

ClearList(&L)

初始化条件:线性表L已经存在。

操作结果:将L重置为空表。

ListEmpty(L)

初始化条件:线性表L已经存在。

操作结果:若L为空表,则返回True,否则返回False。

ListLength(L)

初始化条件:线性表L已经存在。

操作结果:返回L中数据元素的个数。

GetElem(L,i,&e)

初始化条件:线性表L已经存在,1=<i<=ListLength(L)。

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

LocateElem(L,e,compare())

初始化条件:线性表L已经存在,compare()是数据元素的判定函数、

操作结果:返回L中第1个与e满足关系compare()的数据元素的位序。若这样的数据元素不存在,则返回值为0。

PriorElem(L,cur_e,&pre_e)

初始化条件:线性表L已经存在。

操作结果:若cur_e是L的数据元素,且不是第一个,则用pre_e返回它的直接前驱,否则操作失败,pre_e无定义。

NextElem(L,cur_e,&next_e)

初始化条件:线性表L已经存在。

操作结果:若cur_e是L的数据元素,且不是最后一个,则用next_e返回它的后继,否则操作失败,next_e无定义。

ListInsert(&L,i,e)

初始化条件:线性表L已经存在,1=<i<=ListLength(L)+1

操作结果:在L中第i个位置之前插入新的数据元素e,L的长度加1.

ListDelete(&L,i,&e)

初始化条件:线性表L已经存在且非空,1=<i<=ListLength(L)

操作结果:删除L的第i个数据元素,并用e返回其值,L的长度减去1

ListTraverse(L,visit())

初始化条件:线性表L已经存在。

操作结果:依次对L的每个数据元素调用visit().一旦visit()失败,则操作失败。

}ADT List

例1:扩大线性表LA,将存在于线性表LB中而不存在于线性表LA中的数据元素插入到线性表LA中去。只要从线性表LB中依次取得每个数据元素,并依值再线性表LA中进行查访,若不存在则插入。算法如下:

/*将所有在线性表Lb中但不在La中的数据元素插入到La中*/
void union(List &La,List Lb){
La_len = ListLength(La);
Lb_len = ListLength(LB);//求线性表的长度
for(i=1;i<=Lb_len;i++){
GetElem(Lb,i,e);//取Lb中第i个数据元素赋值给e
//La中不存在和e相同的数据元素,则插入
if(!Locate(La,e,equal)) ListInsert(La,++La_len,e);
}

例2:已知线性表LA和LB中的数据元素按照值非递减有序排列,现在要求将LA和LB归并为一个新的线性表LC,且LC中的数据元素按照值非递减有序排列。例如,设LA=(3,5,8,11),LB=(2,6,8,9,11,15,20)则LC=(2,3,5,6,8,8,9,11,11,15,20)

从上述问题要求可知,LC中的数据元素或者是LA中的数据元素,或者是LB中的数据元素,则只要先设LC为空表,然后将LA或者LB中的元素依次插入到LC中即可。为了使得LC中的元素按照值非递减有序排列,可设两个指针i和j分别指向LA和LB中某个元素,若设i当前所指的元素为a,j当前所指的元素为b,则当前应插入到LC的元素c为:

c=a 当a=<b时;c=b当a>b时

显然,指针i和j的初值均为1,在所指元素插入LC之后,在LA或LB中顺序后移,算法实现如下:

/*已知线性表La和Lb中的数据元素按照值非递减排列。
归并La和Lb得到新的线性表Lc,Lc的数据元素也按值非递减排列
*/
void MergeList(List La,List Lb,List &Lc){
InitList(Lc);
i=j=1,k=0;
La_len = ListLength(La);Lb_len = ListLength(Lb);
while((i<=La_len)&&(j<=Lb_len)){//La和Lb非空
GetElem(La,i,ai);GetElem(Lb,j,bj);
if (ai<=bj){
ListInsert(Lc,++k,ai);++i
}
else{
ListInsert(Lc,j,bj);++j;
}
}
while(i<=La_len){
GetElem(La,i++,ai);ListInsert(Lc,++k,ai);
}
while(j<=Lb_len){
GetElem(Lb,j++,bj);ListInsert(Lc,++k,bj)
}
}

上述两个算法的时间复杂度取决于抽象数据类型List定义中基本操作的执行时间。假如GetElem和ListInsert这两个操作的执行时间与表长无关,LocateElem的执行时间和表长成正比,则例1的时间复杂度为哦O(ListLength(LA)*ListLength(LB))。

例2的时间复杂度为O(ListLength(LA)+ListLength(LB)),虽然存在三个循环语句,但只有当i和j均指向表中实际存在的数据元素时,才能取得数据元素的值进行相互比较,并且当其中一个线性表的数据元素均已插入到线性表LC中后,只要将另外一个线性表中的剩余元素依次插入即可。

二、线性表的顺序表示和实现

1.线性表动态分配顺序存储结构

#define List_INIT_SIZE 100//线性表存储空间的初始分配量
#define ListINCREMENT 10//线性表存储空间的分配增量
typedef struct{
	ElemType *elem;//存储空间基址
	int length;//当前长度
	int listsize;//当前分配的存储容量(以sizeof(ElemType)为单位)
}SqList

其中,数组指针elem指示线性表的基地址,length指示线性表的当前长度,listsize指示顺序表当前分配的存储空间大小,一旦因为插入元素而空间不足时,可进行再分配,即为顺序表增加一个大小为存储LISTINCREMENT个数据元素的空间。

下面进行顺序表的初始化操作,其实就是为顺序表分配一个预定义大小的数组空间,并将线性表的当前长度设为0。

/*初始化线性表*/
Status InitList_Sq(SqList &L){
//构造一个空的线性表L
L.elem = (ElemType *)malloc(List_INIT_SIZE*sizeof(ElemType));
if (!L.elem) exit(OVERFLOW);//存储空间分配失败
L.length = 0;//空表长度为0
L.listsize  = LIST_INT_SIZE;//初始存储容量
return OK
}

下面介绍线性表的插入和删除两种操作在顺序存储表示时的实现方法。

例3:线性表插入操作:

思路:一般情况下,在第i个(1=<i<=n)个元素之前插入一个元素时,需要将第n至第i(共n-i+1个元素向后移动一个位置)

算法如下:

/*在顺序线性表L中第i个位置之前插入新的元素e*/
Status ListInsert_Sq(Sqlist &L,int i,ElemType e){
//i的合法值为1=<i<=L.length+1
if (i<1 || i>L.length+1) return Error;//i值不合法
if (L.length>=L.listsize){//当前存储空间已满,增加分配
newbase = (ElemType *)realloc(L.elem,(L.listsize+LISTINCREMENT)*sizeof(ElemType));
if (!newbase) exit(OVERFLOW);//存储分配失败
L.elem = newbase;//新的基址
L.listsize +=LISTINCREMENT;//增加存储容量
}
q = &(L.elem[i-1]);//q为插入的位置
for(p=&(L.elem[L.Length-1]);p>=q;--p) *(p+1) = *p;//插入的位置以及之后的元素右移
*q = e;//插入e
++L.length;//表长增加1
return OK;
}

例4:线性表删除操作

思路:一般情况下,删除第i个(1<=i<=n)个元素时,需要将从第i+1到第n(共n-i)个元素依次向前移动一个位置。

算法如下:

/*在顺序线性表L中删除第i个元素,并用e返回其值*/
Status ListDelete_Sq(SqList &L,int i,ElemType &e){
//i的合法值为1=<i<=L.length
if((i<1) || (i>L.length)) return Error;//i值不合法
p = &(L.elem[i-1]);//p为被删除元素的位置
e = *p;//被删除元素赋值给e
q = L.elem+L.length-1;//表尾元素的位置
for(++p;p<=q;++p)*(p-1) = *p;//被删除元素之后的元素左移
--L.length;//表长减去1
return OK;
}

通过上面两种操作方式可知,当在顺序存储结构的线性表中某个位置上插入或者删除一个数据元素时,其时间主要耗费在移动元素中,也就是说移动元素的操作为预估算法时间复杂度的基本操作,而移动元素的个数取决于插入和删除元素的位置。在顺序存储结构的线性表中插入或者删除一个数据元素,平均约移动表中一半的元素。若表长为n,则上面两个算法的时间复杂度为O(n)。

在这里我们讨论下例题1和例题2中的操作在顺序存储的线性表中的实现和时间复杂度的分析。容易看出,顺序表的“求表长”“取第i个数据元素”的时间复杂度均为O(1),而且这两个例子中进行的插入操作都在表尾进行,则不需要移动元素。

对于例题1的执行时间主要取决于查找函数LocateElem的执行时间,在顺序表中查找是否存在和e相同的数据元素的最简便的方法是,令e和L中的数据元素依次比较。基本操作是“进行两个元素之间的比较”,若L中存在和e相同的元素ai,则比较次数为i,否则为L.length,算法实现如下:

/*在顺序线性表L中查找第一个值与e满足compare()的元素的位序*/
int LocateElem_Sq(SqList L,ElemType e,Status(*compare)(ElemType,ElemType)){
//若找到,则返回其在L中的位序,否则返回0
i=1;//i的初值为第1个元素的位序
p=L;//p的初值为第一个元素的储存位置
while(i<=L.length&&!(*compare)(*p++,e)) ++i;
if(i<=L.length) return i;
else return 0;
}

上面算法的时间复杂度为O(L.length),因此,对于顺序表La和Lb而言,例题1的时间复杂度为O(La.length*Lb.length)

对于例题2,可直接写出如下算法,时间复杂度为O(La.length+Lb.length),实现顺序表的合并。

/*已知顺序线性表La和Lb的元素按照值非递减排列
归并La和Lb得到新的顺序线性表Lc,Lc的元素也按照值非递减排列
*/
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++;//插入La的剩余元素
while(pb<=pb_last) *pc++=*pb++;//插入Lb的剩余元素
}

上面算法的时间复杂度为O(La.length+LB.length)

三、线性表的链式表示和实现

1、线性链表

线性表的链式存储结构的特点是用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的也可以是不连续的)。因此,为了表示每个数据元素ai和其直接后继的数据元素ai+1之间的逻辑关系(即直接后继的存储位置)。这两部分信息组成数据元素的ai的存储映像,称为结点。它包括两个域:其中存储数据元素信息的域称为数据域;存储直接后继存储位置的域称为指针域。指针域中存储的信息称作指针或者链。n个结点链接成的一个链表,即为线性表(a1,a2,...,an)的链式存储结构,又因为此链表的每一个结点中只包含一个指针域,又称为线性链表或者单链表。

用线性链表表示线性表时,数据元素之间的逻辑关系是由结点中的指针指示的。换句话说,指针为数据元素之间的逻辑关系的映像,则逻辑上相邻的两个数据元素其存储的物理位置不要求紧邻,因此,这种存储结构为非顺序映像或者链式映像。

/*线性表的单链表存储结构*/
typedef struct LNode{
ElemType data;
struct LNode *next;
}LNode,*LinkList

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

在单链表中,取得第i个数据元素必须从头指针出发查找,因此,单链表是非随机存取的存储结构。

例5:函数GetElem在单链表中的实现。

Status GetElem_L(LinkList L,int i,ElemType &e){
//L为带有头结点的单链表的头指针
//当第i个元素存在时,其值赋给e并返回OK,否则返回error
p = L->next;j=1//初始化p指向第一个结点,j为计数器
while(p&&j<i){//顺指针向后查找,直到p指向第i个元素或者p为空
p = p->next;++j
}
if(!p||j>i) return ERROR;//第i个元素不存在
e = p->data;
return OK;
}

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

例6:在单链表实现元素的插入和删除操作

(1)插入元素

/*在带头结点的单链表线性表L中第i个位置之前插入元素e*/
Status ListInsert_L(LinkList &L,int i,ElemType e){
p=L;j=0;
while(p&&j<i-1){
p=p->next;++j;//寻找第i-1个结点
}
if(!p||j>i) return ERROR;//i小于1或者大于表长加1
s = (LinkList)malloc(sizeof(LNode))//生成新的结点,由系统生成一个LNode结点,同时将该结点的起始位置赋值给指针变量
s->data=e;s->next = p->next;
p->next = s;
return OK
}

(2)删除元素

/*在带头结点的单链表L中,删除第i个元素,并由e返回其值*/
Status ListDelete_L(linkList &L,int i,ElemType &e){
p=L;j=0;
while(p->next&&j<i-1){//查找第i个结点,并令其p指向其前驱
p=p->next;++j;
}
if(!(p->next||j>i-1)) return ERROR;//删除位置不合理
q = p->next;p->next=q->next;
e=q->data;free(1);
return OK
}

上面两个算法的时间复杂度为O(n).单链表和顺序表存储结构不同,它是一种动态结构,整个可用存储空间可为多个链表共同享用,每个链表占用的空间不需要预先分配划定,而是可以由系统按照需求生成,因此,建立线性表的链式存储结构的过程就是一个动态生成链表的过程。

例7:从表尾到表头逆向建立单链表的方法,其时间复杂度为O(n).

/*逆位序输入n个元素的值,建立带表头结点的单链表线性表L*/
void CreateList_L(LinkList &L,int n){
	L = (LinkList)malloc(sizeof(LNode));
	L->next = NULL;//先建立一个带头结点的单链表
	for(i=n,i<=0,--i){
		p=(LinkList)malloc(sizeof(LNode));//生成新的结点
		scanf(&p->data);//输入元素值
		p->next=L->next;L-next=p;//插入到表头
}

例8:将两个有序链表合并为一个有序链表

思路:假设头指针为La和Lb的单链表分别为线性表LA和LB的存储结构,现在要归并La和Lb得到单链表Lc,需要设立3个指针pa,pb和pc,其中pa和pb分别指向La表和Lb表中当前待比较插入的结点,而pc指向Lc表中当前的最后一个结点,若pa->data<=pb->next,则将pa所指的结点链接到pc所指的结点之后,否则将pb所指几点链接到pc所指结点之后。

/*已知单链线性表La和Lb的元素按照值非递减的顺序排列
	归并La和Lb得到新的单链线性单链表Lc,Lc的元素也按照值非递减排列
	*/
void MergeList_L(LinkList &La,LinkList &Lb,LinkList &Lc){
	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、静态链表

有时候可借用一维数组来描述线性链表,其类型说明如下:

线性表的静态单链表存储结构

#define MAXSIZE 1000//链表的最大长度
typedef struct{
ElemType data;
int cur;
}component,SLinkList[MAXSIZE];

这种描述方法便于在不设置“指针”类型的高级程序设计语言中使用链表结构,在上面描述的链表中,数组的一个分量表示一个结点,同时用游标代替指针指示加点在数组中的相对位置。数组的第0分量可看成头结点,其指针域指示链表的第一个结点。这种存储结构仍然需要预先分配一个较大的空间,但是在线性表的插入和删除操作时可以不需要移动元素,仅仅需要修改指针,所以具有链式存储的主要优点。为了和指针型描述的线性链表区别,我们给这中用数组描述的链表叫做静态链表。

在静态链表中实现定位函数LocateElem,算法如下:

/*在静态单链线性表L中查找第1个值为e的元素,若找到,则返回它在L中的位序,否则返回0*/
int LocateElem_SL(SLinkList S,ElemType e){
i=S[0].cur;//i指示表中第一个结点
while(i&&S[i].data!=e) i=S[i].cur;//在表中顺链查找
return i;
}

3、循环链表

循环链表是另一种形式的链式存储结构。它的特点是表中最后一个结点的指针域指向头结点,整个链表形成一个环。由此,从表中任意一个结点出发均可找到表中其他结点。

循环链表的操作和线性链表基本一致,差别在于算法中的循环条件不是p或者p->next是否为空,而是他们是否等于头指针。但有的时候。若在循环链表中设立尾指针而不设头指针,可使得某些操作简化。例如将两个线性表合并为一个表时,仅将一个表的表尾和另一个表的表头相接。运算时间复杂度为O(1)。

4、双向链表

上面说的链式存储结构的结点中只有一个指示直接后继的指针域,由此,从某个结点出发只能顺时针往后查找其他结点。若要查找结点的直接前驱,则需要从表头指针出发。换句话说,在单链表中,NextElem的执行时间为O(1),而PriorElem的执行时间为O(n),为了克服单链表这种单向性的缺点,可使用双向单链表。

在双向链表的结点中有两个指针域,其一指向直接后继,另一个指向直接前驱。

/*线性表的双向链表存储结构*/
typedef struct DuLNode{
ElemType data;
struct *prior;
struct DuLNode *next;
}DuLNode,*DuLinkList;

双向链表的插入和删除结点

插入结点

/*在带头结点的双链循环线性表L中第i个位置之前插入元素e*/
Status ListInset_DuL(DuLinkList &L,int i ,ElemType e){
if(!(p=GetElemP_Dul(L,i)))//在L中确定插入的位置
    return ERROR;
if(!(s=(DuLinkList)malloc(sizeof(DulNode)))) return ERROR;
s->data=e;
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior =s;
return Ok;
}

删除结点

/*删除带头结点的双链循环线性表L的第i个元素*/
Status ListDelete_DuL(DuLinkList &L,int i,ElemType &e){
if(!(p=GetElemP_Dul(L,i)))//在L中确定第i个元素的位置指针为p
    return ERROR;
e=p->data;
p->prior->next = p->next;
p->next->prior=p->prior;
free(p);return OK;
}

由于链表在空间的合理利用上和插入、删除时不需要移动等的优点,因此在很多场合下,它是线性表的首选存储结构。然而,它也存在着实现某些基本操作如求线性表的长度时不如顺序表存储结构的缺点;另一方面,由于在链表中,结点之间的关系用指针来表示,则数据元素在线性表中的位序的概念已经1淡化,而被数据元素在线性表中的位置所代替。

typedef struct LNode{//结点类型
ElemType data;
struct LNode *next;
}*Link,*Position;


typedef struct{//链表类型
Link head,tail;//分别指向线性链表中的头结点和最后一个结点
int len;//指示线性链表中数据元素的个数
}LinkList;

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值