这篇博客给出的代码不全,我们只是把最复杂的部分在这里实现和分析,其他部分详细的代码会有专门下一篇博客进行相信说明
顺序表的表示
我们在前面已经说明了线性表的定义,以及线性表ADT的定义,接下来我们说明线性表ADT的表示,我们首先来说明线性表的顺序存储,顺序存储中我们要实现两类功能:
①存储表示
②基本操作的函数实现
线性表的顺序存储结构
在计算机中用一组地址连续的存储单元依次存储线性表的各个数据元素,称作线性表的顺序存储结构或顺序映象。用这种方法存储的线性表称作顺序表。
这样我们很容易想到这样的存储结构在我们熟悉的结构里面就是数组。通过图解,我们在这里很容易看到元素如何去存,知道了元素之间的4个线性关系,也就是连续的存储单元。
那么既然是连续的,a2就是跟着a1紧随其后,后面的元素也是一样,并且所有元素的数据类型相同,所占内存的空间大小是一样的,所以很容易能够通过起始位置找到线性表中的每一个元素,假设每一个空间占用L个字节,如图标出,我们就很容易定位到数组的任何一个元素。
假设线性表的每个元素需占 l 个存储单元,则第 i + 1 个 元素的存储位置和第 i 个元素的存储位置之间满足关系:
LOC(ai +1) = LOC(ai ) + l
由此,所有数据元素的存储位置均可通过基地址得到:
LOC(ai ) = LOC(a1) + (i - 1) l
特点:
以物理位置相邻表示逻辑关系;,物理关系就是其逻辑关系,也就是物理上连续,逻辑上连续,任一元素均可随机存取。
随机存取:
在表内查找的过程中查找的元素位置只和起始位置和所要查找到的下标i有关,和表的长度是无关,这种方式称为随机存取。
已知位置、获取该位置上的元素非常方便,与该线性表的长度无关 。
静态线性表和动态线性表说明
静态线性表
接下来我们进入代码过程,我们必须有这样的观念,数据结构的代码并不是一次性就写完整的,需要不断的改进和思考的过程,最终使得代码各方面性能更好。
#define LISTSIZE 20 //线性表存储空间的原始分量
typedef int DataType; //typedef抽象类型
typedef其实就是给变量去别名,这里的作用就是给int类型取一个别名,这样做的好处就是,如果我们实际开发过程中要使用的数据不是int类型的,那么我们只需要在这里的把int改成我们所需要的类型即可。
typedef struct SList
{
DataType data[LISTSIZE]; //存储数据元素的空间
int length; //存储当前已有的元素个数
}SeqList,*SqListPtr; //给结构体分别取名为SeqList,*SqListPtr (后面定义了一个结构体类型的指针)
上面两种在使用的时候其实差别不是很大,你可以定义其中一个,在后面的代码中只要使用你定义的类型并且按照类型正确使用即可。但是我在传递的时候经常传递的是结构体变量的地址进行结构体操作,所以我们定义一个指针类型的结构体,传递我们创建的线性表。
那么在这里我们就需要考虑到,我们现在所定义的结构能不能实现我们之后的更能函数,如果你是刚开始学习数据结构的用户,可能没有这样的思想,在这个过程中难免会碰钉子,这是非常正常的,那么我们现在直接来考虑一下,我们定义了线性表之后肯定要给线性表里面插入数据,如果当我们插入的时候线性表的数据满了,我们开始定义了LISTSIZE的空间,如果空间满了那么我们就会插入失败,我们把这种线性表长度不能改变的线性表称为静态线性表,把线性表长度能过够改变的线性表成为动态线性表。那么我们既然已经看出来了静态线性表的不足,所以我们接下里详细讲解动态线性表,但是我们也会给出静态线性表的详细代码,两者除了动态线性表的长度可变在操作的时候稍稍麻烦一点之外其他的函数功能基本没有区别,我们会给出两者详细的代码,读者可以进行区分,同时线性表的学习是我们之后其他数据结构学习的开端,对于我们学习之后的数据结构有重要意义。
动态线性表
接下来我们详细说明动态线性表:
const int LIST_INIT_SIZE = 100; // 表初始分配的最大长度
const int LISTINCREMENT = 10; // 分配内存的增量
typedef int ElemType;
typedef struct List
{
ElemType* elem; //存储空间的基址
int length; //顺序表中已经存储的元素的个数
int listsize; //顺序表的存储空间大小
}SqList,*sqListPtr;
我们首先进行说明我们给出是初始长度为10 如果长度不够的时候每次给线性表长度扩容为原来的二倍结构体里面使用指针变量存放数据有助于我们通过malloc函数进行扩容。
初始化操作
初始化的操作目的就是要构造一个空的线性表(这里我们构造的是动态的顺序表)
初始化的过程是一个从无到有的过程
那么我们首先需要有一个线性表,一个线性表由三个域构成一个是数据域elen,当前存储元素的长度length,线性表的总长度listsize
如果我们在内存中申请了一段连续的内存空间,那么我们用数据域的指针指向申请的连续内存空间就形成了我们线性表的数据域,我们在初始化的线性表的空的,所以线性表的长度为0,那么listsize就是数据地址指向的内存空间的大小,如果申请的内存空间为10那么这里的listsize就是10。
如果根据我么那上面的描述那么线性表初始化的示意图如下图所示:
当然上面只是举一个例子,具体在实现的过程中我们的线性表的长度会根据实际的定义进行赋值。
那么接下来我们通过C语言编写,bool类型在C语言是不存在的,我们是在C++环境下创建的,所以更加简易话,我们在这里直接使用bool类型:
bool InitList(sqListPtr L)
{
assert(L != NULL); //断言L不为空,如果L为空则所有操作都没有意义
if (L == NULL) //如果L为空
{
printf("%d\t %s error", __LINE__, __FILE__); //打印出错的文件和代码行数便于修改
exit(0);
}
L->elem = (ElemType*)malloc(LIST_INIT_SIZE * sizeof(ElemType)); //动态申请100个空间
if (L->elem == NULL)
{
printf("apply fail\n");
return false;
}
L->length = 0;
L->listsize = LIST_INIT_SIZE;
return true;
}
接下来我们实现插入操作:
在第i个位置之前插入新元素
线性表的插入运算是指在表的第 i (1 <= i <= n +1) 个位置上,
插入一个新结点 b,使长度为 n 的线性表 (a1, …, ai –1, ai, …, an)
变成长度为 n + 1 的线性表 (a1, …, ai –1, b, ai, …, an)
算法思想:
1)检查 i 值是否超出所允许的范围 (1 <= i <= n + 1) ,若超出,则进行“超出范围”错误处理;
2)将线性表的第 i 个元素和它后面所有元素均后移一个位置;
3)将新元素写入到空出的第 i 个位置上;
4)使线性表的长度增 1。
我们给出下面图示:
上面的图示即说明了元素插入的位置是不固定的,也帮助我们理解1 <= i <=n + 1
例如我们总共长度为6 我们可以插入到6+1的位置。
那么我们这里要说明:
如果在最后一个位置插入那么比较简单,我们可以直接插入,但是如果插入的不是最后一个位置,插入位置之后的元素都要向后移动,然后把插入的元素放在插入的位置。并且在的移动的时候必须从后往前覆盖避免数据覆盖。
我们通过图解进行说明:
头插法:
头插法结果:
中间位置插入:
中间位置插入结果:
顺序表尾部插入:
尾部插入元素直接进入空间。
错误的头插元素移动方法(其他位置本质相同):
错误的操作:
那么我们可以看出来整个过程中数据挪动最消耗时间。
部分代码实现及算法复杂度分析
动态线性表的插入实现代码
实现代码为:
/*------------------------------------------------------------
操作目的: 在顺序表的指定位置插入结点,插入位置i表示在第i个
元素之前插入
初始条件: 线性表L已存在,1<=i<=ListLength(L) + 1
操作结果: 在L中第i个位置之前插入新的数据元素e,L的长度加1
函数参数:
SqList *L 线性表L
int i 插入位置
ElemType e 待插入的数据元素
返回值:
bool 操作是否成功
bool ListInsert(sqListPtr L, int i, ElemType e)
{
assert(L != NULL); //断言L不为空,如果L为空则所有操作都没有意义
if (L == NULL) //如果L为空
{
printf("%d\t %s error", __LINE__, __FILE__); //打印出错的文件和代码行数便于修改
exit(0);
}
if ((i < 1) || (i > L->length + 1))
{
printf("pos error\n");
return false;
}
ElemType* newbase = NULL;
//1.判断线性表是否为满
if (L->length == L->listsize)
{
//2.如果满了就扩容然后插入数据
newbase = (ElemType*)realloc(L->elem, (LIST_INIT_SIZE) * 2 * sizeof(ElemType));
if (NULL == newbase)
{
printf("expension fail\n");
return false;
}
L->elem = newbase;
L->listsize *= 2;
for (int j = L->length; j >= i ; j--)
{
L->elem[j] = L->elem[j - 1];
}
L->elem[i - 1] = e;
++L->length;
return true;
}
else
{
//3.如果没有满则直接插入数据
L->elem[i - 1] = e;
L->length++;
return true;
}
return false;
}
动态线性表插入算法的时间复杂度分析:
问题规模是表的长度,设它的值为 n
算法的时间主要花费在向后移动元素的 for 循环语句上。该 语句的循环次数为 (n– i +1)。由此可看出,所需移动结点的次数不仅依赖于表的长度 n,而且还与插入位置 i 有关。
当插入位置在表尾 (i=n +1) 时,不需要移动任何元素;这是最好情况,其时间复杂度 O(1)。
当插入位置在表头 (i = 1) 时,所有元素都要向后移动,循环语句执行 n 次,这是最坏情况,其时间复杂度 O(n)。
算法的平均时间复杂度:设 pi 为在第 i 个元素之前插入一个元素的概率,则在长度为 n 的线性表中插入一个元素时所需移动元素次数的期望值为 :
假设在表中任何位置 (1 , i ,n+1) 上插入结点的机会是均等的,则:
由此可见,在顺序表上做插入运算,平均要移动表上一半元素。当表长 n 较大时,算法的效率相当低。算法的平均时间复杂度为 O(n)。
动态线性表的删除实现
线性表的删除运算是指将线性表的第 i (1 <= i <= n) 个结点
删除,使长度为 n 的线性表 (a1, …, ai –1, ai, ai +1, …, an)
变成长度为 n -1 的线性表 (a1, …, ai –1, ai +1, …, an)
算法思想:
1) 检查 i 值是否超出所允许的范围 (1 i n),若超出,则进
行“超出范围”错误处理;
2) 将线性表的第 i 个元素后面的所有元素均前移一个位置;
3) 使线性表的长度减 1。
删除位置包括以下:
初始顺序表:
删除操作:
删除最后一个元素-------------------删除中间元素------------------删除第一个元素。
动态线性表的插入实现代码
/*------------------------------------------------------------
操作目的: 删除顺序表的第i个结点
初始条件: 线性表L已存在且非空,1<=i<=ListLength(L)
操作结果: 删除L的第i个数据元素,并用e返回其值,L的长度减1
函数参数:
SqList *L 线性表L
int i 删除位置
返回值:
bool 操作是否成功
------------------------------------------------------------*/
bool ListDelete(sqListPtr L, int i)
{
assert(L != NULL); //断言L不为空,如果L为空则所有操作都没有意义
if (L == NULL) //如果L为空
{
printf("%d\t %s error", __LINE__, __FILE__); //打印出错的文件和代码行数便于修改
exit(0);
}
if (i < 1 || i > L->length)
{
printf("pos error\n");
return false;
}
for (int j = i - 1; j < L->length-1; j++)
{
L->elem[j] = L->elem[j + 1];
}
--L->length;
return true;
}
动态线性表删除算法的时间复杂度分析:
问题规模是表的长度,设它的值为 n。
算法的时间主要花费在向前移动元素的 for 循环语句上。该语句的循环次数为 (n – i)。由此可看出,所需移动结点的次数不仅依赖于表的长度 n,而且还与删除位置 i 有关。
当删除位置在表尾 (i = n) 时,不需要移动任何元素;这是最好情况,其时间复杂度 O(1)。
当删除位置在表头 (i = 1) 时,有 n -1 个元素要向前移动,循环语句执行 n -1 次,这是最坏情况其时间复杂度 O(n)。
算法的平均时间复杂度:设 qi 为删除第 i 个元素的概率,则在长度为 n 的线性表中删除一个元素时所需移动元素次数的期望值为:
假设在表中任何位置(1 ,i ,n)删除结点的机会是均等的,则:
由此可见,在顺序表上做删除运算,平均约要移动表上一半元素。当表长 n 较大时,算法的效率相当低。算法的平均时间复杂度为 O(n)。
小结
线性表的元素的增加和删除是线性表中最重要也是比较复杂的操作,读者在阅读的时候可以详细注意一下顺序表在移动过程中注意的问题。其他的操作函数我们会在整体代码中写出来并且注释其功能,目的,条件,参数,返回值和结果。