顺序表概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
在这里,我们要特别注意红色标记,它和数组是有区别的,一定是依次存储数据元素!
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储元素。
- 动态顺序表:使用动态开辟的数组存储。
静态顺序表
首先,我们看一下静态顺序表结构:
#define MAX 100
typedef int SLDataType;
typedef struct SeqList
{
SLDataType arr[MAX];
size_t size;//有效数据的个数
}SeqList;
静态顺序表的初始化我们注意一下:
void SeqListInit(SeqList* ps)
{
assert(ps);
memset(ps->arr, 0, sizeof(SLDataType) * MAX);//初始化数组
ps->size = 0;
}
在这里,初始化数组我们用memset函数来初始化,这是要注意的一点,其它的接口函数都非常简单,下面我们重点讲解一下动态顺序表。
动态顺序表
动态顺序表的结构是什么样的?请往下看:
动态顺序表的结构
了解了动态顺序表的结构,下面我们来实现顺序表的基本接口:
初始化
使用顺序表时,一定要初始化,不然会出现错误。
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->arr = NULL;
ps->size = 0;
ps->capacity = 0;
}
在这里也可以有其它的初始化方式,可以一开始给它一个空间,都是可以的,我们这里为什么是0,下面再说。
检查扩容
当数据满的时候,我们就需要扩容,在很多地方都需要用到,所以我们就把它单独写个函数。
我们先看下面的代码:
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
// 如果满了,我们要扩容
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->arr, sizeof(SLDataType)*ps->capacity*2);
if (tmp == NULL)
{
printf("realloc fail\n");
exit(-1);
}
else
{
ps->arr = tmp;
ps->capacity*=2;
}
}
}
这个代码是有问题的,因为在初始化的时候,我们将capacity初始化为0,这里乘2,还是0。
我们应该这样去写:
这里,我们扩容前先判断一下,capacity是否为0,如果为0,我们就随便赋一个值,如果不为0,我们乘2倍(随便乘几,2倍最合适,因为扩少了,会频繁的扩,增加开销,扩大了,容易浪费)。
ps->arr为NULL,我们不必担心,因为realloc函数的第一个参数如果为NULL,realloc的作用和malloc一样,开辟空间。
如果开辟失败,我们就exit(-1),强制结束,因为都开辟失败了,也就没法进行下面的内容,所以这里用exit而不是return。
尾插函数
void SeqListPushBack(SL* ps, SLDataType x)
{
SeqListCheckCapacity(ps);//检查增容
ps->arr[ps->size] = x;
ps->size++;
}
尾插很简单,因为数组下标是从0开始的,所以ps->size就是最后一个元素下一个位置。插入后,有效数据加1。
头插函数
头插函数比尾插复杂一点,因为在表头插入,表中的数据都要往后挪动一位。假如我们将4插入表头:
我们需要将1,2,3向后移动:
将4放进去:
void SeqListPushFront(SL* ps, SLDataType x)
{
SeqListCheckCapacity(ps);//检查增容
int end = ps->size - 1;
while (end >= 0)
{
ps->arr[end + 1] = ps->arr[end];
end--;
}
ps->arr[0] = x;
ps->size++;
}
尾删函数
尾删函数最简单,直接ps->size–,这样就会认为有效数据少一个:
void SeqListPopBack(SL* ps)
{
//判断顺序表是否为空
if (ps->size == 0)
{
printf("size is NULL\n");
return;
}
ps->size--;
}
头删函数
头删我们只需要将后面的数据向前覆盖:
void SeqListPopFront(SL* ps)
{
if (ps->size == 0)
{
printf("size is NULL\n");
return;
}
int end = 0;
while (end <ps->size-1)
{
ps->arr[end] = ps->arr[end + 1];
end++;
}
ps->size--;
}
在pos位置插入x
假设我们要将3插入到下标为1的地方:
所以,我们要将下标为1,2,3的数据向后移动,然后将3放进去:
这里这样写是有问题的:
当pos为0,也就是头插时,ps->size为0,end为-1,因为pos为size_t,end>=pos会发生整型提升,就会出现死循环。
那么我们该如何解决呢?
方法一:
把pos的类型改为int,虽然能解决问题,但有时候库里的函数实现就是size_t。
方法二:
在end>=pos时,把pos强制转换成int
方法三:
我们上面写的函数end是指向最后一个元素的:
我们把end指向最后一个元素下一个位置:
size_t end = ps->size;
while (end >pos)
{
ps->arr[end] = ps->arr[end-1];
--end;
}
我们把end前一个元素给end,当end为1时就把下标为0的元素移动到后面了。
删除pos位置的数据
这里就很简单了,代码如下:
void SeqListErase(SeqList* psl, size_t pos)
{
assert(psl);
assert(pos < psl->size);
size_t begin = pos + 1;
while (begin < psl->size)
{
psl->arr[begin - 1] = psl->arr[begin];
++begin;
}
psl->size--;
}
这里就不会出现上面的问题,因为begin为pos+1,不会出现小于0情况。
销毁函数
在不用顺序表的时候记得销毁,因为有一些越界报错只有在销毁时才能被编译器检查到,这时候一般是越界问题。
void SeqListDestroy(SeqList* ps)
{
assert(ps);
free(ps->arr);
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
记得把arr置为NULL,否则会出现野指针。
顺序表的优缺点:
优点:连续物理空间,方便下标随机访问。
缺点:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
总结:
到这里,动态顺序表基本结束了,还有查找和打印,这两个比较简单就不写了。如果大家认为我有哪些不足之处或者知识上的错误都可以告诉我,我会在之后的文章中不断改正,也请大家多多包涵。如果大家觉得这篇文章有用的话,也希望大家可以给我关注点赞,你们的支持就是对我最大的鼓励,我们下一篇文章再见。