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

发布于:2025-09-12 ⋅ 阅读:(17) ⋅ 点赞:(0)

前言

                顺序表功能如图所示

        

        

         

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

        

1.1顺序表的基本概念        

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

        

1.2顺序表的结构

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

如下图所示:

        

        

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

        

1.3顺序表的特点

特点:

                

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

        

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

        

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

        

二、顺序表的分类

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

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

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

        

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

        

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

        

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

        

三、顺序表的实现

        

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

        

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

        

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

        

③Test.c              测试顺序表函数

        

3.1 SeqList.h的准备

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

#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);

        

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

        

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的实现

①顺序表初始化

        

//这里不用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函数开辟一定大小的内存空间。

 ②顺序表销毁

        

//顺序表的销毁
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;

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

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

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

        

①:什么时候应该扩容?

        

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

        

②:使用什么进行扩容?

        

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

               

③:每次扩容多大内存?

        

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

        

代码解析:

        

①:

        

if (ps->size == ps->capacity)

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

        

②:      

       

int newCapacity = ps->capacity == 0 ? 4 : ps->capacity * 2; 

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

        

③:

        

		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进行维护,最后别忘记更新当前空间的值。     

        

④顺序表尾插

        

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

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

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

}

温馨提示:

        

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

        

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

        

        

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

        

⑤顺序表头插

        

//顺序表的头插
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操作

        

⑥顺序表尾删

        

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

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

温馨提示:

        

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

        

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

           

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

        

        

⑦顺序表头删

        

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操作。

        

⑧顺序表指定位置插入

        

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操作。

        

⑨顺序表指定位置删除

        

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操作。

        

⑩顺序表的打印

​​​        

//顺序表的打印
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;
}

温馨提示:

        

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

        

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

        

3.3  Test.c的测试

        

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

#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

#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

#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;
}

        

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


网站公告

今日签到

点亮在社区的每一天
去签到