数据结构初阶:动态顺序表的功能实现(用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语言实现,附图分析)的主要内容,如果未能解决你的问题,请参考以下文章

初阶数据结构二C语言实现顺序表

C语言顺序表的动态存储:增删改查的实现

❤❤❤❤顺序表的实现(c语言)----数据结构

❤❤❤❤顺序表的实现(c语言)----数据结构

❤❤❤❤顺序表的实现(c语言)----数据结构

数据结构初阶:线性表