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

数据结构--单链表

数据结构之单链表

  • 概念与结构
    • 结点
    • 链表的性质
    • 链表的打印
  • 实现单链表
  • 链表的分类
  • 单链表算法题之环形链表
    • 环形链表一
    • 环形链表二

概念与结构

概念:链表是⼀种物理存储结构上⾮连续⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
在这里插入图片描述
用火车来形如链表比较贴切,每节车厢代表着链表的每一个节点,每个节点之间又相互连接。

结点

与顺序表不同的是,链表里的每节"车厢"都是独立申请(为动态申请,不需要占用空间,所以空间复杂度始终为O(1),这也是链表的一个优点。)下来的空间,我们称之为“结点/结点”。结点的组成主要有两个部分:当前结点要保存的数据和保存下⼀个结点的地址(指针变量)。图中指针变量plist保存的是第⼀个结点的地址,我们称plist此时“指向”第⼀个结点,如果我们希望plist“指向”第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。链表中每个结点都是独立申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。在这里插入图片描述

链表的性质

1、链式机构在逻辑上是连续的,在物理结构上不⼀定连续
2、结点⼀般是从堆上申请的
3、从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续
结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:
假设当前保存的结点为整型:
struct SListNode
{
int data; //结点数据
struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
};
当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)。
当我们想要从第⼀个结点⾛到最后⼀个结点时,只需要在当前结点拿上下⼀个结点的地址就可以了。
因此我们可以看出:链表的存储数据时单向的,可以从头结点查到尾结点,却无法从尾结点到头节点,这是相对于顺序表的缺点。

链表的打印

在这里插入图片描述
只需要给头指针即可,令图中的pcur指针每次循环指向其所指向的下一个节点,循环结束的判断条件是当pcur所指为空指针时,循环结束,证明链表遍历完成。

实现单链表

头文件,相当于所有实现函数的目录。

SList.h
typedef int SLTDataType;
typedef struct SListNode
{SLTDataType data; //结点数据struct SListNode* next; //指针保存下⼀个结点的地址
}SLTNode;
void SLTPrint(SLTNode* phead);
//头部插⼊删除/尾部插⼊删除
void SLTPushBack(SLTNode** pphead, SLTDataType x);
void SLTPushFront(SLTNode** pphead, SLTDataType x);
void SLTPopBack(SLTNode** pphead);
void SLTPopFront(SLTNode** pphead);
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos);
//销毁链表
void SListDestroy(SLTNode** pphead);

list.c文件,用于对每个功能的函数的实现。

#include"list.h"
#include<assert.h>
#include<string.h>
SListNode* BuySListNode(SLTDateType x)//动态申请第一个节点
{SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));assert(newnode);newnode->data = x;newnode->next = NULL;return newnode;
}
void SListPrint(SListNode* plist)//单链表的打印
{SListNode* pcur = plist;while (pcur){printf("%d->", pcur->data);pcur = pcur->next;}printf("NULL\n");
}
void SListPushBack(SListNode** pplist, SLTDateType x)//单链表的尾插
{assert(pplist);if (*pplist == NULL){*pplist=BuySListNode(x);}else{SListNode* pcur = *pplist;while (pcur->next){pcur = pcur->next;}pcur->next= BuySListNode(x);}
}
void SListPushFront(SListNode** pplist, SLTDateType x)//单链表的头插
{assert(pplist);SListNode* pcur = BuySListNode(x);pcur->next = *pplist;*pplist = pcur;
}
void SListPopBack(SListNode** pplist)//单链表的尾删
{assert(pplist && *pplist);if ((*pplist)->next == NULL){free(*pplist);*pplist = NULL;}else{SListNode* pcur = *pplist;SListNode* ppcr = *pplist;while (pcur->next){ppcr = pcur;pcur = pcur->next;}ppcr->next = NULL;free(pcur);}
}
void SListPopFront(SListNode** pplist)//头删
{assert(pplist && *pplist);if ((*pplist)->next == NULL){free(*pplist);*pplist == NULL;}else{SListNode* pcur = *pplist;pcur = (*pplist)->next;free(*pplist);*pplist = pcur;}
}
SListNode* SListFind(SListNode* plist, SLTDateType x)//查找
{assert(plist);SListNode* pcur = plist;while (pcur->data != x){pcur = pcur->next;}return pcur;
}
void SListInsertAfter(SListNode* pos, SLTDateType x)// 单链表在pos位置之后插入x
{assert(pos);SListNode*pcur=BuySListNode(x);SListNode* lis = pos->next;pos->next = pcur;pcur->next = lis;
}
void SListEraseAfter(SListNode* pos)// 单链表删除pos位置之后的值
{assert(pos);if (pos->next == NULL){perror("删除失败");}else{SListNode* pcur = pos->next->next;free(pos->next);pos->next = pcur;}
}
void SLTInsert(SListNode** pphead, SListNode* pos, SLTDateType x)// 在pos的前面插入
{assert(pphead && pos && *pphead);if (pos == *pphead){SListNode* pcur=BuySListNode(x);pcur->next = *pphead;*pphead = pcur;}else{SListNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}SListNode* plist=BuySListNode(x);pcur->next = plist;plist->next = pos;}
}
void SLTErase(SListNode** pphead, SListNode* pos)// 删除pos位置
{assert(pphead && pos && *pphead);if (pos == *pphead){free(*pphead);*pphead == NULL;}else{SListNode* pcur = *pphead;while (pcur->next != pos){pcur = pcur->next;}pcur->next = pos->next;free(pos);pos == NULL;}
}
void SLTDestroy(SListNode** pphead)//删除链表;
{assert(*pphead && pphead);SListNode* pcur = *pphead;while (pcur){SListNode* next = pcur->next;free(pcur);pcur = next;}*pphead == NULL;
}

链表的分类

链表的结构非常多样,以下情况组合起来就有8种(2x2x2)链表结构:
在这里插入图片描述
在这里插入图片描述
虽然有这么多的链表的结构,但是我们实际中最常⽤还是两种结构:单链表和双向带头循环链表
1.⽆头单向⾮循环链表:结构简单,⼀般不会单独用来存数据。实际中更多是作为其他数据结构的⼦
结构,如哈希桶、图的邻接表等等。另外这种结构在笔试⾯试中出现很多。
2.带头双向循环链表:结构最复杂,⼀般用在单独存储数据。实际中使用的链表数据结构,都是带头
双向循环链表。另外这个结构虽然结构复杂,但是使⽤代码实现以后会发现结构会带来很多优势,实
现反⽽简单了,后⾯我们代码实现了就知道了。

单链表算法题之环形链表

环形链表一

来自力扣的算法题:
链接: https://leetcode.cn/problems/linked-list-cycle/description/
在这里插入图片描述
对于环形链表,快慢指针必不可少,由于快指针要比慢指针遍历的更快,我们可以假设当慢指针刚到环的第一个位置时,快指针与慢指针的差距为N,而环的总长度为C,链表的头节点到环的第一个结点的距离为L,
在这里插入图片描述
此时我们假设快指针走三步,慢指针走一步,所以快指针与慢指针的相对速度为3-1步,如果N为偶数,那么随着快指针的遍历,N每次会减2,直到0,此时快慢指针重合,然后返回。如果N为奇数,那么N的值最后就会由1变为-1,这是说明快指针赶上并超过了慢指针,并没有重合,为了验证下一圈是否会重合,我们这样计算:在慢指针刚进入环时,慢指针走的路程为L,快指针为3L,等于L+C-N+kC(kC的意思是快指针走了k圈环后来到此位置),列出等式:3L=L+C-N+kC;化简:2L=(k+1)C-N;2L一定为偶数,则C-N必须为偶数才符合等式,情况只有两种:C奇数N奇数,C偶数N偶数。N为偶数在一定相遇,不考虑此种情况。N为奇数时,当快指针超过慢指针来到下一圈,此时距离为C-1;前面提到C必需为奇数,那么C-1为偶数,所以必定相遇。

环形链表二

链接: https://leetcode.cn/problems/linked-list-cycle-ii/description/
在这里插入图片描述
大致的一个思路是:仍然使用快慢指针,记录快慢指针相遇的节点,对结点和头节点同时遍历直到指向同一个地址时,此地址为环的起始点。
推理:
在这里插入图片描述
我们所要推理的是:L=C-N。首先让慢指针每次走一步,快指针每次走两步,在相遇点时慢指针走了L+N步,快指针走了L+N+kC步,等于2L+2N步,带入等式:L+N+kC=2L+2N;化简后可得:(k-1)C+C-N=L;其中(k-1)C为快指针走的圈数,加上后对位置没有任何影响,所以在判断时可以省去,为C-N=L;由此可以得出在相遇的位置到环的起始点的距离与头节点到环的起始点的位置相等。

相关文章:

  • 创建Express后端项目
  • python之装饰器、迭代器、生成器
  • linux ip命令使用
  • npm run build报Cannot find module错误的解决方法
  • 容器技术介绍
  • 卷积神经网络(CNN)图像处理与识别原理
  • CE认证大电流计量装置
  • 如何把PDF样本册转换为网址链接
  • 护眼台灯哪个品牌更好?五款由专业眼科医生推荐的护眼台灯
  • 什么是ISO9001认证
  • STM32嵌入式编程学习到提高:【4】UART串口打印
  • DNS与host文件
  • GloVe(全局词向量嵌入)
  • 【Linux】环境变量(初步认识环境变量)
  • openpnp - 散料飞达不要想着做万能版本,能够贴合现有的物料就好
  • Electron入门介绍
  • ES学习笔记(10)--ES6中的函数和数组补漏
  • github从入门到放弃(1)
  • go append函数以及写入
  • Hexo+码云+git快速搭建免费的静态Blog
  • javascript数组去重/查找/插入/删除
  • React Transition Group -- Transition 组件
  • vue中实现单选
  • Vue组件定义
  • WebSocket使用
  • 规范化安全开发 KOA 手脚架
  • 全栈开发——Linux
  • 如何将自己的网站分享到QQ空间,微信,微博等等
  • 如何在 Tornado 中实现 Middleware
  • 深入浅出Node.js
  • 通过获取异步加载JS文件进度实现一个canvas环形loading图
  • 为视图添加丝滑的水波纹
  • kubernetes资源对象--ingress
  • 京东物流联手山西图灵打造智能供应链,让阅读更有趣 ...
  • 如何用纯 CSS 创作一个货车 loader
  • #define,static,const,三种常量的区别
  • #每天一道面试题# 什么是MySQL的回表查询
  • (+4)2.2UML建模图
  • (2)空速传感器
  • (DenseNet)Densely Connected Convolutional Networks--Gao Huang
  • (k8s中)docker netty OOM问题记录
  • (Matalb分类预测)GA-BP遗传算法优化BP神经网络的多维分类预测
  • (pytorch进阶之路)扩散概率模型
  • (WSI分类)WSI分类文献小综述 2024
  • (附源码)springboot 房产中介系统 毕业设计 312341
  • (附源码)springboot码头作业管理系统 毕业设计 341654
  • (企业 / 公司项目)前端使用pingyin-pro将汉字转成拼音
  • (三十)Flask之wtforms库【剖析源码上篇】
  • (使用vite搭建vue3项目(vite + vue3 + vue router + pinia + element plus))
  • (五)IO流之ByteArrayInput/OutputStream
  • (循环依赖问题)学习spring的第九天
  • (一)ClickHouse 中的 `MaterializedMySQL` 数据库引擎的使用方法、设置、特性和限制。
  • (转)IIS6 ASP 0251超过响应缓冲区限制错误的解决方法
  • ***利用Ms05002溢出找“肉鸡
  • *Django中的Ajax 纯js的书写样式1