顺序表是基于数组来实现的,它属于线性表的一种,而顺序表又分为“静态顺序表”和“动态顺序表”。今天我们讲解的是动态顺序表。
一、顺序表实现
实现顺序表第一步是创建顺序表的结构,它是基于一个数组,我们要管理和修改这个数组,就必须添加一些管理数组的变量,这里用结构体来管理,结构体的内容如下:
//设置顺序表内容相关数据类型名重定向
typedef int TypeofSeqList;
//定义顺序表结构体类型,并类型重定向为 SeqList
typedef struct SeqList
{
TypeofSeqList* Arr;
int Size;
int DataCap;
}SeqList;
这里第一个内容是一个指针后期可以用动态内存管理来扩容,我们对变量类型名字进行了重定义,方便后期做一些改动;第二个变量用来储存顺序表的可用数据;第三个变量用来储存已经开辟的,可以储存的最大数据的数量,DataCap就像一口缸,而Size就是缸的实际水位。
有了这个基础,让我们来编写顺序表的函数
1,顺序表初始化函数
//初始化顺序表
void SeqListInit(SeqList* SL)
{
SL->Arr = NULL;
SL->Size = 0;
SL->DataCap = 0;
}
函数的形参是一个SeqList类型的指针,用来接收创建的顺序表,下面三步的作用就是将顺序表里面的指针指向一个有效的地址,然后将其他变量初始化为0。
2,顺序表的销毁函数
//销毁顺序表
void SLDst(SeqList* SL)
{
SL->DataCap = 0;
SL->Size = 0;
free(SL->Arr);
SL->Arr = NULL;
}
顺序表的销毁和初始化有着相似之处,不同的是需要将动态申请的内存归还给操作系统,并且将指针指向NULL用来防止野指针。
3,检查是否需要扩容
//检查是否需要扩容
void CheckLen(SeqList* SL)
{
if (SL->DataCap == SL->Size)
{
int NewDataCap = SL->DataCap == 0 ? 10 : SL->DataCap * 2;
TypeofSeqList* newsl = (TypeofSeqList*)realloc(SL->Arr, NewDataCap * sizeof(TypeofSeqList));
if (newsl != NULL)
{
SL->Arr = newsl;
newsl = NULL;
SL->DataCap = NewDataCap;
}
else
{
perror("realloc");
exit(1);
}
}
}
在这里,这个函数十分重要,在进行数据写入操作时,我们必须保证可用空间足够,也就是DataCap所保存的数据,如果空间不够,将会引起意想不到的意外。
上边提到过Size就是实际水位,DataCap就是水缸的大小,当DataCap等于Size的时候就证明水缸已经满了,需要我们进行扩容。
int NewDataCap = SL->DataCap == 0 ? 10 : SL->DataCap * 2;
TypeofSeqList* newsl = (TypeofSeqList*)realloc(SL->Arr, NewDataCap * sizeof(TypeofSeqList));
在初始化时候,DataCap是等于0的,如果直接写
TypeofSeqList* newsl = (TypeofSeqList*)realloc(SL->Arr, SL->DataCap * sizeof(TypeofSeqList));
那么式子“SL->DataCap * sizeof(TypeofSeqList)”将会等于0,在以后便不再等于0,所以我们在此之前需要判断DataCap是否为0,于是便用语句”int NewDataCap = SL->DataCap == 0 ? 10 : SL->DataCap * 2;“来实现DataCap的初值。由于realloc函数有开辟内存失败的可能,如果用SL->Arr去直接接收,就可能导致已有数据丢失,所以需要创建临时变量来储存realloc返回的地址,如果返回的地址不为NULL,那么再讲返回的地址赋值给SL->Arr即可。如果开辟失败,就直接退出程序。
4,顺序表的尾插
//数据尾插
void SLPushBack(SeqList* SL, TypeofSeqList Data)
{
assert(SL);
CheckLen(SL);
SL->Arr[SL->Size++] = Data;
}
这里为尾部插入数据函数,因为是插入数据,所以先检查是否需要扩容,检查以后便可以写入数据,从尾部写入数据后,Size应该加1,这里方便起见写成“SL->Size++”。
5,顺序表的头插
//数据头插
void SLPushHead(SeqList* SL, TypeofSeqList Data)
{
assert(SL);
CheckLen(SL);
for (int i = SL->Size;i > 0; i--)
{
SL->Arr[i] = SL->Arr[i - 1];
}
SL->Arr[0] = Data;
SL->Size++;
}
头插同尾插原理一样,只不过需要将数据整体后移一个单位,再插入数据
后移原理如下:
从后向前移动可以防止有效数据的覆盖,这里如果想为方便的话可以使用memmove函数来移动。
同样的,插入数据以后Size需要加1.
6,顺序表头删
//数据头删
void SLDeleHead(SeqList* SL)
{
assert(SL);
if (SL->Size == 0)
{
return;
}
for (int i = 0; i < SL->Size-1; i++)
{
SL->Arr[i] = SL->Arr[i+1];
}
SL->Size--;
}
对于删除数据来说,涉及到Size的减1,因为数组元素是没有负数的,所以我们要先判断Size的有效性,然后再进行头删操作,这个原理和头插一样,原理如下
删除之后Size需要减1.
7,顺序表尾删
void SLDeleBack(SeqList* SL)
{
assert(SL);
if (SL->Size == 0)
{
return;
}
SL->Size--;
}
这里依然是判断Size的有效性,然后删除尾部数据,由于数组一定是有值的,所以只需更改Size的大小,因为我们读取数据只能从0到Size的范围读取。
8,顺序表指定位置插入数据
//指定位置插入数据
void SLAssiWrite(SeqList* SL, TypeofSeqList Data, int POS)
{
assert(SL);
for (int i = SL->Size;i>= POS; i--)
{
SL->Arr[i] = SL->Arr[i - 1];
}
SL->Arr[POS - 1] = Data;
SL->Size++;
}
指定位置插入数据需要接收指定的位置,把这个位置及其以后得数据后移,然后再在这个位置插入数据,比如我要在第二个位置插入数据,原理如下:
最后的Size依然需要加1。
9,顺序表指定位置删除数据
//指定位置删除数据
void SLAssiDele(SeqList* SL, size_t POS)
{
assert(SL);
for (int i = POS;i< SL->Size;i++ )
{
SL->Arr[i - 1] = SL->Arr[i];
}
SL->Size--;
}
我们在指定位置插入数据的基础上,可以更容易理解指定位置删除数据,它的方法是将指定位置之后的数据整体前移,覆盖掉删除位置的数据,达到删除的目的,我以删除第二个数据为例,原理如下:
最后Size需要减1。