线性表
线性表是计算机程序设计活动中最经常用到的一种操作对象,也是数据结构中最简单,做基本和最重要的结构形式之一。实际上,线性表结构在很多领域,尤其是在程序设计语言和程序设计过程中大量使用,并非一个陌生概念。
线性表的定义
线性表是由n(n>=0)个属于同一个数据对象的数据元素,a1,a2,……an-1,an组成的有限序列。当1<i<n时,ai的直接前驱元素为ai-1,ai的直接后继数据为ai+1.也就是说,除表中的第1个数据元素a1没有前驱元素及最后一个数据元素an没有后继元素外,其他的每一个数据元素都有且仅有一个直接前驱元素和一个直接后继元素。n为线性表中包含的数据元素的个数,称为线性表的长度。长度为0的表称为空表,空表不包含任何数据元素。
对于非空线性表,每个数据元素在表中都有一个确定的位置,即数据元素ai在表中的位置仅取决于数据元素本身的序号i。
从逻辑上看,线性结构的特点是数据元素之间存在着“一对一”的逻辑关系。通常把具有这种特点的数据结构称为线性结构。反之,任何一个线性结构(其数据元素属于同一个数据对象)都可以用线性表表示出来,这里只要求按照元素的逻辑关系把它们顺序排列就可以了。
线性表的顺序存储结构
在计算机内部可以采用不同方式存储一个线性表,其中最简单对方式就是用一组连续的存储单元依次存储线性表中的数据元素。这种存储结构称为线性表的顺序存储结构。并称此时的线性表为顺序表。
- 线性表的顺序存储结构的类型可以描述如下
#define MaxSize 1000
ElementType A[MaxSize];
int n i;
线性表的顺序存储结构–几种常见操作的实现
1.在长度为n的线性表A的第i个位置插入一个新数据元素item
显然,在进行插入之前应该首先判断线性表是否已满,即分配给线性表的MaxSize个元素的存储空间是否已经被元素全部占用。如果未满还要测试插入的位置i是否合适,合适的插入位置应该是1<=i<=n+1(对应的数组A的下标为0<=i<=n。这里将新的数据元素插在表的末尾是允许的)。出现上述任何一种异常,插入操作都将失败。
如果满足插入条件,具体插入的过程分为以下3个步骤。
- 将线性表的第i个数据元素到第n个数据元素之间的所有元素依次向后移动一个位置(共移动n-i+1个元素)
- 将新的数据元素item插入到线性表的第i个位置上。
- 修改线性表的长度为n+1。
需要注意数据元素依次后移一个未知的方向,必须是从表的末尾元素开始后移,直到将第i个位置的元素后移一个位置为止。
具体算法如下
void INSERTLIST(ElementType A[],int &n, ElementType item)
{
int j;
if(n==MaxSize || i<1 || i>n+1)
ERRORMESSAGE("表满或者插入的为止不正确~"); /* 插入失败*/
for(j=n-1;j>=i-1,j--)
A[j+1]=A[j]; /*数据元素依次后移一个位置 */
A[i-1] = item; /*将item插入表的第i个位置*/
n++; /*表的长度加1*/
}
下面讨论算法的时间效率
从算法不难看到,算法花费的时间主要在插入新元素之前移动其他数据元素的过程上,因此,可以将移动数据元素的操作作为估算算法时间复杂度的基本操作。当i=n+1时,元素移动的次数最少,为0次;当i=0时,元素移动次数最多,为n次。算法的时间度为O(n)。
2.删除长度为n的线性表A的第i个数据元素
与插入操作类似,在做具体操作之前应该首先判断线性表是否为空,如果不空,还需要测试被删除位置i是否合适,合适的删除位置应该是1<=i<=n(对应的数组下标为0<=i<=n-1)。出现上述任何一种异常,删除操作都将失败。
如果满足删除条件,具体删除过程可以分为两步。首先将表的第i+1个数据元素至第n个数据元素(一共是n-i个元素)依稀向前移动一个位置,然后修改线性表的程度为n-1即可。
算法如下。
void DELETELIST(ElementType A[],int &n,int i)
{
int j;
if(i<1||i>n)
ERRORMESSAGE("表空或者删除位置不正确"); /*删除失败*/
for(j=i;j<n;j++)
A[j-1]=A[j]; /*数据元素一次前移一个位置*/
n--; /*表长减1*/
}
从算法中可以看到,删除顺序表中某个位置上的数据元素所花费的时间开销也主要在移动其他元素的操作上,而移动元素的个数也取决于被删元素的位置。
所以算法的平均时间复杂度也为O(n)。
3.确定元素item在长度为n的线性表A中的位置
该操作只需从线性表中的第1个数据元素开始,从前向后依次通过来比较确定给定元素item在表中的位置。如果在表中找到满足条件的数据元素,算法返回被查到元素在表中的位置,否则返回信息-1。本算法没有考虑表中出现多个满足条件的情况。具体算法如下。
void LOCSTE(ElemType A[],int n,ElemType item)
{
for(i=0;i<n;i++)
if(A[i] == item)
return i+1;
return -1;
}
算法中的基本运算是数据元素的比较。时间复杂度O(n)。
4.删除表中重复出现的元素
此算法思想比较简单,即从线性表的第1个元素开始到最后1个元素为止,一次检查在某元素后面的元素是否存在与之相同的元素,若存在,则删除后面那个元素,并即使修改表的长度。具体算法如下。
void PURGE(ElemType A[],int &n)
{
int i=0,j;
while(i<n){
j=i+1; /*从第i+1个元素逐个与第i个元素比较*/
while(j<n)
if(A[j]==A[i])
DELETELIST(A,n,j+1);
else
j++;
i++;
}
}
算法的时间复杂度O(n^2)。
5.对线性表中元素进行排序
所谓线性表的排序操作是指按照线性表中数据元素的值或者某个数据项值的大小排列数据元素,使之成为一个有序表。对线性表进行排序的方法很多,这里仅介绍简单的排序方法。
将一个按值任意排列的线性表,通过排序操作转换为按值有序排列的线性表,通常要经过若干次称为“趟”的操作。若线性表的长度为n,则选择排序方法要经过n-1趟排序才能达到目的,其中每一趟的排序规律都相同。
选择排序方法的基本思想是:第i趟排序是从线性表后面的n-i+1个数据中选择一个值最小的数据元素,并将其与这n-i+1个数据的第1个数据元素交换位置。经过这样的n-1趟排序以后,初始的线性表成了一个按值从小到大排列的线性表。
由于对值最小元素与另外一个元素进行位置交换的过程很简单,于是选择一个值最小元素的过程就是这种排序方法的核心。可以这样选择值最小元素:在每一趟排序前,先假设后面n-i+1个数据元素中的第1个元素最小,记录下它的位置,然后将它与第2个元素比较,若后者小于前者,记录后者的位置就是这n-i+1个元素中值最小元素的位置。
算法如下。
void SELECTSORT(ElemType K[],int n)
{
int i,j,d;
ElemType temp;
for(i=0;i<n-1;i++){
d = i; /*假设最小值元素为未排序元素的第1个元素*/
for(j=i+1;j<n;j++)
if(K[j]<K[d])
d=j; /*寻找真正的值最小元素,记录其位置d */
if(d!=i){
temp = K[d];
K[d]=K[j];
K[j]=K[d];
}
}
}
时间算法为O(n^2)
讨论几个涉及顺序表插入和删除的例子
例1 已知长度为n的顺序表A,请写出删除表中数据信息为item的元素算法。
解题思路:
比较直观而简单的方式是:从表中的第1个数据元素开始到表中最后一个数据元素结束。依次将各数据元素与item做比较,当遇到与item相匹配的数据元素时,随即删除它。因此算法可以设计如下。
void DELETITEM1(ElemType A[],int &n,ElemType item)
{
int i=0;
while(i<n)
if(A[i]==item)
DELETLIST(A,n,i+1);
else
i++;
}
有前面的讨论知道,算法DELETLIST(A,n,i)的时间复杂度是O(n),因而上述算法的时间复杂度是O(n^2)。细心的读者可能已经想到了,如果对算法进行改进,得到一个时间复杂度为O(n)的算法并不难。
改进后的算法思路为:设置一个整型变量k,令其初始值为-1.在对表中第1个数据元素到最后一个数据元素比较的过程中,当A[i]满足条件时,只将k的值增1,不做其他动作;当A[i]不满足条件时,将i送表中位置i-k-1处。最后修改线性表长度为n-k-1即可。按照该思路设计算法如下。
void DELETITEM2(ElemType A[],int &n,ElemType item)
{
int i,k=-1;
if(n>1){
for(i=0;i<n-1;i++)
if(A[i]==item) /*若元素A[i]满足条件*/
k++;
else
A[i-k-1]=A[i]; /*将A[i]送表的i-k-1处*/
n=n-k-1; /*修改表的长度*/
}
}
例2 已知长度为n的非空线性表A采用顺序存储结构,表中数据元素按值的大小非递减排列。请写时间复杂度为O(n)的算法,删除线性表中值相同的多余元素,使表中数据各不相同。
算法描述如下。
void PURGE(ElemType A[],int &n)
{
int i,k=0;
if(n>1){
for(i=1;i<n;i++){
if(A[i]!=A[k])
A[k++]=A[i]
n = k;
}
}
}
例3 已知长度为n的非空线性表A采用顺序存储结构,表中数据元素按值的大小非递减排列,请写出在该表中插入数据信息为item的元素的算法。要求:插入后的线性表仍保持数据元素按值的大小非递减排列。
解题思路:
分两种情况,第1种情况是,当item大于或者等于表的最后那个数据元素时,只需将item直接插在这个元素之后,然后修改表长即可。第2种情况属于一般情况,即当item大于或者等于表中某个元素时,需要从前至后通过比较找到该元素的位置,然后将item插到该位置之后最后修改表长。算法如下。
void INSERTITEM(ElemType A[],int &n,ElemType item)
{
int i;
if(item>=A[n-1])
A[n++]=item;
else{
i=0;
while(A[i]>item)
i++;
INSERTLIST(ElemType A,n,i+1,item);
}
}
时间复杂度为O(n)
顺序存储的小结
特点
线性表的顺序存储结构的最大特性时逻辑上相邻的两个数据元素在物理位置上也相邻,也正是因为这一特点,使得线性表在顺序存储结构下具有鲜明的有点和缺点。
优点
- 构造原理简单,比较直观,易理解
- 若已知每个数据的元素所占用的存储单元个数,并且知道一个数据元素的存储位置,则在任意一个数据元素的位置可以通过一个简单的解析式计算出来。
- 对表中有数据元素,既可以进行顺序访问,也可以进行随机访问,也就是说,既可以从表的第一个元素逐个访问,也可以根据元素位置直接访问,并且任意一个数据元素的时间代价都相同。
- 只需存放数据元素本身,而无其他额外空间开销,相对链式存储结构,存储空间开销小。
缺点
- 需要一片连续的存储单元作为线性表的存储空间
- 存储空间的分配事先进行,使得应该分配的存储空间不容易估计。尤其是在线性表的长度变化比较大时,必须按照可能的最大空间需求量分配,腹肌过大容易导致分配的存储空间不能充分使用;估计过小,空间容量的扩充通常比较困难。
- 进行插入或删除操作时,需要对插入或者删除位置后面所有数据元素逐个进行移动,操作的时间效率第,尤其当表较长,且插入或删除点的位置靠前时,更是如此。
因此,顺序存储结构比较适合于线性表的长度不经常发生变化,或者只需要在顺序存取设备商做批处理的场合。