【基本数据结构】链表

news2024/11/19 10:20:54

文章目录

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

前言

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

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

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

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


链表

简介

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

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

链表-链表.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/)

---

# 写在最后

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

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

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

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

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

相关文章

软件工程期末复习(6)需求分析的任务

需求分析 需求分析的任务 “建造一个软件系统的最困难的部分是决定要建造什么……没有别的工作在做错时会如此影响最终系统&#xff0c;没有别的工作比以后矫正更困难。” —— Fred Brooks 需求难以建立的原因&#x…

CAN模块开发问题概述

问题一 问题描述 工作环境&#xff1a;ECU外接canoe 操作&#xff1a;使用CANoe模拟发送NM报文&#xff0c;然后停发或者断开CANoe 现象&#xff1a;程序跑死&#xff0c;调用call stack查看压栈情况如下图所示 定位代码如下图所示。可见是由于CAN模块在设置Controller状态时…

tomcat--目录结构和文件组成

目录结构 目录说明bin服务启动&#xff0c;停止等相关程序和文件conf配置文件lib库目录logs日志记录webapps应用程序&#xff0c;应用部署目录workjsp编译后的结果文件&#xff0c;建议提前预热访问 /usr/local/apache-tomcat-8.5.100/work/Catalina/localhost/ROOT/org/apac…

[笔试训练](二十二)064:添加字符065:数组变换066:装箱问题

目录 064:添加字符 065:数组变换 066:装箱问题 064:添加字符 添加字符_牛客笔试题_牛客网 (nowcoder.com) 题目&#xff1a; 题解&#xff1a; 枚举所有A&#xff0c;B字符串可能的对应位置&#xff0c;得出对应位置不同字符数量的最小情况 两字符串的字符数量差n-m&…

Hadoop 3.4.0+HBase2.5.8+ZooKeeper3.8.4+Hive+Sqoop 分布式高可用集群部署安装 大数据系列二

创建服务器,参考 虚拟机创建服务器 节点名字节点IP系统版本master11192.168.50.11centos 8.5slave12192.168.50.12centos 8.5slave13192.168.50.13centos 8.5 1 下载组件 Hadoop:官网地址 Hbase:官网地址 ZooKeeper:官网下载 Hive:官网下载 Sqoop:官网下载 为方便同学…

【已解决】力扣打不开

表现&#xff1a; 1.访问国内其他网站都没有问题 2.访问github也能成功 3.wifi没有问题 4.连接同网络的其他主机能打开 唯独力扣打不开&#xff0c;可能是DNS解析错误 》自己网络配置问题 解决办法【亲测可行】 找可用的hosts 打开站长之家&#xff0c;进行DNS查询&#xff…

FreeRTOS事件标志组

目录 一、事件标志组的概念 1、事件标志位 2、事件标志组 二、事件标志组相关API 1、创建事件标志组 2、设置事件标志位 3、清除事件标志位 4、等待事件标志位 三、事件标志组实操 1、实验需求 2、CubeMX配置 3、代码实现 一、事件标志组的概念 1、事件标志位 表…

128.Mit6.S081-实验1-Xv6 and Unix utilities(下)

今天我们继续实验一接下来的内容。 一、pingpong(难度easy) 1.需求 编写一个程序&#xff0c;使用 UNIX 系统调用通过一对管道(每个方向一个管道)在两个进程之间 "ping-pong" 传递一个字节。父进程应该向子进程发送一个字节; 子进程应该打印<pid>: received p…

短视频语音合成:成都鼎茂宏升文化传媒公司

短视频语音合成&#xff1a;技术革新与创意融合的新篇章 随着科技的飞速发展&#xff0c;短视频已经成为人们生活中不可或缺的一部分。在这个快速变化的时代&#xff0c;短视频语音合成技术正逐渐崭露头角&#xff0c;以其独特的魅力和广泛的应用前景&#xff0c;吸引了众多创…

R语言:ROC分析

> install.packages("pROC") > library(pROC) > inputFile"结果.txt" > rtread.table(inputFile, headerT, sep"\t", check.namesF, row.names1) > head(rt) con treat TCGA-E2-A1L7-11A-con…

6. 第K小的和-二分

6.第K小的和 - 蓝桥云课 (lanqiao.cn) #include <bits/stdc.h> #define int long long #define endl \n using namespace std; int n,m,k,an[100005],bm[100005]; int check(int x){int res0;//序列C中<x的数的个数for(int i0;i<n;i){//遍历数组A&#xff0c;对于每…

怎么得到所有大写字母/小写字母组成的字符串

有时候&#xff0c;可能需要获取a~z、A~Z组成的26个字母的字符串&#xff0c;这篇文章介绍一种简单的方法。 只需要几句简单到不能再简单的代码&#xff01;你不会还在傻傻地一个个字母敲吧~ /*** author heyunlin* version 1.0*/ public class Example {/*** 小写字母*/priv…

Vscode编辑器 js 输入log自动补全

最近换了新电脑&#xff0c;新下载了Vscode&#xff0c;记录一下设置项。 Vscode 版本 想要的效果 js文件中输入log&#xff08;点击tab键&#xff09;&#xff0c;自动补全为 console.log() Vscode 文件》首选项》设置 搜索&#xff1a;snippets Emmet: Show Suggestions…

HTML常用标签-表单标签

表单标签 1 表单标签2 表单项标签2.1 单行文本框2.2 密码框2.3 单选框2.4 复选框2.5 下拉框2.6 按钮2.7 隐藏域2.8 多行文本框2.9 文件标签 1 表单标签 表单标签,可以实现让用户在界面上输入各种信息并提交的一种标签. 是向服务端发送数据主要的方式之一 form标签,表单标签,其内…

3ds Max与Maya不同之处?两者哪个更适合云渲染?

3ds Max 和 Maya 都是知名的3D软件&#xff0c;各有其特色。3ds Max 以直观的建模和丰富的插件生态闻名&#xff1b;Maya 则在动画和角色创作方面更为出色。两者都支持云渲染技术&#xff0c;能帮助用户在云端高效完成项目。 一、3ds Max和Maya之间的主要区别&#xff1a; 3ds…

web入门练手案例(二)

下面是一下web入门案例和实现的代码&#xff0c;带有部分注释&#xff0c;倘若代码中有任何问题或疑问&#xff0c;欢迎留言交流~ 数字变色Logo 案例描述 “Logo”是“商标”的英文说法&#xff0c;是企业最基本的视觉识别形象&#xff0c;通过商标的推广可以让消费者了解企…

两小时看完花书(深度学习入门篇)

1.深度学习花书前言 机器学习早期的时候十分依赖于已有的知识库和人为的逻辑规则&#xff0c;需要人们花大量的时间去制定合理的逻辑判定&#xff0c;可以说是有多少人工&#xff0c;就有多少智能。后来逐渐发展出一些简单的机器学习方法例如logistic regression、naive bayes等…

产品品牌CRUD

文章目录 1.renren-generator生成CRUD1.数据库表设计1.数据表设计2.分析 2.代码生成器生成crud1.查看generator.properties&#xff08;不需要修改&#xff09;2.修改application.yml 连接的数据库修改为云数据库3.启动renren-generator模块4.浏览器访问 http://localhost:81/5…

ip addr 或 ip address 是 Linux 系统中的一个命令,用于显示或修改网络接口的地址信息。

ip addr 或 ip address 是 Linux 系统中的一个命令&#xff0c;用于显示或修改网络接口的地址信息。这个命令是 iproute2 软件包的一部分&#xff0c;通常在现代 Linux 发行版中都是预装的。 当你运行 ip addr 或 ip address 命令时&#xff0c;你会看到系统上所有网络接口的地…

ssh错误 ssh_exchange_identification: Connection closed by remote host

一 背景 今天使用终端ssh链接服务器报错&#xff0c;昨天还好的&#xff0c;今天就报错&#xff0c;原以为是服务器ip变了&#xff0c;但是同事使用原来ip可以链接&#xff0c;本人怀疑ssh链接人员是不是超出限制&#xff0c;于是沿着这思路解决&#xff0c;果然成功了。 二 …