在计算机中主要有两种基本的存储结构用于存放线性表:顺序存储结构和链式存储结构。
线性表的顺序存储结构
线性表的顺序存储是指用一组地址连续的存储单元依次存储线性表中的各个元素,使得线性表中在逻辑结构上相邻的数据元素存储在连续的物理存储单元中,即通过数据元素物理存储的连续性来反映数据元素之间逻辑上的相邻关系。采用顺序存储结构存储的线性表通常称为顺序表。可将顺序表归纳为:关系线性化,结点顺序存。
在顺序表中,每个结点ai的存储地址是该结点在表中的逻辑位置i的线性函数,只要知道线性表中第一个元素的存储地址(基地址)和表中每个元素所占存储单元的多少,就可以计算出线性表中任意一个数据元素的存储地址,从而实现对顺序表中数据元素的随机存取。
1.地址的计算
假设线性表中有n个数据元素,每个元素占k个单元,第一个元素的地址为Loc(a1),则可通过如下公式计算出第i个元素的地址Loc(ai):
Loc(ai)=Loc(ai)+(i-1)*k
其中,Loc(a1)称为基地址。
2.线性表顺序存储的表示
线性表的顺序存储结构可借助于高级程序设计语言中的一堆数组来表示,一堆数组的下标与元素在线性表中的序号相对应。
用C语言定义线性表的顺序存储结构如下:
#define MAXSIZE 100 /*此处的宏定义常量表示线性表的最大长度*/
typedef struct
{
ElemType elem[MAXSIZE]; /*线性表占用的数组空间*/
int last; /*记录线性表中最后一个元素在数组elem[]中的位置(下标值),空表置为-1*/
}SeqList;
说明:
①结点类型定义中ElemType数据类型是为了描述的统一而自定的,在实际应用中,用户可以根据自己实际需要来具体定义顺序表中元素的数据类型,如int、char、float或是一种struct结构类型。
②从数组中起始下标为0处开始存放线性表中第一个元素。因此需注意区分元素的序号和该元素在数组中的下标位置之间的对应关系,即数据元素a1的序号为1,而其对应存放在elem数组的下标为0;ai在线性表中的序号值为i,而在顺序表对应的数组elem中的下标为i-1.
注意类型与变量的区别。类型是模板,变量是真正的存储空间。
利用定义的顺序表的数据类型SeqList就可以定义变量了。变量L的定义与使用方法有以下两种:
①通过变量定义语句
SeqList L;
将L定义为SeqList类型的变量,利用L.elem[i-1]来访问顺序表中序号为i的元素ai;通过L.last可以得到顺序表中最后一个元素的下标,而L.last+1就是顺序表的长度。
②通过指针变量定义语句
SeqList L1,*L;L=&L1;
将L定义为指向SeqList类型的指针变量,使用时,可通过L->elem[i-1]来访问顺序表中序号为i的元素ai,使用L->last+1则得到顺序表的长度。
线性表顺序存储结构上的基本运算
下面列举说明如何在线性表的顺序存储结构上实现线性表的基本运算。
1.查找操作
线性表有两种基本的查找运算。
①按序号查找GetData(L,i):查找顺序表L中第i个数据元素。
根据顺序表L的存储特性,表中元素在L的elem数组中顺序存放,故GetData(L,i)的核心语句为L.elem[i-1]。
②按内容查找Locate(L,e):要求查找顺序表L中与给定值e相等的数据元素。
在顺序表L中找到与e相等的元素,则返回该元素在表中的序号;若找不到,则返回一个“空序号”标识,如-1.
【算法思想】查找运算可采用顺序查找,即从第一个元素开始,依次将表中元素与e相比较,若相等,则查找成功,返回该元素在表中的序号;若e与表中的所有元素都不相等,则查找失败,返回-1.
【算法描述】顺序表的按内容查找运算
int Locate(SeqList L,ElemType e)
/*在顺序表L中查找与e相等的元素,若L.elem[i]=e,则找到该元素,并返回i+1,若找不到,则返回-1*/
{ i=0; /*i为扫描计数器,初值为0,即从第一个元素开始比较*/
while((i<=L.last)&&(L.elem[i]!=e)) /*顺序扫描表,直到找到值为e的元素,或扫描到表尾而没找到*/
i++;
if(i<=L.last)
return(i+1);/*若找到值为e的元素,则返回其序号*/
else
return(-1);/*若没找到,则返回空序号*/
}
该算法的时间复杂度为O(n)。
2.插入操作
线性表的插入运算是指在表的第i(1<=i<=n+1)个位置前插入一个新元素e,使长度为n的线性表(e1,……,ei-1,ei,……,en)变成长度为n+1的线性表(e1,……,ei-1,e,ei,……,en)(其中n为L的表长度)。
【算法思想】用顺序表作为线性表的存储结构时,由于结点的物理顺序必须和结点的逻辑顺序保持一致,因此必须将原表中位置n,n-1,……,i上的结点,依次后移到位置n+1,n,……,i+1上,空出第i个位置,然后在该位置上插入新节点e。当i=n+1时,是指在线性表的末尾插入结点,所以无须移动结点,直接将e插入表的末尾即可。
例 已知线性表(4,9,15,28,30,30,42,51,62),在第四个元素之前插入一个元素“21”。
【分析】如果在第四个元素前插入一个元素,则需将第九个位置到第四个位置的元素依次后移一个位置,然后将“21”插入到第四个位置。
【算法描述】顺序表的插入运算
#define OK 1
#define ERROR 0
int InsList(SeqList *L,int i,ElemType e)
/*在顺序表L中第i个数据元素之前插入一个元素e。i的合法取值范围是1<=i<=L-last+2*/
{
int k;
if ((i<1)||(i>L->last+2)) /*首先判断插入位置是否合法*/
{ printf(“插入位置i值不合法”);
return(ERROR);
}
if(L-last>=MAXSIAZE-1)
{ printf("表已满,无法插入");
return(ERROR);
}
for(k=L->last;k>=i-1;k--) /*为插入元素而移动位置*/
L->elem[k+1]=L->elem[k];
L->elem[i-1]=e; /*在C语言数组中,第i个元素的下标为i-1*/
L->last++;
return(OK);
}
【算法分析】当在表尾(i=L->last+2)插入元素时,因为循环的终值大于初值,此时不需要移动元素,可直接在表尾插入e。当在表头(i=1)插入时,移动元素的语句L->elem[k+1]=L->elem[k]需要执行n次,即将表中已存在的n个元素依次后移一个位置才能将e插入。因此,语句L->elem[k+1]=L->elem[k]的语句执行频度与插入位置i有关。
设Eins为在长度为n的表中插入一元素所需移动元素的平均次数,假设Pi为在第i个元素之前插入元素的概率,并假设在任何位置上插入的概率相等,即Pi=1/(n+1),i=1,2,……,n+1,则有
Eins=n+1∑i=1 Pi(n-i+1)=1/n+1 n∑i=1 (n-i+1) =1/n+1 n∑k=1 k=n/2
3.删除操作
线性表的删除运算是指将表的第i(1<=i<=n)个元素删去,使长度为n的线性表(e1,……,ei-1,ei,ei+1,……,en),变成长度为n-1的线性表(e1,……,ei-1,ei+1,……,en)。
【算法思想】用顺序表作为线性表的存储结构时,由于结点的物理顺序必须和结点的逻辑顺序保持一致,因此当需要删除第i个元素时,必须将原表中位置在i+1,i+2,……,n-1,n上的结点,依次前移到位置i,i+1,……,n-1(其中n为L的表长度)。
例 删除线性表(4,9,15,21,28,30,30,42,51,62)第五个元素。
【分析】如果要删除第五个元素,则需将第六个元素到第十个元素依次向前移动一个位置。
【算法描述】顺序表的删除运算
int DelList(SeqList *L,int i,ElemType *e)
/*在顺序表L中删除第i个数据元素,并用指针参数e返回其值。i的合法取值为1<=i<=L.last+1*/
{
int k;
if ((i<1)||(i>L->last+1))
{
printf("删除位置不合法!");
return(ERROR);
}
*e=L->elem[i-1]; /*将删除的元素存放到e所指向的变量中*/
for(k=i;i<=L->last;k++)
L->elem[k-1]=L->elem[k]; /*将后面的元素依次前移*/
L->last--;
Return(OK);
}
【算法分析】如插入运算类似,在顺序表上实现删除运算也必须移动结点,这样才能维持线性表节点间的逻辑关系。当删除表尾(i=L->last+1)元素时,因为循环变量的初值大于终值,此时不需要移动元素,仅将表长度减1即可。当删除表头元素(i=1)时,移动元素的语句L->elem[k+1]=L->elem[k]需执行n-1次。因此删除算法中移位语句L->elem[k-1]=L->elem[k]的执行频度与删除位置有关。
设Edel为删除一个元素所需移动元素的平均次数,Qi为删除第i个元素的概率,并假设在任何位置上删除的概率相等,即Qi=1/n,i=1,2,……,n.则有
Edel= n∑i=1 Qi(n-i)=1/n n∑i=1 (n-i)=1/n n-1∑k=0 k=n-1/2
由以上分析可知,在顺序表中插入和删除一个数据元素时,其时间主要耗费在移动数据元素上。做一次插入或删除平均需要移动表中的一半元素,当n较大时算法效率较低。
例 有两个顺序表LA和LB,其元素均为非递减有序排列,编写算法,将他们合并成一个顺序表LC,要求LC也是非递减有序排列。例如,LA=(2,2,3),LB=(1,3,3,4),则LC=(1,2,2,3,3,3,4)。
【算法思想】设表LC是一个空表,为使LC也是非递减有序排列,可设两个指针i、j分别指向表LA和LB的元素,若LA.elem[i]>LB.elem[j],则当前先将LB.elem[j]插入到表LC中,若LA.elem[i]<=LB.elem[j],则当前先将LA.elem[i]插入到表LC中,如此进行下去,直到其中一个表被扫描完毕,然后再将为扫描完的表中剩余的所有元素放到表LC中。
【算法描述】线性表的合并运算
void mergeList(SeqList *LA,SeqList *LB,SeqList *LC)
{
int i,j,k,l;
i=0;j=0;k=0;
while(i<=LA->last && j<=LB->last)
if(LA->elem[i]<=LB->elem[j])
{
LC->elem[k]=LA->elem[i];
i++;k++;
}
else
{
LC->elem[k]=LB->elem[j];
j++;k++;
}
while(i<=LA->last) /*当表LA有剩余元素时,则将表LA余下的元素赋给表LC*/
{
LC->elem[k]=LA->elem[i];
i++; k++;
}
while(i<=LB->last) /*当表LB有剩余元素时,则将表LB余下的元素赋给表LC*/
{
LC->elem[k]=LB->elem[j];
j++;k++;
}
LC->last=LA->last+LB->last+1;
}
【算法分析】
由于两个待归并的表LA、LB本身是值有序表,且表LC的建立采用的是尾插法建表,插入时不需要移动元素,所以算法的时间复杂度O(LA->last+LB->last).
由上面的讨论可知,线性表顺序表示的优点如下:
①无须为表示节点间的逻辑关系而增加额外的存储空间(因为逻辑上相邻的元素其存储的物理位置也是相邻的)。
②可方便的随机存取表中的任一元素,如GetData(L,i)操作。
线性表顺序表示的缺点如下:
①插入或删除运算不方便,除表尾的位置外,在表的其他位置上进行插入或删除操作都必须移动大量的结点,其效率较低。
②由于顺序表要求占用连续的存储空间,存储分配只能预先进行静态分配。因此当表长变化较大时,难以确定合适的存储规模。若按可能达到的最大长度预先分配表空间,则可能造成一部分空间长期闲置而得不到充分利用;若事先对表长估计不足,则插入操作可能使表长超过预先分配的空间而造成溢出。