手撕数据结构 —— 顺序表(C语言讲解)

news2024/11/24 11:51:16

目录

1.顺序表简介

什么是顺序表

顺序表的分类

2.顺序表的实现

SeqList.h中接口总览

具体实现

顺序表的定义

顺序表的初始化

顺序表的销毁

打印顺序表

​编辑 

检查顺序表的容量

尾插

尾删

​编辑 

头插

头删 

查找

在pos位置插入元素

删除pos位置的值

​编辑 

修改

3.完整代码附录

SeqList.h

SeqList.c


1.顺序表简介

什么是顺序表

顺序表是一种用物理地址连续的存储单元 依次存储数据元素的线性结构。

等等,物理地址连续的存储单元…… 这不就是我们在C语言中学习过的数组吗?是的,我们可以这样理解,顺序表的底层物理结构就是数组。需要注意的是,顺序表的底层物理结构是数组,并不是说顺序表就是数组。顺序表要求依次存储,也就是存储数据元素的时候,从第一个位置紧挨着存储,对顺序表进行增、删、改、查操作之后的顺序表也必须满足这一特性。

顺序表的分类

顺序表一般可以分为静态顺序表动态顺序表。静态顺序表使用定长数组存储元素,动态顺序表使用动态开辟的数组存储元素。

静态顺序表示意图:

动态顺序表示意图:

 

静态顺序表只适用于存储空间大小明确的场景。如果静态顺序表的大小N定大了,就会造成空间的浪费,如果N定小了就会造成空间不够用。在实际应用中,我们很少能够确切的知道需要处理的数据元素的大小,所以,静态顺序表在实际应用中并不实用,现实中基本都是使用动态顺序表,根据需要动态的分配空间。

2.顺序表的实现

基于上述原因,我们实现动态顺序表。我们需要创建两个文件,分别是SeqList.h和SeqList.c,SeqList.h中用来存放接口的声明,SeqList.c中存放接口的定义。(文章末尾附完整代码

SeqList.h中接口总览

实现顺序表,主要实现顺序表的增删改查,需要实现以下接口。

具体实现

顺序表的定义

我们定义的顺序表的底层物理空间为动态开辟的,用变量a指向这块空间,用变量size记录存储的有效数据的个数,用变量capacity表示容量空间的大小,当size == capacity的时候就需要进行扩容操作了。

顺序表的初始化

对于初始化操作,我们主要初始化struct SeqList结构体变量中的成员即可。

  • 对于最开始的容量可以根据实际需求动态设置即可;
  • size初始化为0,因为还没有向顺序表中添加元素。
  • capacity的初始值和动态申请的空间大小一致。

注意:

  • 我们初始化顺序表的时候,指向顺序表的指针不能为空,我们使用assert()函数暴力的检查,如果ps指针为空,程序就会崩溃。使用assert()函数的时候,需要包含头文件assert.h。
  • 我们还需要注意动态内存分配失败的情况,如果初始化的时候,动态分配内存失败,那么后面的程序都没有执行的必要了,我们使用exit()函数终止整个程序。使用exit()函数的时候,需要包含头文件stdlib.h。

后面的所有操作都需要判断指向顺序表的指针是否为空的情况(该指针不能为空!!!)

顺序表的销毁

销毁顺序表时,主要销毁的是struct SeqList结构体变量中的成员

  • 对于变量a,需要将变量a指向的空间释放,也就是把使用权归还给操作系统;并将a置为空,避免出现野指针。
  • 变量size和变量capacity置为0即可。

打印顺序表

直接循环打印即可。

 

检查顺序表的容量

当size == capacity的时候,说明顺序表的所有空间都用来存储有效元素了,当再次往顺序表中插入元素的时候,就没有空间了,需要扩容,我们选择2倍扩容。

  • 扩容的时候,我们选择realloc函数。该函数会自动的帮我们申请一块空间,同时将原数组中的内容拷贝至新数组中,并且释放旧的数组空间,返回新空间的起始地址。

几个注意点:

  • 检查realloc函数执行是否失败,如果失败,终止整个程序。
  • 返回的新空间的起始地址需要赋值给变量a,因为我们始终认为struct SeqList结构体变量中的成员a才是指向数组空间的。
  • 最后不要忘记放大变量capacity的值。

尾插

当进行插入操作的时候,我们需要判断顺序表的容量有没有满,如果满了就扩容,这一步可以复用我们前面实现的SLCheckCapacity()函数。

  • size表示有效元素的个数,作为下标的话就是有效元素的下一个位置。
  • 不要忘记将size++。

进行任何插入操作时,我们都需要先检查容量是否足够。通过复用SLCheckCapacity()函数即可。

可以看出顺序表尾插的时间复杂度是O(1)。

尾删

进行尾部删除元素的时候,我们可以直接让size--即可,因为size表示存储的有效元素的个数,当size--之后,最后一个元素就不是有效元素了,可以被覆盖。当然,你也可以将最后一个有效元素修改为指定值之后再进行size--操作,但这并没有什么意义。 

进行删除操作的时候,需要确保数组中存有有效元素。我们同样可以使用assert()函数进行暴力的检查。

 

可以看出顺序表尾删的时间复杂度也是O(1)。 

我们可以得出结论:顺序表在尾部进行插入和删除的效率非常高,时间复杂度都是O(1),因此,顺序表适合进行尾插尾删操作。

注意:后面所有的删除操作都需要确保数组中存有有效元素。

头插

进行头部插入时,也就是在数组的最开始位置插入元素,我们需要将所有的有效元素向后移动一个位置,然后再插入元素。

 

可以看出顺序表头插的时间复杂度是O(N),如果频繁大量的进行头插操作,效率将非常低下。 

头删 

进行头删操作时,只需要将除了第一个有效元素后面的元素都往前移动一个位置,然后进行size--操作即可。

可以看出顺序表头删的时间复杂度也是O(N),如果频繁大量的进行头删操作,效率将非常低下。

我们可以得出结论:顺序表在头部进行插入和删除的时间复杂度都是O(N),因此,顺序表不适合进行大量的头插、头删操作。

查找

在顺序表中查找元素的时候,只需要遍历顺序表即可,找到了就返回下标,没找到返回-1。-1不是合法的下标,当返回-1的时候,就表明查找的元素不存在。

在pos位置插入元素

在pos位置插入元素,只需要将pos位置以及pos位置之后的元素都向后移动一个位置,然后将pos位置的值覆盖即可。插入元素之后不要忘记将size++。

  • pos的取值必须合法,在插入数据的时候,pos可以是最后一个有效元素的下一个位置。

删除pos位置的值

删除pos位置的值只需要将pos之后的有效元素都向前挪动一个位置,然后将size--即可。

  • 删除pos位置的值的时候也需要注意pos的取值,此时,pos的取值不能是有效元素的下一个位置。

 

修改

进行修改操作时,我们只需要将指定位置的值修改即可。

值得一提的是,直接使用赋值语句就能修改,为什么还需要封装成函数呢?

  • 因为我们封装的函数有严格的边界检查。
  • 直接赋值使用的 [ ] 并没有严格的边界检查,[ ] 的下标检查是一种抽查机制,不能保证准确的发现越界问题。 

3.完整代码附录

SeqList.h

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

// 定义动态顺序表
typedef int SLDataType;

typedef struct SeqList
{
	SLDataType* a;   // 指向动态开辟的数组空间
	int size;        // 存储有效数据个数
	int capacity;    // 空间大小
}SL;

// 初始化
void SLInit(SL* ps);
// 销毁
void SLDestroy(SL* ps);
// 打印
void SLPrint(SL* ps);
// 容量检查
void SLCheckCapacity(SL* ps);

// 头插
void SLPushBack(SL* ps, SLDataType x);
// 头删
void SLPopBack(SL* ps);
// 尾插
void SLPushFront(SL* ps, SLDataType x);
// 尾删
void SLPopFront(SL* ps);

// 返回下标,没有找打返回-1
int SLFind(SL* ps, SLDataType x);

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

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

// 修改pos位置的元素
void SLModify(SL* ps, int pos, SLDataType x);

SeqList.c

#include"SeqList.h"

// 初始化 
void SLInit(SL* ps)
{
	assert(ps);

	ps->a = (SLDataType*)malloc(sizeof(SLDataType)*4);
	if (ps->a == NULL)
	{
		perror("malloc failed");
		exit(-1);
	}

	ps->size = 0;
	ps->capacity = 4;
}

// 销毁 
void SLDestroy(SL* ps)
{
	assert(ps);

	free(ps->a);
	ps->a = NULL;
	ps->capacity = ps->size = 0;
}

// 打印 
void SLPrint(SL* ps)
{
	assert(ps);

	for (int i = 0; i < ps->size; i++)
	{
		printf("%d ", ps->a[i]);
	}
	printf("\n");
}

// 容量检查 
void SLCheckCapacity(SL* ps)
{
	assert(ps);

	if (ps->size == ps->capacity)
	{
		SLDataType* tmp = (SLDataType*)realloc(ps->a, ps->capacity * 2 * (sizeof(SLDataType)));
		if (tmp == NULL)
		{
			perror("realloc failed");
			exit(-1);
		}

		ps->a = tmp;
		ps->capacity *= 2;
	}
}

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

	SLCheckCapacity(ps);

	ps->a[ps->size] = x;
	ps->size++;
}

// 尾删 
void SLPopBack(SL* ps)
{
	assert(ps);

	assert(ps->size > 0);

	ps->size--;
}

// 头插 
void SLPushFront(SL* ps, SLDataType x)
{
	assert(ps);

	SLCheckCapacity(ps);

	// 挪动数据
	int end = ps->size - 1;
	while (end >= 0)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[0] = x;
	ps->size++;
}

// 头删 
void SLPopFront(SL* ps)
{
	assert(ps);

	assert(ps->size > 0);

	int begin = 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;
	}

	ps->size--;
}

// 查找 
int SLFind(SL* ps, SLDataType x)
{
	assert(ps);

	for (int i = 0; i < ps->size; i++)
	{
		if (ps->a[i] == x)
		{
			return i;
		}
	}

	return -1;
}

// 在pos位置插入x
void SLInsert(SL* ps, int pos, SLDataType x)
{
	assert(ps);

	assert(pos >= 0 && pos <= ps->size);
	SLCheckCapacity(ps);

	int end = ps->size - 1;
	while (end >= pos)
	{
		ps->a[end + 1] = ps->a[end];
		--end;
	}
	ps->a[pos] = x;
	ps->size++;
}

// 删除pos位置的值
void SLErase(SL* ps, int pos)
{
	assert(ps);

	assert(pos >= 0 && pos < ps->size);

	int begin = pos + 1;
	while (begin < ps->size)
	{
		ps->a[begin - 1] = ps->a[begin];
		++begin;
	}

	ps->size--;
}

// 修改 
void SLModify(SL* ps, int pos, SLDataType x)
{
	assert(ps);

	assert(pos >= 0 && pos < ps->size);

	ps->a[pos] = x;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2197708.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

内核驱动-如何编译内核以及给内核中添加新文件

1.编译内核 想要编译内核&#xff0c;首先需要先下载内核源代码。可以在官方网站下载源代码压缩包&#xff0c;然后放在Ubuntu的目录下&#xff0c;然后解压&#xff08;解压的指令为&#xff1a;sudo tar -xvf xxxx.gz&#xff09;。解压之后在当前目录下可以看到解压之后的文…

java8 双冒号(::)使用方法

双冒号&#xff08;::&#xff09;运算符是跟函数式接口相关的运算符&#xff0c;作为函数式接口的赋值操作。 双冒号用于静态方法 使用方法&#xff1a;将类的静态方法赋值给一个函数式接口&#xff0c;静态方法的参数个数、类型要跟函数式的接口一致。调用这个函数式接口就…

数字化转型:别让技术迷了眼,战略觉醒才是关键。新媒体营销大客户销售AIGC大模型创新思维专家培训讲师谈数字化转型商业模式短视频内容社私域数字经济人工智能

​数字化转型从根本上讲不是关于技术,而是关于战略。 数字化转型使用新的数字技术来实现重大的业务改进,如增强客户体验、精简运营或创建新的商业模式。数字化转型描述了一家公司试图为数字时代做好准备的旅程。 数字化转型不是关于技术或获取新的技术技能。事实上,它是关于获得…

永磁同步电机环路反步法(backstepping)控制

文章目录 1、反步控制原理1.1 李雅普诺夫稳定性定理1.2 严格反馈系统1.3 一般设计流程 2、永磁同步电机反步控制2.1 反步控制器设计2.2 反步控制仿真 参考 写在前面&#xff1a;本人能力、时间、技术有限&#xff0c;没有对一些细节进行深入研究和分析&#xff0c;也难免有不足…

简易CPU设计入门:取指令(四)

项目代码下载 还是请大家首先准备好本项目所用的源代码。如果已经下载了&#xff0c;那就不用重复下载了。如果还没有下载&#xff0c;那么&#xff0c;请大家点击下方链接&#xff0c;来了解下载本项目的CPU源代码的方法。 下载本项目代码 准备好了项目源代码以后&#xff…

SOMEIP_ETS_174: SD_Unknown_Option_type

测试目的&#xff1a; 验证DUT能够拒绝一个引用了未知选项类型的SubscribeEventgroup消息&#xff0c;并以SubscribeEventgroupNAck作为响应。 描述 本测试用例旨在确保DUT遵循SOME/IP协议&#xff0c;当接收到一个引用了未知选项类型的SubscribeEventgroup消息时&#xff0…

Solidedge二次开发(C#)-将dft文件转换为dwg格式文件

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 1、前言2、在Solid Edge中创建一个par文件3、通过二次开发将dft转换为dwg4、结果显示1、前言 Solid Edge提供了将dft转换为dwg的接口,也即是保存功能。有时在不显示Solid Edge界面的情况下,将其…

【C语言刷力扣】1436.旅行终点站

题目&#xff1a; 解题思路&#xff1a; 两层循环查找&#xff0c;第一次循环中初始化 destination 为 path中每次旅行的终点作为最终的终点。二次循环查找当前 destination &#xff0c;若是作为某次旅行的起点&#xff0c;说明不是最后的终点。 char* destCity(char ***paths…

[Linux#62][TCP] 首位长度:封装与分用 | 序号:可靠性原理 | 滑动窗口:流量控制

目录 一. 认识TCP协议的报头 1.TCP头部格式 2. TCP协议的特点 二. TCP如何封装与分用 TCP 报文封装与解包 如何封装解包&#xff0c;如何分用 分离有效载荷 隐含问题&#xff1a;TCP 与 UDP 报头的区别 封装和解包的逆向过程 如何分用 TCP 报文 如何通过端口号找到绑…

多功能快捷回复软件

各位亲爱的客服宝宝们&#xff0c;每天面对大量的客户咨询&#xff0c;您是否还在手动一个一个地打字回复呢&#xff1f;别担心&#xff0c;我们为您带来了一款多功能快捷回复软件——客服宝。有了它&#xff0c;您的工作将变得无比轻松&#xff01; 客服宝是一款集成了内容存储…

网络编程(14)——基于单例模板实现的逻辑层

十四、day14 今天学习如何通过单例模板实现逻辑层 1. 利用C11特性封装单例模板 和上一节设计的单例模板有些不同&#xff0c;本节设计的单例模板利用了以下四个C11新特性&#xff0c;优化了代码 unique_lock和lock_guard once_flag和call_once std::function condition_v…

1打家劫舍三部曲

刷题刷题找工作&#xff01; s198.打家劫舍 动态规划&#xff1a;开始打家劫舍&#xff01; dp数组表示到第i家的最高金额 dp递归公式&#xff0c;要么抢劫这家&#xff0c;加上i-2所抢的钱&#xff0c;要么不抢&#xff0c;保留上一家的。 …

linux中的火墙优化策略

1.火墙介绍 1. netfilter 2. iptables 3. iptables | firewalld 2.火墙管理工具切换 在rocky9 中默认使用的是 firewalld firewalld -----> iptables dnf install iptables - services - y systemctl stop firewalld systemctl disable firewalld systemctl mask fi…

Vue3 使用 pinia

什么是Pinia Pinia是 Vue 的存储库&#xff0c;它允许您跨组件/页面共享状态&#xff0c;与vuex功能一样。 准备 安装 npm install pinia 或者 yarn add pinia使用 首先修改main.ts文件 main.ts import ./assets/main.cssimport { createApp } from vue import App from…

HTB:Tactics[WriteUP]

目录 连接至HTB服务器并启动靶机 1.Which Nmap switch can we use to enumerate machines when our ping ICMP packets are blocked by the Windows firewall? 2.What does the 3-letter acronym SMB stand for? 3.What port does SMB use to operate at? 4.What comma…

Comfyui segmentAnythingUltra V2报错

&#x1f385;问题表现及解决方案 Comfyui segmentAnythingUltra V2报错&#xff0c;找不到VITMatte模型&#xff0c;这个报错报的比较模糊&#xff0c;所以花了一点时间找模型。 简单来说&#xff0c;到huggingface上&#xff1a; https://huggingface.co/hustvl/vitmatte-s…

麒麟系统串口配置篇

麒麟系统串口配置篇 1.配置串口驱动&#xff08;编译/动态加载串口&#xff09; 解压文件夹,然后在解压后的文件夹所在目录&#xff0c;右键选择打开终端&#xff0c;依次执行以下命令&#xff1a; 以麒麟系统下的CH341串口驱动为例&#xff0c;解压CH341SER_LINUX.zip sudo…

【微服务】网关 - Gateway(下)(day8)

网关过滤工厂 在上一篇文章中&#xff0c;主要是对网关进行了一个总体的介绍&#xff0c;然后对网关中的断言进行了一个描述。在这篇文章中&#xff0c;主要是对网关中的最后一大核心——过滤进行介绍。 当客户端发送过来的请求经过断言之后&#xff0c;如果还想在请求前后添…

如何在 MySQL 中处理 BLOB 和 CLOB 数据类型

在 MySQL 数据库中&#xff0c;BLOB&#xff08;Binary Large Object&#xff09;和 CLOB&#xff08;Character Large Object&#xff09;数据类型用于存储大量的二进制数据和字符数据。本篇文章我们来一起看看如何在 MySQL 中处理 BLOB 和 CLOB 数据类型&#xff0c;并加入如…

7.3美团—Java日常实习面经

7.2晚上投的&#xff0c;发邮件约到了7.3晚上 总时长1小时10分钟左右 自我介绍 拷打项目30min 缓存三兄弟 Redis除了缓存&#xff0c;还能做什么 Redis的数据结构&#xff0c;什么时候用哈希&#xff0c;什么时候用字符串 线程池的执行流程 MySQL索引的数据结构 聚簇索引…