顺序表(Sequential List)是线性表的顺序表示。其特点是:逻辑上相邻的数据元素,其物理位置也是相邻的。所以线性表可以实现随机存取。
数组类型同样可以实现随机存取,所以可以用来描述顺序表。
1.定义
顺序表的核心是用一段连续的内存空间(数组)来存储数据,所以我们需要定义一个结构体来管理这段空间,结构体里至少要包含:
- 存储数据的数组(用来存具体元素)。
- 当前顺序表中元素的个数(长度)。
// 定义顺序表的最大容量
#define MAX_SIZE 100
// 定义顺序表结构体
typedef struct{
int data[MAX_SIZE]; // 用数组存储元素(这里以int类型为例)
int length; // 当前顺序表中元素的个数(长度)
} SqList; // 给结构体起个别名SqList,方便使用
上面的方式虽然能够实现顺序表,但是存在局限性,data[MAX_SIZE]会创建一个大小固定的数组,如果我的顺序表里最多只有10个元素,它也会创建一个100容量的数组,造成空间的浪费。我们需要一种能够动态的根据我们所需要的大小来分配内存的实现方式。请看下面:
// 定义动态顺序表结构体
typedef struct{
int* data; // 指向后续我们创建的顺序表的首地址,动态数组
int length; // 当前元素个数(长度)
int maxSize;// 当前顺序表的容量(最多能存多少元素,可动态调整)
} SqList;
两段代码进行对比,我们可以看到第二段代码并没有直接声明一个数组,而是给定了一个指针,这个指针将来会指向我们后续创建的顺序表的首地址,至于它的大小我们还没有进行分配。知道了这一点,下面我们展示完整的定义代码:
#include <stdio.h> //基本输入输出库
#include <stdlib.h> //内存操作相关库
//定义状态码,让后续函数返回更加直观
#define OK 1 //操作成功
#define ERROR 0 //操作失败
#define OVERFLOW -1 //内存溢出
typedef int Status; //给int取个别名,int=Status,后续函数返回结果用Status,实际就是int
//定义元素类型
typedef int ElemType;//顺序表的元素类型,方便统一修改,int可以换成float,char等等
//核心定义
typedef struct{
ElemType *elem; //这里就不用int*了,而是根据我们前面定义的数据类型来
int maxSize; //最大存储元素的个数
int length; //当前存储元素的个数
}SqList;
2.初始化
上面我们定义了一个顺序表,但是还没有进行初始化,这就会导致elem现在仍是一个野指针,maxSize和length里面装的是随机值。这显然与我们的需求不符,我们需要的应该是一个elem指向一片连续内存空间的首地址,length=0的空表。
//初始化顺序表
Status InitList(SqList *L,int size){
//初始化顺序表的长度至少为1,否则没有意义
if(size<=0)
return ERROR;
//*elem现在还是一个野指针,我们需要为它申请一片连续的内存空间
//申请空间的大小=单个元素所需空间*最大容纳个数,malloc默认返回void*
L->elem = (ElemType*)malloc(size*sizeof(ElemType)); //L->elem = (*L).elem
//加一个判断,以防没有申请到空间出错
if(L->elem == NULL)
return OVERFLOW;
L->maxSize = size; //最大容纳元素个数
L->length = 0; //初始化时里面元素为0
return OK;
}
在main()函数里初始化:
int main(){
SqList list;
if(InitList(&list,10))
printf("初始化成功,当前顺序表最大容量:%d,当前元素个数:%d",list.maxSize,list.length);
return 0;
}
这里需要注意我们在InitList()函数里,传递的是SqList *L,并不是SqList L。如果传递SqList L的话,在函数执行时会产生一个SqList L副本,在函数结束后就会销毁,对原本的SqList L没有任何影响,参考形参和实参,所以我们只能传递L表的地址(指针)。
3.插入元素
顺序表的插入是指在表中第i个位置插入一个新的数据元素,i之后的所有元素则需要往后移一位,如果i=length+1就是在当前线性表末尾插入,则不需要移动元素。
插入元素步骤:
- 判断i值是否合法(1<=i<=L->length+1)
- 检查存储空间是否够,不满足则需要扩容
- 将插入位置及之后的元素后移
- 把新插入的元素放在i位置
- 表长加1
根据上面的步骤,我们来完成顺序表插入代码:
//插入元素
Status ListInsert(SqList *L,int i,ElemType e){
//1.判断i值是否合法
if(i<1 || i>L->length+1)
return ERROR;
//2.判断空间是否需要扩容
if(L->length == L->maxSize){
// 扩容策略:新容量设为原来的2倍(可以根据需求修改)
int newMaxSize = L->maxSize * 2;
//这里可能会返回NULL,所以不能直接赋值给elem
ElemType *newElem = (ElemType*)realloc(L->elem,newMaxSize*sizeof(ElemType));
//判断扩容是否失败
if(newElem==NULL)
return OVERFLOW;
L->elem = newElem;
L->maxSize = newMaxSize; // 更新最大容量
}
//3.把i位置后面的元素依次后移
//如果顺序表可以插入元素说明还存在空位,我们从后面开始移动可以不使用中间变量
for(int j=L->length;j>i-1;j--){
L->elem[j]=L->elem[j-1];
}
//4.把新插入的元素放在i位置,对应下标i-1
L->elem[i-1] = e;
//5.表长加1
L->length++;
return OK;
}
时间复杂度我们可以直接考虑最坏情况,假设现在表中有n个元素,我们需要在第一个位置插入,则需要移动n个元素,时间复杂度O(n).
4.取值
取值操作是根据指定序号值i,去表中查找相应位置的元素。
//顺序表取值
Status GetElem(SqList L,int i,ElemType *e){ //e是用于存储结果的指针
//判断i值合法性
if(i<1 || i>L.length)
return ERROR;
*e = L.elem[i-1];
return OK;
}
这里我们不需要修改顺序表,所以不需要传指针。
测试:
int main(){
SqList list;
if(InitList(&list,5))
printf("初始化成功,当前顺序表最大容量:%d,当前元素个数:%d\n",list.maxSize,list.length);
if(ListInsert(&list,1,7))
printf("插入成功\n");
ElemType e;
if(GetElem(list,1,&e))
printf("第一个元素:%d",e);
return 0;
}
顺序表可以通过下标直接定位到元素,显然时间复杂度为O(1).
5.删除元素
删除顺序表 L 中第 i 个位置( i,1 <= i <= L->length)的元素,并将删除的元素通过 e 指针带出。删除后,后续元素依次前移一位,顺序表长度减 1。
- 判断i的合法性
- 保存删除的值
- 移动元素,将i之后的元素全部向前移一位
- 更新顺序表的长度
//删除元素
Status ListDelete(SqList *L,int i,ElemType *e){
//1.判断i的合法性
if(i<=0 || i>L->length)
return ERROR;
//2.保存删除的元素
*e = L->elem[i-1];
//3.移动元素
for(int j=i;j<L->length;j++){
L->elem[j-1]=L->elem[j];
}
//4.更新顺序表长度
L->length--;
return OK;
}
测试:
int main(){
SqList list;
if(InitList(&list,5))
printf("初始化成功,当前顺序表最大容量:%d,当前元素个数:%d\n",list.maxSize,list.length);
if(ListInsert(&list,1,7))
printf("插入成功,当前元素个数:%d\n",list.length);
ElemType e;
if(GetElem(list,1,&e))
printf("第一个元素:%d\n",e);
if(ListDelete(&list,1,&e))
printf("删除成功:%d,当前元素个数:%d",e,list.length);
return 0;
}
时间复杂度我们考虑最坏情况,假如顺序表有n个元素,我们删除第一个元素,那么需要移动n-1个元素,时间复杂度为O(n-1)=O(n).
6.附录
完整代码如下:
#include <stdio.h> //基本输入输出库
#include <stdlib.h> //内存操作相关库
//定义状态码,让后续函数返回更加直观
#define OK 1 //操作成功
#define ERROR 0 //操作失败
#define OVERFLOW -1 //内存溢出
typedef int Status; //给int取个别名,int=Status,后续函数返回结果用Status,实际就是int
//定义元素类型
typedef int ElemType;//顺序表的元素类型,方便统一修改,int可以换成float,char等等
//核心定义
typedef struct{
ElemType *elem; //这里就不用int*了,而是根据我们前面定义的数据类型来
int maxSize; //最大存储元素的个数
int length; //当前存储元素的个数
}SqList;
//初始化顺序表
Status InitList(SqList *L,int size){
//初始化顺序表的长度至少为1,否则没有意义
if(size<=0)
return ERROR;
//*elem现在还是一个野指针,我们需要为它申请一片连续的内存空间
L->elem = (ElemType*)malloc(size*sizeof(ElemType));
//加一个判断,以防没有申请到空间出错
if(L->elem == NULL)
return OVERFLOW;
L->maxSize = size; //最大容纳元素个数
L->length = 0; //初始化时里面元素为0
return OK;
}
//插入元素
Status ListInsert(SqList *L,int i,ElemType e){
//1.判断i值是否合法
if(i<1 || i>L->length+1)
return ERROR;
//2.判断空间是否需要扩容
if(L->length == L->maxSize){
// 扩容策略:新容量设为原来的2倍(可以根据需求修改)
int newMaxSize = L->maxSize * 2;
//这里可能会返回NULL,所以不能直接赋值给elem
ElemType *newElem = (ElemType*)realloc(L->elem,newMaxSize*sizeof(ElemType));
//判断扩容是否失败
if(newElem==NULL)
return OVERFLOW;
L->elem = newElem;
L->maxSize = newMaxSize; // 更新最大容量
}
//3.把i位置后面的元素依次后移
//如果顺序表可以插入元素说明还存在空位,我们从后面开始移动可以不使用中间变量
for(int j=L->length;j>i-1;j--){
L->elem[j]=L->elem[j-1];
}
//4.把新插入的元素放在i位置,对应下标i-1
L->elem[i-1] = e;
//5.表长加1
L->length++;
return OK;
}
//顺序表取值
Status GetElem(SqList L,int i,ElemType *e){
//判断i值合法性
if(i<1 || i>L.length)
return ERROR;
*e = L.elem[i-1];
return OK;
}
//删除元素
Status ListDelete(SqList *L,int i,ElemType *e){
//1.判断i的合法性
if(i<=0 || i>L->length)
return ERROR;
//2.保存删除的元素
*e = L->elem[i-1];
//3.移动元素
for(int j=i;j<L->length;j++){
L->elem[j-1]=L->elem[j];
}
//4.更新顺序表长度
L->length--;
return OK;
}
int main(){
SqList list;
if(InitList(&list,5))
printf("初始化成功,当前顺序表最大容量:%d,当前元素个数:%d\n",list.maxSize,list.length);
if(ListInsert(&list,1,7))
printf("插入成功,当前元素个数:%d\n",list.length);
ElemType e;
if(GetElem(list,1,&e))
printf("第一个元素:%d\n",e);
if(ListDelete(&list,1,&e))
printf("删除成功:%d,当前元素个数:%d",e,list.length);
return 0;
}