草庐IT

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

乐言.. 2024-02-12 原文

系列文章目录

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

第二章 顺序表,列表

第三章 栈和队列

第四章 二叉树

第五章 排序


作者:🎈乐言🎈

简介:🎈大一学生,目前在致力于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;
}


🚀总结

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

有关数据结构修炼第二篇:顺序表和链表的更多相关文章

  1. ruby - 使用 ruby​​ 将 HTML 转换为纯文本并维护结构/格式 - 2

    我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h

  2. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  3. ruby - Chef 执行非顺序配方 - 2

    我遵循了教程http://gettingstartedwithchef.com/,第1章。我的运行list是"run_list":["recipe[apt]","recipe[phpap]"]我的phpapRecipe默认Recipeinclude_recipe"apache2"include_recipe"build-essential"include_recipe"openssl"include_recipe"mysql::client"include_recipe"mysql::server"include_recipe"php"include_recipe"php::modul

  4. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  5. ruby - 是否有用于序列化和反序列化各种格式的对象层次结构的模式? - 2

    给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最

  6. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  7. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  8. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  9. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  10. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

随机推荐