目录
上期我们讲解了顺序表的基本概念和实现方法(传送门:详解顺序表)。但是顺序表存在着如下三个问题:
- 顺序表中间及头部的插入与删除,需要对原有数据进行移动,时间复杂度为O(N),成本较高
- 使用realloc进行增容时需要申请新空间,释放旧空间,拷贝数据,消耗较高。
- 由于我们无法知道用户实际需要多少空间,在增容时往往可能会有大量的空间剩余。例如在容量为100时满了进行2倍增容到200,如果后续只需插入5个数据,则会浪费95个数据空间;而如果我们每次只扩大1个数据空间,当需要插入95个数据时,就需要进行realloc操作95次,成本较高。
那么这些问题要如何解决呢?通过链表, 我们就可以很轻松的解决以上问题。下面,就让我们感受链表的魅力吧!
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。它的结构如下所示(以单向链表为例):

我们把date和next形成的结构体称为链表的一个结点,我们可以看到,链表就是由一个个结点链接起来的非连续线性结构,不同结点通过next指针连接的,最后一个结点的next指针指向空。链表也是线性表中的一种。
当然,在实际应用中,链表的结构多种多样,通过以下几种情况组合而成的就有8种链表结构:
1. 单 向 、 双 向
2. 带 头结点 、 不 带 头结点
3. 循 环 、 非 循 环
本期我们讲解的是实际应用中最常用的两种结构之一:单向不带头非循环链表。(另外一种是带头双向循环链表)
通过以上链表结构,我们就能很好地解决顺序表的局限:
- 每当我们需要新增数据时,我们只需申请一个新结点用来保存数据,然后用指针将链表与新结点链接起来即可,并不需要进行数据拷贝。
- 由于使用链表存储数据总是处于满载状态,每个结点都是有效数据,需要插入时则申请新结点并与链表连接,因此就不存在空间浪费的问题。
- 链表的结构是线性非连续的,各结点通过指针连接。如果需要在头部或中间插入数据,只需改变结点指针的指向即可,无需再移动数据。
首先,在实现各种接口函数前,我们需要定义一个结构体来代表每一个结点,用date保存结点中的数据,用next保存下一结点的地址。同样的,我们采取typedef的方式将类型重命名方便代码的编写与维护。如下:

由于我们实现的单向链表是不带头结点的,所以我们还需要定义一个指针指向链表的第一个结点(下面称为头指针)。
对于头插,我们只需要创建新结点保存数据,然后将头指针指向新结点,新结点指向原来头指针指向的结点即可,动态效果如下:

具体代码如下:
//用于创建新结点
SLNode* CreateNode(SLDateType x)
{
SLNode* cur = (SLNode*)malloc(sizeof(SLNode));
cur->date = x;
cur->next = NULL;
return cur;
}
//头插
void SListPushFront(SLNode** pphead, SLDateType x)
{
SLNode* NewNode = CreateNode(x); //获取新结点
NewNode->next = *pphead;
*pphead = NewNode; //修改头指针
}
值得注意的是,函数中我们传入的是头指针的地址。这是由于我们需要修改头指针使其指向新的结点,因此需要采用址传递的方式,用二级指针接收,否则只会修改临时变量造成出错。
对于尾插,我们需要先找到链表的尾结点,然后将尾结点的next指向新结点,动态效果如下:

代码如下:
//尾插,初稿
void SListPushBack(SLNode** pphead, SLDateType x)
{
SLNode* NewNode = CreateNode(x);
SLNode* tail = *pphead;
while (tail->next != NULL) //不为尾结点
{
tail = tail->next; //tail指向下一结点
}
//找到尾结点
tail->next = NewNode;
}
但是,上述代码是存在问题的。我们知道->相当于是一种解引用,当链表为空时,即tail==NULL时,此时我们进行tail->next操作显然是非法的,所以我们还需对链表为空的情况做出讨论,最终代码如下:
//尾插,终稿
void SListPushBack(SLNode** pphead, SLDateType x)
{
SLNode* NewNode = CreateNode(x);
if (*pphead == NULL)
{
*pphead = NewNode; //为空就直接修改头指针指向新结点,因此需要传二级指针
}
else
{
SLNode* tail = *pphead;
while (tail->next != NULL) //不为尾结点
{
tail = tail->next; //指向下一结点
}
//找到尾结点
tail->next = NewNode;
}
}
对于头删,我们只需要将头指针指向下一个位置,然后将原来指向的空间free()掉即可。如果链表为空,我们就让函数直接返回,具体动态效果如下:

在代码实现中,我们需要先创建一个临时变量next来保存下一个结点的地址,这是因为free()后就无法通过->得到下一个结点的地址了。具体代码如下:
//头删
void SListPopFront(SLNode** pphead)
{
if (*pphead == NULL)
{
return; //链表为空直接返回
}
else
{
SLNode* next = (*pphead)->next; //存储下一个结点地址
free(*pphead);
*pphead = next; //改变头指针指向,因此需要传二级指针
}
}
对于尾删,我们同样需要找到尾结点。这里和尾插不同的是,我们除了要找到尾结点,还需找到尾结点的前一个结点并使其next置为空,因此我们可以使用两个指针prev和tail进行移动,prev指向tail的上一个结点,具体动图如下:

具体代码如下:
//尾删,初稿
void SListPopBack(SLNode** pphead)
{
if (*pphead == NULL) //没有结点
{
return;
}
else //一个以上结点
{
SLNode* tail = *pphead;
SLNode* prev = NULL;
while (tail->next != NULL) //tail不为尾结点
{
prev = tail;
tail = tail->next; //tail指向下一结点
}
//tail为尾结点,此时prev为尾结点的前一个结点
free(tail); //释放掉尾结点
tail=NULL;
prev->next = NULL;
}
}
这里和头删一样,当链表为空时直接让函数返回。但是以上代码依旧存在着一个问题,就是当链表只有一个结点时,tail直接指向尾结点,此时prev为NULL,禁止对其进行->操作。所以我们还需要对只有一个结点的情况进行讨论,最终代码如下:
//尾删,终稿
void SListPopBack(SLNode** pphead)
{
if (*pphead == NULL) //没有结点
{
return;
}
else if ((*pphead)->next == NULL) //只有一个结点
{
free(*pphead);
*pphead = NULL; //直接将结点释放掉,头指针改为NULL,因此需要传二级指针
}
else //一个以上结点
{
SLNode* tail = *pphead;
SLNode* prev = NULL;
while (tail->next != NULL) //tail不为尾结点
{
prev = tail;
tail = tail->next; //tail指向下一结点
}
//tail为尾结点,此时prev为尾结点的前一个结点
free(tail); //释放掉尾结点
tail=NULL;
prev->next = NULL;
}
}
对于查找,我们只需遍历链表的所有结点即可,故不需要头指针,只需传一级指针即可。当找到需要查找的date时,返回对应结点的指针,若没找到或者链表为空时,返回NULL,代码如下:
//查找
SLNode* SListFind(SLNode* phead, SLDateType x)
{
while (phead)
{
if (phead->date == x)
{
return phead; //找到了,返回结点指针
}
phead = phead->next; //向后查找
}
//没有找到,返回空指针
return NULL;
}
对于插入,我们可以实现一个在指定结点前插入一个新结点的接口,而这个指定结点我们可以通过查找接口来获取。相同的,在进行插入操作时我们还需要得到前一个结点的地址,我们用prev来存储,然后将prev->next改为新结点地址,新结点的next为我们传入的结点的地址。动态效果如下:

//插入,初稿
void SListInsert(SLNode** pphead, SLNode* pos, SLDateType x)
{
if (pos == NULL) //指定结点为空直接返回
{
return;
}
SLNode* NewNode = CreateNode(x); //获取新结点
SLNode* prev = *pphead;
while (prev->next != pos) //prev不指向pos上一结点
{
prev = prev->next; //prev指向下一结点
}
//当prev指向pos上一结点,插入新结点
prev->next = NewNode;
NewNode->next = pos;
}
(嘿嘿,我又来了qwq),上述的代码其实还是存在bug的,就是当我们传入的指定结点为头结点时,prev->next永远不可能等于pos,prev不断向后更新,最终为NULL导致程序崩溃。动态分析如下:

因此我们需要进行分类讨论,而我们发现如果pos指向头结点,那在其前面插入不就相当于头插吗?所以我们可以直接调用头插接口,改进后的代码如下:
//插入,终稿
void SListInsert(SLNode** pphead, SLNode* pos, SLDateType x)
{
if (pos == NULL) //指定结点为空直接返回
{
return;
}
if (*pphead==pos) //pos为头结点指针,直接头插
{
SListPushFront(pphead,x);
}
else
{
SLNode* NewNode = CreateNode(x); //获取新结点
SLNode* prev = *pphead;
while (prev->next != pos) //prev不指向pos上一结点
{
prev = prev->next; //prev指向下一结点
}
//当prev指向pos上一结点,插入新结点
prev->next = NewNode;
NewNode->next = pos;
}
}
对于删除,我们同样可以实现一个删除指定结点的接口,而这个指定结点我们依旧可以通过查找接口来获取。同样,除了将指定的结点free()掉,还需将其上一个结点的next指向pos的下一结点。与插入一样都需要找到上一个结点的位置,在这过程中可能引发的问题也是一样的,这里就不再赘述了,直接上代码:
//删除
void SListErase(SLNode** pphead, SLNode* pos)
{
if (pos == NULL) //指定结点为空直接返回
{
return;
}
if (*pphead == pos) //pos为头结点指针,直接头删
{
SListPopFront(pphead);
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos) //prev不指向pos上一结点
{
prev = prev->next; //使prev指向下一个结点
}
//当prev指向pos上一结点,删除pos指向结点,更新prev->next
prev->next = pos->next;
free(pos);
}
}
对于打印,只需从头结点开始,向后遍历链表,打印每个结点的date直到走到链表尾即可,此时指针指向NULL。由于不修改头指针指向,因此采用值传递即可,用一级指针接收。代码如下:
//打印
void SListPrint(SLNode* phead)
{
while (phead != NULL) //遍历链表直到链表尾
{
printf("%d->", phead->date);
phead = phead->next;
}
//到达链表尾,打印NULL
printf("NULL");
}
通过上面一个个接口的实现,我们得出以下两个值得注意的地方:
- 当我们需要修改头指针时,如插入删除操作时,需要使用址传递,用二级指针来接收头指针的地址。而如果我们不需要修改头指针时,如打印,查找等,建议使用值传递,用一级指针来接收,避免头指针被意外修改。
- 在实现链表接口时,需要时刻考虑到当链表为空,链表只有一个结点,对头结点进行操作,对尾结点进行操作等特殊情况是否会出现bug。
我们可以采用多文件编写的形式,将上述接口的定义实现放在SList.c文件中,然后将接口的声明和结构体的定义放于SList.h头文件中,以达到封装的效果。这样我们如果需要使用单向链表,就只需要在文件中包含对应的头文件SList.h就可以使用我们上面定义的各种接口。以下为本文实现的单向链表完整代码以及效果展示:
//SList.h文件,用于声明接口函数,定义结构体
#pragma once
#include<stdio.h>
#include<stdlib.h>
typedef int SLDateType;
struct SListNode
{
SLDateType date;
struct SListNode* next;
};
typedef struct SListNode SLNode;
// 不会改变链表的头指针,传一级指针
void SListPrint(SLNode* phead);
SLNode* SListFind(SLNode* phead, SLDateType x);
// 可能会改变链表的头指针,传二级指针
void SListPushBack(SLNode** pphead, SLDateType x);
void SListPushFront(SLNode** pphead, SLDateType x);
void SListPopFront(SLNode** pphead);
void SListPopBack(SLNode** pphead);
// 在pos的前面插入x
void SListInsert(SLNode** phead, SLNode* pos, SLDateType x);
// 删除pos位置的值
void SListErase(SLNode** phead, SLNode* pos);
//SList.c文件,用于定义接口函数
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListPrint(SLNode* phead)
{
while (phead != NULL)
{
printf("%d->", phead->date);
phead = phead->next;
}
printf("NULL");
}
SLNode* CreateNode(SLDateType x)
{
SLNode* cur = (SLNode*)malloc(sizeof(SLNode));
cur->date = x;
cur->next = NULL;
return cur;
}
void SListPushBack(SLNode** pphead, SLDateType x)
{
SLNode* NewNode = CreateNode(x);
if (*pphead == NULL)
{
*pphead = NewNode; //为空就直接修改头指针指向新结点,因此需要传二级指针
}
else
{
SLNode* tail = *pphead;
while (tail->next != NULL) //不为尾结点
{
tail = tail->next; //指向下一结点
}
//找到尾结点
tail->next = NewNode;
}
}
void SListPushFront(SLNode** pphead, SLDateType x)
{
SLNode* NewNode = CreateNode(x); //获取新结点
NewNode->next = *pphead;
*pphead = NewNode; //修改头指针
}
void SListPopFront(SLNode** pphead)
{
if (*pphead == NULL)
{
return; //链表为空直接返回
}
else
{
SLNode* next = (*pphead)->next; //存储下一个结点地址
free(*pphead);
*pphead = next; //改变头指针指向,因此需要传二级指针
}
}
void SListPopBack(SLNode** pphead)
{
if (*pphead == NULL) //没有结点
{
return;
}
else if ((*pphead)->next == NULL) //只有一个结点
{
free(*pphead);
*pphead = NULL; //直接将结点释放掉,头指针改为NULL,因此需要传二级指针
}
else //一个以上结点
{
SLNode* tail = *pphead;
SLNode* prev = NULL;
while (tail->next != NULL) //tail不为尾结点
{
prev = tail;
tail = tail->next; //tail指向下一结点
}
//tail为尾结点,此时prev为尾结点的前一个结点
free(tail); //释放掉尾结点
tail = NULL;
prev->next = NULL;
}
}
SLNode* SListFind(SLNode* phead, SLDateType x)
{
while (phead)
{
if (phead->date == x)
{
return phead; //找到了,返回结点指针
}
phead = phead->next; //向后查找
}
//没有找到,返回空指针
return NULL;
}
void SListInsert(SLNode** pphead, SLNode* pos, SLDateType x)
{
if (pos == NULL) //指定结点为空直接返回
{
return;
}
if (*pphead == pos) //pos为头结点指针,直接头插
{
SListPushFront(pphead, x);
}
else
{
SLNode* NewNode = CreateNode(x); //获取新结点
SLNode* prev = *pphead;
while (prev->next != pos) //prev不指向pos上一结点
{
prev = prev->next; //prev指向下一结点
}
//prev指向pos上一结点,插入新结点
prev->next = NewNode;
NewNode->next = pos;
}
}
void SListErase(SLNode** pphead, SLNode* pos)
{
if (pos == NULL) //指定结点为空直接返回
{
return;
}
if (*pphead == pos)
{
SListPopFront(pphead);
}
else
{
SLNode* prev = *pphead;
while (prev->next != pos)
{
prev = prev->next;
}
prev->next = pos->next;
free(pos);
}
}
最后, 我们在text.c文件调用单向链表各个接口进行测试,如下:
//text.c文件,用于测试
#define _CRT_SECURE_NO_WARNINGS 1
#include "SList.h"
void SListText()
{
SLNode* phead = NULL;
printf("起始数据: \n");
SListPrint(phead);
//头插
SListPushFront(&phead, 1);
SListPushFront(&phead, 2);
SListPushFront(&phead, 3);
printf("\n头插入数据后: \n");
SListPrint(phead);
//尾插
SListPushBack(&phead, 4);
SListPushBack(&phead, 5);
SListPushBack(&phead, 6);
printf("\n尾插入数据后: \n");
SListPrint(phead);
//头删
SListPopFront(&phead);
printf("\n头删数据后: \n");
SListPrint(phead);
//尾删
SListPopBack(&phead);
printf("\n尾删数据后: \n");
SListPrint(phead);
//找出数据为5的结点并在其前面插入8
SLNode* cur1 = SListFind(phead, 5);
if (cur1)
{
SListInsert(&phead, cur1, 8);
}
printf("\n5前面插入数据后: \n");
SListPrint(phead);
//删除数据为1的结点
SLNode* cur2 = SListFind(phead, 1);
if (cur2)
{
SListErase(&phead, cur2);
}
printf("\n删除数据1的结点后: \n");
SListPrint(phead);
}
int main()
{
SListText();
return 0;
}
以下就是测试的最终效果:

以上,就是本期的全部内容。
制作不易,能否点个赞再走呢qwq
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我主要使用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
有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
我正在尝试使用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_
无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD
本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01 客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02 数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit
文章目录一、概述简介原理模块二、配置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
我正在尝试在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
文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,