目录
2.2.9 通过插入和删除pos位置的数据来实现尾插、尾删、头插、头删
1.线性表
线性表*(linear list)*是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
2.顺序表
2.1 概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储元素。
- 动态顺序表:使用动态开辟的数组存储。
2.2 接口实现
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
typedef int SLDataType;
// 顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组
size_t size ; // 有效数据个数
size_t capicity ; // 容量空间的大小
}SeqList;
// 基本增删查改接口
// 顺序表初始化
void SeqListInit(SeqList* psl);
// 检查空间,如果满了,进行增容
void CheckCapacity(SeqList* psl);
// 顺序表尾插
void SeqListPushBack(SeqList* psl, SLDataType x);
// 顺序表尾删
void SeqListPopBack(SeqList* psl);
// 顺序表头插
void SeqListPushFront(SeqList* psl, SLDataType x);
// 顺序表头删
void SeqListPopFront(SeqList* psl);
// 顺序表查找
int SeqListFind(SeqList* psl, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* psl, size_t pos);
// 顺序表销毁
void SeqListDestory(SeqList* psl);
// 顺序表打印
void SeqListPrint(SeqList* psl);
2.2.1 顺序表的初始化
在往顺序表中存放数据之前,我们首先要把顺序表进行初始化,即向系统申请空间。直接看代码:
void SLInit(SL *ps)
{
assert(ps);
//向内存申请四个空间
ps->a = malloc(sizeof(SLDateType) * 4);
if (ps->a == NULL)
{
perror("malloc error");
return;
}
//因为顺序表内并未存放数据,所以size为0
ps->size = 0;
ps->capacity = 4;//预设容量为4个数据
}
2.2.2 顺序表尾插
在成功初始化顺序表后,我们需要在往顺序表中存放数据,通常我们会使用尾插来存放数据,即在顺序表的末尾增加数据,如图:
此时我们只需要把数据存放在ps->a[ps->size]中即可,同时让ps->size自增1,表示顺序表中存储的有效数据个数增加了一个。
当然,如果顺序表已经满了,即ps->size == ps->capacity,此时我们则需要进行扩容,我们使用realloc函数来实现扩容的功能。
上代码:
void SLPushBack(SL* ps, SLDateType x)
{
assert(ps);
//如果数据已满则扩容
if (ps->size == ps->capacity)
{
//这里我扩容为原来的两倍
SLDateType* tmp = (SLDateType*)realloc(ps->a, ps->capacity * 2 * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc error");
return;
}
//把扩容后的空间赋给ps->a
ps->a = tmp;
}
//存放数据
ps->a[ps->size] = x;
//有效数据个数增加
ps->size++;
}
2.2.3 顺序表尾删
在往顺序表中存放了数据以后,我们如果需要删除最后一个数据,我们可以通过尾删的方式实现,由于我们使用顺序表时是根据ps->size来判断有效数据个数,因此当我们需要尾删数据时,只需要使ps->size自减1即可。
注意:我们需要判断顺序表中是否存有数据,如果顺序表中压根没有数据,那么何来尾删可言!!!!
上代码:
void SLPopBack(SL* ps)
{
assert(ps->size&&ps);
ps->size--;
}
2.2.4 顺序表头插与检查空间的封装
我们往顺序表中存放数据时,除了使用尾插,我们还可以使用头插的方法,即在顺序表首端增加数据,此时为了保证数据的完整性,我们需要把所有数据向后挪动一位,腾出首端数据的空间,如图:
需要注意的是:我们在挪动数据时需要从最后一个数据开始倒着依次往后挪动,如果从第一个数据开始往后挪动则会造成数据丢失!!!!
当然,我们仍需要判断顺序表是否已满,这时候我们就封装一个函数来复用检查空间的代码,函数如下:
void SLCheckCapacity(SL* ps)
{
assert(ps);
//如果数据已满则扩容
if (ps->size == ps->capacity)
{
//这里我扩容为原来的两倍
SLDateType* tmp = (SLDateType*)realloc(ps->a, ps->capacity * 2 * sizeof(SLDateType));
if (tmp == NULL)
{
perror("realloc error");
return;
}
//把扩容后的空间赋给ps->a
ps->a = tmp;
}
}
然后就是头插的实现,看代码:
void SLPushFront(SL* ps, SLDateType x)
{
assert(ps);
//判断顺序表是否已满
SLCheckCapacity(ps);
//从最后一个数据开始
int end = ps->size - 1;
//倒着依次往后挪动数据,直到挪完第一个数据
while (end >= 0)
{
ps->a[end+1] = ps->a[end];
--end;
}
//将数据存放到顺序表的首端
ps->a[0] = x;
//有效数据个数增加
ps->size++;
}
2.2.5 顺序表的头删
有了头插自然也就有了头删,头删需要注意的是我们需要仍判断顺序表是否有数据,如果没有数据则不需要删除,当然,头删我们只需要将后面的数据往前挪动,覆盖掉第一个数据即可。代码如下:
void SLPopFront(SL* ps)
{
assert(ps->size && ps);
//从第二个数据开始
int begin = 1;
//依次往前挪动数据,直到挪完最后一个数据
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
ps->size--;
}
2.2.6 顺序表查找
如果我们需要在顺序表中查找一个数据,那我们则需要返回他的下标,如果找不到则返回-1表示找不到,这个代码很简单,只需要遍历顺序表即可。
看代码:
int SLFind(SL* ps, SLDateType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
2.2.7 顺序表在pos位置插入
通常我们在使用顺序表的时候如果需要插入数据,其实头插和尾插用的并不太多,用的最多的是在指定位置插入数据,这个代码实现和头插其实很像,我们需要把pos位置及之后的数据整体向后挪动一位,当然前提是顺序表没有满,所以我们依旧需要判断顺序表是否已满。
直接看代码:
void SLInsert(SL* ps, int pos, SLDateType x)
{
assert(ps);
//判断pos是否合理
assert(pos >= 0 && pos <= ps->size);
//判断顺序表是否已满
SLCheckCapacity(ps);
//从最后一个数据开始
int end = ps->size-1;
//倒着依次往后挪动数据,直到挪完pos位置的数据
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
end--;
}
//将数据存放到pos位置
ps->a[pos] = x;
//有效数据个数增加
ps->size++;
}
2.2.8 顺序表删除pos位置的数据
删除数据时,我们不可能只删除头或者尾的数据,我们通常是删除我们想要删除的数据,这个时候我们就需要删除pos位置的数据,这个代码的实现类似于头删,我们需要把pos位置之后的数据整体往前挪动一位,覆盖掉pos位置的数据,当然,我们需要判断顺序表是否为空。直接看代码:
void SLErase(SL* ps, int pos)
{
assert(ps);
//判断pos合理性以及顺序表是否为空
assert(pos >= 0 && pos < ps->size);
//从pos位置的后一个数据开始
int begin = pos + 1;
//依次往前挪动数据,直到移完最后一个数据
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
begin++;
}
//有效数据个数减少
ps->size--;
}
2.2.9 通过插入和删除pos位置的数据来实现尾插、尾删、头插、头删
有了2.2.7与2.2.8,我们实现尾插、尾删、头插、头删的方式就可以变得非常简单,直接看代码
尾插:即在ps->size位置插入数据:
void SLPushBack(SL* ps, SLDateType x)
{
assert(ps);
SLCheckCapacity(ps);
SLInsert(ps, ps->size, x);
}
尾删:即删除ps->size-1位置的数据:
void SLPopBack(SL* ps)
{
assert(ps->size&&ps);
ps->size--;
}
头插:即在0位置插入数据:
void SLPushFront(SL* ps, SLDateType x)
{
assert(ps);
SLCheckCapacity(ps);
SLInsert(ps, 0, x);
}
头删:即删除0位置的数据:
void SLPopFront(SL* ps)
{
assert(ps->size && ps);
SLErase(ps, 0);
}
2.2.10 顺序表的打印和销毁
在完成顺序表的各项操作后,我们需要将顺序表打印出来,这个非常简单,只需要遍历顺序表即可
直接看代码:
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
在使用完顺序表后,我们需要销毁动态开辟的空间,这个代码也比较简单,我们直接看代码就好:
void SLDestroy(SL* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->size = 0;
}
2.2.11 汇总
具体代码我存在了我的gitee仓库:c_practice_2023/DataStructure/SeqList · 游汶祥/C语言 - 码云 - 开源中国 (gitee.com)
2.3 数组相关面试题
2.4 顺序表的问题及思考
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:如何解决以上问题呢?下个博客我会给出了链表的结构来看看。