线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的, 线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
1. 静态顺序表:使用定长数组存储元素。
2.动态顺序表:使用动态开辟的数组存储。
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间 大小,所以下面我们实现动态顺序表。
接口如下:
// 对数据的管理:增删查改
void SeqListInit(SeqList* ps); // 顺序表的初始化
void SeqListDestroy(SeqList* ps); // 顺序表的销毁
void SeqListPrint(SeqList* ps); // 顺序表的打印
void Check_Capacity(SeqList* ps); // 检查是否需要扩容
void SeqListPushBack(SeqList* ps, SLDataType x); // 顺序表的尾插
void SeqListPushFront(SeqList* ps, SLDataType x); // 顺序表的头插
void SeqListPopFront(SeqList* ps); // 顺序表的头删
void SeqListPopBack(SeqList* ps); // 顺序表的尾删
// 顺序表查找
int SeqListFind(SeqList* ps, SLDataType x);
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, int pos, SLDataType x);
// 顺序表删除pos位置的值
void SeqListErase(SeqList* ps, int pos);
//修改指定下标位置元素
void SeqListModify(SeqList *ps, int pos, SLDataType x);
首先,我们要创建一个顺序表类型,该顺序表类型包括了顺序表的起始位置、记录顺序表内已有元素个数的计数器(size),以及记录当前顺序表的容量的变量(capacity)。
typedef int SLDataType; //顺序表类型
typedef struct SeqList // 顺序表的结构(动态)
{
SLDataType* a; //声明了一个指向顺序表的指针,姑且称它为“顺序表指针”
int size; //记录当前顺序表内元素个数
int capacity; //记录当前顺序表的最大容量
} SeqList;
然后,我们需要一个初始化函数,对顺序表进行初始化。
// 顺序表的初始化
void SeqListInit(SeqList *ps) {
assert(ps); //断言,ps不能为NULL
ps->a = NULL; //刚开始时顺序表为空,顺序表指针为NULL
ps->size = 0; //起始时元素个数为0
ps->capacity = 0; //容量为0
}
因为顺序表所用的内存空间是动态开辟在堆区的,所以我们在使用完后需要及时对其进行释放,避免造成内存泄漏。
// 顺序表的销毁
void SeqListDestroy(SeqList *ps) {
assert(ps);
free(ps->a);//释放顺序表指针指向的空间
ps->a = NULL;
ps->size = 0; //元素个数置0
ps->capacity = 0;//容量置0
}
// 顺序表的打印
void SeqListPrint(SeqList *ps) {
assert(ps);
//循环遍历顺序表
for (int i = 0; i < ps->size; i++) {
printf("%d ", ps->a[i]);
}
printf("\n");
}
仔细想想,我们每次需要增加数据的时候,首先都应该先检查顺序表内元素个数是否已达顺序表容量上限。若已达上限,那么我们就需要先对顺序表进行扩容,然后才能增加数据。
// 扩容
void Check_Capacity(SeqList *ps) {
if (ps->capacity == ps->size) {
// 如果容量等于大小说明满了需要扩容 如果size和capacity为0,说明是初始化状态,默认给4个字节,否则就扩容两倍
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
// 将扩容的指针放到临时变量中
SLDataType *tmp = (SLDataType *) realloc(ps->a, sizeof(SLDataType) * newcapacity);
// 若传入realloc的指针为空指针(NULL),则realloc函数的作用相当于malloc函数。
if (tmp == NULL) {
perror("realloc fail");
exit(-1);
} else {
ps->a = tmp;
ps->capacity = newcapacity;
}
}
}
要想在顺序表的表头插入数据,那么就需要先将顺序表原有的数据从后往前依次向后挪动一位,最后再将数据插入表头。
// 顺序表的头插
void SeqListPushFront(SeqList *ps, SLDataType x) {
assert(ps);
Check_Capacity(ps); //检查容量
// 头插-插入到顺序表的最开始的位置,下标是0,但是需要挪动整个顺序表
for (int i = ps->size; i >= 0; i--) // 将所有数据挪动到下一个位置
{
// size的下标是最后位置的下一个
ps->a[i] = ps->a[i - 1];
}
ps->a[0] = x;
ps->size++;
}
尾插相对于头插就比较简单了,直接在表尾插入数据即可。
// 顺序表的尾插
void SeqListPushBack(SeqList *ps, SLDataType x) {
assert(ps);
Check_Capacity(ps);
// 尾插-插入到顺序表中的下一个位置,下标也就是ps->size
ps->a[ps->size] = x;
ps->size++;
}
要做到在指定下标位置插入数据,首先我们需要得到一个下标位置,然后从该下标位置开始(包括该位置),其后的数据从后往前依次向后挪动一位,最后将数据插入到该下标位置。pos默认从1开始。
// 顺序表在pos位置插入x,
void SeqListInsert(SeqList *ps, int pos, SLDataType x) {
pos--;//pos对应数组下标,pos默认从1开始。
assert(ps);
assert(pos >= 0 && pos <= ps->size);
Check_Capacity(ps);
//将pos后面的数据向后挪动一个位置
for (int i = ps->size; i > pos; i--) {
ps->a[i] = ps->a[i - 1];
}
ps->a[pos] = x;
ps->size++;
}
我们可以发现,头插和尾插实际上就是在下标为0的位置和下标为ps->size的位置插入数据,也就意味着我们可以统一使用该函数来实现头插和尾插。
// 顺序表的尾插
void SeqListPushBack(SeqList *ps, SLDataType x) {
SeqListInsert(ps, ps->size + 1, x);//+1 是对应逻辑下标,pos位置默认从1开始
}
// 顺序表的头插
void SeqListPushFront(SeqList *ps, SLDataType x) {
SeqListInsert(ps, 1, x); //1是逻辑下标,真实下标是0
}
要删除表头的数据,我们可以从下标为1的位置开始,依次将数据向前覆盖即可。
// 顺序表的头删
void SeqListPopFront(SeqList *ps) {
assert(ps);
assert(ps->size);
// 将顺序中除了第一个位置的所有数据向前挪动一个下标
for (int i = 0; i < ps->size - 1; i++)
{
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
尾删就更简单了,直接将顺序表的元素个数减一即可。
// 顺序表的尾删
void SeqListPopBack(SeqList *ps) {
assert(ps);
assert(ps->size); //保证顺序表不为空
ps->size--;
}
要删除指定下标位置的数据,我们只需要从下标位置开始,其后的数据从前向后依次覆盖即可。
//顺序表删除pos位置的值
void SeqListErase(SeqList *ps, int pos) {
pos--;//对其数组下标
assert(ps);
assert(pos >= 0 && pos < ps->size);
//从pos位置开始,后面数据都向前挪动一个位置
for (int i = pos; i < ps->size - 1; i++) {
ps->a[i] = ps->a[i + 1];
}
ps->size--;
}
同样的道理,头删和尾删实际上也就是删除下标为0的位置和下标为ps->size - 1的位置的数据,也就意味着我们可以统一使用该函数来实现头删和尾删。
// 顺序表的头删
void SeqListPopFront(SeqList *ps) {
SeqListErase(ps, 1);
}
// 顺序表的尾删
void SeqListPopBack(SeqList *ps) {
SeqListErase(ps, ps->size);
}
查找数据也相对简单,直接遍历一次顺序表即可,若找到了目标数据,则停止遍历,并返回该数据的下标,否则返回-1。
// 顺序表查找
int SeqListFind(SeqList *ps, SLDataType x) {
int i = 0;
for (i = 0; i < ps->size; i++) {
if (ps->a[i] == x) {
return i + 1;//i+1对应逻辑下标
}
}
return -1;//找不到返回-1
}
测试代码:
#include "SeqList.h"
void Test1() {
struct SeqList s1;
SeqListInit(&s1);
SeqListPushBack(&s1, 1);
SeqListPushBack(&s1, 2);
SeqListPushBack(&s1, 3);
SeqListPushBack(&s1, 4);
SeqListPrint(&s1);//1 2 3 4
SeqListPopFront(&s1);
SeqListPopFront(&s1);
SeqListPrint(&s1);//3 4
SeqListPopBack(&s1);
SeqListPopBack(&s1);
SeqListPrint(&s1);//
SeqListPushFront(&s1, 0);
SeqListPushFront(&s1, -1);
SeqListPrint(&s1); //-1 0
SeqListInsert(&s1, 1, 61);//下标为1插入一个61
SeqListInsert(&s1, 2, 62);//下标为1插入一个62
SeqListInsert(&s1, 3, 63);//下标为1插入一个63
SeqListInsert(&s1, 4, 64);//下标为1插入一个64
SeqListPrint(&s1); //61 62 63 64 -1 0
SeqListErase(&s1, 1);
SeqListErase(&s1, 1);
SeqListErase(&s1, 1);
SeqListErase(&s1, 1);
SeqListPrint(&s1);//-1 0
SeqListDestroy(&s1);
}
int main() {
Test1();
return 0;
}
修改数据,就直接对该位置的数据进行再次赋值即可。
//修改指定下标位置元素
void SeqListModify(SeqList *ps, int pos, SLDataType x) {
pos--;//对应数组下标
assert(ps);
assert(pos >= 0 && pos < ps->size);//检查输入下标的合法性
ps->a[pos] = x; //修改数据
}
问题:
- 中间/头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。