数据结构初阶:动态顺序表的功能实现(用C语言实现,附图分析)
Posted 平凡的指针
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构初阶:动态顺序表的功能实现(用C语言实现,附图分析)相关的知识,希望对你有一定的参考价值。
文章目录
一、动态版本顺序表
在实现顺序表之前,我们要知道顺序表按照能不能扩容可以分为两种版本,静态版本和动态版本。
静态版本就是把内容存到数组中,一旦数组在内存栈上开辟了,当数据存满的时候就不能再继续存储了。所以针对这个缺点我们有了动态版本顺序表,我们把数据存到堆区中,可以进行自动扩容处理,当数据存满还能扩容继续存储。
二、动态顺序表的实现思路
顺序表特点:将表中元素一个接一个的存入一组连续的存储单元中,这种存储结构是顺序结构,针对这种线性结构有以下思路:
所以我们先创建一个顺序表结构体类型,结构体类型中有指针,下标,容量。
指针: 用来维护在堆上连续的一段空间,
下标:表示数据存放到哪一个位置了,因为数据只能一个接着一个地存放,要有个下标来记录我数据放到哪一个位置了。
容量:与下标相比较,当下标与容量相等就表示空间存储满了,要进行扩容处理。
创建类型如下:
//我们以int类型数据来实现顺序表
typedef int DataType; //对int类型重新起个名字叫DataType
//创建一个顺序表结构体类型
struct SeqList
{
DataType* a; //数据的指针
int size; //下标
int capacity; //记录开辟空间的最大下标处
};
//对顺序表类型struct SeqList类型重新起个名字叫SL
typedef struct SeqList SL;
//当size 和 capacity相等时要进行扩容处理
这样我们就可以在主函数main()中创建一个顺序表结构体类型的变量,然后我们对数据进行处理,实现顺序表的各种接口函数,就是对这个变量进行操作即可。
三、动态顺序表内存布局图
四、初始化顺序表和内存释放
我们最知道局部变量是在栈区创建的,初始值为随机值,我们得对变量sl进行初始化,并且当退出程序的时候要把堆上开辟的空间还给操作系统,要释放空间。函数如下:
//对变量sl进行初始化函数
void SeqListInit(SL* ps)
{
ps->a = NULL;
ps->size = 0;
ps->capacity = 0;
}
// 释放内存函数
void SeqListDestroy(SL* ps)
{
free(ps->a);
ps->a = NULL;
ps->capacity = 0;
ps->size = 0;
}
五、顺序表接口实现:
1.尾部插入数据
当我们插入数据的时候一定要注意两点:
1、检测空间是否满了,如果满了就要进行扩容处理,否则就会导致内存泄漏
2、插入是否为第一次插入数据,因为初始化函数给的容量是0,就会开辟不了内存,所以当第一次插入数据我们容量要给2,以后空间满了就用二倍来进行扩容处理。
代码如下:
//增加容量函数
DataType* BuyCapacity(SL* ps)
{
//第一次插入数据我们容量要给2,以后空间满了就以二倍来进行扩容处理
ps->capacity = ps->capacity > 0 ? ps->capacity * 2 : 2;
DataType* tmp = (DataType*)realloc(ps->a, sizeof(DataType) * ps->capacity);
if (tmp == NULL)
{
perror("erron");
exit(-1);
}
return tmp;
}
//尾部插入数据函数
void SeqListPushBack(SL* ps, DataType x)
{
if (ps->capacity == ps->size) //检测空间是否满了
{
DataType* tmp = BuyCapacity(ps); //增容函数
ps->a = tmp;
}
ps->a[ps->size] = x; //把数据放到尾部
ps->size++; //下标要往后挪动一位
}
2.头部插入数据
头部插入数据不同于尾部那样直接在后面放进来即可,我们要把数据先往后挪动一位,再把数据放到头部,也有两点要注意:
1、检测空间是否满了,如果满了就要进行扩容处理,否则就会导致内存泄漏
2、挪动数据要从尾部往后面挪,如果从头部往后挪会把数据给覆盖掉。
图片分析如下:
代码实现如下:
//头部插入数据函数
void SeqListPushFront(SL* ps,DataType x)
{
if (ps->capacity == ps->size)
{
DataType* tmp = BuyCapacity(ps);
ps->a = tmp;
}
int i = 0;
for (i = ps->size - 1; i >= 0; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->a[0] = x; //把数据放到头部
ps->size++; //下标要往后挪动一位
}
3.尾部删除数据
尾部删除数据要注意一点,那就是当我空间没有数据就不能再删除了,所以要删除数据时要判断空间内是否有数据。
代码如下:
//尾部删除数据函数
void SeqListPopBack(SL* ps)
{
assert(ps->size > 0); //判断空间是否有数据
ps->size--; //下标自减一即可
}
4.头部删除数据
头部的删除没有尾部删除那样简单,头删需要挪动数据,有两点要注意的地方:
1、要判断空间内是否有数据,有数据才能删除
2、挪动数据要从头部的第二个元素往前面开始挪动,不能从尾部挪动,因为从尾部往前挪会把数据给覆盖掉。
图片分析如下:
5.显示数据
显示数据即把数据打印出来即可,代码如下:
//显示数据函数
void SeqListPrint(SL* ps)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
printf("%d ", ps->a[i]);
}
printf("\\n");
}
6.查找数据
查找函数就是要找到某一个数据对应的下标,找到了则返回数据的下标,找不到就返回 -1。所以我们遍历一遍数组即可,代码如下:
//查找数据函数
int SeqListFind(SL* ps, DataType x)
{
int i = 0;
for (i = 0; i < ps->size; i++)
{
if (ps->a[i] == x)
{
//找到数据就直接返回下标
return i;
}
}
//找不到就返回-1
return -1;
}
7.在某个位置插入数据
在某个位置插入数据有三点要注意:
1、检测空间是否满了,如果满了就要进行扩容处理,否则就会导致内存泄漏
2、挪动数据要从尾部开始往后面挪,每一个元素往后面挪动一位
3、要对插入的位置进行判断,插入的下标要大于或者等于0 并且要小于或者等于数组的有效位置
图片分析:
代码如下:
//插入数据函数
void SeqListInsert(SL* ps, DataType x,int pos)
{
//对插入的位置进行判断
assert(pos >= 0 && pos <= ps->size);
if (ps->capacity == ps->size)
{
DataType* tmp = BuyCapacity(ps);
ps->a = tmp;
}
int i = 0;
//从尾部开始移动
for (i = ps->size - 1; i >= pos; i--)
{
ps->a[i + 1] = ps->a[i];
}
ps->a[pos] = x;
ps->size++;
}
8.在某个位置删除数据
这个和头插类似,也有三点要注意的地方:
1、要判断空间内是否有数据,有数据才能删除
2、挪动数据要从传过来下标的第二个元素往前面开始挪动,不能从尾部挪动,因为从尾部往前挪会把数据给覆盖掉。
3、要对删除的位置进行判断,删除的下标要大于或者等于0 并且要小于数组的有效位置
图片分析如下:
代码如下:
//删除数据函数
void SeqListErase(SL* ps, int pos)
{
//要判断空间内是否有数据
assert(ps->size != 0);
//要对删除的位置进行判断
assert(pos >= 0 && pos < ps->size);
//挪动数据
int i = 0;
for (i = pos + 1; i < ps->size; i++)
{
ps->a[i - 1] = ps->a[i];
}
ps->size--;
}
六、对头插 尾插 头删 尾删 的改造
头插、尾插:
大家发现没有,我们的在某个位置插入数据函数和头插 尾插函数是不是比较类似,其实1、当在某个位置插入数据函数,要插入的下标是0时就是头插函数了,
2、当在某个位置插入数据函数,要插入的下标是数组的size时就是尾插函数了
所以我们写头插 尾插函数时完全什么都不要干,只需要调用某个位置插入数据函数,并且把下标传过去就可以了。
代码如下:
//尾插函数
void SeqListPushBack(SL* ps, DataType x)
{
//直接调用插入函数,插入的下标是size
SeqListInsert(ps, x, ps->size);
}
//头插函数
void SeqListPushFront(SL* ps,DataType x)
{
//直接调用插入函数,插入的下标是0
SeqListInsert(ps, x, 0);
}
头删、尾删:
某个位置删除数据函数和头删 尾删函数是不是比较类似,
1、当在某个位置删除数据函数,要删除的下标是0时就是头删除函数了,
2、当在某个位置删除数据函数,要删除的下标是数组的有size时就是尾删函数了
所以我们写头删、尾删函数时完全什么都不要干,只需要调用某个位置删除数据函数,并且把下标传过去就可以了。
代码如下:
//尾删函数
void SeqListPopBack(SL* ps)
{
//调用删除函数
SeqListErase(ps, ps->size - 1);
}
//头删函数
void SeqListPopFront(SL* ps)
{
//调用删除函数
SeqListErase(ps, 0);
}
所以我们实现头插 尾插 头删 尾删可以直接调用插入、删除函数。
七、总结
总的来说,顺序表的实现有几点要注意:
1、插入的时候要判断容量是否满了;
2、删除的数据要判断容量是否为空;
3、插入、删除的位置是否在有效范围内;
4、移动数据要从哪个位置开始移动。
下面附上我的测试代码:
#define _CRT_SECURE_NO_WARNINGS 1
#include "SeqList.h"
void test1()
{
SL sl;
SeqListInit(&sl);
printf("尾插:\\n");
SeqListPushBack(&sl, 1);
SeqListPushBack(&sl, 2);
SeqListPushBack(&sl, 3);
SeqListPushBack(&sl, 4);
SeqListPushBack(&sl, 5);
SeqListPushBack(&sl, 6);
SeqListPrint(&sl);
printf("头插:\\n");
SeqListPushFront(&sl, 10);
SeqListPushFront(&sl, 20);
SeqListPushFront(&sl, 30);
SeqListPrint(&sl);
printf("尾删:\\n");
SeqListPopBack(&sl);
SeqListPopBack(&sl);
SeqListPopBack(&sl);
SeqListPrint(&sl);
printf("头删:\\n");
SeqListPopFront(&sl);
SeqListPopFront(&sl);
SeqListPopFront(&sl);
SeqListPrint(&sl);
printf("中间插入:\\n");
SeqListInsert(&sl, 20, 3);
SeqListInsert(&sl, 20, 1);
SeqListInsert(&sl, 20, 2);
SeqListPrint(&sl);
printf("中间删除:\\n");
SeqListErase(&sl, 5);
SeqListErase(&sl, 0);
SeqListPrint(&sl);
SeqListDestroy(&sl);
}
int main()
{
test1();
return 0;
}
测试结果:
以上就是我的顺序表内容了,其中前面我只有把源文件SeqList.c 和测试文件test.c拆开来分析了。如果想要顺序表的全部内容,阔以移步到gitee上获取。
三个源文件内容链接:
https://gitee.com/fait-juyuan/data-structure/tree/master/test_10_14_%E9%A1%BA%E5%BA%8F%E8%A1%A8/test_10_14_%E9%A1%BA%E5%BA%8F%E8%A1%A8
以上是关于数据结构初阶:动态顺序表的功能实现(用C语言实现,附图分析)的主要内容,如果未能解决你的问题,请参考以下文章