顺序表:数据结构中的基础线性存储结构

前言

顺序表功能如图所示

一、顺序表的概念结构及其特点

1.1顺序表的基本概念

++顺序表++ 是计算机科学中++线性表++的++两种基本存储结构之一++ (另一种是链表),它通过++一组连续的存储单元++ 来++依次存储++线性表中的数据元素,使得元素的++物理存储位置++ 与++逻辑顺序++完全一致。

1.2顺序表的结构

1.逻辑结构:在逻辑结构上,顺序表是线性的,就像幼儿园里小朋友们,一个跟着一个排队,有一个小朋友做排头,有一个小朋友做排尾,在中间的小朋友每一个都知道前面的小朋友是谁,在中间的每一个小朋友知道后面的小朋友是谁,就如同用一根线,将其串联起来。

如下图所示:

2.物理结构:在物理结构上,顺序表是基于数组的,一个连续开辟的空间,每个元素的地址是相互紧挨,也如同用一根线一样串联起来,所以在物理结构上也是线性的。

1.3顺序表的特点

特点:

①顺序表具有动态分配空间或则静态开辟空间、支持随机访问和顺序访问,逻辑顺序与物理顺序一致。

②每个元素可以都有唯一的位置,可以通过索引直接访问元素。

③值得注意的是:元素可以是任意类型,包括基本数据类型、结构体等。

二、顺序表的分类

顺序表与数组的关联,顺序表的底层结构是数组,但又不完全是数组,它是对数组基于封装,使其具有了增、删、改、查及其接口的功能,使其具有更加完善的功能适配。

一般来说,我们将顺序表分为两类,一类是静态顺序表,一类是动态顺序表。

①静态顺序表:使⽤定⻓数组存储元素。

对于静态顺序表而言,因为其空间是固定的,我们改给多大空间合适呢,如果空间给小了,导致数据存储不下,引起数据丢失等问题,如果空间给大了,导致空间浪费,程序运行效率降低。

②动态顺序表:使用可更改空间的数组存储元素。

对于动态顺序表而言,因为其空间是可变的,我们可以通过灵活的改变空间,以适应空间不足,或者空间过大的问题。

三、顺序表的实现

这里我们以动态顺序表为例,实现顺序表的各个功能和使用,我们通过三个文件实现顺序表:

①SeqList.h 顺序表函数的声明 及 顺序表结构体的实现

②SeqList.c 顺序表函数的实现

③Test.c 测试顺序表函数

3.1 SeqList.h的准备

头文件需要包含的声明为:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#pragma once
//动态顺序表的实现
	
typedef int SLDataType;     //通过将 int 命名为SLDataType 以便后续更改类型 
	
//定义顺序表的结构
typedef struct SeqList
{
	SLDataType* arr;		//动态开辟顺序表
	int size;	//有效元素个数
	int capacity;		//顺序表的容量
}SL;
	
//顺序表初始化
void SLInit(SL* ps);

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

//顺序表扩容
void SLCheckCapacity(SL* ps);

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

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

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

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

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

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

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

//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x);

对于这个头文件我们主要关注 :++顺序表的结构体定义++

cpp 复制代码
typedef int SLDataType;

typedef struct SeqList
{
    SLDataType* arr;        //动态开辟顺序表
    int size;    //有效元素个数
    int capacity;        //顺序表的容量
}SL;

①首先定义了一个结构体名为:struct SeqList

②通过typedef将其重命名为 SL

③成员分析:

成员一:SLDataType* arr;

分析:1.定义了一个指针变量,后续开辟动态内存,用该指针进行维护

2.将int类型重命名为SLDataType,方便对其进行改造,只需要将int进行改变,可以将其变为char 、 struct等类型

3.如图所示用arr维护动态内存开辟的空间,可以将动态内存开辟的空间视作数组。

成员二:int size

分析:1.记录数组中存放的有效元素个数。

成员三:int capacity

分析:1.动态内存开辟的空间总大小,也可以认为数组的长度。

通过声明好头文件后,接下来,博主将用图解+代码的方式,对如上的函数进行逐个实现。

3.2 SeqList.c的实现

①顺序表初始化

cpp 复制代码
//这里不用SL ps作为参数,因为形参是实参的临时拷贝,改变形参不影响实参
void SLInit(SL* ps)
{
	assert(ps);
	ps->arr = NULL;	
	ps->size = 0;	//默认顺序表有效个数为0
	ps->capacity = 0; //默认顺序表空间为0
}

对顺序表初始化的时候,我们需要关注一个重点:初始化函数的参数。

有些帅读者就会向问,若使用void SLInit(SL ps)作为参数会发生什么呢?

答:我们注意到这里是传值传递,对于传值传递 ,形参是实参的临时拷贝,改变形参会不会影响实参呢,显然这里改变形参不会影响实参,那就相当于没有对实参进行初始化,这段代码变成了无效代码。

温馨提示:博主这里将顺序表的默认空间置为0,其实可以用malloc 函数 或者 calloc函数开辟一定大小的内存空间。

②顺序表销毁

cpp 复制代码
//顺序表的销毁
void SLDestroy(SL* ps)
{
	//由于是动态开辟的,所以要进行free
	//判断是否为空,如果不为空将其free,并置为空
	assert(ps);
	if (ps->arr)
	{
		//不为空
		free(ps->arr);

		//此时ps->arr仍然保存着arr的地址
		//但我们不能够访问这片空间,此时改指针为野指针,我们需要将其置空处理
		ps->arr = NULL;
	}
	//有效个数清空
	ps->size = 0;

	//开辟的空间清0
	ps->capacity = 0;
}

温馨提示:

①:这里因为我们使用动态内存开辟空间,所以尤其要重视对于内存的释放,如果有帅观众还不太熟悉动态内存开辟空间的话,可以看一下博主写的动态内存开辟的博客。

②:将空间存储的有效个数 和 空间的总大小置空处理。

③顺序表扩容(核心知识)

cpp 复制代码
//顺序表的扩容
void SLCheckCapacity(SL* ps)
{
	assert(ps);
	
	//什么时候应该扩容?空间不够用的时候。
	//什么时候空间不够用?当有效个数等于空间容量的时候。
	if (ps->size == ps->capacity)
	{
		//使用什么进行扩容? realloc调整动态开辟的空间
		//每次扩多大?根据数学推导,成倍数增加容量是最好的,一般为2~3倍最佳。

		//realloc(ps->arr,ps->capacity * 2 *sizeof(SLDataType) );
		//直接传入ps->capacity这样是正确的吗?
		//因为我们初始化为0,所以我们需要进行额外处理。

		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;  //若刚开始为0,则开辟4个空间
        

		SLDataType* tmp=(SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));	                                    

        //防止空间开辟失败
		if (tmp == NULL)
		{
			perror(tmp);
			exit(1);
		}
		//开辟成功,用ps->arr进行维护
		ps->arr = tmp;

        //将临时指针置为空
		ps->tmp=NULL;

		//更新当前空间
		ps->capacity = newCapacity;
	}
}

对于顺序表扩容,我们要明白以下知识点:

①:什么时候应该扩容?

答:空间不够用的时候,即当有效个数等于空间容量的时候。

②:使用什么进行扩容?

答:使用realloc进行动态内存开辟,但要单独去判断其是否开辟成功

③:每次扩容多大内存?

答:根据数据推导,一般来说,成倍数开辟空间,2~3倍是最佳。

代码解析:

①:

cpp 复制代码
if (ps->size == ps->capacity)

这里通过判断是否容量不够,如果size==capacity的话说明不在能够添加数据,要进行扩容处理。

②:

cpp 复制代码
int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; 

这里读者因为初始化的时候,开辟0个大小的空间,所以运用三目运算符进行判断,如果是0(即第一次开辟空间)就对其开辟四个大小的空间,后续成倍增加空间,保证了空间足够且不过于浪费。

③:

cpp 复制代码
		SLDataType* tmp=(SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));	                                    

        //防止空间开辟失败
		if (tmp == NULL)
		{
			perror(tmp);
			exit(1);
		}
		//开辟成功,用ps->arr进行维护
		ps->arr = tmp;

        //将临时指针置为空
	    tmp=NULL;

		//更新当前空间
		ps->capacity = newCapacity;
	}

这里使用动态内存开辟空间 ,先赋值给临时指针tmp,判断是否内存开辟成功,如果开辟失败则退出程序,反之赋值给原来的指针arr进行维护,最后别忘记更新当前空间的值。

④顺序表尾插

cpp 复制代码
//顺序表的尾插
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);

	//判断是否空间足够
	SLCheckCapacity(ps);

	ps->arr[ps->size++] = x;

}

温馨提示:

1.当进行插入数据的时候优先要判断是否空间足够大,如果空间不够要进行扩容处理。

2.对于尾部插入一个元素的示意图:

3.别忘记对size进行加1操作

⑤顺序表头插

cpp 复制代码
//顺序表的头插
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);

	//判断空间是否足够
	SLCheckCapacity(ps);

	for (int i = ps->size; i>0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];    //根据最后一个元素移动,确定判断条件    arr[1]=arr[0]
	}
	//移动结束后,将元素插入第一个位置,即下标为0处。
	ps->arr[0] = x;
	ps->size++;
}

温馨提示:

1.当进行插入数据的时候优先要判断是否空间足够大,如果空间不够要进行扩容处理。

2.对于头部插入一个元素的示意图:

3.别忘记对size进行加1操作

⑥顺序表尾删

cpp 复制代码
//顺序表的尾删
void SLPopBack(SL* ps)
{
	assert(ps);
	//判断是否可以进行删除
	assert(ps->size > 0);

	//将size个数减少一个即可,删除的元素可以被覆盖
	ps->size--;
}

温馨提示:

1.当进行删除元素的时候,要优先判断是否在顺序表中可以进行删除元素。

2.实际上对size--操作即可,size当前位置的元素可以后续被进行覆盖。

3.对于尾部删除一个元素的示意图:

⑦顺序表头删

cpp 复制代码
void SLPopFront(SL* ps)
{
	assert(ps);
	//判断是否可以进行删除
	assert(ps->size > 0);
	for (int i = 0; i<ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];  //根据最后一个元素移动,确定判断条件    arr[ps->size-2]=arr[ps->size-1]
	}
	//删除一个元素,ps->size的有效个数要减少
	ps->size--;
}

温馨提示:

1.当进行删除元素的时候,要优先判断是否在顺序表中可以进行删除元素。

2.头删的示意图如下所示:

3.别忘记对size进行减1操作。

⑧顺序表指定位置插入

cpp 复制代码
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	//判断pos是否在有效的位置(size下标可以插入一个元素)
	assert(pos >= 0 && pos <= ps->size);
	//判断空间是否足够
	SLCheckCapacity(ps);
	for (int i = ps->size; i>pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];	//根据最后一个元素移动,确定判断条件    arr[pos+1]=arr[pos];
	}
	ps->arr[pos] = x;
	//增添一个元素,有效个数要增加1
	ps->size++;
}

温馨提示:

1.判断待插入元素的位置是否合理

2.当进行插入数据的时候优先要判断是否空间足够大,如果空间不够要进行扩容处理。

3.指定位置插入数据的示意图:

4.别忘记对size进行加1操作。

⑨顺序表指定位置删除

cpp 复制代码
void SLErase(SL* ps, int pos)
{
	assert(ps);
	//判断pos是否在有效的位置 (size下标没有元素可删除)
	assert(pos >= 0 && pos < ps->size);
    assert(ps->size>0);
	for (int i = pos; i<ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];  //根据最后一个元素移动,确定判断条件  arr[size-2]=arr[size-1]
	}
	//删除一个元素,有效个数要减少一个
	ps->size--;
}

温馨提示:

1.判断删除的位置是否有效

2.判断是否可以进行删除元素

3.指定位置删除元素的示意图:

4.最后别忘记进行size减1操作。

⑩顺序表的打印

​​​

cpp 复制代码
//顺序表的打印
void SLPrint(SL* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
}

⑪顺序表的查找

cpp 复制代码
//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x)
{
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			printf("找到了!");
			return i;
		}
	}
	//未查找到
	printf("顺序表中没有该元素\n");
	return -1;
}

温馨提示:

①通过顺序查找,返回查找到的第一个元素的下标。

②如果没有查找到该元素,则返回-1

3.3 Test.c的测试

针对上面11条函数进行了测试,博主建议大家写完一条测试一条,更能有效率的保证代码正确。

cpp 复制代码
#include"SeqList.h"
void test1()
{
	SL s;
	SLInit(&s);	
	SLDestroy(&s);
	SLCheckCapacity(&s);
	for (int i = 1; i <= 5; i++)
	{
		SLPushBack(&s, i);
	}
	SLPrint(&s);
	for (int i = 1; i <= 5; i++)
	{
		SLPushFront(&s, i);
	}
	printf("\n");
	SLPrint(&s);
	for (int i = 1; i <= 5; i++)
	{
		SLPopFront(&s);
	}
	printf("\n");
	SLPrint(&s);

	SLInsert(&s, 0, 99);
	SLInsert(&s, 2, 88);
	SLInsert(&s, s.size, 77);
	printf("\n");
	SLPrint(&s);


	SLErase(&s, 0);
	SLErase(&s, 2);
	SLErase(&s, s.size - 1);
		
	printf("\n");
	SLPrint(&s);

	printf("\n");
	int ret=SLFind(&s, 1);
	printf("下标为:%d \n", ret);

	ret = SLFind(&s, 88);
	printf("下标为:%d \n", ret);

	ret = SLFind(&s, 0);
	printf("下标为:%d \n", ret);
}


int main()
{
	test1();
}

四、完整的代码

4.1SeqList.h

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#pragma once
//动态顺序表的实现
	
typedef int SLDataType;     //通过将 int 命名为SLDataType 以便后续更改类型 
	
//定义顺序表的结构
typedef struct SeqList
{
	SLDataType* arr;		//动态开辟顺序表
	int size;	//有效元素个数
	int capacity;		//顺序表的容量
}SL;
	
//顺序表初始化
void SLInit(SL* ps);

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

//顺序表扩容
void SLCheckCapacity(SL* ps);

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

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

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

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

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

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

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

//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x);

4.2SeqList.c

cpp 复制代码
#include"SeqList.h"

//顺序表的初始化
	
//这里不用SL ps作为参数,因为形参是实参的临时拷贝,改变形参不影响实参
void SLInit(SL* ps)
{
	assert(ps);
	ps->arr = NULL;	
	ps->size = 0;	//默认顺序表有效个数为0
	ps->capacity = 0; //默认顺序表空间为0
}

//顺序表的销毁
void SLDestroy(SL* ps)
{
	//由于是动态开辟的,所以要进行free
	//判断是否为空,如果不为空将其free,并置为空
	assert(ps);
	if (ps->arr)
	{
		//不为空
		free(ps->arr);

		//此时ps->arr仍然保存着arr的地址
		//但我们不能够访问这片空间,此时改指针为野指针,我们需要将其置空处理
		ps->arr = NULL;
	}
	//有效个数清空
	ps->size = 0;

	//开辟的空间清0
	ps->capacity = 0;
}

	
//顺序表的扩容
void SLCheckCapacity(SL* ps)
{
	assert(ps);
	
	//什么时候应该扩容?空间不够用的时候。
	//什么时候空间不够用?当有效个数等于空间容量的时候。
	if (ps->size == ps->capacity)
	{
		//使用什么进行扩容? realloc调整动态开辟的空间
		//每次扩多大?根据数学推导,成倍数增加容量是最好的,一般为2~3倍最佳。

		//realloc(ps->arr,ps->capacity * 2 *sizeof(SLDataType) );
		//直接传入ps->capacity这样是正确的吗?
		//因为我们初始化为0,所以我们需要进行额外处理。

		int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;  //若刚开始为0,则开辟4个空间
		SLDataType* tmp=(SLDataType*)realloc(ps->arr, newCapacity * sizeof(SLDataType));	//防止空间开辟失败
		if (tmp == NULL)
		{
			perror(tmp);
			exit(1);
		}
		//开辟成功,用ps->arr进行维护
		ps->arr = tmp;
		
		//对临时变量进行置空
		tmp=NULL;

		//更新当前空间
		ps->capacity = newCapacity;
	}
}
	
//顺序表的尾插
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);

	//判断是否空间足够
	SLCheckCapacity(ps);

	ps->arr[ps->size++] = x;

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

	//判断空间是否足够
	SLCheckCapacity(ps);

	for (int i = ps->size; i>0; i--)
	{
		ps->arr[i] = ps->arr[i - 1];    //根据最后一个元素移动,确定判断条件    arr[1]=arr[0]
	}
	//移动结束后,将元素插入第一个位置,即下标为0处。
	ps->arr[0] = x;
	ps->size++;
}
	
//顺序表的尾删
void SLPopBack(SL* ps)
{
	assert(ps);
	//判断是否可以进行删除
	assert(ps->size > 0);

	//将size个数减少一个即可,删除的元素可以被覆盖
	ps->size--;
}
		
void SLPopFront(SL* ps)
{
	assert(ps);
	//判断是否可以进行删除
	assert(ps->size > 0);
	for (int i = 0; i<ps->size-1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];  //根据最后一个元素移动,确定判断条件    arr[ps->size-2]=arr[ps->size-1]
	}
	//删除一个元素,ps->size的有效个数要减少
	ps->size--;
}
	
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	//判断pos是否在有效的位置(size下标可以插入一个元素)
	assert(pos >= 0 && pos <= ps->size);
	//判断空间是否足够
	SLCheckCapacity(ps);
	for (int i = ps->size; i>pos; i--)
	{
		ps->arr[i] = ps->arr[i - 1];	//根据最后一个元素移动,确定判断条件    arr[pos+1]=arr[pos];
	}
	ps->arr[pos] = x;
	//增添一个元素,有效个数要增加1
	ps->size++;
}
	
void SLErase(SL* ps, int pos)
{
	assert(ps);
	//判断pos是否在有效的位置 (size下标没有元素可删除)
	assert(pos >= 0 && pos < ps->size);
	assert(ps->size > 0);
	for (int i = pos; i < ps->size - 1; i++)
	{
		ps->arr[i] = ps->arr[i + 1];  //根据最后一个元素移动,确定判断条件  arr[size-2]=arr[size-1]
	}
	//删除一个元素,有效个数要减少一个
	ps->size--;
}

//顺序表的打印
void SLPrint(SL* ps)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->arr[i]);
	}
}

//顺序表的查找(按照从前往后的顺序查找,返回第一个元素为x的下标)
int SLFind(SL* ps, SLDataType x)
{
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			printf("找到了!");
			return i;
		}
	}
	//未查找到
	printf("顺序表中没有该元素\n");
	return -1;
}

既然看到这里了,不妨点赞+收藏,感谢大家,若有问题请指正。

相关推荐
默默无名的大学生4 小时前
数据结构——链表的基本操作
数据结构·算法
_OP_CHEN4 小时前
数据结构(C语言篇):(十一)二叉树概念介绍
c语言·开发语言·数据结构·二叉树·学习笔记··
Neverfadeaway4 小时前
C语言————冒泡排序(例题2)
c语言·数据结构·算法·冒泡排序·升序排列·降序排列
散1124 小时前
01数据结构-B树
数据结构·b树
亦良Cool4 小时前
001-Pandas的数据结构
数据结构·pandas
nsjqj5 小时前
数据结构中的 二叉树
数据结构
初学小白...5 小时前
红黑树-数据结构
数据结构
captain3765 小时前
qqq数据结构补充
数据结构
知彼解己6 小时前
【算法】四大基础数据结构
数据结构·算法