顺序表详解|顺序表常见错误并调试分析

前言:

        今天我们开始学习基础的数据结构——顺序表,数据结构就是将数据在内存存储起来,在内存管理数据。


一、线性表

        1、线性表(Linear list)是n个具有相同特性的数据元素的有限序列,线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串……

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

顺序表详解|顺序表常见错误并调试分析_第1张图片

顺序表:用一组连续的存储单元依次存储数据元素,数据元素之间的逻辑关系由元素的存储位置表示。 

顺序表详解|顺序表常见错误并调试分析_第2张图片

链表:用一组任意的存储单元存储数据元素,数据元素之间的逻辑关系由指针表示。 

二、顺序表

1、概念即结构

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

        顺序表一般可以分为:

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

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

        顺序表的本质:

顺序表的本质就是数组,但是在数组的基础上,它还要求数据是从开始位置连续存储的,不能跳跃间隔。

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

//顺序表的静态存储——开少了不够用,开多了浪费
#include

#define N 7//定义宏常量N为顺序表中数组的大小(优点后续能做到一改全改)
typedef int SLDataType;//重定义顺序表中元素的类型(优点后续能做到一改全改)

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

        顺序表最大的缺点:顺序表的空间,在编译阶段就已经确定了,不能修改。所以就会产生空间开少了不够,开多了浪费的问题。

顺序表详解|顺序表常见错误并调试分析_第3张图片

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

//顺序表的动态存储——按需申请
#include

typedef int SLDataType;//重定义顺序表中元素的类型
typedef struct SeqList
{
	SLDataType* array;//指向动态开辟的数组
	size_t size;//有效数据的个数
	size_t capacity;//容量空间的大小
}SL;

        动态顺序表改良了静态顺序表的缺点,空间按需申请,系统根据程序需要即时分配。

顺序表详解|顺序表常见错误并调试分析_第4张图片

 2、接口实现

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

        接口函数:接口函数就是某个模块写了给其他模块用的函数,简单的说接口函数就是类中的公有函数。

        我们实现顺序表,也是尽量将其模块化(即通过接口实现),写一部分调试一部分,防止最后代码bug过多。

项目名称:SeqList

项目分为三个模块:

        (1)顺序表的测试模块(调试模块):Test.c

        (2)顺序表的实现模块(接口函数的实现):SeqList.c

        (3)顺序表的声明模块(头文件、顺序表结构体、接口函数的声明):SeqList.h

(1)声明模块:SeqList.h


//预处理,包含后续用到的头文件
#include
#include
#include
#include

//宏常量——优点能做到一改全改
#define INIT_CAPACITY 4 //初始化顺序表是,开辟空间的大小

//顺序表的动态存储——按需申请
typedef int SLDataType;//重定义顺序表中元素的类型(优点:能做到一改全改)
typedef struct SeqList
{
	SLDataType* array;//指向动态开辟的数组
	int size;//有效数据的个数
	int capacity;//容量空间的大小
}SL;

//数据的管理常用的无非就四种:增删查改
//基本增删查改接口

//顺序表初始化
void SLInit(SL* ps);

//检查空间,如果满了,进行增容
void CheckCapacity(SL* ps);

//顺序表尾插
void SLPushBack(SL* ps, SLDataType x);

//顺序表尾删
void SLPopBack(SL* ps);

//顺序表头插
void SLPushFront(SL* ps, SLDataType x);

//顺序表头删
void SLPopFront(SL* ps);

//顺序表查找
int SLFind(SL* ps, SLDataType x);

//顺序表在pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x);

//顺序表删除pos位置的值
void SLErase(SL* ps, int pos);

//顺序表销毁
void SLDestory(SL* ps);

//顺序表打印
void SLPrint(SL* ps);

        接口函数:命名风格是跟着STL走的,建议大家也是一样,方便后期学习。

0X01顺序表初始化(SLInit)

        顺序表初始化模块①实现顺序表初始化array指向开辟数组空间起始位置;②实现初始化有几个有效数据个数;③实现初始化容量空间的大小。(注意:参数的传递为传址调用——形参的改变要影响实参的改变)

        SeqList.c文件实现:

//顺序表初始化
void SLInit(SL* ps)
{
	//函数参数为指针且要求不能为NULL,则一定要断言指针的有效性(方便我们查错)
	assert(ps);
	//①给数组初始化开辟INIT_CAPACITY个SLDataType类型的空间
	ps->array = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
	//判断malloc是否开辟成功
	if (NULL == ps->array)
	{
		//打印错误信息
		perror("SLInit::malloc");
		//退出
		return;
	}
	//②有效数据个数的初始化
	ps->size = 0;
	//③容量空间大小的初始化
	ps->capacity = INIT_CAPACITY;
}

        Test.c文件调试:

//实现测试顺序表的初始化功能
void TestSeqList1()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);
}

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

        F10调试观察是否初始化成功:如下图

顺序表详解|顺序表常见错误并调试分析_第5张图片

0X02顺序表的扩容&尾插&打印&销毁

        顺序表的扩容模块:每一次添加新数据的时候我们都要调用该模块是否进行扩容,如果顺序表的有效数据个数等于容量空间大小时即要扩容,一般扩容我们扩容2倍(其他的也可以)。在扩容时我们常常遇到一些问题,今天我来为大家演示一遍。

        SeqList.c文件实现:扩容的错误代码演示

//检查空间,如果满了,进行增容
void CheckCapacity(SL* ps)
{
	//ps不能为空,所以一般先断言指针的有效性
	assert(ps);
	//判断是否扩容,即有效数据个数等于容量空间时扩容
	if (ps->size == ps->capacity)
	{
		//防止扩容失败,先用一个临时指针变量存储申请空间的起始地址
		SLDataType* tmp = (SLDataType*)realloc(ps->array, ps->capacity * 2);
		//判断是否扩容成功,扩容成功仍用ps->array指向这块空间,否则提示扩容失败并退出
		if (tmp != NULL)
		{
			ps->array = tmp;
			//扩容成功,顺序表的容量空间也要更新
			ps->capacity *= 2;
		}
		else
		{
			//打印错误信息
			perror("CheckCapacity::realloc");
			exit(-1);
		}
	}
}

        Test.c文件调试:

void TestSeqList1()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);

	//尾插7个新数据
	SLPushBack(&s, 1);
	SLPushBack(&s, 2);
	SLPushBack(&s, 3);
	SLPushBack(&s, 4);
	SLPushBack(&s, 5);
	SLPushBack(&s, 6);
	SLPushBack(&s, 7);

	//打印顺序表观察
	SLPrint(&s);

	//销毁顺序表
	SLDestory(&s);
}

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

        运行结果有一个奇怪的值:如下图

顺序表详解|顺序表常见错误并调试分析_第6张图片

        F10调试之后,发现是free出错:如下图

顺序表详解|顺序表常见错误并调试分析_第7张图片

free出错一般有两个可能:

        ①释放指针为野指针或者释放指针没有指向开辟空间的起始地址 ;

        ②对动态开辟的空间越界访问。

        根据我们调试和free可能出错的原因,我们将错误锁定在扩容,仔细查错后发现是越界访问了,因为realloc等动态开辟申请空间大小的单位是字节,所以扩容之后空间大小为8字节,不是我们认为的32个字节。

使用realloc需要注意的几点:

        1、realloc开辟空间有三种情况:①原地扩容;②另找一块空间扩容;③扩容失败。所以为了防止扩容失败之后原来的空间也不能使用,一定要先使用临时变量来指向开辟的空间。

        2、所有动态开辟的函数malloc、realloc申请空间的大小都是字节,要注意别造成越界访问。

对于该知识点还有问题的可以去复习下该知识点,链接如下:动态内存管理(1)_从前慢,现在也慢的博客-CSDN博客

        SeqList.c文件实现:扩容的正确代码演示

void CheckCapacity(SL* ps)
{
	//ps不能为空,所以一般先断言指针的有效性
	assert(ps);
	//判断是否扩容,即有效数据个数等于容量空间时扩容
	if (ps->size == ps->capacity)
	{
		//防止扩容失败,先用一个临时指针变量存储申请空间的起始地址
		SLDataType* tmp = (SLDataType*)realloc(ps->array, sizeof(SLDataType) * ps->capacity * 2);
		//判断是否扩容成功,扩容成功仍用ps->array指向这块空间,否则提示扩容失败并退出
		if (tmp != NULL)
		{
			ps->array = tmp;
			//扩容成功,顺序表的容量空间也要更新
			ps->capacity *= 2;
		}
		else
		{
			//打印错误信息
			perror("CheckCapacity::realloc");
			exit(-1);
		}
	}
}

        顺序表的尾插模块:尾插就是在最后面添加一个新数据,每一次添加数据都要调用CheckCapacity函数判断是否扩容,顺序表是从头开始连续存储的结构,所以直接ps->array[ps->size] = x即添加成功,添加成功后注意有效数据个数+1。

        图示:

顺序表详解|顺序表常见错误并调试分析_第8张图片

        SeqList.c文件实现:顺序表的尾插

void SLPushBack(SL* ps, SLDataType x)
{
	//断言指针的有效性
	assert(ps);
	//添加新数据,先调用CheckCapacity函数,判断是否扩容
	CheckCapacity(ps);
	//尾插——顺序表是从头开始连续存储size个有效数据,且数组下标从0开始
	ps->array[ps->size] = x;
	//尾插之后,有效数据个数更新
	ps->size++;
}

        顺序表的打印模块:顺序表是从开始位置连续存储size个数据,所以我们直接for循环,从第一个开始遍历到size个即可。

        SeqList.c文件实现:顺序表的打印

void SLPrint(SL* ps)
{
	//ps不能为NULL,先断言
	assert(ps);
	//for循环遍历顺序表
	size_t i = 0;
	for (i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->array[i]);
	}
	printf("\n");
}

        顺序表的销毁模块:因为顺序表我们是动态开辟的内存,当我们不再使用顺序表,如果服务器一直运行,就会造成内存泄漏,所以我们要有一个手动销毁的接口(注意销毁之后有效数据和容量空间都为0)。

         SeqList.c文件实现:顺序表的销毁

void SLDestory(SL* ps)
{
	//使用完销毁
	free(ps->array);
	//释放之后,ps->array不改变,防止非法访问置为NULL
	ps->array = NULL;
	//释放之后,有效数据个数和容量空间都为0
	ps->capacity = ps->size = 0;
}

        Test.c文件调试:

void TestSeqList1()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);

	//尾插7个新数据
	SLPushBack(&s, 1);
	SLPushBack(&s, 2);
	SLPushBack(&s, 3);
	SLPushBack(&s, 4);
	SLPushBack(&s, 5);
	SLPushBack(&s, 6);
	SLPushBack(&s, 7);

	//打印顺序表观察
	SLPrint(&s);

	//销毁顺序表
	SLDestory(&s);
}

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

        运行结果:成功

顺序表详解|顺序表常见错误并调试分析_第9张图片

0X03顺序表的尾删

        顺序表的尾删模块:因为顺序表的数据是以有效数据的个数size为标准的,所以我们直接size减1即可。

        SeqList.c文件实现:顺序表的错误尾删演示

void SLPopBack(SL* ps)
{
	//ps->array[ps->size - 1] = 0;//没有意义,因为遍历顺序表是按照size为条件的
	ps->size--;
}

          Test.c文件调试:

void TestSeqList1()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);

	//尾插4个新数据
	SLPushBack(&s, 1);
	SLPushBack(&s, 2);
	SLPushBack(&s, 3);
	SLPushBack(&s, 4);

	//打印顺序表观察
	SLPrint(&s);

	//尾删5个数据,并尾删后每次打印
	SLPopBack(&s);
	SLPrint(&s);
	
	SLPopBack(&s);
	SLPrint(&s); 
	
	SLPopBack(&s);
	SLPrint(&s);

	SLPopBack(&s);
	SLPrint(&s);

	SLPopBack(&s);
	SLPrint(&s);

	//销毁顺序表
	SLDestory(&s);
}

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

        运行结果:(奇怪)

顺序表详解|顺序表常见错误并调试分析_第10张图片

        为什么呢?我们F10调试观察:

顺序表详解|顺序表常见错误并调试分析_第11张图片

         我们发现尾删到第五个数据时,size变成了一个0xffffffff数,这是因为有效数据只有4个,减到第五个时为-1,-1的补码就是0xffffffff,又因为打印时i是无符号类型所以size也是无符号整形就变成了一个超级大的正数。

        SeqList.c文件实现:顺序表的正确尾删演示

void SLPopBack(SL* ps)
{
	//断言指针的有效性
	assert(ps);
	//①暴力检查,断言——表达式为假,直接结束程序并提示错误位置
	assert(ps->size > 0);
	//②温柔的检查
	/*if (ps->size == 0)
	{
		return;
	}*/
	//ps->array[ps->size - 1] = 0;//没有意义,因为遍历顺序表是按照size为条件的
	ps->size--;
}

        运行结果:

顺序表详解|顺序表常见错误并调试分析_第12张图片

        assert提示我们错误在哪里,我们直接改错即可。

0X04顺序表的头插

        顺序表的头插模块:顺序表的头插就是要在起始位置插入一个数据,添加一个新数据先调用CheckCapacity函数判断是否扩容。因为顺序表是从头开始连续存储的,所以要将以前的数据都向后挪动,才在起始位置添加新数据,注意添加数据之后有效数据个数+1.

        图示:

顺序表详解|顺序表常见错误并调试分析_第13张图片

         SeqList.c文件实现:顺序表的头插演示

void SLPushFront(SL* ps, SLDataType x)
{
	//断言指针的有效性
	assert(ps);
	//添加新数据判断是否扩容
	CheckCapacity(ps);
	//将以前的数据向后挪动,从最后一个开始
	int end = ps->size - 1;//数组下标从0开始
	while (end >= 0)
	{
		ps->array[end + 1] = ps->array[end];
		end--;
	}
	//挪动完之后,在起始位置添加新数据
	ps->array[0] = x;
	//添加完数据,有效数据个数+1
	ps->size++;
}

        Test文件调试:

void TestSeqList2()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);

	//头插4个新数据
	SLPushFront(&s, 1);
	SLPushFront(&s, 2);
	SLPushFront(&s, 3);
	SLPushFront(&s, 4);

	//打印顺序表观察
	SLPrint(&s);

	//销毁顺序表
	SLDestory(&s);
}

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

        运行结果:

顺序表详解|顺序表常见错误并调试分析_第14张图片

0X05顺序表的头删

        顺序表的头删模块:头删就是将起始位置的数据删除,我们while循环每一次只需将后一个数据向前挪动一位,挪动到begin < ps->size结束,最后再将有效数据个数-1即可。

        图示:

顺序表详解|顺序表常见错误并调试分析_第15张图片

          SeqList.c文件实现:顺序表的头删演示

void SLPopFront(SL* ps)
{
	//断言指针的有效性
	assert(ps);
	//断言顺序表是否为空为空,直接报错并提示
	assert(ps->size > 0);
	//while循环一次将后一个数据向前挪动一位
	int begin = 1;
	while (begin < ps->size)
	{
		ps->array[begin - 1] = ps->array[begin];
		begin++;
	}
	//删除之后,有效数据个数-1
	ps->size--;
}

        Test文件调试:

void TestSeqList2()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);

	//头插4个新数据
	SLPushFront(&s, 1);
	SLPushFront(&s, 2);
	SLPushFront(&s, 3);
	SLPushFront(&s, 4);

	//打印顺序表观察
	SLPrint(&s);

	//头删
	SLPopFront(&s);
	SLPrint(&s);

	SLPopFront(&s);
	SLPrint(&s);

	SLPopFront(&s);
	SLPrint(&s);

	SLPopFront(&s);
	SLPrint(&s);

	SLPopFront(&s);
	SLPrint(&s);
	//销毁顺序表
	SLDestory(&s);
}

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

        运行结果:

顺序表详解|顺序表常见错误并调试分析_第16张图片

assert断言是一个非常好的工具,如果表达式为假直接报错并提示错误信息的准确位置。 

0X06顺序表在指定位置插入x

        顺序表在指定位置插入x模块:顺序表是连续存储的,要在指定位置下标pos插入数据时,我们要注意插入下标pos是否合理。

        图示:

顺序表详解|顺序表常见错误并调试分析_第17张图片

         SeqList.c文件实现:顺序表在指定位置插入x

void SLInsert(SL* ps, int pos, SLDataType x)
{
	//断言ps指针的有效性
	assert(ps);
	//断言指定位置下标pos是否合理
	assert(pos >= 0 && pos <= ps->size);
	//添加新数据调用CheckCapacity函数判断是否扩容
	CheckCapacity(ps);
	//从最后一个数据开始挪到数据
	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->array[end + 1] = ps->array[end];
		end--;
	}
	//挪动完之后插入数据,并有效数据个数+1
	ps->array[pos] = x;
	ps->size++;
}

        该函数可以实现在指定位置插入数据,所以前面的头插尾插可以复用该函数实现。

        SeqList.c文件实现:复用SLInsert函数的尾插

void SLPushBack(SL* ps, SLDataType x)
{
	//断言指针的有效性
	assert(ps);
	添加新数据,先调用CheckCapacity函数,判断是否扩容
	//CheckCapacity(ps);
	尾插——顺序表是从头开始连续存储size个有效数据,且数组下标从0开始
	//ps->array[ps->size] = x;
	尾插之后,有效数据个数更新
	//ps->size++;

	//有了SLInsert函数,我们可以直接复用该函数进行尾插x
	SLInsert(ps, ps->size, x);
}

         SeqList.c文件实现:复用SLInsert函数的头插

void SLPushFront(SL* ps, SLDataType x)
{
	//断言指针的有效性
	assert(ps);
	添加新数据判断是否扩容
	//CheckCapacity(ps);
	将以前的数据向后挪动,从最后一个开始
	//int end = ps->size - 1;//数组下标从0开始
	//while (end >= 0)
	//{
	//	ps->array[end + 1] = ps->array[end];
	//	end--;
	//}
	挪动完之后,在起始位置添加新数据
	//ps->array[0] = x;
	添加完数据,有效数据个数+1
	//ps->size++;

	//有了SLInsert函数,我们可以直接复用该函数进行头插x
	SLInsert(ps, 0, x);
}

        Test文件调试:

void TestSeqList3()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);

	//头插4个新数据
	SLPushFront(&s, 1);
	SLPushFront(&s, 2);
	SLPushFront(&s, 3);
	SLPushFront(&s, 4);

	//打印顺序表观察
	SLPrint(&s);

	//在下标为2的位置插入10
	SLInsert(&s, 2, 10);
	SLPrint(&s);

	//在下标为5的位置插入20即尾插
	SLInsert(&s, 5, 20);
	SLPrint(&s);

	//销毁顺序表
	SLDestory(&s);
}

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

        运行结果:

顺序表详解|顺序表常见错误并调试分析_第18张图片

0X07删除指定位置下标pos的数据

        删除指定位置pos的数据,我们也要注意pos的位置,注意因为数组下标从0开始,所以ps->size位置没有有效数据。删除指定位置pos思想和头删一样,从后面一个数据开始向前挪动直到begin < ps->size 。

        SeqList.c文件实现:删除指定位置pos的数据

void SLErase(SL* ps, int pos)
{
	//断言指针的有效性
	assert(ps);
	//断言位置的有效性,注意因为数组下标从0开始所以ps->size位置无有效数据(间接判断了size>0)
	assert(pos >= 0 && pos < ps->size);
	//在指定下标pos位置删除值,类似头删,将pos后面的数据向前挪动
	int begin = pos + 1;
	while (begin < ps->size)
	{
		//将后一个数据向前挪动
		ps->array[begin - 1] = ps->array[begin];
		begin++;
	}
	//删除之后有效数据个数-1
	ps->size--;
}

        该函数可以实现在指定位置删除数据,所以前面的头删尾删可以复用该函数实现

        SeqList.c文件实现:复用SLErase函数的头删

void SLPopFront(SL* ps)
{
	//断言指针的有效性
	assert(ps);
	//断言顺序表是否为空为空,直接报错并提示
	//assert(ps->size > 0);
	while循环一次将后一个数据向前挪动一位
	//int begin = 1;
	//while (begin < ps->size)
	//{
	//	ps->array[begin - 1] = ps->array[begin];
	//	begin++;
	//}
	删除之后,有效数据个数-1
	//ps->size--;

	//有了SLErase函数,我们可以直接复用该函数进行头删
	SLErase(ps, 0);
}

        SeqList.c文件实现:复用SLErase函数的尾删

void SLPopBack(SL* ps)
{
	//断言指针的有效性
	assert(ps);
	//①暴力检查,断言——表达式为假,直接结束程序并提示错误位置
	//assert(ps->size > 0);
	//②温柔的检查
	/*if (ps->size == 0)
	{
		return;
	}*/
	//ps->array[ps->size - 1] = 0;//没有意义,因为遍历顺序表是按照size为条件的
	//ps->size--;
	
	//有了SLErase函数,我们可以直接复用该函数进行尾删
	SLErase(ps, ps->size - 1);
}

         Test文件调试:

void TestSeqList3()
{
	SL s;
	//注意SLInit为传址调用,形参的改变要影响实参
	SLInit(&s);

	//头插4个新数据
	SLPushFront(&s, 1);
	SLPushFront(&s, 2);
	SLPushFront(&s, 3);
	SLPushFront(&s, 4);

	//打印顺序表观察
	SLPrint(&s);

	//在下标为2的位置插入10
	SLInsert(&s, 2, 10);
	SLPrint(&s);

	//在下标为5的位置插入20即尾插
	SLInsert(&s, 5, 20);
	SLPrint(&s);

	//删除下标3的数据
	SLErase(&s, 3);
	SLPrint(&s);

	//销毁顺序表
	SLDestory(&s);
}

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

        运行结果:

顺序表详解|顺序表常见错误并调试分析_第19张图片

我们删除该数据的同时可以将该空间还给操作系统吗,即可以缩容吗?

        答案是:不可以, 缩容——释放一部分空间。操作系统不支持,因为它是整块申请,整块释放的。realloc函数一般都是扩容,不会将其缩小,因为防止被别人占用了,后期想扩容后续空间不足,发生异地扩容。

0X08顺序表查找

        顺序表查找模块:我们只需遍历顺序表,如果找到返回该值下标,没有找到返回-1.

        SeqList.c文件实现:

int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	//遍历顺序表,如果找到就返回该值的下标,没有找到就返回-1
	int i = 0;
	for (i = 0; i < ps->size; i++)
	{
		if (ps->array[i] == x)
		{
			return i;
		}
	}
	//没找到
	return -1;
}

三、总代码(菜单测试)

        我们最后可以做一个菜单来测试一下我们所有的功能,但是数据结构的本质意义是存储数据的,所以做菜单的意义并不大。(不建议,一开始就做菜单,做菜单不易调试,最好先写好接口函数调试完了,在写菜单)

        声明模块:SeqList.h

//预处理,包含后续用到的头文件
#include
#include
#include
#include

//宏常量——优点能做到一改全改
#define INIT_CAPACITY 4 //初始化顺序表是,开辟空间的大小

//顺序表的动态存储——按需申请
typedef int SLDataType;//重定义顺序表中元素的类型(优点:能做到一改全改)
typedef struct SeqList
{
	SLDataType* array;//指向动态开辟的数组
	int size;//有效数据的个数
	int capacity;//容量空间的大小
}SL;

//数据的管理常用的无非就四种:增删查改
//基本增删查改接口

//顺序表初始化
void SLInit(SL* ps);

//检查空间,如果满了,进行增容
void CheckCapacity(SL* ps);

//顺序表尾插
void SLPushBack(SL* ps, SLDataType x);

//顺序表尾删
void SLPopBack(SL* ps);

//顺序表头插
void SLPushFront(SL* ps, SLDataType x);

//顺序表头删
void SLPopFront(SL* ps);

//顺序表查找
int SLFind(SL* ps, SLDataType x);

//顺序表在pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x);

//顺序表删除pos位置的值
void SLErase(SL* ps, int pos);

//顺序表销毁
void SLDestory(SL* ps);

//顺序表打印
void SLPrint(SL* ps);

       实现模块: SeqList.c

//接口函数的实现
#include"SeqList.h"

//顺序表初始化
void SLInit(SL* ps)
{
	//函数参数为指针且要求不能为NULL,则一定要断言指针的有效性(方便我们查错)
	assert(ps);
	//①给数组初始化开辟INIT_CAPACITY个SLDataType类型的空间
	ps->array = (SLDataType*)malloc(sizeof(SLDataType) * INIT_CAPACITY);
	//判断malloc是否开辟成功
	if (NULL == ps->array)
	{
		//打印错误信息
		perror("SLInit::malloc");
		//退出
		return;
	}
	//②有效数据个数的初始化
	ps->size = 0;
	//③容量空间大小的初始化
	ps->capacity = INIT_CAPACITY;
}

//检查空间,如果满了,进行增容
void CheckCapacity(SL* ps)
{
	//ps不能为空,所以一般先断言指针的有效性
	assert(ps);
	//判断是否扩容,即有效数据个数等于容量空间时扩容
	if (ps->size == ps->capacity)
	{
		//防止扩容失败,先用一个临时指针变量存储申请空间的起始地址
		SLDataType* tmp = (SLDataType*)realloc(ps->array, sizeof(SLDataType) * ps->capacity * 2);
		//判断是否扩容成功,扩容成功仍用ps->array指向这块空间,否则提示扩容失败并退出
		if (tmp != NULL)
		{
			ps->array = tmp;
			//扩容成功,顺序表的容量空间也要更新
			ps->capacity *= 2;
		}
		else
		{
			//打印错误信息
			perror("CheckCapacity::realloc");
			exit(-1);
		}
	}
}

//顺序表尾插
void SLPushBack(SL* ps, SLDataType x)
{
	//断言指针的有效性
	assert(ps);
	添加新数据,先调用CheckCapacity函数,判断是否扩容
	//CheckCapacity(ps);
	尾插——顺序表是从头开始连续存储size个有效数据,且数组下标从0开始
	//ps->array[ps->size] = x;
	尾插之后,有效数据个数更新
	//ps->size++;

	//有了SLInsert函数,我们可以直接复用该函数进行尾插x
	SLInsert(ps, ps->size, x);
}

//顺序表尾删
void SLPopBack(SL* ps)
{
	//断言指针的有效性
	assert(ps);
	//①暴力检查,断言——表达式为假,直接结束程序并提示错误位置
	//assert(ps->size > 0);
	//②温柔的检查
	/*if (ps->size == 0)
	{
		return;
	}*/
	//ps->array[ps->size - 1] = 0;//没有意义,因为遍历顺序表是按照size为条件的
	//ps->size--;
	
	//有了SLErase函数,我们可以直接复用该函数进行尾删
	SLErase(ps, ps->size - 1);
}

//顺序表头插
void SLPushFront(SL* ps, SLDataType x)
{
	//断言指针的有效性
	assert(ps);
	添加新数据判断是否扩容
	//CheckCapacity(ps);
	将以前的数据向后挪动,从最后一个开始
	//int end = ps->size - 1;//数组下标从0开始
	//while (end >= 0)
	//{
	//	ps->array[end + 1] = ps->array[end];
	//	end--;
	//}
	挪动完之后,在起始位置添加新数据
	//ps->array[0] = x;
	添加完数据,有效数据个数+1
	//ps->size++;

	//有了SLInsert函数,我们可以直接复用该函数进行头插x
	SLInsert(ps, 0, x);
}

//顺序表头删
void SLPopFront(SL* ps)
{
	//断言指针的有效性
	assert(ps);
	//断言顺序表是否为空为空,直接报错并提示
	//assert(ps->size > 0);
	while循环一次将后一个数据向前挪动一位
	//int begin = 1;
	//while (begin < ps->size)
	//{
	//	ps->array[begin - 1] = ps->array[begin];
	//	begin++;
	//}
	删除之后,有效数据个数-1
	//ps->size--;

	//有了SLErase函数,我们可以直接复用该函数进行头删
	SLErase(ps, 0);
}

//顺序表查找
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);
	//遍历顺序表,如果找到就返回该值的下标,没有找到就返回-1
	int i = 0;
	for (i = 0; i < ps->size; i++)
	{
		if (ps->array[i] == x)
		{
			return i;
		}
	}
	//没找到
	return -1;
}

//顺序表在pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x)
{
	//断言ps指针的有效性
	assert(ps);
	//断言指定位置下标pos是否合理
	assert(pos >= 0 && pos <= ps->size);
	//添加新数据调用CheckCapacity函数判断是否扩容
	CheckCapacity(ps);
	//从最后一个数据开始挪到数据
	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->array[end + 1] = ps->array[end];
		end--;
	}
	//挪动完之后插入数据,并有效数据个数+1
	ps->array[pos] = x;
	ps->size++;
}

//顺序表删除pos位置的值
void SLErase(SL* ps, int pos)
{
	//断言指针的有效性
	assert(ps);
	//断言位置的有效性,注意因为数组下标从0开始所以ps->size位置无有效数据(间接判断了size>0)
	assert(pos >= 0 && pos < ps->size);
	//在指定下标pos位置删除值,类似头删,将pos后面的数据向前挪动
	int begin = pos + 1;
	while (begin < ps->size)
	{
		//将后一个数据向前挪动
		ps->array[begin - 1] = ps->array[begin];
		begin++;
	}
	//删除之后有效数据个数-1
	ps->size--;
}

//顺序表销毁
void SLDestory(SL* ps)
{
	//使用完销毁
	free(ps->array);
	//释放之后,ps->array不改变,防止非法访问置为NULL
	ps->array = NULL;
	//释放之后,有效数据个数和容量空间都为0
	ps->capacity = ps->size = 0;
}

//顺序表打印
void SLPrint(SL* ps)
{
	//ps不能为NULL,先断言
	assert(ps);
	//for循环遍历顺序表
	size_t i = 0;
	for (i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->array[i]);
	}
	printf("\n");
}

        测试模块: Test.c

#include"SeqList.h"
//自定义函数:实现菜单功能
void menu()
{
	printf("*************************************\n");
	printf("*      1.尾插         2.尾删        *\n");
	printf("*      3.头插         4.头删        *\n");
	printf("*      5.打印         6.查找        *\n");
	printf("*          7.指定位置插入           *\n");
	printf("*          8.指定位置删除           *\n");
	printf("*      9.销毁         0.退出        *\n");
	printf("*************************************\n");
}

//枚举顺序表的功能:增加代码的可读性和维护性
enum SQ
{
	CLOSE,//0——退出,枚举默认从0开始,一次递增1
	PUSH_BACK,
	POP_BACK,
	PUSH_FRONT,
	POP_FRONT,
	PRINT,
	SEARCH,
	INSERT,
	EARSE,
	DESTORY,
};

//测试菜单功能函数
void MenuTest()
{
	//定义一个顺序表变量
	SL s1;
	//调用初始化函数,初始化顺序表
	SLInit(&s1);
	//定义功能选择变量
	int input = 0;
	//定义插入变量存储插入数据
	SLDataType x = 0;
	//定义插入位置变量
	int pos = 0;
	//do……while——至少执行一次
	do
	{
		//调用菜单
		menu();
		printf("请选择功能:>");
		scanf("%d", &input);
		//switch多分支语句
		switch (input)
		{
		case CLOSE://退出(0)
			printf("已退出\n");
			break;
		case PUSH_BACK://尾插(1)
			printf("请输入你要尾插的数据,以-404结束!\n");
			scanf("%d", &x);
			while (x != -404)
			{
				SLPushBack(&s1, x);
				scanf("%d", &x);
			}
			break;
		case POP_BACK://尾删(2)
			SLPopBack(&s1);
			printf("删除成功!\n");
			break;
		case PUSH_FRONT://头插(3)
			printf("请输入你要头插的数据,以-404结束!\n");
			scanf("%d", &x);
			while (x != -404)
			{
				SLPushFront(&s1, x);
				scanf("%d", &x);
			}
			break;
		case POP_FRONT://头删(4)
			SLPopFront(&s1);
			printf("删除成功!\n");
			break;
		case PRINT://打印(5)
			SLPrint(&s1);
			break;
		case SEARCH://查找(6)
			printf("请输入要查找的数据:>");
			scanf("%d", &x);
			int ret = SLFind(&s1, x);
			if (ret != -1)
			{
				printf("找到了,下标为%d\n", ret);
			}
			else
			{
				printf("找不到\n");
			}
			break;
		case INSERT://指定位置插入(7)
			printf("请输入你要插入的位置:>");
			scanf("%d", &pos);
			printf("请输入你要插入的数据,以-404结束!\n");
			scanf("%d", &x);
			while (x != -404)
			{
				SLInsert(&s1, pos, x);
				scanf("%d", &x);
			}
			break;
		case EARSE://指定位置删除(8)
			printf("请输入你要删除的位置:>");
			scanf("%d", &pos);
			SLErase(&s1, pos);
			printf("删除成功!\n");
			break;
		case DESTORY://销毁(9)
			SLDestory(&s1);
			break;
		default:
			printf("选择错误,请重新选择\n");
			break;
		}
	} while (input);
}
	

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

顺序表我们就大致学完,下期进行单链表的学习。

你可能感兴趣的:(数据结构初阶,c语言,数据结构,算法)