数据结构修炼第二篇:顺序表和链表

news2024/11/19 17:33:26

系列文章目录

第一章 时间复杂度和空间复杂度

第二章 顺序表,列表

第三章 栈和队列

第四章 二叉树

第五章 排序


作者:🎈乐言🎈

简介:🎈大一学生,目前在致力于c/c++/python,高数的学习,有问题尽管问我,关注后私聊!

持续更新专栏:《c进阶》,《数据结构修炼》🚀(优质好文持续更新中)🎈

文章目录

目录

系列文章目录

文章目录

前言

线性表

各个接口的实现

1.初始化顺序表

2.销毁顺序表

3.检查顺序表容量是否满了

4.顺序表尾插

5.顺序表尾删

6.顺序表头插

7.顺序表头删

8.在顺序表中查找定值

9.在顺序表指定位置插入数据

链表

无头单向循环链表的实现

单链表定义:

 动态申请一个节点

销毁(释放)所有节点

打印单链表

单链表头插

单链表尾删

单链表头部删除

单链表在pos位置之后插入

单链表中删除位置为pos的节点

单链表删除指定pos位置之后的节点

求单链表的长度:

判断但俩表是否为空

总结



前言

在顺序表和链表的学习过程中,相信大家会有一些困惑,因为这里用到了大量结构体和数组的知识,乐言在这里将会对顺序表和单链表进行深刻的讲解,有代码问题,可以后台私信作者!!!!!!!!如果文章有错误,欢迎指正!!!!!!


🚀线性表

线性表(linear list)是n个具有相同特性的数据元素的 有限序列 。 线性表是一种在实际中广泛使
用的数据结构,常见的线性表: 顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在 物理结构上并不一定是连续的
线性表在物理上存储时,通常以 数组 链式结构 的形式存储

顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存
储。在数组上完成数据的增删查改。
顺序表有静态的和动态的两种,首先我们来看 静态的顺序表的代码:
#define N 10000
typedef int sldataline;
struct Seqline
{
    sldataline a[N];
    int size;
};

我们在这里可以发现,静态的顺序表开辟的内存是固定的,给多了浪费空间,给少了又不够,还得重新调整代码,所以静态的顺序表十分不好用,在实际的生活场景中用的也不多。

所以我们通常使用动态的顺序表:

typedef int SLDataType;
typedef struct Seqlist
{
    SLDataTpye *a;
    size_t size;
    size_t capacity
}SeqList;

我们要加上容量空间,同时size为存储的有效数据的个数 

如果空间不够就可以扩容


🚀各个接口的实现

1.初始化顺序表

要防止传进来的指针为空,所以一定要提前加上断言

void SeqListInit(SeqList*psl)
{
    assert(psl!=NULL);
    psl->size=0;
    psl->capacity=0;
}

2.销毁顺序表

依然要记得加上断言,防止传进来的指针是空指针

void SeqListDestory(SeqList*psl)
{
    assert(psl!=NULL);
    free(psl->a);
    psl->size=0;
    psl->capacity=0;
}

首先释放开辟的空间,然后将指针置为空,将数据和空间容量大小都重置为0

3.检查顺序表容量是否满了

如果我们一个个加入数据,这样重复操作对我们来说太过麻烦,所以我们可以一次性扩容两倍,这样能应付大多数情况,两倍是一个折中的方式

void CheckCapacity(SeqList*psl)
{
    assert(psl!=NULL);
    if(psl->size == psl->capacity)
    {
        size_t newcapacity;
        if(psl->capacity == 0)
            newcapacity = psl->capacity = 4;
        else
            newcapacity = 2 * psl->capacity;
    }
    SLDataTpye*p=(SLDataType*)realloc(psl->a,newcapacity*sizeof(SLDataType));//扩容
    if(p == NULL)
          {
               perror("realloc");
                exit(-1);
            }
    psl->p;
    psl->capacity = newcapacity;
}

4.顺序表尾插

void SeqListPushBack(Seqlist*psl,SLDataType x)
{
    assert(psl !=NULL);
    CheckCapacity(psl);
    psl->a[psl->size] =x;
    psl->size++;
}

5.顺序表尾删

void SeqListPopBack(SeqList*psl)
{
    assert(psl->NULL);
    assert(psl->size >0);
    psl->size --;
}

因为我们无法得知SLDataType是什么类型,他随时都可以改,因此我们不能单纯直接将随意赋值为0(psl->a[psl->size-1]=0)

6.顺序表头插

因为顺序表是连续存储的,所以我们在顺序表头部插入的时候,我们要依次挪动数据

void SeqListPushFront(SeqList*psl,SLDType x)
{
    assert(psl);
    CheckCapacity(psl);
    int i = 0;
    for(i=psl->size-1;i>=0;i--)
    {
        psl->a[i+1] = psl->a[i];
    }
    psl->a[0] = x;
    psl->size++;
}

7.顺序表头删

因为顺序表是连续存储的,所以要依次挪动数据

void SeqListPopFront(SeqList* psl)
{
    assert(psl);
    assert(psl->size >0);
    int i =0;
    for(i=1;i<psl->size;i++)
        {
            psl->a[i-1]=psl->a[i];
        }
        psl->size--;
}

依次覆盖即可,然后再把有效数据-1;

8.在顺序表中查找定值

int SeqListFind(const SeqList*psl,SLDataType x)
{
    assert(psl);
    int i = 0;
    for(i=0;i<psl->size;i++)
    {
        if(psl->[i]==x)
        {
            return i;
        }
    }
     return -1;
}

9.在顺序表指定位置插入数据

void SeqListInsert(SeqList*psl,size_t pos,SLDataType x)
{
    assert(psl);
    assert(pos>=0 && pos <=psl->size);
    CheckCapacity(psl);
    size_t i =0;
    for(i=psl->size;i>pos;i--)
        {
            psl->a[i]=psl->a[i-1];
        }
    psl->a[pos]=x;
    psl->size++;
}

🚀链表

顺序表我们在使用时,虽然下标可以随时访问,但是会出现一系列问题会引起我们的思考

1. 中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容       到 200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空           间。
思考:如何解决以上问题呢?下面给出了链表的结构来看看

🚀无头单向循环链表的实现

第一步我们依然是先新建一个工程:

  • SList.h(单链表的类型定义,函数接口声明,引用头文件)
  • SList.c(单链表各个接口函数的实现)
  • test.c(主函数,测试函数的各个接口功能) 

单链表定义:

单链表类似如图结构

一部分储存结构体的数据,另一部分储存下一块结构体空间的地址

代码如下:

typedef int SLDatatype;//定义单链表数据类型

typedef struct SListNode//定义单链表节点
{
    SLDataType data;//数据空间
    struct SListNode*next;//指针空间
}SListNode;

 动态申请一个节点

代码如下:

SListNode* BuySListNode(SLTDataType x)
{
    SListNode* node=(SListNode*)malloc(sizeof(SListNode));
    if(node == NULL)
    {
        perror("malloc");
        return;
    }
}

销毁(释放)所有节点

我们在这里有一个遍历链表的操作

{
	SListNode* node = (SListNode*)malloc(sizeof(SListNode));
	if (node == NULL)
	{
		perror("malloc");
		return;
	}
	node->data = x;
	node->next = NULL;
}
void SListDestory(SListNode** pphead)
{
	assert(pphead);
	SListNode* cur = *pphead;
	while (cur != NULL)
	{
		SListNode* next = cur->next;
		free(cur);
	}
	*pphead = NULL;
}

最后不能忘了置空指针

打印单链表

void SListPrint(SListNode* phead)
{
	SListNode* ptr = phead;
	while (ptr != NULL)
	{
		printf("%d->", ptr->data);
		ptr = ptr->next;
	}
	printf("NULL\n");
}

单链表尾插

下面是一个明显的错误

 代码看似逻辑清晰,其实问题暴露无遗。此处的指针传参,相当于把plist指针变量的值拷贝给phead给phead赋值newhead,phead的值改变是不会影响plist

这种写法会导致一个问题:         

  • 当链表为空时,我们需要改变plist的指向,让他指向第一个节点,然而初始值plist和phead都指向NULL,调用函数后,phead指向了新的节点,但是plist还是指向NULL
  • 问题出现,我们又该如何解决?

plist指向的是第一个节点的指针,想要在函数中改变plist的值(指向),必须把plist指针进行指针传参,因为plist原本就是一级指针,我们要想改变他,我们只能取他的地址进行传参

在函数中,我们想要改变int的值,就要传递int*,当我们想要传递int*的时候,我们就要传递int**......

正确写法是通过二级指针传参,改变plist的指向

单链表为空的时候,plist直接指向新节点

单链表不为空,会找到尾部节点,然后将尾部节点的next指向新节点!!!

//单链表尾插
void SListPushBack(SListNode** pphead, SLTDataType x)
{
	assert(pphead);
	SListNode* newnode = BuyListNode(x);
	if (*pphead == NULL)
		*pphead = newnode;
	else if (**pphead != NULL)
	{
		SListNode* tail = *phead;
		while (tail->next != NULL)
		{
			tail = tail->next;
		}
		tail->next = newnode;
	}
}

单链表头插

头插具体实现算法如图:

新数据的next指向plist指向的节点

 代码实现:

void SListPushFront(SListNode* pphead, SLTDataType x)
{
	assert(pphead);
	SListNode* newnode = BuySListNode(x);
	newnode->next = *pphead;
	pphead = *newnode;
}

单链表尾删

思想:

1:找到链表尾节点的上一个节点。

2:双指针,分别找到链表尾节点和它上一个节点

如图所示:

 

  •  当单链表只有一个节点时,删除节点,plist指向NULL
  • 而当单链表有多个节点时,先找到单链表尾节点的前一个节点,删除,然后将该节点的next指向NULL
  • 可能会改变plist的指向,所以我们要用二级指针接受

代码如下:

void SListPopBack(SListNode** pphead)
{
	assert(pphead);
	assert(*pphead);
	SListNode* tail = *pphead;
	if ((*pphead)->next == NULL)
	{
		free(*pphead);
		*pphead = NULL;
	}
	else
	{
		while (tail->next->next != NULL)
		{
			tail = tail->next;
		}
		free(tail->next);
		tail->next = NULL;
	}
}

如果pphead为空,那么直接释放+置空

如果pphead不为空,那么直接判断下一个节点是否为空为空则置空,不为空则继续判断

思路2代码实现:

void SListPopBack(SListNode** phead)
{
	assert(pphead);
	assert(*pphead);
	SListNode* tail = *pphead;
	SListNode* prev = *pphead;
	while (tail->next)
	{
		prev = tail;
		tail = tail->next;
	}
	free(tail);
	prev->next = NULL;
}

单链表头部删除

void SListPopFront(SListNode** pphead)
{
	assert(pphead);
	assert(**pphead);
	SListNode* cur = *pphead;
	*pphead = cur->next;
	free(cur);
	cur = NULL;
}

将*pphead移向第二个节点即可。

在单链表中查找指定点的节点

SListNode* SListFind(SListNode** pphead, SLDataType x)
{
	SListNode* cur = *pphead;
	while (cur != NULL)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}
	return NULL;
}

单链表在pos位置之后插入

 我们只需在pos位置的next指向一个新开辟的地址,新地址的next指向原来的下一个地址

代码实现:

void SListInsertAfter(SListNode* pos, SLDataType x)
{
	assert(pos);
	SListNode* newnode = BuySListNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

单链表中删除位置为pos的节点

考虑到pos位置可能为第一个节点,所以我们有必要选择进行语句

代码如下:

void SListDel(SListNode** pphead,SListNode* pos)
{
	assert(pphead);
	assert(*pphead);
	assert(pos);
	if (pos == *pphead)
	{
		SListPopFront(pphead);
	}
	else
	{
		SListNode* prev = *pphead;
		while (prev->next != pos)
			prev = prev->next;
	}
	prev->next = pos->prev;
	free(pos);
	pos = NULL;
}

单链表删除指定pos位置之后的节点

图示:

代码实现: 

此处我们要定义一个新的指针,来存放pos的下一个节点的地址

void SListDelAfter(SListNode* pos)
{
	assert(pos);
	assert(pos->next);
	SListNode* posnext = pos->next;
	pos->next = pos->next->next;
	free(posnext);
}

求单链表的长度:

遍历寻找是否为空即可

int SListSize(SListNode* phead)
{
	int size = 0;
	SListNode* cur = phead;
	while (cur != NULL)
	{
		size++;
		cur = cur->next;
	}
	return size;
}

判断但俩表是否为空

bool SListEmpty(SListNode* phead)
{
	return phead == NULL;
}


🚀总结

本文具体写了顺序表和单链表增删查改的一系列操作,希望各位同志们也要动手操作一下,这样才能融会贯通,熟稔于心

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

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

相关文章

二十四节气-谷雨文案、海报分享,谷雨润万物,不觉夏已至。

谷雨&#xff0c;是二十四节气之第6个节气&#xff0c;春季的最后一个节气。 谷雨有三候&#xff1a; 一候萍始生&#xff0c;即谷雨后降雨量增多&#xff0c;春雨绵绵&#xff0c;浮萍开始生长&#xff1b; 二候鸣鸠拂其羽&#xff0c;布谷鸟便开始提醒人们播种了&#xff…

【SCI征稿】IEEE旗下1区人工智能类SCI, 稳定检索22年,仅3个月左右录用~

一、期刊简介&#xff1a; 1区人工智能类SCI&EI (高质量) 【期刊概况】IF:6.0-7.0, JCR1区, 中科院3区&#xff1b; 【终审周期】走期刊部系统&#xff0c;3个月左右录用&#xff1b; 【检索情况】SCI&EI双检&#xff0c;正刊&#xff1b; 【数据库收录年份】2001…

自编码器简单介绍—使用PyTorch库实现一个简单的自编码器,并使用MNIST数据集进行训练和测试

文章目录 自编码器简单介绍什么是自编码器&#xff1f;自动编码器和卷积神经网络的区别&#xff1f;如何构建一个自编码器&#xff1f;如何训练自编码器&#xff1f;如何使用自编码器进行图像压缩&#xff1f;总结使用PyTorch构建简单的自动编码器第一步&#xff1a;导入库和数…

中级软件设计师备考---数据库系统2

目录 规范化理论并发控制数据库完整性约束数据备份 规范化理论 函数依赖 部分函数依赖&#xff1a;在一个关系中&#xff0c;一个非主属性依赖于该关系的某个候选键的一部分属性。举个例子&#xff0c;假设有一个关系R(A,B,C,D)&#xff0c;其中(A,B)是候选键。 如果C仅依赖于A…

微服务学习之面试知识相关总结(Nacos、MQ)

文章目录 壹 微服务Nacos1.1 SpringCloud常见组件1.2 Nacos的服务注册表结构1.3 Nacos如何支撑内部数十万服务注册压力1.4 Nacos避免并发读写冲突问1.5 Nacos与Eureka的区别1.6 Sentinel的限流与Gateway的限流的差别1.7 Sentinel的线程隔离与Hystix的线程隔离的差别 贰 MQ知识2…

前端Img图片不同格式的互相转化

目录 一、格式简介 二、格式互转 2.1、base64在视图上进行页面展示 2.3、将二进制流转为url进行页面展示 2.3、静态路径转二进制流传给后端 一、格式简介 主要有base64(数字字母组成的乱码那种)、url(http://xxx地址那种)、二进制流(后端上传图片的格式)、本地项目文件夹…

【JavaEE】社区版IDEA(2021.X版本及之前)创建SpringBoot项目

目录 下载Spring Boot Helper 创建项目 下载相关依赖 判断成功 删除多余文件 项目建好后添加依赖 输出Hello World SpringBoot的优点 下载Spring Boot Helper 创建项目 下载相关依赖 如果没有配置过国内源&#xff0c;参考【JavaEE】Spring项目的创建与使用_p_fly的博…

[架构之路-173]-《软考-系统分析师》-5-数据库系统-6-分布式数据库系统

目录 5 . 6 分布式数据库系统 5.6.1分布式数据库槪述 0. 分布式数据库特点 1 . 分布式数据库的体系结构 2 . 分布式数据库的优点 5.6.2 数据切片 1 . 数据分片方法的分类 2 . 数据分片的原则 3 . 分布透明性 5.6.3分布式数据库查询优化 2 . 副本的选择与多副本的更新策…

数据迁移实践 | MySQL到ClickHouse,HTAP黄金搭档

MySQL是世界上最流行的开源数据库&#xff0c;也是OLTP界的顶流&#xff0c;但是对于OLAP分析型业务场景的能力太弱。ClickHouse是最近几年数仓OLAP分析查询领域的黑马&#xff0c;当红炸子鸡&#xff0c;有意思的是天然兼容MySQL语法。所以很多用户喜欢OLTP放MySQL&#xff0c…

2023年湖北安全员ABC证报考条件都有哪些?甘建二告诉你

一、安全员ABC证是什么&#xff1f; 安全员A、B、C证属于建筑三类人员证书。建筑三类人员&#xff1a;是指建筑施工企业主要负责人、项目负责人和专职安全生产管理人员。 建筑企业的法人代表&#xff0c;必须取得A证才能担任法人代表 建造师必须取得B证才能担任项目负责人 …

FreeRTOS 信号量(一)

文章目录 一、信号量简介二、二值信号量1. 二值信号量简介2. 创建二值信号量①函数 vSemaphoreCreateBinary ()②函数 xSemaphoreCreateBinary()③函数 xSemaphoreCreateBinaryStatic() 3. 二值信号量创建过程分析4. 释放信号量①函数 xSemaphoreGive()②函数 xSemaphoreGiveFr…

UFD203A101 3BHE019361R0101电 工理论、电子技术、信息处理、控制理论、电力系统分析

UFD203A101 3BHE019361R0101电 工理论、电子技术、信息处理、控制理论、电力系统分析 作为电气工程及其自动化专业的大学生都会关心电气工程及其自动化就业方向是什么&#xff1f;电气工程专业就业方向怎样&#xff1f;自动化专业就业方向怎样&#xff1f; 对于很多本专业的在校…

实验06:哈夫曼编码

1.实验目的&#xff1a; 理解贪心算法的思想&#xff0c;掌握哈夫曼编码的技术和图像编解码算法的基本。 2.实验内容&#xff1a; 统计图像像素灰度值的分布特性&#xff0c;利用哈夫曼编码构造码表&#xff0c;实现对图像的编码和解码。 3.实验要求&#xff1a; 首先完成…

《JavaEE》InputStream, OutputStream 的用法

目录 File类 路径 绝对路径 相对路径 InputStream和OutputStream的使用 InputStream基本用法 OutputStream基本用法 功能实现 我们先来尝试着使用一些File类完成一些基本操作 我们查看这个文本是否存在 如果不存在我们创建一个新的文本出来 在当前文件夹中创建一个新…

MATLAB函数封装1:生成QT可以调用的.dll动态链接库

在进行相关算法的开发和设计过程中&#xff0c;MATLAB具有特别的优势&#xff0c;尤其是对于矩阵运算的处理&#xff0c;具有很多现成的方法和函数可以进行调用&#xff0c;同时MATLAB支持把函数封装成不同的语言方便完成算法的集成。 这里记录利用MATLAB封装成C动态链接库&…

git 自学笔记

git 自学笔记 git 是一个开源的分布式版本控制软件&#xff0c;可以敏捷的处理任何大小项目。 git 的工作流程大体如下&#xff1a; 首先克隆一个git资源作为工作目录 在克隆的资源上添加或者修改文件 如果其他人也修改了&#xff0c;就要对资源进行更新 在提交时也要查看有没有…

通过使用生成对抗市场模型改进基于强化学习的交易的泛化

Improving Generalization in Reinforcement Learning–Based Trading by Using a Generative Adversarial Market Model | IEEE Journals & Magazine | IEEE Xplore Improving Generalization in Reinforcement Learning–Based Trading by Using a Generative Adversaria…

ASEMI代理ADG736BRMZ-REEL7原装ADI车规级ADG736BRMZ-REEL7

编辑&#xff1a;ll ASEMI代理ADG736BRMZ-REEL7原装ADI车规级ADG736BRMZ-REEL7 型号&#xff1a;ADG736BRMZ-REEL7 品牌&#xff1a;ADI /亚德诺 封装&#xff1a;MSOP-10 批号&#xff1a;2023 安装类型&#xff1a;表面贴装型 引脚数量&#xff1a;10 类型&#xff1…

c/c++:指针p+p-p*p/,数组a+1,指针减指针,指针实现strlen函数,指针的比较运算,指针数组,多级指针

c&#xff1a; 2022找工作是学历、能力和运气的超强结合体&#xff0c;遇到寒冬&#xff0c;大厂不招人&#xff0c;此时学会c的话&#xff0c; 我所知道的周边的会c的同学&#xff0c;可手握10多个offer&#xff0c;随心所欲&#xff0c;而找啥算法岗的&#xff0c;基本gg 提…

IMX6ULLPRO单独编译kernel+dtb内核模块以及uboot

目录 为什么编译驱动程序之前要先编译内核&#xff1f; 驱动程序要用到内核文件&#xff1a; 编译内核 编译安装内核模块 编译内核模块 安装内核模块到 Ubuntu 某个目录下备用 安装内核和模块到开发板上 Bootloader 介绍 编译 u-boot 镜像 为什么编译驱动程序之前要先编…