数据结构预算法--链表(单链表,双向链表)

1.链表


目录

1.链表

1.1链表的概念及结构

1.2 链表的分类

2.单链表的实现(不带哨兵位)

2.1接口函数

2.2函数的实现

3.双向链表的实现(带哨兵位)

3.1接口函数

3.2函数的实现


1.1链表的概念及结构

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

现实中 数据结构中


1.2 链表的分类

实际中链表的结构非常多样,以下情况组合起来就有8种链表结构:
1. 单向或者双向

 2. 带头或者不带头

3. 循环或者非循环

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


2.单链表的实现(不带哨兵位)


2.1接口函数

// 1、无头+单向+非循环链表增删查改实现
typedef int SLTDateType;
typedef struct SListNode
{
	SLTDateType data;
	struct SListNode* next;
}SListNode;
// 动态申请一个结点
SListNode* BuySListNode(SLTDateType x);
// 单链表打印
void SListPrint(SListNode* plist);
// 单链表尾插
void SListPushBack(SListNode** pplist, SLTDateType x);
// 单链表的头插
void SListPushFront(SListNode** pplist, SLTDateType x);
// 单链表的尾删
void SListPopBack(SListNode** pplist);
// 单链表头删
void SListPopFront(SListNode** pplist);
// 单链表查找
SListNode* SListFind(SListNode* plist, SLTDateType x);
// 单链表在pos位置之后插入x
// 分析思考为什么不在pos位置之前插入?
void SListInsertAfter(SListNode* pos, SLTDateType x);
// 单链表删除pos位置之后的值
// 分析思考为什么不删除pos位置?
void SListEraseAfter(SListNode* pos);
//销毁单链表
void SLTDestroy(SLNode** pphead);

2.2函数的实现

1. 动态申请一个结点

SListNode* BuySListNode(SLTDateType x)
{
    SListNode* new = (SListNode*)malloc(sizeof(SListNode));
    if (new == NULL)
    {
        perror(malloc);
        exit(-1);
    }
    new->data =x;
    new->next = NULL;
    return new;
}

动态申链表,并给新malloc出的空间传值。


2.单链表尾插

void SListPushBack(SListNode** pplist, SLTDateType x)
{
    assert(pphead);
    SListNode* newlist = BuySListNode(x);
    if (*pplist == NULL)
    {
        *pplist = newlist;
    }
    else {
        SListNode* tail = NULL;
        tail = *pplist;
        while (tail->next != NULL)
        {
            tail = tail->next;
        }
        tail->next = newlist;
    }
}

1. 创建一个新节点,并为其分配内存空间。 2. 将新节点的数据赋值为要插入的数据。 3. 将新节点的指针域(next)设置为NULL,表示它是链表的最后一个节点。 4. 如果链表为空,将头指针指向新节点;否则,找到链表的最后一个节点,将其指针域指向新节点。


3.单链表的尾删

void SListPopBack(SListNode** pplist)
{
    assert(pphead);
    assert(*pplist);
    if ((*pplist)->next == NULL)
    {
        free(*pplist);
        *pplist = NULL;
    }
    else {
        SListNode* tail = *pplist;
        SListNode* prve = NULL;
        while (tail->next != NULL)
        {    
            prve = tail;
            tail = tail->next;
        }
        free(tail);
        tail = NULL;//不置空也没问题,出作用域自动销毁
        prve->next = NULL;
    }
    
}

1. 如果链表为空,直接返回空链表。 2. 如果链表只有一个节点,释放该节点的内存空间,并将头指针指向NULL。 3. 遍历链表,找到倒数第二个节点。 4. 将倒数第二个节点的指针域设置为NULL,表示它是链表的最后一个节点。 5. 释放最后一个节点的内存空间。


3.单链表的头插

void SListPushFront(SListNode** pplist, SLTDateType x)
{
    assert(pphead);
    SListNode* newlist = BuySListNode(x);
    newlist->next = *pplist;
    *pplist = newlist; 
}

1. 创建一个新节点,并为其分配内存空间。 2. 将新节点的数据赋值为要插入的数据。 3. 将新节点的指针域指向当前的头节点。 4. 将头指针指向新节点。


4.单链表头删

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

1. 如果链表为空,直接返回空链表。 2. 将头指针指向第二个节点。 3. 释放第一个节点的内存空间。


5.单链表查找

SListNode* SListFind(SListNode* plist, SLTDateType x)
{
    SListNode* find = plist;
    while (find != NULL)
    {
        if (find->data == x)
        {
            return find;
            break;
        }
        find = find->next;
    }
    return NULL;
}

1. 如果链表为空,返回NULL。 2. 遍历链表,逐个比较节点的数据与目标值。 3. 如果找到匹配的节点,返回该节点的指针;否则,返回NULL。


6.在pos节点后插入

void SListInsertAfter(SListNode* pos, SLTDateType x)
{
    assert(pphead);
	if (pos == NULL) {
		return;
	}

	SListNode* newlist = BuySListNode(x);
	newlist->next = pos->next;
	pos->next = newlist;
}

1.判断了指定位置是否为NULL,如果为NULL,则直接返回,不进行插入操作。2.我们创建一个新节点,并将新节点的数据赋值为要插入的数据。3.我们将新节点的指针域指向指定位置节点原来的下一个节点,然后将指定位置节点的指针域指向新节点,完成插入操作。


7.删除pos节点后的值

void SListEraseAfter(SListNode* pos)
{
    assert(pphead);
	if (pos == NULL || pos->next == NULL) {
		return;
	}

	SListNode* temp = pos->next;
	pos->next = temp->next;
	free(temp);
}

1.判断了指定位置是否为NULL或者指定位置的下一个节点是否为NULL,如果是,则直接返回,不进行删除操作。2.我们创建一个临时指针temp,指向指定位置节点的下一个节点。3.我们将指定位置节点的指针域指向temp节点的下一个节点,然后释放temp节点的内存空间,完成删除操作。


8.销毁单链表

void SLTDestroy(SLNode** pphead)
{
	assert(pphead);

	SLNode* cur = *pphead;
	while (cur)
	{
		SLNode* next = cur->next;
		free(cur);
		cur = next;
	}

	*pphead = NULL;
}
先保存下一个的地址,在销毁当前节点。

9.分析思考为什么不在pos位置之前插入?为什么不删除pos位置?

        这是因为单链表的节点只有一个指针指向下一个节点,没有指向前一个节点的指针。那该如何解决呢?请看后面双向链表的实现。        

3.双向链表的实现(带哨兵位)


3.1接口函数

#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;
	LTDataType data;
}LTNode;

LTNode* BuyLTNode(LTDataType x);
LTNode* LTInit();
void LTPrint(LTNode* phead);
void LTPushBack(LTNode* phead, LTDataType x);
void LTPopBack(LTNode* phead);

void LTPushFront(LTNode* phead, LTDataType x);
void LTPopFront(LTNode* phead);

int LTSize(LTNode* phead);

LTNode* LTFind(LTNode* phead, LTDataType x);

// pos֮ǰx
void LTInsert(LTNode* pos, LTDataType x);
// ɾposλ
void LTErase(LTNode* pos);

3.2函数的实现

        我们可以注意到,在单链表和双线链表出参的不同,单链表传的是二级指针,而双向链表传的是一级指针。实际上这是有无哨兵位造成的,当没有哨兵位时,我们需要用二级指针去保存链表第一个节点的地址,此时改变的是结构体的指针,因此需要用结构体的二级指针,而带哨兵位,我们只需要改变哨兵位后面的节点(结构体),此时改变的是结构体,因此只需要用结构体的一级指针。

        双向链表相较于单链表实际上就多了头指针域,这样就能找到当前节点的上一个节点,也就是可以轻松的做到在任意节点前插入。双向链表做到了“首尾呼应”,自然为节点不用指向NULL。

这样很多操作就变的简单,快捷,高效。


1.动态申请一个结点

LTNode* BuyLTNode(LTDataType x)
{
	LTNode* node = (LTNode*)malloc(sizeof(LTNode));
	if (node == NULL)
	{
		perror("malloc fail");
		exit(-1);
	}

	node->data = x;
	node->next = NULL;
	node->prev = NULL;

	return node;
}

2.头节点(哨兵位)的初始化

LTNode* LTInit()
{
	LTNode* phead = BuyLTNode(0);
	phead->next = phead;
	phead->prev = phead;

	return phead;
}

      

         这里涉及到改变结构体的值的操作,当然也可以写成接收二级指针的形式,档期当前的方式当然也是可行的。这里的操作就是对哨兵位的初始化,我们可以看到,我们将头节点的前,后指针域都指向了自己,这样就保持了循环的效果。


  3.双向链表尾插 

void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* tail = phead->prev;
	LTNode* newnode = BuyLTNode(x);

	newnode->prev = tail;
	tail->next = newnode;

	newnode->next = phead;
	phead->prev = newnode;
}

       

        双向链表要实现尾删是非常便捷的,不用循环找到尾节点,因为头节点的前指针域就指向了尾节点,所以我们只需要一步就能找到尾了。然后我们只需尾节点 指向新节点,然后让新节点指向尾节点,之后再让新节点指向头节点,最后让头节点指向新节点就好了。


4.尾删

void LTPopBack(LTNode* phead)
{
	assert(phead);
	assert(phead->next != phead);

	LTNode* tail = phead->prev;
	LTNode* tailPrev = tail->prev;
	free(tail);

	tailPrev->next = phead;
	phead->prev = tailPrev;

	
}

        要删除尾节点,首先要找到尾节点和尾节点的前一个节点,然后释放掉尾节点,让新的尾节点指向头,再让头指向尾。


5.打印双向链表

void LTPrint(LTNode* phead)
{
	assert(phead);
    assert(phead->next!=phead);
	printf("phead<=>");
	LTNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d<=>", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

       

        当我们打印双向链表时,只存在头节点就不要打印了,所以我们可以加上第二句断言。

打印的时候我们从头节点的下一个节点开始打印,最后走一圈遍历到头的时候就停止打印。


        这里我就省略头插,头删,求节点个数和查找了,因为操作大同小异,十分简单,最后会奉上完整代码。


6.pos节点前插入

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* posPrev = pos->prev;
	LTNode* newnode = BuyLTNode(x);

	posPrev->next = newnode;
	newnode->prev = posPrev;
	newnode->next = pos;
	pos->prev = newnode;
}

        想要在pos节点的前面插入,那么只需要找到pos节点和pos节点前面的节点就可以了,找pos节点我们可以配合查找函数来使用,找到想要的pos节点就可以了。


7.删除pos节点

void LTErase(LTNode* pos)
{
	assert(pos);
	LTNode* posPrev = pos->prev;
	LTNode* posNext = pos->next;

	free(pos);

	posPrev->next = posNext;
	posNext->prev = posPrev;
}

想要删除pos节点,只需要找到pos节点的前一个和pos节点的后一个,free掉pos节点,然后让pos节点的前一个和pos节点的后一个连接就好了。

        这就是链表的全部内容了,希望对各位老铁有帮助,接下来我会更新链表的OJ题目,希望各位老铁,多多支持!!! 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

2023美团外卖商家销量

数据内容字段如下 外卖ID 外卖STR 外卖商家名称 地址 城市 省份 电话 纬度 经度 月销 起送价 评分 经营许可证 食品许可证 资源下载&#xff1a;https://download.csdn.net/download/WANJIAWEN1002/88444367?spm1001.2014.3001.5503

linux espeak语音tts;pyttsx3 ubuntu使用

整体使用espeak声音很机械不太自然 1、linux espeak语音tts 安装&#xff1a; sudo apt install espeak使用&#xff1a; #中文男声 espeak -v zh 你好 #中文女声 espeak -v zhf3 你好 #粤语男声 espeak -v zhy 你好注意&#xff1a;espeak -v zh 你好 &#xff08;Full d…

【LeetCode笔试题】27.移除元素

问题描述 给你一个数组 nums 和一个值 val&#xff0c;你需要 原地 移除所有数值等于 val 的元素&#xff0c;并返回移除后数组的新长度。 不要使用额外的数组空间&#xff0c;你必须仅使用 O(1) 额外空间并 原地 修改输入数组。 元素的顺序可以改变。你不需要考虑数组中超出新…

有趣的 TCP 抢带宽行为

昨天发了一篇 非技术文章&#xff0c;很多人找我讨论&#xff0c;浓缩成一句话&#xff0c;就是 “死道友而不死贫道”&#xff0c;我的简历上写着这些把戏能带来什么&#xff0c;我的 blog 上写着这么做是多么无耻&#xff0c;哈哈。 看看共享链路上如何挤占带宽&#xff1a; …

ElasticSearch7.x - HTTP 操作 - 索引操作

创建索引 对比关系型数据库,创建索引就等同于创建数据库 在 Postman 中,向 ES 服务器发 PUT 请求 :http://192.168.254.101:9200/shopping 说明 {"acknowledged"【响应结果】: true, # true 操作成功"shards_acknowledged"【分片结果】: true, # 分片操…

立体库堆垛机放货动作控制程序功能

放货动作程序功能块 DB11.DBX0.0 为左出货台有货 DB11.DBX1.0 为右出货台有货 左出货台车就位 DB11.DBX0.2 右出货台车就位 DB11.DBX1.2 左出货台车就位 DB11.DBX0.2 右出货台车就位 DB11.DBX1.2 左出货台车就位 DB11.DBX0.2 右出货台车就位 DB11.DBX1.2

使用迁移学习在线校准深度学习模型

使用迁移学习在线校准深度学习模型 本文参考的是2023年发表于Engineering Applications of Artificial Intelligence, EAAI的Deep Gaussian mixture adaptive network for robust soft sensor modeling with a closed-loop calibration mechanism 1. 动机 概念漂移导致历史训…

想学好Python,一定不能错过这些项目!整整70个,附带源码课件

在程序员的求职中&#xff0c;「项目经历」往往是最重要的一环&#xff0c;它能最直观地体现你的编程能力。对于在校生来说&#xff0c;一个好的「项目经历」甚至可以等同于工作经验。可以说&#xff0c;把项目经历写好了&#xff0c;求职就通过了一半。&#xff08;文末有教程…

什么是UV贴图?

UV 是与几何图形的顶点信息相对应的二维纹理坐标。UV 至关重要&#xff0c;因为它们提供了表面网格与图像纹理如何应用于该表面之间的联系。它们基本上是控制纹理上哪些像素对应于 3D 网格上的哪个顶点的标记点。它们在雕刻中也很重要。 为什么UV映射很重要&#xff1f; 默认情…

Lightgraph.js节点图引擎【低代码开发利器】

Lightgraph.js是一个 Javascript 节点图引擎库&#xff0c;可以实现类似虚幻引擎的蓝图编程&#xff0c;包括一个编辑器来构建和测试节点图&#xff0c;支持浏览器和Node.js&#xff0c;可以轻松集成到任何现有的 Web 应用程序中&#xff0c;并且无需编辑器即可运行节点图。 在…

基于SSM的科技公司门户网站

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

DDD系列 - 第2讲 从贫血模型、事务脚本到面向对象(富血模型)、DDD领域模型的跨越

目录 一、灵魂拷问二、CRUD Boy现状三、贫血模型四、事务脚本五、从贫血模型演变到面向对象&#xff08;富血模型&#xff09;六、借助DDD领域模型摆脱事务脚本七、更多 一、灵魂拷问 Java作为面向对象的编程语言&#xff0c;使用Java编程的你面向对象了吗&#xff1f; 二、C…

css实现div倾斜效果

效果如下&#xff1a; <!DOCTYPE html> <html><head><meta charset"UTF-8"><title></title></head> <style> *{margin:0;padding: 0;} .box1{margin:30px 100px;width:100px;height:200px;background:blueviolet;} …

Android环境安装

一、环境 安装OS&#xff1a;Windows10 IDE: Android Studio Giraffe | 2022.3.1 Patch 2 Build #AI-223.8836.35.2231.10811636, built on September 15, 2023 JDK:Java8 二、安装Android Studio IDE和JDK Windows下构建安卓开发环境一点也不难就是有点麻烦。 第一、下载…

你的代码有bug

作为程序员&#xff0c;我们时常会收到这样的反馈&#xff1a;“你的代码有bug”。当面临这种情况时&#xff0c;我们可能会感到受伤和失落。然而&#xff0c;我们应该认识到&#xff0c;代码问题是一种常见现象&#xff0c;无论是谁都可能遇到。通过接受批评和建议&#xff0c…

代码随想录训练营Day2:1.有序数组的平方 2.长度最小的子数组3,螺旋矩阵

本专栏内容为&#xff1a;代码随想录训练营学习专栏&#xff0c;用于记录训练营的学习经验分享与总结。 文档讲解&#xff1a;代码随想录 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;C &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓…

JRebel热部署——效率提升100倍(程序员工具必备)

1. 下载JRebel 2.激活程序 这里推荐一个免费获取jrebel激活服务器地址和激活邮箱的地址:点击进入 进入网站之后就可以获取到激活链接和邮箱 点击进入激活 复制过去激活就可以 然后就可以看到激活成功了 3.如何使用 代码修改后&#xff0c;直接CtrlShitF9 即可重新启动 4…

Cross-Origin跨站问题详解(跨站请求、跨站cookie)

背景&#xff1a;我部署frontend和backend到两个不同的docker容器&#xff0c;前端路径为http://localhost:3000&#xff0c;后端路径为http://localhost:4000。我设置了用户登录功能&#xff0c;并使用cookie进行session管理。当我的前端登录时&#xff0c;创建了一个session&…

bat脚本设置变量有空格踩到的坑

SET PATH c:\xxx;%PATH% 我想把一个路径作为path环境变量最前面的一个&#xff0c;所以使用了上面的语句。 但是没有生效&#xff0c;我还以为是其他什么原因&#xff0c;后来又有一个类似的需求&#xff1a; set output output\x64 结果在使用 %output% 的时候是一个空格&…

2024最新fl studio 21.2.0.3842中文版完整下载

FL Studio 21.2.0.3842中文版完整下载是最好的音乐开发和制作软件也称为水果音乐软件。它是最受欢迎的工作室&#xff0c;因为它包含了一个主要的听觉工作场所。2024最新fl studioFL Studio 21版有不同的功能&#xff0c;如它包含图形和音乐音序器&#xff0c;帮助您使完美的配…