数据结构之顺序表

本文详细介绍了C语言中动态顺序表的数据结构、接口实现,包括初始化、空间检查、插入、删除操作,以及如何避免内存泄漏。重点讨论了空间管理策略,特别是扩容时选择2倍容量的原因。
摘要由CSDN通过智能技术生成

1.什么是线性表?

在介绍顺序表之前,我们先介绍线性表。线性表 ( linear list ) 是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...

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

也就是说顺序表是线性表顺序存储结构中的一种。

2.顺序表

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

顺序表一般可以分为:

2.1 静态顺序表:使用定长数组存储元素。

#define N 7
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType array[N];//定长数组
	size_t size; //有效数据的个数
}SeqList;

2.2动态顺序表:使用动态开辟的数组存储。

typedef struct SeqList
{
	SLDatatype* _a;//该指针指向动态开辟的数组
	size_t _size;//顺序表有效数据的大小
	size_t _capacity;//顺序表的容量
}SeqList;

本文介绍动态顺序表中各个接口的实现。

3.顺序表相关接口的实现(C语言)

3.1顺序表的初始化

//1、初始化顺序表
void SeqListInit(SeqList* ps)
{
	assert(ps);

	//初始化时,开辟4个大小为sizeof(SLDataType)的空间
	ps->_a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
	if (ps->_a == NULL)
	{
		printf("申请内存失败\n");
		exit(-1);//结束掉程序
	}

	ps->_size = 0;
	ps->_capacity = 4;
}

顺序表在初始化时可以适当的开辟空间,可以直接将指针_a给设置为NULL,顺序表的大小size和容量capacity设置为0。当然也可以开辟4个大小为sizeof(SLDataType)的空间。

3.2检查顺序表的空间

顺序表在进行插入和删除操作时,会涉及到顺序表空间大小的问题,在插入时候首先要判断顺序表的空间是否已满。如果空间已满则需要开辟空间,才能够进行插入操作。

//2、检查数组的容量是否够用,不够用就扩容
//这里一般扩容为原来的2倍
void SeqListCheckCapacity(SeqList* ps)
{
	//如果满了需要增容,一般增加2倍
	if (ps->_size >= ps->_capacity)
	{
		ps->_capacity *= 2;
		ps->_a = (SLDataType*)realloc(ps->_a, sizeof(SLDataType) * ps->_capacity);
		if (ps->_a == NULL)
		{
			printf("扩容失败\n");
			exit(-1);
		}
	}
}

但是为什么这里顺序表满了扩容为原来空间大小的2倍?而不是3倍?4倍?或者一次扩容1或者2、3个SLDataType类型的空间?

因为如果1次增容1个(即增容比较小),就会频繁增容。如果一次增容1000个空间(即增容太多),增容又太多,会造成空间浪费。

realloc函数可能原地扩容,也可能异地扩容。如果空间比较大,增一次容的代价就大,比如此时有10000个数据,增加到20000个,如果后面没有足够大的空间,realloc函数就需要异地扩容。即重新开辟一块空间扩容,要拷贝原来10000个数据到新开辟的空间中,并释放旧空间,代价就比较大,数组的数据量越大,越要降低增容的频次,否则就如上所述,增容的代价比较大。

增加的倍数越大,可能浪费的空间就越多;如果一次扩容2倍,最多只浪费了50%的空间。如果每次扩容的空间越小,后面有可能频繁的增容,代价比较大。

3.3打印函数

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

3.4顺序表尾插

//4、尾插
void pushBack(SeqList * ps, SLDataType x)
{
	assert(ps);

	SeqListCheckCapacity(ps);
	ps->_a[ps->_size] = x;
	ps->_size++;
}

在对顺序表进行尾部插入数据时,我们首先要检查一下顺序表空间的大小,如果顺序表容量不满,则可以直接在顺序表的尾部插入数据。如果顺序表的容量已满,则就需要先对顺序表的容量进行扩容,再进行插入。 SeqListCheckCapacity()函数就是要检查顺序表的容量是否已满。

3.5顺序表尾删

//5、尾删
void popBack(SeqList* ps)
{
	assert(ps);
	assert(ps->_size > 0);

	ps->_size--;
}

在对顺序表进行尾删操作之前,要首先判断顺序表是否为空,即ps->_size==0时,此时就不需要进行尾删了。为防止越界访问,因此在该代码中加了两个断言就可以防止这种情况发生。尾删只需要将ps->_size--操作就可以了。

3.6顺序表头插

//4、头插
void SeqListPsuhFront(SeqList* ps, SLDataType x)
{
	assert(ps);
	
	//顺序表头插首先要判断顺序表的容量是否足够
	SeqListCheckCapacity(ps);

	int end = ps->_size;
	while (end >= 0)
	{
		ps->_a[end] = ps->_a[end - 1];
		--end;
	}
	ps->_a[0] = x;
	++ps->_size;
}

顺序表的头插首先要检查顺序表的容量是否足够,保证容量大小充足。然后将顺序表中的数据依次往后移动一位,将顺序表的第一个位置给空出来,最后将新元素插入空位置。

3.7顺序表头删

//5、头删
void SeqListPopFront(SeqList* ps)
{
	assert(ps);
	assert(ps->_size > 0);

	int start = 0;
	while (start < ps->_size-1)
	{
		ps->_a[start] = ps->_a[start + 1];
		++start;
	}
	--ps->_size;
}

顺序表的头删和尾删的逻辑大致是相同的,首先我们需要检查一下顺序表的大小_size是否为0。接着我们只需要将顺序表中后面的数据覆盖前面的数据,这样就完成了头删的操作。

3.8顺序表查找元素x的位置

//6、顺序表查找元素x的位置
int SeqListFind(SeqList* ps, SLDataType x)
{
	assert(ps);

	int i = 0;
	while(i < ps->_size)
	{
		if (ps->_a[i] == x)
			return i;

		i++;
	}
	return -1;
}

首先遍历顺序表,找到要查找的元素就返回该元素的下标,如果找不到就返回-1。

3.9销毁malloc函数开辟的空间

//7、销毁malloc函数开辟的空间,防止出现野指针
//实际上销毁的是堆区动态开辟的空间
//结构体变量不需要销毁,因为结构体变量是局部变量,函数栈帧结束后,自动销毁
void SeqListDestory(SeqList* ps)
{
	assert(ps);

	free(ps->_a);
	ps->_a = NULL;
	ps->_size = 0;
	ps->_capacity = 0;
}

动态开辟的空间是在堆区申请的,用完之后一定要将它释放,防止出现内存泄漏。

4.代码复用

在日常的开发过程中,尽量减少重复代码,提高代码的可复用性。

4.1任意位置插入数据

顺序表任意位置插入数据,相当于顺序表头插和尾插的升级版,只是多了一个参数pos,如果pos==0,则相当于头插,如果pos==ps->_size,就相当于尾插。

//8、指定位置的插入
void SeqListInsert(SeqList* ps, int pos, SLDataType x)
{
	assert(ps);

	//断言一下pos,因为插入的数据x必须在一个有效的位置
	assert(pos <= ps->_size && pos >= 0);
	
	//顺序表任意位置插入数据,首先要判断顺序表的容量是否足够
	SeqListCheckCapacity(ps);

	int end = ps->_size-1;
	while (end >= pos)
	{
		ps->_a[end + 1] = ps->_a[end];
		end--;
	}
	ps->_a[pos] = x;
	ps->_size++;
}

4.2任意位置删除数据

首先需断言要删除的数据要在一个有效的位置,pos不能等于_size,因为_size不是一个有效位置,顺序表的下标最大为_size-1。然后将pos后面的数据整体向前移动,覆盖原来pos位置所在的数据,最后_size--。

//9、指定位置的删除
void SeqListErase(SeqList* ps, int pos)
{
	assert(ps);
	//删除的数据要在一个有效的位置
	assert(pos >= 0 && pos < ps->_size);

	int start = pos;
	while (start < ps->_size-1)
	{
		ps->_a[start] = ps->_a[start+1];
		start++;
	}
	ps->_size--;
}

由于尾删、尾插、头删、头插是任意位置插入删除数据的特殊情况,我们可以将部分代码代码进行复用。源代码如下:可参考:顺序表代码实现

4.3源代码

1、SeqList.h

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

//顺序表的要求:有效数组在数组中必须是连续的
//动态顺序表设计(大小可变)
//该代码的作用是假如要改变数组中存储数据的类型,将int改为double就可以了
//其他地方不变
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType* _a;//指动态开辟的数组
	size_t _size;//数组中有效数据的个数
	size_t _capacity;//容量空间的大小
}SeqList;

//1、初始化顺序表
void SeqListInit(SeqList* ps);

//2、检查数组的容量是否够用,不够用就扩容
void SeqListCheckCapacity(SeqList* ps);

//2、尾插
void pushBack(SeqList* ps, SLDataType x);

//3、尾删
void popBack(SeqList* ps);

//4、头插
void SeqListPsuhFront(SeqList* ps, SLDataType x);

//5、头删
void SeqListPopFront(SeqList* ps);

//6、指定位置的插入
void SeqListInsert(SeqList* ps, int pos, SLDataType x);

//7、指定位置的删除
void SeqListErase(SeqList* ps, int pos);

//8、顺序表查找元素x的位置
int SeqListFind(SeqList* psl, SLDataType x);

//9、销毁malloc函数开辟的空间
void SeqListDestory(SeqList* ps);

//10、打印
void SeqListPrint(SeqList* ps);

2、SeqList.c

#include"SeqList.h"

//1、初始化顺序表
void SeqListInit(SeqList* ps)
{
	assert(ps);

	//初始化时,开辟4个大小为sizeof(SLDataType)的空间
	ps->_a = (SLDataType*)malloc(sizeof(SLDataType) * 4);
	if (ps->_a == NULL)
	{
		printf("申请内存失败\n");
		exit(-1);//结束掉程序
	}

	ps->_size = 0;
	ps->_capacity = 4;
}

//2、检查数组的容量是否够用,不够用就扩容
//这里一般扩容为原来的2倍
void SeqListCheckCapacity(SeqList* ps)
{
	//如果满了需要增容,一般增加2倍
	if (ps->_size >= ps->_capacity)
	{
		ps->_capacity *= 2;
		ps->_a = (SLDataType*)realloc(ps->_a, sizeof(SLDataType) * ps->_capacity);
		if (ps->_a == NULL)
		{
			printf("扩容失败\n");
			exit(-1);
		}
	}
}


//3、尾插
void pushBack(SeqList * ps, SLDataType x)
{
	/*assert(ps);

	SeqListCheckCapacity(ps);
	ps->_a[ps->_size] = x;
	ps->_size++;*/
	SeqListInsert(ps, ps->_size, x);
}

//4、尾删
void popBack(SeqList* ps)
{
	/*assert(ps);
	assert(ps->_size > 0);*/

	//ps->_size--;
	SeqListErase(ps, ps->_size - 1);
}


//4、头插
void SeqListPsuhFront(SeqList* ps, SLDataType x)
{
	//assert(ps);
	
	顺序表头插首先要判断顺序表的容量是否足够
	//SeqListCheckCapacity(ps);

	//int end = ps->_size;
	//while (end >= 0)
	//{
	//	ps->_a[end] = ps->_a[end - 1];
	//	--end;
	//}
	//ps->_a[0] = x;
	//++ps->_size;
	SeqListInsert(ps, 0, x);
}

//5、头删
void SeqListPopFront(SeqList* ps)
{
	/*assert(ps);
	assert(ps->_size > 0);

	int start = 0;
	while (start < ps->_size-1)
	{
		ps->_a[start] = ps->_a[start + 1];
		++start;
	}
	--ps->_size;*/
	SeqListErase(ps,0);
}



//6、顺序表查找元素x的位置
int SeqListFind(SeqList* ps, SLDataType x)
{
	assert(ps);

	int i = 0;
	while(i < ps->_size)
	{
		if (ps->_a[i] == x)
			return i;

		i++;
	}
	return -1;
}

//7、销毁malloc函数开辟的空间,防止出现野指针
//实际上销毁的是堆区动态开辟的空间
//结构体变量不需要销毁,因为结构体变量是局部变量,函数栈帧结束后,自动销毁
void SeqListDestory(SeqList* ps)
{
	assert(ps);

	free(ps->_a);
	ps->_a = NULL;
	ps->_size = 0;
	ps->_capacity = 0;
}

//8、指定位置的插入
void SeqListInsert(SeqList* ps, int pos, SLDataType x)
{
	assert(ps);

	//断言一下pos,因为插入的数据x必须在一个有效的位置
	assert(pos <= ps->_size && pos >= 0);
	
	//顺序表任意位置插入数据,首先要判断顺序表的容量是否足够
	SeqListCheckCapacity(ps);

	int end = ps->_size-1;
	while (end >= pos)
	{
		ps->_a[end + 1] = ps->_a[end];
		end--;
	}
	ps->_a[pos] = x;
	ps->_size++;
}

//9、指定位置的删除
void SeqListErase(SeqList* ps, int pos)
{
	assert(ps);
	//删除的数据要在一个有效的位置
	assert(pos >= 0 && pos < ps->_size);

	int start = pos;
	while (start < ps->_size-1)
	{
		ps->_a[start] = ps->_a[start+1];
		start++;
	}
	ps->_size--;
}


//10、打印
void SeqListPrint(SeqList* ps)
{
	assert(ps);
	int i = 0;
	for (i = 0; i < ps->_size; i++)
	{
		printf("%d ", ps->_a[i]);
	}
	printf("\n");
}

3、test.c

#include"SeqList.h"

void TestSeqList1()
{
	//1、初始化数组
	SeqList s;
	SeqListInit(&s);

	//2、尾插
	pushBack(&s, 1);
	pushBack(&s, 2);
	pushBack(&s, 3);
	pushBack(&s, 4);
	pushBack(&s, 5);
	pushBack(&s, 6);
	SeqListPrint(&s);

	//3、尾删
	popBack(&s);
	popBack(&s);
	SeqListPrint(&s);

	//4、头插
	SeqListPsuhFront(&s, 10);
	SeqListPsuhFront(&s, 11);
	SeqListPrint(&s);

	//5、头删
	SeqListPopFront(&s);
	SeqListPopFront(&s);
	SeqListPrint(&s);

	//6、顺序表查找元素x的位置
	printf("%d\n", SeqListFind(&s, 2));

	//8、指定位置的插入
	SeqListInsert(&s, 0, 20);
	SeqListInsert(&s, 1, 30);
	SeqListPrint(&s);

	//9、指定位置的删除
	SeqListErase(&s, 1);
	SeqListPrint(&s);


	//7、销毁malloc函数开辟的空间,防止出现野指针
	SeqListDestory(&s);
}

int main()
{
	TestSeqList1();
	
	return 0;
}

  • 36
    点赞
  • 59
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值