目录
一.顺序表的概念
顺序表是数据结构中的线性结构的一员,其表现在逻辑结构上相邻,物理结构上也相邻。换句话说,顺序表就是一组物理地址空间连续的存储单元依次存储数据元素的线性结构,其底层是使用数组的形式进行存储数据元素。
形如:
上图这种形式就是顺序表。
会发现其形式和数组几乎一摸一样,那与数组又有什么区别呢?
可以类比数组是街头的苍蝇馆子,顺序表是高档餐厅,数组能做的操作,顺序表都能实现,并且更加完善。(如何完善,下文解释)
-分类
顺序表分为静态顺序表和动态顺序表。
静态顺序表:使用宏来定义顺序表的大小。
#include<stdio.h>
#define N 7 //顺序表大小为7
typedef int SLDataType; //将int重命名
typedef struct SeqList
{
SLDataType a[N]; //定长数组
int size; //有效元素个数
}SL;
定义size,因为不知道顺序表中有多少空间已经被使用了,有可能全都使用也有可能只使用了几个空间,所以这里要定义一个实际数据个数(有效数据),来判别用了多少个空间,以此好进行对表的操作,比如遍历表等等操作都需要它。
静态顺序表由于将空间大小固定死,导致会有缺陷,空间少了不够用,空间大了,造成浪费。
动态顺序表:使用动态内存开辟来定义顺序表的大小。
#include<stdio.h>
typedef int SLDataType; //将int重命名
typedef struct SeqList
{
SLDataType* a; //指针a,指向的是顺序表,便于动态开辟空间
int size; //有效元素个数
int capacity; //空间容量
}SL;
定义capacity,方便后续扩容顺序表。
为什么上面两种分类都将int 重命名成SLDtataType?
是因为不知道这个顺序表到底要存储什么类型的数据啊,假设这里给固定死类型,那如果后期想要存储其他类型的数据的话,岂不是要将所有含有此数据类型的数据全改了,那肯定麻烦,所以这里重命名一下,要是后期要更改类型,那直接就在修改重命名就行了。
所以说一般情况下,通常是使用动态内存表来定义顺序表大小。
二.顺序表的操作
1.创建顺序表结构
这里我使用动态开辟空间方法来创建顺序表结构
//定义动态顺序表结构
typedef int SLDataType; //int重定义
typedef struct SeqList //struct SeqList重定义
{
SLDataType* arr; //用指针arr指向顺序表,且未确定大小
int size; //有效数据个数
int capacity; //空间容量
}SL;
//或者typedef struct SeqList SL;
2.初始化顺序表
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL; //指针初始化为NULL
ps->size = ps->capacity = 0; //个数及容量初始化成0
}
定义一个初始化顺序表的方法SLInit,参数为传入过来的顺序表的地址(传址调用),用指针接受。
成功初始化:
3.增容
在进行插入,不管是尾插、头插还是任意位置插入的操作时,都要先判断顺序表内容量capacity是否足够,若不足,则要先增大顺序表容量,否则将无法进行正常的插入操作。
那么要怎样判断需要增容,又怎样增容,增容多大?
1)判断是否需要增容
画图示意:
情况一:
当有效数据个数(size)等于容量(capacity)时,就证明需要增容了。
情况二:
当arr为空时,此时arr是一张空表,则相当于没有给arr表赋值,所以还是初始化状态,有size=capacity=0,这时也需要对表进行增容,其实这里表空的情况下就不算是增容,而是给表一个初始空间。
2)怎样增容
我这里使用realloc来进行增容操作,如果传过来地址是一个空指针,则视为上文情况二,对顺序表一个初始空间;如果传过来地址指向的表空间已满,则视为上文情况一,对顺序表进行增容。
一次该增容多大的空间呢?
(1)不能一次少量扩容(扩容频繁),会导致程序效率变低。
(2)不能一次大量扩容,会造成空间浪费。
所以这里推荐成倍扩容:一般都是2倍增长,或者1.5倍增长。
为何是2倍增长,这算是一个概率问题,解释起来比较麻烦,
下面有一篇在C++里对vector扩容的详解,这里的增容是类似的道理。
链接: https://blog.csdn.net/qq_37535749/article/details/113489917
//增容
void SLChcekCapacity(SL* ps)
{
assert(ps); //防止ps为NULL
//判断顺序表是否需要增容
if (ps->size == ps->capacity)
{
int newcapacity = (ps->capacity == 0 ? 4 : 2 * ps->capacity);
SLDataType* ptr = (SL*)realloc(ps->arr, newcapacity*sizeof(SLDataType));
//注意realloc第二个参数是字节数,是空间个数乘以空间存储数据的类型大小
if (ptr == NULL) //防止realloc增容失败
{
perror("realloc");
exit(1); //退出程序
}
ps->arr = ptr; //将新的空间地址赋值给arr
ps->capacity = newcapacity; //将新的容量大小赋值给原变量
}
}
4.尾插
尾插也就是在表的尾部插入,即在size处插入数据,插入完成之后size大小会加一。
当顺序表为空时(size=0)同样适用,直接在下标为0处直接插入。
//尾插
void SLPush_Back(SL* ps, SLDataType x)
{
assert(ps); //防止ps为NULL
//空间不足的时候 --- 先增容
SLChcekCapacity(ps);
//空间充足的时候 --- 插入
ps->arr[ps->size++] = x;
}
时间复杂度O(1),空间复杂度O(1)。
测试运行:
(1)空间充足时尾插 — 直接尾插
调试观察:
(2)空间不足时进行尾插 — 先增容,再尾插
调试观察:
5.头插
头插既是在表的下标为0处之前插入数据,直接插入是不行的,这样会造成数据覆盖,所以要进行头插,需要先将表中元素整体向后移动一位,这样表的下标为0处就空了出来,再进行插入操作,同样插入完成之后size大小也会加一。
移动顺序是先从表尾开始,依次向后移动,如果顺序调换则会造成数据覆盖。
当顺序表为空时(size=0)同样适用,不移动元素,也和尾插一样,直接在下标为0处直接插入。
//头插
void SLPush_Front(SL* ps, SLDataType x)
{
assert(ps); //ps不能为空
//空间不足的时候 --- 先增容
SLChcekCapacity(ps);
//先将元素整体向后移动一位
int i;
for (i = ps->size - 1; i >= 0; i--)
{
ps->arr[i + 1] = ps->arr[i]; //元素向后移动
}
//再插入
ps->arr[0] = x;
ps->size++;
}
时间复杂度O(n),空间复杂度O(1)。
测试运行:
这里就不再细致的列出每种情况了,和上文尾插是差不多的。
6.尾删
尾删即在表的下标size-1处删除元素,所谓删除,在尾部直接将size减1即可,表循环打印元素时,循环上限就是在被“删除”的位置,不会打印此元素,所以也相当于被“删除”了。同时表删除到空时将不能再被删除。
//尾删
void SLPop_back(SL* ps)
{
assert(ps && ps->size!=0); //ps不能为空,并且size==0就不能再删除了
//直接size向前移动一位即可
--ps->size;
}
时间复杂度O(1),空间复杂度O(1)。
测试运行:
(1)正常情况删除,表内元素非空
(2)表内元素为空,再删除,此时assert断言报错
7.头删
头删就是在表的下标为0处进行删除操作,所谓删除,在头部既是直接数据覆盖,将除开下标为0处的数据,其余数据整体向前移动一位,移动完毕之后size减1。同时表删除到空时将不能再被删除。
移动顺序是先从表头的下一位开始,依次向前移动,如果顺序调换则会造成数据覆盖。
//头删
void SLPop_Front(SL* ps)
{
assert(ps && ps->arr); //ps不能传空,并且表空就不能再继续删除了
//直接将剩余数据整体向前移动一位
int i;
for (i = 0; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
//删除完毕之后size减1
--ps->size;
}
时间复杂度O(n),空间复杂度O(1)。
测试运行:
(1)正常情况删除,表内元素非空
(2)表内元素为空,再删除,此时assert断言报错
8.任意pos位置之前插入数据
当pos在表头位置时,插入即为头插操作;当pos在表中位置时,插入是先将pos位置之后的元素整体向后移动一位,再将空出来的pos位置插入数据;当pos在表尾位置时,插入即为尾插操作。同时pos位置要合理。
//任意pos位置插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps && (pos >= 0 && pos <= ps->size)); //ps不能传空,pos位置要合理
//空间不足时 --- 先增容
SLChcekCapacity(ps);
//空间充足时 --- 先移动,再插入
//移动
int i;
for (i = ps->size - 1; i >= pos; i--)
{
ps->arr[i + 1] = ps->arr[i];
}
//插入
ps->arr[pos] = x;
ps->size++;
}
时间复杂度O(n),空间复杂度O(1)。
测试运行:
(1)pos在下标为0处时 - - - 头插
(2)pos在下标为size处时 - - - 尾插
(3)pos在表中位置时
9.任意pos位置删除数据
当pos在下标为0处时,即为头删操作;当pos在表中位置时,删除是直接将pos位置之后的元素整体向前移动一位,直接覆盖数据,变成“删除”效果;当pos在下标为size-1处时,即为尾删操作。同时pos位置要合理。
//任意pos位置删除数据
void SLErase(SL* ps, int pos)
{
assert(ps && (pos >= 0 && pos < ps->size)); //ps不能传空,pos位置要合理
//覆盖pos位置元素 --- 移动
int i;
for (i = pos; i < ps->size; i++)
{
ps->arr[i] = ps->arr[i + 1];
}
--ps->size;
}
时间复杂度O(n),空间复杂度O(1)。
测试运行:
(1)pos在下标为0处时 - - - 头删
(2)pos在下标为size-1处时 - - - 尾删
(3)pos在表中位置时
10.查找元素
int SLFind(SL* ps, SLDataType x)
{
assert(ps); //ps不能传空
int i;
for (i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return 0; //0代表找到
}
}
}
测试运行:
(1)找到了
(2)没找到
11.打印
//打印
void SLPrint(SL* ps)
{
assert(ps); //ps不能为空
int i;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
printf("\n");
}
12.销毁
//销毁
void SLDestroy(SL* ps)
{
assert(ps); //ps不能为空
if (ps->arr != 0) //如果arr不是空表,先销毁
{
free(ps->arr);
ps->arr = NULL;
}
ps->size = ps->capacity = 0; //size与capacity也要重置为0
}
三.总结
顺序表的底层逻辑是数组,所以它在逻辑结构上相邻,物理结构上也相邻。
实现顺序表的各种操作中,方法中几乎都使用传址调用,形参的改变要影响到实参。
各种操作中,尾部的插入,删除操作时间复杂度为O(1),头部的插入,删除操作时间复杂度为O(n),所以对于顺序表来说,使用尾部进行插入删除操作更便捷。
————————————————————————————————————————-—
版权声明:本文中对vector扩容的详解为博主好吃还得是柚子原创文章,遵循 CC 4.0 BY-SA 版权协议。