文章目录
前言
在了解顺序表之前,我们要先了解一下什么是线性表?
线性表是n个具有相同特性的数据元素的有序数列,在实际应用广泛的线性表有顺序表、链表、栈、队列、字符串……
线性表在逻辑上是线性结构,也就是说是连续的一条直线,但是它在物理结构上不一定是连续的,线性表在物理结构上通常是以数组和链表的形式进行存储的。
顺序表:
链表
本篇文章我们先来了解以下顺序表。
一、顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存
储。在数组上完成数据的增删查改。
顺序表实质上就是数组。
二、顺序表的分类
1.静态顺序表
静态顺序表:使用定长数组存储数据,顺序表定义之后,它的大小就不能改变了。
先来一串代码带大家看看怎么声明一个静态顺序表:
#define N 10//定义数组的大小(元素的个数)
typedef int SLDataType;//顺序表中存储的数据的类型
//静态顺序表
typedef struct SeqList
{
SLDataType a[N];//放数据的数组
SLDataType size;//有效数据的个数
}SeqList;//重定义结构体类型的名称
2.动态顺序表
由静态顺序表的定义可以看出:静态顺序表的定长数组的空间大小由N决定,N如果定大了,空间开多了浪费;如果N定小了,空间开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小
动态顺序表:使用动态开辟的数组存储数据,它的大小是可以改变的。
动态顺序表的声明:
typedef int SLDateType;//顺序表的数据类型
typedef struct SeqList
{
SLDataType* a;//指向动态开辟的数组
int size;//有效数据数
int capacity;//容量空间大小
}SeqList;
三、动态顺序表的接口
我们所调用的函数也叫做接口,是用来实现某一项具体的功能的。
要实现一个动态顺序表需要的接口有以下几个:
void SeqListInit(SeqList* ps);//顺序表的初始化
void SeqListDestory(SeqList* ps);//销毁顺序表(避免内存泄漏)
void Capacity(SeqList* ps);//检查容量
void SLPrint(const SeqList* ps);//打印顺序表的数据(方便观察)
//对数据进行管理——增删查改
//头插和尾插,头删和尾删
void SLPushFront(SeqList* ps, SLDataType x);//头插
void SLPushBack(SeqList* ps, SLDataType x);//尾插
void SLPopFront(SeqList* ps);//头删
void SLPopBack(SeqList* ps);//尾删
//在顺序表中进行查找
int SeqListFind(SeqList* ps, SLDataType x);//查找某个数据
// 顺序表在pos位置插入x
void SeqListInsert(SeqList* ps, size_t pos, SLDataType x);//在pos所指向的数据位置插入数据
// 顺序表删除pos位置的值
void SeqListErase(SeqList*ps, size_t pos);//删除pos所指向的数据
//顺序表中修改pos位置处的值
void SeqListModify(SeqList*ps, size_t pos, SLDataType x);//修改pos所指向的数据
四、接口的实现
1.初始化顺序表
//初始化顺序表
void SeqListInit(SeqList* ps)
{
assert(ps);
ps->a = NULL;//初始化地址
ps->size = 0;
ps->capacity = 0;//初始化数据个数和容量大小
}
关于在程序中检查错误的方式:
①温柔的检查: 如果出现错误,程序就不继续执行 If(ps->size==0) { //……代码
return;//或者exit(-1);因为一般情况下,我们运行程序成功就返回0,则运行失败就返回-1. }
②暴力的检查(推荐用这种): 使用断言,如果发生错误,程序会报警告 assert()函数
2.销毁顺序表
//销毁顺序表
void SeqListDestory(SeqList* ps)
{
assert(ps);
free(ps->a);//释放动态空间
ps->a = NULL;
ps->capacity = ps->size = 0;//将数据个数和容量大小置为0
}
3.检查顺序表容量
//检查容量
void SLCheckCapacity(SeqList* ps)
{
if (ps->capacity == ps->size)
{
int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
SLDataType* temp = (SLDataType*)realloc(ps->a, newcapacity*sizeof(SLDataType));
if (temp == NULL)
{
perror("realloc fail");//如果realloc开辟空间失败就会报错
return;
}
ps->a = temp;
ps->capacity = newcapacity;
}
}
注意:
1.不能直接将返回的指针传给ps,避免因为空间开辟失败导致将空指针传给ps,使得ps丢掉原先的地址。
2.realloc在开辟动态内存空间时,如果传给它的是一个空指针,那么他就会开辟一个新的内存空间,用法类似malloc。
4.打印顺序表
//打印顺序表的数据(方便观察)
void SLPrint(const SeqList* ps)
{
int i = 0;
for (i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
5.头插
//头插
void SLPushFront(SeqList* ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
int end = 0;
for (end = ps->size; end > 0; end--)
{
ps->a[end] = ps->a[end - 1];
}
ps->a[0] = x;
ps->size++;
}
当然,头插也可以直接调用函数SeqListInsert()
SeqListInsert(ps, 0, x);
6.尾插
//尾插
void SLPushBack(SeqList*ps, SLDataType x)
{
assert(ps);
SLCheckCapacity(ps);
ps->a[ps->size] = x;
ps->size++;
}
尾插也可以直接用函数SeqListInsert()
SeqListInsert(ps, ps->size, x);
7.头删
//头删
void SLPopFront(SeqList* ps)
{
assert(ps);
assert(ps->size);//如果顺序表中没有数据了,继续进行头删就会报错
int begin = 0;
for (begin = 0; begin < ps->size-1; ++begin)
{
ps->a[begin] = ps->a[begin + 1];//用后一个元素覆盖前一个元素,相当于头删第一个元素
}
ps->size--;
}
可以借用函数SeqListErase实现头删
SeqListErase(ps, 0);
8.尾删
//尾删
void SLPopBack(SeqList* ps)
{
assert(ps);
assert(ps->size);//如果顺序表中没有元素了仍在删除就会报错
ps->size--;//只能size--不用free被删除的空间,因为free无法释放部分动态开辟的内存空间
}
可以借用函数SeqListErase实现尾删
SeqListErase(ps, ps->size-1);
9.查找数据
//查找某个数据(找到了就返回该数据的下标,没找到就返回-1)
int SeqListFind(SeqList* ps, SLDataType x)
{
assert(ps);
int i = 0;
while (i < ps->size)
{
if (ps->a[i] == x)
{
return i;
}
++i;
}
return -1;
}
目前数据量较小可以直接查找(遍历整个数组),它的时间复杂度为O(n) 如果要用二分法查找,需要先排序,所以不太推荐。
10.插入数据
//在pos所指向的数据位置插入数据
void SeqListInsert(SeqList*ps, size_t pos, int x)
{
assert(ps);
assert(pos <= ps->size);
SLCheckCapacity(ps);
// 挪动数据(错误示范)
/*int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}*/
//正确的做法
size_t end = ps->size;
for (; end > pos; --end)
{
ps->a[end] = ps->a[end - 1];
}
ps->a[pos] = x;
++ps->size;
}
可以用这个函数来代替头插和尾插,但是顺序表一般会常用到尾插,所以还是需要实现头插和尾插。
注意:
上面错误示范中,当end = 0时存在死循环问题。因为size_t是无符号整型,当end = 0时再给end减1就会得到一个特别大的正数而非-1,就会导致程序进入死循环。 (如果只是将end的类型改为int,则在进行end与pos的比较时会发生数据类型转换,会把有符号数提升为无符号数,依然会产生这个问题)
解决办法:①把end的类型改为int,将pos的类型强转为int; ②判断用pos <end 不要用 <= 。
11.删除数据
//删除pos所指向的数据
void SeqListErase(SeqList*ps, size_t pos)
{
assert(ps);
assert(pos < ps->size);
int begin = pos;
while (begin < ps->size - 1)
{
ps->a[begin] = ps->a[begin + 1];
++begin;
}
ps->size--;
}
12.修改数据
//修改pos所指向的数据
void SeqListModify(SeqList*ps, size_t pos, SLDataType x)
{
assert(ps);
assert(pos < ps->size);
ps->a[pos] = x;
}
删除数据、插入数据以及修改数据都可以结合查找数据函数来使用,大家也可以试着自己来实现一下。
五、主函数(测试接口)
这是我用于测试接口的主函数代码,大家要进行测试也可以根据自己的喜好来插入数据或者删除数据,也可以在主函数实现一个菜单来调用这些接口。
#define _CRT_SECURE_NO_WARNINGS
#pragma once
#include"SeqList.h"
void test1()//头插、尾插、头删、尾删
{
SeqList s;
SeqListInit(&s);
SLPushBack(&s, 1);//尾插
SLPushBack(&s, 2);//尾插
SLPushBack(&s, 3);//尾插
SLPushBack(&s, 4);//尾插
SLPushBack(&s, 5);//尾插
SLPushFront(&s, 10);//头插
SLPushFront(&s, 12);//头插
SLPushFront(&s, 13);//头插
SLPushFront(&s, 41);//头插
SLPushFront(&s, 61);//头插
SLPrint(&s);//打印顺序表的数据(方便观察)
SLPopFront(&s);//头删
SLPopFront(&s);//头删
SLPopFront(&s);//头删
SLPopFront(&s);//头删
SLPrint(&s);//打印顺序表的数据(方便观察)
SLPopBack(&s);//尾删
SLPopBack(&s);//尾删
SLPopBack(&s);//尾删
SLPopBack(&s);//尾删
SLPopBack(&s);//尾删
SLPrint(&s);//打印顺序表的数据(方便观察)
SeqListDestory(&s);//创建一个顺序表,使用后要销毁,避免内存泄漏
}
void test2()//查找数据
{
SeqList s;
SeqListInit(&s);
SLPushBack(&s, 1);//尾插
SLPushBack(&s, 2);//尾插
SLPushBack(&s, 3);//尾插
SLPushBack(&s, 4);//尾插
SLPushBack(&s, 5);//尾插
SLPrint(&s);//打印顺序表的数据(方便观察)
//可以用输入数据的方式进行测试也可以直接用在相应位置写上数据进行测试
//SLDataType n = 0;
//printf("请输入要查找的数据:>");
//scanf("%d", &n);
//int ret = SeqListFind(&s, n);//查找某个数据
//if (ret != -1)
//{
// printf("该数据的下标为%d", ret);
//}
//else
//{
// printf("没找到!");
//}
int ret = SeqListFind(&s, 1);//查找某个数据
if (ret != -1)
{
printf("该数据的下标为%d", ret);
}
else
{
printf("没找到!");
}
SeqListDestory(&s);//创建一个顺序表,使用后要销毁,避免内存泄漏
}
void test3()//在pos位置插入一个数
{
SeqList s;
SeqListInit(&s);
SLPushBack(&s, 1);//尾插
SLPushBack(&s, 2);//尾插
SLPushBack(&s, 3);//尾插
SLPushBack(&s, 4);//尾插
SLPushBack(&s, 5);//尾插
SLPrint(&s);//打印顺序表的数据(方便观察)
printf("请输入要插入数据的下标:>");
size_t n = 0;
scanf("%d", &n);
printf("请输入要插入的数据:>");
SLDataType m = 0;
scanf("%d", &m);
SeqListInsert(&s, n, m);//在pos位置插入一个数
SLPrint(&s);//打印顺序表的数据(方便观察)
SeqListDestory(&s);//创建一个顺序表,使用后要销毁,避免内存泄漏
}
void test4()//删除pos所指向的数据
{
SeqList s;
SeqListInit(&s);
SLPushBack(&s, 1);//尾插
SLPushBack(&s, 2);//尾插
SLPushBack(&s, 3);//尾插
SLPushBack(&s, 4);//尾插
SLPushBack(&s, 5);//尾插
SLPrint(&s);//打印顺序表的数据(方便观察)
printf("请输入要删除的数据的下标:>");
size_t n = 0;
scanf("%d", &n);
SeqListErase(&s, n);//删除pos所指向的数据
SLPrint(&s);//打印顺序表的数据(方便观察)
SeqListDestory(&s);//创建一个顺序表,使用后要销毁,避免内存泄漏
}
void test5()//修改pos所指向的数据
{
SeqList s;
SeqListInit(&s);
SLPushBack(&s, 1);//尾插
SLPushBack(&s, 2);//尾插
SLPushBack(&s, 3);//尾插
SLPushBack(&s, 4);//尾插
SLPushBack(&s, 5);//尾插
SLPrint(&s);//打印顺序表的数据(方便观察)
printf("请输入要修改的数据的下标:>");
size_t n = 0;
scanf("%d", &n);
printf("请输入要修改的数据的值:>");
SLDataType m = 0;
scanf("%d", &m);
SeqListModify(&s, n, m);//修改pos所指向的数据
SLPrint(&s);//打印顺序表的数据(方便观察)
SeqListDestory(&s);//创建一个顺序表,使用后要销毁,避免内存泄漏
}
int main()
{
//test1();
//test2();
//test3();
//test4();
test5();
return 0;
}
学习数据结构时的建议(敲代码):
①建议边写边编译;
②数据结构不要上来直接写出菜单,应该分功能模块来实现;
③调试时可以一次在main函数中只调试一个函数(方便、快捷、不易出错)。
六、顺序表的问题和缺点
①中部和头部的插入和删除不方便,时间复杂度为O(n)
②顺序表的增容要申请新的空间,可能需要拷贝数据到新的空间,并且释放旧空间,会有不小的系统消耗,会降低效率
③增容一般呈两倍增长,必然会导致空间浪费。
彩蛋
1.关于扩容
当内存用满了动态顺序表是可以进行扩容的,我们一般是扩容二倍。
但是为什么要扩容二倍?
答:因为2倍比较合适。
扩容太多会导致空间浪费;扩容太少又会导致需要多次扩容,会造成效率的损失(因为扩容是有代价的,如果异地扩容消耗就会比较大)。
2.关于缩容:
realloc是可以进行缩容的,但是我们的原则是绝不缩容,原因:
①缩容要付出代价,缩容有两种情况:原地缩容和异地缩容(异地的话它有可能会重新开辟一块空间,把缩容后的数据放进去,再返回新地址),异地扩容会造成系统消耗的
②如果缩容之后有需要插入数据这时候又需要再次扩容,就会造成系统消耗,导致效率降低
总结:缩容就是用时间换空间的做法,不缩容就是用空间换时间的做法
总结
以上就是今天要讲的内容,本文介绍了数据结构中的线性表,主要介绍了顺序表的定义以及实现。
本文作者也是一个正在学习编程的萌新,目前也只是刚开始接触数据结构这方面的内容,如果有什么内容方面的错误或者不严谨,欢迎大家在评论区指出。
最后,如果本篇文章对你有所启发的话,也希望可以支持支持作者,谢谢大家!