线性表是一种线性结构,在一个线性表中数据元素的类型是相同的,或者说线性表是由 同一类型的数据元素构成的线性结构。
定义:线性表是具有相同数据类型的n(n≥0)个数据元素的有限序列,通常记为: (a1,a2,… ai-1,ai,ai+1,…an) 其中n为表长, n=0 时称为空表。 需要说明的是:ai为序号为 i 的数据元素(i=1,2,…,n),通常将它的数据类型抽象为 ElemType,ElemType根据具体问题而定。
线性表的实现
1、线性表的顺序存储结构
(1)顺序表
线性表的顺序存储是指在内存中用地址连续的一块存储空间顺序存放线性表的各元素, 用这种存储形式存储的线性表称其为顺序表。
设a1的存储地址为Loc(a1),每个数据元素占d个存储地址,则第i个数据元素的地址为: Loc(a i)=Loc(a1)+(i-1)*d 1≤i≤n 这就是说只要知道顺序表首地址和每个数据元素所占地址单元的个数就可求出第i个数 据元素的地址来,这也是顺序表具有按数据元素的序号随机存取的特点。线性表的动态分配顺序存储结构:
#defineLIST_INIT_SIZE 100//存储空间的初始分配量 #defineLISTINCREMENT 10//存储空间的分配增量 typedefstruct{ ElemType*elem;//线性表的存储空间基址 int length;//当前长度 int listsize;//当前已分配的存储空间 }SqList;
(2)顺序表上基本运算的实现
顺序表的初始化:
顺序表的初始化即构造一个空表,这对表是一个加工型的运算,因此,将L设为引用参数, 首先动态分配存储空间,然后,将length置为0,表示表中没有数据元素。
intInit_SqList (SqList &L) { L.elem= (ElemType * )malloc(LIST_INIT_SIZE *sizeof(ElemType)); if(!L.elem) exit (OVERFLOW); //存储分配失败 L.length=0; L. listsize= LIST_INIT_SIZE;//初始存储容量 returnOK; }
顺序表的插入运算:
线性表的插入是指在表的第i(i的取值范围: 1≤i≤n+1)个位置上插入一个值为 x 的新元素, 插入后使原表长为 n的表成为表长为 n+1 表。
顺序表上完成这一运算则通过以下步骤进行:
① 将ai~an 顺序向下移动,为新元素让出位置;(注意数据的移动方向:从后往前依次 后移一个元素)
② 将 x 置入空出的第i个位置;
③ 修改表长。
intInsert_SqList (SqList &L,inti,ElemType x) { if(i <1|| i > L.length+1)returnERROR;//插入位置不合法 if(L.length >= L.listsize)returnOVERFLOW; //当前存储空间已满,不能插入 //需注意的是,若是采用动态分配的顺序表,当存储空间已满时也可增加分配 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; }
删除运算
线性表的删除运算是指将表中第 i (i 的取值范围为 :1≤ i≤n)个元素从线性表中去掉, 删除后使原表长为 n 的线性表成为表长为 n-1 的线性表。
顺序表上完成这一运算的步骤如下:
① 将ai+1~an 顺序向上移动;(注意数据的移动方向:从前往后依次前移一个元素)
② 修改表长。顺序表的删除运算与插入运算相同,其时间主要消耗在了移动表中元素上。
2、线性表的链式存储结构
单链表表示 :
链表是通过一组任意的存储单元来存储线性表中的数据元素的。
为建立起数据元素之间 的线性关系,对每个数据元素ai,除了存放数据元素的自身的信息 ai 之外,还需要和ai一起 存放其后继 ai+1 所在的存储单元的地址,这两部分信息组成一个“结点”。其中,存放数据元素信息的称为数据域,存放其后继地址的称为指针域。
线性表的单链表存储结构C语言描述下:
typedefstructLNode { ElemType data; //数据域 structLNode *next;//指针域 }LNode,*LinkList; LinkList L;//L 为单链表的头指针
通常用“头指针”来标识一个单链表,如单链表L、单链表H等,是指某链表的第一个结点 的地址放在了指针变量 L、H 中, 头指针为“NULL”则表示一个空表。
单链表上基本运算的实现
建立单链表:
●头插法——在链表的头部插入结点建立单链表
链表与顺序表不同,它是一种动态管理的存储结构,链表中的每个结点占用的存储空间 不是预先分配,而是运行时系统根据需求而生成的,因此建立单链表从空表开始,每读入一 个数据元素则申请一个结点,然后插在链表的头部。
LinkList CreateListF ( ) { LinkList L=NULL; //空表 LNode *s;int x;//设数据元素的类型为int scanf("%d",&x);while(x!=flag) { s=(LNode *)malloc(sizeof(LNode)); s->data=x; s->next=L; L=s; scanf ("%d",&x); } return L; }
●尾插法——在单链表的尾部插入结点建立单链表 头插入建立单链表简单,但读入的数据元素的顺序与生成的链表中元素的顺序是相反的, 若希望次序一致,则用尾插入的方法。因为每次是将新结点插入到链表的尾部,所以需加入 一个指针 r 用来始终指向链表中的尾结点,以便能够将新结点插入到链表的尾部。
LinkList CreateListR1 ( ) { LinkList L=NULL; LNode*s,*r=NULL; int x;//设数据元素的类型为int scanf("%d",&x); while(x!=flag) { s=(LNode *)malloc(sizeof(LNode)); s->data=x; if(L==NULL) L=s;//第一个结点的处理 else r->next=s;//其它结点的处理 r=s;//r 指向新的尾结点 scanf("%d",&x); } if( r!=NULL) r->next=NULL;//对于非空表,后结点的指针域放空指针 return L;
}
头结点的加入会带来以下两个优点:
第一个优点:由于开始结点的位置被存放在头结点的指针域中,所以在链表的第一个位 置上的操作就和在表的其它位置上的操作一致,无需进行特殊处理;
第二个优点:无论链表是否为空,其头指针是指向头结点在的非空指针(空表中头结点 的指针域为空),因此空表和非空表的处理也就统一了。
查找操作:
●按序号查找 Get_LinkList(L,i)
从链表的第一个元素结点起,判断当前结点是否是第i个,若是,则返回该结点的指针, 否则继续后一个,表结束为止,没有第i个结点时返回空。
LNode * Get_LinkList(LinkList L,inti); { LNode* p=L; intj=0; while(p->next !=NULL && j<i ) { p=p->next; j++; } if(j==i) return p; else return NULL; }
插入运算
●后插结点:设p指向单链表中某结点,s指向待插入的值为x的新结点,将s插入到p的 后面。
操作如下:
①s->next=p->next;
②p->next=s;
注意:两个指针的操作顺序不能交换。
删除运算
●删除结点 设p指向单链表中某结点,删除p。要实现对结点p的删除,首先要找到 p的前驱结点q,然后完成指针的操作即可。
操作如下:
①q=L; while (q->next!=p) q=q->next; //找*p的直接前驱
②q->next=p->next;
free(p);
循环链表
对于单链表而言,后一个结点的指针域是空指针,如果将该链表头指针置入该指针域, 则使得链表头尾结点相连,就构成了单循环链表。
双向链表
每个结点再加一个指向前驱的指针域,用这种结点组成的链表称为双向链表。
线性表的双向链表存储结构C语言描述下:
typedefstructDuLNode { ElemType data; structDuLNode *prior,*next; }DuLNode,*DuLinkList;
双向链表中结点的插入:
设 p 指向双向链表中某结点,s 指向待插入的值为 x 的新结点, 将s 插入到p 的前面。
操作如下:
① s->prior=p->prior;
② p->prior->next=s;
③ s->next=p;
④ p->prior=s;
指针操作的顺序不是唯一的,但也不是任意的,操作①必须要放到操作④的前面完成, 否则*p 的前驱结点的指针就丢掉了。
双向链表中结点的删除:
设 p 指向双向链表中某结点,删除*p。
操作如下:
①p->prior->next=p->next;
②p->next->prior=p->prior;
free(p);
顺序表和链表的比较
1.基于空间的考虑
顺序表和静态链表中存储空间是静态分配的, 顺序表在程序执行之前必须明确规定它的存储规 模。若线性表的长度 n 变化较大,则存储规模难于预先确定。估计过大将造成空 间浪费,估计太小又将使空间溢出的机会增多。在静态链表中,同时存在若干个结点类型相同的链表,则它们可以共享空 间,使各链表之间能够相互调节余缺,减少溢出机会。动态链表的存储空间是动 态分配的,只要内存空间尚有空闲,就不会产生溢出。因此,当线性表的长度变 化较大,难以估计其存储规模时,采用动态链表作为存储结构较好。
存储密度(Storage Density)是指结点数据本身所占的存储量和整个结点结构 所占的存储量之比,即:存储密度=结点数据本身所占的存储量/结点结构所占的 存储总量链表中的每个结点,除了数据域外,还要额外设置指针(或游标)域, 从存储密度来讲,这是不经济的。
一般地,存储密度越大,存储空间的利用率就高。显然,顺序表的存储密度 为 1,而链表的存储密度小于 1。例如单链表的结点的数据均为整数,指针所占 空间和整型量相同,则单链表的存储密度为 50%。因此若不考虑顺序表中的备用 结点空间,则顺序表的存储空间利用率为 100%,而单链表的存储空间利用率为 50%。由此可知,当线性表的长度变化不大,易于事先确定其大小时,为了节约 存储空间,宜采用顺序表作为存储结构。
2.基于时间的考虑
顺序表是由向量实现的,它是一种随机存取结构,对表中任一结点都可以在 O (1) 时间内直接地存取,而链表中的结点,需从头指针起顺着链找才能取得。 因此,若线性表的操作主要是进行查找,很少做插入和删除时,宜采用顺序表做 存储结构。
在链表中的任何位置上进行插入和删除,都只需要修改指针。而在顺序表中 进行插入和删除,平均要移动表中近一半的结点,尤其是当每个结点的信息量较 大时,移动结点的时间开销就相当可观。因此,对于频繁进行插入和删除的线性表,宜采用链表做存储结构。若表的插入和删除主要发生在表的首尾两端,则宜采用尾指针表示的单循环链表。
3.基于语言的考虑
在没有提供指针类型的高级语言环镜中,若要采用链表结构,则可以使用光 标实现的静态链表。虽然静态链表在存储分配上有不足之处,但它是和动态链表 一样,具有插入和删除方便的特点。
值得指出的是,即使是对那些具有指针类型的语言,静态链表也有其用武之地。特别是当线性表的长度不变,仅需改变结点之间的相对关系时,静态链表比 动态链表可能更方便。
总之,两种存储结构各有长短,选择那一种由实际问题中的主要因素决定。通常“较稳定” 的线性表选择顺序存储,而频繁做插入删除的即动态性较强的线性表宜选择链式存储。
线性表应用 —
1、一元多项式的表示
一元多项式可按升幂的形式写成: Pn(x) = p0+p1xe1+p2xe2+…+pnxen, 其中,ei为第 i项的指数,pi是指数 ei的项的系数,(且 1≤e1≤e2≤…≤en)
在计算机内,Pn(x)可以用一个线性表 P来表示: P= (p0,p1,p2, …,pn )
设有两个一元多项式 Pn(x) 和 Qm(x),假设 m<n,则两个多项式相加的结果 Rn(x)= Pn(x) + Qm(x),也可以用线性表 R来表示: R=(p0+q0,p1+ q1,,p2+ q2,…,pm+ qm ,pm+1,…,pn)
2、一元多项式的存储
一元多项式的操作可以利用线性表来处理。一元多项式也有顺序存储和链式存储两种方法。 (1)一元多项式的顺序存储表示 对于一元多项式:Pn(x) = p0+p1xe1+p2xe2+…+pnxen
有两种顺序存储方式:
第一种:只存储各项的系数,存储位置下标对应其指数项 p0 p1 p2 … pn ,适用于存储非零系数多的一元多项式。
第二种:系数及指数均存入顺序表 p0 0 p1 1 p2 2 … … pn n
适用于存储非零项少且指数高的一元多项式,此时只存储非零项的系数 和指数即可。
例如:R(x)=1+5x10000+7x20000
(2)一元多项式的链式存储表示
在链式存储中,对一元多项式只存储非零项的指数项和系数项 用单链表存储表示的结点结构为:
结点结构体定义如下:
struct Polynode { int coef; int exp; Polynode *next; } Polynode , * Polylist;
例:建立一元多项式链式存储算法
【算法思想】通过键盘输入一组多项式的系数和指数,用尾插法建立一元 多项式的链表。以输入系数 0 为结束标志,并约定建立多项式链表时,总 是按指数从小到大的顺序排列。
【算法描述】
Polylist polycreate() { Polynode *head, *rear, *s; int c,e; head=(Polynode *)malloc(sizeof(Polynode)); /*建立多项 式的头结点*/ rear=head; /* rear 始终指向单链表的尾,便于尾插法建表*/ scanf(“%d,%d”,&c,&e);/*键入多项式的系数和指数项*/ while(c!=0) /*若 c=0,则代表多项式的输入结束*/ { s=(Polynode*)malloc(sizeof(Polynode)); /*申请新 的结点*/ s->coef=c ; s->exp=e ; rear->next=s ; /*在当前表尾做插入*/ rear=s; scanf(“%d,%d”,&c,&e); } rear->next=NULL; /*将表的最后一个结点的 next 置 NULL,以 示表结束*/ return(head); }
3、一元多项式的相加运算
(1)用单链表表示的两个一元多项式
两个多项式:A(x)=7+3x+9x8+5x17 B(x)=8x+22x7-9x8
(2)多项式相加的运算规则
为了保证“和多项式”中各项仍按升幂排列,在两个多项式中:
①指数相同项的对应系数相加,若和不为零,则构成“和多项式”中的一 项;
②指数不相同的项仍按升幂顺序复抄到“和多项式”中。
【算法思想】以单链表 polya 和 polyb 分别表示两个一元多项式 A 和 B, A+B 的求和运算,就等同于单链表的插入问题(将单链表 polyb 中的结点插入 到单链表 polya中),因此 “和多项式“中的结点无需另生成。
为实现处理,设 p、q 分别指向单链表 polya 和 polyb 的当前项,比较 p、q 结点的指数项,由此得到下列运算规则:
① 若 p->exp< q->exp,则结点 p所指的结点应是“和多项式”中的 一项,令指针 p后移;
② 若 p->exp=q->exp,则将两个结点中的系数相加,当和不为零时 修改结点 p的系数域,释放 q结点;若和为零,则和多项式中无此项,从 A中 删去 p结点,同时释放 p和 q结点。
③ 若 p->exp>q->exp,则结点 q所指的结点应是“和多项式”中的 一项,将结点 q插入在结点 p之前,且令指针 q在原来的链表上后移;
【算法描述】
void polyadd(Polylist polya, Polylist polyb) /*将两个多项式相加,然后将和多项式存放在多项式 polya 中,并将多项式 ployb删除*/ { Polynode * p, *q, *tail; *temp; int sum; p=polya->next ; /*令 p和 q分别指向 polya和 polyb多项式链表中的第一个 结点*/ q=polyb->next ; tail=polya; /* tail指向和多项式的尾结点*/ /*初始化*/ while (p!=NULL && q!=NULL) /*当两个多项式均未扫描结束时*/ { if (p->exp< q->exp) /*规则⑴:如果 p指向的多项式项的指数小于 q的指数,将 p结点加入到和 多项式中*/ { tail ->next=p; tail =p; p=p->next; } else if ( p->exp= =q->exp) /*规则⑵:若指数相等,则相应的系数相加*/ { sum=p->coef + q->coef ; if (sum!=0) /*若系数和非零,则系数和置入结点 p,释放结点 q,并 将指针后移*/ { p->coef=sum; tail ->next=p; tail =p; p=p->next; temp=q; q=q->next; free(temp); } else { temp=p ; p=p->next ; free(temp); /*若系数和为零,则删除结点 p与 q,并将指针指向下一个结点*/ temp=q ; q=q->next ; free(temp); } } else { tail ->next=q; tail =q; /*规则⑶:将 q结点加入到“和多项式中”*/ q =q->next; } } } if(p!=NULL) /*多项式 A 中还有剩余,则将剩余的结点加入到和多项式 中*/ tail ->next=p; else /*否则,将 B 中的结点加入到和多项式中*/ tail ->next=q; }
假设 A 多项式有 M 项,B 多项式有 N 项,则上述算法的时间复杂度为 O(M+N)
推广: 通过对多项式加法的介绍,可以将其推广到实现两个多项式的相乘,因为 乘法可以分解为一系列的加法运算。