线性表(一):顺序表的增删改查及源码
<!--author--Kang-->
<!--time--2020/7/26-->
线性表的顺序存储:
用一组连续的存储单元一次存储线性表的数据元素,通常这种存储结构为线性表的顺序表(Sequential List)。特点时逻辑上相邻的数据元素再物理次序上也是相邻的
a[i]的地址计算(偏移量):数组首地址+i*每个元素大小
是在计算机内存中以数组的形式保存的线性表,是用一组地址连续的存储单元依次存储相同数据类型的数据元素的线性结构。
顺序表的增删改查
一、顺序表的创建
把学号为 1 的学生存放在数组下标为 0 的位置中,接着依次存储即可。所以为了创建一个顺序表,必须要在内存中开辟一块空间,那么第一个数据元素的首地址就变得非常关键;
此外,既然顺序表是容器,那么容器的容量一定要确定,就是最多允许存放多少个数据元素,而数组的长度就是这个容器的最大容量。
另外对于当前容器中已经存放了多少个数据元素也是很重要的。
根据上面的分析,我们发现顺序表有三个必须的属性:
1)实现机制:运用数组来存放数据元素,数组的首地址就是线性表的首地址。
2)最大容量:数组的长度。
3)线性表长度:数组中已经存放的数据元素的个数。
根据这些信息,我们可以定义出线性表的顺序存储的结构体类型了:
#define MAX_SIZE 100//定义顺序表的容量 typedef int ElementType;//表中元素类型,以int为例,给int取别名(也可以是结构体类型) typedef struct seqential_list{ ElementType data[MAX_SIZE];//顺序表的实现核心,使用数组存储 int length;//顺序表长度 }SEQ_LIST; 注意: //typedef int ElementType; 中ElementType为数据元素类型,当元素类型为结构体类型时,也适用 #define MAX_SIZE 100 typedef struct student{ int number; char name[32]; char gennder[4]; }STUDENT; typedef STUDENT ElementType;//此时元素类型为学生结构体类型,并给该类型取别名为ElementType typedef struct seqential_list{ ElementType data[MAX_SIZE];//顺序表的实现核心,使用静态数组分配方式存储元素, /* 使用动态分配方式: */ /* ElementType *data; data = (ElementType *) malloc (sizeof(ElementType)*MAX_SIZE); */ int length;//顺序表长度,表示当前顺序表中存放元素个数(不是数组长度) }
顺序表的创建:
首先时初始化操作,初始化的目的时要将顺序表准备好,以便后续的其他操作,因此一般会在定义了顺序表后调用一次。
使用静态数组实现的顺序表的初始化:
由于数组已经定义完成,而对于顺序表的操作基本思都依赖于其商都,所以只需要在初始化时将长度赋为0即可,甚至于直接在定义该顺序表时直接初始化即可:
//初始化函数: int init_list(SEQ_LIST *L){ L->length = 0; return 1; } //或者定义顺序表时直接初始化化: SEQ_LIST list = {0};
使用动态分配内存的方式实现的顺序表的初始化:
在函数中完成内存分配和清空操作
int init_list(SEQ_LIST *L){ L->data = (ElementType *) malloc(sizeof(ElementType) * MAX_SIZE); if(L->data == null)//内存可能会分配失败 { return 0; } L->length = 0; return 1; }
注意:对于动态分配内存的方式实现的顺序表,最好还需要补充一个操作,即销毁线性表的操作,用来释放程序中申请到的内存空间。
顺序表的其他操作:
获取顺序表的长度:
int gei_length(SEQ_LIST L){ return L.length; }
判断线性表是否为空:
int is_empty(SEQ_LIST L){ return L.length == 0; }
顺序表的遍历:
(实质为数组的循环输出,其终止条件与顺序表长度有关)
void show(SEQ_LIST *L){ for(int j = 0;j<L-length;j++){ printf("%d",L-data[j]);//这里以数组元素为int类型为例,若不是基本类型,需要在这里修改输出内容 if(j<L-length-1){// 为了美观,这里对于前面的数据输出时加一个逗号 printf(","); }else{//最后一个符号用回车符 printf("\n"); } } }
顺序表的插入:
步骤:
1)判断顺序表是否已满,如果满了,操作失败;
2)判断插入位置是否合理,不合理则操作失败;
3)从最后一个数据元素开始向前遍历到第index位置,分别将它们依次向后移动一位;
4)将要插入的元素填入第index位置处;
5)顺序表长度+1;
//插入操作: int insert(SEQ_LIST *L, int i, ElementType e){//传指针,把e插入到顺序表第i个位置,i为下标,实际上要减一处理, //1)判断顺序表是否已满,如果满了,操作失败; if (L->length == MAX_SIZE) { return 0;//0表示失败 } //2)判断插入位置是否合理,不合理则操作失败; if (i<1||i>L->length+1)//合理位置:1<=index<=MAX_SIZE { return 0;//0表示失败 } //3)从最后一个数据元素开始向前遍历到第index位置,分别将它们依次向后移动一位; for (int j =L->length-1;j>=i-1; j--) { L->data[j + 1] = L->data[j];//将下标为j的元素赋值给下标为j+1的元素,完成元素后移一位操作 } //4)将要插入的元素填入第index位置处; L->data[i - 1] = e;//将元素e插入到第i个位置(i-1); //5)顺序表长度 + 1; L->length++; return 1; }
//main函数
int main() { SEQ_LIST list; init_list(&list); /*int rlt = insert(&list,2,1); printf("%d\n", rlt);*///输出为0,说明失败,因为顺序表需要按顺序插入,第一个位置没有插入数据,直接插入第二个位置会失败 insert(&list, 1, 1); insert(&list, 2, 7); insert(&list, 3, 2); insert(&list, 4, 8); insert(&list, 5, 9); insert(&list, 6, 6); insert(&list, 7, 2); insert(&list, 8, 8); insert(&list, 9, 5); insert(&list, 10, 0); show(&list);//输出:1,7,2,8,9,6,2,8,5,0 printf("=======================\n"); //插入已有元素位置 insert(&list, 1, 0); insert(&list, 10, 1);//此时数组长度为11位,前面插入了元素0,再从新数组第十个位置上再插入元素1 show(&list);//输出:【0】,1,7,2,8,9,6,2,8,【1】,5,0 return 0; }
顺序表的删除
步骤:
1)判断顺序表是否为空,如果没有数据,操作失败;
2)判断删除位置是否何理,不合理则操作失败;
3)从被删除的数据开始,依次被后一位置上的数据覆盖,直到最后一个数据
4)顺序表当前长度减一
//删除操作 int Delete(SEQ_LIST *L,int i) { //1)判断顺序表是否为空,如果没有数据,操作失败; if (L->length ==0) { return 0; } //2)判断删除位置是否何理,不合理则操作失败; if (i<1 || i>L->length)//1<=index<=L->length 这里是第i个元素,不是下标 { return 0;//0表示失败 } //3)从被删除的数据开始,依次被后一位置上的数据覆盖,直到最后一个数据 for (int j = i-1; j < L->length-1; j++)//第i个元素下标为i-1,j的取值范围为 i-1 <= j <= L->length-1 { L->data[j] = L->data[j + 1]; } //4)顺序表当前长度减一 L->length--; return 1; }
//main:接在上面插入操作main中调用后面 int rlt = Delete(&list, 6);//输出0表示删除失败,输出1表示删除成功 if (rlt) { printf("删除成功!\n"); show(&list); }else{ printf("删除失败!\n"); }
顺序表的修改:
步骤:
1)判断顺序表是否为空,如果为空,则操作失败;
2)判断修改位置是否合理,不合理则操作失败;
3)将i位置中的数据修改成最新值。
//修改操作 int update(SEQ_LIST *L,int i,ElementType e) { //1)判断顺序表是否为空,如果为空,则操作失败; if (L->length == 0) { return 0; } //2)判断修改位置是否合理,不合理则操作失败; if (i < 1 || i > L->length) { return 0; } //3)将i位置中的数据修改成最新值。 L->data[i - 1] = e; return 1;//修改成功 }
//调用修改 int rlt2 = update(&list, 10, 2); if (rlt2) { printf("修改成功!\n");//0,1,7,2,8,6,2,8,1,【2】,0 show(&list); } else { printf("修改失败!\n"); }
顺序表的查询操作:
步骤:
1)判断顺序表是否为空,如果为空,则操作失败;
2)从第一个数据元素开始向后遍历,比对是否与被查找数相等,直到最后一个数据;
3)如果找到,返回此数据元素在顺序表的位置,否则返回0,代表数据表中无此数据元素
int serch(SEQ_LIST *L,ElementType e) { //1)判断顺序表是否为空,如果为空,则操作失败; if (L->length ==0) { return 0; } //2)从第一个数据元素开始向后遍历,比对是否与被查找数相等,直到最后一个数据; for (int i = 0; i < L->length; i++) { if (L->data[i]==e) { return i+1; } } //3)如果找到,返回此数据元素在顺序表的位置,否则返回0,代表数据表中无此数据元素 return 0; }
//main函数调用: int rlt3 = serch(&list, 7); printf("rlt3所在位置为第%d个元素\n", rlt3);
优化后:
步骤:
1)判断顺序表是否为空,如果为空,则操作失败;
2)从指定位置开始向后遍历,比对是否与被查找数相等,直到最后一个数据;
3)如果找到,返回此数据元素在顺序表的位置,否则返回0,代表数据表中无此数据元素
//方案二:查找操作优化版 int serch2(SEQ_LIST *L, int from,ElementType e) { //1)判断顺序表是否为空,如果为空,则操作失败; if (L->length == 0) { return 0; } //2)从第from个(下标为 from-1)数据元素开始向后遍历,比对是否与被查找数相等,直到最后一个数据; for (int i = from-1; i < L->length; i++) { if (L->data[i] == e) { return i + 1; } } //3)如果找到,返回此数据元素在顺序表的位置,否则返回0,代表数据表中无此数据元素 return 0; }
//main: //查找操作方案二调用: int from = 0; while (1) { from = serch2(&list, from+1, 0); if (from == 0) { break; } printf("匹配到所查元素与顺序表中第%d个元素相同\n",from); }
至此,顺序表的增删改查结束!
顺序表操作的时间复杂度:
插入操作:如果元素刚好插在最后一个位置,那么线性表中数据元素无需移动,此时时间复杂度为O(1),即最好情况。如果元素插在第一个位置,那么所有元素都要向后移动一位,时间复杂度为O(n),即最坏情况。取其平均值,(n+1)/2所以插入的平均时间复杂度为O(n)。
删除、查找与插入操作的时间复杂度相同;
修改操作的时间复杂度,因为基于固定位置无需进行比对或者移动,所以时间复杂度为O(1)
顺序表的优缺点:
顺序表的存储在逻辑上和物理上都是连续的,因为物理空间连续,所以可以快速的存取表里任何数据,但是当需要增加或者减少数据元素时,就需要进行大量的移动操作;此外,线性表底层运用数组实现,一旦数组初始化确定长度后,很难再扩充容量或者需要额外的性能开销。所以线性表的顺序存储有之独特的优势,也有很多无法忍受的缺陷:
优点: ①无需为表示表中元素之间的逻辑关系而增加额外的空间
②可以快速地存取表中任意位置的元素
缺点: ①插入和删除需要大量地移动数据
②顺序表的存储容量难以扩充
P.S.源码:
#include<stdio.h> #include <malloc.h> #define MAX_SIZE 100//定义顺序表的容量 typedef int ElementType;//表中元素类型,以int为例,给int取别名(也可以是结构体类型) typedef struct seqential_list { ElementType data[MAX_SIZE]; //顺序表的实现核心,使用数组存储 int length;//顺序表长度 }SEQ_LIST; //typedef int ElementType; 中ElementType为数据元素类型,当元素类型为结构体类型时,也适用 //#define MAX_SIZE 100 // //typedef struct student { // int number; // char name[32]; // char gennder[4]; //}STUDENT; // //typedef STUDENT ElementType;//此时元素类型为学生结构体类型,并给该类型取别名为ElementType // // //typedef struct seqential_list { // ElementType data[MAX_SIZE];//顺序表的实现核心,使用静态数组分配方式存储元素, // /* // 使用动态分配方式: // */ // //ElementType *data; //data = (ElementType *)malloc(sizeof(ElementType)*MAX_SIZE); // //int length;//顺序表长度,表示当前顺序表中存放元素个数(不是数组长度) //}SEQ_lIST; // // //初始化函数: int init_list(SEQ_LIST *L) { L->length = 0; return 1; } //或者定义顺序表时直接初始化化: //SEQ_LIST list = { 0 }; //获取长度 int gei_length(SEQ_LIST L) { return L.length; } //判断是否为空 int is_empty(SEQ_LIST L) { return L.length == 0; } //顺序表遍历: void show(SEQ_LIST *L) { for (int j = 0; j < L->length; j++) { printf("%d", L->data[j]);//这里以数组元素为int类型为例,若不是基本类型,需要在这里修改输出内容 if (j < L->length - 1) {// 为了美观,这里对于前面的数据输出时加一个逗号 printf(","); } else {//最后一个符号用回车符 printf("\n"); } } } //插入操作: int insert(SEQ_LIST *L, int i, ElementType e){//在原数组上修改要传指针,把e插入到顺序表第i个位置,i为下标,实际上要减一处理, //1)判断顺序表是否已满,如果满了,操作失败; if (L->length == MAX_SIZE) { return 0;//0表示失败 } //2)判断插入位置是否合理,不合理则操作失败; if (i<1||i>L->length+1)//合理位置:1<=index<=L->length+1 ①这里是第i个元素,不是下标 ② 插入到最后一个元素后面则i为 length+1 { return 0;//0表示失败 } //3)从最后一个数据元素开始向前遍历到第index位置,分别将它们依次向后移动一位; for (int j =L->length-1;j>=i-1; j--) { L->data[j + 1] = L->data[j];//将下标为j的元素赋值给下标为j+1的元素,完成元素后移一位操作 } //4)将要插入的元素填入第index位置处; L->data[i - 1] = e;//将元素e插入到第i个位置(i-1); //5)顺序表长度 + 1; L->length++; return 1; } //删除操作 int Delete(SEQ_LIST *L,int i) { //1)判断顺序表是否为空,如果没有数据,操作失败; if (L->length ==0) { return 0; } //2)判断删除位置是否何理,不合理则操作失败; if (i<1 || i>L->length)//1<=index<=L->length 这里是第i个元素,不是下标 { return 0;//0表示失败 } //3)从被删除的数据开始,依次被后一位置上的数据覆盖,直到最后一个数据 for (int j = i-1; j < L->length-1; j++)//第i个元素下标为i-1,j的取值范围为 i-1 <= j <= L->length-1 { L->data[j] = L->data[j + 1]; } //4)顺序表当前长度减一 L->length--; return 1; } //修改操作 int update(SEQ_LIST *L,int i,ElementType e) { //1)判断顺序表是否为空,如果为空,则操作失败; if (L->length == 0) { return 0; } //2)判断修改位置是否合理,不合理则操作失败; if (i < 1 || i > L->length) { return 0; } //3)将i位置中的数据修改成最新值。 L->data[i - 1] = e; return 1;//修改成功 } //查找操作 //方案一:所查找的元素无重复项,该方法只能查找第一个与所查元素相同的位置,后面重复的位置无法查找 int serch(SEQ_LIST *L,ElementType e) { //1)判断顺序表是否为空,如果为空,则操作失败; if (L->length ==0) { return 0; } //2)从第一个数据元素开始向后遍历,比对是否与被查找数相等,直到最后一个数据; for (int i = 0; i < L->length; i++) { if (L->data[i]==e) { return i+1; } } //3)如果找到,返回此数据元素在顺序表的位置,否则返回0,代表数据表中无此数据元素 return 0; } //方案二:查找操作优化版 int serch2(SEQ_LIST *L, int from,ElementType e) { //1)判断顺序表是否为空,如果为空,则操作失败; if (L->length == 0) { return 0; } //2)从第from个(下标为 from-1)数据元素开始向后遍历,比对是否与被查找数相等,直到最后一个数据; for (int i = from-1; i < L->length; i++) { if (L->data[i] == e) { return i + 1; } } //3)如果找到,返回此数据元素在顺序表的位置,否则返回0,代表数据表中无此数据元素 return 0; } int main() { SEQ_LIST list; init_list(&list); /*int rlt = insert(&list,2,1); printf("%d\n", rlt);*///输出为0,说明失败,因为顺序表需要按顺序插入,第一个位置没有插入数据,直接插入第二个位置会失败 insert(&list, 1, 1); insert(&list, 2, 7); insert(&list, 3, 2); insert(&list, 4, 8); insert(&list, 5, 9); insert(&list, 6, 6); insert(&list, 7, 2); insert(&list, 8, 8); insert(&list, 9, 5); insert(&list, 10, 0); show(&list);//输出:1,7,2,8,9,6,2,8,5,0 printf("=======================\n"); //插入已有元素位置 insert(&list, 1, 0); insert(&list, 10, 1);//此时数组长度为11位,前面插入了元素0,再从新数组第十个位置上再插入元素1 show(&list);//输出:【0】,1,7,2,8,9,6,2,8,【1】,5,0 //调用删除 int rlt1 = Delete(&list, 6);//输出0表示删除失败,输出1表示删除成功 if (rlt1) { printf("删除成功!\n"); show(&list); }else{ printf("删除失败!\n"); } //调用修改 int rlt2 = update(&list, 10, 2); if (rlt2) { printf("修改成功!\n");//0,1,7,2,8,6,2,8,1,【2】,0 show(&list); } else { printf("修改失败!\n"); } //调用查找操作: int rlt3 = serch(&list, 7); printf("rlt3所在位置为第%d个元素\n", rlt3); //查找操作方案二调用: int from = 0; while (1) { from = serch2(&list, from+1, 0); if (from == 0) { break; } printf("匹配到所查元素与顺序表中第%d个元素相同\n",from); } return 0; }