前言:
顺序表功能如图所示


一、顺序表的概念结构及其特点
1.1顺序表的基本概念
++顺序表++ 是计算机科学中++线性表++的++两种基本存储结构之一++ (另一种是链表),它通过++一组连续的存储单元++ 来++依次存储++线性表中的数据元素,使得元素的++物理存储位置++ 与++逻辑顺序++完全一致。
1.2顺序表的结构
1.逻辑结构:在逻辑结构上,顺序表是线性的,就像幼儿园里小朋友们,一个跟着一个排队,有一个小朋友做排头,有一个小朋友做排尾,在中间的小朋友每一个都知道前面的小朋友是谁,在中间的每一个小朋友知道后面的小朋友是谁,就如同用一根线,将其串联起来。
如下图所示:
2.物理结构:在物理结构上,顺序表是基于数组的,一个连续开辟的空间,每个元素的地址是相互紧挨,也如同用一根线一样串联起来,所以在物理结构上也是线性的。
1.3顺序表的特点
特点:
①顺序表具有动态分配空间或则静态开辟空间、支持随机访问和顺序访问,逻辑顺序与物理顺序一致。
②每个元素可以都有唯一的位置,可以通过索引直接访问元素。
③值得注意的是:元素可以是任意类型,包括基本数据类型、结构体等。
二、顺序表的分类
顺序表与数组的关联,顺序表的底层结构是数组,但又不完全是数组,它是对数组基于封装,使其具有了增、删、改、查及其接口的功能,使其具有更加完善的功能适配。
一般来说,我们将顺序表分为两类,一类是静态顺序表,一类是动态顺序表。
①静态顺序表:使⽤定⻓数组存储元素。
对于静态顺序表而言,因为其空间是固定的,我们改给多大空间合适呢,如果空间给小了,导致数据存储不下,引起数据丢失等问题,如果空间给大了,导致空间浪费,程序运行效率降低。
②动态顺序表:使用可更改空间的数组存储元素。
对于动态顺序表而言,因为其空间是可变的,我们可以通过灵活的改变空间,以适应空间不足,或者空间过大的问题。
三、顺序表的实现
这里我们以动态顺序表为例,实现顺序表的各个功能和使用,我们通过三个文件实现顺序表:
①SeqList.h 顺序表函数的声明 及 顺序表结构体的实现
②SeqList.c 顺序表函数的实现
③Test.c 测试顺序表函数
3.1 SeqList.h的准备
头文件需要包含的声明为:
cpp
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#pragma once
//动态顺序表的实现
typedef int SLDataType; //通过将 int 命名为SLDataType 以便后续更改类型
//定义顺序表的结构
typedef struct SeqList
{
SLDataType* arr; //动态开辟顺序表
int size; //有效元素个数
int capacity; //顺序表的容量
}SL;
//顺序表初始化
void SLInit(SL* ps);
//顺序表销毁
void SLDestroy(SL* ps);
//顺序表扩容
void SLCheckCapacity(SL* ps);
//顺序表尾插
void SLPushBack(SL* ps, SLDataType x);
//顺序表头插
void SLPushFront(SL* ps, SLDataType x);
//顺序表尾删
void SLPopBack(SL* ps);
//顺序表头删
void SLPopFront(SL* ps);
//顺序表指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x);
//顺序表指定位置删除
void SLErase(SL* ps, int pos);
//顺序表的打印
void SLPrint(SL* ps);
//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x);
对于这个头文件我们主要关注 :++顺序表的结构体定义++
cpptypedef int SLDataType; typedef struct SeqList { SLDataType* arr; //动态开辟顺序表 int size; //有效元素个数 int capacity; //顺序表的容量 }SL;
①首先定义了一个结构体名为:struct SeqList
②通过typedef将其重命名为 SL
③成员分析:
成员一:SLDataType* arr;
分析:1.定义了一个指针变量,后续开辟动态内存,用该指针进行维护
2.将int类型重命名为SLDataType,方便对其进行改造,只需要将int进行改变,可以将其变为char 、 struct等类型
3.如图所示用arr维护动态内存开辟的空间,可以将动态内存开辟的空间视作数组。
成员二:int size
分析:1.记录数组中存放的有效元素个数。
成员三:int capacity
分析:1.动态内存开辟的空间总大小,也可以认为数组的长度。
通过声明好头文件后,接下来,博主将用图解+代码的方式,对如上的函数进行逐个实现。
3.2 SeqList.c的实现
①顺序表初始化
cpp
//这里不用SL ps作为参数,因为形参是实参的临时拷贝,改变形参不影响实参
void SLInit(SL* ps)
{
assert(ps);
ps->arr = NULL;
ps->size = 0; //默认顺序表有效个数为0
ps->capacity = 0; //默认顺序表空间为0
}
对顺序表初始化的时候,我们需要关注一个重点:初始化函数的参数。
有些帅读者就会向问,若使用void SLInit(SL ps)作为参数会发生什么呢?
答:我们注意到这里是传值传递,对于传值传递 ,形参是实参的临时拷贝,改变形参会不会影响实参呢,显然这里改变形参不会影响实参,那就相当于没有对实参进行初始化,这段代码变成了无效代码。
温馨提示:博主这里将顺序表的默认空间置为0,其实可以用malloc 函数 或者 calloc函数开辟一定大小的内存空间。
②顺序表销毁
cpp
//顺序表的销毁
void SLDestroy(SL* ps)
{
//由于是动态开辟的,所以要进行free
//判断是否为空,如果不为空将其free,并置为空
assert(ps);
if (ps->arr)
{
//不为空
free(ps->arr);
//此时ps->arr仍然保存着arr的地址
//但我们不能够访问这片空间,此时改指针为野指针,我们需要将其置空处理
ps->arr = NULL;
}
//有效个数清空
ps->size = 0;
//开辟的空间清0
ps->capacity = 0;
}
温馨提示:
①:这里因为我们使用动态内存开辟空间,所以尤其要重视对于内存的释放,如果有帅观众还不太熟悉动态内存开辟空间的话,可以看一下博主写的动态内存开辟的博客。
②:将空间存储的有效个数 和 空间的总大小置空处理。
③顺序表扩容(核心知识)
cpp
//顺序表的扩容
void SLCheckCapacity(SL* ps)
{
assert(ps);
//什么时候应该扩容?空间不够用的时候。
//什么时候空间不够用?当有效个数等于空间容量的时候。
if (ps->size == ps->capacity)
{
//使用什么进行扩容? realloc调整动态开辟的空间
//每次扩多大?根据数学推导,成倍数增加容量是最好的,一般为2~3倍最佳。
//realloc(ps->arr,ps->capacity * 2 *sizeof(SLDataType) );
//直接传入ps->capacity这样是正确的吗?
//因为我们初始化为0,所以我们需要进行额外处理。
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; //若刚开始为0,则开辟4个空间
SLDataType* tmp=(SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));
//防止空间开辟失败
if (tmp == NULL)
{
perror(tmp);
exit(1);
}
//开辟成功,用ps->arr进行维护
ps->arr = tmp;
//将临时指针置为空
ps->tmp=NULL;
//更新当前空间
ps->capacity = newCapacity;
}
}
对于顺序表扩容,我们要明白以下知识点:
①:什么时候应该扩容?
答:空间不够用的时候,即当有效个数等于空间容量的时候。
②:使用什么进行扩容?
答:使用realloc进行动态内存开辟,但要单独去判断其是否开辟成功
③:每次扩容多大内存?
答:根据数据推导,一般来说,成倍数开辟空间,2~3倍是最佳。
代码解析:
①:
cppif (ps->size == ps->capacity)
这里通过判断是否容量不够,如果size==capacity的话说明不在能够添加数据,要进行扩容处理。
②:
cppint newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
这里读者因为初始化的时候,开辟0个大小的空间,所以运用三目运算符进行判断,如果是0(即第一次开辟空间)就对其开辟四个大小的空间,后续成倍增加空间,保证了空间足够且不过于浪费。
③:
cppSLDataType* tmp=(SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType)); //防止空间开辟失败 if (tmp == NULL) { perror(tmp); exit(1); } //开辟成功,用ps->arr进行维护 ps->arr = tmp; //将临时指针置为空 tmp=NULL; //更新当前空间 ps->capacity = newCapacity; }
这里使用动态内存开辟空间 ,先赋值给临时指针tmp,判断是否内存开辟成功,如果开辟失败则退出程序,反之赋值给原来的指针arr进行维护,最后别忘记更新当前空间的值。
④顺序表尾插
cpp
//顺序表的尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//判断是否空间足够
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
温馨提示:
1.当进行插入数据的时候优先要判断是否空间足够大,如果空间不够要进行扩容处理。
2.对于尾部插入一个元素的示意图:
3.别忘记对size进行加1操作
⑤顺序表头插
cpp
//顺序表的头插
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]
}
//移动结束后,将元素插入第一个位置,即下标为0处。
ps->arr[0] = x;
ps->size++;
}
温馨提示:
1.当进行插入数据的时候优先要判断是否空间足够大,如果空间不够要进行扩容处理。
2.对于头部插入一个元素的示意图:
3.别忘记对size进行加1操作
⑥顺序表尾删
cpp
//顺序表的尾删
void SLPopBack(SL* ps)
{
assert(ps);
//判断是否可以进行删除
assert(ps->size > 0);
//将size个数减少一个即可,删除的元素可以被覆盖
ps->size--;
}
温馨提示:
1.当进行删除元素的时候,要优先判断是否在顺序表中可以进行删除元素。
2.实际上对size--操作即可,size当前位置的元素可以后续被进行覆盖。
3.对于尾部删除一个元素的示意图:
⑦顺序表头删
cpp
void SLPopFront(SL* ps)
{
assert(ps);
//判断是否可以进行删除
assert(ps->size > 0);
for (int i = 0; i<ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1]; //根据最后一个元素移动,确定判断条件 arr[ps->size-2]=arr[ps->size-1]
}
//删除一个元素,ps->size的有效个数要减少
ps->size--;
}
温馨提示:
1.当进行删除元素的时候,要优先判断是否在顺序表中可以进行删除元素。
2.头删的示意图如下所示:
3.别忘记对size进行减1操作。
⑧顺序表指定位置插入
cpp
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
//判断pos是否在有效的位置(size下标可以插入一个元素)
assert(pos >= 0 && pos <= ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
for (int i = ps->size; i>pos; i--)
{
ps->arr[i] = ps->arr[i - 1]; //根据最后一个元素移动,确定判断条件 arr[pos+1]=arr[pos];
}
ps->arr[pos] = x;
//增添一个元素,有效个数要增加1
ps->size++;
}
温馨提示:
1.判断待插入元素的位置是否合理
2.当进行插入数据的时候优先要判断是否空间足够大,如果空间不够要进行扩容处理。
3.指定位置插入数据的示意图:
4.别忘记对size进行加1操作。
⑨顺序表指定位置删除
cpp
void SLErase(SL* ps, int pos)
{
assert(ps);
//判断pos是否在有效的位置 (size下标没有元素可删除)
assert(pos >= 0 && pos < ps->size);
assert(ps->size>0);
for (int i = pos; i<ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1]; //根据最后一个元素移动,确定判断条件 arr[size-2]=arr[size-1]
}
//删除一个元素,有效个数要减少一个
ps->size--;
}
温馨提示:
1.判断删除的位置是否有效
2.判断是否可以进行删除元素
3.指定位置删除元素的示意图:
4.最后别忘记进行size减1操作。
⑩顺序表的打印
cpp
//顺序表的打印
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
}
⑪顺序表的查找
cpp
//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
printf("找到了!");
return i;
}
}
//未查找到
printf("顺序表中没有该元素\n");
return -1;
}
温馨提示:
①通过顺序查找,返回查找到的第一个元素的下标。
②如果没有查找到该元素,则返回-1
3.3 Test.c的测试
针对上面11条函数进行了测试,博主建议大家写完一条测试一条,更能有效率的保证代码正确。
cpp
#include"SeqList.h"
void test1()
{
SL s;
SLInit(&s);
SLDestroy(&s);
SLCheckCapacity(&s);
for (int i = 1; i <= 5; i++)
{
SLPushBack(&s, i);
}
SLPrint(&s);
for (int i = 1; i <= 5; i++)
{
SLPushFront(&s, i);
}
printf("\n");
SLPrint(&s);
for (int i = 1; i <= 5; i++)
{
SLPopFront(&s);
}
printf("\n");
SLPrint(&s);
SLInsert(&s, 0, 99);
SLInsert(&s, 2, 88);
SLInsert(&s, s.size, 77);
printf("\n");
SLPrint(&s);
SLErase(&s, 0);
SLErase(&s, 2);
SLErase(&s, s.size - 1);
printf("\n");
SLPrint(&s);
printf("\n");
int ret=SLFind(&s, 1);
printf("下标为:%d \n", ret);
ret = SLFind(&s, 88);
printf("下标为:%d \n", ret);
ret = SLFind(&s, 0);
printf("下标为:%d \n", ret);
}
int main()
{
test1();
}
四、完整的代码
4.1SeqList.h
cpp
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#pragma once
//动态顺序表的实现
typedef int SLDataType; //通过将 int 命名为SLDataType 以便后续更改类型
//定义顺序表的结构
typedef struct SeqList
{
SLDataType* arr; //动态开辟顺序表
int size; //有效元素个数
int capacity; //顺序表的容量
}SL;
//顺序表初始化
void SLInit(SL* ps);
//顺序表销毁
void SLDestroy(SL* ps);
//顺序表扩容
void SLCheckCapacity(SL* ps);
//顺序表尾插
void SLPushBack(SL* ps, SLDataType x);
//顺序表头插
void SLPushFront(SL* ps, SLDataType x);
//顺序表尾删
void SLPopBack(SL* ps);
//顺序表头删
void SLPopFront(SL* ps);
//顺序表指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x);
//顺序表指定位置删除
void SLErase(SL* ps, int pos);
//顺序表的打印
void SLPrint(SL* ps);
//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x);
4.2SeqList.c
cpp
#include"SeqList.h"
//顺序表的初始化
//这里不用SL ps作为参数,因为形参是实参的临时拷贝,改变形参不影响实参
void SLInit(SL* ps)
{
assert(ps);
ps->arr = NULL;
ps->size = 0; //默认顺序表有效个数为0
ps->capacity = 0; //默认顺序表空间为0
}
//顺序表的销毁
void SLDestroy(SL* ps)
{
//由于是动态开辟的,所以要进行free
//判断是否为空,如果不为空将其free,并置为空
assert(ps);
if (ps->arr)
{
//不为空
free(ps->arr);
//此时ps->arr仍然保存着arr的地址
//但我们不能够访问这片空间,此时改指针为野指针,我们需要将其置空处理
ps->arr = NULL;
}
//有效个数清空
ps->size = 0;
//开辟的空间清0
ps->capacity = 0;
}
//顺序表的扩容
void SLCheckCapacity(SL* ps)
{
assert(ps);
//什么时候应该扩容?空间不够用的时候。
//什么时候空间不够用?当有效个数等于空间容量的时候。
if (ps->size == ps->capacity)
{
//使用什么进行扩容? realloc调整动态开辟的空间
//每次扩多大?根据数学推导,成倍数增加容量是最好的,一般为2~3倍最佳。
//realloc(ps->arr,ps->capacity * 2 *sizeof(SLDataType) );
//直接传入ps->capacity这样是正确的吗?
//因为我们初始化为0,所以我们需要进行额外处理。
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; //若刚开始为0,则开辟4个空间
SLDataType* tmp=(SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType)); //防止空间开辟失败
if (tmp == NULL)
{
perror(tmp);
exit(1);
}
//开辟成功,用ps->arr进行维护
ps->arr = tmp;
//对临时变量进行置空
tmp=NULL;
//更新当前空间
ps->capacity = newCapacity;
}
}
//顺序表的尾插
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//判断是否空间足够
SLCheckCapacity(ps);
ps->arr[ps->size++] = x;
}
//顺序表的头插
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]
}
//移动结束后,将元素插入第一个位置,即下标为0处。
ps->arr[0] = x;
ps->size++;
}
//顺序表的尾删
void SLPopBack(SL* ps)
{
assert(ps);
//判断是否可以进行删除
assert(ps->size > 0);
//将size个数减少一个即可,删除的元素可以被覆盖
ps->size--;
}
void SLPopFront(SL* ps)
{
assert(ps);
//判断是否可以进行删除
assert(ps->size > 0);
for (int i = 0; i<ps->size-1; i++)
{
ps->arr[i] = ps->arr[i + 1]; //根据最后一个元素移动,确定判断条件 arr[ps->size-2]=arr[ps->size-1]
}
//删除一个元素,ps->size的有效个数要减少
ps->size--;
}
void SLInsert(SL* ps, int pos, SLDataType x)
{
assert(ps);
//判断pos是否在有效的位置(size下标可以插入一个元素)
assert(pos >= 0 && pos <= ps->size);
//判断空间是否足够
SLCheckCapacity(ps);
for (int i = ps->size; i>pos; i--)
{
ps->arr[i] = ps->arr[i - 1]; //根据最后一个元素移动,确定判断条件 arr[pos+1]=arr[pos];
}
ps->arr[pos] = x;
//增添一个元素,有效个数要增加1
ps->size++;
}
void SLErase(SL* ps, int pos)
{
assert(ps);
//判断pos是否在有效的位置 (size下标没有元素可删除)
assert(pos >= 0 && pos < ps->size);
assert(ps->size > 0);
for (int i = pos; i < ps->size - 1; i++)
{
ps->arr[i] = ps->arr[i + 1]; //根据最后一个元素移动,确定判断条件 arr[size-2]=arr[size-1]
}
//删除一个元素,有效个数要减少一个
ps->size--;
}
//顺序表的打印
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; i++)
{
printf("%d ", ps->arr[i]);
}
}
//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
printf("找到了!");
return i;
}
}
//未查找到
printf("顺序表中没有该元素\n");
return -1;
}
既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。
