目录
1.顺序表概念:
顺序表本质上就是数组(且要求数据是必须连续存储的,不能有跳跃间隔),简单来讲顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可分为:
-
静态顺序表:使用定长数组存储元素。
-
动态顺序表:使用动态开辟的数组存储。
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间 大小。
2.顺序表实现:
所以下面分区快实现动态顺序表的增删查改,首先是头文件:
在定义类型的时候我们可以直接用typedef去重命名,这样到时候需要更改顺序表内存储的数据类型时直接更改typedef就可以了。
typedef int SLDataType; // 定义类型
typedef struct SeqList
{
SLDataType* a;
int size; // 有效数据个数
int capacity; // 空间容量
}SL;
// 初始化、删除顺序表
void SLInit(SL* psl);
void SLDestroy(SL* psl);
// 打印扩容
void SLPrint(SL* psl);
void SLCheckCapacity(SL* psl);
// 头插尾插
void SLPushBack(SL* psl, SLDataType x);
void SLPushFront(SL* psl, SLDataType x);
// 头删尾删
void SLPopBack(SL* psl);
void SLPopFront(SL* psl);
// 任意下标位置的插入删除
void SLInsert(SL* psl, int pos, SLDataType x);
void SLErase(SL* psl, int pos);
// 查找:找到返回下标,没有找到返回-1
int SLFind(SL* psl, SLDataType x);
// 更改数据
void SLChange(SL* psl, int pos);
1.顺序表的初始化与销毁:
void SLInit(SL* psl)
{
assert(psl);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
void SLDestroy(SL* psl)
{
assert(psl);
if (psl->a != NULL)
{
free(psl->a);
psl->a = NULL;
psl->size = 0;
psl->capacity = 0;
}
}
2.顺序表的打印与扩容:
打印:顺序表的打印本质上就是数组的打印。
扩容:
对于先前结构的定义为size代表顺序表内数据存储的个数,capacity代表顺序表的容量,所以当size=capacity时就代表顺序表容量满了,需要进行扩容,扩容在原本的基础上扩容,所以使用realloc进行扩容。
进行扩容时可以直接使用三目操作符(x ? m : n)去进行条件判断,如果原capacity为0,就扩容为4,增加4个空间,如果原本就有容量,则扩容成原来的二倍容量。
void SLPrint(SL* psl)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
printf("%d ", psl->a[i]);
}
printf("\n");
}
void SLCheckCapacity(SL* psl)
{
assert(psl);
if (psl->size == psl->capacity)
{
int newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newCapacity);
if (tmp == NULL)
{
perror("realloc fail");
return -1;
}
psl->a = tmp;
psl->capacity = newCapacity;
}
}
3.顺序表的头插与尾插:
尾插:
顺序表的尾插相对而言简单些(时间复杂度O(1),首先对顺序表的容量进行判断是否需要扩容(capacity函数),然后将数据置入顺序表即可。
头插:
顺序表的头插相对而言时间复杂度会高些O(N),因为顺序表进行头插需要向后挪动数据。先判断顺序表容量是否需要扩容(capacity函数),然后将顺序表的数据都向后挪动一位,完成后将数据插入顺序表的第一位即可。
void SLPushBack(SL* psl, SLDataType x)
{
assert(psl);
SLCheckCapacity(psl);
psl->a[psl->size] = x;
psl->size++;
}
void SLPushFront(SL* psl, SLDataType x)
{
assert(psl);
SLCheckCapacity(psl);
int end = psl->size - 1;
while (end >= 0)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[0] = x;
psl->size++;
}
4.顺序表的头删尾删:
尾删:
顺序表的尾删相对简单些(时间复杂度O(1)。先判断顺序表内是否有元素,有的话就直接将size--即可,作用为无法通过size访问打印到要尾删的元素,下一次尾插时直接通过size改变这个元素。
头删:
顺序表的头删一样是需要挪动数据,所以时间复杂度为O(N),先判断顺序表内是否有元素,有的话就从a[1]开始,到size-1,依次往前挪动元素,再将size--即可。
void SLPopBack(SL* psl)
{
assert(psl);
assert(psl->size > 0);
psl->size--;
}
void SLPopFront(SL* psl)
{
assert(psl);
assert(psl->size > 0);
int begin = 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
++begin;
}
psl->size--;
}
5.顺序表的任意位置插入与删除:
插入:
顺序表的任意位置插入需要挪动数据,首先插入位置需要符合size范围内,其次检查顺序表的容量,然后将顺序表尾的元素到pos位置(插入位置)的元素依次向后挪动,最后将数据插入pos位置即可。
删除:
顺序表的任意位置删除同样需要挪动数据,首先删除位置需要符合size范围内,然后将顺序表的pos+1的位置到顺序表尾的数据依次往前挪动,最后将size--即可。
void SLInsert(SL* psl, int pos, SLDataType x)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
SLCheckCapacity(psl);
int end = psl->size - 1;
while (end >= pos)
{
psl->a[end + 1] = psl->a[end];
--end;
}
psl->a[pos] = x;
psl->size++;
}
void SLErase(SL* psl, int pos)
{
assert(psl);
assert(pos >= 0 && pos < psl->size);
int begin = pos + 1;
while (begin < psl->size)
{
psl->a[begin - 1] = psl->a[begin];
++begin;
}
psl->size--;
}
6.顺序表的查找与更改:
查找:
顺序表的查找要通过数组的遍历来实现,如果找到了就返回数组的下标,如果未找到就返回-1。
更改:
顺序表任意位置的更改需要在size范围内进行,需要更改第pos个的数据直接通过下标pos-1就可以实现。
int SLFind(SL* psl, SLDataType x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
void SLChange(SL* psl, int pos, SLDateType x)
{
assert(psl);
assert(pos > 0 && pos <= psl->size);
psl->a[pos - 1] = x;
}
3.顺序表的优缺点:
顺序表的优点:
1.顺序表的逻辑空间连续的同时物理空间也连续。
2.尾插尾删的效率比较高(时间复杂度O(1))。
3.支持下标的随机访问,(时间复杂度O (1)),适合有经常读取元素的场景。
顺序表的缺点:
1.数据的头插头删 / 中间位置插入删除数据时需要挪动数据(时间复杂度O(N))。
2.扩容是有一定消耗的,一般存在一定空间的浪费。