1. 顺序表定义
先声明:顺序表是一种思想而不是一个程序,变量名及实现方法都是不是固定的。
通过实现顺序表以及对其进行操作的各种函数,我们能更方便地对数据进行管理。
相比于数组,静态的顺序表多了一个参数size来记录数组中有效数据的个数。
#define MAX 100
typedef int SLDataType;
struct SeqList
{
SLDataType data[MAX];
int size;//有效数据个数
};
除了静态的顺序表之外,还有一种动态的顺序表。
相比于静态的顺序表,动态的顺序表将数组改为了一块动态申请的空间。
这样,我们顺序表的大小就可以随情况而变化,更加灵活。
那么,既然我们使用的是一块动态开辟的空间,我们就还需要一个变量capacity来记录这块空间的大小。
struct SeqList
{
SLDataType* data;
int size;//有效数据个数
int capacity;//容量
};
在实际使用过程中,常采用如下的定义方式:
typedef struct SeqList
{
SLDataType* data;
int size;//有效数据个数
int capacity;//容量
}SL;
一般来说,我们更推荐使用动态的顺序表。
动态的顺序表相比于静态顺序表,其容量可变的特性使得其不会出现表满而无法插入元素的现象,在元素较少时,也不会造成空间的浪费。
需要实现来对顺序表进行操作的函数有 :
//顺序表初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps);
//检查扩容
void CheckCapacity(SL* ps);
//打印
void SLPrint(SL s);
//尾部插入
void SLPushBack(SL* ps, SLDataType x);
//头部插入
void SLPushFront(SL* ps, SLDataType x);
//尾部删除
void SLPopBack(SL* ps);
//头部删除
void SLPopFront(SL* ps);
//找到某个数据,返回下标
int SLFind(SL* ps, SLDataType x);
//指定位置下标插入
void SLInsert(SL* ps, int pos, SLDataType x);
//指定位置下标删除
void SLErase(SL* ps, int pos);
注意,虽然顺序表是以数组为基础的数据结构,但是其本质上是一个结构体,所以在传参时应采用传址调用。
2. 顺序表初始化
对结构体中的三个成员分别进行初始化即可。
void SLInit(SL* ps)//初始化
{
ps->data = NULL;
ps->size = 0;
ps->capacity = 0;
}
3. 顺序表销毁
由于动态顺序表的数组是动态开辟的,所以在顺序表停用时,一定要将其销毁(主要是free掉data数组)。
销毁后,表中不再有元素,所以最好将size和capacity都置为0。
void SLDestroy(SL* ps)//销毁
{
if (ps->data)
{
free(ps->data);
}
ps->data = NULL;
ps->size = 0;
ps->capacity = 0;
}
4. 检查扩容
当表的容量不够时,我们无法对在表中插入数据。
所以,在插入数据之前,一定要先检查表是否已满(size==capacity)。
若表满,则对表进行扩容操作,一般来说,将表的容量扩充到原来的两倍较为合理。
int newcapacity = ps->capacity * 2;
但问题是,刚刚初始化好的表的capacity为0,按上面这样写就无法对刚初始化好的表,或者被销毁过的表进行扩容,所以我们想到下面这样的写法。
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
接着,用realloc函数对data数组进行扩容。
SLDataType* tmp = (SLDataType*)realloc(ps->data, sizeof(SLDataType) * newcapacity);
由于realloc函数可能扩容失败(返回NULL),于是我们先创建一个新的变量来接收返回值,确认扩容成功之后,在将其赋值给data。
void CheckCapacity(SL* ps)//检查扩容
{
if (ps->size == ps->capacity)
{
//通常来说是成倍的增加
//一般用2或3倍较为合理
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* tmp = (SLDataType*)realloc(ps->data, sizeof(SLDataType) * newcapacity);
if (tmp == NULL)
{
perror("realloc fail!");
exit(-1);
}
//空间申请成功
ps->data = tmp;
ps->capacity = newcapacity;
}
}
5. 打印顺序表
到上一步,我们的准备工作就基本完成了,接下来,就可以开始实现对顺序表进行增删查改的函数的。
但是,在写这些函数时,我们希望能够看看表中的数据是否能被这些函数正确操作。
于是在此之前,我们先来实现一个可以打印顺序表的函数。
void SLPrint(SL s)//打印
{
for (int i = 0; i < s.size; i++)
{
printf("%d ", s.data[i]);
}
printf("\n");
}
6. 尾插
顾名思义,就是在顺序表的末尾插入数据。
void SLPushBack(SL* ps, SLDataType x)//尾插
{
if (ps == NULL)
{
return;
}
//assert(ps);也可
CheckCapacity(ps);
ps->data[ps->size] = x;
ps->size++;
}
7. 头插
顾名思义,就是在顺序表的开头插入数据。
相比于尾插,这个操作就要麻烦的多,因为在头部插入数据之前,我们要先将原有的数据全部向后移动一格。
void SLPushFront(SL* ps, SLDataType x)//头插
{
if(ps == NULL)
{
return;
}
CheckCapacity(ps);
for (int i = ps->size; i > 0; i--)
{
ps->data[i] = ps->data[i - 1];
}
ps->data[0] = x;
ps->size++;
}
8. 尾删
顾名思义,就是将顺序表尾部的数据删除。
这一步,其实只需要让size自减1即可,这样一来,最末尾上的数据就不算有效数据了。
void SLPopBack(SL* ps)//尾部删除
{
assert(ps);
if(ps->size > 0)//也可以是assert(ps->size)
ps->size--;
}
9. 头删
顾名思义,就是将顺序表头部的数据删除。
与头插相反,这里我们需要让所有数据全部向前移动一格,然后让size--,第一个数据在这个过程中就被第二个数据覆盖掉了。
void SLPopFront(SL* ps)//头部删除
{
assert(ps);
assert(ps->size);
for (int i = 0; i < ps->size - 1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
10. 查找某个元素
int SLFind(SL* ps, SLDataType x)//找到某个数据,返回下标
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
if (ps->data[i] == x)
return i;
}
return -1;
}
11. 在指定位置插入数据
与头插类似,先给新数据腾出位置,然后再进行插入。
只要是插入,都需要先检查是否需要扩容。
void SLInsert(SL* ps, int pos, SLDataType x)//指定位置下标插入
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
CheckCapacity(ps);
for (int i = ps->size; i > pos; i--)//给新数据挪位置
{
ps->data[i] = ps->data[i - 1];
}
ps->data[pos] = x;
ps->size++;
}
12. 删除指定位置数据
与头删类似。
void SLErase(SL* ps, int pos)//指定位置下标删除
{
assert(ps);
assert(pos >= 0 && pos <= ps->size - 1);
for (int i = pos; i < ps->size - 1; i++)
{
ps->data[i] = ps->data[i + 1];
}
ps->size--;
}
13. 总结
代码仅作参考,顺序表是一种思想而不是固定的程序或代码,只要能达到相同的目的即可。
其细节要依据实际要处理的问题来进行修改。