目录
线性结构
线性结构顾名思义,在逻辑结构上每个数据都是连接起来的,像一条线一样。顺序表对应线性结构的顺序存储,顺序存储中,存储的每个元素的地址是连续的。其实也就是常说的数组,平时使用的数组就是一种静态的顺序表。
动态顺序表
动态顺序表的优势
静态顺序表在实际应用上没有什么意义,因为静态顺序表在使用时是把存储个数给写死了,如果空间给大了,空间用不完,给小了空间不够用。
动态的顺序表在一定程度上可以减少空间的浪费,当空间不够用时,动态顺序表可以扩容。
SeqList结构体
动态的顺序表,当空间满了应该增容,要判断存储的元素是否满了,应该要记录下当前顺序表可以存储多少个元素,也就是记录下当前顺序表容量。
为了顺序表还能存储其他类型的数据,最好是将数组的类型重命名,方便之后想存储其他类型数据时,能够方便修改。
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
size_t size;//已经存储的元素个数
size_t capacity;//记录容量
}SeqList;
SeqListInit(顺序表初始化)
初始初始化没有什么讲究,唯一要注意的是capacity在初始化时,可以给他初始化成0,也可以一开始就给一些空间。
void SeqListInit(SeqList* ps)
{
//检查空指针
assert(ps);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
SeqListCheckCapacity(容量检查)
在插入数据前都要判断下容量是否为满,顺序表有三种插入方式,那么为了避免代码的冗余,可以把它独立出来封装出一个函数。
当数组还是空时,realloc是相当于一次malloc。realloc扩容有两种模式:
第一种是原地扩容,也就是在原来数组上增加一些空间,这是比较好的一种情况。
另一种情况是异地扩容,当数组后面的空间不够扩容的新空间大小时,数组会去堆区找一块能够容纳新空间大小的地方,将原来数组的数据拷贝到新空间位置,并释放掉原数组的空间,异地扩容会有一定的时间开销。
为了减少异地扩容带来的时间开销,我们应该尽量的避免频繁扩容。数组可能开大了用不完,开小了空间可能会不够用,折中考虑建议每次开二倍的空间,扩容后空间大小等于新空间大小。
注意,当我们的capacity在初始化时初始化为0,0乘以任何数都等于0,所以capacity = 0时,要先给点空间。
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
//检查容量
if (ps->size == ps->capacity)
{
//容量不能是0
size_t newcapcity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(ps->a,sizeof(SLDateType) * newcapcity);
if (tmp == NULL)
{
//扩容失败
printf("%s\n", strerror(errno));
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapcity;
}
}
SeqListPushBack(尾插数据)
有了SeqListCheckCapacity的支撑顺序表尾插就非常方便了。顺序表的ps->size就是新空间的位置,每次尾插一个数据让size向后走一步,顺序表的队尾元素永远都在size-1位置。
void SeqListPushBack(SeqList* ps, SLDateType x)
{
assert(ps);
SeqListCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
SeqListPopBack(顺序表尾删)
顺序表尾删也是十分方便的,先保证顺序表要有数据,然后直接让size--,最后一个元素就被删除掉了。当在插入一个元素会直接把那个位置的数据给覆盖掉。那要问那个被删除的位置还在那吗?答案是肯定的,它还在那个位置,但是他已经不是一个有效数据的范围了。被删除的位置无需置成某一个值,虽然可以,但没必要。假如将它置成0,如果我再插入一个新的元素,插入的元素就是0呢?那置0是不是就毫无意义,还不如就一个size--省事。
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
ps->size--;
}
SeqListPushFront(顺序表头插)
因为是插入数据,所以判断数据是否要扩容也是必要的。顺序表的头插是比较麻烦的,因为数组是连续的(成也萧何败萧何),要头插数据就要把所有元素都先后移一位,为头插的数据腾出位置之后,才能头插,那么它挪动数据的时间复杂度是O(N)。挪动数据必须是从后往前挪,否则前面的数据会将顺序表的数据全部覆盖。
正确(√):
代码:
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
SeqListCheckCapacity(ps);
size_t end = ps->size;
while (end > 0)
{
ps->a[end] = ps->a[end-1];
end--;
}
ps->a[0] = x;
ps->size++;
}
SeqListPopFront(顺序表头删)
删除数据首先数据不能为空,与头插一样,头删依旧需要移动数据。头删时只要让后一个数据覆盖到前一个数据的位置就完成头删了,挪动的次数除去第一个数据不需要挪动,也就是要挪动N-1次,那么除去常数项,顺序表头删的时间复杂度就是O(N)。与头插不同的是,如果还是从后往前挪数据,后面的数据会将前面的数据全部覆盖。
正确(√):
代码:
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
size_t end = 0;
while (end < ps->size)
{
ps->a[end] = ps->a[end + 1];
end++;
}
ps->size--;
}
SeqListFind(查找数据)
查找一个数据找他的下标,要找一个数据就要一个一个数据的去比较。最坏的情况就是找不到这个数据,也有可能这个数据在最后一个位置,因为在遍历顺序表,所以Find的时间复杂度就是O(N)。
int SeqListFind(SeqList* ps, SLDateType x)
{
for (int i = 0;i < ps->size;i++)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
SeqListInsert(pos的位置插入元素)
Insert通常会配合Find使用,但是不排除直接使用的,所以最好还是判断一下pos是否在顺序表数据的合法范围内。插入数据与头插时一样,需要将在pos位置之后的数据都后移一位。
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDateType x)
{
assert(ps);
if (pos > ps->size)
{
printf("pos 越界\n");
return;
}
SeqListCheckCapacity(ps);
size_t end = ps->size;
while (end > pos)
{
ps->a[end] = ps->a[end - 1];
end--;
}
ps->a[end] = x;
ps->size++;
}
Insert也可以被头插和尾插复用,省时省力~~
void SeqListPushBack(SeqList* ps, SLDateType x)
{
//assert(ps);
//SeqListCheckCapacity(ps);
//ps->a[ps->size] = x;
//ps->size++;
SeqListInsert(ps, ps->size, x);
}
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
//SeqListCheckCapacity(ps);
//size_t end = ps->size;
//while (end > 0)
//{
// ps->a[end] = ps->a[end-1];
// end--;
//}
//ps->a[0] = x;
//ps->size++;
SeqListInsert(ps, 0, x);
}
SeqListErase(删除pos位置)
为了防止pos越界应当判断下pos是否合法。当要删除pos位置同样需要挪动数据,让pos位置开始,依次将后面的数据依次覆盖,Erase同样可以被尾头删复用。
void SeqListErase(SeqList* ps, size_t pos)
{
assert(ps);
assert(ps->size > 0);
if (pos > ps->size)
{
printf("pos 越界\n");
return;
}
if (ps->size > 0)
{
for (size_t i = pos;i < ps->size;i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
}
复用:
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
//size_t end = 0;
//while (end < ps->size)
//{
// ps->a[end] = ps->a[end + 1];
// end++;
//}
//ps->size--;
SeqListErase(ps, 0);
}
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
//ps->size--;
SeqListErase(ps, ps->size-1);
}
SeqListDestory(释放空间)
在堆区开辟的空间不用了,记得还给操作系统。
void SeqListDestory(SeqList* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
全代码:
#pragma once
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include<string.h>
#include<errno.h>
typedef int SLDateType;
typedef struct SeqList
{
SLDateType* a;
size_t size;
size_t capacity;
}SeqList;
// 对数据的管理:增删查
void SeqListInit(SeqList* ps);
void SeqListDestory(SeqList* ps);
void SeqListPrint(SeqList* ps);
void SeqListPushBack(SeqList* ps, SLDateType x);
void SeqListPushFront(SeqList* ps, SLDateType x);
void SeqListPopFront(SeqList* ps);
void SeqListPopBack(SeqList* ps);
// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDateType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, size_t pos);
// SeqList.c
void SeqListCheckCapacity(SeqList* ps)
{
assert(ps);
//检查容量
if (ps->size == ps->capacity)
{
//容量是否为0?
size_t newcapcity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDateType* tmp = (SLDateType*)realloc(ps->a,sizeof(SLDateType) * newcapcity);
if (tmp == NULL)
{
//扩容失败
printf("%s\n", strerror(errno));
exit(-1);
}
ps->a = tmp;
ps->capacity = newcapcity;
}
}
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
void SeqListDestory(SeqList* ps)
{
assert(ps);
free(ps->a);
ps->a = NULL;
ps->capacity = ps->size = 0;
}
void SeqListPrint(SeqList* ps)
{
assert(ps);
if (ps->size == 0)
{
printf("NULL\n");
return;
}
for (int i = 0;i < ps->size;i++)
{
printf("%d ",ps->a[i]);
}
printf("\n");
}
void SeqListPushBack(SeqList* ps, SLDateType x)
{
//assert(ps);
//SeqListCheckCapacity(ps);
//ps->a[ps->size] = x;
//ps->size++;
SeqListInsert(ps, ps->size, x);
}
void SeqListPushFront(SeqList* ps, SLDateType x)
{
assert(ps);
//SeqListCheckCapacity(ps);
//size_t end = ps->size;
//while (end > 0)
//{
// ps->a[end] = ps->a[end-1];
// end--;
//}
//ps->a[0] = x;
//ps->size++;
SeqListInsert(ps, 0, x);
}
void SeqListPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
//size_t end = 0;
//while (end < ps->size)
//{
// ps->a[end] = ps->a[end + 1];
// end++;
//}
//ps->size--;
SeqListErase(ps, 0);
}
void SeqListPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size > 0);
//ps->size--;
SeqListErase(ps, ps->size-1);
}
// 顺序表查找
int SeqListFind(SeqList* ps, SLDateType x)
{
for (int i = 0;i < ps->size;i++)
{
if (ps->a[i] == x)
return i;
}
return -1;
}
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDateType x)
{
assert(ps);
if (pos > ps->size)
{
printf("pos 越界\n");
return;
}
SeqListCheckCapacity(ps);
size_t end = ps->size;
while (end > pos)
{
ps->a[end] = ps->a[end - 1];
end--;
}
ps->a[end] = x;
ps->size++;
}
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, size_t pos)
{
assert(ps);
assert(ps->size > 0);
if (pos > ps->size)
{
printf("pos 越界\n");
return;
}
if (ps->size > 0)
{
for (size_t i = pos;i < ps->size;i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
}
顺序表优点和缺点
优点:
顺序表优点是地址连续,方便排序,很多排序算法都是基于数组地址的连续。因为地址连续,所以顺序表支持下标的随机访问,可以在任意合法下标内随机访问数据。
缺点:
一、因为顺序表地址连续,在进行头插头删和pos位置插入数据时都要挪动数据。头插头删时间复杂度是O(N),pos位置删除插入要挪动N - (pos+1)次,但pos也可能是0的位置,也就是头删,所以pos位置删除插入的时间复杂度也是O(N)。
二、顺序表都会有空间浪费,无论是静态顺序表还是动态顺序表都无法避免空间浪费的问题,只不过动态顺序表比静态顺序表要更加灵活一点。