目录
- 前言
- 1. 顺序表的概念
- 2. 动态顺序表
- 2.1 顺序表的初始化与销毁
- 2.2 顺序表的尾插
- 容量检查
- 2.3 顺序表的尾删
- 2.4 顺序表的头插
- 2.5 顺序表的头删
- 2.6 固定位置的插入
- 2.7 固定位置的删除
- 2.8 查找和打印
- 2.9 修改元素
- 主函数部分(菜单)
- 结语
前言
本章会用C语言来描述数据结构中的顺序表,实现简单的增删查改操作,其中头文件包含在新建的头文件SeqList.h内,顺序表的实现在新建的Seqlist.c内,主函数Text.c将会实现菜单,方便我们进行功能的选择。
1. 顺序表的概念
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储,在数组上完成数据的增删查改。
顺序表分为静态和动态两个版本,一般我们都是使用动态的版本进行操作
因为静态的版本使用定长数组存储元素,而动态的版本使用动态开辟的数组存储
2. 动态顺序表
2.1 顺序表的初始化与销毁
我们知道,一个顺序表有这么几个组成部分:起始地址,存储的数据个数,以及容量。 在进行初始化时,我们应该将起始地址置为空(NULL),个数和容量置为0。//初始化
void SLInit(SL* ps1)
{
ps1->a = (SLDatatype*)malloc(sizeof(SLDatatype) * 4);
if (ps1->a == NULL)
{
perror("malloc fail");//顺序表为空就报错,说明申请空间失败
return;
}
ps1->capacity = 4;//我们设定初始空间为4个SLDatatype(这里是int)大小
ps1->size = 0;
}
注意要释放掉内存
void SLDestory(SL* ps1)
{
free(ps1->a);//释放
ps1->a = NULL;//将顺序表置为空
ps1->size = 0;//有效数字置0
ps1->capacity = 0;//容量置0
}
2.2 顺序表的尾插
尾插,顾名思义,就是在顺序表的尾部插入数据,在实现尾插功能的同时,我们要写一个检查顺序表容量的函数,插入的时候我们要检查空间是否足够,不够则要进行扩容,足够则插入。
void SLPushBack(SL* ps1, SLDatatype x)
{
SLCheckCapacity(ps1);//检查容量
ps1->a[ps1->size] = x;//尾插数据
size++
//ps1->a[ps1->size++] = x;
}
容量检查
void SLCheckCapacity(SL* ps1)
{
if (ps1->size == ps1->capacity)//有效数据大小如果和容量相等说明需要扩容了
{
SLDatatype* tmp = (SLDatatype*)realloc(ps1->a, sizeof(SLDatatype) * ps1->capacity * 2);
//如果空间不够了,就扩容到原来的二倍
if (tmp == NULL)
{
perror("realloc fail");//如果tmp为空说明扩容空间失败
return;
}
ps1->a = tmp;//检查确保tmp不为空再赋给a
ps1->capacity *= 2;//容量变为原来的二倍
}
}
2.3 顺序表的尾删
实现尾删功能我们需要注意的时,说是删,但是我们可不能真删了,并不是把数据置为\n或是\0,只需要把有效数据-1就行了。
注意:不可以对要删除数的空间进行释放,因为动态开辟出的空间有一个特点:一起开辟,一起释放,在这里不可以实现。
void SLPopBack(SL* ps1)
{ //尾删数据
assert(ps1);//暴力检查一下
//size不能一直-1
if(ps1->size>0)
{
p->size--;
}
}
2.4 顺序表的头插
除了需要把顺序表中【0,size-1】的元素依次向后移动一位,再插入数值
我们要从后往前向后移,因为从前往后的话数据会被覆盖
void SLPushFront(SL* ps1, SLDatatype x)
{
SLCheckCapacity(ps1);//检查容量
for (int i = ps1->size - 1; i >= 0; i--)//头插需要将顺序表中【0,size-1】的元素依次向后移动一位
{
ps1->a[i + 1] = ps1->a[i];
}
ps1->a[0] = x; //头插数据
ps1->size++; //有效数据+1
}
2.5 顺序表的头删
头删很简单,与头插相反,头插是将从最后一个数开始向后覆盖,而头删是从第二个元素开始从前往后覆盖前一个元素。
void SLPopFront(SL* ps1)
{
assert(ps1->a > 0);//暴力检查
for (int i = 0; i < ps1->size - 1; i++)
{
ps1->a[i] = ps1->a[i + 1];
}
}
2.6 固定位置的插入
思路和头插没区别,只要找到要插入的位置,将它后面位置的元素全部向后移一位,然后插入,就OK了,需要注意的是pos(插入的位置)也要进行断言
void SLInsert(SL* ps1, int pos, SLDatatype x)
{
assert(ps1);//断言检查空指针
assert(pos>=0 && pos <= ps1->size);//断言检查pos是否合规
SLCheckCapacity(ps1);//检查容量
int end = ps1->size - 1;
while (pos <= end)
{
ps1->a[end + 1] = ps1->a[end];
--end;
}
ps1->a[pos] = x;
ps1->size++;
}
2.7 固定位置的删除
同理,从要删除的位置开始,从前往后将后一位的值赋给前一位,太简单了
void SLEarse(SL* ps1, int pos)
{
assert(ps1);
assert(pos >= 0 && pos <= ps1->size);
SLCheckCapacity(ps1);
int start = pos+1;
while (start < ps1->size)
{
ps1->a[start - 1] = ps1->a[start];
++start;
}
ps1->size--;
}
2.8 查找和打印
过于简单,直接上代码
int SLFind(SL* psl, SLDatatype x)
{
assert(psl);
for (int i = 0; i < psl->size; i++)
{
if (psl->a[i] == x)
{
return i;
}
}
return -1;
}
void SLPrint(SL* ps1)
{
for (int i = 0; i < ps1->size; i++)
{
printf("%d ",ps1->a[i]);
}
printf("\n");
}
2.9 修改元素
void SLModify(SL* ps1, int pos, SLDatatype x)
{
assert(ps1);
assert(0 <= pos && pos < ps1->size);
ps1->a[pos] = x;
}
主函数部分(菜单)
void menu()
{
printf("***************************************\n");
printf("1、尾插数据 2、尾删数据\n");
printf("3、头插数据 4、头删数据\n");
printf("5、打印数据 -1、退出\n");
printf("***************************************\n");
}
int main()
{
int option = 0;
SL s;
SLInit(&s);
while (option != -1)
{
menu();
printf("请输入你的操作:>");
scanf("%d", &option);
if (option == 1)
{
/*printf("请输入要尾插的数据,以-1结束:");
int x = 0;
scanf("%d", &x);
while (x != -1)
{
SLPushBack(&s, x);
scanf("%d", &x);
}*/
int n = 0;
printf("请输入要尾插的数据个数,再依次输入要插入的数据:");
scanf("%d", &n);
int x = 0;
while (n > 0)
{
scanf("%d", &x);
SLPushBack(&s, x);
n--;
}
}
else if (option == 5)
{
SLPrint(&s);
}
else if (option == -1)
{
break;
}
else
{
printf("无此选项,请重新输入\n");
}
}
SLDestroy(&s);
return 0;
}
结语
就这样,顺序表从定义到结束,增删查改的功能也全部实现了,学到这里,我们就大致掌握了顺序表,同时我们也要思考一些问题:
1.中间/头部的插入,时间复杂度为O(N)
2.增容需要申请新空间,拷贝数据,释放旧空间,这个过程会有损耗
3.增容一般是以两倍的方式增长,所以必定会造成浪费,比如我们当前容量为100,已经满了,我们扩容到200,但是只新插入了5个数据,这就浪费了95个数据空间
如何处理呢?