目录
顺序表中尾插相较于头插不需要挪动数据,所以这里我们先完成尾插
文中的每个函数的代码都提供了,文末也提供了个人的码云链接,希望可以帮助到大家
顺序表
顺序表可以理解为在计算机存储中的一段连续的物理地址依次存储的线性结构,一般都采用数组来进行存储。并且在数组的基础上完成对数据的增删查改。
可以简单的理解为就是数组,并且是从第一个位置开始存储的。
静态or动态
静态顺序表
静态顺序表中首先会开辟一段固定长度的数组用来存储数据,在这个基础上在进行操作。
#define N 7//静态顺序表中固定的数据长度
typedef int SLDataType//对变量的类型进行重命名
typedef struct SList
{
SLDataType arr[N];
int _size;//顺序表中存储的数据长度
}SList;//对结构体SList进行重命名
在这里对所存储的变量类型用typedef进行重命名是非常有必要的,这样便于更改以后在顺序表中存储的数据类型。
这里对于N的定义可以方便我们之后更改一个位置即可
动态顺序表
但是在大多数情况下数据的多少(N)不是可以提前预估了的,太大太小都不合适。这里就可以采用动态顺序表
在动态顺序表中,顺序表可以根据所存储数据的多少进行自动的扩容,即采用动态开辟的数组进行存储数据。
typedef int SLDataType
typedef struct SList
{
SLDataType* a;//建立指针用来指向动态开辟的数组
int _size;//动态顺序表的长度
int _capicity;//用于记录存放的有效数据长度
}SList:
在这里就可以猜想他的一个大致思路,_arr指向的是一个动态开辟的数组用于存放数据,_size是整个动态数组的长度,_capicity 是动态数组空间中有效数据(已经存放数据)的长度。
每存储一个有效数据时_capicity加一个(_capicity++),当_capicity>=_size的时候就需要考虑是否要开辟空间来存储更多的数据
顺序表的实现
顺序表是按照数组一样一个一个挨着存储的。
关于实现顺序表首先应该定义好结构体并完成对结构体的初始化,其次应该考虑我们在加入数据的时候究竟是采用头插还是尾插,但无论是头插尾插,我们都需要对传进函数的顺序表检查他的空间容量,是否满足我们插入数据大小的要求,不满足的话是不是要进行扩容以及如何扩容
那就先从初始化开始吧
初始化
初始化我们可以做什么呢?
看看刚刚定义的结构体,指针a在定义的时候如果没有进行置为NULL是不是就成为了一个野指针了?_size和_capicity如果不置值的话是不是就成为随机值了?
所以我们初始化的时候->
void SeqListInit(SL* ps)//初始化
{
ps->a = NULL;//将指针置为NULL
ps->_size = ps->_capacity = 0;//让指向数据位置和指向存储空间的值为0
}
在进行头插和尾插的时候我们需要检查空间,所以我们先说检查空间,
但是理论上只有写了头插尾插之后,我们才知道自己需要检查什么
检查空间
首先我们应该检查结构体指针是不是为NULL;接着进行容量的检查,是不是需要扩容?扩容多少?如果不满足我们应该怎么办
首先检查指针是否为空,我们可以使用assert来进行判断结构体指针是否为NULL;
其次检查顺序表的容量空间,满了可以考虑直接乘2倍进行扩容,但是我们在初始顺序表的时候_size和_capacity均为0,那应该怎么办呢?这里可以考虑采用条件操作符,如果刚开始_size和_capacity均为0时,我们可以将_capacity置为4,否则置为2倍;在扩容的时候也应该对新开辟的空间进行强制类型转换,这里为什么不用malloc呢?因为当顺序表所在的空间无法进行扩容的时候,就需要重新找新的空间,并把原来的数据拷贝过去,并且释放原来的空间
如果进行了扩容,别忘记对扩容后的数据进行检查。并且将顺序表指针指向开辟的空间
void SLCheckCapacity(SL* ps)//检查空间
{
//检查结构体指针是否为空
assert(ps != NULL);
//检查容量空间,满了扩容
if (ps->_size == ps->_capacity)//空间放满了
{
//利用条件语句
int newCapacity = ps->_capacity == 0 ? 4 : ps->_capacity * 2;//这里的4也没有明确答案
SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SLDataType));//扩容
if (tmp == NULL)//检查一下
{
printf("realloc fail\n");
exit(-1);//直接结束程序
}
ps->a = tmp;
ps->_capacity = newCapacity;
}
}
顺序表中尾插相较于头插不需要挪动数据,所以这里我们先完成尾插
尾插
尾插前应该先对顺序表空间进行检查,这里直接调用检查函数即可
在尾部插入数据的时候,我们可以理解为插入的位置就是有效数据的个数;并且插入数据之后,存放有效数据的_size应当增加
void SLPushBack(SL* ps, SLDataType x)//尾插
{
SLCheckCapacity(ps);//检查空间
ps->a[ps->_size] = x;//在尾部插入数据,下标可以理解为有效数据的个数
ps->_size++;//有效数据个数增加
}
在尾插结束后,我们可以写一个打印函数来打印我们顺序表中的数据,也便于我们后续的检查
打印
打印数据,我们需要检查结构体指针是否合法,同时注意有效数据的个数,避免打印越界
void SLprint(SL* ps)//打印数据
{
assert(ps != NULL);
for (int i = 0; i < ps->_size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
头插
同样,头插时也应先检查空间
但是头插应该注意的是我们需要挪动数据,从后向前依次挪动。这里需要定义一个变量来保存我们挪动的数据的位置。可以采用while语句,当第一个位置的数据被挪动之后,结束挪动。
挪动数据结束后,应该将需要插入的数据插入到第一个位置,并且别忘记将存储有效数据个数的_size增加
void SLPushFront(SL* ps, SLDataType x)//头插
{
SLCheckCapacity(ps);//检查空间
//挪动数据,从后往前挪
int end = ps->_size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
//插入数据
ps->a[0] = x;//放入数据
ps->_size++;
}
头插尾插结束后就该进行头删,尾删,相同的是尾删因为不需要挪动数据,这里先写尾删
尾删
在尾删的时候,我们并不需要将数据真正的置为0,或者将空间free掉,因为我们在定义结构体的过程中,已经定义了真正有效数据的个数为_size,这时候我们将_size减减就好。
size表示的是数据的有效位置,删除数据后容量没有变所以_capacity不用动。可是如果不对_size做检查的话,一直减下去就容易造成越界访问,对后期加入的数据也不方便。
对_size做检查可以用if或者直接使用assert断言(这个就属于个人喜好了,这里我们采用assert断言)
void SLPopBack(SL* ps)//尾删
{
//assert断言检查
assert(ps->size > 0);//为真没事,为假报错
ps->size--;//size--就可以
}
头删
相较于尾删,头删我们需要对数据进行挪动。
头删的时候我们挪动数据就要从前往后移动,直接将后面的数据赋值给他前面的数据即可,这里需要一个判断,并且在挪动数据结束后务必记得将_size减少
void SLPopFront(SL* ps)//头删
{
//检查size
assert(ps->_size > 0);
//挪动数据,从前往后挪
int begin = 1;
while (begin < ps->_size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->_size--;//有效数据个数减一
}
在任意位置进行插入
这里需要注意的是我们不但需要检查结构体指针,并且需要检查数据插入的位置pos是否在顺序表允许范围之内,即0—_size之间,并且紧接着检查顺序表空间。
检查结束后,由于是任意位置插入数据,在插入之前我们应该腾出位置来存放数据,即从后向前挪动数据,一直挪动到pos位置。
挪动结束后,将要插入的数据赋值给这个位置即可,最后别忘记有效数据个数_size增加。
void SLInsert(SL* ps, int pos, SLDataType x)//在某个位置进行插入
{
assert(ps);//检查结构体指针
assert(pos >= 0 && pos <= ps->_size);//检查pos在顺序表允许的范围之内
SLCheckCapacity(ps);//检查空间
//pos开始,从后往前挪
int end = ps->_size;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;//插入
ps->_size++;
}
在任意位置删除数据
在删除位置之前,我们应当对指针和删除位置是否合法进行检查
检查结束之后,同头删一样,我们并不需要对原数据和空间进行操作,只需要将后面的数据赋值给前面的数据,并且将_size减小即可。
void SLErase(SL* ps, int pos)//在某个位置删除
{
assert(ps);
assert(pos >= 0 && pos < ps->_size);//顺便检查了空
//从pos,从前往后挪
int begin = pos;
while (begin < ps->_size-1)
{
ps->a[begin] = ps->a[begin + 1];
++begin;
}
ps->_size--;
}
头插,尾插,头删,尾删结束之后,我们可以增加一个新的操作
即数据的查找和修改操作
查找
在查找操作中,除了需要对结构体指针进行检查之外,我们是否应该考虑出现重复数据的情况,这里出于简便并没有写,有想法的可以试一试
在查找数据时,我们只需要遍历整个顺序表,并且将遍历的值和需要查找的数据进行对比即可,当找到时返回该数据所在的位置,当没有找到时,返回-1
这里只需要置返回值,其余的打印我们交给菜单即可,也便于后续的使用
int SLFind(SL* ps, SLDataType x)//查找
{
assert(ps);
for (int i = 0; i < ps->_size; ++i)
{
if (ps->a[i] == x)
return i;
}
return -1;//没有找到
}
修改
在修改的时候,由于会对顺序表中的数据进行更改,我们需要判断更改的位置是否在有效数据内,避免发生越界的情况
在检查结束后,直接将修改后的数据赋值到原来的位置上即可
如果想要查找某个数据并且将其修改,这里可以配合查找函数共同使用,即将查找函数的返回值给了修改函数即可
int SLModify(SL* ps, int pos, SLDataType x)//修改
{
assert(ps);
assert(pos >= 0 && pos < ps->_size);
ps->a[pos] = x;
}
写在最后
写到这里,顺序表的大致流程也就可以实现了,这时我们可以写个菜单或者更多的使用方法,作者也是新人,能力有限就不一一写了,文章有点长,谢谢各位看到这里,
如果文中有某些错误,希望大家指出,博主会尽快修改,感谢各位斧正
最后这是我码云的链接,如果有需要的话可以自提