用C语言实现顺序表

目录

一. 为什么要实现顺序表

二. 顺序表的特点

1.线性表

2. 顺序表的优缺点

三. 顺序表的逐步实现

 1.准备工作

2. 设计动态顺序表的结构体

3. 实现具体功能

3.1 初始化SLInit

 3.2 销毁SLDestroy

3.3 申请动态内存空间SLCheckCapacity

3.4 尾插SLPushBack

 3.5 尾删SLPopBack

3.6 头插SLPushFront

3.7 头删SLPopFront

3.8 打印顺序表SLPrint

3.9 查找SLFind

3.10 任意位置前插入数据SLInsert

3.11 任意位置删除数据SLErase

四.动态顺序表的功能测试

1. 测试1

2. 测试2

五.参考代码


一. 为什么要实现顺序表

我们在使用C语言编写代码的时候,如果需要处理大量数据,就需要一个数据结构来储存和管理这些数据。

我们可能会考虑建立数组来储存信息,但是数组在建立之后它的容量大小就已经确定,很多时候,我们难以确定要分配给这个数组多少元素个数,如果分配多了,就会造成空间的浪费,如果分配少了,又无法满足实际需求,这在实际应用中是非常不合适的。

下面是静态顺序表的实现,静态顺序表本身确定了大小,使用定长数组存储元素,使用范围十分有限。

#define N 100
typedef int SLDataType; 

//静态顺序表
typedef struct SeqList
{
	SLDataType arr[N];//静态数组
	int size;//有效的元素个数
}SL;

有什么办法可以动态调整数组的大小呢?没错,就是动态内存管理。

C语言动态内存管理icon-default.png?t=N7T8https://blog.csdn.net/qq_51904510/article/details/136941147

我们通过realloc实时调整分配的动态内存空间的大小,实现空间的动态分配与扩容,这样就可以减少空间的浪费,同时存储任意量的数据,不用担心空间不够的问题。

二. 顺序表的特点

1.线性表

顺序表是线性表的一种,那么什么是线性表,线性表有什么特点呢?

线性表(linear list)n个具有相同特性的数据元素的有限序列

线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串 ...

线性表在逻辑上是线性结构,也就说是连续的一条直线。

但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。

而顺序表的特点:在物理上连续,在逻辑上也连续

2. 顺序表的优缺点

优点:

            1.随机访问,即可在O(1)时间内找到第i个元素。

            2.存储密度高,每个节点只存储数据元素本身。

缺点:

            1.拓展容量不方便。

            2.插入、删除操作不方便,需要移动大量元素。

三. 顺序表的逐步实现

 1.准备工作

我们创建三个文件,分别是:

文件作用
SeqList.h顺序表的结构体部分和函数的声明
SeqList.c顺序表进行相关操作的函数的实现
test.c主函数和测试代码

现在,我们在SeqList.h中写入包含的头文件:

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

然后让两个.c文件分别包含.h文件,这样就将三个文件链接了起来。

2. 设计动态顺序表的结构体

我们需要一个指针来记录我们分配给顺序表的元素个数,以及实际存储有效元素的个数,最后还需要一个指针指向我们动态开辟的空间,最终,我们设计出了以下结构体并将其重命名以简化写法:

//动态顺序表
typedef struct SeqList
{
	SLDataType* arr;//指向动态开辟的内存空间
	int size;//有效的元素个数
	int capacity;//动态开辟的元素个数
}SL;

由于我们目前需要存储的元素类型不确定,所以我们用SLDataType来重命名我们的元素类型:

typedef int SLDataType;//假设我们现在需要存放int类型的数据

3. 实现具体功能

既然我们要对大量数据进行管理,那么数据的增加删除查找修改,就是必不可少的,不过在此之前,我们需要对我们的顺序表进行初始化,在使用结束后,也应该对其进行销毁,释放动态内存空间以防止内存泄漏。

以下代码放在SeqList.c中

3.1 初始化SLInit

 在顺序表使用前,我们需要对其进行初始化,需要将其元素个数设置为0,将其指针置空。

我们需要在函数中操作实参,因此函数参数传递指针是一个不错的选择。

void SLInit(SL* ps)
{
    assert(ps);
	ps->arr = NULL;
	ps->size = 0;
	ps->capacity = 0;
}

 3.2 销毁SLDestroy

在顺序表使用结束后,我们也需要对其进行销毁,需要释放掉其动态申请的空间,将其元素个数设置为0,并将其指针置空。

void SLDestroy(SL* ps)
{
    assert(ps);
	if (ps->arr)//ps->arr不为空,证明空间未释放
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = 0;
	ps->capacity = 0;
}

3.3 申请动态内存空间SLCheckCapacity

下面,如果我们需要插入数据,就必须为顺序表申请动态内存空间,即扩容。

经过数学计算,我们认为每次扩容后的空间为扩容前的两倍最佳。

那么,什么情况下需要扩容呢?

我们认为在有效数据个数等于空间元素个数时需要立刻扩容。 

void SLCheckCapacity(SL* ps)
{
	//先判断空间够不够
	if (ps->size == ps->capacity)
	{
		int newcapacity = 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newcapacity;
	}
}

于是我们写出了上面的代码,当有效数据个数等于空间元素个数时,定义新的空间元素个数为原来的两倍,并使用realloc调整空间,然后判断是否申请成功,如果申请成功,就将新的空间元素个数和新空间的地址赋给原来的;如果失败,就报错退出。

那么,这段代码万无一失了吗?其实,上面的代码仍有缺陷

如果我们是第一次插入数据呢?原来的空间元素个数为0,乘两倍依旧是0啊!

因此,我们需要在扩容前进行判断,判断原来的空间元素个数是否为0,如果为0,就先开辟4个元素大小的空间,如果不是,就变成两倍。

void SLCheckCapacity(SL* ps)
{
	assert(ps);
	//在插入数据前先判断空间够不够
	if (ps->size == ps->capacity)
	{
		//申请空间 -- 增容 --> realloc
		//增容一般是两倍或者三倍的增加,过大或者过小,频繁的增容,会使程序的运行效率降低
		//注意:判断capacity是否为0
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newcapacity;
	}
}

有的人可能会问:你直接使用realloc,假如第一次传递了空指针怎么办呢?

 我们可以看关于realloc的介绍,如下:

realloc介绍

如果传递给realloc一个空指针,这个函数的行为类似于malloc 。所以如果传递了空指针,依然可以正常进行扩容。

3.4 尾插SLPushBack

现在,我们解决了增容的问题,现在我们来进行尾插,即在数组的末尾(在有效元素之后)插入数据。

这里的参数为顺序表指针ps和要插入的数据类型SLDataType x。

void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);

	SLCheckCapacity(ps);

	//尾插
	ps->arr[ps->size++] = x;
	//ps->arr[ps->size] = x;
	//++(ps->size);
}

这段代码还是比较好理解的,先判断是否需要增容,然后将数据元素插入到有效元素后,然后有效元素自增

注意:数组下标从0开始

 3.5 尾删SLPopBack

 尾删的代码也还是比较简单的,就是实现在尾部删除最后一个元素即可,我们只需要确定传入的顺序表指针非空,以便可以对顺序表进行操作,确定有效元素个数非零,确保顺序表中还有元素,最后直接将有效元素个数减一,即完成了尾删操作。

我们并不需要确定删除后该位置的元素是什么,因为它已经不再是一个有效的元素,在我们增加数据时会将其覆盖。

void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);
	//ps->arr[ps->size - 1] = -1;
	--(ps->size);
}

3.6 头插SLPushFront

现在,如果我们需要在顺序表的头部插入数据,就会麻烦一点,因为我们需要将数组中的每个元素向后移动一位,然后将插入的数据放在第一位。

void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);

	SLCheckCapacity(ps);

	//所有数据整体往后面移动一位
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}

	ps->arr[0] = x;
	ps->size++;
}

3.7 头删SLPopFront

头删也是同理,需要将每个元素向前移动一位

void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	--(ps->size);
}

3.8 打印顺序表SLPrint

对顺序表的打印可以方便我们观察顺序表中的元素,判断我们的操作函数是否起到了相应的作用。

打印输出顺序表中的每个元素并不需要对顺序表做出任意修改,因此参数并不需要携带指针,进行实参的拷贝并传递给实参即可,然后遍历数组中的每一个元素。

注意:此处的SLDataType为int类型,如果需要更改为其他类型,需要更改相应的代码。

void SLPrint(SL s)
{
	for (int i = 0; i < s.size; i++)
	{
		printf("%d ", s.arr[i]);
	}
	printf("\n");
}

3.9 查找SLFind

对顺序表的操作同样相应对顺序表进行遍历,通过对数据的比对查找是否包含该数据,如果包含返回元素的下标,如果不包含返回EOF(-1)。

int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;
		}
	}
	return EOF;
}

3.10 任意位置前插入数据SLInsert

现在,我们需要在任意位置插入数据,需要判断传入的数组插入位置是否合法,然后判断增容,最后依次挪动该位置后面的数据并插入该数据。

void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);

	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

如果pos == 0,相当于头插,如果pos == ps->size 相当于尾插 。

3.11 任意位置删除数据SLErase

任意位置删除数据仅需要将后面的数据前移一位即可。

void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

如果pos == 0,相当于头删,如果pos == ps->size - 1 相当于尾删。

四.动态顺序表的功能测试

上面我们实现了动态顺序表进行操作的函数,好像只实现了对顺序表的增、删、查,似乎没有实现修改啊?其实,我们既然实现了任意位置的增加删除,在任意位置的修改就是删除在插入即可。

与此同时,我们在上面函数的实现的同时,也应当同时在test.c中对我们写的函数进行测试,不然当你写完了再测试如果大量报错,会打击我们编程的积极性,同时影响我们实时对代码逻辑的判断和修改,这里由于连贯性的原因,统一放在了这里。

1. 测试1

void SLtest(void)
{
	SL sl;
	SLInit(&sl);//初始化
	
	for (int i = 0; i < 10; i++)
	{
		SLPushBack(&sl, i);//尾插1~9
		SLPrint(sl);
	}

	for (int i = 0; i < 10; i++)
	{
		SLPushFront(&sl, i);//头插1~9
		SLPrint(sl);
	}

	SLPopFront(&sl);//头删
	SLPrint(sl);

	SLPopBack(&sl);//尾删
	SLPrint(sl);

	SLDestroy(&sl);//销毁
}

运行结果

2. 测试2

void SLtest2()
{
	SL sl;
	SLInit(&sl);//初始化

	for (int i = 0; i < 10; i++)
	{
		SLPushBack(&sl, i);//尾插1~9
	}
	SLPrint(sl);

	SLErase(&sl, 0);//删掉第一个元素
	SLPrint(sl);

	SLInsert(&sl, 1, 10);//在第二个元素前插入10
	SLPrint(sl);

	int ret = 0;
	ret = SLFind(&sl, 5);//查找5这个元素
	printf("ret=%d\n", ret);

	ret = SLFind(&sl, 11);//查找11这个元素
	printf("ret=%d\n", ret);

	SLDestroy(&sl);//销毁
}

运行结果

五.参考代码

1. SeqList.h

#pragma once

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int SLDataType;

//动态顺序表
typedef struct SeqList
{
	SLDataType* arr;
	int size;
	int capacity;
}SL;

//顺序表的初始化
void SLInit(SL* ps);
//顺序表的销毁
void SLDestroy(SL* ps);
//头部插入数据
void SLPushFront(SL* ps, SLDataType x);
//尾部插入数据
void SLPushBack(SL* ps, SLDataType x);
//打印顺序表
void SLPrint(SL s);
//头部删除数据
void SLPopFront(SL* ps);
//尾部删除数据
void SLPopBack(SL* ps);
//任意位置前插入数据
void SLInsert(SL* ps, int pos, SLDataType x);
//任意位置删除数据
void SLErase(SL* ps, int pos);
//顺序表的查找
int SLFind(SL* ps, SLDataType x);

2. SeqList.c

#include "SeqList.h"

void SLInit(SL* ps)
{
	assert(ps);
	ps->arr = NULL;
	ps->size = 0;
	ps->capacity = 0;
}

void SLDestroy(SL* ps)
{
	assert(ps);
	if (ps->arr)
	{
		free(ps->arr);
	}
	ps->arr = NULL;
	ps->size = 0;
	ps->capacity = 0;
}

void SLCheckCapacity(SL* ps)
{
	assert(ps);
	//在插入数据前先判断空间够不够
	if (ps->size == ps->capacity)
	{
		//申请空间 -- 增容 --> realloc
		//增容一般是两倍或者三倍的增加,过大或者过小,频繁的增容,会使程序的运行效率降低
		//注意:判断capacity是否为0
		int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(1);
		}
		ps->arr = tmp;
		ps->capacity = newcapacity;
	}
}

void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);

	在插入数据前先判断空间够不够
	//if (ps->size == ps->capacity)
	//{
	//	//申请空间 -- 增容 --> realloc
	//	//增容一般是两倍或者三倍的增加,过大或者过小,频繁的增容,会使程序的运行效率降低
	//	//注意:判断capacity是否为0
	//	int newcapacity = ps->capacity == 0 ? 4 : 2 * ps->capacity;
	//	SLDataType* tmp = (SLDataType*)realloc(ps->arr, newcapacity * sizeof(SLDataType));
	//	if (tmp == NULL)
	//	{
	//		perror("realloc failed");
	//		exit(1);
	//	}
	//	ps->arr = tmp;
	//	ps->capacity = newcapacity;
	//}

	SLCheckCapacity(ps);

	//尾插
	ps->arr[ps->size++] = x;
	//ps->arr[ps->size] = x;
	//++(ps->size);
}

void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);

	SLCheckCapacity(ps);

	//所有数据整体往后面移动一位
	for (int i = ps->size; i > 0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}

	ps->arr[0] = x;
	ps->size++;
}

void SLPrint(SL s)
{
	for (int i = 0; i < s.size; i++)
	{
		printf("%d ", s.arr[i]);
	}
	printf("\n");
}

void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);
	//ps->arr[ps->size - 1] = -1;
	--(ps->size);
}

void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);
	for (int i = 0; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	--(ps->size);
}

void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);

	SLCheckCapacity(ps);

	for (int i = ps->size; i > pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];
	}
	ps->arr[pos] = x;
	ps->size++;
}

void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(pos >= 0 && pos < ps->size);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];
	}
	ps->size--;
}

int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;
		}
	}
	return EOF;
}

3. test.c

#include "SeqList.h"

void SLtest(void)
{
	SL sl;
	SLInit(&sl);//初始化
	
	for (int i = 0; i < 10; i++)
	{
		SLPushBack(&sl, i);//尾插1~9
		SLPrint(sl);
	}

	for (int i = 0; i < 10; i++)
	{
		SLPushFront(&sl, i);//头插1~9
		SLPrint(sl);
	}

	SLPopFront(&sl);//头删
	SLPrint(sl);

	SLPopBack(&sl);//尾删
	SLPrint(sl);

	SLDestroy(&sl);//销毁
}

void SLtest2()
{
	SL sl;
	SLInit(&sl);//初始化

	for (int i = 0; i < 10; i++)
	{
		SLPushBack(&sl, i);//尾插1~9
	}
	SLPrint(sl);

	SLErase(&sl, 0);//删掉第一个元素
	SLPrint(sl);

	SLInsert(&sl, 1, 10);//在第二个元素前插入10
	SLPrint(sl);

	int ret = 0;
	ret = SLFind(&sl, 5);//查找5这个元素
	printf("ret=%d\n", ret);

	ret = SLFind(&sl, 11);//查找11这个元素
	printf("ret=%d\n", ret);

	SLDestroy(&sl);//销毁
}

int main()
{
	SLtest();
	SLtest2();
	return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值