什么是顺序表?
概念:顺序表是⽤⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采⽤数组存储。
顺序表和数组有什么区别?
顺序表的底层结构是数组,对数组的封装,实现看常用的增删改查等借口。
分类
顺序表分为 静态顺序表 和 动态顺序表 两种。
1)静态顺序表
概念:使用定长数组存储元素。
//静态顺序表
typedef int SLdatatype;//将int类型定义为SLdatatype方便以后类型的替换
#define N 7
typedef struct SeqList
{
SLdatatype a[N];//定长数组
int size;//有效数据的个数
}SL;
缺点:空间给少了不够用,给多了造成空间的浪费。
2)动态顺序表
//动态顺序表--按需申请
typedef int SLdatatype;//将int类型定义为SLdatatype方便以后替换
typedef struct SeqList
{
SLdatatype* arr;
int capacity;//可用空间的大小
int size;//有效数据的个数
}SL;
动态顺序表的实现
动态顺序表的实现需要依靠三个文件
1.SeqList.h
我们需要对动态顺序表的一系列功能函数进行声明,例如结构体指针的初始化、顺序表的增删插改函数等...
//"SeqList.h"的实现
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int SLdatatype;
//结构体指针
typedef struct SeqList
{
SLdatatype* arr;
int capacity;//可用空间大小
int size;//有效数据大小
}SL;
//初始化
void SLInit(SL* ps);
//销毁
void SLdestroy(SL* ps);
//检查可用空间大小是否足够
void SLcheck(SL* ps);
//尾插
void SLPushBack(SL* ps,SLdatatype x);
//尾删
void SLPushdelete(SL* ps);
//头插
void SLTouBack(SL* ps,SLdatatype x);
//头删
void SLToudelete(SL* ps);
//指定位置插入
void SLZhiBack(SL* ps,SLdatatype x,int pos);
//指定位置删除
void SLZhidelete(SL* ps,int pos);
//顺序表打印
void SLprint(SL* ps);
//查找
int SLFind(SL* ps,SLdatatype x);
对顺序表所要实现的功能声明完后,我们就要进入其中去具体实现这些函数。
2.SeqList.c
在SeqList.c中,我们会将头文件中的函数进行具体的实现。
1)结构体指针的编写
在动态顺序表中,我们要实现的顺序表功能要能够拥有增加、删除、查找、修改等功能。
诸如此类功能我们通过自定义函数进行实现。已知顺序表的底部基础为数组。
SLdatatype* arr;//构建数组指针完成对顺序表的底部构建
并且也要发挥动态顺序表对于内存分配的优点,需要随时计算空间大小是否充足。
int capacity;//表示可用空间大小
int size;//表示有效数据的个数
将两者结合,构建结构体指指针。
通过arr 来构建顺序表基本,靠capacity分析是否需要额外开辟内存空间,size来计算有效数据的个数进行对比。
//动态顺序表--按需申请
typedef int SLdatatype;//将int类型定义为SLdatatype方便以后替换
typedef struct SeqList
{
SLdatatype* arr;
int capacity;//可用空间的大小
int size;//有效数据的个数
}SL;
2)初始化
在初始化之前,我们已经实现了对顺序表结构体指针的代码编写,但没有对其中的内容进行初始化。
在顺序表运行过程中,我们需要通过传递自己创建的结构体指针变量来进行初始化过程,但其中的具体大小尚未明确,所以可以对arr置为NULL。capacity和size置为0;
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
需要注意的是,我们在传递自己的结构体指针变量时,需要进行传址操作,而不是传值操作。传址操作可以通过形参对实参进行修改,从而达到初始化的目的;但是传值操作只是单单对形参进行了修改,而未对实参进行修改,无法达到初始化的作用。
//test.c中的初始化传址操作
#include"SeqList.h"
SL s;//自己构建的结构体指针变量
SLInit(&s);//正确的传址操作√√√
SLInit(s);//传值操作,错错错错错错错错!!!!!!!!!!
3)销毁
销毁操作作为顺序表最后一步操作,也是至关重要的。
已知我们已经完成了对顺序表增删查改等功能的运用,最后需要对动态开辟的内存进行释放。
//销毁
void SLdestroy(SL* ps)
{
if(ps->arr)//等价于 ps->arr!=NULL
{
free(ps->arr);
}
ps->arr = NULL;//将指针置为空,防止成为野指针
ps->capacity = ps->size = 0;
}
4)检查可用空间大小是否足够
在进行增删查改的功能之前,我们需要判断自己开辟的动态内存空间是否足够,以免发生报错。
可用空间大小是否充足,我可以通过比较size与capacity的大小来观察数组是否仍有可用空间。
如果ps->size==ps->capacity,则表示空间已满,但也存在可用空间为0的情况,这时我们需要去确定可用空间大小是否为0,是则开辟一块新的空间,反之则按照增容规律将可用空间大小变为原先的2倍
//判断空间是否充足
if(ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
}
在此基础上,为了不对自己创建变量进行直接的修改,我们通过创建一个新的数组指针来进行增容操作。
如果利用realloc进行的增容操作失败,则进行报错,并且退出函数;如果成功则对原先的值进行赋值覆盖。
//增容
SLdatatype* tmp = (SLdatatype*)realloc(ps->arr,newCapacity * sizeof(SLdatatype));
if(tmp == NULL)
{
perror("realloc");//如果增容失败则进行报错
exit(1);//退出函数
}
ps->arr = tmp;//将增容完成的函数赋值给原先自己定义的指针
ps->capacity = newCapacity;//增容完成,对capacity的值进行重新赋值
完整的检查函数:
//检查空间大小是否充足
void SLcheck(SL* ps)
{
//判断空间大小是否充足?
if(ps->size == ps->capacity)
{
int newCapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLdatacity* tmp = (SLdatatype*)realloc(ps->arr,newCapacity * sizeof(SLdatacity));
if(tmp == NULL)
{
perror("realloc");
exit(1);
}
ps->arr = tmp;
ps->capacity = newCapacity;
}
}
5)顺序表打印
所有操作完成以后,可以通过顺序表打印来对自己的顺序表操作进行一个反馈。
构建一个for循环,遍历数组,打印arr中的所有内容(有效数据size)
//顺序表打印
void SLprint(SL* ps)
{
for(int i = 0;i < ps->size;i++)
{
printf("%d ",ps->arr[i]);//在对顺序表进行操作以后,打印arr中的所有数据
}
printf("\n");
}
6)尾部插入
在数组尾部进行输入插入的功能,一次只能插入一个数据。
通过对数组进行解引用操作找到数组尾部,并且赋值。同时,数组有效数据个数增加,需要记录。
注意事项:1.需要检查数组可用空间大小是否足够,应用SLcheck函数进行检查。
2.对数组进行解引用操作前,需要考虑数组是否为空,通过assert及时进行断言。
//尾部插入
void SLPushBack(SL* ps,SLdatatype x)
{
assert(ps);//断言操作,防止因对野指针进行解引用操作而发生非法访问
SLcheck(ps);//检查可用空间是否足够,不足则可以进行增容
ps->arr[ps->size] = x;//在尾部赋值自己想要赋值的数据
ps->size++;//赋值成功,有效数据增加,进行记录
}
7)尾部删除
删除数据尾部的一个有效数据,一次只能删除一个数据。
注意事项:1.数组进行解引用操作前,需要考虑数组是否为空,通过assert及时进行断言。
2.需要检查数组是否具有有效数据个数,如果没有有效数据,也需要进行断言。
assert(ps);
assert(ps->size);//有效数据个数断言,如果没有则进行报错提示
size表示数组有效数据个数,size所在位置为数组有效数据个数下标+1,size-1则可以从尾部删除一个有效数据。
//尾部删除
void SLPushdelete(SL* ps)
{
assert(ps);
assert(ps->size);
ps->size--;//尾部删除数据操作
}
8)头部插入
在数组下标为0的位置插入一个数据,其他所有数据向前移动一个下标。
注意事项:1.数组进行解引用操作前,需要考虑数组是否为空,通过assert及时进行断言。
2.需要检查数组可用空间大小是否足够,应用SLcheck函数进行检查。
assert(ps);
SLcheck(ps);//检查可用空间大小是否足够
头部插入数据时,所有数据都要向前移动一个下标,如果从下标数字较低的数组数据开始向前移动,则会对之后要移动的数据进行覆盖,则头部插入出错。那么就需要从下标数字最大的数组数据开始向前移动,移动完位置的较大下标数字的数组数据被下标数字小却未移动的数据覆盖则不会发生错误。
头部插入的实现:
//头部插入
void SLTouBack(SL* ps,SLdatatype x)
{
assert(ps);
SLcheck(ps);
for(int i = ps->size;i > 0;i--)
{
ps->arr[i] = ps->arr[i-1];//所有数据从后向前,向前移动一个下标
}
ps->arr[0] = x;//数据头部插入
ps->size++;//有效数据个数增加,记录
}
9)头部删除
在数组下标为0的位置删除一个数据,其他所有数据向后移动一个下标。
注意事项:1.数组进行解引用操作前,需要考虑数组是否为空,通过assert及时进行断言。
2.需要检查数组可用空间大小是否足够,应用SLcheck函数进行检查。
assert(ps);
assert(ps->size);//检查可用空间大小是否足够
头部插入数据时,所有数据都要向后移动一个下标,如果从下标数字较大的数组数据开始向后移动,则会对之后要移动的数据进行覆盖,则头部删除出错。那么就需要从下标数字最小的数组数据开始向后移动,移动完位置的较小下标数字的数组数据被下标数字大却未移动的数据覆盖则不会发生错误。
头部删除的实现:
//头部删除
void SLToudelete(SL* ps)
{
assert(ps);
assert(ps->size);
for(int i = 0;i < ps->size - 1;i++)
{
ps->arr[i] = ps->arr[i+1];//所有数据从前向后,向后移动一个下标
}
ps->size--;//数据头部删除
}
10)指定位置插入
从指定位置的下标插入数据,其前方的数据都要向前移动一个下标。
定义一个for循环,从指定位置下标开始循环,按照8)头部插入的规律,将数据从后向前,向前移动一个单位。注意事项同上。
//指定位置插入
void SLZhiBack(SL* ps,SLdatatype x,int pos)//pos表示指定位置的下标
{
//注意事项
assert(ps);
assert(ps->size);
for(int i = ps->size;i > pos;i--)
{
ps->arr[i] = ps->arr[i-1];//从后方数据开始向后移动,直到到pos位置
}
ps->arr[pos] = x;//指定位置插入数据
ps->size++;//有效数据增加,记录
}
11)指定位置删除
从指定位置的下标插入数据,其前方的数据都要向前移动一个下标。
定义一个for循环,从指定位置下标开始循环,按照9)头部删除的规律,将数据从前向后,向后移动一个单位。注意事项同上。
//指定位置删除
void SLZhidelete(SL* ps,int pos)//pos表示指定位置的下标
{
//注意事项
assert(ps);
assert(pos >= 0 && pos < ps->size);
for(int i = pos;i < ps->size-1;i++)
{
ps->arr[i] = ps->arr[i+1];//从前方数据开始向前移动,直到到pos位置
}
ps->size--;//指定位置删除数据
}
12)查找
在数组中查找是否有自己需要查找的目标数据。
从头开始遍历数组,对比是否具有相同的项,有则返回数字,没有则返回无效下标。
//查找
int SLFind(SL* ps,SLdatatype x)
{
//注意事项
assert(ps);
for(int i = 0;i < ps->size;i++)//遍历数组
{
if(ps->arr[i] == x)//如果找到
{
return i;
}
}
//没有找到,返回一个无效下标
return -1;
}
3.test.c
测试函数,在其中对SeqList.c的一系列功能进行测试,监控是否符合要求。
//测试文件
#include"SeqList.h"
void Seqtest01()
{
SL s;//定义结构体指针
SLInit(&s);//初始化结构体指针
/*SLPushBack(&s,1);
SLPushBack(&s,2);
SLPushBack(&s,3);
SLPushBack(&s,4);*/ //尾部插入1 2 3 4
SLTouBack(&s,4);
SLTouBack(&s,3);
SLTouBack(&s,2);
SLTouBack(&s,1); //头部插入1 2 3 4
SLPushdelete(&s); //尾部删除1 2 3
SLToudelete(&s); //头部删除2 3
SLZhiBack(&s,4,1); //指定位置插入2 4 3
SLZhidelete(&s,1); //指定位置删除2 3
SLprint(&s); //打印顺序表
SLdestroy(&s); //销毁顺序表
}
int main()
{
Seqtest01();
return 0;
}
总结
熟练掌握顺序表的增删查改等功能的实现,有助于我们对c++数据结构的进一步了解。
其中不乏对细节的考虑与独到之处,作为一名初识顺序表的学生来说,我得到了更多的收获。