【基本数据结构】链表

文章目录

  • 前言
  • 链表
    • 简介
      • 头节点与尾节点
      • 特性
    • 分类
      • 单向链表
      • 双向链表
      • 循环链表
    • 单链表基本操作
      • 定义并初始化单链表
      • 读取节点
      • 插入节点
      • 删除节点
      • 修改节点
  • 参考资料
  • 写在最后

前言

本系列专注更新基本数据结构,现有以下文章:

【算法与数据结构】数组.

【算法与数据结构】链表.

【算法与数据结构】哈希表.


链表

简介

链表是一种线性结构,但不同于数组在内存中占据一块连续的内存,链表使用的是内存中一组任意的存储单元来存储具有相同的数据类型的元素。这组任意的存储单元可以是连续的,也可以不是连续的。

以单链表为例,链表的存储方式如下图所示。

链表-链表.drawio

链表将一组任意的存储单元串联在一起。每一个存储单元被称为链表的一个节点,节点是一个结构体。结构体内存储两个变量,一个是节点的值,另一个是指向链表下一个节点的指针。一个节点值为整型的链表结构体可以这样定义:

struct ListNode {
    int val;
    ListNode* next;
}

头节点与尾节点

链表的头节点指的是链表的第一个节点(有的资料中将第一个元素之前的节点称为头节点,就是我们会面要讲到的呀节点),通常给一个链表的头节点,我们就可以通过遍历得到链表中的每一个节点。这里需要区分一下头节点与头指针。

头指针是指向链表第一个节点的指针。在单向链表中,头指针指向链表的头节点,就是后面会提到的呀节点的next指针,即 dummy->next。在双向链表中,头指针同样指向链表的头节点。

链表的尾节点是指链表中最后一个节点。在单向链表中,尾节点的 next 指针通常指向空指针 nullptr,表示链表的末尾。在双向链表中,尾节点的 next 指针同样指向空指针 nullptr,而 prev 指针则指向倒数第二个节点,表示双向链表的末尾。

特性

链表不需要实现事先分配内存,在需要存储空间时可以临时申请。因为链表不需要内存中一块连续的存储空间,所以相比数组可以更好的利用内存中零散的空间。相比于数组,使用链表执行数据的插入、删除以及移动效率会高一点。但是空间开销相比数组会大一点,因为链表的每一个节点需要存储两个变量,而数组的每个位置只需要存储一个变量。


分类

单向链表

定义

单向链表指的是链表的每一个节点里的指针都会指向下一个节点的链表。

单向链表节点类设计如下所示, C++11 \texttt{C++11} C++11 的标准库中虽然也定义了 forward_list \texttt{forward\_list} forward_list 单向链表,但因为单向链表的定义与操作相对简单,所有我们通常自己定义节点。

struct ListNode {
    int val;
    ListNode* next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode* _next) : val(x), next(_next) {}
 };

结构图

链表-单向链表.drawio

双向链表

定义

双向链表是对单向链表的升级,除了具备单向链表的 next 节点之外,还有一个 prev 指针,该指针指向当前节点的上一个节点。头节点的上一个节点为 nullptr 节点,尾节点的下一个节点为 nullptr

双向链表的节点类设计如下所示。 C++11 \texttt{C++11} C++11 的标准库中虽然也定义了 list \texttt{list} list 双向链表.

struct ListNode {
    int val;
    ListNode* next;
    ListNode* prev;
    ListNode() : val(0), next(nullptr), prev(nullptr) {}
    ListNode(int x) : val(x), next(nullptr), prev(nullptr) {}
    ListNode(int x, ListNode* _next, ListNode* _prev) : val(x), next(_next), prev(_prev) {}
 };

结构图

链表-双向链表.drawio

循环链表

定义

循环链表有两种,一种是在单向链表中将尾节点的 next 指针指向由空指针改为指向头节点形成的单向循环链表,另一种指的是双向循环链表。

双向循环循环链表是在双向链表的基础上,将链表的头节点和尾节点连接在一起,即将头节点的 prev 指针指向尾节点,尾节点的 next 指针指向头节点。通过这样的操作可以实现从循环链表的任何一个节点出发都能找到其他的任意节点。

循环链表的节点类设计与双向链表的节点类设计一致。

结构图

链表-循环链表.drawio

单链表基本操作

链表是一种具有增、删、改、查这四种基本操作的基本数据结构。单链表作为一种形式最简单的链表自然也具备这四种操作。本节会介绍定义并初始化单链表以及提到的四种基本操作,中间还会穿插介绍如何计算链表的长度。

在这单向链表、双向链表和循环链表中,单向链表最为基础,并且是算法类面试题中链表这一块的考察重点,需要重点掌握。

定义并初始化单链表

// 定义节点
struct ListNode {
    int val;
    ListNode* next;
    ListNode() : val(0), next(nullptr) {}
    ListNode(int x) : val(x), next(nullptr) {}
    ListNode(int x, ListNode* _next) : val(x), next(_next) {}
 };

// 定义链表头节点
ListNode* head = new ListNode(0);

在此例子中,我们首先定义了链表的节点类,然后定义了一个节点 head 作为链表的头节点,头节点的 next 指针指向一个空节点。

读取节点

在数组这种顺序结构中,我们计算任意一个元素的存储位置是很容易的(C/C++ 中虽然是通过下标进行索引的,但其底层是通过数组的首地址与下标之间的计算获得对应位置的地址,再取地址中的元素)。但是在单链表中我们无法像数组那样通过索引得知第 N 个节点是什么,只能从头节点开始一个节点一个节点的查找。

获得链表的第 N 个节点(N >= 1 )的算法思路:

  • 在查找链表的第 N 个节点之前需要先统计链表中的节点总数,如果总数 cnt < N,则直接返回 nullptr,否则接着执行以下步骤。
  • 声明一个指向链表头节点的节点 cur,使用 for 循环或者 while 循环(迭代),将节点向后移动 N-1 次(将 cur 更新为 cur->next)。
  • 循环结束后,返回 cur 即为需要查找的节点。
// 计算以 head 为头节点的链表的节点数
int getN(ListNode* head) {
	int cnt = 0;
    while (head != nullptr) {
        ++cnt;
        head = head->next;
    }
    return cnt;
}


ListNode* getNthNode(ListNode* head, int N) {
	int cnt = getN(head);
    if (N > cnt) {
        return nullptr;
    }
    
    ListNode* cur = head;
    while (N > 1) {
        cur = cur->next;
    }
    return cur;
}

在此例子中,我们使用函数 getN 计算链表的长度(节点的数量)。我们从链表的头节点开始遍历链表,只要当前的链表不为空(nullptr),就更新 cnt = cnt + 1,并更改 head 为下一个节点。

插入节点

在给定链表中的指定位置插入一个节点,需要考虑以下几个问题:

  • (1)给定的链表是否为空;
  • (2)指定位置是否越界;
  • (3)指定的位置位于链表的头部、中间还是尾部。

如果给定的链表为空,则直接返回新插入的节点;如果指定的位置越界,直接返回给定链表的头节点即可。对于问题(3)中的三种情况,我们逐条进行分析。

在链表中间插入元素

顾名思义,插入节点的位置位于链表的中间位置,在链表第 i 个位置(头节点被称为第一个位置)之前插入值为 val 的链节点,通常:

  • (1)遍历链表找到第 i-1 个节点 preNode
  • (2)新建需要插入的节点 newNode
  • (3)将节点 newNode 的 next 指针连接到(指向)preNode 节点的下一个节点;
  • (4)将节点 preNode 的 next 指针连接到 newNode 节点;
  • (5)最后返回头节点 head

一图胜千言,上述变换过程见下图所示:

链表-插入节点.drawio (1)

在链表头部插入节点

在链表头部插入节点更加简单:

  • 新建需要插入的节点 newNode
  • 将节点 newNode 的 next 指针连接到(指向)head 节点,newNode 作为链表新的头节点。

在链表尾部插入元素

遍历找到链表的最后一个节点,将该节点的 next 指针指向新建的节点 newNode 即可。

总结

下面就是一个往给定链表中指定位置插入一个元素的示例:

ListNode* insertNode(ListNode* head, int pos, int newVal) {
    // 问题(1)
    if (head == nullptr) {	
        return new ListNode(newVal);
    }
    
    // 问题(2)
    int N = getN(head);	// 获取链表长度
    if (pos < 0 || pos > N+1) { // 注意这里的 大于 N+1 是考虑到要在尾部插入节点
        return head;
    }
    
    // 问题(3)
    ListNode* cur = head;
    int i = 1;
    while (i < pos-1) {		// 找到第 pos 个节点后退出循环
		cur = cur->next;
        ++i;
    }
    
    ListNode* newNode = new ListNode(newVal);
    // 在链表头部插入节点
    if (cur == head) { 
     cur->next = head;
        return newNode;
    }
    
    // 在链表中间或尾部插入节点
    newNode->next = cur->next;	// 当在在链表尾部插入节点时,此时 cur->next = nullptr
    cur->next = newNode;
    return head;
}

Note:以上代码中 在链表中间插入元素 是在链表的第 i 个位置之前插入节点,如果是在第 i 个位置之后插入节点,代码会有细微的变换,请读者注意。

删除节点

删除链表中的节点与插入节点操作一样都需要考虑一下三个情况:

  • 待删除的链表为空;
  • 删除的节点是非法的(越界);
  • 删除的节点分别位于链表的头部、中间位置或者尾部。

以下以图解的形式对第三种请款进行说明。前两种情况比较简单,将直接在代码中进行展示。

链表-删除节点.drawio

(1)删除链表中间位置的节点需要先找到被删除节点的上一个节点 `prevNode;

(2)将 prevNode 的 next 指针指向 prev->next->next

(3)最后得到删除的链表。

示例代码

ListNode* removeNode(ListNode* head, int pos) {
	// 链表为空
    if (head == nullptr) {	
        return nullptr;
    }
    
    // 删除的节点是非法的
    int N = getN(head);	// 获取链表长度
    if (pos < 0 || pos > N) {
        return head;
    }
    
    // 情况三
    ListNode* preNode = head;
    int i = 1;
    while (i < pos-1) {		// 找到第 pos 个节点后提出循环
		preNode = preNode->next;
        ++i;
        
    }
    
    // 删除头节点
    if (preNode == head) { 
        return preNode->next;
    }
    
    // 删除链表中间或尾部的节点
    preNode->next = preNode->next->next;
    return head;
}

修改节点

修改主要指的是修改节点的值。比如将第 i 个节点的值修改为指定值 val。思路清晰直接看代码:

void modifyNthVal(ListNode* head, int n, int val) {
	if (head == nullptr) {
		return;
    }
    
    int N = getN(head);	// 获取链表长度
    if (n < 0 || n > N) {	// 越界
		return;
    }
    
    ListNode* cur = head;
    while (--n) {
		cur = cur->next;	
    }
    cur->val = val;
}

参考资料

【书籍】大话数据结构

【文章】一文讲透链表操作,看完你也能轻松写出正确的链表代码

【文章】链表基础知识


写在最后

如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家觉得有些地方需要补充,欢迎评论区交流。

next;
}
cur->val = val;
}


# 参考资料

【书籍】大话数据结构

【文章】[一文讲透链表操作,看完你也能轻松写出正确的链表代码](https://www.cnblogs.com/lonely-wolf/p/15761239.html)

【文章】[链表基础知识](https://algo.itcharge.cn/02.Linked-List/01.Linked-List-Basic/01.Linked-List-Basic/)

---

# 写在最后

如果您发现文章有任何错误或者对文章有任何疑问,欢迎私信博主或者在评论区指出 💬💬💬。

如果大家觉得有些地方需要补充,欢迎评论区交流。

最后,感谢您的阅读,如果有所收获的话可以给我点一个 👍 哦。

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

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

相关文章

Python 操作数据库

十、Python3 操作数据库 1、Python3 操作 MySQL 1、基本介绍 Python3 操作 MySQL 数据库 可以使用的模块是 pymysql 和 MySQLdb。 这个两个模块都是通过自己的 API 执行原生的 SQL 语句实现的。 MySQLdb 是最早出现的一个操作 MySQL 数据库的模块&#xff0c;核心由C语言编…

LangChain-Chatchat 实践

1. 说明 比较了几个AI LLM的集成应用工具(比如Quivr, Dify, one-api), 还是LangChain-Chatchat更符合我的需要: 支持私有化部署不同的LLM知识库支持Api支持开源免费, 容易二开 相关路径: 条项路径LangChain-Chatchat 项目/data0/Projects/Langchain-ChatchatLLM 语言模型保…

【计算机毕业设计】ssm旅游景点管理系统设计

现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统 数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本旅游景点管理系统就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大的数据信息&…

vaspkit 画 Charge-Density Difference

(echo 314;echo $(cat 1))|vaspkit 文件1提前写好使用的CHGCAR路径 SPIN_DW.vasp ../ML2scf/SPIN_DW.vasp ../ML1scf/SPIN_DW.vasp POSite and negative 默认为blue,and 青色 (RGB 30 245 245) 正值&#xff1a;blue 。负值&#xff1a;青色 RGB 30 245 245。 提示&…

LLM Agent智能体综述(万字长文)

前言 &#x1f3c6;&#x1f3c6;&#x1f3c6;在上一篇文章中&#xff0c;我们介绍了如何部署MetaGPT到本地&#xff0c;获取OpenAI API Key并配置其开发环境&#xff0c;并通过一个开发小组的多Agent案例感受了智能体的强大&#xff0c;在本文中&#xff0c;我们将对AI Agent…

C++设计模式|创建型 5.原型模式

1.什么是原型模式&#xff1f; 原型模式⼀种创建型设计模式&#xff0c;该模式的核⼼思想是基于现有的对象创建新的对象&#xff0c;⽽不是从头开始创建。 在原型模式中&#xff0c;通常有⼀个原型对象&#xff0c;它被⽤作创建新对象的模板。新对象通过复制原型对象的属性和状…

Mysql数据存储格式分析

一、整体存储逻辑 1.1 Mysql数据存放位置 不同的存储引擎&#xff0c;对Mysql数据的存储是不同的。新建一个test数据库&#xff0c;里面有t1,t2和test5三张表&#xff0c;以Innodb和Myisam存储引擎为例&#xff1a; Innodb存储引擎&#xff1a; .frm文件&#xff1a;与表相…

【Nginx】如何在 Nginx 中阻止来自特定国家的 IP 地址访问

文章目录 前言一、准备工作二、查看 Nginx 服务器都拥有哪些模块2.1 先查看本地nginx是否有ngx_http_geoip2模块2.2 安装nginx并配置ngx_http_geoip2模块2.2.1下载所需版本的nginx到服务器2.2.2 先安装所需依赖2.2.3 解压文件2.2.4 下载ngx_http_geoip2模块2.2.5 编译安装nginx…

解决webstorm没有vue语法提示;webstorm没有代码提示

解决webstorm没有vue语法提示&#xff1b;webstorm没有代码提示 使用webstorm 2023.x 开发vue项目。发现死活没有vue语法提示&#xff0c;即便是npm install、清理缓存。对比其他vue项目却有语法提示&#xff0c;最后发现依赖库被忽略了&#xff1a; 删除掉node_modules 的忽略…

国外IP代理免费试用技巧

随着互联网的普及&#xff0c;人们越来越依赖于网络来获取信息、进行交流和娱乐。国外IP代理就成了利器之一。在本文中&#xff0c;我们将探讨如何免费使用国外IP代理。 一、了解国外IP代理的原理 国外IP代理&#xff0c;简单来说&#xff0c;就是通过连接到位于国外的代理服务…

linux 环境下 分布式文件搭建fastDFS

1.软件信息 地址&#xff1a;happyfish100 (YuQing) GitHub 1.fastdfs-master.zip 2.fastdfs-nginx-module-master.zip 3.libfastcommon-master.zip 4.libserverframe-master.zip yum install make cmake gcc gcc-c perl 2.安装libfastcommon unzip libfastcommon-mast…

怎么转换音频?看这3款音频转换器

随着数字媒体的发展&#xff0c;音频文件在我们的日常生活中占据了越来越重要的地位。有时候在不同的应用场景里&#xff0c;无论是音乐、语音还是其他类型的音频内容&#xff0c;我们都需要对其进行转换以满足不同的需求。 本文将为您介绍3款常用的音频转换器&#xff0c;帮助…

代码随想录训练营Day31:动态规划3:0-1背包

1.0-1背包基础 有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i]&#xff0c;得到的价值是value[i] 。每件物品只能用一次&#xff0c;求解将哪些物品装入背包里物品价值总和最大。 1.1动态规划五部曲 确定dp数组以及下标的含义&#xff1a;dp[i][j] 表示…

sql操作、发送http请求和邮件发送 全栈开发之路——后端篇(2)

全栈开发一条龙——前端篇 第一篇&#xff1a;框架确定、ide设置与项目创建 第二篇&#xff1a;介绍项目文件意义、组件结构与导入以及setup的引入。 第三篇&#xff1a;setup语法&#xff0c;设置响应式数据。 第四篇&#xff1a;数据绑定、计算属性和watch监视 第五篇 : 组件…

RK3566(泰山派):3.1寸屏幕D310T9362V1SPEC触摸驱动(竖屏)

RK3566&#xff08;泰山派&#xff09;&#xff1a;3.1寸屏幕D310T9362V1SPEC触摸驱动&#xff08;竖屏&#xff09; 文章目录 RK3566&#xff08;泰山派&#xff09;&#xff1a;3.1寸屏幕D310T9362V1SPEC触摸驱动&#xff08;竖屏&#xff09;电路配置i2c1设备树创建驱动编写…

什么是50etf期权?期权的三级交易权限是什么?

今天期权懂带你了解什么是50etf期权&#xff1f;期权的三级交易权限是什么&#xff1f;期权三级交易权限&#xff0c;作为股票期权交易中的最高级别权限&#xff0c;赋予了投资者更广泛、更灵活的交易选择和策略。 什么是50etf期权&#xff1f; ETF期权是指在支付一定额度的权…

如何让机器理解人类语言?Embedding技术详解

如何让机器理解人类语言&#xff1f;Embedding技术详解 文章目录 如何让机器理解人类语言&#xff1f;Embedding技术详解介绍什么是词嵌入&#xff1f;什么是句子嵌入&#xff1f;句子嵌入模型实现句子嵌入的方法值得尝试的句子嵌入模型 句子嵌入库实践Step 1Step 2Step 3 Doc2…

SwiftUI中三大渐变色的介绍

在SwiftUI中&#xff0c;渐变色是一种常用的视觉效果&#xff0c;用于创建平滑过渡的颜色变化。通过使用渐变色&#xff0c;我们可以实现丰富多彩的界面设计&#xff0c;增强用户体验。 1. 渐变色的种类和用途 种类&#xff1a; 线性渐变&#xff08;Linear Gradient&#x…

oracle多条重复数据,取最新的

1、原理讲解-可直接看2 筛选出最新的 SELECT * FROM ( SELECT t.*, ROW_NUMBER() OVER (PARTITION BY LOCALAUTHID ORDER BY LASTUPDATETIME DESC) AS rn FROM USER_LOCALAUTH_STATE t ) t WHERE t.rn 1; 解释&#xff1a; 这个序号是基于[LOCALAUTHID]字段进行分…

计算机vcruntime140.dll找不到如何修复,分享5种靠谱的修复教程

当您在运行某个应用程序或游戏时遇到提示“找不到vcruntime140.dll”&#xff0c;这通常意味着系统中缺少了Visual C Redistributable for Visual Studio 2015或更高版本的一个重要组件。这个错误通常发生在运行某些程序时&#xff0c;系统无法找到所需的动态链接库文件。小编将…