在了解顺序表之前我们先要知道线性表这个概念
1、线性表
1.1 线性表的概念
对于一组拥有n个数据元素的线性表,其严格数学定义是:其中任何一个数据元素a(i),有且仅有一个直接前驱a(i-1),有且仅有一个直接后继a(i+1)。首元素a0无直接前驱,尾元素a(n-1)无直接后继。
满足这种数学关系的一组数据,当中的数据是一个挨着一个的,常被称为一对一关系。反之,如果数据之间的关系不是一对一的,就是非线性的。
1.2 生活中的例子
例如:一个班级中的以学号编排的学生,一条正常排队等候的队列等。
拥有同样的特点:除了首尾元素,其余任何一个元素前后都对应相邻的另一个元素
1.3 注意:
线性表是一种数据内部的逻辑关系,与存储形式无关
线性表既可以采用连续的顺序存储,也可以采用离散的链式存储
2、顺序表
2.1 顺序表的基本概念
顺序表:顺序存储的线性表
链式表:链式存储的线性表,简称链表
顺序存储:将数据存储到一片连续的内存中,在c语言环境下,可以是具名的栈数组,或者是匿名的堆数组
存储方式不仅仅只是提供数据的存储空间,而是必须要能体现数据之间的逻辑关系。
当采用顺序存储的方式来存放数据时,唯一能用来表达数据间本身的逻辑关系的就是存储位置。
2.2 基本操作
在执行增删改删等基本动作之前,需要初始化顺序表。在此篇博客中创建的顺序表是带有管理结构体的。
顺序表设计:为了方便操作顺序表,需要一个专门管理顺序表的“管理结构体”
管理结构体中一般会包含:1、顺序表总容量
2、顺序表当前最末元素下标位置
3、顺序表指针
//管理结构体代码
typedef struct //给类型取别名
{
int capacity; //顺序表容量
int last; //最末元素下标
int* data; //顺序表,以整型数据为例
}sequenceList
在使用顺序表之前需要初始化顺序表,所谓初始化就是建立一个不包含任何元素的顺序表,设置好管理结构体中的表的总容量、末元素下标,申请号顺序表内存空间等系列准备工作。
//初始化顺序表
sequenceList* init(int cap)
{
sequenceList* list = malloc(sizeof(sequenceList)); //开辟堆区空间成功则开始初始化
if(list != NULL)
{
list->capacity = cap; //顺序表的总容量为传参进来的cap
list->last = -1; //顺序表的末元素下标从-1开始
list->data = malloc(cap * sizeof(int)); //顺序表的数据部分也需要开辟堆区空间
if(list->data == NULL)
{
free(list); //开辟失败需要释放顺序表开辟的堆区空间,避免内存泄漏
return NULL;
}
}
return list;
}
初始化成功之后就可以对顺序表进行一些基本操作,例如增加删除节点。
在顺序表中增加一个数据,可以有多种方式,比如在原数组的末尾增加,或者在原数组的头部增加,或者在数组中间任意一个位置增加。根据实际需要来决定。
//顺序表头部增加或删除数据
//在顺序表增加之前要进行顺序表是否为满的判断,如果顺序表为满,则不可以再进行增加
//在顺序表删除之前要进行顺序表是否为空的判断,如果顺序表为空,则不可以再进行删除
//判断顺序表是否为空
bool isEmpty(sequenceList *list)
{
return list->last == -1; //最简洁的判断方式,当顺序表末元素的下标为-1时则为空
}
//判断顺序表是否为满
bool isFull(sequenceList* list)
{
return list->last == list->capacity - 1; //最简洁的判断方式,当顺序表的下标等于顺序表总容量大小 - 1则说明顺序表已满(顺序表第一个元素的下标为0)
}
//在顺序表表头插入一个新数据
bool insert(sequenceList* list,int data)
{
//判断顺序表是否为满
if(isFull(list))
{
return false;
}
//将所有数据往后移动一位
for(int i = list->last;i >= 0;i--)
{
list->data[i + 1] = list->data[i];
}
//将增加的数据放到表头,末元素的下标+1
list->data[0] = data;
list-<last++;
return true;
}
//将顺序表表头的数据删除掉
bool remove(sequenceList* list)
{
//判断顺序表是否为空
if(isEmpty(list))
{
return false;
}
//将所有数据往前移动一位
for(int i = 0;i < list->last;i++)
{
list->data[i] = list->data[i + 1];
}
//顺序表的末元素下标-1
list->last--;
return true;
}
但是很多时候都不是删除顺序表的表头数据,而是删除指定的数据,怎么实现呢,只需要在删除表头数据的代码增加一小段代码就可以
//将顺序表的数据删除掉
bool remove(sequenceList* list)
{
//判断顺序表是否为空
if(isEmpty(list))
{
return false;
}
//找到要删除的节点
int pos = -1;
for(int i = 0;i < list->last;i++)
{
if(list->data[i] == data)
{
pos = i;
break;
}
}
//如果顺序表没有我们要删除的数据
//if(pos == -1)
if(i > list->last)
{
return false;
}
//将要删除的数据之后的数据往前移动一位
for(int i = pos;i < list->last;i++)
{
list->data[i] = list->data[i + 1];
}
//顺序表的末元素下标-1
list->last--;
return true;
}
这样貌似就满足了我们的指定删除数据的需求,但仔细一看,似乎代码长度好像又太长了,又想到我们的需求里面本身就有查找的功能,那我们就可以把上面的代码拆分为删除和查找两个功能函数
//将指定数据删除掉
bool remove(sequenceList* list)
{
//判断顺序表是否为空
if(isEmpty(list))
{
return false;
}
//获取删除数据的下标
int pos = find(list,data);
if(pos == -1)
{
return false;
}
//将所有数据往前移动一位
for(int i = pos;i < list->last;i++)
{
list->data[i] = list->data[i + 1];
}
//顺序表的末元素下标-1
list->last--;
return true;
}
//查找指定数据的下标
int find(sequenceList* list,int data)
{
//判断顺序表是否为空
if(isEmpty(list))
{
return false;
}
//找到要删除的节点
int pos = -1;
for(int i = 0;i < list->last;i++)
{
if(list->data[i] == data)
{
pos = i;
return pos;
}
}
//如果顺序表没有我们要删除的数据
//if(pos == -1)
if(i > list->last)
{
return -1;
}
}
现在有了查找这个函数,那么写顺序表的修改功能就很容易了
//修改顺序表的指定数据
bool change(sequenceList* list,int find,int data)
{
//需要判断顺序表是否为空
if(isEmpty(list))
{
return false;
}
//利用查找函数找到要修改的顺序表数据的下标
int pos = find(list,find);
if(pos == -1)
{
return false;
}
//直接赋值就可以完成修改
list->data[pos] = data;
return true;
}
那我们初始化顺序表之后,对顺序表进行了一系列的操作之后,怎么知道自己是否操作成功呢,那我们就需要一个查看的功能函数,它能帮助我们测试我们的操作是否成功
//查看顺序表的内容
void show(sequenceList* list)
{
//判断顺序表是否为空
if(ifEmpty(list))
{
return;
}
for(int i = 0;i < list->last;i++)
{
printf("%d ",list->data[i]);
}
printf("\n");
}
在我们使用完顺序表之后,程序之前需要销毁顺序表,因为开辟了堆区空间,如果不使用free,就有内存泄漏的风险,可以造成程序崩溃等危险
//销毁顺序表
void destroy(sequenceList* list)
{
//判断顺序表是否为空
if(list == NULL)
{
return;
}
//释放顺序表的数据部分,再释放掉顺序表
free(list->data);
free(list);
}
2.3 顺序表的优缺点
顺序存储中,由于逻辑关系是用物理位置来表达的,因此上述示例代码可以很清楚看到,增删数据都非常困难,需要成片地移动数据。顺序表对数据节点地增删操作时很不友好的。
优点:
1、不需要多余地信息来记录数据间地关系,存储密度高
2、所有数据顺序存储在一片连续地内存中,支持立即访问任意一个随机数据,例如:顺序 表中第i个节点是list->data[i]
缺点:
1、插入、删除时需要保持数据地物理位置反映其逻辑关系,一般需要成片移动数据
2、当数据节点数量较多时,需要一整片较大的连续内存空间
3、当数据节点数量变化剧烈时,内存的释放和分配不灵活
那么,关于顺序表的相关函数接口就到这里了,那么最后再加上我的主函数测试代表
int main()
{
sequenceList* list = init_list(10);
if (list == NULL)
{
printf("初始化顺序表失败!\n");
}
else
{
printf("初始化顺序表成功!\n");
}
int n;
while (1)
{
scanf("%d", &n);
if (n > 0)
{
if (!insert(list, n))
{
printf("容量已满,插入失败!\n");
continue;
}
}
else if (n < 0)
{
if (!removeNode(list, -n))
{
printf("查无此数,删除失败!\n");
continue;
}
}
show(list);
}
destroy(list);
return 0;
}