文章向导
线性表及顺序存储结构
元素获取、插入、删除的实现
插入、删除操作的算法时间复杂度
一、线性表及顺序存储结构
线性表即有限个数据元素的序列,而其顺序存储结构则指的是用一段地址连续的存储单元依次存储线性表中的数据。在高级语言中(如C语言)一般可用一维数组来实现顺序存储结构。
二、元素获取、插入、删除的实现
定义在一个线性表上的操作有许多,比如初始化、清空、读/写、查找定位、插入/删除等。对于不同的应用,线性表的基本操作是不同的,但对于实际问题中所涉及到的复杂操作,基本上都是由这些基本操作来组合实现。
这里主要讲解读写、插入、删除三种基本操作的算法实现,其他的实现读者可在阅读完下面的例程后自行练习。
1.获取元素操作
获取线性表L中某个位置的元素值。
算法思路:
- 若未创建线性表或查找位置不合理,则抛出异常;
- 将查找位置对应的数组元素返回给变量e 。
#include<stdio.h>
#define MAXSIZE 10 /*存储空间初始值*/
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType;
typedef int Status; //Status是函数的类型,其值是结果状态代码, 如OK等
typedef struct
{
ElemType data[MAXSIZE]; //存放数据元素
int length; //线性表的长度
} SqList;
/*
* Function: 返回线性表中第Get_Loc个位置的元素值
* Initial conditions: 线性表L已存在, 且1<= Get_Loc <= ListLength(L)
* result of operation: 用e返回L中第Get_Loc位置的数据元素值
*/
Status GetElem(SqList L, int Get_Loc, ElemType* e)
{
/*输入参数检查*/
if (L.length == 0 || Get_Loc < 1 || Get_Loc > L.length)
{
return ERROR;
}
else
{
*e = L.data[Get_Loc-1]; //查找位置与数组下标的关系
return OK;
}
}
void Test_GetElem(void)
{
SqList L; //创建一个顺序线性表
ElemType* e; //待返回的值, 隐含危险:记得初始化
int Get_Loc; //查找的位置
int i;
int tmp;
e = &tmp; //初始化至合法区域
puts("Please Enter value of Get_Loc:\n");
scanf("%d", &Get_Loc);
puts("Please Enter length of List:\n");
scanf("%d", &L.length);
puts("Please Enter value of List element:\n");
/*L.length的值应始终 <= MAXSIZE*/
for (i=0; i<L.length; i++)
{
scanf("%d", &L.data[i]);
}
/*调用获取方法*/
if ( GetElem(L, Get_Loc, e) )
{
printf("*e = %d\n", *e); //打印获取到的元素
}
else
{
printf("ERROR!\n");
}
}
int main(void)
{
Test_GetElem();
return 0;
}
上述是完整且可运行的测试代码,具体的算法实现集成在GetElem函数中,该函数中的内容也是大多数据结构教材的示例代码。但想要对算法获得一直观的理解,光靠GetElem这一个函数是远远不够的,所以笔者才编写了完整的测试程序。程序带有关键性注释,同时读者阅读时应思考下函数Status GetElem(SqList L, int Get_Loc, ElemType* e)定义中第一个参数定义为SqList* L与定义为SqList L的区别,以及第三个参数定义为ElemType* e与ElemType e的区别。
另外,程序中使用到指针的部分一定要注意合法的初始化。由此可见,编写一份合格的测试程序,不仅要对算法有清晰的认知,同时也要有良好的语言基础。
2.插入元素操作
在线性表L中的第i个位置插入新元素e。
算法思路:
- 若插入位置不合理,则抛出异常;
- 若线性表长度大于数组存储长度,则抛出异常或动态增加容量(本例为抛出异常);
- 从表尾元素开始向前遍历到第i个位置,然后将它们都向后移动一个位置;
- 将要插入元素填入位置i处;
- 表长加1
#include<stdio.h>
#define MAXSIZE 10 /*存储空间初始值*/
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType;
typedef int Status; //Status是函数的类型,其值是结果状态代码, 如OK等
typedef struct
{
ElemType data[MAXSIZE]; //存放数据元素
int length; //线性表的长度
} SqList;
/*
* Function: 在线性表L的第Ins_Loc个位置插入新元素e
* Initial conditions: 线性表L已存在, 且1<= Ins_Loc <= ListLength(L)
* result of operation: 在L中插入新的数据元素e, 且L的长度加1
* Notes: 定义为ElemType e 而不是 ElemType* e 是因为该参数只需进行传值调用
* 定义为SqList* L 而不是 SqList L 是因为插入数据并不只是单纯的值拷贝, 还需要记录插入后序列的变动
*/
Status ListInsert(SqList* L, int Ins_Loc, ElemType e)
{
int i;
/*顺序线性表已满 or Insert_Loc不在范围时, 均返回ERROR;
*同时,使用L->length+1是考虑到插入表尾时的情况
*/
if (L->length == MAXSIZE || Ins_Loc < 1 || Ins_Loc > (L->length + 1))
{
return ERROR;
}
if (Ins_Loc <= L->length) /*若插入数据位置不在表尾*/
{
/*L->length-1为位置与下标的关系
*Insert_Loc-1也为位置与下标的关系, 同时也是作为从表尾遍历到第Insert_Loc个位置的判据
*/
for (i = L->length - 1; i >= Ins_Loc - 1; i--)
{
L->data[i+1] = L->data[i]; //将要插入位置后的数据元素均向后移动一位
}
}
L->data[Ins_Loc-1] = e; //将新元素插入
L->length++;
return OK;
}
void Test_InsElem(void)
{
SqList Buf[MAXSIZE];
SqList* L = Buf; //隐含危险:若不初始化则段错误
ElemType e; //待插入元素
int Ins_Loc;//插入位置
int i;
puts("Please Enter value of InsElem");
scanf("%d", &e);
puts("Please Enter value of Ins_Loc");
scanf("%d", &Ins_Loc);
puts("Please Enter length of List");
scanf("%d", &L->length);
puts("Please Enter value of List element:");
/*L.length的值应始终 <= MAXSIZE*/
for (i=0; i<L->length; i++)
{
scanf("%d", &L->data[i]);
}
/*调用插入方法*/
if ( ListInsert(L, Ins_Loc, e) )
{
puts("插入元素后的序列为:");
for (i=0; i<L->length; i++)
{
printf("%d ", L->data[i]);
}
printf("\n");
printf("L->length = %d\n", L->length);
}
else
{
printf("ERROR!\n");
}
}
int main()
{
Test_InsElem();
return 0;
}
同理,算法的具体实现放在ListInsert函数中。程序中带有的大量关键性注释大多为笔者的思考与调试心得,特别是刚才所提及的参数定义比较。另外,感兴趣的读者可以将Test_InsElem函数中SqList* L不进行初始化看看效果,以及初始化时改用malloc动态分配内存与用结构数组来初始化进行比较。
3.删除操作
删除线性表L中第i个位置的元素,并用变量e返回删除的值。
算法思路:
- 若删除位置不合理,则抛出异常;
- 取出删除元素;
- 从删除元素位置遍历至最后一个元素的位置,分别将它们向前移动一个位置;
- 表长减1
#include<stdio.h>
#define MAXSIZE 10 /*存储空间初始值*/
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType;
typedef int Status; //Status是函数的类型,其值是结果状态代码, 如OK等
typedef struct
{
ElemType data[MAXSIZE]; //存放数据元素
int length; //线性表的长度
} SqList;
/*
* Function: 删除线性表L中的第Del_Loc个数据元素
* Initial conditions: 线性表L已存在, 且1<= Del_Loc <= ListLength(L)
* result of operation: 删除线性表L中的第Del_Loc位置的数据元素, 并用e返回其值, 且表长减1
*/
Status ListDelete(SqList* L, int Del_Loc, ElemType* e)
{
int i;
/*若线性表为空或删除位置不正确*/
if (L->length == 0 || Del_Loc<1 || Del_Loc>L->length)
{
return ERROR;
}
*e = L->data[Del_Loc-1]; //用e返回待删除的元素值
if (Del_Loc < L->length) //若删除的位置不是表尾,但若删除的是表尾则为偷懒处理
{
/*将删除位置处的后继元素前移*/
for (i=Del_Loc; i<L->length; i++)
{
L->data[i-1] = L->data[i];
}
}
L->length--; //删除表尾时的偷懒处理
return OK;
}
void Test_DelElem(void)
{
SqList Buf[MAXSIZE];
SqList* L = Buf;
ElemType* e; //删除元素的返回值(记录)
int Del_Loc; //待删除的位置
int i;
puts("Please Enter value of Del_Loc");
scanf("%d", &Del_Loc);
puts("Please Enter length of List");
scanf("%d", &L->length);
puts("Please Enter value of List element:\n");
/*L.length的值应始终 <= MAXSIZE*/
for (i=0; i<L->length; i++)
{
scanf("%d", &L->data[i]);
}
/*调用删除方法*/
if ( ListDelete(L, Del_Loc, e) )
{
printf("删除的元素为%d\n", *e);
puts("删除后的序列为\n");
for (i=0; i<L->length; i++)
{
printf("%d ", L->data[i]);
}
printf("\n");
printf("L->length = %d\n", L->length);
}
else
{
puts("ERROR!");
}
}
三、算法时间复杂度的分析
由“获取元素操作”的示例可知,线性表的顺序存储结构在读、写操作时,不论是哪个位置其时间复杂度都为O(1)。而插入或删除时,因为程序的执行次数取决于问题的输入规模(即Ins_Loc或Del_Loc的值),程序的直接体现则为for循环的执行次数。下面具体分析插入、删除元素是的时间复杂度。
1.插入操作的时间复杂度
假定在任意位置插入元素的概率Pi相等: (表长为n,n+1个位置)
元素插入位置的可能值:i=1,2,3, … ,n, n+1
相应的移动次数:(n-i+1) = n,n-1, … ,1,0
由概率论中数学期望的观点可得平均移动次数:
最后由时间复杂度的推导可知,插入操作的平均时间复杂度为O(n)。
为何要分析平均移动次数呢?因为插入位置的不同,移动次数自然也不同,而考虑最坏或最好的情况都不具有代表性,自然应分析平均情况。
2.删除操作的时间复杂度
假定在线性表中的任意位置删除元素的概率相等:(表长为n,n个位置)
元素删除位置的可能值:i=1,2,…,n
相应向前移动的次数:(n-i)=n-1,n-2,…,0
由概率论中数学期望的观点可得平均移动次数:
最后由时间复杂度的推导可知,删除操作的平均时间复杂度也为O(n)。
参阅资料
《大话数据结构》
http://www.docin.com/p-1723299970.html