链表 --- C语言实现

本篇文章来详细介绍一下数据结构中的链表。

目录

1.链表的概念及结构

2.链表的分类

3.单链表的实现

4.链表的面试题

5.双向链表的实现

6.顺序表和链表的区别


1.链表的概念及结构

概念:链表是一种物理存储结构上非连续非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
 

注意:

  1. 从上图可看出,链式结构在逻辑上是连续的,但是在物理上不一定连续
  2. 现实中的结点一般都是从堆上申请出来的
  3. 从堆上申请的空间,是按照一定的策略来分配的,两次申请的空间可能连续,也可能不连续

2.链表的分类

实际中链表的结构非常多样,以下3种情况组合起来就有8种链表结构,2^3 = 8:
 

1.单项或者双向

 

2.带头或者不带头

 

3.循环或者非循环

 虽然有这么多的链表的结构,但是我们实际中最常用的还是两种结构

 1.无头单向非循环链表

 

2.带头双向循环链表

1.无头单向非循环链表结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多。
2.带头双向循环链表∶结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。另外这个结构虽然结构复杂,但是使用代码实现以后会发现结构会带来很多优势,实现反而简单了,后面我们代码实现了就知道了。

3.单链表的实现

//无头+单行+非循环链表的增删改查实现#include<stdio.h>
#include<stdlib.h>
#include<assert.h>typedef int SLTDataType;typedef struct SListNode
{SLTDataType data;struct SListNode* next;
}SListNode;// 动态申请一个节点
SListNode* BuySListNode(SLTDataType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?因为单链表只能向后访问
void SlistInsertAfter(SListNode* pos, SLTDataType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?因为单链表只能向后访问
void SlistEraseAfter(SListNode* pos);// 单链表的销毁
void SListDestroy(SListNode** pphead);//在pos之前插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x);//删除pos位置的值
void SListErase(SListNode** pphead, SListNode* pos);

接口实现:

// 动态申请一个节点
SListNode* BuySListNode(SLTDataType x)
{SListNode* newnode = (SListNode*)malloc(sizeof(SListNode));if (newnode == NULL){perror("malloc fail");return NULL;}//申请成功newnode->data = x;newnode->next = NULL;return newnode;
}
// 单链表打印
void SListPrint(SListNode* plist)
{SListNode* cur = plist;while (cur){printf("%d->", cur->data);cur = cur->next;}printf("NULL\n");
}
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDataType x)
{assert(pplist);//链表为空,pphead也不为空,因为它是头指针plist的地址SListNode* newnode = BuySListNode(x);//空链表if (*pplist == NULL){*pplist = newnode;}else{SListNode* tail = *pplist;while (tail->next){tail = tail->next;}tail->next = newnode;}
}
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDataType x)
{assert(pplist);//链表为空,pphead也不为空,因为它是头指针plist的地址SListNode* newnode = BuySListNode(x);newnode->next = *pplist;*pplist = newnode;
}
// 单链表的尾删
void SListPopBack(SListNode** pplist)
{assert(pplist);//链表为空,pphead也不为空,因为它是头指针plist的地址assert(*pplist);//空链表不能尾删if ((*pplist)->next == NULL){free(*pplist);*pplist = NULL;}else{//方法一:SListNode* tail = *pplist;while (tail->next->next){tail = tail->next;}free(tail->next);tail->next = NULL;//方法二/*SListNode* tail = *pplist;SListNode* prev = *pplist;while (tail->next){prev = tail;tail = tail->next;}free(tail);prev->next = NULL;*/}
}
// 单链表头删
void SListPopFront(SListNode** pplist)
{assert(pplist);//链表为空,pphead也不为空,因为它是头指针plist的地址assert(*pplist);//空链表不能头删SListNode* del = *pplist;*pplist = (*pplist)->next;free(del);del = NULL;
}
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDataType x)
{SListNode* cul = plist;while (cul){if (cul->data == x)return cul;cul = cul->next;}return NULL;
}
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?没有地址,找不到,单链表只能找后面的
void SlistInsertAfter(SListNode* pos, SLTDataType x)
{assert(pos);SListNode* newnode = BuySListNode(x);newnode->next = pos->next;pos->next = newnode;
}
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?没有地址,找不到
void SlistEraseAfter(SListNode* pos)
{assert(pos);assert(pos->next);SListNode* del = pos->next;pos->next = pos->next->next;free(del);del = NULL;
}// 单链表的销毁
void SListDestroy(SListNode** pphead)
{SListNode* del = *pphead;while (*pphead){del = *pphead;*pphead = (*pphead)->next;free(del);}
}//在pos之前插入
void SListInsert(SListNode** pphead, SListNode* pos, SLTDataType x)
{assert(pphead);SListNode* newnode = BuySListNode(x);if (pos == *pphead){newnode->next = *pphead;*pphead = newnode;}else{SListNode* cur = *pphead;while (cur){if (cur->next == pos){newnode->next = pos;cur->next = newnode;return;}cur = cur->next;}}
}//删除pos位置的值
void SListErase(SListNode** pphead, SListNode* pos)
{assert(pphead);assert(*pphead);assert(pos);if (pos == *pphead){*pphead = (*pphead)->next;free(pos);}else{SListNode* cur = *pphead;while (cur){if (cur->next == pos){cur->next = pos->next;free(pos);return;}cur = cur->next;}}
}

4.链表的面试题

1.删除链表中等于给定值val的所有结点。OJ链接

2.反转一个单链表。OJ链接

3.给定一个带有头结点head的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。ОJ链接

4.输入一个链表,输出该链表中倒数第k个结点。OJ链接

5.将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有结点组成的。OJ链接

6.编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前。OJ链接

7.链表的回文结构。OJ链接

8.输入两个链表,找出它们的第一个公共结点。OJ链接

9.给定一个链表,判断链表中是否有环。OJ链接

【思路】

快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针率先走到链表的末尾。

【扩展问题】

1.为什么快指针每次走两步,慢指针走一步可以?

假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在慢指针走到一圈之前,快指针肯定是可以追上慢指针的,即相遇。

2.快指针一次走3步,走4步,...n步行吗?

假设:快指针每次走3步,满指针每次走一步,此时快指针肯定先进环,慢指针后来才进
环。假设慢指针进环时候,快指针的位置如图所示:

此时按照上述方法来绕环移动,每次快指针走3步,慢指针走1步,是永远不会相遇的,快指针刚好将慢指针套圈了,因此不行。
只有快指针走2步,慢指针走一步才可以,因为换的最小长度是1,即使套圈了两个也在相同的位置。

10.给定一个链表,返回链表开始入环的第一个结点。如果链表无环,则返回NULL。OJ链接

结论:

双指针:先让一个指针走一步,一个指针走两步,最终两个指针会在环内相遇。再让一个指针从链表起始位置开始遍历链表,同时让一个指针从判环时相遇点的位置开始绕环运行,两个指针都是每次均走一步,最终肯定会在入口点的位置相遇。

证明:

说明:

phead为链表的起始点,E为环入口点,M与判断是否是环的时候相遇点(快慢指针第9题)

设:

环的长度为R,H到E的距离为LE到M的距离为X则:M到E的距离为R-x

在判环时,快慢指针相遇时所走的路径长度:

  • fast: L +X + nR
  • slow: L+ x

注意:

1.当慢指针进入环时,快指针可能已经在环中绕了n圈了,n至少为1因为:快指针先进环走到M的位置,最后又在M的位置与慢指针相遇

2.慢指针进环之后,快指针肯定会在慢指针走一圈之内追上慢指针

因为:慢指针进环后,快慢指针之间的距离最多就是环的长度,而两个指针在移动时,每次它们至今的距离都缩减一步(速度差是1),因此在慢指针移动一圈之前快指针肯定是可以追上慢指针的

而快指针速度是满指针的两倍,因此有如下关系是:2*(L+ X)= L+ X + nR
L+ x = nR
L= nR- x(n为1,2,3,4.......n的大小取决于环的大小,环越小n越大)

极端情况下,假设n = 1,此时:L =R- x

即:一个指针从链表起始位置运行,一个指针从相遇点位置绕环,每次都走一步,两个指针最终会在入口点的位置相遇

11.给定一个链表,每个结点包含一个额外增加的随机指针,该指针可以指向链表中的任何结点
或空结点。

要求返回这个链表的深度拷贝。OJ链接
 

5.双向链表的实现

// 带头+双向+循环链表增删查改实现
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>typedef int LTDataType;typedef struct ListNode
{struct ListNode* prev;LTDataType data;struct ListNode* next;
}LTNode;//初始化
//void InitListNode(LTNode** phead)//使用二级指针
LTNode* InitListNode();//使用返回值
//打印
void LTPrint(LTNode* phead);
//尾插
void LTPushBack(LTNode* phead, LTDataType x);
//头插
void LTPushFront(LTNode* phead, LTDataType x);
//判空
bool LTEmpty(LTNode* phead);
//尾删
void LTPopBack(LTNode* phead);
//头删
void LTPopFront(LTNode* phead);//查找
LTNode* LTFind(LTNode* phead, LTDataType x);//pos之前插入(与顺序表一致)
void LTInsert(LTNode* pos, LTDataType x);
//删除pos位置的值
void LTErase(LTNode* pos);
//释放链表
void LTDestroy(LTNode* phead);

 接口实现:

LTNode* BuyaNewNode(LTDataType x)
{LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));if (newnode == NULL){perror("malloc fail");return NULL;}newnode->prev = NULL;newnode->next = NULL;newnode->data = x;return newnode;
}//初始化
LTNode* InitListNode()
{LTNode* phead = BuyaNewNode(0);phead->next = phead;phead->prev = phead;return phead; 
}//打印
void LTPrint(LTNode* phead)
{assert(phead);printf("gurad<==>");LTNode* cur = phead->next;while (cur != phead){printf("%d<==>", cur->data);cur = cur->next;}printf("gurad\n");
}//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{//LTInsert(phead, x);assert(phead);LTNode* newnode = BuyaNewNode(x);LTNode* tail = phead->prev;tail->next = newnode;newnode->prev = tail;newnode->next = phead;phead->prev = newnode;
}//头插
void LTPushFront(LTNode* phead, LTDataType x)
{//LTInsert(phead->next, x);assert(phead);LTNode* newnode = BuyaNewNode(x);newnode->next = phead->next;phead->next = newnode;newnode->prev = phead;newnode->next->prev = newnode;
}bool LTEmpty(LTNode* phead)
{if (phead->next == phead){return true;}else{return false;}
}//尾删
void LTPopBack(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//空链表 //LTErase(phead->prev);LTNode* tail = phead->prev;LTNode* tailprev = tail->prev;free(tail);phead->prev = tailprev;tailprev->next = phead;}//头删
void LTPopFront(LTNode* phead)
{assert(phead);assert(!LTEmpty(phead));//空链表 //LTErase(phead->next);LTNode* frist = phead->next;LTNode* second = frist->next;free(frist);phead->next = second;second->prev = phead;
}//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){if (cur->data == x){return cur;}cur = cur->next;}return NULL;
}//pos之前插入(与顺序表一致)
void LTInsert(LTNode* pos, LTDataType x)
{assert(pos);LTNode* newnode = BuyaNewNode(x);LTNode* prev = pos->prev;newnode->prev = prev;newnode->next = pos;prev->next = newnode;pos->prev = newnode;
}//删除pos位置的值
void LTErase(LTNode* pos)
{assert(pos);LTNode* prev = pos->prev;LTNode* next = pos->next;prev->next = next;next->prev = prev;free(pos);
}//释放链表
void LTDestroy(LTNode* phead)
{assert(phead);LTNode* cur = phead->next;while (cur != phead){LTNode* next = cur->next;free(cur);cur = next;}free(phead);
}

6.顺序表和链表的区别

链表(双向循环带头链表):

 优点:

  1. 任意位置插入删除O(1)
  2. 按需申请释放空间

 缺点:

  1. 不支持下标随机访问
  2. CPU高速缓存命中率会更低

顺序表:

缺点:

  1. 前面部分插入删除数据,效率是O(N),需要挪动数据。
  2. 空间不够,需要扩容。a、扩容是需要付出代价的 b、一般还会伴随空间浪费。

优点:

  1. 尾插尾删效率不错。
  2. 下标的随机访问。
  3. CPU高速缓存命中率会更高
     
不同点顺序表链表
存储空间上物理上一定连续逻辑上连续,但物理上不一定连续
随机访问支持O(1)不支持,为O(N)
任意位置插入或者删除元素可能需要搬移元素,效率低O(N)只需要修改指针指向
插入动态顺序表,空间不够时需要扩容没有容量的概念
应用场景元素高效存储+频繁访问任意位置插入或删除频繁
缓存利用率

 备注:缓存利用率参考存储体系结构 以及 程序的局部性原理。

 数据结构是为了帮助我们跟好的管理内存。内存需要电,关机后就会消失,磁盘存储的数据在关机后也不会消失。

在CPU与内存之间存在寄存器和三级缓存。内存小使用寄存器(一般几个字节),大的使用三级缓存。

CPU读取数据时

  1.  先去看数据是否在缓存,在就叫缓存命中,则直接访问
  2. 不在就不命中,先加载数据到缓存,再访问

因为缓存一次会加载需要的数据以及这个数据旁边的数据,数组是连续存放的,所以缓存利用率高

可以参考:与程序员相关的CPU缓存知识

本篇结束。

 

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

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

相关文章

【HTML5】拖放详解及实现案例

文章目录 效果预览代码实现 效果预览 代码实现 <!DOCTYPE html> <html><head><meta charset"utf-8"><title>一颗不甘坠落的流星</title><style>#div1,#div2 {float: left;width: 100px;height: 27px;margin: 10px;paddin…

关于 Qt在windows使用mingw32编译器时从Qt5.9切换至Qt5.12出现“C2001:常量中有换行符“不修改编码 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/131901444 红胖子(红模仿)的博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV、OpenGL、ffmpeg、OSG、单片机、软…

Git简介与工作原理:了解Git的基本概念、版本控制系统和分布式版本控制的工作原理

&#x1f337;&#x1f341; 博主 libin9iOak带您 Go to New World.✨&#x1f341; &#x1f984; 个人主页——libin9iOak的博客&#x1f390; &#x1f433; 《面试题大全》 文章图文并茂&#x1f995;生动形象&#x1f996;简单易学&#xff01;欢迎大家来踩踩~&#x1f33…

day33哈希表

1.哈希表 常见的哈希表分为三类&#xff0c;数组&#xff0c;set&#xff0c;map&#xff0c;C语言的话是不是只能用数组和 2.例题 题目一&#xff1a; 分析&#xff1a;题目就是判断两个字符串出现的次数是否相同&#xff1b; 1&#xff09;哈希表26个小写字母次数初始化为0&…

RB-tree(红黑树)详解

RB-tree(红黑树) 红黑树的规则如下&#xff1a; 1.每个节点不是红色就是黑色 2.根节点为黑色 3.如果节点为红色&#xff0c;那么它的子节点必须为黑色 4.任何一个节点到NULL&#xff08;树的尾端&#xff09;的任何路径所包含的黑节点个数相同 简而言之就是每个路径的黑色节点数…

模拟量输出FC S_RTI(信捷C语言源代码)

模拟量输出FC SCL源代码请查看下面博客: PLC模拟量输出 模拟量转换FC S_RTI_博途模拟量转换指令_RXXW_Dor的博客-CSDN博客1、本文主要展示西门子博途模拟量输出转换的几种方法, 方法1:先展示下自编FC:计算公式如下:intput intput Real ISH Real //工程量上限 ISL Real //工…

【数据挖掘】将NLP技术引入到股市分析

一、说明 在交易中实施的机器学习模型通常根据历史股票价格和其他定量数据进行训练&#xff0c;以预测未来的股票价格。但是&#xff0c;自然语言处理&#xff08;NLP&#xff09;使我们能够分析财务文档&#xff0c;例如10-k表格&#xff0c;以预测股票走势。 二、对自然语言处…

【OpenCV】常见问题及解决办法

文章目录 0 前言1 中文乱码问题2 非法路径问题 0 前言 本篇博客主要是总结OpenCV使用过程中遇到的一些问题&#xff0c;以及对应的解决办法&#xff0c;这里重点是关注OpenCV&#xff0c;既有基于C的&#xff0c;也有基于Python的&#xff0c;比较全面&#xff0c;而且也会随着…

RocketMQ教程-安装和配置

Linux系统安装配置 64位操作系统&#xff0c;推荐 Linux/Unix/macOS 64位 JDK 1.8 Maven3.0 yum 安装jdk8 yum 安装maven 1.下载安装Apache RocketMQ RocketMQ 的安装包分为两种&#xff0c;二进制包和源码包。 点击这里 下载 Apache RocketMQ 5.1.3的源码包。你也可以从这…

Windows11的VS201x编译OpenCV+Contrib+CUDA

(1) CUDA下载&#xff0c;注意要和cudnn版本号相关。 我安装的是cuda11.0,注意VS2015不能编译CUDA11&#xff0c;所以用VS2015的话需要下载CUDA 10。因为更高的版本目前还没有cudnn。 (2) 下载和安装VS2015。 (3) 下载和解压CMake。 CMake地址&#xff1a; Releases Kitw…

Android dp to pix resources.getDimension(R.dimen.xxx) ,kotlin

Android dp to pix resources.getDimension(R.dimen.xxx) ,kotlin <?xml version"1.0" encoding"utf-8"?> <resources><dimen name"my_size_dp">20dp</dimen><dimen name"my_size_px">20px</dime…

数据仓库表设计理论

数据仓库表设计理论 数仓顾名思义是数据仓库&#xff0c;其数据来源大多来自于业务数据(例如:关系型数据库)&#xff0c;当设计数仓中表类型时(拉链表、增量表、全量表、流水表、切片表)时&#xff0c;应先观察业务数据的特点再设计数仓表结构 首先业务数据是会不断增长的-即…

练习——动态内存分配的笔试题

今天我们分享几道经典的笔试题&#xff0c;做完直接变成陈泽 第一题 ~~ --------------------------------------------------------------------------------------------------~~ void GetMemory(char* p) {p (char*)malloc(100); } void Test(void) {char* str NULL;Get…

阿里云容器镜像仓库(ACR)的创建和使用

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

使用spark进行hbase的bulkload

使用spark进行hbase的bulkload 一、 背景 HBase 是一个面向列&#xff0c;schemaless&#xff0c;高吞吐&#xff0c;高可靠可水平扩展的 NoSQL 数据库&#xff0c;用户可以通过 HBase client 提供的 put get 等 api 实现在数据的实时读写。在过去的几年里&#xff0c;HBase …

HTTP 请求走私漏洞(HTTP Request Smuggling)

一、什么是Http 请求走私漏洞&#xff1f; HTTP请求走私漏洞&#xff08;HTTP Request Smuggling&#xff09;是一种安全漏洞&#xff0c;利用了HTTP协议中请求和响应的解析和处理方式的不一致性。攻击者通过构造特定的恶意请求&#xff0c;以欺骗服务器和代理服务器&#xff0…

Godot 4 源码分析 - 增加管道通信

学习研究Godot 4&#xff0c;很爽&#xff0c;虽然很庞杂&#xff0c;但相对于自己的水平来说&#xff0c;很强大&#xff0c;尤其是vulkan这块直接打包可用&#xff0c;省得自己从头琢磨。 一点一点地消化、优化与完善&#xff0c;最终才能成为自己的。 这段时间就在Godot的…

Pytorch迁移学习使用Resnet50进行模型训练预测猫狗二分类

目录 1.ResNet残差网络 1.1 ResNet定义 1.2 ResNet 几种网络配置 1.3 ResNet50网络结构 1.3.1 前几层卷积和池化 1.3.2 残差块&#xff1a;构建深度残差网络 1.3.3 ResNet主体&#xff1a;堆叠多个残差块 1.4 迁移学习猫狗二分类实战 1.4.1 迁移学习 1.4.2 模型训练 1.…

华为数通HCIP-ISIS基础

IS-IS的基本概念 isis&#xff08;中间系统到中间路由协议&#xff09; 链路状态路由协议、IGP、无类路由协议&#xff1b; IS-IS是一种链路状态路由协议&#xff0c;IS-IS与OSPF在许多方面非常相似:运行IS-IS协议的直连设备之间通过发送Hello报文发现彼此&#xff0c;然后建…

从零开始搭建vue3 + ts + pinia + vite +element-plus项目

前言&#xff1a;据说vue2将于 2023 年 12 月 31 日停止维护&#xff0c;最近打算搭建一个vue3项目来学习一下&#xff0c;以防忘记&#xff0c;记录一下搭建过程。 一、使用npm创建项目 前提条件&#xff1a;已安装 16.0 或更高版本的 Node.js 执行 “npm init vuelatest”…