一、什么是顺序表
顺序表其实就是线性表的一种,他的底层实际上就是一个数组。我们一般称之为“SeqList”代表sequence + list 顺序表。
注:线性表结构在物理上不一定是线性的,但是逻辑上一定是线性的
顺序表在物理结构上是相邻的,在逻辑结构上也是相邻的。
二、顺序表的实现
既然顺序表的底层是数组,那么我们用画图来生动形象的表示顺序表中的元素。
上图即为一个“灰常简单”的数组示意图,首先我们要知道,顺序表的实现分为两种,一种是静态数组(定死数组的长度),另一种是动态数组(你长我更长),那么两者之间有什么区别呢?
2.1 静态数组
静态数组的结构体代码如下:
struct SeqList{
int arr[10]
int size;
};
在这里arr就是我们顺序表的数组,size指我们已经占用的有效元素的个数,但是这样就会出现一些问题,比如数组空间满了但是数据还没用完的情况出现。
猪大哥:“喂,我还没上车呢,怎么装不下我了!!”笑死,所以这也是顺序表非常不好的一个情况,于是我们引出了动态数组。
2.2动态数组
动态动态顾名思义就是让数组的容量动起来,随着数据的不断增多而增加,这个时候只有一个存放有效数据的size肯定是行不通的,于是就引入了另一个变量---capacity用来存放当前顺序表的最大容量。
typedef struct SeqList {
SLDataType* arr;
int size; //有效数据大小
int capacity;//容量
}SL;
满了我们就增容,不满我们就笑呵呵,无敌了。
2.3静态数组和动态数组的区别
两种方式都介绍完了,那么我们该用哪一种呢?
静态:定长 ,给小了不够用 给大了浪费空间
动态:动态增容,非常灵活
由此看来,还是动态数组更胜一筹啊!!!
三、顺序表方法
利用顺序表我们可以实现许多功能,下面我们会一一讲解,先来看看都有什么方法。
//方法声明
void SLInit(SL *ps);//初始化
void SLDestory(SL* ps); //销毁
void SLPushBack(SL* ps, SLDataType x); //尾插
void SLPushFront(SL* ps, SLDataType x);//头插
void SLPopBack(SL* ps); // 尾删
void SLPopFront(SL* ps);//头删
void SLPrint(SL s);
void SLInsert(SL* ps, int pos, SLDataType x);//在指定位置之前插入元素
void SLErase(SL* ps, int pos);//删除指定位置的元素
SLDataType SLFind(SL* ps, SLDataType x);//查找
下面我们就来依次讲解。
3.1初始化顺序表
初始化顺序表就是让一个顺序表“诞生”,就是空的什么样,我什么样。
可以看到里面什么也没有,就是数组空,容量空,有效元素空,三大皆空
void SLInit(SL* ps) {
ps->arr = NULL;
ps->size = ps->capacity = 0;
}
这样我们就完成了顺序表的初始化。
3.2销毁顺序表
与初始化类似,直接无脑归零,但是如果还有元素,记得给人家释放了。
void SLDestory(SL * ps){
if (ps->arr) {
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
3.3尾插数据
在我们进行一系列操作时,首先要画图,这样会帮助我们更好地理解。以上面动物园的车为例,如果这个车是个动态的数组,猪大哥想要上车并且在队伍的最后,应该怎么操作呢
此时数组的长度是4,size正好在队伍的最后,猪大哥想要上去首先要进行扩容,在原有空间基础上进行扩容,这里我们使用realloc(待扩容的空间,扩容大小)。综合起来就是,先判断空间是否已经满了,没满直接插入,满了就进行扩容。
注:一定记得判断是否传入ps,表都没有自然没办法插入了
void SLPushBack(SL* ps, SLDataType x) {
assert(ps);
if (ps->size == ps->capacity) {
//申请空间 增容--》realloc
//一次增容增多大? ---》2倍
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp =(SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));//要申请多大的空间
if (tmp == NULL) {
perror("realloc fail");
exit(1);//程序直接退出,不再继续执行。
}
//空间申请成功
ps->arr = tmp;
ps->capacity = newcapacity;
}
ps->arr[ps->size] = x;
ps->size++;
}
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
这条语句的意思是,如果capacity=0,那么首先我要把她变成4,否则就在原有的基础上扩容2倍就可以了。不要忘记最后的size++。
3.4头插数据
头插数据与尾插数据十分类似,但是头插的空间不是凭空在前面创造出一个,而是将后面的元素都向后挪动一位从而空出一个空间来。
问题:怎么挪,直接从0开始arr[i+1] =arr[i] 吗?
我们画个图来直观的看一下:
我们要头插一个数据888,如果进行从0开始的arr[i+1] =arr[i] 的话就会出现以下情况
他会把后面的元素进行覆盖,空间是出来一个了但是元素全乱了,于是我们采用从后面开始移动的办法。
都挪动过去之后,空间自然就出来了。
void SLPushFront(SL* ps, SLDataType x) {
assert(ps);
if (ps->size == ps->capacity) {
//申请空间 增容--》realloc
//一次增容增多大? ---》2倍
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));//要申请多大的空间
if (tmp == NULL) {
perror("realloc fail");
exit(1);//程序直接退出,不再继续执行。
}
//空间申请成功
ps->arr = tmp;
ps->capacity = newcapacity;
}
for (int i = ps->size; i > 0; i--) {
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[0] = x;
ps->size++;
}
3.5打印数据
这个没什么好说的,直接上代码
void SLPrint(SL s) {
for (int i = 0; i < s.size; i++) {
printf("%d ", s.arr[i]);
}
}
3.6尾删数据
尾删特别简单,不用那种free掉,直接size--就可以了,但是需要判断里面是不是有数据。
void SLPopBack(SL* ps) {
assert(ps);
assert(ps->size);
ps->size--;
}
3.7头删数据
头删也需要进行数据的挪动,不过这次是从前往后的进行挪动,这样才不会影响其他数据。
void SLPopFront(SL* ps) {
assert(ps);
assert(ps->size);
for (int i = 0; i < ps->size - 1; i++) {
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
3.8指定位置之前插入数据
void SLInsert(SL* ps, int pos, SLDataType x)
看着函数声明,谈谈注意事项。
1.首先ps肯定要加个断言,毋庸置疑。这个pos,也需要加,因为肯定会有pos不合法的地方,由于我们实在pos前面加,所以pos是可以等于size的。
2.怎么插入呢?首先肯定要进行数据的挪动,那么我们要找到pos处,从pos开始进行挪动。
3.空间满了进行扩容。
4.size++
void SLInsert(SL* ps, int pos, SLDataType x) {
assert(ps);
assert(pos >= 0 && pos <= ps->size);
if (ps->size == ps->capacity) {
//申请空间 增容--》realloc
//一次增容增多大? ---》2倍
int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));//要申请多大的空间
if (tmp == NULL) {
perror("realloc fail");
exit(1);//程序直接退出,不再继续执行。
}
//空间申请成功
ps->arr = tmp;
ps->capacity = newcapacity;
}
for (int i = ps->size; i > pos; i--) {
ps->arr[i] = ps->arr[i - 1];//最后应该是arr[pos+1] = arr[pos]
}
ps->arr[pos] = x;
ps->size++;
}
3.9指定位置删除数据
void SLErase(SL* ps, int pos) {
assert(ps);
assert(pos >= 0 && pos < ps->size);//检验pos合法性
for (int i = pos; i < ps->size-1; i++) {//挪动数据
ps->arr[i] = ps->arr[i + 1];
}
ps->size--;
}
3.10查找数据
SLDataType SLFind(SL* ps, SLDataType x) {
assert(ps);
for (int i = 0; i < ps->size; i++) {
if (ps->arr[i] == x) {
return i;
}
}
return -1;
}
四、小结
其实顺序表的实现十分简单,真正的重点还在链表这里,下一次我会给大家带来链表相关的讲解包括力扣上的编程题。