【 数据结构 】顺序表的实现 - 详解(C语言版)

本文详细介绍了如何使用C语言实现动态顺序表,包括初始化、打印、容量检查、尾插、尾删、头插、头删、查找、修改和销毁等操作。重点讨论了realloc函数的使用,以及在顺序表中插入和删除数据的实现细节,强调了防止数组越界的重要性。
摘要由CSDN通过智能技术生成

目录

前言

线性表:

顺序表:

概念及结构:

 顺序表的实现:

 头文件:SeqList.h

 realloc函数讲解:

具体函数的实现:SeqList.c

顺序表的初始化:

顺序表的打印:

容量的检查:

顺序表的尾插:

顺序表的尾删:

顺序表的头插:

顺序表的头删:

在顺序表的指定位置插入数据:

在顺序表的指定位置删除数据:

顺序表的查找: 

顺序表的修改:

顺序表的销毁:

数组越界的检查:

前言

本文用C语言来描述数据结构中的顺序表,包括顺序表的增加数据,删除数据,查找指定数据,修改指定数据,也就是简单的增删查改操作。


线性表:(Sequence List)

  1. 线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
  2. 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。


顺序表:

概念及结构:

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

(1)静态顺序表:使用定长数组存储元素:

 (2)动态顺序表:使用动态开辟的数组存储

 注意:

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


备注:

一般我们写一个项目的时候,将所要包含的头文件,函数的声明,结构体等放在一个头文件.h 里面,一般将函数的定义也就是函数实现的过程放在.c的文件里面,一般将函数的测试也就是主函数写在另一个.c的文件里面也就是test.c.这个文件里面。


 顺序表的实现:

 头文件:SeqList.h

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

//数据类型的定义
typedef int DataType;

//结构体的定义
typedef struct SeqList
{
	int* a;
	int size;
	int capacity;
}SeqList,SL;

//顺序表的打印
void SeqListPrint(SeqList* ps1);
//初始化顺序表
void SeqListInit(SeqList* ps1);
//检查容量
void SeqListCheckCapacity(SeqList* ps1);
//顺序表尾插
void SeqListPushBack(SeqList* ps1, DataType x);
//顺序表的尾删
void SeqListPopBack(SeqList* ps1);
//顺序表的头插
void SeqListPushFront(SeqList* ps1, DataType x);
//顺序表的头删
void SeqListPopFront(SeqList* ps1);
//顺序表的查找
int SeqListFind(SeqList* ps1, DataType x);
//在顺序表pos位置插入x
void SeqListInsert(SeqList* ps1, size_t pos, DataType x);
//在顺序表pos位置删除数据
void SeqListErase(SeqList* ps1, size_t pos);
//顺序表的修改
void SeqListModify(SeqList* ps1, size_t pos, DataType x);
//顺序表的销毁
void SeqListDestroy(SeqList* ps1);

 realloc函数讲解:

realloc函数是内存追加函数,第一个参数是想要追加的空间的起始地址,第二个参数是先要扩容的新空间的大小,返回值是扩容后新空间的起始地址,若扩容失败返回空指针NULL,同时realloc函数返回的是void*类型的指针,应将realloc返回的指针强制类型转换成所需要的指针类型。

注意:

realloc函数扩容存在两种情况:

1.  就地扩容

     A 空间想要扩容到原来的两倍,此时realloc函数返回这块空间的起始地址 - ptr指针。

 2.  异地扩容

    A 空间想要扩容到原来的两倍,但是此时A空间后面没有足够的空余空间,realloc函数就会再找一块新的空间,这个新的空间满足所需要的空间大小,realloc函数会将A空间的内容拷贝到新的空间并返回新的空间的地址ptr1。

一般不会直接将原来空间存储起始地址的变量作为接收realloc返回值的接收的变量,往往开辟一个新的变量来存储realloc返回的地址,因为当空间扩容失败后会返回NULL,如果扩容开辟失败的话直接返回NULL会将原来的地址的空间首地址赋值成NULL这样不仅没有扩容成功,原来的空间内存也找不到了。

所以realloc函数标准写法应该如下段代码:

int main()
{
	//开辟10个整形的空间
	//int arr[10];
	int* p = (int*)calloc(10,sizeof(int));
	if (p == NULL)
	{
		printf("%s\n", strerror(errno));
		return 0;//结束代码
	}
	//使用
	int i = 0;
	for (i = 0; i < 10; i++)
	{
		printf("%d ", p[i]);
	}
	//需要增容
	int* ptr = (int*)realloc(p, 80);
	//返回的是重新增容的空间的起始地址
	//当realloc增容失败是会返回空指针(NULL)
    //有可能会造成原来数据的丢失,所以不能直接将返回值赋给p
	if (ptr != NULL)
	{
		p = ptr;//增容成功;
	}
	free(p);
	//如果没有free()程序结束的时候才被释放
	//内存被占用 - 内存泄漏
	//一般开辟和释放同时存在
	p = NULL;

	return 0;
}

具体函数的实现:SeqList.c


顺序表的初始化:

//初始化顺序表
void SeqListInit(SeqList* ps1)
{
	ps1->size = 0;
	ps1->a = NULL;
	ps1->capacity = 0;
}

顺序表的打印:

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

assert函数断言传过来的指针是否为空,若为空就直接结束程序 。


容量的检查:

//检查容量
void SeqListCheckCapacity(SeqList* ps1)
{
	assert(ps1);
	if (ps1->size == ps1->capacity)
	{
		size_t newcapacity = ps1->capacity == 0 ? 4 : ps1->capacity * 2;
		int* tmp = realloc(ps1->a, sizeof(DataType) * newcapacity);
		if (tmp == NULL)
		{
			printf("%s\n", strerror(errno));
			exit(-1);
		}
		else
		{
			ps1->a = tmp;
		}
		ps1->capacity = newcapacity;
	}
}

因为创建的顺序表示个动态存储的顺序表那么就得满足当容量不足的时候,能够进行扩容,而不是一开始在定义结构体的时候就将顺序表的容量写死。

这里采用的是检查容量的方式来实现顺序表的动态存储,(size)是已经存入的数据个数,(capacity)是可以存储数据的个数,当存入和容量相等即空间满了的时候,这里采用realloc函数对顺序表进行扩容。因为realloc函数实在堆区申请空间的所以一次扩容不宜过多这里是一次扩容到原来的两倍。


顺序表的尾插:

//顺序表的尾插
void SeqListPushBack(SeqList* ps1, DataType x)
{
	assert(ps1);

	//SeqListCheckCapacity(ps1);
	//ps1->a[ps1->size] = x;
	//ps1->size++;
	SeqListInsert(ps1, ps1->size, x);
}

先检查顺序表的空间是否充足用来插入一个新的数据,再在顺序表得尾部添加一个数据。


顺序表的尾删:

//顺序表的尾删
void SeqListPopBack(SeqList* ps1)
{
	/*assert(ps1);
	if (ps1->size > 0)
	{
		ps1->size--;
	}*/
	SeqListErase(ps1, ps1->size - 1);
}

无需过多操作直接将size--就可以了,因为size是存入数据的个数,size--也就是将最后一个元素去掉。


顺序表的头插:

//顺序表的头插
void SeqListPushFront(SeqList* ps1, DataType x)
{
	assert(ps1);
	/*SeqListCheckCapacity(ps1);
	int end = ps1->size - 1;
	while (end >= 0)
	{
		ps1->a[end + 1] = ps1->a[end];
		end--;
	}
	ps1->size++;
	ps1->a[0] = x;*/
	SeqListInsert(ps1, 0, x);
}

先检查顺序表的空间是否充足用来插入一个新的数据。因为是头插就得将第一个的位置腾出来,那就要将顺序表中每一个元素向后移动一个位置,这里采用从后往前挪动的方法即将最后一个数据向后挪动一个,再将到倒数第二个数据向后挪动一个,依次向前,因为如果从前往后挪例如将第一个挪到第二个的时候会将第二个数据覆盖,从而修改了顺序表原有的数据。


顺序表的头删:

//顺序表的头删
void SeqListPopFront(SeqList* ps1)
{
	assert(ps1);
	/*int begin = 1;
	if (ps1->size > 0)
	{
		while (begin < ps1->size)
		{
			ps1->a[begin - 1] = ps1->a[begin];
			begin++;
		}
		ps1->size--;
	}*/
	SeqListErase(ps1, 0);
}

 先检查顺序表的空间是否充足用来插入一个新的数据。因为是删除掉第一个数据,就需要用到覆盖。思路是将整个顺序表即从第二个开始整体向前挪动一个单位,这里采用的是从二个数据开始向前挪一个,再将第三个向前挪一个,依次下去这样第一个数据就会被覆盖掉,如果采用从后往前依次挪动的话,会造成顺序表中的数据被覆盖从而内容被修改。


在顺序表的指定位置插入数据:

//在顺序表pos位置插入x
void SeqListInsert(SeqList* ps1, size_t pos, DataType x)
{
	assert(ps1);
	assert(pos <= ps1->size);

	SeqListCheckCapacity(ps1);
	size_t end = ps1->size;
	while (end > pos)
	{
		ps1->a[end] = ps1->a[end - 1];
		end--;
	}
	ps1->a[pos] = x;
	ps1->size++;
}

对要插入的pos位置断言,如果要插入的位置不符合规范就直接结束程序。检查顺序表的空间是否充足用来插入一个新的数据。因为是在指定位置插入数据,这里才去从后向前挪的方式,因为是要将pos的位置腾出来,思路和头插一样,就不再赘述。

这里值得注意的是这个循环体:

 

如果要是在pos == 0的地方插入数据的话就相当于头插。end只需要走到第二个数据的位置(即end到1的时候就结束了)这样就可以实现将pos位置之后的位置向后挪一个单位的操作。


因为通常标准数组的下标都是用无符号整形表示的,pos的类型为无符号的,如果采用这种方法:

 

 如果要是在pos == 0的地方插入数据的话就相当于头插。这里的end会走到下标为0的位置,此时end再自减就为-1,因为end的类型为sizez_t(无符号整形)就会将-1的补码按照无符号整形解读成一个很大的数即(4294967295)那么循环永不停止,就是死循环。

如果将end的类型设置为int有符号整形即(int end  = ps1->size - 1;)那当end为0时再次自减一次还会发生和上面一样的问题,因为这里发生了算数转换,当int类型的数据和size_t类型的数据进行比较时,int类型的数据要转换成size_t类型的数据这时-1又被转成很大的数即(4294967295)。循环不会停止。

上述代码为第一种解决方案,第二种解决方案就是将pos的数据类型进行强制类型转换即(int)pos,这样循环才能够停下来。


在顺序表的指定位置删除数据:

//在顺序表pos位置删除数据
void SeqListErase(SeqList* ps1, size_t pos)
{
	assert(ps1);
	assert(pos < ps1->size);

	int begin = pos + 1;
	while (begin < ps1->size)
	{
		ps1->a[begin - 1] = ps1->a[begin];
		begin++;
	}

	ps1->size--;
}

这里将pos位置的数据删除,就是将pos之后的所有数据向前挪动一个单位将pos位置的数据覆盖掉。这里采取从前向后挪动的方式与头删类似,在此不再赘述,这里要注意数组越界的问题。


顺序表的查找: 

//顺序表的查找
int SeqListFind(SeqList* ps1, DataType x)
{
	assert(ps1);
	for (int i = 0; i < ps1->size; i++)
	{
		if (x == ps1->a[i])
			return i;
	}
	return -1;
}

x为想要查找的数据,如果查找到了返回这个值的下标,如果找不到返回-1。


顺序表的修改:

//顺序表的修改
void SeqListModify(SeqList* ps1, size_t pos, DataType x)
{
	assert(ps1);
	assert(pos < ps1->size);

	ps1->a[pos] = x;
}

将指定pos位置的数据修改成x。


顺序表的销毁:

//顺序表的销毁
void SeqListDestroy(SeqList* psl)
{
	assert(psl);
	free(psl->a);
	psl->a = NULL;
	psl->capacity = psl->size = 0;
}

将顺序表连续的空间的首地址指向空,有效数据个数和容量均置为0。


数组越界的检查:

如果上述代码中没有assert断言的话,会出现越界访问的问题。编译器对越界的检查是抽查,即有的越界会被查出来,有的越界就有可能查不出来,所以我们要在写程序的时候尽量避免越界的可能。

一般数组在越界的时候是不会当时就检测出越界,像静态的数组例如:int arr[10] 这种数组越界的检查会在函数结束的时候检查,例如:像malloc 开辟的动态数组,会在free释放空间的时候检查数组是否越界。如果是在free的时候报错了,就要检查是否是数组越界的情况。

以下是使用C语言实现顺序表判定子集的代码: ```c #include <stdio.h> #define MAXSIZE 100 typedef struct { int data[MAXSIZE]; int length; } SqList; // 初始化顺序表 void init(SqList *L) { L->length = 0; } // 向顺序表中插入元素 void insert(SqList *L, int x) { if (L->length == MAXSIZE) { printf("顺序表已满,无法插入。\n"); return; } L->data[L->length++] = x; } // 判断顺序表B是否是顺序表A的子集 int isSubset(SqList *A, SqList *B) { int i, j; for (i = 0; i < B->length; i++) { for (j = 0; j < A->length; j++) { if (B->data[i] == A->data[j]) { break; } } if (j == A->length) { return 0; } } return 1; } int main() { SqList A, B; init(&A); init(&B); // 向顺序表A中插入元素 insert(&A, 1); insert(&A, 2); insert(&A, 3); insert(&A, 4); insert(&A, 5); // 向顺序表B中插入元素 insert(&B, 2); insert(&B, 4); insert(&B, 6); // 判断顺序表B是否是顺序表A的子集 if (isSubset(&A, &B)) { printf("B是A的子集。\n"); } else { printf("B不是A的子集。\n"); } return 0; } ``` 在该代码中,我们定义了一个`SqList`结构体来顺序表,其中`data`数组用于存储顺序表中的元素,`length`顺序表的长度。我们通过`init`函数来初始化顺序表,通过`insert`函数来向顺序表中插入元素。 在`isSubset`函数中,我们使用两个循环来遍历顺序表B和顺序表A中的元素,如果在顺序表A中找到了顺序表B中的元素,则继续循环查找顺序表B中的下一个元素。如果在顺序表A中找不到顺序表B中的元素,则直接返回0顺序表B不是顺序表A的子集。如果顺序表B中的所有元素都在顺序表A中找到了,则返回1顺序表B是顺序表A的子集。 最后,在`main`函数中,我们初始化了两个顺序表A和B,并向它们中插入了一些元素。然后,我们调用`isSubset`函数来判断顺序表B是否是顺序表A的子集,并输出结果。
评论 15
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

yy_上上谦

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

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

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

打赏作者

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

抵扣说明:

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

余额充值