C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C语言 顺序表

C语言 超详细顺序表的模拟实现实例建议收藏

作者:鹿九丸

程序中经常需要将一组数据元素作为整体管理和使用,需要创建这种元素组,用变量记录它们,传进传出函数等。一组数据中包含的元素个数可能发生变化,顺序表则是将元素顺序地存放在一块连续的存储区里,元素间的顺序关系由它们的存储顺序自然表示

概念及结构

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

顺序表一般可以分为:

静态顺序表:使用定长数组存储元素,元素的数目无法进行修改。

//顺序表的静态存储
#define N 7
typedef int SLDataType;
typedef struct SeqList
{
	SLDataType array[N];//定长数组
	size_t size;//有效数据的个数
}SeqList;

动态顺序表

//顺序表的动态存储
typedef struct SeqList
{
	SLDataType* array;//指向动态开辟的数组,空间不够可以增容
	size_t size;//有效数据的个数
	size_t capacity;//容量空间的大小
};

接口实现

静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多了浪 费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下面我们实现动态顺序表。

1 顺序表的动态存储

typedef int SLDataType;//顺序表中存储的数据,此处假设是int型
typedef struct SeqList
{
	int* a;//指向动态开辟的数组空间,空间可以随时增容
	int size;//存储数据个数
	int capacity;//存储空间大小
}SL,SeqList;

2 顺序表初始化

void SeqListInit(SeqList* psl);//声明
void SeqListInit(SeqList* psl)
{
	assert(psl);//进行断言是因为当psl为NULL时,下面的操作将无法进行,因为空指针是无法进行解引用的。
	psl->a = NULL;
	psl->size = 0;
	psl->capacity = 0;
}//函数实现

注意:进行断言是因为当psl为NULL时,下面的操作将无法进行,因为空指针是无法进行解引用的,后面也是如此。

3 顺序表的销毁

void SeqListDestroy(SeqList* psl);
void SeqListDestroy(SeqList* psl)
{
	assert(psl);
	free(psl->a);
	psl->a = NULL;
	psl->capacity = psl->size = 0;
}

注意:free后面括号中的指针必须是malloc开辟出来的那块空间,且不能有任何的偏差(即指针不能发生移动)。

下面进行举例:

image-20220313121909217

像上面这样使用是完全没有问题的,但是像下面这样进行使用就出现了问题:

image-20220313122035082

tmp进行自增操作后,就指向了下图所示位置:

image-20220313122258326

free的位置是tmp++后的位置,这不符合C语言的规定,且即使正常的释放掉了,前面的那一块int空间也将引起内存泄漏问题,即动态开辟的内存忘记释放。

4 顺序表的尾插

void SeqListPushBack(SeqList* psl,SLDataType x);//声明
void SeqListPushBack(SeqList* psl, SLDataType x)
{
	assert(psl);
	//如果满了,就进行扩容
	SeqListCheckCapacity(psl);
	psl->a[psl->size] = x;
	psl->size++;
}

image-20220313123214723

5 顺序表的尾删

void SeqListPopBack(SeqList* psl);
void SeqListPopBack(SeqList* psl)
{
	assert(psl);
	if(psl->size > 0)
	{
		psl->size -= 1;
	}
}

6 顺序表的头插

void SeqListPushFront(SeqList* psl, SLDataType x);
void SeqListPushFront(SeqList* psl, SLDataType x)
{
	assert(psl);
	SeqListCheckCapacity(psl);
	int end = psl->size - 1;
	while (end >= 0)
	{
		psl->a[end+1] = psl->a[end];
		--end;
	}
	psl->a[0] = x;
	psl->size++;
}

顺序表的头插会涉及到后续元素的移动,头插时要将顺序表中的元素从后面开始进行移动,因为从前面开始移动的话会出现覆盖现象。下面是图示:

image-20220313150759211

7 顺序表的头删

同理,如果想要元素不被覆盖,就只能从前向后进行移动。

void SeqListPopFront(SeqList* psl);
void SeqListPopFront(SeqList* psl)
{
	//挪出数据,腾出头部空间
	//方法一:从1开始移动
	/*assert(psl);
	if (psl->size > 0)
	{
		int begin = 1;
		while (begin < psl->size)
		{
			psl->a[begin - 1] = psl->a[begin];
			begin++;
		}
		psl->size--;
	}*/
	//方法二:从0开始移动
	assert(psl);
	if (psl->size > 0)
	{
		int begin = 0;
		while (begin < psl->size - 1)
		{
			psl->a[begin] = psl->a[begin + 1];
			begin++;
		}
		psl->size--;
	}
}

下图是两种移动方式的区别:

image-20220313152013487

问:为什么不可以直接将指针进行加1操作?

8 顺序表容量的检查与扩容

void SeqListCheckCapacity(SeqList* psl);
void SeqListCheckCapacity(SeqList* psl)
{
	assert(psl);
	if (psl->capacity == psl->size)
	{
		size_t newCapacity = psl->capacity == 0 ? 4 : psl->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(psl->a, sizeof(SLDataType) * newCapacity);
		if (tmp == NULL)
		{
			printf("realloc fail\n");
			exit(-1);
		}
		else
		{
			psl->a = tmp;
			psl->capacity *= 2;
		}
	}
}

注意点1:此处考虑使用的是如果容量不够,就将容量扩容为原容量的两倍,但是最开始的容量是0,所以要考虑到最开始为0的情况。

注意点2:要对扩容是否成功进行检测,即判断刚申请的空间是否为空。

9 顺序表任意位置的插入

void SeqListInsert(SeqList* psl,size_t pos,SLDataType x);
void SeqListInsert(SeqList* psl, size_t pos, SLDataType x)
{
	assert(psl);
	//较为温和的检查方式
	/*if (pos > psl->size)
	{
		exit(-1);
	}*/
	assert(pos <= psl->size);//较为暴力的检查方式
	SeqListCheckCapacity(psl);
	size_t end = psl->size;
	while (end > pos)
	{
		psl->a[end] = psl->a[end-1];
		--end;
	}
	psl->a[pos] = x;
	psl->size++;
}

注意:

问:为什么end从psl->size开始?

答:此处需要注意的是pos和end的类型,为什么呢?因为两者都是无符号类型,所以尤其注意当end到了-1的时候,就会变成一个很大的数字,此时如果以此作为下标进行访问,一定会出现越界访问内存的情况,考虑一种极端情况,当pos为0的时候,end最小的时候就是为0,所以此时不会出现越界的情况,但是如果end是从psl->size - 1开始的话,那么while循环体内的语句就变成下面这样:

while(end > pos)
{
	psl->a[end+1] = psl->a[end];
	--end;
}

最后end的最小值会变成-1,但是因为end是size_t类型,所以会变成一个很大的数字,在whle()循环条件判定时条件始终是满足的,同时在进入循环体内之后,会出现越界访问内存的操作。所以两种情况的图如下所示:

image-20220311164610452

10 顺序表任意位置的删除

void SeqListErase(SeqList* psl, size_t pos);
void SeqListErase(SeqList* psl, size_t pos)
{
	assert(psl);
	assert(pos <= psl->size);
	size_t begin = pos+1;
	while (begin < psl->size)
	{
		psl->a[begin-1] = psl->a[begin];
		++begin;
	}
	psl->size--;
}

11 顺序表的打印

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

12 顺序表元素的查找

int SeqListFind(SeqList* psl,SLDataType x);
int SeqListFind(SeqList* psl, SLDataType x)
{
	assert(psl);
	for (int i = 0; i < psl->size; i++)
	{
		if (psl->a[i] == x)
			return i;//找到了对应元素,返回相应的下标
	}
	return -1;//说明没有找到对应的元素
}

13 顺序表元素的修改

void SeqListModify(SeqList* psl, size_t pos, SLDataType x);
void SeqListModify(SeqList* psl, size_t pos, SLDataType x)
{
	assert(psl);
	assert(pos < psl->size);
	psl->a[pos] = x;
}

到此这篇关于C语言 超详细顺序表的模拟实现实例建议收藏的文章就介绍到这了,更多相关C语言 顺序表内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文