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

目录

前言

线性表:

顺序表:

概念及结构:

 顺序表的实现:

 头文件:SeqList.h

 realloc函数讲解:

具体函数的实现:SeqList.c

顺序表的初始化:

顺序表的打印:

容量的检查:

顺序表的尾插:

顺序表的尾删:

顺序表的头插:

顺序表的头删:

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

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

顺序表的查找: 

 顺序表的修改:

顺序表的销毁:

数组越界的检查:


前言

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


线性表:(Sequence List)

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

【 数据结构 】顺序表的实现 - 详解(C语言版)_第1张图片


顺序表:

概念及结构:

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

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

【 数据结构 】顺序表的实现 - 详解(C语言版)_第2张图片

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

【 数据结构 】顺序表的实现 - 详解(C语言版)_第3张图片

 注意:

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


备注:

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


 顺序表的实现:

 头文件:SeqList.h

#include
#include
#include
#include
#include

//数据类型的定义
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指针。

【 数据结构 】顺序表的实现 - 详解(C语言版)_第4张图片

 2.  异地扩容

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

【 数据结构 】顺序表的实现 - 详解(C语言版)_第5张图片

一般不会直接将原来空间存储起始地址的变量作为接收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);
}

【 数据结构 】顺序表的实现 - 详解(C语言版)_第6张图片

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


顺序表的头删:

//顺序表的头删
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);
}

【 数据结构 】顺序表的实现 - 详解(C语言版)_第7张图片

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


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

//在顺序表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的位置腾出来,思路和头插一样,就不再赘述。

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

【 数据结构 】顺序表的实现 - 详解(C语言版)_第8张图片

 【 数据结构 】顺序表的实现 - 详解(C语言版)_第9张图片

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


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

【 数据结构 】顺序表的实现 - 详解(C语言版)_第10张图片

 【 数据结构 】顺序表的实现 - 详解(C语言版)_第11张图片

 如果要是在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语言)