当前位置: 首页 > article >正文

启航数据结构算法之雅舟,悠游C++智慧之旅——线性艺术:顺序表之细腻探索

在这里插入图片描述

在这里插入图片描述

人无完人,持之以恒,方能见真我!!!
共同进步!!

文章目录

  • 一、线性表的概念
  • 二、顺序表
    • 1.概念与结构
    • 2.顺序表的分类
      • 静态顺序表
      • 动态顺序表
  • 三、顺序表的实现
    • 1.顺序表的结构
    • 2.顺序表的初始化和销毁
      • 初始化函数
      • 销毁函数
    • 3.顺序表的扩容
    • 4.顺序表的尾插和头插
      • 尾插函数
      • 头插函数
    • 5.顺序表的尾删和头删
      • 尾删函数
      • 头删函数
    • 6.顺序表的查找
    • 7.指定位置插⼊/删除数据
      • 指定位置之前插入
      • 指定位置删除数据

一、线性表的概念

线性表(linearlist)是n个具有相同特性的数据元素的有限序列,线性表在物理结构上并不⼀定是连续的,在逻辑结构上是连续的

物理结构就是在存储数据时真实的内存存储位置,物理结构上连续就是说在存储数据时,使用的是一段连续的内存空间存储数据,例如数组,它开辟出来的内存空间就是连续不断的,我们在数组的章节也做了介绍,数组就属于物理结构连续的一种结构

而逻辑结构独立于物理结构,它关注的是数据元素之间的关系,逻辑结构连续就是,它虽然在物理结构上可能不是采用连续存储数据的方式,但是我们可以通过某种方式使得这些数据连续起来,使得在访问数据时可以将物理结构上分散的数据连续起来,这就是逻辑结构连续

我们今天介绍的线性表在逻辑上是线性结构,但是它在物理结构上并不⼀定是连续的,也就是在访问数据时,我们一定可以按照顺序进行访问,但是在存储数据时,我们既可以采用连续的内存空间存储数据,也可以采用不连续的内存空间存储数据,只要我们采用某种方式使得线性表的数据可以连续的访问即可

当线性表的物理结构是连续的时候,一般使用数组来存储数据,当线性表的物理结构不是连续的时候,一般以链式的结构存储,线性表是⼀种在实际中⼴泛使⽤的数据结构,常⻅的线性表:顺序表、链表、栈、队列、字符串等等,我们今天介绍的就是线性表之一的顺序表

二、顺序表

1.概念与结构

概念:顺序表是⽤⼀段物理地址连续的存储单元依次存储数据元素的线性结构,⼀般情况下采⽤数组存储,如图:

在这里插入图片描述

那么肯定就会有人有疑问了,顺序表既然底层使用了数组,为什么不直接使用数组,而要取一个顺序表的名字,那么它们真实的区别是什么呢?其实很简单,数组只是数组而已,一种可以连续存储数据的结构

但是我们在上一篇文章讲过,数据结构不仅要能够存储数据,还要能够合理的管理数据,我们的顺序表虽然底层是数组,但是在数组的基础上提供了增删查改等多种方法让我们可以去合理的管理数据

所以严格地说,单独的数组不算一种数据结构,而拥有增删查改等等功能的数组,也就是顺序表,才是一种数据结构,那么顺序表到底长什么样子呢?它其实是一个结构体,不同的顺序表定义的结构体不同,所以我们先来学习顺序表的分类

2.顺序表的分类

顺序表又可以分为 静态顺序表 和 动态顺序表,我们接下来就来学习一下这两种不同顺序表的概念,以及我们平常使用的到底是哪种顺序表

静态顺序表

静态顺序表的静态是指顺序表底层的数组的元素大小是静态的,也就是静态顺序表使⽤的是定⻓的数组存储元素,而顺序表实际上就是使用一个结构体来进行维护,一般静态顺序表的结构如下:

typedef int SLDateType;

#define N 7

struct SeqList
{
	SLDateType arr[N];
	int size;
};

在上面我们提到了,顺序表其实是由一个结构体进行维护的,不同种类的顺序表的结构体不同,在静态顺序表中,底层数组的元素大小是确定了的,一般使用#define来定义一个常量来充当它的大小

而由于我们并不知道顺序表中会存储什么数据类型,所以我们可以使用typedef关键字对类型进行重命名,以后我们想要修改顺序表的数据类型时,就可以只修改typedef这一条语句,否则的话每次修改数据类型将会十分麻烦

我们也可以看到,这里结构体的名字就是SeqList,它里面包含了两个单词,
Seq代表英文单词 sequential,意思是顺序的,List就是表的意思,合起来就是我们的顺序表

在静态顺序表中的结构有两个成员,应该就是底层存储数据的数组,它的大小是确定的,第二个成员是整型变量size,它的含义是目前顺序表中有效元素的个数

静态顺序表的缺点也很明显,就是它的大小是固定的,给的空间大了就会浪费空间,小了又会不够用

动态顺序表

在上面静态顺序表的静态是指它的大小是固定的,同理,动态顺序表里面的动态就是指顺序表的大小是不固定的,也就是顺序表底层的数组的大小是不固定的,可以动态的变化,比如开始时给出4个元素的大小,不够时顺序表可以实现自动增容

//动态顺序表
typedef int SLDateType;

struct SeqList
{ 
	SLDateType* arr;
	int size;
	int capacity;
};

在动态顺序表中,数组通过动态内存开辟来申请空间,如果不够用就再次申请,所以第一个成员是指向这个数组开头的指针,第二个成员就是当前顺序表中的有效元素的个数,而第三个成员则是当前顺序表的总容量

如果总容量和有效元素个数相等,说明开辟的数组的空间满了,需要增容,所以这两个成员也是必不可少的

在动态顺序表中,我们的空间浪费可以显著减少,至少不会担心给出的空间大小不够,因为它可以自动增容,跟静态顺序表比起来要灵活得多,所以我们一般情况下都会使用动态顺序表,平常说的顺序表如果没有明确说明是静态的,那么都是动态的

所以我们下面顺序表的实现也是使用的动态顺序表

三、顺序表的实现

1.顺序表的结构

在上面我们也讲过,一般提起的顺序表默认都是动态顺序表,所以我们这里顺序表的结构就是动态顺序表的结构,这个顺序表是一个结构体,它有三个成员,分别是底层存放数据的数组,然后就是当前有效元素个数,以及整个顺序表当前的容量,如下:

typedef int SLDateType;

typedef struct SeqList
{ 
	SLDateType* arr;
	int size;
	int capacity;
}SL;

其中,数组是一个指针,因为我们的空间需要动态开辟,这个指针就指向了我们开辟的空间,可以把它当作正常使用,在动态内存管理部分我们也说过可以使用malloc来模拟实现数组的使用,链接:在21世纪的我用C语言探寻世界本质——动态内存管理及相关笔试题

在这里我们对数组的数据类型作了优化,以后需要用到哪种数据我们就可以只修改typedef后面的int,方便我们使用,其次就是我们给顺序表也取了一个别名,叫SL,这样就可以少写一部分,但意思和原先还是一样的

2.顺序表的初始化和销毁

初始化函数

我们创建一个顺序表之后需要对它进行初始化才能使用,初始化的方法也很简单,就是将我们顺序表的第一个成员置为NULL,然后把有效元素个数和总容量都置为0

由于我们初始化顺序表时要修改里面的成员,所以我们在传参时需要传地址,那么我们的顺序表初始化函数的参数类型就是SL*,如下:

void SLInit(SL* ps)
{
	assert(ps);
	ps->arr = NULL;
	ps->capacity = ps->size = 0;
}

销毁函数

接着就是顺序表的销毁了,由于我们的顺序表的空间是动态申请的,所以我们使用完顺序表之后需要对申请的空间进行释放,以免造成内存泄漏,释放完之后将我们的指针置为NULL,有效元素个数和总容量重新置为0,这就是我们顺序表的销毁

然后顺序表的销毁还是需要修改里面的数据,所以还是传地址,如下:

void SLDestroy(SL* ps)
{
	assert(ps);
//如果数组不为空,就释放动态申请的空间
	if (ps->arr)
		free(ps->arr);
	ps->arr = NULL;
	ps->size = ps->capacity = 0;
}

3.顺序表的扩容

我们实现的顺序表是动态顺序表,底层的数组的大小是动态变化的,所以我们需要写一个函数来检查数组是否已经满了,如果满了就需要进行扩容,经过研究,确定了每次空间扩大2倍较好,可以尽量的减少浪费

需要扩容的条件就是,如果我们顺序表中的有效元素个数size(就是size不为0)和总容量capacity相等,那么顺序表就满了,需要进行二倍扩容,将空间扩大二倍

但是我们还要注意一个点,顺序表初始情况下容量为0,我们需要特殊处理一下,如果检测到它的容量为0,那么就直接给它申请4个元素的空间,如果不为0,那么就对原本的空间进行二倍扩容

由于我们扩容可能修改顺序表的大小以及指针指向,所以这个扩容函数还是需要传地址,如下:

//检查函数
void SLCheckCapacity(SL* ps)
{
	assert(ps);
	if (ps->capacity == ps->size)
	{
		//如果还是初始化的状态就默认4个空间
		ps->capacity = ps->size = 0 ? 4 : ps->capacity * 2;
		SLDataType* tmp = (SLDataType*)realloc(ps->arr, sizeof(SLDataType)*ps->capacity * 2);
		if (NULL == tmp)
		{
			perror("realloc fail");
			return;
		}
		//realloc成功开辟空间后再赋值给ps->arr
		ps->arr = tmp;
	}
}

4.顺序表的尾插和头插

我们初始化好了顺序表之后,就要使用顺序表来存放数据了,我们首先介绍头插和尾插,头插就是在顺序表开头插入数据,而尾插则是在顺序表尾部插入数据,要注意的是,在插入之前我们都要判断一下容量够不够,不够的话要进行增容,接下来就让我们来学习尾插和头插

尾插函数

由于尾插比较简单,所以我们首先介绍尾插函数,尾插就是向顺序表的最后面插入数据,也就是向数组的最后一个位置插入数据,那么我们怎么找到数组中最后一个数据的位置呢?

这就要用到我们前面定义的size了,还记得顺序表中的size成员吗,它就是存放的有效元素的个数,我们可以通过size来进行尾插,如图:

在这里插入图片描述

通过这个图片我们可以发现,有效元素个数size是4,刚好size这个下标就是我们数组最后一个有效元素的下一个元素的下标,所以我们尾插的时候只需要在size这个下标处插入数据,同时记得让我们的size++,表示我们有效数据个数+1了

最后还有个要注意的点,就是我们插入数据前要看看容量够不够,这时候我们就要使用我们上面写的检查函数了,如果已经满了,就可以增容后插入数据

由于我们可能对顺序表进行修改,所以还是要传地址,如下:

//尾插函数
void SLPushBack(SL* ps, SLDataType x)
{
	assert(ps);
	SLCheckCapacity(ps);//检查函数,尾插前先检查函数的size是否是有效个数
	ps->arr[ps->size++] = x;//放入数据后size++
}

头插函数

头插函数相对于尾插函数要稍微复杂一点,我们可以思考一下,如何将一个数据插入到一个数组的最开始,是不是要把数组中的所有元素整体向后移动一位,然后再把我们的数据放进下标为0的位置上

那么怎么将数组中的所有元素整体向后移动一位呢,这里就可以用到我们在内存函数部分学过的函数:memmove

其中memmove的第一个参数是我们要移动的目标位置的地址,很明显就是顺序表中第二个元素的地址,即 ps->arr+1,第二个参数就是我们源位置的地址,是顺序表中第一个元素的地址,即ps->arr,它的第三个参数就是要拷贝的字节数,就是有效元素个数乘以一个元素的大小

头插还是要插入数据,所以还是要使用我们的检查函数,最后由于我们还是需要修改顺序表,所以还是传地址,如下:

//头插函数
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);
	//检查函数 
	SLCheckCapacity(ps);
	memmove(ps->arr + 1, ps->arr, sizeof(SLDataType) * ps->size);//向后挪动
	ps->arr[0] = x;//放入数据
	ps->size++;//放入数据后size++ 

5.顺序表的尾删和头删

有了插入函数那么就有删除函数,这里我们还是先讲头删和尾删,头删就是删除顺序表第一个元素,尾删就是删除顺序表最后一个元素,我们要注意的是,删除之前我们需要看看顺序表是否有数据,没有数据自然不能删除数据

尾删函数

因为尾删要简单一些,所以先说尾删,那么我们怎么实现尾删呢?其实很简单,我们只需要让size- -,其它什么都不需要做

因为我们尾插的时候是直接把要插入的数据直接放进size下标的元素中,如果size本身有数据也不影响我们覆盖它,我们画几张图来分析一下,首先是我们的尾删,也就是将size直接-1

在这里插入图片描述
在这里插入图片描述

可以看到我们的尾删就是让size-1,现在我们来尾插一个6,看看有没有影响:

在这里插入图片描述

可以看到,尾插操作会将5这个位置的数据进行覆盖,并不会影响尾插,头插也是这样,会覆盖size位置的数据,这里就不再演示了,可以自行推导一下

最后一点要注意的是我们最好在尾删之前要进行一下断言,如果是空的,那我们就不用删了,确保顺序表中的size,也就是有效数据个数>0,如下:

//尾删
void SLPopBack(SL* ps)
{
	assert(ps);
	assert(ps->size > 0); //确保顺序表中的size,也就是有效数据个数 > 0
	ps->size--;
}

头删函数

头删函数也很简单,我们的目的就是删除下标为0处的数据,我们就可以把从下标为1到下标为size-1的元素全部向前移动一位

在这里插入图片描述

可以看到最后我们实现了头删,虽然size位置上还有一个数据5留在那里,但是由于那里的下标是size,所以并不会影响插入,理由跟上面的尾删一致

想要下标为1到下标为size-1的元素全部向前移动一位,我们可以一步解决,就是使用库函数memmove,它的第一个参数就是目标地址,也就是ps->arr,第二个参数是源地址,也就是ps->arr+1,最后就是移动多少个字节,就是(- -ps->size)乘以每个元素的大小

//头删
void SLPopFront(SL* ps)
{
	assert(ps);
	assert(ps->size > 0);//删除要判断元素个数

	//用首元素后面的数据覆盖前面的数据达到头删的效果
	memmove(ps->arr, ps->arr + 1, --ps->size * sizeof(SLDataType));
}

6.顺序表的查找

顺序表的查找就是根据给出的值,去看它存不存在我们的顺序表中,如果存在就返回那个元素的下标,如果不存在就返回一个错误的下标,这里我们就返回-1,这个方法的实现只需要遍历数组就可以了,非常简单,如下:

int SLFind(SL* ps,SLDateType x)
{
	assert(ps);
	for (int i = 0; i < ps->size; i++)
	{
		if (ps->arr[i] == x)
		{
			return i;
		}
	}
	return -1;
}

7.指定位置插⼊/删除数据

指定位置的插入和删除比较难,我讲的还是使用memmove的方法,代码量比较少,但是可能比较难理解,如果有不懂的地方欢迎提出来

指定位置之前插入

由于是指定位置之前插入,首先我们要有这个指定位置,在这里一般就是指那个位置的下标,它作为函数的参数传过来,所以这个函数有三个参数,一个是顺序表的地址,一个是指定位置的下标,最后一个是我们要插入的数据,如下:

void SLInsert(SL* ps, int pos, SLDateType x)

在这里插入图片描述

现在我们要在3之前插入一个6,也就是在下标为pos的数据之前插入6,我们就可以这样,让pos及以后的数据往后挪动移位,然后把6放入pos中,如下图:

在这里插入图片描述

这样就实现了在3之前插入数据了,首先我们来实现第一步,就是将pos及以后的数据往后挪动一位,我们还是使用memmove,第一个参数就是我们移动的目的地的地址,也就是ps->arr+pos+1,第二个参数是我们的源地址,就是ps->arr+pos

最难的就是最后一个参数,就是我们要移动多少字节,要移动几个元素,这里我们可以使用实例来帮我们解决

在上面的例子里面,要移动的元素有三个,就是5-2,不正是ps->size - pos吗,所以很多情况下我们都可以通过一个例子来推出整个式子,如果下次写这个函数时忘记了,就可以这样画图来推,最后整个移动阶段的代码如下:

memmove(ps->arr + pos + 1, ps->arr + pos, sizeof(SLDateType) * (ps->size - pos));

最后就是,我们在函数开始前,也要判断一下pos的值是否有效,也就是是否是0到size的数,进行一下断言,完整代码如下:

//指定位置插入
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);
	assert(pos >= 0 && pos <= ps->size);//判断pos是否是0 —— size的值
	//检查
	SLCheckCapacity(ps);
	//pos+1位置 //向pos位置后挪动
	memmove(ps->arr + pos + 1, ps->arr + pos, (ps->size - pos) * sizeof(SLDataType));
	ps->arr[pos] = x;
	ps->size++;
}

一般这个SLInsert函数会和上面讲过的查找函数一起使用,用查找方法找某个数据,然后将返回值作为SLInsert函数的参数,就可以实现在某个数据前进行插入数据

指定位置删除数据

这个指定位置删除数据和上面的指定位置之前插入数据很像,思路是差不多的,如果理解了上面的代码学习这个函数就轻松多了

首先我们要在指定位置删除数据,还是需要一个该位置的下标pos,这个就作为参数传过来,如下:

void SLErase(SL* ps,int pos)

我们删除指定位置的数据的思路就是,直接将pos后面的数据往前挪动一位,直接覆盖掉pos位置的数据,然后让size-1,这样就实现了删除指定位置的数据,我们可以画图来理解一下:

在这里插入图片描述

首先就是将pos之后的数据往前移动一位,还是使用memmove函数,第一个参数就是目标地址,这里的目标地址就是ps->arr+pos,然后第二个参数就是源地址,就是 ps->arr+pos+1

然后第三个参数就是要移动多少个字节,主要还是要求移动多少个元素,我们还是使用之前的方法,通过代入一个例子来把它写出来,在上面的例子中,我们要移动2个元素,不正好就是ps->size-pos-1吗?所以我们最后就可以写出这关键的一步:

memmove(ps->arr + pos, ps->arr + pos + 1, sizeof(SLDateType) * (ps->size - pos - 1));

下一步就是让size-1了,最后就是,我们在函数开始前,也要判断一下pos的值是否有效,也就是是否是0到size的数,进行一下断言,完整代码如下:

//指定位置删除
void SLErase(SL* ps, int pos)
{
	assert(ps);
	assert(ps->size > 0);//删除要判断元素个数
	assert(pos >= 0 && pos <= ps->size);//判断pos是否是0 —— size的值
	//因为size是元素个数,所以要多减1,不懂就画图
	memmove(ps->arr + pos, ps->arr + pos + 1, sizeof(SLDataType) * (ps->size - pos - 1));
	ps->size--;
}

这个SLErase函数一般也是配合上面的查找方法使用,先查找到某个数据的下标,然后再把返回值作为SLErase函数的参数,实现删除某个指定的元素的效果

到这里我们的顺序表就结束了,其中还有诸多可以优化的地方,欢迎大家自行探索哦~~


http://www.kler.cn/a/467053.html

相关文章:

  • 数据分析思维(七):分析方法——群组分析方法
  • GPT分区 使用parted标准分区划分,以及相邻分区扩容
  • 如何配置【Docker镜像】加速器+【Docker镜像】的使用
  • 如何利用人工智能算法优化知识分类和标签?
  • 125个Docker的常用命令
  • OpenCV计算机视觉 05 图像边缘检测(Sobel算子、Scharr算子、Laplacian算子、Canny边缘检测)
  • 仿生的群体智能算法总结之三(十种)
  • 【数据结构-单调队列】力扣1438. 绝对差不超过限制的最长连续子数组
  • 链表算法练习
  • Arduino Uno简介与使用方法
  • 如何逐步操作vCenter修改DNS服务器?
  • React 中的受控组件与非受控组件:深度剖析与实战应用
  • 微服务拆分的艺术:构建高效、灵活的系统架构
  • 清华发布Hyper-YOLO:超图计算+目标检测!捕捉高阶视觉关联
  • spring默认线程池SimpleAsyncTaskExecutor特点为什么要尽量避免使用
  • Java四大常用JSON解析性能对比:Hutool、Fastjson2、Gson与Jackson测试
  • Nginx:日志管理
  • 零基础WPF使用NLog记录日志
  • CPU与GPU的区别
  • C/C++中 <<与<<=的介绍和区别
  • Ungoogled Chromium127 编译指南 MacOS 篇(一)- 项目介绍
  • 【Leetcode 热题 100】74. 搜索二维矩阵
  • 【2025最新计算机毕业设计】基于Spring Boot+Vue影院购票系统(高质量源码,提供文档,免费部署到本地)
  • Python 开发框架搭建简单博客系统:代码实践与应用
  • Edge安装问题,安装后出现:Could not find Edge installation
  • 30分钟学会css