数据结构(一):顺序表

本文详细介绍了顺序表的概念和结构,包括静态顺序表与动态顺序表,并通过C语言实现了顺序表的基本功能,如初始化、销毁、数据的增删查改。重点探讨了动态顺序表的内存管理和扩容策略,以及在数据插入和删除时的时间复杂度分析。同时,总结了顺序表的优缺点,帮助读者深入理解顺序表的数据结构及其应用。
摘要由CSDN通过智能技术生成


一、概念及结构

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:

1.静态顺序表

即使用定长数组存储数据。

代码如下(示例):

#define N 100
typedef int SeqListDataType;//这里将int定义为SeqListDataType方便使用
typedef struct SeqList
{
	SeqListDataType a[N];//存储数据的数组
	int size;//顺序表中当前有效数据的个数
}SeqList;

2.动态顺序表

即使用动态开辟的数组存储数据。

代码如下(示例):

typedef int SeqListDataType;
typedef struct SeqList
{
	SeqListDataType* a;//存储数据的数组(动态开辟)
	int size;//顺序表中当前有效数据的个数
	int capacity;//顺序表中最大存储的个数
}SeqList;

静态顺序表的缺点在于存储数据的数组是定长的,所以在使用是可能造成空间浪费,也可能会出现顺序表存满的情况。而动态开辟可以很好地解决这一问题。下面的代码以动态开辟的顺序表为例来介绍。


二、顺序表功能的实现

1.顺序表的初始化、销毁

代码如下(示例):

//初始化
void SeqListInit(SeqList* p)
{
	assert(p);//用assert来防止传入空指针时程序崩溃

	p->a = NULL;
	p->size = 0;
	p->capacity = 0;
}

//销毁
void SeqListDestroy(SeqList* p)
{
	assert(p != NULL);//写法不同,功能同上
	
	free(p->a);//释放动态开辟的空间
	p->a = NULL;
	p->size = 0;
	p->capacity = 0;
}

这里为了方便观察,再实现一个打印顺序表中数据的函数。

代码如下(示例):

//打印
void SeqListPrint(SeqList* p)
{
	assert(p);
	int i = 0;
	for (i = 0; i < p->size; i++)
		printf("%d ", p->a[i]);
	printf("\n");
}

2.数据的增加

(1)头部插入数据

在头部插入数据前,需要将所有数据向后挪一位,腾出第一个数据的位置来插入新数据。

要注意这里必须从后向前依次挪动数据,否则前面一位的数据会覆盖后面一位的数据,导致数据的丢失。

代码如下(示例):

void SeqListPushFront(SeqList* p, SeqListDataType x)
{
	assert(p);
	
	CheckCapacity(p);//该函数见下文
	
	int end = p->size;
	for (; end > 0; end--)
	{
		p->a[end] = p->a[end - 1];//从后向前挪动数据
	}
	p->a[0] = x;
	p->size++;//注意此处要让有效数据的个数+1
}

头插的函数实现后,会发现代码中有一个问题:如果此时顺序表中的数据已满,即当p->size和p->capacity相等时,那么在插入数据时就会出现越界访问的问题。所以需要在插入数据前判断当前顺序表是否已满,如果未满则正常插入数据,否则进行扩容,这里把这一功能封装为一个函数。

代码如下(示例):

//判断线性表是否已满
void CheckCapacity(SeqList* p)
{
	if (p->size == p->capacity)
	{
		int newcapacity = (p->capacity == 0) ? 4 : (p->capacity * 2);
		//如果容量为0(第一次插入数据),默认开辟4个SeqListDataType的空间
		//如果是在插入数据过程中顺序表已满,则将空间变为2倍(将空间变为2倍也可能会出现空间浪费的情况)
		//这里扩容的倍数需要具体情况具体分析,此处以2倍为例
		SeqListDataType* newA = (SeqListDataType*)realloc(p->a, sizeof(SeqListDataType)*newcapacity);
		if (newA == NULL)//开辟失败
		{
			printf("newcapacity fail\n");
			return;
		}
		p->a = newA;
		p->capacity = newcapacity;
	}
}

有了CheckCapacity函数后,只需在挪动数据前调用该函数,后面的插入数据即可正常实现。


(2)尾部插入数据

尾插数据与头插数据相比更加简单,因为不需要挪动数据,直接在数组末尾插入数据即可。

代码如下(示例):

//尾插
void SeqListPushBack(SeqList* p, SeqListDataType x)
{
	assert(p);
	
	CheckCapacity(p);

	p->a[p->size] = x;
	//p->size表示当前顺序表中有效的数据个数
	//a[p->size]访问当前最后一个数据的下一个位置
	p->size++;
}

3.数据的删除

(1)头部删除数据

头删只需从头开始从后向前挪动数据即可,原来第一位的数据会被覆盖以达到删除的目的。

代码如下(示例):

//头删
void SeqListPopFront(SeqList* p)
{
	assert(p);
	assert(p->size > 0);
	int i = 0;
	for (i = 0; i < p->size - 1; i++)//从前向后遍历挪动数据
		p->a[i] = p->a[i + 1];
	p->size--;
}

(2)尾部删除数据

尾删同样也比头删简单,只需让数组中有效数据的个数-1即可,由于顺序表的size减小1,所以此时不会访问到被删除的数据,虽然没有真正的删除掉末尾的数据,但达到了删除的目的。

代码如下(示例):

//尾删
void SeqListPopBack(SeqList* p)
{
	assert(p);
	assert(p->size > 0);

	CheckCapacity(p);

	p->size--;
}

4.任意位置插入、删除数据

任意位置实现数据的插入只需将该位置及该位置之后的数据向后挪动一位,并将该位置改为要插入的数据即可。

代码如下(示例):

//中间插入数据
void SeqListInsert(SeqList* p, int pos, SeqListDataType x)
{
	assert(p);
	assert(pos >= 0 && pos <= p->size);
	CheckCapacity(p);

	int i = p->size;
	for (; i > pos; i--)
		p->a[i] = p->a[i - 1];//向后挪动数据
	p->a[i] = x;
	p->size++;
}

任意位置实现数据的删除只需将该位置及该位置之后的数据向前挪动一位即可。

代码如下(示例):

//中间删除数据
void SeqListErase(SeqList* p, int pos)
{
	assert(p);
	assert(pos >= 0 && pos < p->size);
	CheckCapacity(p);

	int i = pos;
	for (; i < p->size - 1; i++)
		p->a[i] = p->a[i + 1];//向前挪动数据
	p->size--;
}

不难发现,头插、尾插、头删、尾删是这两个函数的特殊情况,所以顺序表头部和尾部的操作可通过调用这两个函数实现。

代码如下(示例):

//头插
void SeqListPushFront(SeqList* p, SeqListDataType x)
{
	SeqListInsert(p, 0, x);
}

//尾插
void SeqListPushBack(SeqList* p, SeqListDataType x)
{
	SeqListInsert(p, p->size, x);
}

//头删
void SeqListPopFront(SeqList* p)
{
	SeqListErase(p, 0);
}

//尾删
void SeqListPopBack(SeqList* p)
{
	SeqListErase(p, p->size - 1);
}

5.查找数据

查找数据只需遍历顺序表即可。

代码如下(示例):

//查找(第一个)值为x的数据的下标
int SeqListFind(SeqList* p, SeqListDataType x)
{
	assert(p);
	int i = 0;
	for (i = 0; i < p->size; i++)
	{
		if (p->a[i] == x)
			return i;
	}
	return -1;//找不到返回-1
}

6.修改数据

代码如下(示例):

//将pos位置的值修改为x
void SeqListModify(SeqList* p, int pos, SeqListDataType x)
{
	assert(p);
	assert(pos >= 0 && pos < p->size);

	p->a[pos] = x;
}

三、顺序表的优劣

优点:
1.由于数据是连续存放的,所以空间利用率较高,访问时命中率高。
2.数据存放在数组中,可以直接通过下标访问,时间复杂度为O(1)。
3.对尾部进行操作时,时间复杂度为O(1)。

缺点:
1.在中间和头部的位置插入删除数据时,由于需要挪动数据,时间复杂度为O(N)。
2.增容时需要申请新空间,如果申请的新空间与原来不同,则需要将顺序表中的全部数据拷贝一次,这一过程会有不小的消耗。
3.一次扩容的大小难以确定。如果一次扩容较大,则会出现浪费空间的情况,如果一次扩容较小,又会由于频繁的扩容导致性能的下降。


感谢阅读,如有错误请批评指正

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

山舟

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值