文章目录
前言
本文需要一定的结构体、指针和动态内存分配知识,对此不太了解的小伙伴可以先了解相关知识。顺序表作为最基础的数据结构,是一个比较简单的结构,它更符合我们在日常生活中的所见。
一、概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
结构图示
下面是顺序表的逻辑结构:
如图所示,顺序表在内存中是顺序存储的。
1.静态顺序表
静态顺序表是使用定长数组来存储元素的顺序表。
具体演示如下:
typedef int SLDataType;//数据类型重命名,需要修改数据类型可以直接修改
//静态顺序表 - 开多了浪费,开少了不够用
struct Sqlist
{
int a[N];
int size;
};
2.动态顺序表
动态顺序表是使用动态开辟的数组存储元素的顺序表。
具体演示如下:
typedef int SLDataType;
typedef struct Sqlist
{
SLDataType* a;
int size;// 有效数据个数
int capacity;// 空间容量
}SL;
二、动态顺序表的实现
静态顺序表只适用于知道多少数据的场景,在平时使用的比较少,使用动态
顺序表既可以节省内存,又可以解决内存可能分配不够的问题。
工程项目中,一般在头文件中进行函数声明,函数体和主函数分开写入两个文件中,方便进行管理,在主函数中进行接口调用。
1.顺序表定义
typedef int SLDataType;
typedef struct Sqlist
{
SLDataType* a;
int size;// 有效数据个数
int capacity;// 空间容量
}SL;
2.初始化顺序表
- 封装函数执行初始化,可以进行调用;
- 及时使用perror函数减少代码调试的需要,可以直接找到问题所在;
- 在使用malloc函数时,要注意进行判断防止内存未分配成功造成系统奔溃;
- 注意magic number 的出现,尽量在头文件中使用define进行定义。
void SLInit(SL* ps)
{
assert(ps);
ps->a = (SLDataType*)malloc(sizeof(SLDataType)*INIT_CAPACITY);
if (ps->a == NULL)
{
perror("malloc fail");
return;
}
ps->size = 0;
ps->capacity = INIT_CAPACITY;
}
3.打印顺序表
将顺序表内容打印到屏幕上。
- 使用assert进行断言,在断言出错时可以直接提示错误的位置,省去不不要的麻烦。
void SLPrint(SL* ps)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
printf("%d ", ps->a[i]);
}
printf("\n");
}
4.顺序表销毁
对动态开辟的内存进行释放,防止内存泄漏。
void SLDestory(SL* ps)
{
assert(ps);
ps->capacity = ps->size = 0;//赋值表达式,从右往左赋值
free(ps);
ps->a = NULL;
}
5.尾插(从最后开始插入数据)
先判断是否需要扩容
- 这里将函数体使用static关键字,使得函数只在此源文件内部使用
static void is_expand(SL* ps)
{
assert(ps);
if (ps->size == ps->capacity)
{
SLDataType* tmp = (SLDataType*)realloc(ps->a, sizeof(SLDataType)* ps->capacity * 2);
if (tmp == NULL)
{
perror("realloc fail");
return;
}
ps->a = tmp;//赋值给a去管理地址
ps->capacity *= 2;
}
}
尾插
- 巧妙使用后置++,先进行使用,再加1的特性,减少代码冗余。
void SLPushBack(SL* ps, SLDataType x)
{
assert(ps);
//扩容
is_expand(ps);
/*ps->a[ps->size] = x;
ps->size++;*/ //这两种写法一样的
ps->a[ps->size++] = x;
}
6.尾删(从最后开始删除)
void SLPopBack(SL* ps)
{
assert(ps);
//暴力检查
assert(ps->size > 0);
// 温柔检查
//ps->a[ps->size] = 0; //这句事实上没有意义,只要减掉size数据就会减少一条。
/*if (0 == ps->size)
{
return;
}*/
ps->size--;
}
7.头插(从开头位置插入)
头插的操作比较麻烦,需要将后面所有的数据往后移,然后再插入。
也需要调用判断是否需要扩展空间的函数,以免动态空间不够,造成越界访问。
void SLPushFront(SL* ps, SLDataType x)
{
assert(ps);
is_expand(ps);
int end = ps->size - 1;
while (end >= 0)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[0] = x;
ps->size++;
}
8.头删(从开头位置删除)
void SLPopFront(SL* ps)
{
assert(ps);
assert(ps->size > 0);
int begin = 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
9.插入函数(寻找某个元素在它后面插入数据)
这个函数完成了头插与尾插的功能,可以直接对它进行调用。
void SLInsert(SL* ps, int pos, SLDataType x) //可以复用到头插和尾插中
{
assert(ps);
assert(pos >= 0 && pos <= ps->size);
is_expand(ps);
int end = ps->size - 1;
while (end >= pos)
{
ps->a[end + 1] = ps->a[end];
--end;
}
ps->a[pos] = x;
ps->size++;
}
10.擦除函数(寻找某个元素,并将其擦除)
void SLErase(SL* ps, int pos)
{
assert(ps);
assert(pos >= 0 && pos < ps->size);
int begin = pos + 1;
while (begin < ps->size)
{
ps->a[begin - 1] = ps->a[begin];
++begin;
}
ps->size--;
}
11.查找(找到某个元素并返回它的序号)
查找某个元素,找到返回它的序号,找不到返回-1.
int SLFind(SL* ps, SLDataType x)
{
assert(ps);
for (int i = 0; i < ps->size; ++i)
{
if (ps->a[i] == x)
{
return i;
}
}
return -1;
}
三、函数使用案例
首先对结构体进行初始化,然后调用函数。
#include "Sqlist.h"
void TestSeqList1()
{
SL s;
SLInit(&s);
SLPushBack(&s, 1);
SLPushBack(&s, 2);
SLPushBack(&s, 3);
SLPushBack(&s, 4);
SLPushBack(&s, 5);
SLPushBack(&s, 6);
SLPushBack(&s, 7);
SLPushBack(&s, 8);
SLPushBack(&s, 9);
SLPrint(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPopBack(&s);
SLPrint(&s);
SLDestory(&s);
}
int main()
{
TestSeqList1();
return 0;
}
结果:
总结
顺序表的增删改查功能实现比较简单,但是在写的过程中还是容易出现问题,在每个功能完成成应当立即进行测试,以免调用此功能时影响到后续功能的实现,尤其要注意内存泄漏和开辟内存不足导致的越界访问问题。
请各位多多支持,不要忘记三连哦!