动态顺序表
文章目录
一、线性表的定义
顺序表在本质上就是线性表的顺序存储,所以在学习顺序表之前,我们有必要对线性表做一个初步的了解。相关概念如下:
线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储
线性表的顺序存储被称作为顺序表,链式存储被称作为链表。
本篇博客主要讲诉动态顺序表的使用。
二、顺序表的概念及动态顺序表的结构定义
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
静态顺序表:使用定长数组存储元素。(数组大小固定,无法扩容,空间开的过大容易造成空间浪费,过小了又可能不够用)
动态顺序表:使用动态开辟的数组存储。(数组大小由动态内存开辟,可以扩容,弥补了静态顺序表的缺陷)
动态顺序表的结构定义如下:
// 动态顺序表 -- 按需扩展空间
typedef int SLDataType; // 定义顺序表的元素类型
typedef struct SeqList
{
SLDataType* a; // 指向动态开辟数组的指针
size_t size; // 记录存储了多少个有效数据
size_t capacity; // 容量空间的大小
}SL;
如下图所示,a作为指针指向1,也就是数组的首元素。size表示存储数据的个数,图中为5,capicity表示容量空间,图中为7
三、动态顺序表的实现
1.初始化
顺序表中有三个值,将size和capicity赋值为0,将a置为NULL
void SLInit(SL* ps)
{
assert(ps); // 断言,ps不为空
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
这里简单说一下assert这个函数,这个是断言函数,需要包含头文件assert.h。简单来说,必须满足assert函数中的条件,否则代码会直接报错,无法继续运行。此处我们使用它确保传入的ps不是空指针。后面的函数中我们还会用到。
2.销毁
释放顺序表的空间,并将a指针置为空(size和capicity置不置0都可以,这里就不置为0了)
void SLDestroy(SL* ps)
{
assert(ps);
if (ps->a)
{
free(ps->a);
ps->a = NULL;
}
}
3.检查扩容
判断顺序表的容量有没有满,如果容量满的话就扩容,并更新容量的大小
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->capacity == ps->size) // 如果size与capacity相等(即数据实际个数等于存储容量,则代表容量满了,需要扩容。
{
int newCapacity = ps->capacity == 0 ? 5 : ps->capacity * 2; // 三目表达式,如果capacity为0,那么直接将newCapacity置为5,不为0则置为原来的两倍
SLDataType* tmp = (SLDataType*)realloc(ps->a, newCapacity * sizeof(SLDataType)); // 使用realloc动态开辟内存
if (tmp == NULL) // 内存为空,开辟失败
{
perror("realloc fail"); // 报错
exit(-1); // 结束运行
}
ps->a = tmp; // 开辟成功,将开辟好的空间赋值给ps->a
ps->capacity = newCapacity; // 更新容量的大小
}
}
这里针对realloc开辟内存做一个简单的讲诉,realloc是扩容函数,需要包含头文件#include <stdlib.h>
,但是如果指针指向的空间没有开辟,也可以当作malloc函数,所以这里不需要考虑第一次开辟的问题。realloc第二个参数是开辟空间字节数的大小,所以newCapacity需要乘SLDataType的字节大小,即newCapacity * sizeof(SLDataType)
。最后,由于realloc返回类型是void*
,所以在前面需要使用强制类型转换(SLDataType*)
perror函数需要头文件#include<stdio.h>
4.尾插
在数组尾部插入数据,首先要考虑是否需要扩容,这里就可以调用上一步写好的函数
SLCheckCapacity
,然后插入数据,并将size+1
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
// 检查是否需要扩容
SLCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
5.尾删
在数组尾部删除数据,首先确保顺序表中还有数据还可以删除,即
ps->size > 0
,然后删除数据(一种思路是将数据置为0,不过没有必要,因为size-1后不会用到该数据),并将size-1
void SLPopBack(SL* ps)
{
assert(ps);
assert(ps->size > 0); // 确保顺序表中还存在数据
ps->size--;
}
6.打印
打印顺序表中的元素
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
7.头插
在数组头部插入数据,首先要考虑是否需要扩容,这里可以调用之前写好的函数
SLCheckCapacity
,然后从起始位置开始,所有的数据向后挪动一位,给起始位置留出空间,再将数据插入起始位置,最后将size+1
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
// 挪动数据
int end = ps->size - 1;
while (end >= 0) // a[0]处的数据也需要挪动,所以是end>=0
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[0] = x;
ps->size++;
}
8.头删
在数组头部删除数据,首先确保顺序表中还有数据还可以删除,即
ps->size > 0
,然后将除ps->a[0]
以外的数据全部向前挪动一位,此时原a[0]处的数据会被a[1]覆盖,便完成了删除同步数据的目的,最后将size-1
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0); // 确保还有数据
// 挪动数据
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
9.任意位置插入
在任意位置插入数据,首先要考虑确保插入位置合理,这里可以使用
assert
函数,还需要考虑是否需要扩容,这里可以调用之前写好的函数SLCheckCapacity
,然后从指定位置开始,所有的数据向后挪动一位,给指定位置留出空间,再将数据插入指定位置,最后将size+1
void SLInsert(SL* ps, int pos, SLDataType x) // pos为指定位置的下标
{
assert(ps);
assert(pos >= 0 && pos <= ps->size); // 确保插入位置的下标在合理的区间
SLCheckCapacity(ps); // 检查容量
// 开始挪动数据,这里和头插有点类似,只是while中的循环条件不同
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
ps->a[pos] = x;
ps->size++;
}
这里有一个有趣的地方,可以发现,如果函数中pos为0,那么就相当于头插,如果pos为ps->size,那么就相当于尾插,所以我们可以使用SLInsert
函数来替代头插和尾插函数,这个在后面还会提及
10.任意位置删除
在任意位置删除数据,首先要考虑确保删除位置的合理,和任意位置插入一样,可以使用
assert
,还需要确保顺序表中还有数据还可以删除,即ps->size > 0
,然后将除指定位置以外的数据全部向前挪动一位,此时指定位置处的数据会被后一个数据覆盖(如果指定位置处的数据是最后一个数据,那么将不会被覆盖,不过没有关系,因为如果这样size-1后该位置的数据就不会被使用到,就像尾删一样),便完成了删除同步数据的目的,最后将size-1
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
// assert(ps->size > 0); 可以不用加,因为如果ps->size=0,第二个断言就会直接报错
// 挪动数据覆盖,这里和头删有点类似,只是begin的初始值不同
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
和上面类似,可以发现,如果函数中pos为0,那么就相当于头删,如果pos为ps->size - 1,那么就相当于尾删,所以我们可以使用SLInsert
函数来替代头删和尾删函数,这个也被称为复用优化,在后面还会提及。
11.寻找指定元素的下标
给定一个数值,找出该数值在顺序表的下标,如果顺序表中存在这个数值,返回下标。不存在则返回-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;
}
这个函数可以和之前的SLInsert
和SLErase
函数结合使用,比如可以先使用SLFind
函数找到对应值的下标,再使用SLErase
函数删除这个位置的值,这样就由原来Erase
函数只能删除指定下标的值变为了删除第一个指定数值。当然,配合的方式很多,这里只举出了最常用的一种。
四、复用优化
正如先前所诉,我们可以使用
SLInsert
函数和SLErase
函数,通过固定pos变量的方式来替换尾插尾删,头插头删函数
1.尾插函数优化
void SLPushBack(SL* ps, SLDataType x)
{
SLInsert(ps, ps->size, x);
}
2.尾删函数优化
void SLPopBack(SL* ps)
{
SLErase(ps, ps->size - 1);
}
3.头插函数优化
void SLPushFront(SL* ps, SLDataType x)
{
SLInsert(ps, 0, x);
}
4.头删函数优化
void SLPopFront(SL* ps)
{
SLErase(ps, 0);
}