前言:
最近本哲♂学家学习了顺序表,下面我给大家分享一下关于顺序表的知识。
一、什么是顺序表
-
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组
上完成数据的增删查改。 -
顺序表:可动态增长的数组,要求数据是连续存储的。
二、顺序表
1、顺序表的定义
typedef int SLDataType; //类型重命名,后续要存储其它类型时方便更改
typedef struct SeqList
{
SLDataType* arr; //指向动态开辟的数组
size_t size; //有效数据个数
size_t capacity; //容量大小
}SL;
注意将数组定义为数组指针:如果定义为数组则属性表空间太大,造成空间浪费。
2、顺序表的文件
- SeqList.h(顺序表的类型定义、接口函数声明、引用的头文件)
- SeqList.c(顺序表接口函数的实现)
- Test.c(主函数、测试顺序表各个接口功能)
3、头文件
#pragma once//防止头文件二次引用
typedef int SLDataType;//方便后续改存储类型
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef struct seqlist
{
SLDataType* arr;//存放数据的数组
int size;//有效元素个数
int capacity;//空间的大小
}SL;
// 初始化和销毁
void SLInit(SL * ps);
void SLDestroy(SL* ps);
void SLPrint(SL* ps);
//扩容
void SLCheckCapacity(SL* ps);
//头部插入删除 / 尾部插入删除
void SLPushBack(SL* ps, SLDataType x);
void SLPopBack(SL* ps);
void SLPushFront(SL* ps, SLDataType x);
void SLPopFront(SL* ps);
//指定位置之前插入/删除数据
void SLInsert(SL* ps, int pos, SLDataType x);
void SLErase(SL* ps, int pos);
//找到指定位置的数据
int SLFind(SL* ps, SLDataType x);
三、函数的实现
1、顺序表的初始化与销毁
//初始化
void SLInit(SL* ps)
{
ps->arr = NULL;
ps->capacity = 0;
ps->size = 0;
}
//销毁
void SLDestroy(SL* ps)
{
assert(ps->arr);//防止对空指针释放空间
if (ps->arr != NULL)//或者直接写ps->arr
{
free(ps->arr);
}
ps->arr = NULL;
ps->capacity = ps->size = 0;
}
2、顺序表的打印
为了方便后续观察代码的正确性,我们可以写出打印函数
//打印
void SLPrint(SL* ps)
{
assert(ps->arr);//防止为空
for (int i = 0; i < ps->size; i++)
{
printf("%d", ps->arr[i]);
}
printf("\n");
}
3、扩容
注意我们扩容空间的时候不能改变size的值,因为我们设定size为有效元素个数。
void SLCheckCapacity(SL* ps)//如果空间不够就经行扩容
{
if (ps->capacity == ps->size)//这样说明空间不够
{
int newcapacity = (ps->capacity == 0 ? 4 : 2 * ps->capacity);//是0则新的空间大小为4,不是则为2倍原来空间大小
SLDataType* tmp = (SLDataType*) realloc(ps->arr, newcapacity * sizeof(SLDataType));
if (tmp == NULL)//如果没有开辟成功
{
perror("realloc fail!");
exit(1);//直接退出程序,不再继续执行
}
//如果开辟成功
ps->arr = tmp;
ps->capacity = newcapacity;
tmp = NULL;//完事后将创建的临时指针放置为空
}
}
事实上这一步是在为后面插入数据做准备。
4、尾插和尾删
1、尾插:
void SLPushBack(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);//检查空间是否够用
assert(ps->arr);
ps->arr[ps->size] = x;
ps->size++;
//或者直接写出ps->arr[ps->size++] = x;
}
2、尾删
这里可能会想到将要删除的数据置为0,实际上这样还是会占用开辟的空间,那不如我们不用将删除数据置0,直接将有效数据个数减一即可。(同时只有当数据表数据个数不为0才可以删除)
void SLPopBack(SL* ps)
{
assert(ps->arr);
assert(ps->size);//判断数据是否为0
ps->size--;
}
测试后发现是对的。
5、头插和头删
1、头插
头插是将数据放在头部,所以后面的数据要整体向后移动,所以我们可以简单画个图
如图我们可以发现是将size-1的数据移动到size的位置所以代码可以这么写:
void SLPushFront(SL* ps, SLDataType x)
{
SLCheckCapacity(ps);//检查空间是否够用
assert(ps->arr);
for (int i = 0; i < ps->size; i++)//移到后面。
{
ps->arr[ps->size - i] = ps->arr[ps->size - 1 - i];
}
//或者这么写
//for (int i = ps->size; i > 0; i--)
//{
// ps->arr[i] = ps->arr[i - 1];//arr[1] = arr[0]
//}
ps->arr[0] = x;
ps->size++;
}
2、头删
头删只不过是头插的逆过程我们直接将后面的数据向前移动不用管第一个数据即可:
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]; //arr[size-2] = arr[size-1]
}
ps->size--;
}
测试发现没有任何问题
6、指定位置之前插入和删除数据
1、插入
指定位置插入实际上是头插的一种特殊形式(有个偷懒的办法最后写出的条件带入size和0看是否与上面的形式相同即可):
//指定位置插
void SLInsert(SL* ps, int pos, SLDataType x)
{
SLCheckCapacity(ps);//检查空间是否够用
assert(ps->arr);
for (int i = ps->size; i > pos; i--)//后面的向后移动
{
ps->arr[i] = ps->arr[i - 1];
}
ps->arr[pos] = x;
ps->size++;
}
2、删除
删除同理:
void SLErase(SL* ps, int pos)
{
assert(ps); //断言
assert(ps->size); //顺序表不能为空
assert(pos >= 0 && pos < ps->size); //检查pos下标的合法性
int i = 0;
for (i = pos + 1; i < ps->size; i++) //将pos位置后面的数据依次向前挪动一位
{
ps->arr[i - 1] = ps->arr[i];
}
ps->size--; //有效数据个数-1
}
测试发现没有问题
7、找到指定位置的数据
这个就更简单了,只需要将数组遍历一遍即可。
int SLFind(SL* ps, SLDataType x)
{
for (int i = 0; i < ps->size; i++)
{
if (ps->arr[i] == x)
{
return i;//找到返回下标
}
}
return -1;//找不到返回无效值
}
总结
顺序表是比数组更加灵活的存储方式,也是我们接触的第一个数据结构,熟练掌握是十分重要的。