线性表
线性表是一种具有N个相同特性的元素的有序序列,顺序表其实是线性表的一种,顺序表内存着N个相同特性的元素的有序序列
所谓的相同特性,可以从两个层面解释:物理结构和逻辑结构。
物理结构是数据在内存上的存储形式,
而逻辑结构是人为想象出的数据的形式
线性表的逻辑结构是线性的,而物理结构不一定是线性的。
其他的线性表:顺序表,链表,栈,队列,字符串等
顺序表
顺序表的底层其实就是数组,它与数组一样,在内存上是连续存储的,数据地址是连续的。
顺序表在内存中的图示其实与数组一致:
相较于数组,顺序表只不过增加了增删改查等接口
顺序表的分类
顺序表有两种分类:静态顺序表和动态顺序表。
静态顺序表使用定长数组来存储元素,静态顺序表结构体如下:
#define N 10
typedef int SLDataType;
typedef struct SeqList {
SLDatatype arr[N]; //顺序表的底层是数组,静态顺序表用定长数组表示
int size; //有效长度也称被使用的长度
}SL;
理论上我们可以给顺序表的大小赋予无限大,但是如果顺序表的空间未使用完则会造成内存的浪费,如果给顺序表赋予了较小的空间又不够用,这是个头疼的问题,为了解决这种问题,我们可以使用动态顺序表,通过按需申请内存来避免内存浪费和内存不足的问题,动态顺序表结构如下:
typedef int SLDataType;
typedef struct SeqList {
SLDatatype* arr; //指针指向指定空间再开辟动态内存
int max_size; //空间容量
int size; //有效长度或被使用的长度
}SL;
动态顺序表的逻辑实现
当我们声明一个顺序表后,需要对顺序表进行初始化后才能使用:
顺序表初始的样子应该是没有数据的,且还没被创建动态内存使用的,因此结构体的最大空间容量和被使用长度应该同时为0,指向动态内存的指针应该为NULL
//顺序表初始化
void SLInit(SL* ps) {
ps->arr = NULL;
ps->max_size = ps->size = 0;
}
当顺序表初始化完成后,就可以在顺序表里插入数据了,这个操作也分两种,头插和尾插。
头插的时候,顺序表的数据需要从最后一位开始依次向后移动一位,直到顺序表第一位无数据时将待插数据插入,然后更新顺序表有效个数
//顺序表的头插
void SLPushFront(SL* ps, SLDataType x) {
assert(ps);//如果在为空的地方插入数据,报错
//顺序表中的元素相继向后退一位
for (int i = ps->size; i > 0; i--) {
ps->arr[i] = ps->arr[i - 1];//arr[1]=arr[0]
}
ps->arr[0] = x;
ps->size++;
}
而尾插的时候就显得简单很多,只需要在顺序表末尾直接插入数据,随后更新顺序表的有效数据个数即可
//顺序表的尾部插入
void SLPushBack(SL* ps, SLDataType x) {
assert(ps);
ps->arr[ps->size++] = x;
}
逻辑上是这样,但这样的代码其实是有问题的,因为我们是通过动态内存修改顺序表的大小,但是我们上面头插和尾插并没有进行过动态内存的调整,简单的说:如果顺序表为空时或者顺序表的有效空间等于最大空间时,就是错误的,因此我们在进行头插和尾插的操作时需要对当前顺序表的空间进行检查。
//插入元素时的空间检查
void SLCheckCapacity(SL* ps) {
//先检查是否有空间进行插入,如果没有空间先扩容
//扩容一般是 成倍数扩容
if (ps->max_size == ps->size) {
//如果当前的最大空间为0,顺序表当前为初始化完的状态,那么赋予顺序表大小为4
//如果顺序表当前顺序表的最大空间不是0,那么顺序表的最大容量呈倍数扩大
int newCapacity = ps->max_size == 0 ? 4 : 2 * ps->max_size;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
//也有可能扩容失败
if (tmp == NULL) {
perror("tmp");
exit(-1);
}
ps->arr = tmp;//指针指向新的开辟内存的地址
ps->max_size = newCapacity;//更新顺序表的最大容量
}
}
那么只需在上述头插和尾插代码中,进行插入操作前进行当前顺序表空间的检查就没问题了
//顺序表的头部插入
void SLPushFront(SL* ps, SLDataType x) {
assert(ps);//如果在为空的地方插入数据,报错
SLCheckCapacity(ps);//检查是否有足够的空间插入
//顺序表中的元素相继向后退一位
for (int i = ps->size; i > 0; i--) {
ps->arr[i] = ps->arr[i - 1];//arr[1]=arr[0]
}
ps->arr[0] = x;
ps->size++;
}
//顺序表的尾部插入
void SLPushBack(SL* ps, SLDataType x) {
assert(ps);
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
有插入那么就有删除,顺序表的删除也分头删和尾删。
对于头删操作,我们可以通过顺序表的下一位数据向上一位数据进行覆盖进行修改,然后让顺序表的有效数据个数-1,当顺序表的有效数据个数-1后,最后一个4就不存在于顺序表中了:
顺序表头删代码如下:
//顺序表的头删
void SLPopFront(SL* ps) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
从上面的头删可得,通过顺序表的有效个数进行-1操作后,顺序表会舍弃最后一个数据,这其实就是尾删,尾删相对于头删操作,省去了数据依次向前挪一位的操作。
//顺序表的尾删
void SLPopBack(SL* ps) {
assert(ps);
ps->size--;
}
而对于顺序表的指点位置插入,其实就是另类的头插,只不过将头插的位置变成指定位置和需要注意插入数据的位置是否合法罢了
//顺序表的指定位置插入
void SLInsert(SL* ps, SLDataType x, int pos) {
assert(ps);
//检查插入的位置是否合理
assert(pos >= 0 && pos <= ps->size);
//检查是否有足够的空间插入
SLCheckCapacity(ps);
//pos位置开始往后退一位
for (int i = ps->size; i >pos; i--) {
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
对于顺序表在指定位置删除,也是另类的头删,需要注意的问题和顺序表指定位置插入一致
//顺序表在指定位置删除
void SLErase(SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);
for (int i = pos; i < ps->size-1; i++) {
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
因为顺序表的底层是数组,因此对于顺序表的查找我们可以采取数组查找元素的方法:从头开始遍历,如果找到待查找元素则返回。
//顺序表查找元素
void SLFind(SL* ps, SLDataType x) {
assert(ps);
for (int i = 0; i < ps->size - 1; i++) {
if (ps->arr[i] == x) {
printf("%d\n", i);
break;
}
}
}