前言
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
一、顺序表
顺序表一般可以分为:
静态顺序表:使用定长数组存储元素。
静态顺序表 -- N太小,可能不够用,N太大,可能浪费空间。
静态顺序表使用定长数组存储元素。
#define N 100
typedef int SLDataType;
struct SeqList
{
SLDataType data[N]; //定长数组
int size;
};
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。
动态顺序表:使用动态开辟的数组存储。
//动态顺序表
typedef int SLDataType;
typedef struct SeqList
{
SLDataType* data; //指向动态开辟的数组。
int size; //有效数据的个数
int capacity; // 容量空间的大小
}SL;
当顺序表的结构体定义完成后,SL s,即为创建一个顺序表,此时顺序表里面的数据都是随机数据,所以在创建顺序表后,需要先对顺序表进行初始化,然后才能开始使用顺序表存储数据。顺序表的初始化如下。在初始化顺序表时,给存储数据的数组开辟4个空间用来存储数据。
void SLInit(SL* ps)
{
assert(ps);
SLDataType* tmp = (SLDataType*)calloc(4,sizeof(SLDataType)); //初始化时自动开辟4个空间
if (tmp == NULL)
{
perror("SLInit");
exit(-1); //退出程序
}
ps->data = tmp;
ps->size = 0;
ps->capacity = 4;
}
当顺序表初始化完成后,接下来要实现的就是插入元素的操作了,顺序表的尾插法是在顺序表的尾部插入数据,在插入数据时还需要先检查顺序表是否需要扩容,如果需要扩容,就将顺序表的容量扩大二倍。检查顺序表是否需要扩容的函数如下。
void SLCheckCapacity(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity&& ps->size!=0)
{
SLDataType* tmp = (SLDataType*)realloc(ps->data, (2 * ps->capacity) * sizeof(SLDataType));
if (tmp == NULL)
{
perror("SLCheckCapacity");
exit(-1);
}
ps->data = tmp;
ps->capacity = ps->capacity * 2;
}
}
当检查完顺序表是否需要扩容后,接下来就是从尾部插入数据了,具体实现代码如下。
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps); //先检查是否需要扩容
ps->data[ps->size] = x; //插入数据
ps->size++;
}
顺序表的尾插法比较好实现,当实现顺序表的头插法时,需要先将数据都整体向后移一位,然后将顺序表的头部插入新元素。实现代码如下。
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
//先使顺序表的元素都向后移一位,
int end = ps->size - 1;
while (end >= 0)
{
ps->data[end + 1] = ps->data[end];
end--;
}
ps->data[0] = x; //将顺序表头部插入新元素
ps->size++;
}
当顺序表插入元素后,可以实现一个方法用来打印顺序表的各元素,其代码如下。
void SLPrint(const SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->data[i]);
}
printf("\n");
}
顺序表的头插法和尾插法实现后,就该实现头删法和尾删法了,顺序表的尾删法就是从尾部将数据删除。因为打印顺序表时是按照顺序表的有效数据长度来打印的,所以尾删法直接将有效数据长度-1即可,这样遍历顺序表时就遍历不到最后一个元素了。代码如下。
void SLPopBack(SL* ps)
{
assert(ps->size>0);
ps->size--;
}
顺序表的头删法实现时,需要将首元素之后的数据整体向前移一位,当顺序表的容量刚好等于有效数据长度时,这时最后的元素后面的元素向前移位时就发生了越界访问,所以只需将最后一个元素移到倒数第二个元素的位置上即可,不用去管最后一个元素。这样首元素就被第二个元素给覆盖了,然后使有效数据长度-1,这样就实现了顺序表的头删法。代码如下。
void SLPopFront(SL* ps)
{
assert(ps->size > 0);
int i = 0;
for (i = 0; i < ps->size - 1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
顺序表的特点就是可以随机访问,所以顺序表可以实现任意位置的插入和删除,顺序表实现任意位置插入时,需要将插入位置后面的元素都向后移一位。代码如下。
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps&&pos>=0&&pos<=ps->size);
SLCheckCapacity(ps);
int end = ps->size;
while (end > pos)
{
ps->data[end] = ps->data[end - 1];
end--;
}
ps->data[pos] = x;
ps->size++;
}
顺序表实现任意位置的删除时,需要将删除元素的位置后面的元素都向前移一位。代码如下。
void SLErase(SL* ps, int pos)
{
assert(ps && pos >= 0 && pos < ps->size&&ps->size>0);
int end = pos;
int i = 0;
for (i = pos; i < ps->size-1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
当顺序表的任意位置插入和删除都实现后,前面实现的头插法和尾插法,头删法和尾删法都可以用这两个方法来实现,具体如下。
void SLPushBack(SL* ps, SLDataType x)
{
SLInsert(ps, ps->size, x);
}
void SLPushFront(SL* ps, SLDataType x)
{
SLInsert(ps, 0, x);
}
void SLPopBack(SL* ps)
{
SLErase(ps, ps->size - 1);
}
void SLPopFront(SL* ps)
{
SLErase(ps, 0);
}
顺序表的元素查找需要遍历顺序表,如果查找到元素,就将元素的下标返回,如果没有查找到元素,就返回-1,代码实现如下。
int SLFind(const SL* ps, SLDataType x)
{
assert(ps);
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->data[i] == x)
{
return i;
}
}
return -1;
}
当通过查找元素得到元素的下标后,就可以对元素进行修改操作,具体实现如下。
void SLModify(SL* ps, int pos, SLDataType x)
{
assert(ps&&pos>=0&&pos<ps->size);
ps->data[pos] = x;
}
并且这些方法可以搭配使用,然后实现删除指定元素等操作。
例如
int x = 0;
printf("请输入你要删除的值:");
scanf("%d", &x);
int pos = SLFind(&s, x);
if (pos != -1)
{
SLErase(&s, pos);
}
else
{
printf("没有这个元素\n");
}
在这些操作都执行完后,就要进行顺序表的销毁,即将顺序表所使用的空间都释放。
代码如下。
void SLDestory(SL* ps)
{
if (ps->data)
{
free(ps->data);
ps->data = NULL;
ps->capacity = ps->size = 0;
}
}
总结
顺序表可以实现随机访问,即通过下标就可以访问元素。
顺序表的中间/头部的插入删除,时间复杂度为O(N)。
顺序表增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
顺序表增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。